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