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