• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Tools for gathering various kinds of metadata (Cargo.lock, Cargo metadata, Crate Index info).
2 
3 mod dependency;
4 mod metadata_annotation;
5 
6 use std::collections::{BTreeMap, BTreeSet};
7 use std::env;
8 use std::fs;
9 use std::io::BufRead;
10 use std::path::{Path, PathBuf};
11 use std::process::Command;
12 use std::str::FromStr;
13 use std::sync::{Arc, Mutex};
14 
15 use anyhow::{anyhow, bail, Context, Result};
16 use cargo_lock::Lockfile as CargoLockfile;
17 use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand};
18 use semver::Version;
19 use tracing::debug;
20 
21 use crate::config::CrateId;
22 use crate::lockfile::Digest;
23 use crate::select::Select;
24 use crate::utils::target_triple::TargetTriple;
25 
26 pub(crate) use self::dependency::*;
27 pub(crate) use self::metadata_annotation::*;
28 
29 // TODO: This should also return a set of [crate-index::IndexConfig]s for packages in metadata.packages
30 /// A Trait for generating metadata (`cargo metadata` output and a lock file) from a Cargo manifest.
31 pub(crate) trait MetadataGenerator {
generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>32     fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>;
33 }
34 
35 /// Generates Cargo metadata and a lockfile from a provided manifest.
36 pub(crate) struct Generator {
37     /// The path to a `cargo` binary
38     cargo_bin: Cargo,
39 
40     /// The path to a `rustc` binary
41     rustc_bin: PathBuf,
42 }
43 
44 impl Generator {
new() -> Self45     pub(crate) fn new() -> Self {
46         Generator {
47             cargo_bin: Cargo::new(PathBuf::from(
48                 env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()),
49             )),
50             rustc_bin: PathBuf::from(env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string())),
51         }
52     }
53 
with_cargo(mut self, cargo_bin: Cargo) -> Self54     pub(crate) fn with_cargo(mut self, cargo_bin: Cargo) -> Self {
55         self.cargo_bin = cargo_bin;
56         self
57     }
58 
with_rustc(mut self, rustc_bin: PathBuf) -> Self59     pub(crate) fn with_rustc(mut self, rustc_bin: PathBuf) -> Self {
60         self.rustc_bin = rustc_bin;
61         self
62     }
63 }
64 
65 impl MetadataGenerator for Generator {
generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>66     fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)> {
67         let manifest_dir = manifest_path
68             .as_ref()
69             .parent()
70             .expect("The manifest should have a parent directory");
71         let lockfile = {
72             let lock_path = manifest_dir.join("Cargo.lock");
73             if !lock_path.exists() {
74                 bail!("No `Cargo.lock` file was found with the given manifest")
75             }
76             cargo_lock::Lockfile::load(lock_path)?
77         };
78 
79         let mut other_options = vec!["--locked".to_owned()];
80         if self.cargo_bin.is_nightly()? {
81             other_options.push("-Zbindeps".to_owned());
82         }
83 
84         let metadata = self
85             .cargo_bin
86             .metadata_command()?
87             .current_dir(manifest_dir)
88             .manifest_path(manifest_path.as_ref())
89             .other_options(other_options)
90             .exec()?;
91 
92         Ok((metadata, lockfile))
93     }
94 }
95 
96 /// Cargo encapsulates a path to a `cargo` binary.
97 /// Any invocations of `cargo` (either as a `std::process::Command` or via `cargo_metadata`) should
98 /// go via this wrapper to ensure that any environment variables needed are set appropriately.
99 #[derive(Debug, Clone)]
100 pub(crate) struct Cargo {
101     path: PathBuf,
102     full_version: Arc<Mutex<Option<String>>>,
103 }
104 
105 impl Cargo {
new(path: PathBuf) -> Cargo106     pub(crate) fn new(path: PathBuf) -> Cargo {
107         Cargo {
108             path,
109             full_version: Arc::new(Mutex::new(None)),
110         }
111     }
112 
113     /// Returns a new `Command` for running this cargo.
command(&self) -> Result<Command>114     pub(crate) fn command(&self) -> Result<Command> {
115         let mut command = Command::new(&self.path);
116         command.envs(self.env()?);
117         if self.is_nightly()? {
118             command.arg("-Zbindeps");
119         }
120         Ok(command)
121     }
122 
123     /// Returns a new `MetadataCommand` using this cargo.
metadata_command(&self) -> Result<MetadataCommand>124     pub(crate) fn metadata_command(&self) -> Result<MetadataCommand> {
125         let mut command = MetadataCommand::new();
126         command.cargo_path(&self.path);
127         for (k, v) in self.env()? {
128             command.env(k, v);
129         }
130         Ok(command)
131     }
132 
133     /// Returns the output of running `cargo version`, trimming any leading or trailing whitespace.
134     /// This function performs normalisation to work around `<https://github.com/rust-lang/cargo/issues/10547>`
full_version(&self) -> Result<String>135     pub(crate) fn full_version(&self) -> Result<String> {
136         let mut full_version = self.full_version.lock().unwrap();
137         if full_version.is_none() {
138             let observed_version = Digest::bin_version(&self.path)?;
139             *full_version = Some(observed_version);
140         }
141         Ok(full_version.clone().unwrap())
142     }
143 
is_nightly(&self) -> Result<bool>144     pub(crate) fn is_nightly(&self) -> Result<bool> {
145         let full_version = self.full_version()?;
146         let version_str = full_version.split(' ').nth(1);
147         if let Some(version_str) = version_str {
148             let version = Version::parse(version_str).context("Failed to parse cargo version")?;
149             return Ok(version.pre.as_str() == "nightly");
150         }
151         bail!("Couldn't parse cargo version");
152     }
153 
use_sparse_registries_for_crates_io(&self) -> Result<bool>154     pub(crate) fn use_sparse_registries_for_crates_io(&self) -> Result<bool> {
155         let full_version = self.full_version()?;
156         let version_str = full_version.split(' ').nth(1);
157         if let Some(version_str) = version_str {
158             let version = Version::parse(version_str).context("Failed to parse cargo version")?;
159             return Ok(version.major >= 1 && version.minor >= 68);
160         }
161         bail!("Couldn't parse cargo version");
162     }
163 
164     /// Determine if Cargo is expected to be using the new package_id spec. For
165     /// details see <https://github.com/rust-lang/cargo/pull/13311>
166     #[cfg(test)]
uses_new_package_id_format(&self) -> Result<bool>167     pub(crate) fn uses_new_package_id_format(&self) -> Result<bool> {
168         let full_version = self.full_version()?;
169         let version_str = full_version.split(' ').nth(1);
170         if let Some(version_str) = version_str {
171             let version = Version::parse(version_str).context("Failed to parse cargo version")?;
172             return Ok(version.major >= 1 && version.minor >= 78);
173         }
174         bail!("Couldn't parse cargo version");
175     }
176 
env(&self) -> Result<BTreeMap<String, String>>177     fn env(&self) -> Result<BTreeMap<String, String>> {
178         let mut map = BTreeMap::new();
179 
180         if self.use_sparse_registries_for_crates_io()? {
181             map.insert(
182                 "CARGO_REGISTRIES_CRATES_IO_PROTOCOL".into(),
183                 "sparse".into(),
184             );
185         }
186         Ok(map)
187     }
188 }
189 
190 /// A configuration describing how to invoke [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html).
191 #[derive(Debug, Clone, PartialEq, Eq)]
192 pub enum CargoUpdateRequest {
193     /// Translates to an unrestricted `cargo update` command
194     Eager,
195 
196     /// Translates to `cargo update --workspace`
197     Workspace,
198 
199     /// Translates to `cargo update --package foo` with an optional `--precise` argument.
200     Package {
201         /// The name of the crate used with `--package`.
202         name: String,
203 
204         /// If set, the `--precise` value that pairs with `--package`.
205         version: Option<String>,
206     },
207 }
208 
209 impl FromStr for CargoUpdateRequest {
210     type Err = anyhow::Error;
211 
from_str(s: &str) -> Result<Self, Self::Err>212     fn from_str(s: &str) -> Result<Self, Self::Err> {
213         let lower = s.to_lowercase();
214 
215         if ["eager", "full", "all"].contains(&lower.as_str()) {
216             return Ok(Self::Eager);
217         }
218 
219         if ["1", "yes", "true", "on", "workspace", "minimal"].contains(&lower.as_str()) {
220             return Ok(Self::Workspace);
221         }
222 
223         let mut split = s.splitn(2, '=');
224         Ok(Self::Package {
225             name: split.next().map(|s| s.to_owned()).unwrap(),
226             version: split.next().map(|s| s.to_owned()),
227         })
228     }
229 }
230 
231 impl CargoUpdateRequest {
232     /// Determine what arguments to pass to the `cargo update` command.
get_update_args(&self) -> Vec<String>233     fn get_update_args(&self) -> Vec<String> {
234         match self {
235             CargoUpdateRequest::Eager => Vec::new(),
236             CargoUpdateRequest::Workspace => vec!["--workspace".to_owned()],
237             CargoUpdateRequest::Package { name, version } => {
238                 let mut update_args = vec!["--package".to_owned(), name.clone()];
239 
240                 if let Some(version) = version {
241                     update_args.push("--precise".to_owned());
242                     update_args.push(version.clone());
243                 }
244 
245                 update_args
246             }
247         }
248     }
249 
250     /// Calls `cargo update` with arguments specific to the state of the current variant.
update( &self, manifest: &Path, cargo_bin: &Cargo, rustc_bin: &Path, ) -> Result<()>251     pub(crate) fn update(
252         &self,
253         manifest: &Path,
254         cargo_bin: &Cargo,
255         rustc_bin: &Path,
256     ) -> Result<()> {
257         let manifest_dir = manifest.parent().unwrap();
258 
259         // Simply invoke `cargo update`
260         let output = cargo_bin
261             .command()?
262             // Cargo detects config files based on `pwd` when running so
263             // to ensure user provided Cargo config files are used, it's
264             // critical to set the working directory to the manifest dir.
265             .current_dir(manifest_dir)
266             .arg("update")
267             .arg("--manifest-path")
268             .arg(manifest)
269             .args(self.get_update_args())
270             .env("RUSTC", rustc_bin)
271             .output()
272             .with_context(|| {
273                 format!(
274                     "Error running cargo to update packages for manifest '{}'",
275                     manifest.display()
276                 )
277             })?;
278 
279         if !output.status.success() {
280             eprintln!("{}", String::from_utf8_lossy(&output.stdout));
281             eprintln!("{}", String::from_utf8_lossy(&output.stderr));
282             bail!(format!("Failed to update lockfile: {}", output.status))
283         }
284 
285         Ok(())
286     }
287 }
288 
289 pub(crate) struct LockGenerator {
290     /// The path to a `cargo` binary
291     cargo_bin: Cargo,
292 
293     /// The path to a `rustc` binary
294     rustc_bin: PathBuf,
295 }
296 
297 impl LockGenerator {
new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self298     pub(crate) fn new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self {
299         Self {
300             cargo_bin,
301             rustc_bin,
302         }
303     }
304 
305     #[tracing::instrument(name = "LockGenerator::generate", skip_all)]
generate( &self, manifest_path: &Path, existing_lock: &Option<PathBuf>, update_request: &Option<CargoUpdateRequest>, ) -> Result<cargo_lock::Lockfile>306     pub(crate) fn generate(
307         &self,
308         manifest_path: &Path,
309         existing_lock: &Option<PathBuf>,
310         update_request: &Option<CargoUpdateRequest>,
311     ) -> Result<cargo_lock::Lockfile> {
312         debug!("Generating Cargo Lockfile for {}", manifest_path.display());
313 
314         let manifest_dir = manifest_path.parent().unwrap();
315         let generated_lockfile_path = manifest_dir.join("Cargo.lock");
316 
317         if let Some(lock) = existing_lock {
318             debug!("Using existing lock {}", lock.display());
319             if !lock.exists() {
320                 bail!(
321                     "An existing lockfile path was provided but a file at '{}' does not exist",
322                     lock.display()
323                 )
324             }
325 
326             // Install the file into the target location
327             if generated_lockfile_path.exists() {
328                 fs::remove_file(&generated_lockfile_path)?;
329             }
330             fs::copy(lock, &generated_lockfile_path)?;
331 
332             if let Some(request) = update_request {
333                 request.update(manifest_path, &self.cargo_bin, &self.rustc_bin)?;
334             }
335 
336             // Ensure the Cargo cache is up to date to simulate the behavior
337             // of having just generated a new one
338             let output = self
339                 .cargo_bin
340                 .command()?
341                 // Cargo detects config files based on `pwd` when running so
342                 // to ensure user provided Cargo config files are used, it's
343                 // critical to set the working directory to the manifest dir.
344                 .current_dir(manifest_dir)
345                 .arg("fetch")
346                 .arg("--locked")
347                 .arg("--manifest-path")
348                 .arg(manifest_path)
349                 .env("RUSTC", &self.rustc_bin)
350                 .output()
351                 .context(format!(
352                     "Error running cargo to fetch crates '{}'",
353                     manifest_path.display()
354                 ))?;
355 
356             if !output.status.success() {
357                 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
358                 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
359                 bail!(format!(
360                     "Failed to fetch crates for lockfile: {}",
361                     output.status
362                 ))
363             }
364         } else {
365             debug!("Generating new lockfile");
366             // Simply invoke `cargo generate-lockfile`
367             let output = self
368                 .cargo_bin
369                 .command()?
370                 // Cargo detects config files based on `pwd` when running so
371                 // to ensure user provided Cargo config files are used, it's
372                 // critical to set the working directory to the manifest dir.
373                 .current_dir(manifest_dir)
374                 .arg("generate-lockfile")
375                 .arg("--manifest-path")
376                 .arg(manifest_path)
377                 .env("RUSTC", &self.rustc_bin)
378                 .output()
379                 .context(format!(
380                     "Error running cargo to generate lockfile '{}'",
381                     manifest_path.display()
382                 ))?;
383 
384             if !output.status.success() {
385                 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
386                 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
387                 bail!(format!("Failed to generate lockfile: {}", output.status))
388             }
389         }
390 
391         cargo_lock::Lockfile::load(&generated_lockfile_path).context(format!(
392             "Failed to load lockfile: {}",
393             generated_lockfile_path.display()
394         ))
395     }
396 }
397 
398 /// A generator which runs `cargo vendor` on a given manifest
399 pub(crate) struct VendorGenerator {
400     /// The path to a `cargo` binary
401     cargo_bin: Cargo,
402 
403     /// The path to a `rustc` binary
404     rustc_bin: PathBuf,
405 }
406 
407 impl VendorGenerator {
new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self408     pub(crate) fn new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self {
409         Self {
410             cargo_bin,
411             rustc_bin,
412         }
413     }
414     #[tracing::instrument(name = "VendorGenerator::generate", skip_all)]
generate(&self, manifest_path: &Path, output_dir: &Path) -> Result<()>415     pub(crate) fn generate(&self, manifest_path: &Path, output_dir: &Path) -> Result<()> {
416         debug!(
417             "Vendoring {} to {}",
418             manifest_path.display(),
419             output_dir.display()
420         );
421         let manifest_dir = manifest_path.parent().unwrap();
422 
423         // Simply invoke `cargo generate-lockfile`
424         let output = self
425             .cargo_bin
426             .command()?
427             // Cargo detects config files based on `pwd` when running so
428             // to ensure user provided Cargo config files are used, it's
429             // critical to set the working directory to the manifest dir.
430             .current_dir(manifest_dir)
431             .arg("vendor")
432             .arg("--manifest-path")
433             .arg(manifest_path)
434             .arg("--locked")
435             .arg("--versioned-dirs")
436             .arg(output_dir)
437             .env("RUSTC", &self.rustc_bin)
438             .output()
439             .with_context(|| {
440                 format!(
441                     "Error running cargo to vendor sources for manifest '{}'",
442                     manifest_path.display()
443                 )
444             })?;
445 
446         if !output.status.success() {
447             eprintln!("{}", String::from_utf8_lossy(&output.stdout));
448             eprintln!("{}", String::from_utf8_lossy(&output.stderr));
449             bail!(format!("Failed to vendor sources with: {}", output.status))
450         }
451 
452         debug!("Done");
453         Ok(())
454     }
455 }
456 
457 /// A generate which computes per-platform feature sets.
458 pub(crate) struct FeatureGenerator {
459     /// The path to a `cargo` binary
460     cargo_bin: Cargo,
461 
462     /// The path to a `rustc` binary
463     rustc_bin: PathBuf,
464 }
465 
466 impl FeatureGenerator {
new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self467     pub(crate) fn new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self {
468         Self {
469             cargo_bin,
470             rustc_bin,
471         }
472     }
473 
474     /// Computes the set of enabled features for each target triplet for each crate.
475     #[tracing::instrument(name = "FeatureGenerator::generate", skip_all)]
generate( &self, manifest_path: &Path, target_triples: &BTreeSet<TargetTriple>, ) -> Result<BTreeMap<CrateId, Select<BTreeSet<String>>>>476     pub(crate) fn generate(
477         &self,
478         manifest_path: &Path,
479         target_triples: &BTreeSet<TargetTriple>,
480     ) -> Result<BTreeMap<CrateId, Select<BTreeSet<String>>>> {
481         debug!(
482             "Generating features for manifest {}",
483             manifest_path.display()
484         );
485 
486         let manifest_dir = manifest_path.parent().unwrap();
487         let mut target_triple_to_child = BTreeMap::new();
488         debug!("Spawning processes for {:?}", target_triples);
489         for target_triple in target_triples {
490             // We use `cargo tree` here because `cargo metadata` doesn't report
491             // back target-specific features (enabled with `resolver = "2"`).
492             // This is unfortunately a bit of a hack. See:
493             // - https://github.com/rust-lang/cargo/issues/9863
494             // - https://github.com/bazelbuild/rules_rust/issues/1662
495             let output = self
496                 .cargo_bin
497                 .command()?
498                 .current_dir(manifest_dir)
499                 .arg("tree")
500                 .arg("--locked")
501                 .arg("--manifest-path")
502                 .arg(manifest_path)
503                 .arg("--prefix=none")
504                 // https://doc.rust-lang.org/cargo/commands/cargo-tree.html#tree-formatting-options
505                 .arg("--format=|{p}|{f}|")
506                 .arg("--color=never")
507                 .arg("--workspace")
508                 .arg("--target")
509                 .arg(target_triple.to_cargo())
510                 .env("RUSTC", &self.rustc_bin)
511                 .stdout(std::process::Stdio::piped())
512                 .stderr(std::process::Stdio::piped())
513                 .spawn()
514                 .with_context(|| {
515                     format!(
516                         "Error spawning cargo in child process to compute features for target '{}', manifest path '{}'",
517                         target_triple,
518                         manifest_path.display()
519                     )
520                 })?;
521             target_triple_to_child.insert(target_triple, output);
522         }
523         let mut crate_features =
524             BTreeMap::<CrateId, BTreeMap<TargetTriple, BTreeSet<String>>>::new();
525         for (target_triple, child) in target_triple_to_child.into_iter() {
526             let output = child
527                 .wait_with_output()
528                 .with_context(|| {
529                     format!(
530                         "Error running cargo in child process to compute features for target '{}', manifest path '{}'",
531                         target_triple,
532                         manifest_path.display()
533                     )
534                 })?;
535             if !output.status.success() {
536                 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
537                 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
538                 bail!(format!("Failed to run cargo tree: {}", output.status))
539             }
540             debug!("Process complete for {}", target_triple);
541             for (crate_id, features) in
542                 parse_features_from_cargo_tree_output(output.stdout.lines())?
543             {
544                 debug!("\tFor {} features were: {:?}", crate_id, features);
545                 crate_features
546                     .entry(crate_id)
547                     .or_default()
548                     .insert(target_triple.clone(), features);
549             }
550         }
551         let mut result = BTreeMap::<CrateId, Select<BTreeSet<String>>>::new();
552         for (crate_id, features) in crate_features.into_iter() {
553             let common = features
554                 .iter()
555                 .fold(
556                     None,
557                     |common: Option<BTreeSet<String>>, (_, features)| match common {
558                         Some(common) => Some(common.intersection(features).cloned().collect()),
559                         None => Some(features.clone()),
560                     },
561                 )
562                 .unwrap_or_default();
563             let mut select: Select<BTreeSet<String>> = Select::default();
564             for (target_triple, fs) in features {
565                 if fs != common {
566                     for f in fs {
567                         select.insert(f, Some(target_triple.to_bazel()));
568                     }
569                 }
570             }
571             for f in common {
572                 select.insert(f, None);
573             }
574             result.insert(crate_id, select);
575         }
576         Ok(result)
577     }
578 }
579 
580 /// Parses the output of `cargo tree --format=|{p}|{f}|`. Other flags may be
581 /// passed to `cargo tree` as well, but this format is critical.
parse_features_from_cargo_tree_output<I, S, E>( lines: I, ) -> Result<BTreeMap<CrateId, BTreeSet<String>>> where I: Iterator<Item = std::result::Result<S, E>>, S: AsRef<str>, E: std::error::Error + Sync + Send + 'static,582 fn parse_features_from_cargo_tree_output<I, S, E>(
583     lines: I,
584 ) -> Result<BTreeMap<CrateId, BTreeSet<String>>>
585 where
586     I: Iterator<Item = std::result::Result<S, E>>,
587     S: AsRef<str>,
588     E: std::error::Error + Sync + Send + 'static,
589 {
590     let mut crate_features = BTreeMap::<CrateId, BTreeSet<String>>::new();
591     for line in lines {
592         let line = line?;
593         let line = line.as_ref();
594         if line.is_empty() {
595             continue;
596         }
597         let parts = line.split('|').collect::<Vec<_>>();
598         if parts.len() != 4 {
599             bail!("Unexpected line '{}'", line);
600         }
601         // We expect the crate id (parts[1]) to be either
602         // "<crate name> v<crate version>" or
603         // "<crate name> v<crate version> (<path>)"
604         // "<crate name> v<crate version> (proc-macro) (<path>)"
605         // https://github.com/rust-lang/cargo/blob/19f952f160d4f750d1e12fad2bf45e995719673d/src/cargo/ops/tree/mod.rs#L281
606         let crate_id_parts = parts[1].split(' ').collect::<Vec<_>>();
607         if crate_id_parts.len() < 2 && crate_id_parts.len() > 4 {
608             bail!(
609                 "Unexpected crate id format '{}' when parsing 'cargo tree' output.",
610                 parts[1]
611             );
612         }
613         let version_str = crate_id_parts[1].strip_prefix('v').ok_or_else(|| {
614             anyhow!(
615                 "Unexpected crate version '{}' when parsing 'cargo tree' output.",
616                 crate_id_parts[1]
617             )
618         })?;
619         let version = Version::parse(version_str).context("Failed to parse version")?;
620         let crate_id = CrateId::new(crate_id_parts[0].to_owned(), version);
621         let mut features = if parts[2].is_empty() {
622             BTreeSet::new()
623         } else {
624             parts[2].split(',').map(str::to_owned).collect()
625         };
626         crate_features
627             .entry(crate_id)
628             .or_default()
629             .append(&mut features);
630     }
631     Ok(crate_features)
632 }
633 
634 /// A helper function for writing Cargo metadata to a file.
write_metadata(path: &Path, metadata: &cargo_metadata::Metadata) -> Result<()>635 pub(crate) fn write_metadata(path: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
636     let content =
637         serde_json::to_string_pretty(metadata).context("Failed to serialize Cargo Metadata")?;
638 
639     fs::write(path, content).context("Failed to write metadata to disk")
640 }
641 
642 /// A helper function for deserializing Cargo metadata and lockfiles
load_metadata( metadata_path: &Path, ) -> Result<(cargo_metadata::Metadata, cargo_lock::Lockfile)>643 pub(crate) fn load_metadata(
644     metadata_path: &Path,
645 ) -> Result<(cargo_metadata::Metadata, cargo_lock::Lockfile)> {
646     // Locate the Cargo.lock file related to the metadata file.
647     let lockfile_path = metadata_path
648         .parent()
649         .expect("metadata files should always have parents")
650         .join("Cargo.lock");
651     if !lockfile_path.exists() {
652         bail!(
653             "The metadata file at {} is not next to a `Cargo.lock` file.",
654             metadata_path.display()
655         )
656     }
657 
658     let content = fs::read_to_string(metadata_path)
659         .with_context(|| format!("Failed to load Cargo Metadata: {}", metadata_path.display()))?;
660 
661     let metadata =
662         serde_json::from_str(&content).context("Unable to deserialize Cargo metadata")?;
663 
664     let lockfile = cargo_lock::Lockfile::load(&lockfile_path)
665         .with_context(|| format!("Failed to load lockfile: {}", lockfile_path.display()))?;
666 
667     Ok((metadata, lockfile))
668 }
669 
670 #[cfg(test)]
671 mod test {
672     use super::*;
673 
674     #[test]
deserialize_cargo_update_request_for_eager()675     fn deserialize_cargo_update_request_for_eager() {
676         for value in ["all", "full", "eager"] {
677             let request = CargoUpdateRequest::from_str(value).unwrap();
678 
679             assert_eq!(request, CargoUpdateRequest::Eager);
680         }
681     }
682 
683     #[test]
deserialize_cargo_update_request_for_workspace()684     fn deserialize_cargo_update_request_for_workspace() {
685         for value in ["1", "true", "yes", "on", "workspace", "minimal"] {
686             let request = CargoUpdateRequest::from_str(value).unwrap();
687 
688             assert_eq!(request, CargoUpdateRequest::Workspace);
689         }
690     }
691 
692     #[test]
deserialize_cargo_update_request_for_package()693     fn deserialize_cargo_update_request_for_package() {
694         let request = CargoUpdateRequest::from_str("cargo-bazel").unwrap();
695 
696         assert_eq!(
697             request,
698             CargoUpdateRequest::Package {
699                 name: "cargo-bazel".to_owned(),
700                 version: None
701             }
702         );
703     }
704 
705     #[test]
deserialize_cargo_update_request_for_precise()706     fn deserialize_cargo_update_request_for_precise() {
707         let request = CargoUpdateRequest::from_str("cargo-bazel@1.2.3").unwrap();
708 
709         assert_eq!(
710             request,
711             CargoUpdateRequest::Package {
712                 name: "cargo-bazel@1.2.3".to_owned(),
713                 version: None
714             }
715         );
716     }
717 
718     #[test]
deserialize_cargo_update_request_for_precise_pin()719     fn deserialize_cargo_update_request_for_precise_pin() {
720         let request = CargoUpdateRequest::from_str("cargo-bazel@1.2.3=4.5.6").unwrap();
721 
722         assert_eq!(
723             request,
724             CargoUpdateRequest::Package {
725                 name: "cargo-bazel@1.2.3".to_owned(),
726                 version: Some("4.5.6".to_owned()),
727             }
728         );
729     }
730 
731     #[test]
parse_features_from_cargo_tree_output_prefix_none()732     fn parse_features_from_cargo_tree_output_prefix_none() {
733         assert_eq!(
734             parse_features_from_cargo_tree_output(
735                 vec![
736                     Ok::<&str, std::io::Error>(""), // Blank lines are ignored.
737                     Ok("|multi_cfg_dep v0.1.0 (/private/tmp/ct)||"),
738                     Ok("|chrono v0.4.24|default,std|"),
739                     Ok("|cpufeatures v0.2.1||"),
740                     Ok("|libc v0.2.117|default,std|"),
741                     Ok("|serde_derive v1.0.152 (proc-macro) (*)||"),
742                     Ok("|chrono v0.4.24|default,std,serde|"),
743                 ]
744                 .into_iter()
745             )
746             .unwrap(),
747             BTreeMap::from([
748                 (
749                     CrateId {
750                         name: "multi_cfg_dep".to_owned(),
751                         version: Version::new(0, 1, 0),
752                     },
753                     BTreeSet::from([])
754                 ),
755                 (
756                     CrateId {
757                         name: "cpufeatures".to_owned(),
758                         version: Version::new(0, 2, 1),
759                     },
760                     BTreeSet::from([])
761                 ),
762                 (
763                     CrateId {
764                         name: "libc".to_owned(),
765                         version: Version::new(0, 2, 117),
766                     },
767                     BTreeSet::from(["default".to_owned(), "std".to_owned()])
768                 ),
769                 (
770                     CrateId {
771                         name: "serde_derive".to_owned(),
772                         version: Version::new(1, 0, 152),
773                     },
774                     BTreeSet::from([])
775                 ),
776                 (
777                     CrateId {
778                         name: "chrono".to_owned(),
779                         version: Version::new(0, 4, 24),
780                     },
781                     BTreeSet::from(["default".to_owned(), "std".to_owned(), "serde".to_owned()])
782                 ),
783             ])
784         );
785     }
786 }
787