Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 98 additions & 4 deletions .config/jp/tools/src/cargo.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::time::{Duration, Instant};

use camino::{Utf8Path, Utf8PathBuf};
use jp_tool::{AccessPolicy, Capability, Outcome};
use serde_json::Value;

use crate::{
Context, Tool,
fs::utils::{authorize, resolve_workspace_path},
util::{ToolResult, error, unknown_tool},
util::{OneOrMany, ToolResult, error, unknown_tool},
};

mod check;
Expand Down Expand Up @@ -65,34 +67,126 @@ pub async fn run(ctx: Context, t: Tool) -> ToolResult {
Err(message) => return error(message),
};

// Flags to append to `RUSTFLAGS`, set via `options.rustflags`. Malformed
// values are refused for the same reason as `root`: silently ignoring them
// would compile with flags the caller believes are in effect.
let rustflags = match t.options.get("rustflags") {
None | Some(Value::Null) => rustflags(&[]),
Some(value) => match serde_json::from_value::<OneOrMany<String>>(value.clone()) {
Ok(flags) => rustflags(&flags.into_vec()),
Err(_) => {
return error(format!(
"The `rustflags` tool option must be a string or an array of strings, got \
`{value}`."
));
}
},
};

let started = Instant::now();
let outcome = match subcommand {
"check" => cargo_check(&root, t.opt("package")?, checksum_freshness).await,
"check" => cargo_check(&root, &rustflags, t.opt("package")?, checksum_freshness).await,
"expand" => {
cargo_expand(&root, t.req("item")?, t.opt("package")?, checksum_freshness).await
cargo_expand(
&root,
&rustflags,
t.req("item")?,
t.opt("package")?,
checksum_freshness,
)
.await
}
"test" => {
cargo_test(
&root,
&rustflags,
t.opt("package")?,
t.opt("testname")?,
t.opt("backtrace")?,
checksum_freshness,
)
.await
}
"format" => cargo_format(&root, t.opt("package")?).await,
"format" => cargo_format(&root, &rustflags, t.opt("package")?).await,
"install_tools" => cargo_install_tools(&root).await,
"update" => cargo_update(&root, t.req("packages")?).await,
_ => return unknown_tool(t),
};

let outcome = note_duration(outcome, started.elapsed());

if root == ctx.root {
return outcome;
}

note_root(outcome, &root)
}

/// Append how long the cargo invocation took.
///
/// Wall-clock duration is the whole signal when tuning compile times, and a
/// caller reading only the tool's text cannot otherwise tell a warm cache from
/// a full rebuild — the two are indistinguishable when both end in "Check
/// succeeded".
/// Failures are timed too: a three-second failure and a three-minute one call
/// for different responses.
fn note_duration(outcome: ToolResult, elapsed: Duration) -> ToolResult {
let note = format!("(took {})", format_duration(elapsed));

match outcome {
Ok(Outcome::Success { content }) => Ok(Outcome::Success {
content: format!("{content}\n\n{note}"),
}),
Ok(Outcome::Error {
message,
trace,
transient,
}) => Ok(Outcome::Error {
message: format!("{message}\n\n{note}"),
trace,
transient,
}),
Err(error) => Err(format!("{error}\n\n{note}").into()),
other => other,
}
}

/// Render a duration at a precision that matches how it will be read.
///
/// Sub-minute builds are compared against each other, where a tenth of a second
/// distinguishes a warm cache from a small rebuild; past a minute nobody cares
/// about the fraction.
fn format_duration(elapsed: Duration) -> String {
let seconds = elapsed.as_secs();

if seconds >= 60 {
return format!("{}m {}s", seconds / 60, seconds % 60);
}

format!("{:.1}s", elapsed.as_secs_f64())
}

/// Warnings are reported, not fatal: these tools surface diagnostics rather
/// than failing on them, and CI runs its own `-D warnings` pass.
const BASE_RUSTFLAGS: &str = "-W warnings";

