| //! 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<Context> { |
| // 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<Self> { |
| 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::<String>(); |
| 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::<String>(); |
| tracing::debug!("Digest hash: {}", hash); |
| |
| Self(hash) |
| } |
| |
| pub(crate) fn bin_version(binary: &Path) -> Result<String> { |
| 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<str> for Digest { |
| fn eq(&self, other: &str) -> bool { |
| self.0 == other |
| } |
| } |
| |
| impl PartialEq<String> 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("5e0fd9106767c43deb77bb1024ca24e99685110a1085c03abc028c75e180831f".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, |
| ); |
| } |
| } |