blob: 5d8e6d6f7d80fcee27d7cd979c984705d7398ded [file] [log] [blame]
//! 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,
);
}
}