|
| 1 | +//! Manage cargo lock files for minimal and recent dependency versions. |
| 2 | +
|
| 3 | +use crate::environment::quiet_println; |
| 4 | +use crate::quiet_cmd; |
| 5 | +use crate::toolchain::{check_toolchain, Toolchain}; |
| 6 | +use clap::ValueEnum; |
| 7 | +use std::fs; |
| 8 | +use xshell::Shell; |
| 9 | + |
| 10 | +/// The standard Cargo lockfile name. |
| 11 | +const CARGO_LOCK: &str = "Cargo.lock"; |
| 12 | + |
| 13 | +/// Represents the different types of managed lock files. |
| 14 | +#[derive(Debug, Clone, Copy, ValueEnum)] |
| 15 | +pub enum LockFile { |
| 16 | + /// Uses minimal versions that satisfy dependency constraints. |
| 17 | + Minimal, |
| 18 | + /// Uses recent/updated versions of dependencies. |
| 19 | + Recent, |
| 20 | +} |
| 21 | + |
| 22 | +impl Default for LockFile { |
| 23 | + fn default() -> Self { |
| 24 | + LockFile::Recent |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +impl LockFile { |
| 29 | + /// Get the filename for this lock file type. |
| 30 | + pub fn filename(&self) -> &'static str { |
| 31 | + match self { |
| 32 | + LockFile::Minimal => "Cargo-minimal.lock", |
| 33 | + LockFile::Recent => "Cargo-recent.lock", |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +/// Update Cargo-minimal.lock and Cargo-recent.lock files. |
| 39 | +/// |
| 40 | +/// * `Cargo-minimal.lock` - Uses minimal versions that satisfy dependency constraints. |
| 41 | +/// * `Cargo-recent.lock` - Uses recent/updated versions of dependencies. |
| 42 | +/// |
| 43 | +/// The minimal versions strategy uses a combination of `-Z direct-minimal-versions` |
| 44 | +/// and `-Z minimal-versions` to ensure two rules. |
| 45 | +/// |
| 46 | +/// 1. Direct dependency versions in manifests are accurate (not bumped by transitive deps). |
| 47 | +/// 2. The entire dependency tree uses minimal versions that still satisfy constraints. |
| 48 | +/// |
| 49 | +/// This helps catch cases where you've specified a minimum version that's too high, |
| 50 | +/// or where your code relies on features from newer versions than declared. |
| 51 | +pub fn run(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> { |
| 52 | + check_toolchain(sh, Toolchain::Nightly)?; |
| 53 | + |
| 54 | + let repo_dir = sh.current_dir(); |
| 55 | + quiet_println(&format!("Updating lock files in: {}", repo_dir.display())); |
| 56 | + |
| 57 | + // The `direct-minimal-versions` and `minimal-versions` dependency resolution strategy |
| 58 | + // flags each have a little quirk. `direct-minimal-versions` allows transitive versions |
| 59 | + // to upgrade, so we are not testing against the actual minimum tree. `minimal-versions` |
| 60 | + // allows the direct dependency versions to resolve upward due to transitive requirements, |
| 61 | + // so we are not testing the manifest's versions. Combo'd together though, we can get the |
| 62 | + // best of both worlds to ensure the actual minimum dependencies listed in the crate |
| 63 | + // manifests build. |
| 64 | + |
| 65 | + // Check that all explicit direct dependency versions are not lying, |
| 66 | + // as in, they are not being bumped up by transitive dependency constraints. |
| 67 | + quiet_println("Checking direct minimal versions..."); |
| 68 | + remove_lock_file(sh)?; |
| 69 | + quiet_cmd!(sh, "cargo check --all-features -Z direct-minimal-versions").run()?; |
| 70 | + |
| 71 | + // Now that our own direct dependency versions can be trusted, check |
| 72 | + // against the lowest versions of the dependency tree which still |
| 73 | + // satisfy constraints. Use this as the minimal version lock file. |
| 74 | + quiet_println("Generating Cargo-minimal.lock..."); |
| 75 | + remove_lock_file(sh)?; |
| 76 | + quiet_cmd!(sh, "cargo check --all-features -Z minimal-versions").run()?; |
| 77 | + copy_lock_file(sh, LockFile::Minimal)?; |
| 78 | + |
| 79 | + // Conservatively bump of recent dependencies. |
| 80 | + quiet_println("Updating Cargo-recent.lock..."); |
| 81 | + restore_lock_file(sh, LockFile::Recent)?; |
| 82 | + quiet_cmd!(sh, "cargo check --all-features").run()?; |
| 83 | + copy_lock_file(sh, LockFile::Recent)?; |
| 84 | + |
| 85 | + quiet_println("Lock files updated successfully"); |
| 86 | + |
| 87 | + Ok(()) |
| 88 | +} |
| 89 | + |
| 90 | +/// Remove Cargo.lock file if it exists. |
| 91 | +fn remove_lock_file(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> { |
| 92 | + let lock_path = sh.current_dir().join(CARGO_LOCK); |
| 93 | + if lock_path.exists() { |
| 94 | + fs::remove_file(&lock_path)?; |
| 95 | + } |
| 96 | + Ok(()) |
| 97 | +} |
| 98 | + |
| 99 | +/// Copy Cargo.lock to a specific lock file. |
| 100 | +fn copy_lock_file(sh: &Shell, target: LockFile) -> Result<(), Box<dyn std::error::Error>> { |
| 101 | + let source = sh.current_dir().join(CARGO_LOCK); |
| 102 | + let dest = sh.current_dir().join(target.filename()); |
| 103 | + fs::copy(&source, &dest)?; |
| 104 | + Ok(()) |
| 105 | +} |
| 106 | + |
| 107 | +/// Restore a specific lock file to Cargo.lock. |
| 108 | +pub fn restore_lock_file(sh: &Shell, source: LockFile) -> Result<(), Box<dyn std::error::Error>> { |
| 109 | + let src_path = sh.current_dir().join(source.filename()); |
| 110 | + let dest_path = sh.current_dir().join(CARGO_LOCK); |
| 111 | + fs::copy(&src_path, &dest_path)?; |
| 112 | + Ok(()) |
| 113 | +} |
0 commit comments