Skip to content

feat: Introduce perma unstable --compile-time-deps option for cargo build #15674

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 19, 2025
Merged
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
1 change: 1 addition & 0 deletions src/bin/cargo/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub fn cli() -> Command {
.arg_build_plan()
.arg_unit_graph()
.arg_timings()
.arg_compile_time_deps()
.arg_manifest_path()
.arg_lockfile_path()
.arg_ignore_rust_version()
Expand Down
1 change: 1 addition & 0 deletions src/bin/cargo/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub fn cli() -> Command {
.arg_target_dir()
.arg_unit_graph()
.arg_timings()
.arg_compile_time_deps()
.arg_manifest_path()
.arg_lockfile_path()
.arg_ignore_rust_version()
Expand Down
3 changes: 3 additions & 0 deletions src/cargo/core/compiler/build_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct BuildConfig {
pub timing_outputs: Vec<TimingOutput>,
/// Output SBOM precursor files.
pub sbom: bool,
/// Build compile time dependencies only, e.g., build scripts and proc macros
pub compile_time_deps_only: bool,
}

fn default_parallelism() -> CargoResult<u32> {
Expand Down Expand Up @@ -129,6 +131,7 @@ impl BuildConfig {
future_incompat_report: false,
timing_outputs: Vec::new(),
sbom,
compile_time_deps_only: false,
})
}

Expand Down
4 changes: 4 additions & 0 deletions src/cargo/core/compiler/build_runner/compilation_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
bcx: &BuildContext<'a, 'gctx>,
) -> CargoResult<Arc<Vec<OutputFile>>> {
let ret = match unit.mode {
_ if unit.skip_non_compile_time_dep => {
// This skips compilations so no outputs
vec![]
}
CompileMode::Doc => {
let path = if bcx.build_config.intent.wants_doc_json_output() {
self.out_dir(unit)
Expand Down
91 changes: 48 additions & 43 deletions src/cargo/core/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,50 +180,55 @@ fn compile<'gctx>(
return Ok(());
}

// Build up the work to be done to compile this unit, enqueuing it once
// we've got everything constructed.
fingerprint::prepare_init(build_runner, unit)?;

let job = if unit.mode.is_run_custom_build() {
custom_build::prepare(build_runner, unit)?
} else if unit.mode.is_doc_test() {
// We run these targets later, so this is just a no-op for now.
Job::new_fresh()
} else if build_plan {
Job::new_dirty(
rustc(build_runner, unit, &exec.clone())?,
DirtyReason::FreshBuild,
)
} else {
let force = exec.force_rebuild(unit) || force_rebuild;
let mut job = fingerprint::prepare_target(build_runner, unit, force)?;
job.before(if job.freshness().is_dirty() {
let work = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
rustdoc(build_runner, unit)?
} else {
rustc(build_runner, unit, exec)?
};
work.then(link_targets(build_runner, unit, false)?)
// If we are in `--compile-time-deps` and the given unit is not a compile time
// dependency, skip compling the unit and jumps to dependencies, which still
// have chances to be compile time dependencies
if !unit.skip_non_compile_time_dep {
// Build up the work to be done to compile this unit, enqueuing it once
// we've got everything constructed.
fingerprint::prepare_init(build_runner, unit)?;

let job = if unit.mode.is_run_custom_build() {
custom_build::prepare(build_runner, unit)?
} else if unit.mode.is_doc_test() {
// We run these targets later, so this is just a no-op for now.
Job::new_fresh()
} else if build_plan {
Job::new_dirty(
rustc(build_runner, unit, &exec.clone())?,
DirtyReason::FreshBuild,
)
} else {
// We always replay the output cache,
// since it might contain future-incompat-report messages
let show_diagnostics = unit.show_warnings(bcx.gctx)
&& build_runner.bcx.gctx.warning_handling()? != WarningHandling::Allow;
let work = replay_output_cache(
unit.pkg.package_id(),
PathBuf::from(unit.pkg.manifest_path()),
&unit.target,
build_runner.files().message_cache_path(unit),
build_runner.bcx.build_config.message_format,
show_diagnostics,
);
// Need to link targets on both the dirty and fresh.
work.then(link_targets(build_runner, unit, true)?)
});

job
};
jobs.enqueue(build_runner, unit, job)?;
let force = exec.force_rebuild(unit) || force_rebuild;
let mut job = fingerprint::prepare_target(build_runner, unit, force)?;
job.before(if job.freshness().is_dirty() {
let work = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
rustdoc(build_runner, unit)?
} else {
rustc(build_runner, unit, exec)?
};
work.then(link_targets(build_runner, unit, false)?)
} else {
// We always replay the output cache,
// since it might contain future-incompat-report messages
let show_diagnostics = unit.show_warnings(bcx.gctx)
&& build_runner.bcx.gctx.warning_handling()? != WarningHandling::Allow;
let work = replay_output_cache(
unit.pkg.package_id(),
PathBuf::from(unit.pkg.manifest_path()),
&unit.target,
build_runner.files().message_cache_path(unit),
build_runner.bcx.build_config.message_format,
show_diagnostics,
);
// Need to link targets on both the dirty and fresh.
work.then(link_targets(build_runner, unit, true)?)
});

job
};
jobs.enqueue(build_runner, unit, job)?;
}

// Be sure to compile all dependencies of this target as well.
let deps = Vec::from(build_runner.unit_deps(unit)); // Create vec due to mutable borrow.
Expand Down
1 change: 1 addition & 0 deletions src/cargo/core/compiler/standard_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ fn generate_roots(
/*dep_hash*/ 0,
IsArtifact::No,
None,
false,
));
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/cargo/core/compiler/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ pub struct UnitInner {
///
/// [`FeaturesFor::ArtifactDep`]: crate::core::resolver::features::FeaturesFor::ArtifactDep
pub artifact_target_for_features: Option<CompileTarget>,

/// Skip compiling this unit because `--compile-time-deps` flag is set and
/// this is not a compile time dependency.
///
/// Since dependencies of this unit might be compile time dependencies, we
/// set this field instead of completely dropping out this unit from unit graph.
pub skip_non_compile_time_dep: bool,
}

