xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/src/lockfile.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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