Skip to content

Commit 089daf2

Browse files
committed
feat(cargo): reuse native compiler outputs across clean roots
Expand verified reuse to non-incremental dependency and workspace metadata/rlib units on graduated Apple Silicon macOS and ARM64 Linux. Preserve fail-closed class boundaries and existing wrappers, and add representative fixtures plus reproducible performance evidence.
1 parent f32023b commit 089daf2

48 files changed

Lines changed: 3791 additions & 535 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changes/native-compiler-cache-9fd7.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,11 @@
22
"cargo-rail" = "minor"
33
---
44

5-
Add compiler observation and verified native compiler-result caching for non-incremental workspace library metadata checks on Apple Silicon macOS with Cargo/rustc 1.97.1, preserving custom wrappers and sccache while bypassing unproven classes. Unsupported platforms remain buildable and execute cold through explicit fail-closed boundaries.
5+
Add portable, verified native compiler-result caching for non-incremental dependency and workspace library
6+
metadata/rlib units on Apple Silicon macOS and ARM64 Linux with Cargo/rustc 1.97.1. Ordinary `cargo rail run` check
7+
and build actions can reuse byte-exact outputs across clean roots without restoring or fabricating Cargo target state,
8+
incremental state, or fingerprints.
9+
10+
Preserve custom wrappers and sccache, keep incremental builds and unproven linker/build-script/proc-macro classes
11+
explicitly bypassed, and fail closed on input, toolchain, environment, SDK/linker, cache-object, and output mutations.
12+
Add a representative registry/Git/native/proc-macro fixture plus reproducible cold/warm benchmarks and cache evidence.

.config/nextest.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ retries = 0
1111
[test-groups]
1212
cargo-subprocess = { max-threads = 2 }
1313

14+
[[profile.default.overrides]]
15+
filter = 'binary(integration) & test(/test_native_cache_fixture::/)'
16+
test-group = 'cargo-subprocess'
17+
slow-timeout = { period = "30s", terminate-after = 6 }
18+
1419
[[profile.default.overrides]]
1520
filter = 'binary(integration) & test(/test_(instrumentation|scale_fixture|source_snapshot|unify|unused_detection|undeclared_features)/)'
1621
test-group = 'cargo-subprocess'
@@ -25,6 +30,10 @@ slow-timeout = { period = "60s", terminate-after = 4 }
2530
leak-timeout = { period = "2s", result = "fail" }
2631
retries = 0
2732

33+
[[profile.commit.overrides]]
34+
filter = 'binary(integration) & test(/test_native_cache_fixture::/)'
35+
test-group = 'cargo-subprocess'
36+
2837
[[profile.commit.overrides]]
2938
filter = 'binary(integration) & test(/test_(instrumentation|scale_fixture|source_snapshot|unify|unused_detection|undeclared_features)/)'
3039
test-group = 'cargo-subprocess'

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ build-all:
2424
bench-unify packages="25" runs="10":
2525
@scripts/bench/unify.sh "{{ packages }}" "{{ runs }}"
2626

27+
bench-native-cache runs="10":
28+
@scripts/bench/native-cache.sh "{{ runs }}"
29+
2730
gen-fixture members output:
2831
@scripts/fixtures/generate-workspace.sh "{{ members }}" "{{ output }}"
2932