impl UnitInner {
Expand Down Expand Up @@ -245,6 +252,7 @@ impl UnitInterner {
dep_hash: u64,
artifact: IsArtifact,
artifact_target_for_features: Option<CompileTarget>,
skip_non_compile_time_dep: bool,
) -> Unit {
let target = match (is_std, target.kind()) {
// This is a horrible hack to support build-std. `libstd` declares
Expand Down Expand Up @@ -281,6 +289,7 @@ impl UnitInterner {
dep_hash,
artifact,
artifact_target_for_features,
skip_non_compile_time_dep,
});
Unit { inner }
}
Expand Down
1 change: 1 addition & 0 deletions src/cargo/core/compiler/unit_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ fn new_unit_dep_with_profile(
/*dep_hash*/ 0,
artifact.map_or(IsArtifact::No, |_| IsArtifact::Yes),
artifact_target,
false,
);
Ok(UnitDep {
unit,
Expand Down
5 changes: 5 additions & 0 deletions src/cargo/core/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,11 @@ impl Target {
*self.kind() == TargetKind::CustomBuild
}

/// Returns `true` if it is a compile time depencencies, e.g., build script or proc macro
pub fn is_compile_time_dependency(&self) -> bool {
self.is_custom_build() || self.proc_macro()
}

/// Returns the arguments suitable for `--crate-type` to pass to rustc.
pub fn rustc_crate_types(&self) -> Vec<CrateType> {
self.kind().rustc_crate_types()
Expand Down
26 changes: 25 additions & 1 deletion src/cargo/ops/cargo_compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ pub fn create_bcx<'a, 'gctx>(
&units,
&scrape_units,
host_kind_requested.then_some(explicit_host_kind),
build_config.compile_time_deps_only,
);

let mut extra_compiler_args = HashMap::new();
Expand Down Expand Up @@ -582,12 +583,16 @@ where `<compatible-ver>` is the latest version supporting rustc {rustc_version}"
/// This is also responsible for adjusting the `debug` setting for host
/// dependencies, turning off debug if the user has not explicitly enabled it,
/// and the unit is not shared with a target unit.
///
/// This is also responsible for adjusting whether each unit should be compiled
/// or not regarding `--compile-time-deps` flag.
fn rebuild_unit_graph_shared(
interner: &UnitInterner,
unit_graph: UnitGraph,
roots: &[Unit],
scrape_units: &[Unit],
to_host: Option<CompileKind>,
compile_time_deps_only: bool,
) -> (Vec<Unit>, Vec<Unit>, UnitGraph) {
let mut result = UnitGraph::new();
// Map of the old unit to the new unit, used to avoid recursing into units
Expand All @@ -602,8 +607,10 @@ fn rebuild_unit_graph_shared(
&mut result,
&unit_graph,
root,
true,
false,
to_host,
compile_time_deps_only,
)
})
.collect();
Expand All @@ -628,14 +635,21 @@ fn traverse_and_share(
new_graph: &mut UnitGraph,
unit_graph: &UnitGraph,
unit: &Unit,
unit_is_root: bool,
unit_is_for_host: bool,
to_host: Option<CompileKind>,
compile_time_deps_only: bool,
) -> Unit {
if let Some(new_unit) = memo.get(unit) {
// Already computed, no need to recompute.
return new_unit.clone();
}
let mut dep_hash = StableHasher::new();
let skip_non_compile_time_deps = compile_time_deps_only
&& (!unit.target.is_compile_time_dependency() ||
// Root unit is not a dependency unless other units are dependant
// to it.
unit_is_root);
let new_deps: Vec<_> = unit_graph[unit]
.iter()
.map(|dep| {
Expand All @@ -645,8 +659,13 @@ fn traverse_and_share(
new_graph,
unit_graph,
&dep.unit,
false,
dep.unit_for.is_for_host(),
to_host,
// If we should compile the current unit, we should also compile
// its dependencies. And if not, we should compile compile time
// dependencies only.
skip_non_compile_time_deps,
);
new_dep_unit.hash(&mut dep_hash);
UnitDep {
Expand Down Expand Up @@ -712,6 +731,7 @@ fn traverse_and_share(
unit.dep_hash,
unit.artifact,
unit.artifact_target_for_features,
unit.skip_non_compile_time_dep,
);

// We can now turn the deferred value into its actual final value.
Expand Down Expand Up @@ -742,8 +762,11 @@ fn traverse_and_share(
// Since `dep_hash` is now filled in, there's no need to specify the artifact target
// for target-dependent feature resolution
None,
skip_non_compile_time_deps,
);
assert!(memo.insert(unit.clone(), new_unit.clone()).is_none());
if !unit_is_root || !compile_time_deps_only {
assert!(memo.insert(unit.clone(), new_unit.clone()).is_none());
}
new_graph.entry(new_unit.clone()).or_insert(new_deps);
new_unit
}
Expand Down Expand Up @@ -904,6 +927,7 @@ fn override_rustc_crate_types(
unit.dep_hash,
unit.artifact,
unit.artifact_target_for_features,
unit.skip_non_compile_time_dep,
)
};
units[0] = match unit.target.kind() {
Expand Down
1 change: 1 addition & 0 deletions src/cargo/ops/cargo_compile/unit_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ impl<'a> UnitGenerator<'a, '_> {
/*dep_hash*/ 0,
IsArtifact::No,
None,
false,
)
})
.collect()
Expand Down
9 changes: 9 additions & 0 deletions src/cargo/util/command_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ pub trait CommandExt: Sized {
.hide(true),
)
}

fn arg_compile_time_deps(self) -> Self {
self._arg(flag("compile-time-deps", "").hide(true))
}
}

impl CommandExt for Command {
Expand Down Expand Up @@ -806,6 +810,7 @@ Run `{cmd}` to see possible targets."
build_config.build_plan = self.flag("build-plan");
build_config.unit_graph = self.flag("unit-graph");
build_config.future_incompat_report = self.flag("future-incompat-report");
build_config.compile_time_deps_only = self.flag("compile-time-deps");

if self._contains("timings") {
for timing_output in self._values_of("timings") {
Expand Down Expand Up @@ -840,6 +845,10 @@ Run `{cmd}` to see possible targets."
gctx.cli_unstable()
.fail_if_stable_opt("--unit-graph", 8002)?;
}
if build_config.compile_time_deps_only {
gctx.cli_unstable()
.fail_if_stable_opt("--compile-time-deps", 14434)?;
}

let opts = CompileOptions {
build_config,
Expand Down
14 changes: 14 additions & 0 deletions src/doc/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -2190,3 +2190,17 @@ More information can be found in the [config chapter](config.md#cache).
## doctest-xcompile

Doctest cross-compiling is now unconditionally enabled starting in Rust 1.89. Running doctests with `cargo test` will now honor the `--target` flag.

## compile-time-deps

This permanently-unstable flag to only build proc-macros and build scripts (and their required dependencies),
as well as run the build scripts.

It is intended for use by tools like rust-analyzer and will never be stabilized.

Example:

```console
cargo +nightly build --compile-time-deps -Z unstable-options
cargo +nightly check --compile-time-deps --all-targets -Z unstable-options
```
Loading