Skip to content

Commit ff78240

Browse files
committed
tasks: add lock task
1 parent 5f2bc94 commit ff78240

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

tasks/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,43 @@ features_with_no_std = ["serde", "rand"]
6161

6262
* `RBMT_LOG_LEVEL=quiet` - Suppress verbose output and reduce cargo noise.
6363

64+
## Lock Files
65+
66+
To ensure your crate works with the full range of declared dependency versions, `rbmt` requires two lock files in your repository.
67+
68+
* `Cargo-minimal.lock` - Minimum versions that satisfy your dependency constraints.
69+
* `Cargo-recent.lock` - Recent/updated versions of dependencies.
70+
71+
The `rbmt lock` command generates and maintains these files for you. You can then use `--lock-file` with any command to test against either version set.
72+
73+
### Usage
74+
75+
**Generate/update lock files**
76+
77+
```bash
78+
rbmt lock
79+
```
80+
81+
1. Verify that direct dependency versions aren't being bumped by transitive dependencies.
82+
2. Generate `Cargo-minimal.lock` with minimal versions across the entire dependency tree.
83+
3. Update `Cargo-recent.lock` with conservatively updated dependencies.
84+
85+
**Use a specific lock file**
86+
87+
```bash
88+
# Test with minimal versions.
89+
rbmt --lock-file minimal test stable
90+
91+
# Test with recent versions.
92+
rbmt --lock-file recent test stable
93+
94+
# Works with any command.
95+
rbmt --lock-file minimal lint
96+
rbmt --lock-file minimal docs
97+
```
98+
99+
When you specify `--lock-file`, the tool copies that lock file to `Cargo.lock` before running the command. This allows you to test your code against different dependency version constraints.
100+
64101
## Workspace Integration
65102

66103
`rbmt` can simply be installed globally, or as a dev-dependency for more granular control of dependency versions.

tasks/src/lock.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
}

tasks/src/main.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod bench;
22
mod docs;
33
mod environment;
44
mod lint;
5+
mod lock;
56
mod test;
67
mod toolchain;
78

@@ -10,12 +11,17 @@ use std::process;
1011
use xshell::Shell;
1112

1213
use environment::{change_to_repo_root, configure_log_level};
14+
use lock::LockFile;
1315
use toolchain::Toolchain;
1416

1517
#[derive(Parser)]
1618
#[command(name = "rbmt")]
1719
#[command(about = "Rust Bitcoin Maintainer Tools", long_about = None)]
1820
struct Cli {
21+
/// Lock file to use for dependencies (defaults to recent).
22+
#[arg(long, global = true, value_enum)]
23+
lock_file: Option<LockFile>,
24+
1925
#[command(subcommand)]
2026
command: Commands,
2127
}
@@ -36,6 +42,8 @@ enum Commands {
3642
#[arg(value_enum)]
3743
toolchain: Toolchain,
3844
},
45+
/// Update Cargo-minimal.lock and Cargo-recent.lock files.
46+
Lock,
3947
}
4048

4149
fn main() {
@@ -44,6 +52,16 @@ fn main() {
4452
configure_log_level(&sh);
4553
change_to_repo_root(&sh);
4654

55+
// Restore the specified lock file before running any command (except Lock itself).
56+
if let Some(lock_file) = cli.lock_file {
57+
if !matches!(cli.command, Commands::Lock) {
58+
if let Err(e) = lock::restore_lock_file(&sh, lock_file) {
59+
eprintln!("Error restoring lock file: {}", e);
60+
process::exit(1);
61+
}
62+
}
63+
}
64+
4765
match cli.command {
4866
Commands::Lint => {
4967
if let Err(e) = lint::run(&sh) {
@@ -75,5 +93,11 @@ fn main() {
7593
process::exit(1);
7694
}
7795
}
96+
Commands::Lock => {
97+
if let Err(e) = lock::run(&sh) {
98+
eprintln!("Error updating lock files: {}", e);
99+
process::exit(1);
100+
}
101+
}
78102
}
79103
}

0 commit comments

Comments
 (0)