/// Build the `RUSTFLAGS` value, appending any configured flags to the base.
///
/// Setting `RUSTFLAGS` at all overrides `rustflags` from `.cargo/config.toml`
/// wholesale, so a workspace that relies on those (a linker choice, extra `-Z`
/// flags) has to restate them through `options.rustflags`.
///
/// Every compiling cargo tool sets the variable, so they agree on the flag set
/// and a shared target directory stays warm when alternating between them.
/// Configured flags come last, so they win over the base.
fn rustflags(extra: &[String]) -> String {
if extra.is_empty() {
return BASE_RUSTFLAGS.to_owned();
}

format!("{BASE_RUSTFLAGS} {}", extra.join(" "))
}

/// Capabilities a cargo subcommand needs on the directory it runs in.
///
/// These gate whether cargo is spawned at all; they cannot bound what it does
Expand Down
6 changes: 4 additions & 2 deletions .config/jp/tools/src/cargo/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ use crate::util::{

pub(crate) async fn cargo_check(
root: &Utf8Path,
rustflags: &str,
package: Option<String>,
checksum_freshness: bool,
) -> ToolResult {
cargo_check_impl(
root,
rustflags,
package.as_deref(),
checksum_freshness,
&DuctProcessRunner,
Expand All @@ -24,14 +26,14 @@ pub(crate) async fn cargo_check(

fn cargo_check_impl<R: ProcessRunner>(
root: &Utf8Path,
rustflags: &str,
package: Option<&str>,
checksum_freshness: bool,
runner: &R,
) -> ToolResult {
let clippy_scope = package.map_or("--workspace".to_owned(), |v| format!("--package={v}"));

// Prevent warnings from being treated as errors, e.g. on CI.
let mut env = vec![("RUSTFLAGS", "-W warnings")];
let mut env = vec![("RUSTFLAGS", rustflags)];
if checksum_freshness {
// Use content checksums instead of file mtimes for cargo's freshness
// checks, so that sibling checkouts (git worktrees) sharing a target
Expand Down
21 changes: 11 additions & 10 deletions .config/jp/tools/src/cargo/check_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fn test_cargo_check_with_warnings() {
.expect("comfort")
.returns_success("");

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();

assert_eq!(result.into_content().unwrap(), indoc::indoc! {r#"
```
Expand Down Expand Up @@ -80,7 +80,7 @@ fn test_cargo_check_no_warnings() {
.expect("comfort")
.returns_success("");

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();

assert_eq!(
result.into_content().unwrap(),
Expand All @@ -103,7 +103,7 @@ fn clean_clippy_with_comfort_drift_appends_note() {
status: ExitCode::from_code(1),
});

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();

// The header is clippy-scoped, not a blanket "Check succeeded", so it does
// not contradict the drift note below it.
Expand Down Expand Up @@ -134,7 +134,7 @@ fn clippy_warnings_and_comfort_drift_are_both_reported() {
status: ExitCode::from_code(1),
});

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();

assert_eq!(result.into_content().unwrap(), indoc::indoc! {"
```
Expand Down Expand Up @@ -163,7 +163,7 @@ fn comfort_drift_listing_is_bounded() {
status: ExitCode::from_code(1),
});

let content = cargo_check_impl(&ctx.root, None, false, &runner)
let content = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner)
.unwrap()
.unwrap_content();

Expand Down Expand Up @@ -193,7 +193,7 @@ fn comfort_real_failure_is_reported_as_error() {
status: ExitCode::from_code(2),
});

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();
match result {
Outcome::Error { message, .. } => {
assert_eq!(message, "comfort failed: comfort: parse error");
Expand All @@ -214,7 +214,7 @@ fn clippy_failure_short_circuits_before_running_comfort() {
status: ExitCode::from_code(101),
});

let result = cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
let result = cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();
match result {
Outcome::Error { message, .. } => {
assert_eq!(message, "Cargo command failed: error: build failed");
Expand Down Expand Up @@ -252,7 +252,8 @@ fn package_scope_is_passed_through_to_both_tools() {
])
.returns_success("");

let result = cargo_check_impl(&ctx.root, Some("my_pkg"), false, &runner).unwrap();
let result =
cargo_check_impl(&ctx.root, "-W warnings", Some("my_pkg"), false, &runner).unwrap();
assert_eq!(
result.into_content().unwrap(),
"Check succeeded. No warnings or errors found."
Expand All @@ -277,7 +278,7 @@ fn checksum_freshness_reaches_cargo() {
.returns_success("")
.into();

cargo_check_impl(&ctx.root, None, true, &runner).unwrap();
cargo_check_impl(&ctx.root, "-W warnings", None, true, &runner).unwrap();

let call = runner
.call_with_arg("clippy")
Expand All @@ -304,7 +305,7 @@ fn checksum_freshness_is_absent_unless_opted_into() {
.returns_success("")
.into();

cargo_check_impl(&ctx.root, None, false, &runner).unwrap();
cargo_check_impl(&ctx.root, "-W warnings", None, false, &runner).unwrap();

let call = runner
.call_with_arg("clippy")
Expand Down
13 changes: 11 additions & 2 deletions .config/jp/tools/src/cargo/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ const MAX_EXPANDED_BYTES: usize = 100_000;

pub(crate) async fn cargo_expand(
root: &Utf8Path,
rustflags: &str,
item: String,
package: Option<String>,
checksum_freshness: bool,
) -> ToolResult {
cargo_expand_impl(root, &item, package, checksum_freshness, &DuctProcessRunner)
cargo_expand_impl(
root,
rustflags,
&item,
package,
checksum_freshness,
&DuctProcessRunner,
)
}

fn cargo_expand_impl<R: ProcessRunner>(
root: &Utf8Path,
rustflags: &str,
item: &str,
package: Option<String>,
checksum_freshness: bool,
Expand All @@ -36,7 +45,7 @@ fn cargo_expand_impl<R: ProcessRunner>(
}
args.push(item);

let mut env = vec![("RUST_BACKTRACE", "1")];
let mut env = vec![("RUST_BACKTRACE", "1"), ("RUSTFLAGS", rustflags)];
if checksum_freshness {
// Use content checksums instead of file mtimes for cargo's freshness
// checks, so that sibling checkouts (git worktrees) sharing a target
Expand Down
2 changes: 1 addition & 1 deletion .config/jp/tools/src/cargo/expand_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn test_cargo_expand_success() {

let runner = MockProcessRunner::success(stdout);

let result = cargo_expand_impl(&ctx.root, "main", None, false, &runner).unwrap();
let result = cargo_expand_impl(&ctx.root, "-W warnings", "main", None, false, &runner).unwrap();

assert_eq!(result.into_content().unwrap(), indoc::indoc! {r#"
```rust
Expand Down
12 changes: 8 additions & 4 deletions .config/jp/tools/src/cargo/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ use crate::util::{
truncate,
};

pub(crate) async fn cargo_format(root: &Utf8Path, package: Option<String>) -> ToolResult {
cargo_format_impl(root, package.as_deref(), &DuctProcessRunner)
pub(crate) async fn cargo_format(
root: &Utf8Path,
rustflags: &str,
package: Option<String>,
) -> ToolResult {
cargo_format_impl(root, rustflags, package.as_deref(), &DuctProcessRunner)
}

fn cargo_format_impl<R: ProcessRunner>(
root: &Utf8Path,
rustflags: &str,
package: Option<&str>,
runner: &R,
) -> ToolResult {
Expand All @@ -35,8 +40,7 @@ fn cargo_format_impl<R: ProcessRunner>(
"--files-with-diff",
],
root,
// Prevent warnings from being treated as errors, e.g. on CI.
&[("RUSTFLAGS", "-W warnings")],
&[("RUSTFLAGS", rustflags)],
)?;

if !cargo_status.is_success() {
Expand Down
Loading
Loading