Skip to content

Commit 95f90df

Browse files
committed
Merge #26: Re-write in xshell
5533a45 tasks: add package filter so can run on a subset of crates (Nick Johnson) ae24339 tasks: add lock task (Nick Johnson) f5b2ffd tasks: add tests task (Nick Johnson) 99ea567 tasks: add docs and bench commands (Nick Johnson) 7e16550 tasks: add a toolchain check (Nick Johnson) 32ee8d6 tasks: add the lint task (Nick Johnson) fe25b2f Make repo a workspace and add a tasks crate (Nick Johnson) Pull request description: Re-write the ci/run_task.sh script in xshell, attempting to match as much functionality as possible, but some user interfaces are changed for easier maintenance. The largest change is dropping the config scripts, `contrib/crates.sh`, `contrib/whitelist_deps.sh`, and `contrib/test_vars.sh`, for `contrib/rbmt.toml`. The first patch makes the repo a workspace and initializes the new crate for the xshell based maintainer tools. The following patches add commands to match existing functionality. They also add a new `lock` command for lock file management as well as a `--package` filter (matching cargo's interface) to run commands on a subset of a workspace's crates. Closes #25, #6 ACKs for top commit: tcharding: ACK 5533a45 Tree-SHA512: 2281ca2aef4011708c3f1ccb5ca556df7d7ee9d66a81301242f12f6c3da18cde24bd0d49dac1e5806664744db162f68f9bb41f2f7694f024423a5771d420c774
2 parents e7ad5b2 + 5533a45 commit 95f90df

File tree

15 files changed

+2841
-1
lines changed

15 files changed

+2841
-1
lines changed

Cargo.lock

Lines changed: 1744 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
members = [
3+
"releases",
4+
"tasks",
5+
]
6+
resolver = "2"

releases/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ crates_io_api = { version = "0.11.0", default-features = false, features = ["rus
1818
semver = { version = "1.0.22", default-features = false, features = ["std"] }
1919
serde_json = { version = "1.0.116", default-features = false, features = [] }
2020
serde = { version = "1.0.198", features = ["derive"] }
21-
tokio = { version = "1.37.0", features = ["rt", "macros"] }
21+
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
2222
toml = { version = "0.8.12", default-features = false, features = ["display", "parse"] }

tasks/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## [0.1.0] -
4+
5+
* Initial release of `rust-bitcoin-maintainer-tools` (executable: `rbmt`) matching functionality of shell scripts.

tasks/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "rust-bitcoin-maintainer-tools"
3+
version = "0.1.0"
4+
authors = ["Nick Johnson <[email protected]>"]
5+
license = "CC0-1.0"
6+
edition = "2021"
7+
rust-version = "1.74.0"
8+
9+
[[bin]]
10+
name = "rbmt"
11+
path = "src/main.rs"
12+
13+
[dependencies]
14+
xshell = "0.2"
15+
clap = { version = "4", features = ["derive"] }
16+
serde = { version = "1.0", features = ["derive"] }
17+
serde_json = "1.0"
18+
toml = "0.8"

tasks/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Maintainer Tools
2+
3+
Maintainer tools for Rust-based projects in the Bitcoin domain. Built with [xshell](https://github.com/matklad/xshell).
4+
5+
## Configuration
6+
7+
Configuration for `rbmt` is stored in `contrib/rbmt.toml`. The file can live at both the workspace root (e.g. `$ROOT/contrib/rbmt.toml`) as well as per-crate (e.g. `$ROOT/$CRATE/contrib/rbmt.toml`) within a repository.
8+
9+
### Lint
10+
11+
The `lint` command detects duplicate dependencies, but some may be unavoidable (e.g., during dependency updates where transitive dependencies haven't caught up). Configure the `[lint]` section to whitelist specific duplicates for a workspace (or a crate if only one crate in a repository).
12+
13+
```toml
14+
[lint]
15+
allowed_duplicates = [
16+
"syn",
17+
"bitcoin_hashes",
18+
]
19+
```
20+
21+
### Test
22+
23+
The `test` command can be configured to run feature matrix testing for your crate. Configure with the `contrib/rbmt.toml` file at the crate level.
24+
25+
```toml
26+
[test]
27+
# Examples to run with specific features enabled.
28+
# Format: "example_name:feature1 feature2"
29+
examples = [
30+
"example1:serde",
31+
"example2:serde rand",
32+
]
33+
34+
# Features to test with the conventional `std` feature enabled.
35+
# Tests each feature alone with std, all pairs, and all together.
36+
# Example: ["serde", "rand"] tests: std+serde, std+rand, std+serde+rand
37+
features_with_std = ["serde", "rand"]
38+
39+
# Features to test without the `std` feature.
40+
# Tests each feature alone, all pairs, and all together.
41+
# Example: ["serde", "rand"] tests: serde, rand, serde+rand
42+
features_without_std = ["serde", "rand"]
43+
44+
# Exact feature combinations to test.
45+
# Use for crates that don't follow conventional `std` patterns.
46+
# Each inner array is tested as-is with no automatic combinations.
47+
# Example: [["serde", "rand"], ["rand"]] tests exactly those two combinations
48+
exact_features = [
49+
["serde", "rand"],
50+
["rand"],
51+
]
52+
53+
# Features to test with an explicit `no-std` feature enabled.
54+
# Only use if your crate has a `no-std` feature (rust-miniscript pattern).
55+
# Tests each feature with no-std, all pairs, and all together.
56+
# Example: ["serde", "rand"] tests: no-std+serde, no-std+rand, no-std+serde+rand
57+
features_with_no_std = ["serde", "rand"]
58+
```
59+
60+
### Environment Variables
61+
62+
* `RBMT_LOG_LEVEL=quiet` - Suppress verbose output and reduce cargo noise.
63+
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+
101+
## Workspace Integration
102+
103+
`rbmt` can simply be installed globally, or as a dev-dependency for more granular control of dependency versions.
104+
105+
### 1. Install globally
106+
107+
Install the tool globally on your system with `cargo install`.
108+
109+
```bash
110+
cargo install [email protected]
111+
```
112+
113+
Then run from anywhere in your repository.
114+
115+
```bash
116+
rbmt lint
117+
```
118+
119+
### 2. Add as a dev-dependency
120+
121+
Add as a dev-dependency to a workspace member. This pins the tool version in your lockfile for reproducible builds.
122+
123+
```toml
124+
[dev-dependencies]
125+
rust-bitcoin-maintainer-tools = "0.1.0"
126+
```
127+
128+
Then run via cargo.
129+
130+
```bash
131+
cargo run --bin rbmt -- lint
132+
```
133+
134+
It might be worth wrapping in an [xtask](https://github.com/matklad/cargo-xtask) package for a clean interface.

tasks/justfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# List available recipes.
2+
_default:
3+
@just --list
4+
5+
# Run tests.
6+
test:
7+
cargo test
8+
9+
# Install rbmt from the local path.
10+
install:
11+
cargo install --path .

tasks/src/bench.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! Benchmark testing tasks.
2+
3+
use crate::environment::{get_crate_dirs, quiet_println};
4+
use crate::quiet_cmd;
5+
use crate::toolchain::{check_toolchain, Toolchain};
6+
use xshell::Shell;
7+
8+
/// Run benchmark tests for all crates in the workspace.
9+
pub fn run(sh: &Shell, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
10+
check_toolchain(sh, Toolchain::Nightly)?;
11+
12+
let crate_dirs = get_crate_dirs(sh, packages)?;
13+
14+
quiet_println(&format!(
15+
"Running bench tests for {} crates",
16+
crate_dirs.len()
17+
));
18+
19+
for crate_dir in &crate_dirs {
20+
quiet_println(&format!("Running bench tests in: {}", crate_dir));
21+
22+
// Use pushd pattern to change and restore directory.
23+
let _dir = sh.push_dir(crate_dir);
24+
25+
quiet_cmd!(sh, "cargo bench")
26+
.env("RUSTFLAGS", "--cfg=bench")
27+
.run()?;
28+
}
29+
30+
Ok(())
31+
}

tasks/src/docs.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! Documentation building tasks.
2+
3+
use crate::quiet_cmd;
4+
use crate::toolchain::{check_toolchain, Toolchain};
5+
use xshell::Shell;
6+
7+
/// Build documentation for end users with the stable toolchain.
8+
///
9+
/// This verifies that `cargo doc` works correctly for users with stable Rust.
10+
/// Uses basic rustdoc warnings to catch common documentation issues.
11+
pub fn run(sh: &Shell, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
12+
check_toolchain(sh, Toolchain::Stable)?;
13+
14+
let mut cmd = quiet_cmd!(sh, "cargo doc --all-features");
15+
16+
// Add package filters if specified.
17+
for package in packages {
18+
cmd = cmd.args(&["-p", package]);
19+
}
20+
21+
cmd.env("RUSTDOCFLAGS", "-D warnings").run()?;
22+
23+
Ok(())
24+
}
25+
26+
/// Build documentation for docs.rs with the nightly toolchain.
27+
///
28+
/// This emulates the docs.rs build environment by using the nightly toolchain
29+
/// with `--cfg docsrs` enabled. This catches docs.rs-specific issues.
30+
pub fn run_docsrs(sh: &Shell, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
31+
check_toolchain(sh, Toolchain::Nightly)?;
32+
33+
let mut cmd = quiet_cmd!(sh, "cargo doc --all-features");
34+
35+
// Add package filters if specified.
36+
for package in packages {
37+
cmd = cmd.args(&["-p", package]);
38+
}
39+
40+
cmd.env(
41+
"RUSTDOCFLAGS",
42+
"--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links",
43+
)
44+
.run()?;
45+
46+
Ok(())
47+
}

tasks/src/environment.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::env;
2+
use xshell::{cmd, Shell};
3+
4+
/// Environment variable to control output verbosity.
5+
/// Set to "quiet" to suppress informational messages and reduce cargo output.
6+
/// Any other value (or unset) defaults to verbose mode.
7+
const LOG_LEVEL_ENV_VAR: &str = "RBMT_LOG_LEVEL";
8+
9+
/// Path to the RBMT configuration file relative to workspace/crate root.
10+
pub const CONFIG_FILE_PATH: &str = "contrib/rbmt.toml";
11+
12+
/// Check if we're in quiet mode via environment variable.
13+
pub fn is_quiet_mode() -> bool {
14+
env::var(LOG_LEVEL_ENV_VAR).is_ok_and(|v| v == "quiet")
15+
}
16+
17+
/// Helper macro to create commands that respect quiet mode.
18+
#[macro_export]
19+
macro_rules! quiet_cmd {
20+
($sh:expr, $($arg:tt)*) => {{
21+
let mut cmd = xshell::cmd!($sh, $($arg)*);
22+
if $crate::environment::is_quiet_mode() {
23+
cmd = cmd.quiet();
24+
}
25+
cmd
26+
}};
27+
}
28+
29+
/// Print a message unless in quiet mode.
30+
pub fn quiet_println(msg: &str) {
31+
if !is_quiet_mode() {
32+
println!("{}", msg);
33+
}
34+
}
35+
36+
/// Configure shell log level and output verbosity.
37+
/// Sets cargo output verbosity based on LOG_LEVEL_ENV_VAR.
38+
pub fn configure_log_level(sh: &Shell) {
39+
if is_quiet_mode() {
40+
sh.set_var("CARGO_TERM_VERBOSE", "false");
41+
sh.set_var("CARGO_TERM_QUIET", "true");
42+
} else {
43+
sh.set_var("CARGO_TERM_VERBOSE", "true");
44+
sh.set_var("CARGO_TERM_QUIET", "false");
45+
}
46+
}
47+
48+
/// Change to the repository root directory.
49+
///
50+
/// # Panics
51+
///
52+
/// Panics if not in a git repository or git command fails.
53+
pub fn change_to_repo_root(sh: &Shell) {
54+
let repo_dir = cmd!(sh, "git rev-parse --show-toplevel")
55+
.read()
56+
.expect("Failed to get repository root, ensure you're in a git repository");
57+
sh.change_dir(&repo_dir);
58+
}
59+
60+
/// Get list of crate directories in the workspace using cargo metadata.
61+
/// Returns fully qualified paths to support various workspace layouts including nested crates.
62+
///
63+
/// # Arguments
64+
///
65+
/// * `packages` - Optional filter for specific package names. If empty, returns all packages.
66+
pub fn get_crate_dirs(sh: &Shell, packages: &[String]) -> Result<Vec<String>, Box<dyn std::error::Error>> {
67+
let metadata = cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?;
68+
let json: serde_json::Value = serde_json::from_str(&metadata)?;
69+
70+
let crate_dirs: Vec<String> = json["packages"]
71+
.as_array()
72+
.ok_or("Missing 'packages' field in cargo metadata")?
73+
.iter()
74+
.filter_map(|package| {
75+
let manifest_path = package["manifest_path"].as_str()?;
76+
// Extract directory path from the manifest path,
77+
// e.g., "/path/to/repo/releases/Cargo.toml" -> "/path/to/repo/releases".
78+
let dir_path = manifest_path.trim_end_matches("/Cargo.toml");
79+
80+
// Filter by package name if specified.
81+
if !packages.is_empty() {
82+
let package_name = package["name"].as_str()?;
83+
if !packages.contains(&package_name.to_string()) {
84+
return None;
85+
}
86+
}
87+
88+
Some(dir_path.to_string())
89+
})
90+
.collect();
91+
92+
Ok(crate_dirs)
93+
}

0 commit comments

Comments
 (0)