scripts/bench/native-cache.sh

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env bash
2+
set -uo pipefail
3+
4+
timing_dir="${CARGO_RAIL_UNIT_TIMINGS:?CARGO_RAIL_UNIT_TIMINGS must name an output directory}"
5+
workspace="${CARGO_RAIL_BENCH_WORKSPACE:?CARGO_RAIL_BENCH_WORKSPACE must name the fixture root}"
6+
mkdir -p "$timing_dir"
7+
record="$timing_dir/unit-$$"
8+
9+
crate_name="unknown"
10+
crate_type="unknown"
11+
emit="unknown"
12+
source="unknown"
13+
test_mode="false"
14+
native_link="false"
15+
incremental="false"
16+
17+
arguments=("$@")
18+
index=1
19+
while ((index < ${#arguments[@]})); do
20+
argument="${arguments[$index]}"
21+
next=""
22+
if ((index + 1 < ${#arguments[@]})); then
23+
next="${arguments[$((index + 1))]}"
24+
fi
25+
case "$argument" in
26+
--crate-name)
27+
crate_name="$next"
28+
((index += 2))
29+
continue
30+
;;
31+
--crate-name=*) crate_name="${argument#--crate-name=}" ;;
32+
--crate-type)
33+
crate_type="$next"
34+
((index += 2))
35+
continue
36+
;;
37+
--crate-type=*) crate_type="${argument#--crate-type=}" ;;
38+
--emit)
39+
emit="$next"
40+
((index += 2))
41+
continue
42+
;;
43+
--emit=*) emit="${argument#--emit=}" ;;
44+
--test) test_mode="true" ;;
45+
-l | -l* | -Lnative* | *linker=* | *link-arg=* | *link-args=*) native_link="true" ;;
46+
*incremental=*) incremental="true" ;;
47+
*.rs)
48+
if [[ "$source" == unknown ]]; then
49+
source="$argument"
50+
fi
51+
;;
52+
esac
53+
((index += 1))
54+
done
55+
56+
case "$source" in
57+
"$workspace"/*) source_class="workspace" ;;
58+
*/registry/src/*) source_class="registry" ;;
59+
*/git/checkouts/*) source_class="git" ;;
60+
/*) source_class="host" ;;
61+
*.rs) source_class="workspace" ;;
62+
*) source_class="host" ;;
63+
esac
64+
65+
printf '%s\0' "$@" >"$record.argv"
66+
/usr/bin/time -p -o "$record.time" "$@"
67+
status=$?
68+
real="$(awk '$1 == "real" { print $2 }' "$record.time")"
69+
user="$(awk '$1 == "user" { print $2 }' "$record.time")"
70+
sys="$(awk '$1 == "sys" { print $2 }' "$record.time")"
71+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
72+
"$crate_name" "$crate_type" "$emit" "$source_class" "$test_mode" "$native_link" "$incremental" \
73+
"${real:-0}" "${user:-0}" "${sys:-0}" "$status" >"$record.tsv"
74+
exit "$status"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
echo "usage: $0 <destination> [shared-git-source]" >&2
6+
exit 2
7+
}
8+
9+
[[ $# -ge 1 && $# -le 2 ]] || usage
10+
destination="$1"
11+
[[ -n "$destination" && "$destination" != -* ]] || usage
12+
shared_git_source="${2:-${destination}.git-source}"
13+
[[ -n "$shared_git_source" && "$shared_git_source" != -* ]] || usage
14+
15+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
16+
template="$repo_root/tests/fixtures/native_cache/real_world"
17+
18+
if [[ -e "$destination" ]]; then
19+
[[ -d "$destination" ]] || { echo "fixture destination is not a directory: $destination" >&2; exit 2; }
20+
else
21+
mkdir -p "$destination"
22+
fi
23+
destination="$(cd "$destination" && pwd -P)"
24+
[[ -z "$(find "$destination" -mindepth 1 -maxdepth 1 -print -quit)" ]] || {
25+
echo "fixture destination must be empty: $destination" >&2
26+
exit 2
27+
}
28+
29+
cp -R "$template/." "$destination"
30+
31+
git_source_template="$destination/git-source"
32+
if [[ -e "$shared_git_source" ]]; then
33+
[[ -d "$shared_git_source/.git" ]] || {
34+
echo "shared Git source is not a fixture repository: $shared_git_source" >&2
35+
exit 2
36+
}
37+
rm -r -- "$git_source_template"
38+
else
39+
mkdir -p "$(dirname "$shared_git_source")"
40+
mv "$git_source_template" "$shared_git_source"
41+
git -C "$shared_git_source" init --quiet --initial-branch=main --object-format=sha1
42+
git -C "$shared_git_source" config core.autocrlf false
43+
git -C "$shared_git_source" config core.filemode false
44+
git -C "$shared_git_source" add --all
45+
git_tree="$(git -C "$shared_git_source" write-tree)"
46+
git_commit="$({
47+
GIT_AUTHOR_NAME='cargo-rail fixture' \
48+
GIT_AUTHOR_EMAIL='fixture@cargo-rail.invalid' \
49+
GIT_AUTHOR_DATE='2000-01-01T00:00:00Z' \
50+
GIT_COMMITTER_NAME='cargo-rail fixture' \
51+
GIT_COMMITTER_EMAIL='fixture@cargo-rail.invalid' \
52+
GIT_COMMITTER_DATE='2000-01-01T00:00:00Z' \
53+
git -C "$shared_git_source" commit-tree "$git_tree" -m 'Create native-cache Git dependency'
54+
})"
55+
git -C "$shared_git_source" update-ref refs/heads/main "$git_commit"
56+
fi
57+
git_source="$(cd "$shared_git_source" && pwd -P)"
58+
git_commit="$(git -C "$git_source" rev-parse --verify HEAD)"
59+
60+
manifest="$destination/Cargo.toml"
61+
rendered="$destination/Cargo.toml.rendered"
62+
git_url="file://$git_source"
63+
sed -e "s|__FIXTURE_GIT_URL__|$git_url|" -e "s|__FIXTURE_GIT_REV__|$git_commit|" "$manifest" >"$rendered"
64+
mv "$rendered" "$manifest"
65+
66+
prefetch_manifest="$destination/git-prefetch/Cargo.toml"
67+
prefetch_rendered="$destination/git-prefetch/Cargo.toml.rendered"
68+
sed -e "s|__FIXTURE_GIT_URL__|$git_url|" -e "s|__FIXTURE_GIT_REV__|$git_commit|" \
69+
"$prefetch_manifest" >"$prefetch_rendered"
70+
mv "$prefetch_rendered" "$prefetch_manifest"
71+
cargo metadata --manifest-path "$prefetch_manifest" --format-version=1 >/dev/null
72+
rm -r -- "$destination/git-prefetch"
73+
74+
cargo generate-lockfile --manifest-path "$manifest" --offline --quiet
75+
git -C "$destination" init --quiet --initial-branch=main --object-format=sha1
76+
git -C "$destination" config core.autocrlf false
77+
git -C "$destination" config core.filemode false
78+
git -C "$destination" add --all
79+
fixture_tree="$(git -C "$destination" write-tree)"
80+
fixture_commit="$({
81+
GIT_AUTHOR_NAME='cargo-rail fixture' \
82+
GIT_AUTHOR_EMAIL='fixture@cargo-rail.invalid' \
83+
GIT_AUTHOR_DATE='2000-01-02T00:00:00Z' \
84+
GIT_COMMITTER_NAME='cargo-rail fixture' \
85+
GIT_COMMITTER_EMAIL='fixture@cargo-rail.invalid' \
86+
GIT_COMMITTER_DATE='2000-01-02T00:00:00Z' \
87+
git -C "$destination" commit-tree "$fixture_tree" -m 'Materialize native-cache workload'
88+
})"
89+
git -C "$destination" update-ref refs/heads/main "$fixture_commit"
90+
91+
echo "materialized native-cache fixture at $destination" >&2

src/commands/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ pub enum Commands {
266266
/// Execute supported Rust actions in fresh isolated roots
267267
#[arg(long)]
268268
hermetic: bool,
269-
/// Disable local action-result reuse for this hermetic execution
270-
#[arg(long, requires = "hermetic")]
269+
/// Disable local result reuse for this execution
270+
#[arg(long)]
271271
no_cache: bool,
272272
/// Dry-run action plan format (json/github require --dry-run)
273273
#[arg(long, short = 'f', default_value_t, value_enum)]

src/commands/run.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::action::{
1010
use crate::action_key::{analyze as analyze_action_key, resolution_identity};
1111
use crate::cargo::{ResolutionFeatures, ResolutionPackages, ResolutionRequest, TargetSpecificationIdentity};
1212
use crate::commands::common::{ActionOutputFormat, PlanOutputFormat, format_preview_list};
13+
use crate::compiler::collector::prepare_direct_cargo_action;
1314
use crate::config::MAX_ACTIONS;
1415
use crate::error::{RailError, RailResult};
1516
use crate::git::detect_default_base_ref;
@@ -1480,8 +1481,36 @@ fn run_or_print_action(opts: &RunOptions, ctx: &WorkspaceContext, action: &Expan
14801481
.argv()
14811482
.split_first()
14821483
.ok_or_else(|| RailError::message("expanded action argv cannot be empty"))?;
1484+
let native_cache = if matches!(action.kind(), ActionKind::Build | ActionKind::Distribution) {
1485+
Some(if opts.no_cache {
1486+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("native_cache_disabled_by_request")
1487+
} else if cargo_cli_configuration_override(arguments) {
1488+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("cargo_cli_configuration_not_graduated")
1489+
} else if action_compiler_wrapper_capability(action) {
1490+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("action_compiler_wrapper_preserved")
1491+
} else if !action.environment().inherit() || !action.environment().entries().is_empty() {
1492+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("action_environment_not_graduated")
1493+
} else if std::env::var_os("CARGO_INCREMENTAL").as_deref() != Some(std::ffi::OsStr::new("0")) {
1494+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("native_cache_incremental_policy_not_graduated")
1495+
} else if std::env::var_os("RUSTC_FORCE_INCREMENTAL").is_some() {
1496+
crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("forced_incremental_compilation_not_graduated")
1497+
} else {
1498+
match prepare_direct_cargo_action(ctx.snapshot()?) {
1499+
Ok(setup) => setup,
1500+
Err(_) => crate::compiler::native_cache::DirectNativeCacheSetup::Bypassed("native_cache_identity_unavailable"),
1501+
}
1502+
})
1503+
} else {
1504+
None
1505+
};
14831506
let mut command = Command::new(program);
14841507
let working_directory = action.validate_paths(ctx.workspace_root())?;
1508+
if let Some(configuration) = native_cache
1509+
.as_ref()
1510+
.and_then(crate::compiler::native_cache::DirectNativeCacheSetup::cargo_config_argument)
1511+
{
1512+
command.arg("--config").arg(configuration);
1513+
}
14851514
command.args(arguments).current_dir(&working_directory);
14861515
if !action.environment().inherit() {
14871516
command.env_clear();
@@ -1519,6 +1548,31 @@ fn run_or_print_action(opts: &RunOptions, ctx: &WorkspaceContext, action: &Expan
15191548
let status = command
15201549
.status()
15211550
.map_err(|error| RailError::message(format!("{} failed: {}", action.id(), error)))?;
1551+
if opts.explain
1552+
&& let Some(native_cache) = &native_cache
1553+
{
1554+
if let Some(reason) = native_cache.bypass_reason() {
1555+
println!("action `{}` native compiler cache: bypassed ({reason})", action.id());
1556+
} else if let Some(report) = native_cache.report() {
1557+
let reasons = report
1558+
.reasons
1559+
.iter()
1560+
.map(|(reason, count)| format!("{reason}={count}"))
1561+
.collect::<Vec<_>>()
1562+
.join(",");
1563+
println!(
1564+
"action `{}` native compiler cache: hits={} misses={} bypasses={} setup_bytes_hashed={} bytes_hashed={} bytes_restored={} reasons={}",
1565+
action.id(),
1566+
report.hits,
1567+
report.misses,
1568+
report.bypasses,
1569+
report.setup_bytes_hashed,
1570+
report.bytes_hashed,
1571+
report.bytes_restored,
1572+
reasons,
1573+
);
1574+
}
1575+
}
15221576
if !status.success() {
15231577
return Err(RailError::ExitWithCode {
15241578
code: status.code().unwrap_or(1),
@@ -1527,6 +1581,27 @@ fn run_or_print_action(opts: &RunOptions, ctx: &WorkspaceContext, action: &Expan
15271581
Ok(())
15281582
}
15291583

1584+
fn cargo_cli_configuration_override(arguments: &[String]) -> bool {
1585+
arguments
1586+
.iter()
1587+
.any(|argument| argument == "--config" || argument.starts_with("--config="))
1588+
}
1589+
1590+
fn action_compiler_wrapper_capability(action: &ExpandedAction) -> bool {
1591+
action.environment().entries().iter().any(|entry| {
1592+
let name = match entry {
1593+
ActionEnvironmentEntry::Fixed { name, .. }
1594+
| ActionEnvironmentEntry::Pass { name }
1595+
| ActionEnvironmentEntry::Secret { name }
1596+
| ActionEnvironmentEntry::Cargo { name, .. } => name,
1597+
};
1598+
matches!(
1599+
name.as_str(),
1600+
"RUSTC_WRAPPER" | "RUSTC_WORKSPACE_WRAPPER" | "CARGO_BUILD_RUSTC_WRAPPER" | "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
1601+
)
1602+
})
1603+
}
1604+
15301605
fn print_hermetic_result(
15311606
action_id: &str,
15321607
report: &crate::hermetic::HermeticExecutionReport,

0 commit comments

Comments
 (0)