diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 600383fcc50..fff28ded721 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -256,6 +256,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update nightly && rustup default nightly + - run: rustup target add aarch64-unknown-none - run: rustup component add rust-src - run: cargo build - run: cargo test -p cargo --test build-std diff --git a/src/cargo/core/compiler/build_context/target_info.rs b/src/cargo/core/compiler/build_context/target_info.rs index aa05819cb50..841db3a1520 100644 --- a/src/cargo/core/compiler/build_context/target_info.rs +++ b/src/cargo/core/compiler/build_context/target_info.rs @@ -1096,6 +1096,24 @@ impl<'gctx> RustcTargetData<'gctx> { } } + /// Returns an iterator that contains all possible non-host targets for this session, + /// including those set by artifact dependencies or per-pkg-targets. + /// + /// Should be called only after workspace resolution to ensure that all targets have + /// been collected. + pub fn all_kinds(&self) -> impl Iterator + '_ { + self.target_config.keys().copied().map(CompileKind::Target) + } + + /// Returns whether `build_std` was enabled in any of the targets we have queried. + pub fn build_std(&self) -> bool { + self.host_config.build_std.is_some() + || self + .target_config + .values() + .any(|config| config.build_std.is_some()) + } + pub fn get_unsupported_std_targets(&self) -> Vec<&str> { let mut unsupported = Vec::new(); for (target, target_info) in &self.target_info { diff --git a/src/cargo/core/compiler/standard_lib.rs b/src/cargo/core/compiler/standard_lib.rs index 18253b5d465..c39bc6444be 100644 --- a/src/cargo/core/compiler/standard_lib.rs +++ b/src/cargo/core/compiler/standard_lib.rs @@ -10,48 +10,78 @@ use crate::core::{PackageId, PackageSet, Resolve, Workspace}; use crate::ops::{self, Packages}; use crate::util::errors::CargoResult; +use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; +use std::hash::Hash; use std::path::PathBuf; use super::BuildConfig; -fn std_crates<'a>(crates: &'a [String], default: &'static str, units: &[Unit]) -> HashSet<&'a str> { - let mut crates = HashSet::from_iter(crates.iter().map(|s| s.as_str())); +fn add_default_std_crates + From<&'static str> + Eq + Hash>( + crates: &mut HashSet, + default: &'static str, + include_libtest: bool, +) { // This is a temporary hack until there is a more principled way to // declare dependencies in Cargo.toml. if crates.is_empty() { - crates.insert(default); + crates.insert(default.into()); } if crates.contains("std") { - crates.insert("core"); - crates.insert("alloc"); - crates.insert("proc_macro"); - crates.insert("panic_unwind"); - crates.insert("compiler_builtins"); - // Only build libtest if it looks like it is needed (libtest depends on libstd) - // If we know what units we're building, we can filter for libtest depending on the jobs. - if units - .iter() - .any(|unit| unit.mode.is_rustc_test() && unit.target.harness()) - { - crates.insert("test"); + crates.insert("core".into()); + crates.insert("alloc".into()); + crates.insert("proc_macro".into()); + crates.insert("panic_unwind".into()); + crates.insert("compiler_builtins".into()); + if include_libtest { + crates.insert("test".into()); } } else if crates.contains("core") { - crates.insert("compiler_builtins"); + crates.insert("compiler_builtins".into()); } +} + +fn std_crates( + target_data: &RustcTargetData<'_>, + cli_crates: Option<&[String]>, + include_libtest: bool, +) -> HashMap> { + let mut map = HashMap::new(); + for kind in target_data.all_kinds().chain([CompileKind::Host]) { + let requested_crates = if let Some(crates) = &target_data.target_config(kind).build_std { + crates.val.as_slice() + } else if let Some(cli_crates) = cli_crates { + cli_crates + } else { + continue; + }; - crates + let mut actual_crates = requested_crates + .iter() + .map(Clone::clone) + .collect::>(); + add_default_std_crates( + &mut actual_crates, + if target_data.info(kind).maybe_support_std() { + "std" + } else { + "core" + }, + include_libtest, + ); + map.insert(kind, actual_crates); + } + map } /// Resolve the standard library dependencies. /// -/// * `crates` is the arg value from `-Zbuild-std`. +/// * `cli_crates` is the arg value from `-Zbuild-std`. pub fn resolve_std<'gctx>( ws: &Workspace<'gctx>, target_data: &mut RustcTargetData<'gctx>, build_config: &BuildConfig, - crates: &[String], - kinds: &[CompileKind], + cli_crates: Option<&[String]>, ) -> CargoResult<(PackageSet<'gctx>, Resolve, ResolvedFeatures)> { if build_config.build_plan { ws.gctx() @@ -69,31 +99,32 @@ pub fn resolve_std<'gctx>( // `[dev-dependencies]`. No need for us to generate a `Resolve` which has // those included because we'll never use them anyway. std_ws.set_require_optional_deps(false); + let mut build_std_features = HashSet::new(); let specs = { - // If there is anything looks like needing std, resolve with it. - // If not, we assume only `core` maye be needed, as `core the most fundamental crate. - // - // This may need a UI overhaul if `build-std` wants to fully support multi-targets. - let maybe_std = kinds - .iter() - .any(|kind| target_data.info(*kind).maybe_support_std()); - let mut crates = std_crates(crates, if maybe_std { "std" } else { "core" }, &[]); + let mut set = HashSet::new(); + for (compile_kind, new_set) in std_crates(target_data, cli_crates, false) { + set.extend(new_set); + if let Some(features) = &target_data.target_config(compile_kind).build_std_features { + build_std_features.extend(features.val.as_slice().iter().map(String::clone)); + } else if let Some(features) = &gctx.cli_unstable().build_std_features { + build_std_features.extend(features.iter().map(String::clone)); + } else { + build_std_features + .extend(["panic-unwind", "backtrace", "default"].map(String::from)); + } + } // `sysroot` is not in the default set because it is optional, but it needs - // to be part of the resolve in case we do need it or `libtest`. - crates.insert("sysroot"); - let specs = Packages::Packages(crates.into_iter().map(Into::into).collect()); + // to be part of the resolve in case we do need it for `libtest`. + set.insert("sysroot".into()); + let specs = Packages::Packages(set.into_iter().collect()); specs.to_package_id_specs(&std_ws)? }; - let features = match &gctx.cli_unstable().build_std_features { - Some(list) => list.clone(), - None => vec![ - "panic-unwind".to_string(), - "backtrace".to_string(), - "default".to_string(), - ], - }; + + let build_std_features = build_std_features.into_iter().collect::>(); let cli_features = CliFeatures::from_command_line( - &features, /*all_features*/ false, /*uses_default_features*/ false, + &build_std_features, + /*all_features*/ false, + /*uses_default_features*/ false, )?; let dry_run = false; let resolve = ops::resolve_ws_with_opts( @@ -118,76 +149,62 @@ pub fn resolve_std<'gctx>( /// * `crates` is the arg value from `-Zbuild-std`. /// * `units` is the root units of the build. pub fn generate_std_roots( - crates: &[String], + cli_crates: Option<&[String]>, units: &[Unit], std_resolve: &Resolve, std_features: &ResolvedFeatures, - kinds: &[CompileKind], package_set: &PackageSet<'_>, interner: &UnitInterner, profiles: &Profiles, target_data: &RustcTargetData<'_>, ) -> CargoResult>> { // Generate a map of Units for each kind requested. - let mut ret = HashMap::new(); - let (maybe_std, maybe_core): (Vec<&CompileKind>, Vec<_>) = kinds + let mut ret: HashMap> = HashMap::new(); + // Only build libtest if it looks like it is needed (libtest depends on libstd) + // If we know what units we're building, we can filter for libtest depending on the jobs. + let include_libtest = units .iter() - .partition(|kind| target_data.info(**kind).maybe_support_std()); - for (default_crate, kinds) in [("core", maybe_core), ("std", maybe_std)] { - if kinds.is_empty() { - continue; - } - generate_roots( - &mut ret, - default_crate, - crates, - units, - std_resolve, - std_features, - &kinds, - package_set, - interner, - profiles, - target_data, - )?; - } + .any(|unit| unit.mode.is_rustc_test() && unit.target.harness()); + let crates = std_crates(target_data, cli_crates, include_libtest); - Ok(ret) -} + let all_crates = crates + .values() + .flat_map(|set| set) + .map(|s| s.as_str()) + .collect::>(); + // collect as `Vec` for stable order + let all_crates = all_crates.into_iter().collect::>(); + let std_ids = all_crates + .iter() + .map(|crate_name| { + std_resolve + .query(crate_name) + .map(|pkg_id| (pkg_id, *crate_name)) + }) + .collect::>>()?; + let std_pkgs = package_set.get_many(std_ids.keys().copied())?; -fn generate_roots( - ret: &mut HashMap>, - default: &'static str, - crates: &[String], - units: &[Unit], - std_resolve: &Resolve, - std_features: &ResolvedFeatures, - kinds: &[&CompileKind], - package_set: &PackageSet<'_>, - interner: &UnitInterner, - profiles: &Profiles, - target_data: &RustcTargetData<'_>, -) -> CargoResult<()> { - let std_ids = std_crates(crates, default, units) + // a map of the requested std crate and its actual package. + let std_pkgs = std_pkgs .iter() - .map(|crate_name| std_resolve.query(crate_name)) - .collect::>>()?; - let std_pkgs = package_set.get_many(std_ids)?; + .map(|pkg| (*std_ids.get(&pkg.package_id()).unwrap(), *pkg)) + .collect::>(); - for pkg in std_pkgs { - let lib = pkg - .targets() - .iter() - .find(|t| t.is_lib()) - .expect("std has a lib"); - // I don't think we need to bother with Check here, the difference - // in time is minimal, and the difference in caching is - // significant. - let mode = CompileMode::Build; - let features = std_features.activated_features(pkg.package_id(), FeaturesFor::NormalOrDev); - for kind in kinds { - let kind = **kind; - let list = ret.entry(kind).or_insert_with(Vec::new); + for (&kind, crates) in &crates { + let list = ret.entry(kind).or_default(); + for krate in crates { + let pkg = std_pkgs.get(krate.as_str()).unwrap(); + let lib = pkg + .targets() + .iter() + .find(|t| t.is_lib()) + .expect("std has a lib"); + // I don't think we need to bother with Check here, the difference + // in time is minimal, and the difference in caching is + // significant. + let mode = CompileMode::Build; + let features = + std_features.activated_features(pkg.package_id(), FeaturesFor::NormalOrDev); let unit_for = UnitFor::new_normal(kind); let profile = profiles.get_profile( pkg.package_id(), @@ -213,7 +230,8 @@ fn generate_roots( )); } } - Ok(()) + + Ok(ret) } fn detect_sysroot_src_path(target_data: &RustcTargetData<'_>) -> CargoResult { diff --git a/src/cargo/core/compiler/unit_dependencies.rs b/src/cargo/core/compiler/unit_dependencies.rs index 3255c820861..3c2d38f14ba 100644 --- a/src/cargo/core/compiler/unit_dependencies.rs +++ b/src/cargo/core/compiler/unit_dependencies.rs @@ -177,16 +177,19 @@ fn attach_std_deps( let mut found = false; for (unit, deps) in state.unit_dependencies.iter_mut() { if !unit.kind.is_host() && !unit.mode.is_run_custom_build() { - deps.extend(std_roots[&unit.kind].iter().map(|unit| UnitDep { - unit: unit.clone(), - unit_for: UnitFor::new_normal(unit.kind), - extern_crate_name: unit.pkg.name(), - dep_name: None, - // TODO: Does this `public` make sense? - public: true, - noprelude: true, - })); - found = true; + // only attach roots for which the user has requested `build-std` for. + if let Some(roots) = std_roots.get(&unit.kind) { + deps.extend(roots.iter().map(|unit| UnitDep { + unit: unit.clone(), + unit_for: UnitFor::new_normal(unit.kind), + extern_crate_name: unit.pkg.name(), + dep_name: None, + // TODO: Does this `public` make sense? + public: true, + noprelude: true, + })); + found = true; + } } } // And also include the dependencies of the standard library itself. Don't diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index 6a406cc9227..78014b6f0b0 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -287,13 +287,13 @@ pub fn create_bcx<'a, 'gctx>( resolved_features, } = resolve; - let std_resolve_features = if let Some(crates) = &gctx.cli_unstable().build_std { + let build_std = gctx.cli_unstable().build_std.is_some() || target_data.build_std(); + let std_resolve_features = if build_std { let (std_package_set, std_resolve, std_features) = standard_lib::resolve_std( ws, &mut target_data, &build_config, - crates, - &build_config.requested_kinds, + gctx.cli_unstable().build_std.as_deref(), )?; pkg_set.add_set(std_package_set); Some((std_resolve, std_features)) @@ -354,14 +354,6 @@ pub fn create_bcx<'a, 'gctx>( // assuming `--target $HOST` was specified. See // `rebuild_unit_graph_shared` for more on why this is done. let explicit_host_kind = CompileKind::Target(CompileTarget::new(&target_data.rustc.host)?); - let explicit_host_kinds: Vec<_> = build_config - .requested_kinds - .iter() - .map(|kind| match kind { - CompileKind::Host => explicit_host_kind, - CompileKind::Target(t) => CompileKind::Target(*t), - }) - .collect(); // Passing `build_config.requested_kinds` instead of // `explicit_host_kinds` here so that `generate_root_units` can do @@ -398,14 +390,13 @@ pub fn create_bcx<'a, 'gctx>( Vec::new() }; - let std_roots = if let Some(crates) = gctx.cli_unstable().build_std.as_ref() { + let std_roots = if build_std { let (std_resolve, std_features) = std_resolve_features.as_ref().unwrap(); standard_lib::generate_std_roots( - &crates, + gctx.cli_unstable().build_std.as_deref(), &units, std_resolve, std_features, - &explicit_host_kinds, &pkg_set, interner, &profiles, diff --git a/src/cargo/ops/cargo_fetch.rs b/src/cargo/ops/cargo_fetch.rs index dc90964119f..fde1ad0eb18 100644 --- a/src/cargo/ops/cargo_fetch.rs +++ b/src/cargo/ops/cargo_fetch.rs @@ -65,14 +65,15 @@ pub fn fetch<'a>( deps_to_fetch.extend(deps); } + let build_std = gctx.cli_unstable().build_std.is_some() || data.build_std(); + // If -Zbuild-std was passed, download dependencies for the standard library. - if let Some(crates) = &gctx.cli_unstable().build_std { + if build_std { let (std_package_set, _, _) = standard_lib::resolve_std( ws, &mut data, &build_config, - crates, - &build_config.requested_kinds, + gctx.cli_unstable().build_std.as_deref(), )?; packages.add_set(std_package_set); } diff --git a/src/cargo/util/context/target.rs b/src/cargo/util/context/target.rs index 0dd5cfc4ce9..5cbdbecb09d 100644 --- a/src/cargo/util/context/target.rs +++ b/src/cargo/util/context/target.rs @@ -32,6 +32,10 @@ pub struct TargetConfig { pub rustdocflags: OptValue, /// The path of the linker for this target. pub linker: OptValue, + /// The list of crates from the standard library to compile for this target. + pub build_std: OptValue, + /// Features enabled for the standard library itself when building the standard library. + pub build_std_features: OptValue, /// Build script override for the given library name. /// /// Any package with a `links` value for the given library name will skip @@ -125,6 +129,19 @@ fn load_config_table(gctx: &GlobalContext, prefix: &str) -> CargoResult = gctx.get(&format!("{prefix}.rustflags"))?; let rustdocflags: OptValue = gctx.get(&format!("{prefix}.rustdocflags"))?; let linker: OptValue = gctx.get(&format!("{prefix}.linker"))?; + let build_std: OptValue = gctx.get(&format!("{prefix}.build-std"))?; + let build_std_features: OptValue = + gctx.get(&format!("{prefix}.build-std-features"))?; + + if (build_std.is_some() || build_std_features.is_some()) + && !gctx.cli_unstable().unstable_options + { + return Err(anyhow::format_err!( + "the `build_std` and `build_std_features` fields on targets are unstable, \ + pass `-Z unstable-options` on the nightly channel to enable it" + ) + .into()); + } // Links do not support environment variables. let target_key = ConfigKey::from_str(prefix); let links_overrides = match gctx.get_table(&target_key)? { @@ -136,6 +153,8 @@ fn load_config_table(gctx: &GlobalContext, prefix: &str) -> CargoResult continue, + "ar" | "linker" | "runner" | "rustflags" | "rustdocflags" | "build-std" + | "build-std-features" => continue, _ => {} } let mut output = BuildOutput::default(); diff --git a/tests/build-std/main.rs b/tests/build-std/main.rs index cf83e63c0aa..3ac84a552b8 100644 --- a/tests/build-std/main.rs +++ b/tests/build-std/main.rs @@ -21,7 +21,7 @@ #![allow(clippy::disallowed_methods)] use cargo_test_support::prelude::*; -use cargo_test_support::{basic_manifest, paths, project, rustc_host, str, Execs}; +use cargo_test_support::{basic_manifest, cross_compile, paths, project, rustc_host, str, Execs}; use std::env; use std::path::Path; @@ -441,3 +441,75 @@ fn test_panic_abort() { .arg("-Zbuild-std-features=panic_immediate_abort") .run(); } + +#[cargo_test(build_std_real)] +fn bindeps() { + if !cross_compile::requires_target_installed("aarch64-unknown-none") { + return; + } + + let p = project() + .file( + "Cargo.toml", + &r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2021" + authors = [] + resolver = "2" + + [dependencies] + bar = { path = "bar/", artifact = "staticlib", target = "aarch64-unknown-none" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + edition = "2021" + + [lib] + crate-type = ["staticlib"] + "#, + ) + .file( + "bar/src/lib.rs", + r#" + #![no_std] + pub fn bar() {} + #[panic_handler] + fn panic(info: &core::panic::PanicInfo) -> ! { + loop {} + } + "#, + ) + .file( + ".cargo/config.toml", + &format!( + r#" + [target.aarch64-unknown-none] + build-std = ['core'] + "# + ), + ) + .build(); + let mut build = p.cargo("build -v --lib -Zunstable-options -Zbindeps"); + build + .target_host() + .masquerade_as_nightly_cargo(&["build-std"]) + .with_stderr_data( + str![[r#" +... +[RUNNING] `rustc --crate-name bar [..]--target aarch64-unknown-none[..] +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ); + + build.run(); +} diff --git a/tests/testsuite/standard_lib.rs b/tests/testsuite/standard_lib.rs index 099a5fe6f96..3f4ed0f083a 100644 --- a/tests/testsuite/standard_lib.rs +++ b/tests/testsuite/standard_lib.rs @@ -864,3 +864,176 @@ fn fetch() { .with_stderr_does_not_contain("[DOWNLOADED] [..]") .run(); } + +// TODO: doing for a different target does not trigger the error +#[cargo_test(build_std_mock)] +fn target_config_gate() { + let host = rustc_host(); + let p = project() + .file( + "src/lib.rs", + r#" + #![no_std] + pub fn foo() {} + "#, + ) + .file( + ".cargo/config.toml", + &format!( + r#" + [target.{host}] + build-std = ['core'] + "# + ), + ) + .build(); + let mut build = p.cargo("build -v --lib"); + build.masquerade_as_nightly_cargo(&["build-std"]).with_stderr_data(str![[ + r#" +[ERROR] the `build_std` and `build_std_features` fields on targets are unstable, pass `-Z unstable-options` on the nightly channel to enable it + +"#]]) + .with_status(101); + + build.run(); +} + +#[cargo_test(build_std_mock)] +fn target_config() { + let setup = setup(); + let host = rustc_host(); + + let p = project() + .file( + "src/lib.rs", + r#" + #![no_std] + pub fn foo() { + core::custom_api(); + } + "#, + ) + .file( + ".cargo/config.toml", + &format!( + r#" + [target.{host}] + build-std = ['core'] + "# + ), + ) + .build(); + let mut build = p.cargo("build -v --lib -Zunstable-options"); + enable_build_std(&mut build, &setup); + + build.run(); +} + +// check that per-pkg-target interacts with target configured build-std nicely in that: +// - the forced target is used over the explicitly requested target +// - the build-std config on the forced target works correctly +#[cargo_test(build_std_mock)] +fn per_pkg_target() { + let setup = setup(); + let host = rustc_host(); + let alternate = "aarch64-unknown-none"; + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + cargo-features = ["per-package-target"] + [package] + name = "foo" + version = "0.1.0" + edition = "2021" + forced-target = "{host}" + "# + ), + ) + .file( + "src/lib.rs", + r#" + #![no_std] + pub fn foo() { + core::custom_api(); + } + "#, + ) + .file( + ".cargo/config.toml", + &format!( + r#" + [target.{host}] + build-std = ['core'] + "# + ), + ) + .build(); + let mut build = p.cargo("build -v --lib -Zunstable-options"); + build + .arg("--target") + .arg(alternate) + .with_stderr_data( + str![[r#" +[UPDATING] `dummy-registry` index +[DOWNLOADING] crates ... +[DOWNLOADED] registry-dep-using-std v1.0.0 (registry `dummy-registry`) +[DOWNLOADED] registry-dep-using-core v1.0.0 (registry `dummy-registry`) +[DOWNLOADED] registry-dep-using-alloc v1.0.0 (registry `dummy-registry`) +[COMPILING] compiler_builtins v0.1.0 ([..]/library/compiler_builtins) +[COMPILING] core v0.1.0 ([..]/library/core) +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[RUNNING] `[..] rustc --crate-name compiler_builtins [..]--target [HOST_TARGET][..]` +[RUNNING] `[..] rustc --crate-name core [..]--target [HOST_TARGET][..]` +[RUNNING] `[..] rustc --crate-name foo [..]--target [HOST_TARGET][..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) + .with_stderr_does_not_contain("aarch64-unknown-none"); + enable_build_std(&mut build, &setup); + + build.run(); +} + +// configuring `build-std` for a different target does not cause us to automatically build std for +// the current target. +#[cargo_test(build_std_mock)] +fn other_target_config() { + let setup = setup(); + + let p = project() + .file( + "src/lib.rs", + r#" + #![no_std] + pub fn foo() {} + "#, + ) + .file( + ".cargo/config.toml", + &format!( + r#" + [target.aarch64-unknown-none] + build-std = ['core'] + "# + ), + ) + .build(); + let mut build = p.cargo("build -v --lib"); + build.target_host().with_status(101).with_stderr_data( + str![[r#" +... +error[E0463]: can't find crate for `core` +[ERROR] could not compile `foo` (lib) due to 1 previous error + +"#]] + .unordered(), + ); + enable_build_std(&mut build, &setup); + + build.run(); +}