1 //! Utility module for interacting with the cargo-bazel lockfile.
2
3 use std::collections::BTreeMap;
4 use std::ffi::OsStr;
5 use std::fs;
6 use std::path::Path;
7 use std::process::Command;
8
9 use anyhow::{bail, Context as AnyhowContext, Result};
10 use hex::ToHex;
11 use serde::{Deserialize, Serialize};
12 use sha2::{Digest as Sha2Digest, Sha256};
13
14 use crate::config::Config;
15 use crate::context::Context;
16 use crate::metadata::Cargo;
17 use crate::splicing::{SplicingManifest, SplicingMetadata};
18
lock_context( mut context: Context, config: &Config, splicing_manifest: &SplicingManifest, cargo_bin: &Cargo, rustc_bin: &Path, ) -> Result<Context>19 pub(crate) fn lock_context(
20 mut context: Context,
21 config: &Config,
22 splicing_manifest: &SplicingManifest,
23 cargo_bin: &Cargo,
24 rustc_bin: &Path,
25 ) -> Result<Context> {
26 // Ensure there is no existing checksum which could impact the lockfile results
27 context.checksum = None;
28
29 let checksum = Digest::new(&context, config, splicing_manifest, cargo_bin, rustc_bin)
30 .context("Failed to generate context digest")?;
31
32 Ok(Context {
33 checksum: Some(checksum),
34 ..context
35 })
36 }
37
38 /// Write a [crate::context::Context] to disk
write_lockfile(lockfile: Context, path: &Path, dry_run: bool) -> Result<()>39 pub(crate) fn write_lockfile(lockfile: Context, path: &Path, dry_run: bool) -> Result<()> {
40 let content = serde_json::to_string_pretty(&lockfile)?;
41
42 if dry_run {
43 println!("{content:#?}");
44 } else {
45 // Ensure the parent directory exists
46 if let Some(parent) = path.parent() {
47 fs::create_dir_all(parent)?;
48 }
49 fs::write(path, content + "\n")
50 .context(format!("Failed to write file to disk: {}", path.display()))?;
51 }
52
53 Ok(())
54 }
55
56 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
57 pub(crate) struct Digest(String);
58
59 impl Digest {
new( context: &Context, config: &Config, splicing_manifest: &SplicingManifest, cargo_bin: &Cargo, rustc_bin: &Path, ) -> Result<Self>60 pub(crate) fn new(
61 context: &Context,
62 config: &Config,
63 splicing_manifest: &SplicingManifest,
64 cargo_bin: &Cargo,
65 rustc_bin: &Path,
66 ) -> Result<Self> {
67 let splicing_metadata = SplicingMetadata::try_from((*splicing_manifest).clone())?;
68 let cargo_version = cargo_bin.full_version()?;
69 let rustc_version = Self::bin_version(rustc_bin)?;
70 let cargo_bazel_version = env!("CARGO_PKG_VERSION");
71
72 // Ensure the checksum of a digest is not present before computing one
73 Ok(match context.checksum {
74 Some(_) => Self::compute(
75 &Context {
76 checksum: None,
77 ..context.clone()
78 },
79 config,
80 &splicing_metadata,
81 cargo_bazel_version,
82 &cargo_version,
83 &rustc_version,
84 ),
85 None => Self::compute(
86 context,
87 config,
88 &splicing_metadata,
89 cargo_bazel_version,
90 &cargo_version,
91 &rustc_version,
92 ),
93 })
94 }
95
96 /// A helper for generating a hash and logging it's contents.
compute_single_hash(data: &str, id: &str) -> String97 fn compute_single_hash(data: &str, id: &str) -> String {
98 let mut hasher = Sha256::new();
99 hasher.update(data.as_bytes());
100 hasher.update(b"\0");
101 let hash = hasher.finalize().encode_hex::<String>();
102 tracing::debug!("{} hash: {}", id, hash);
103 hash
104 }
105
compute( context: &Context, config: &Config, splicing_metadata: &SplicingMetadata, cargo_bazel_version: &str, cargo_version: &str, rustc_version: &str, ) -> Self106 fn compute(
107 context: &Context,
108 config: &Config,
109 splicing_metadata: &SplicingMetadata,
110 cargo_bazel_version: &str,
111 cargo_version: &str,
112 rustc_version: &str,
113 ) -> Self {
114 // Since this method is private, it should be expected that context is
115 // always None. This then allows us to have this method not return a
116 // Result.
117 debug_assert!(context.checksum.is_none());
118
119 let mut hasher = Sha256::new();
120
121 hasher.update(Digest::compute_single_hash(
122 cargo_bazel_version,
123 "cargo-bazel version",
124 ));
125 hasher.update(b"\0");
126
127 // The lockfile context (typically `cargo-bazel-lock.json`).
128 hasher.update(Digest::compute_single_hash(
129 &serde_json::to_string(context).unwrap(),
130 "lockfile context",
131 ));
132 hasher.update(b"\0");
133
134 // This content is generated by various attributes in Bazel rules and written to a file behind the scenes.
135 hasher.update(Digest::compute_single_hash(
136 &serde_json::to_string(config).unwrap(),
137 "workspace config",
138 ));
139 hasher.update(b"\0");
140
141 // Data collected about Cargo manifests and configs that feed into dependency generation. This file
142 // is also generated by Bazel behind the scenes based on user inputs.
143 hasher.update(Digest::compute_single_hash(
144 &serde_json::to_string(splicing_metadata).unwrap(),
145 "splicing manifest",
146 ));
147 hasher.update(b"\0");
148
149 hasher.update(Digest::compute_single_hash(cargo_version, "Cargo version"));
150 hasher.update(b"\0");
151
152 hasher.update(Digest::compute_single_hash(rustc_version, "Rustc version"));
153 hasher.update(b"\0");
154
155 let hash = hasher.finalize().encode_hex::<String>();
156 tracing::debug!("Digest hash: {}", hash);
157
158 Self(hash)
159 }
160
bin_version(binary: &Path) -> Result<String>161 pub(crate) fn bin_version(binary: &Path) -> Result<String> {
162 let safe_vars = [OsStr::new("HOMEDRIVE"), OsStr::new("PATHEXT")];
163 let env = std::env::vars_os().filter(|(var, _)| safe_vars.contains(&var.as_os_str()));
164
165 let output = Command::new(binary)
166 .arg("--version")
167 .env_clear()
168 .envs(env)
169 .output()
170 .with_context(|| format!("Failed to run {} to get its version", binary.display()))?;
171
172 if !output.status.success() {
173 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
174 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
175 bail!("Failed to query cargo version")
176 }
177
178 let version = String::from_utf8(output.stdout)?.trim().to_owned();
179
180 // TODO: There is a bug in the linux binary for Cargo 1.60.0 where
181 // the commit hash reported by the version is shorter than what's
182 // reported on other platforms. This conditional here is a hack to
183 // correct for this difference and ensure lockfile hashes can be
184 // computed consistently. If a new binary is released then this
185 // condition should be removed
186 // https://github.com/rust-lang/cargo/issues/10547
187 let corrections = BTreeMap::from([
188 (
189 "cargo 1.60.0 (d1fd9fe 2022-03-01)",
190 "cargo 1.60.0 (d1fd9fe2c 2022-03-01)",
191 ),
192 (
193 "cargo 1.61.0 (a028ae4 2022-04-29)",
194 "cargo 1.61.0 (a028ae42f 2022-04-29)",
195 ),
196 ]);
197
198 if corrections.contains_key(version.as_str()) {
199 Ok(corrections[version.as_str()].to_string())
200 } else {
201 Ok(version)
202 }
203 }
204 }
205
206 impl PartialEq<str> for Digest {
eq(&self, other: &str) -> bool207 fn eq(&self, other: &str) -> bool {
208 self.0 == other
209 }
210 }
211
212 impl PartialEq<String> for Digest {
eq(&self, other: &String) -> bool213 fn eq(&self, other: &String) -> bool {
214 &self.0 == other
215 }
216 }
217
218 #[cfg(test)]
219 mod test {
220 use crate::config::{CrateAnnotations, CrateNameAndVersionReq};
221 use crate::splicing::cargo_config::{AdditionalRegistry, CargoConfig, Registry};
222 use crate::utils::target_triple::TargetTriple;
223
224 use super::*;
225
226 use std::collections::BTreeSet;
227
228 #[test]
simple_digest()229 fn simple_digest() {
230 let context = Context::default();
231 let config = Config::default();
232 let splicing_metadata = SplicingMetadata::default();
233
234 let digest = Digest::compute(
235 &context,
236 &config,
237 &splicing_metadata,
238 "0.1.0",
239 "cargo 1.57.0 (b2e52d7ca 2021-10-21)",
240 "rustc 1.57.0 (f1edd0429 2021-11-29)",
241 );
242
243 assert_eq!(
244 Digest("7f8d38b770a838797e24635a9030d4194210ff331f1a5b59c753f23fd197b5d8".to_owned()),
245 digest,
246 );
247 }
248
249 #[test]
digest_with_config()250 fn digest_with_config() {
251 let context = Context::default();
252 let config = Config {
253 generate_binaries: false,
254 generate_build_scripts: false,
255 annotations: BTreeMap::from([(
256 CrateNameAndVersionReq::new("rustonomicon".to_owned(), "1.0.0".parse().unwrap()),
257 CrateAnnotations {
258 compile_data_glob: Some(BTreeSet::from(["arts/**".to_owned()])),
259 ..CrateAnnotations::default()
260 },
261 )]),
262 cargo_config: None,
263 supported_platform_triples: BTreeSet::from([
264 TargetTriple::from_bazel("aarch64-apple-darwin".to_owned()),
265 TargetTriple::from_bazel("aarch64-unknown-linux-gnu".to_owned()),
266 TargetTriple::from_bazel("aarch64-pc-windows-msvc".to_owned()),
267 TargetTriple::from_bazel("wasm32-unknown-unknown".to_owned()),
268 TargetTriple::from_bazel("wasm32-wasi".to_owned()),
269 TargetTriple::from_bazel("x86_64-apple-darwin".to_owned()),
270 TargetTriple::from_bazel("x86_64-pc-windows-msvc".to_owned()),
271 TargetTriple::from_bazel("x86_64-unknown-freebsd".to_owned()),
272 TargetTriple::from_bazel("x86_64-unknown-linux-gnu".to_owned()),
273 ]),
274 ..Config::default()
275 };
276
277 let splicing_metadata = SplicingMetadata::default();
278
279 let digest = Digest::compute(
280 &context,
281 &config,
282 &splicing_metadata,
283 "0.1.0",
284 "cargo 1.57.0 (b2e52d7ca 2021-10-21)",
285 "rustc 1.57.0 (f1edd0429 2021-11-29)",
286 );
287
288 assert_eq!(
289 Digest("610cbb406b7452d32ae31c45ec82cd3b3b1fb184c3411ef613c948d88492441b".to_owned()),
290 digest,
291 );
292 }
293
294 #[test]
digest_with_splicing_metadata()295 fn digest_with_splicing_metadata() {
296 let context = Context::default();
297 let config = Config::default();
298 let splicing_metadata = SplicingMetadata {
299 direct_packages: BTreeMap::from([(
300 "rustonomicon".to_owned(),
301 cargo_toml::DependencyDetail {
302 version: Some("1.0.0".to_owned()),
303 ..cargo_toml::DependencyDetail::default()
304 },
305 )]),
306 manifests: BTreeMap::new(),
307 cargo_config: None,
308 };
309
310 let digest = Digest::compute(
311 &context,
312 &config,
313 &splicing_metadata,
314 "0.1.0",
315 "cargo 1.57.0 (b2e52d7ca 2021-10-21)",
316 "rustc 1.57.0 (f1edd0429 2021-11-29)",
317 );
318
319 assert_eq!(
320 Digest("e81dba9d36276baa8d491373fe09ef38e71e68c12f70e45b7c260ba2c48a87f5".to_owned()),
321 digest,
322 );
323 }
324
325 #[test]
digest_with_cargo_config()326 fn digest_with_cargo_config() {
327 let context = Context::default();
328 let config = Config::default();
329 let cargo_config = CargoConfig {
330 registries: BTreeMap::from([
331 (
332 "art-crates-remote".to_owned(),
333 AdditionalRegistry {
334 index: "https://artprod.mycompany/artifactory/git/cargo-remote.git"
335 .to_owned(),
336 token: None,
337 },
338 ),
339 (
340 "crates-io".to_owned(),
341 AdditionalRegistry {
342 index: "https://github.com/rust-lang/crates.io-index".to_owned(),
343 token: None,
344 },
345 ),
346 ]),
347 registry: Registry {
348 default: "art-crates-remote".to_owned(),
349 token: None,
350 },
351 source: BTreeMap::new(),
352 };
353
354 let splicing_metadata = SplicingMetadata {
355 cargo_config: Some(cargo_config),
356 ..SplicingMetadata::default()
357 };
358
359 let digest = Digest::compute(
360 &context,
361 &config,
362 &splicing_metadata,
363 "0.1.0",
364 "cargo 1.57.0 (b2e52d7ca 2021-10-21)",
365 "rustc 1.57.0 (f1edd0429 2021-11-29)",
366 );
367
368 assert_eq!(
369 Digest("f1b8ca07d35905bbd8bba79137ca7a02414b4ef01f28c459b78d1807ac3a8191".to_owned()),
370 digest,
371 );
372 }
373 }
374