From 5f833db69be1ae445a75e79c777cd34f3ba778a8 Mon Sep 17 00:00:00 2001 From: Arlo Siemsen Date: Sat, 8 Feb 2025 11:37:01 -0600 Subject: [PATCH] feat: Add SBOM pre-cursor files Adds a new option `build.sbom` that adds generation of a JSON file containing dependency information alongside compiled artifacts. --- Cargo.lock | 2 +- crates/cargo-test-support/Cargo.toml | 2 +- crates/cargo-test-support/src/lib.rs | 10 + src/cargo/core/compiler/build_config.rs | 14 + .../compiler/build_context/target_info.rs | 2 + .../build_runner/compilation_files.rs | 26 +- src/cargo/core/compiler/build_runner/mod.rs | 15 +- src/cargo/core/compiler/fingerprint/mod.rs | 7 +- src/cargo/core/compiler/mod.rs | 17 +- src/cargo/core/compiler/output_depinfo.rs | 11 +- src/cargo/core/compiler/output_sbom.rs | 181 +++++ src/cargo/core/features.rs | 2 + src/cargo/util/context/mod.rs | 2 + src/doc/src/reference/unstable.md | 94 +++ tests/testsuite/cargo/z_help/stdout.term.svg | 24 +- tests/testsuite/main.rs | 1 + tests/testsuite/sbom.rs | 712 ++++++++++++++++++ 17 files changed, 1099 insertions(+), 23 deletions(-) create mode 100644 src/cargo/core/compiler/output_sbom.rs create mode 100644 tests/testsuite/sbom.rs diff --git a/Cargo.lock b/Cargo.lock index 2a53fb6efb2..2b95baebad0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,7 +457,7 @@ version = "0.4.2" [[package]] name = "cargo-test-support" -version = "0.7.2" +version = "0.7.3" dependencies = [ "anstream", "anstyle", diff --git a/crates/cargo-test-support/Cargo.toml b/crates/cargo-test-support/Cargo.toml index 444291a5cc8..a61934a8b3c 100644 --- a/crates/cargo-test-support/Cargo.toml +++ b/crates/cargo-test-support/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-test-support" -version = "0.7.2" +version = "0.7.3" edition.workspace = true rust-version = "1.85" # MSRV:1 license.workspace = true diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 9eeb4eb00f8..cf7057669fe 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -415,6 +415,16 @@ impl Project { .join(paths::get_lib_filename(name, kind)) } + /// Path to a dynamic library. + /// ex: `/path/to/cargo/target/cit/t0/foo/target/debug/examples/libex.dylib` + pub fn dylib(&self, name: &str) -> PathBuf { + self.target_debug_dir().join(format!( + "{}{name}{}", + env::consts::DLL_PREFIX, + env::consts::DLL_SUFFIX + )) + } + /// Path to a debug binary. /// /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug/foo` diff --git a/src/cargo/core/compiler/build_config.rs b/src/cargo/core/compiler/build_config.rs index 80aa592dde3..edd14c5d8d1 100644 --- a/src/cargo/core/compiler/build_config.rs +++ b/src/cargo/core/compiler/build_config.rs @@ -48,6 +48,8 @@ pub struct BuildConfig { pub future_incompat_report: bool, /// Which kinds of build timings to output (empty if none). pub timing_outputs: Vec, + /// Output SBOM precursor files. + pub sbom: bool, } fn default_parallelism() -> CargoResult { @@ -99,6 +101,17 @@ impl BuildConfig { }, }; + // If sbom flag is set, it requires the unstable feature + let sbom = match (cfg.sbom, gctx.cli_unstable().sbom) { + (Some(sbom), true) => sbom, + (Some(_), false) => { + gctx.shell() + .warn("ignoring 'sbom' config, pass `-Zsbom` to enable it")?; + false + } + (None, _) => false, + }; + Ok(BuildConfig { requested_kinds, jobs, @@ -115,6 +128,7 @@ impl BuildConfig { export_dir: None, future_incompat_report: false, timing_outputs: Vec::new(), + sbom, }) } diff --git a/src/cargo/core/compiler/build_context/target_info.rs b/src/cargo/core/compiler/build_context/target_info.rs index 6a0bea3f79e..d12c39b4dd8 100644 --- a/src/cargo/core/compiler/build_context/target_info.rs +++ b/src/cargo/core/compiler/build_context/target_info.rs @@ -72,6 +72,8 @@ pub enum FileFlavor { Rmeta, /// Piece of external debug information (e.g., `.dSYM`/`.pdb` file). DebugInfo, + /// SBOM (Software Bill of Materials pre-cursor) file (e.g. cargo-sbon.json). + Sbom, } /// Type of each file generated by a Unit. diff --git a/src/cargo/core/compiler/build_runner/compilation_files.rs b/src/cargo/core/compiler/build_runner/compilation_files.rs index 29a5458ca90..c691a19ad0b 100644 --- a/src/cargo/core/compiler/build_runner/compilation_files.rs +++ b/src/cargo/core/compiler/build_runner/compilation_files.rs @@ -495,13 +495,37 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { CompileMode::Test | CompileMode::Build | CompileMode::Bench - | CompileMode::Check { .. } => self.calc_outputs_rustc(unit, bcx)?, + | CompileMode::Check { .. } => { + let mut outputs = self.calc_outputs_rustc(unit, bcx)?; + if bcx.build_config.sbom && bcx.gctx.cli_unstable().sbom { + let sbom_files: Vec<_> = outputs + .iter() + .filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable)) + .map(|output| OutputFile { + path: Self::append_sbom_suffix(&output.path), + hardlink: output.hardlink.as_ref().map(Self::append_sbom_suffix), + export_path: output.export_path.as_ref().map(Self::append_sbom_suffix), + flavor: FileFlavor::Sbom, + }) + .collect(); + outputs.extend(sbom_files.into_iter()); + } + outputs + } }; debug!("Target filenames: {:?}", ret); Ok(Arc::new(ret)) } + /// Append the SBOM suffix to the file name. + fn append_sbom_suffix(link: &PathBuf) -> PathBuf { + const SBOM_FILE_EXTENSION: &str = ".cargo-sbom.json"; + let mut link_buf = link.clone().into_os_string(); + link_buf.push(SBOM_FILE_EXTENSION); + PathBuf::from(link_buf) + } + /// Computes the actual, full pathnames for all the files generated by rustc. /// /// The `OutputFile` also contains the paths where those files should be diff --git a/src/cargo/core/compiler/build_runner/mod.rs b/src/cargo/core/compiler/build_runner/mod.rs index 1a55d211068..16476693784 100644 --- a/src/cargo/core/compiler/build_runner/mod.rs +++ b/src/cargo/core/compiler/build_runner/mod.rs @@ -309,7 +309,10 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { fn collect_tests_and_executables(&mut self, unit: &Unit) -> CargoResult<()> { for output in self.outputs(unit)?.iter() { - if output.flavor == FileFlavor::DebugInfo || output.flavor == FileFlavor::Auxiliary { + if matches!( + output.flavor, + FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom + ) { continue; } @@ -446,6 +449,16 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { self.files().metadata(unit).unit_id() } + /// Returns the list of SBOM output file paths for a given [`Unit`]. + pub fn sbom_output_files(&self, unit: &Unit) -> CargoResult> { + Ok(self + .outputs(unit)? + .iter() + .filter(|o| o.flavor == FileFlavor::Sbom) + .map(|o| o.path.clone()) + .collect()) + } + pub fn is_primary_package(&self, unit: &Unit) -> bool { self.primary_packages.contains(&unit.pkg.package_id()) } diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs index b409b7aac35..379bab0b159 100644 --- a/src/cargo/core/compiler/fingerprint/mod.rs +++ b/src/cargo/core/compiler/fingerprint/mod.rs @@ -1517,7 +1517,12 @@ fn calculate_normal( let outputs = build_runner .outputs(unit)? .iter() - .filter(|output| !matches!(output.flavor, FileFlavor::DebugInfo | FileFlavor::Auxiliary)) + .filter(|output| { + !matches!( + output.flavor, + FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom + ) + }) .map(|output| output.path.clone()) .collect(); diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 1b15f450351..2fd6f682736 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -47,6 +47,7 @@ pub(crate) mod layout; mod links; mod lto; mod output_depinfo; +mod output_sbom; pub mod rustdoc; pub mod standard_lib; mod timings; @@ -60,7 +61,7 @@ use std::env; use std::ffi::{OsStr, OsString}; use std::fmt::Display; use std::fs::{self, File}; -use std::io::{BufRead, Write}; +use std::io::{BufRead, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -85,6 +86,7 @@ use self::job_queue::{Job, JobQueue, JobState, Work}; pub(crate) use self::layout::Layout; pub use self::lto::Lto; use self::output_depinfo::output_depinfo; +use self::output_sbom::build_sbom; use self::unit_graph::UnitDep; use crate::core::compiler::future_incompat::FutureIncompatReport; pub use crate::core::compiler::unit::{Unit, UnitInterner}; @@ -307,6 +309,8 @@ fn rustc( let script_metadata = build_runner.find_build_script_metadata(unit); let is_local = unit.is_local(); let artifact = unit.artifact; + let sbom_files = build_runner.sbom_output_files(unit)?; + let sbom = build_sbom(build_runner, unit)?; let hide_diagnostics_for_scrape_unit = build_runner.bcx.unit_can_fail_for_docscraping(unit) && !matches!( @@ -392,6 +396,12 @@ fn rustc( if build_plan { state.build_plan(buildkey, rustc.clone(), outputs.clone()); } else { + for file in sbom_files { + tracing::debug!("writing sbom to {}", file.display()); + let outfile = BufWriter::new(paths::create(&file)?); + serde_json::to_writer(outfile, &sbom)?; + } + let result = exec .exec( &rustc, @@ -685,6 +695,7 @@ where /// completion of other units will be added later in runtime, such as flags /// from build scripts. fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult { + let gctx = build_runner.bcx.gctx; let is_primary = build_runner.is_primary_package(unit); let is_workspace = build_runner.bcx.ws.is_member(&unit.pkg); @@ -700,7 +711,7 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult base.args(args); } base.args(&unit.rustflags); - if build_runner.bcx.gctx.cli_unstable().binary_dep_depinfo { + if gctx.cli_unstable().binary_dep_depinfo { base.arg("-Z").arg("binary-dep-depinfo"); } if build_runner.bcx.gctx.cli_unstable().checksum_freshness { @@ -709,6 +720,8 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult if is_primary { base.env("CARGO_PRIMARY_PACKAGE", "1"); + let file_list = std::env::join_paths(build_runner.sbom_output_files(unit)?)?; + base.env("CARGO_SBOM_PATH", file_list); } if unit.target.is_test() || unit.target.is_bench() { diff --git a/src/cargo/core/compiler/output_depinfo.rs b/src/cargo/core/compiler/output_depinfo.rs index d9efbaae3ff..80f6ffcb71f 100644 --- a/src/cargo/core/compiler/output_depinfo.rs +++ b/src/cargo/core/compiler/output_depinfo.rs @@ -141,11 +141,12 @@ pub fn output_depinfo(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Ca .map(|f| render_filename(f, basedir)) .collect::>>()?; - for output in build_runner - .outputs(unit)? - .iter() - .filter(|o| !matches!(o.flavor, FileFlavor::DebugInfo | FileFlavor::Auxiliary)) - { + for output in build_runner.outputs(unit)?.iter().filter(|o| { + !matches!( + o.flavor, + FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom + ) + }) { if let Some(ref link_dst) = output.hardlink { let output_path = link_dst.with_extension("d"); if success { diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs new file mode 100644 index 00000000000..78f4a37b5d5 --- /dev/null +++ b/src/cargo/core/compiler/output_sbom.rs @@ -0,0 +1,181 @@ +//! cargo-sbom precursor files for external tools to create SBOM files from. +//! See [`build_sbom_graph`] for more. + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::path::PathBuf; + +use cargo_util_schemas::core::PackageIdSpec; +use itertools::Itertools; +use serde::Serialize; + +use crate::core::TargetKind; +use crate::util::interning::InternedString; +use crate::util::Rustc; +use crate::CargoResult; + +use super::{BuildRunner, CompileMode, Unit}; + +/// Typed version of a SBOM format version number. +#[derive(Serialize, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct SbomFormatVersion(u32); + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize)] +#[serde(rename_all = "snake_case")] +enum SbomDependencyType { + /// A dependency linked to the artifact produced by this unit. + Normal, + /// A dependency needed to run the build for this unit (e.g. a build script or proc-macro). + /// The dependency is not linked to the artifact produced by this unit. + Build, +} + +#[derive(Serialize, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +struct SbomIndex(usize); + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +struct SbomDependency { + index: SbomIndex, + kind: SbomDependencyType, +} + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +struct SbomCrate { + id: PackageIdSpec, + features: Vec, + dependencies: Vec, + kind: TargetKind, +} + +impl SbomCrate { + pub fn new(unit: &Unit) -> Self { + let package_id = unit.pkg.package_id().to_spec(); + let features = unit.features.iter().map(|f| f.to_string()).collect_vec(); + Self { + id: package_id, + features, + dependencies: Vec::new(), + kind: unit.target.kind().clone(), + } + } +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "snake_case")] +struct SbomRustc { + version: String, + wrapper: Option, + workspace_wrapper: Option, + commit_hash: Option, + host: String, + verbose_version: String, +} + +impl From<&Rustc> for SbomRustc { + fn from(rustc: &Rustc) -> Self { + Self { + version: rustc.version.to_string(), + wrapper: rustc.wrapper.clone(), + workspace_wrapper: rustc.workspace_wrapper.clone(), + commit_hash: rustc.commit_hash.clone(), + host: rustc.host.to_string(), + verbose_version: rustc.verbose_version.clone(), + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub struct Sbom { + version: SbomFormatVersion, + root: SbomIndex, + crates: Vec, + rustc: SbomRustc, + target: InternedString, +} + +/// Build an [`Sbom`] for the given [`Unit`]. +pub fn build_sbom(build_runner: &BuildRunner<'_, '_>, root: &Unit) -> CargoResult { + let bcx = build_runner.bcx; + let rustc: SbomRustc = bcx.rustc().into(); + + let mut crates = Vec::new(); + let sbom_graph = build_sbom_graph(build_runner, root); + + // Build set of indicies for each node in the graph for fast lookup. + let indicies: HashMap<&Unit, SbomIndex> = sbom_graph + .keys() + .enumerate() + .map(|(i, dep)| (*dep, SbomIndex(i))) + .collect(); + + // Add a item to the crates list for each node in the graph. + for (unit, edges) in sbom_graph { + let mut krate = SbomCrate::new(unit); + for (dep, kind) in edges { + krate.dependencies.push(SbomDependency { + index: indicies[dep], + kind: kind, + }); + } + crates.push(krate); + } + let target = match root.kind { + super::CompileKind::Host => build_runner.bcx.host_triple(), + super::CompileKind::Target(target) => target.rustc_target(), + }; + Ok(Sbom { + version: SbomFormatVersion(1), + crates, + root: indicies[root], + rustc, + target, + }) +} + +/// List all dependencies, including transitive ones. A dependency can also appear multiple times +/// if it's using different settings, e.g. profile, features or crate versions. +/// +/// Returns a graph of dependencies. +fn build_sbom_graph<'a>( + build_runner: &'a BuildRunner<'_, '_>, + root: &'a Unit, +) -> BTreeMap<&'a Unit, BTreeSet<(&'a Unit, SbomDependencyType)>> { + tracing::trace!("building sbom graph for {}", root.pkg.package_id()); + + let mut queue = Vec::new(); + let mut sbom_graph: BTreeMap<&Unit, BTreeSet<(&Unit, SbomDependencyType)>> = BTreeMap::new(); + let mut visited = HashSet::new(); + + // Search to collect all dependencies of the root unit. + queue.push((root, root, false)); + while let Some((node, parent, is_build_dep)) = queue.pop() { + let dependencies = sbom_graph.entry(parent).or_default(); + for dep in build_runner.unit_deps(node) { + let dep = &dep.unit; + let (next_parent, next_is_build_dep) = if dep.mode == CompileMode::RunCustomBuild { + // Nodes in the SBOM graph for building/running build scripts are moved on to their parent as build dependencies. + (parent, true) + } else { + // Proc-macros and build scripts are marked as build dependencies. + let dep_type = match is_build_dep || dep.target.proc_macro() { + false => SbomDependencyType::Normal, + true => SbomDependencyType::Build, + }; + dependencies.insert((dep, dep_type)); + tracing::trace!( + "adding sbom edge {} -> {} ({:?})", + parent.pkg.package_id(), + dep.pkg.package_id(), + dep_type, + ); + (dep, false) + }; + if visited.insert(dep) { + queue.push((dep, next_parent, next_is_build_dep)); + } + } + } + sbom_graph +} diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 7cf4225e5ac..f5aab9dc60b 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -792,6 +792,7 @@ unstable_cli_options!( root_dir: Option = ("Set the root directory relative to which paths are printed (defaults to workspace root)"), rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"), rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"), + sbom: bool = ("Enable the `sbom` option in build config in .cargo/config.toml file"), script: bool = ("Enable support for single-file, `.rs` packages"), separate_nightlies: bool, skip_rustdoc_fingerprint: bool, @@ -1304,6 +1305,7 @@ impl CliUnstable { "root-dir" => self.root_dir = v.map(|v| v.into()), "rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?, "rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?, + "sbom" => self.sbom = parse_empty(k, v)?, "separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?, "checksum-freshness" => self.checksum_freshness = parse_empty(k, v)?, "skip-rustdoc-fingerprint" => self.skip_rustdoc_fingerprint = parse_empty(k, v)?, diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index b74450c202e..efce16ccb3f 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2692,6 +2692,8 @@ pub struct CargoBuildConfig { pub out_dir: Option, pub artifact_dir: Option, pub warnings: Option, + /// Unstable feature `-Zsbom`. + pub sbom: Option, } /// Whether warnings should warn, be allowed, or cause an error. diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index da2ece53686..e7fab986de8 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -74,6 +74,7 @@ Each new feature described below should explain how to use it. * [public-dependency](#public-dependency) --- Allows dependencies to be classified as either public or private. * [msrv-policy](#msrv-policy) --- MSRV-aware resolver and version selection * [precise-pre-release](#precise-pre-release) --- Allows pre-release versions to be selected with `update --precise` + * [sbom](#sbom) --- Generates SBOM pre-cursor files for compiled artifacts * [update-breaking](#update-breaking) --- Allows upgrading to breaking versions with `update --breaking` * [feature-unification](#feature-unification) --- Enable new feature unification modes in workspaces * Output behavior @@ -419,6 +420,99 @@ It's possible to update `my-dependency` to a pre-release with `update -Zunstable This is because `0.1.2-pre.0` is considered compatible with `0.1.1`. It would not be possible to upgrade to `0.2.0-pre.0` from `0.1.1` in the same way. +## sbom +* Tracking Issue: [#13709](https://github.com/rust-lang/cargo/pull/13709) +* RFC: [#3553](https://github.com/rust-lang/rfcs/pull/3553) + +The `sbom` build config allows to generate so-called SBOM pre-cursor files +alongside each compiled artifact. A Software Bill Of Material (SBOM) tool can +incorporate these generated files to collect important information from the cargo +build process that are difficult or impossible to obtain in another way. + +To enable this feature either set the `sbom` field in the `.cargo/config.toml` + +```toml +[unstable] +sbom = true + +[build] +sbom = true +``` + +or set the `CARGO_BUILD_SBOM` environment variable to `true`. The functionality +is available behind the flag `-Z sbom`. + +The generated output files are in JSON format and follow the naming scheme +`.cargo-sbom.json`. The JSON file contains information about dependencies, +target, features and the used `rustc` compiler. + +SBOM pre-cursor files are generated for all executable and linkable outputs +that are uplifted into the target or artifact directories. + +### Environment variables Cargo sets for crates + +* `CARGO_SBOM_PATH` -- a list of generated SBOM precursor files, separated by the platform PATH separator. The list can be split with `std::env::split_paths`. + +### SBOM pre-cursor schema + +```json5 +{ + // Schema version. + "version": 1, + // Index into the crates array for the root crate. + "root": 0, + // Array of all crates. There may be duplicates of the same crate if that + // crate is compiled differently (different opt-level, features, etc). + "crates": [ + { + // Package ID specification + "id": "path+file:///sample-package#0.1.0", + // List of target kinds: bin, lib, rlib, dylib, cdylib, staticlib, proc-macro, example, test, bench, custom-build + "kind": ["bin"], + // Enabled feature flags. + "features": [], + // Dependencies for this crate. + "dependencies": [ + { + // Index in to the crates array. + "index": 1, + // Dependency kind: + // Normal: A dependency linked to the artifact produced by this crate. + // Build: A compile-time dependency used to build this crate (build-script or proc-macro). + "kind": "normal" + }, + { + // A crate can depend on another crate with both normal and build edges. + "index": 1, + "kind": "build" + } + ] + }, + { + "id": "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.16", + "kind": ["bin"], + "features": [], + "dependencies": [] + } + ], + // Information about rustc used to perform the compilation. + "rustc": { + // Compiler version + "version": "1.86.0-nightly", + // Compiler wrapper + "wrapper": null, + // Compiler workspace wrapper + "workspace_wrapper": null, + // Commit hash for rustc + "commit_hash": "bef3c3b01f690de16738b1c9f36470fbfc6ac623", + // Host target triple + "host": "x86_64-pc-windows-msvc", + // Verbose version string: `rustc -vV` + "verbose_version": "rustc 1.86.0-nightly (bef3c3b01 2025-02-04)\nbinary: rustc\ncommit-hash: bef3c3b01f690de16738b1c9f36470fbfc6ac623\ncommit-date: 2025-02-04\nhost: x86_64-pc-windows-msvc\nrelease: 1.86.0-nightly\nLLVM version: 19.1.7\n" + } +} +``` + ## update-breaking * Tracking Issue: [#12425](https://github.com/rust-lang/cargo/issues/12425) diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index e0a828959fd..c7791176d58 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +