//! Utility module for interacting with the cargo-bazel lockfile. use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs; use std::path::Path; use std::process::Command; use anyhow::{bail, Context as AnyhowContext, Result}; use hex::ToHex; use serde::{Deserialize, Serialize}; use sha2::{Digest as Sha2Digest, Sha256}; use crate::config::Config; use crate::context::Context; use crate::metadata::Cargo; use crate::splicing::{SplicingManifest, SplicingMetadata}; pub(crate) fn lock_context( mut context: Context, config: &Config, splicing_manifest: &SplicingManifest, cargo_bin: &Cargo, rustc_bin: &Path, ) -> Result { // Ensure there is no existing checksum which could impact the lockfile results context.checksum = None; let checksum = Digest::new(&context, config, splicing_manifest, cargo_bin, rustc_bin) .context("Failed to generate context digest")?; Ok(Context { checksum: Some(checksum), ..context }) } /// Write a [crate::context::Context] to disk pub(crate) fn write_lockfile(lockfile: Context, path: &Path, dry_run: bool) -> Result<()> { let content = serde_json::to_string_pretty(&lockfile)?; if dry_run { println!("{content:#?}"); } else { // Ensure the parent directory exists if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(path, content + "\n") .context(format!("Failed to write file to disk: {}", path.display()))?; } Ok(()) } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)] pub(crate) struct Digest(String); impl Digest { pub(crate) fn new( context: &Context, config: &Config, splicing_manifest: &SplicingManifest, cargo_bin: &Cargo, rustc_bin: &Path, ) -> Result { let splicing_metadata = SplicingMetadata::try_from((*splicing_manifest).clone())?; let cargo_version = cargo_bin.full_version()?; let rustc_version = Self::bin_version(rustc_bin)?; let cargo_bazel_version = env!("CARGO_PKG_VERSION"); // Ensure the checksum of a digest is not present before computing one Ok(match context.checksum { Some(_) => Self::compute( &Context { checksum: None, ..context.clone() }, config, &splicing_metadata, cargo_bazel_version, &cargo_version, &rustc_version, ), None => Self::compute( context, config, &splicing_metadata, cargo_bazel_version, &cargo_version, &rustc_version, ), }) } /// A helper for generating a hash and logging it's contents. fn compute_single_hash(data: &str, id: &str) -> String { let mut hasher = Sha256::new(); hasher.update(data.as_bytes()); hasher.update(b"\0"); let hash = hasher.finalize().encode_hex::(); tracing::debug!("{} hash: {}", id, hash); hash } fn compute( context: &Context, config: &Config, splicing_metadata: &SplicingMetadata, cargo_bazel_version: &str, cargo_version: &str, rustc_version: &str, ) -> Self { // Since this method is private, it should be expected that context is // always None. This then allows us to have this method not return a // Result. debug_assert!(context.checksum.is_none()); let mut hasher = Sha256::new(); hasher.update(Digest::compute_single_hash( cargo_bazel_version, "cargo-bazel version", )); hasher.update(b"\0"); // The lockfile context (typically `cargo-bazel-lock.json`). hasher.update(Digest::compute_single_hash( &serde_json::to_string(context).unwrap(), "lockfile context", )); hasher.update(b"\0"); // This content is generated by various attributes in Bazel rules and written to a file behind the scenes. hasher.update(Digest::compute_single_hash( &serde_json::to_string(config).unwrap(), "workspace config", )); hasher.update(b"\0"); // Data collected about Cargo manifests and configs that feed into dependency generation. This file // is also generated by Bazel behind the scenes based on user inputs. hasher.update(Digest::compute_single_hash( &serde_json::to_string(splicing_metadata).unwrap(), "splicing manifest", )); hasher.update(b"\0"); hasher.update(Digest::compute_single_hash(cargo_version, "Cargo version")); hasher.update(b"\0"); hasher.update(Digest::compute_single_hash(rustc_version, "Rustc version")); hasher.update(b"\0"); let hash = hasher.finalize().encode_hex::(); tracing::debug!("Digest hash: {}", hash); Self(hash) } pub(crate) fn bin_version(binary: &Path) -> Result { let safe_vars = [OsStr::new("HOMEDRIVE"), OsStr::new("PATHEXT")]; let env = std::env::vars_os().filter(|(var, _)| safe_vars.contains(&var.as_os_str())); let output = Command::new(binary) .arg("--version") .env_clear() .envs(env) .output() .with_context(|| format!("Failed to run {} to get its version", binary.display()))?; if !output.status.success() { eprintln!("{}", String::from_utf8_lossy(&output.stdout)); eprintln!("{}", String::from_utf8_lossy(&output.stderr)); bail!("Failed to query cargo version") } let version = String::from_utf8(output.stdout)?.trim().to_owned(); // TODO: There is a bug in the linux binary for Cargo 1.60.0 where // the commit hash reported by the version is shorter than what's // reported on other platforms. This conditional here is a hack to // correct for this difference and ensure lockfile hashes can be // computed consistently. If a new binary is released then this // condition should be removed // https://github.com/rust-lang/cargo/issues/10547 let corrections = BTreeMap::from([ ( "cargo 1.60.0 (d1fd9fe 2022-03-01)", "cargo 1.60.0 (d1fd9fe2c 2022-03-01)", ), ( "cargo 1.61.0 (a028ae4 2022-04-29)", "cargo 1.61.0 (a028ae42f 2022-04-29)", ), ]); if corrections.contains_key(version.as_str()) { Ok(corrections[version.as_str()].to_string()) } else { Ok(version) } } } impl PartialEq for Digest { fn eq(&self, other: &str) -> bool { self.0 == other } } impl PartialEq for Digest { fn eq(&self, other: &String) -> bool { &self.0 == other } } #[cfg(test)] mod test { use crate::config::{CrateAnnotations, CrateNameAndVersionReq}; use crate::splicing::cargo_config::{AdditionalRegistry, CargoConfig, Registry}; use crate::utils::target_triple::TargetTriple; use super::*; use std::collections::BTreeSet; #[test] fn simple_digest() { let context = Context::default(); let config = Config::default(); let splicing_metadata = SplicingMetadata::default(); let digest = Digest::compute( &context, &config, &splicing_metadata, "0.1.0", "cargo 1.57.0 (b2e52d7ca 2021-10-21)", "rustc 1.57.0 (f1edd0429 2021-11-29)", ); assert_eq!( Digest("7f8d38b770a838797e24635a9030d4194210ff331f1a5b59c753f23fd197b5d8".to_owned()), digest, ); } #[test] fn digest_with_config() { let context = Context::default(); let config = Config { generate_binaries: false, generate_build_scripts: false, annotations: BTreeMap::from([( CrateNameAndVersionReq::new("rustonomicon".to_owned(), "1.0.0".parse().unwrap()), CrateAnnotations { compile_data_glob: Some(BTreeSet::from(["arts/**".to_owned()])), ..CrateAnnotations::default() }, )]), cargo_config: None, supported_platform_triples: BTreeSet::from([ TargetTriple::from_bazel("aarch64-apple-darwin".to_owned()), TargetTriple::from_bazel("aarch64-unknown-linux-gnu".to_owned()), TargetTriple::from_bazel("aarch64-pc-windows-msvc".to_owned()), TargetTriple::from_bazel("wasm32-unknown-unknown".to_owned()), TargetTriple::from_bazel("wasm32-wasi".to_owned()), TargetTriple::from_bazel("x86_64-apple-darwin".to_owned()), TargetTriple::from_bazel("x86_64-pc-windows-msvc".to_owned()), TargetTriple::from_bazel("x86_64-unknown-freebsd".to_owned()), TargetTriple::from_bazel("x86_64-unknown-linux-gnu".to_owned()), ]), ..Config::default() }; let splicing_metadata = SplicingMetadata::default(); let digest = Digest::compute( &context, &config, &splicing_metadata, "0.1.0", "cargo 1.57.0 (b2e52d7ca 2021-10-21)", "rustc 1.57.0 (f1edd0429 2021-11-29)", ); assert_eq!( Digest("610cbb406b7452d32ae31c45ec82cd3b3b1fb184c3411ef613c948d88492441b".to_owned()), digest, ); } #[test] fn digest_with_splicing_metadata() { let context = Context::default(); let config = Config::default(); let splicing_metadata = SplicingMetadata { direct_packages: BTreeMap::from([( "rustonomicon".to_owned(), cargo_toml::DependencyDetail { version: Some("1.0.0".to_owned()), ..cargo_toml::DependencyDetail::default() }, )]), manifests: BTreeMap::new(), cargo_config: None, }; let digest = Digest::compute( &context, &config, &splicing_metadata, "0.1.0", "cargo 1.57.0 (b2e52d7ca 2021-10-21)", "rustc 1.57.0 (f1edd0429 2021-11-29)", ); assert_eq!( Digest("e81dba9d36276baa8d491373fe09ef38e71e68c12f70e45b7c260ba2c48a87f5".to_owned()), digest, ); } #[test] fn digest_with_cargo_config() { let context = Context::default(); let config = Config::default(); let cargo_config = CargoConfig { registries: BTreeMap::from([ ( "art-crates-remote".to_owned(), AdditionalRegistry { index: "https://artprod.mycompany/artifactory/git/cargo-remote.git" .to_owned(), token: None, }, ), ( "crates-io".to_owned(), AdditionalRegistry { index: "https://github.com/rust-lang/crates.io-index".to_owned(), token: None, }, ), ]), registry: Registry { default: "art-crates-remote".to_owned(), token: None, }, source: BTreeMap::new(), }; let splicing_metadata = SplicingMetadata { cargo_config: Some(cargo_config), ..SplicingMetadata::default() }; let digest = Digest::compute( &context, &config, &splicing_metadata, "0.1.0", "cargo 1.57.0 (b2e52d7ca 2021-10-21)", "rustc 1.57.0 (f1edd0429 2021-11-29)", ); assert_eq!( Digest("f1b8ca07d35905bbd8bba79137ca7a02414b4ef01f28c459b78d1807ac3a8191".to_owned()), digest, ); } }