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