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, HashMap};
7 use std::env;
8 use std::ffi::OsString;
9 use std::fs;
10 use std::io::BufRead;
11 use std::path::{Path, PathBuf};
12 use std::process::Command;
13 use std::str::FromStr;
14 use std::sync::{Arc, Mutex};
15
16 use anyhow::{anyhow, bail, Context, Result};
17 use cargo_lock::Lockfile as CargoLockfile;
18 use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand};
19 use semver::Version;
20 use serde::{Deserialize, Serialize};
21 use tracing::debug;
22 use url::Url;
23
24 use crate::config::CrateId;
25 use crate::lockfile::Digest;
26 use crate::select::{Select, SelectableScalar};
27 use crate::utils::symlink::symlink;
28 use crate::utils::target_triple::TargetTriple;
29
30 pub(crate) use self::dependency::*;
31 pub(crate) use self::metadata_annotation::*;
32
33 // TODO: This should also return a set of [crate-index::IndexConfig]s for packages in metadata.packages
34 /// A Trait for generating metadata (`cargo metadata` output and a lock file) from a Cargo manifest.
35 pub(crate) trait MetadataGenerator {
generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>36 fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>;
37 }
38
39 /// Generates Cargo metadata and a lockfile from a provided manifest.
40 pub(crate) struct Generator {
41 /// The path to a `cargo` binary
42 cargo_bin: Cargo,
43
44 /// The path to a `rustc` binary
45 rustc_bin: PathBuf,
46 }
47
48 impl Generator {
new() -> Self49 pub(crate) fn new() -> Self {
50 let rustc_bin = PathBuf::from(env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()));
51 Generator {
52 cargo_bin: Cargo::new(
53 PathBuf::from(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())),
54 rustc_bin.clone(),
55 ),
56 rustc_bin,
57 }
58 }
59
with_cargo(mut self, cargo_bin: Cargo) -> Self60 pub(crate) fn with_cargo(mut self, cargo_bin: Cargo) -> Self {
61 self.cargo_bin = cargo_bin;
62 self
63 }
64
with_rustc(mut self, rustc_bin: PathBuf) -> Self65 pub(crate) fn with_rustc(mut self, rustc_bin: PathBuf) -> Self {
66 self.rustc_bin = rustc_bin;
67 self
68 }
69 }
70
71 impl MetadataGenerator for Generator {
generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>72 fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)> {
73 let manifest_dir = manifest_path
74 .as_ref()
75 .parent()
76 .expect("The manifest should have a parent directory");
77 let lockfile = {
78 let lock_path = manifest_dir.join("Cargo.lock");
79 if !lock_path.exists() {
80 bail!("No `Cargo.lock` file was found with the given manifest")
81 }
82 cargo_lock::Lockfile::load(lock_path)?
83 };
84
85 let metadata = self
86 .cargo_bin
87 .metadata_command_with_options(manifest_path.as_ref(), vec!["--locked".to_owned()])?
88 .exec()?;
89
90 Ok((metadata, lockfile))
91 }
92 }
93
94 /// Cargo encapsulates a path to a `cargo` binary.
95 /// Any invocations of `cargo` (either as a `std::process::Command` or via `cargo_metadata`) should
96 /// go via this wrapper to ensure that any environment variables needed are set appropriately.
97 #[derive(Debug, Clone)]
98 pub(crate) struct Cargo {
99 path: PathBuf,
100 rustc_path: PathBuf,
101 full_version: Arc<Mutex<Option<String>>>,
102 cargo_home: Option<PathBuf>,
103 }
104
105 impl Cargo {
new(path: PathBuf, rustc: PathBuf) -> Cargo106 pub(crate) fn new(path: PathBuf, rustc: PathBuf) -> Cargo {
107 Cargo {
108 path,
109 rustc_path: rustc,
110 full_version: Arc::new(Mutex::new(None)),
111 cargo_home: None,
112 }
113 }
114
115 #[cfg(test)]
with_cargo_home(mut self, path: PathBuf) -> Cargo116 pub(crate) fn with_cargo_home(mut self, path: PathBuf) -> Cargo {
117 self.cargo_home = Some(path);
118 self
119 }
120
121 /// Returns a new `Command` for running this cargo.
command(&self) -> Result<Command>122 pub(crate) fn command(&self) -> Result<Command> {
123 let mut command = Command::new(&self.path);
124 command.envs(self.env()?);
125 if self.is_nightly()? {
126 command.arg("-Zbindeps");
127 }
128 Ok(command)
129 }
130
131 /// Returns a new `MetadataCommand` using this cargo.
132 /// `manifest_path`, `current_dir`, and `other_options` should not be called on the resturned MetadataCommand - instead pass them as the relevant args.
metadata_command_with_options( &self, manifest_path: &Path, other_options: Vec<String>, ) -> Result<MetadataCommand>133 pub(crate) fn metadata_command_with_options(
134 &self,
135 manifest_path: &Path,
136 other_options: Vec<String>,
137 ) -> Result<MetadataCommand> {
138 let mut command = MetadataCommand::new();
139 command.cargo_path(&self.path);
140 for (k, v) in self.env()? {
141 command.env(k, v);
142 }
143
144 command.manifest_path(manifest_path);
145 // Cargo detects config files based on `pwd` when running so
146 // to ensure user provided Cargo config files are used, it's
147 // critical to set the working directory to the manifest dir.
148 let manifest_dir = manifest_path
149 .parent()
150 .ok_or_else(|| anyhow::anyhow!("manifest_path {:?} must have parent", manifest_path))?;
151 command.current_dir(manifest_dir);
152
153 let mut other_options = other_options;
154 if self.is_nightly()? {
155 other_options.push("-Zbindeps".to_owned());
156 }
157 command.other_options(other_options);
158 Ok(command)
159 }
160
161 /// Returns the output of running `cargo version`, trimming any leading or trailing whitespace.
162 /// This function performs normalisation to work around `<https://github.com/rust-lang/cargo/issues/10547>`
full_version(&self) -> Result<String>163 pub(crate) fn full_version(&self) -> Result<String> {
164 let mut full_version = self.full_version.lock().unwrap();
165 if full_version.is_none() {
166 let observed_version = Digest::bin_version(&self.path)?;
167 *full_version = Some(observed_version);
168 }
169 Ok(full_version.clone().unwrap())
170 }
171
is_nightly(&self) -> Result<bool>172 pub(crate) fn is_nightly(&self) -> Result<bool> {
173 let full_version = self.full_version()?;
174 let version_str = full_version.split(' ').nth(1);
175 if let Some(version_str) = version_str {
176 let version = Version::parse(version_str).context("Failed to parse cargo version")?;
177 return Ok(version.pre.as_str() == "nightly");
178 }
179 bail!("Couldn't parse cargo version");
180 }
181
use_sparse_registries_for_crates_io(&self) -> Result<bool>182 pub(crate) fn use_sparse_registries_for_crates_io(&self) -> Result<bool> {
183 let full_version = self.full_version()?;
184 let version_str = full_version.split(' ').nth(1);
185 if let Some(version_str) = version_str {
186 let version = Version::parse(version_str).context("Failed to parse cargo version")?;
187 return Ok(version.major >= 1 && version.minor >= 68);
188 }
189 bail!("Couldn't parse cargo version");
190 }
191
192 /// Determine if Cargo is expected to be using the new package_id spec. For
193 /// details see <https://github.com/rust-lang/cargo/pull/13311>
194 #[cfg(test)]
uses_new_package_id_format(&self) -> Result<bool>195 pub(crate) fn uses_new_package_id_format(&self) -> Result<bool> {
196 let full_version = self.full_version()?;
197 let version_str = full_version.split(' ').nth(1);
198 if let Some(version_str) = version_str {
199 let version = Version::parse(version_str).context("Failed to parse cargo version")?;
200 return Ok(version.major >= 1 && version.minor >= 77);
201 }
202 bail!("Couldn't parse cargo version");
203 }
204
env(&self) -> Result<BTreeMap<String, OsString>>205 fn env(&self) -> Result<BTreeMap<String, OsString>> {
206 let mut map = BTreeMap::new();
207
208 map.insert("RUSTC".into(), self.rustc_path.as_os_str().to_owned());
209
210 if self.use_sparse_registries_for_crates_io()? {
211 map.insert(
212 "CARGO_REGISTRIES_CRATES_IO_PROTOCOL".into(),
213 "sparse".into(),
214 );
215 }
216
217 if let Some(cargo_home) = &self.cargo_home {
218 map.insert("CARGO_HOME".into(), cargo_home.as_os_str().to_owned());
219 }
220
221 Ok(map)
222 }
223 }
224
225 /// A configuration describing how to invoke [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html).
226 #[derive(Debug, Clone, PartialEq, Eq)]
227 pub enum CargoUpdateRequest {
228 /// Translates to an unrestricted `cargo update` command
229 Eager,
230
231 /// Translates to `cargo update --workspace`
232 Workspace,
233
234 /// Translates to `cargo update --package foo` with an optional `--precise` argument.
235 Package {
236 /// The name of the crate used with `--package`.
237 name: String,
238
239 /// If set, the `--precise` value that pairs with `--package`.
240 version: Option<String>,
241 },
242 }
243
244 impl FromStr for CargoUpdateRequest {
245 type Err = anyhow::Error;
246
from_str(s: &str) -> Result<Self, Self::Err>247 fn from_str(s: &str) -> Result<Self, Self::Err> {
248 let lower = s.to_lowercase();
249
250 if ["eager", "full", "all"].contains(&lower.as_str()) {
251 return Ok(Self::Eager);
252 }
253
254 if ["1", "yes", "true", "on", "workspace", "minimal"].contains(&lower.as_str()) {
255 return Ok(Self::Workspace);
256 }
257
258 let mut split = s.splitn(2, '=');
259 Ok(Self::Package {
260 name: split.next().map(|s| s.to_owned()).unwrap(),
261 version: split.next().map(|s| s.to_owned()),
262 })
263 }
264 }
265
266 impl CargoUpdateRequest {
267 /// Determine what arguments to pass to the `cargo update` command.
get_update_args(&self) -> Vec<String>268 fn get_update_args(&self) -> Vec<String> {
269 match self {
270 CargoUpdateRequest::Eager => Vec::new(),
271 CargoUpdateRequest::Workspace => vec!["--workspace".to_owned()],
272 CargoUpdateRequest::Package { name, version } => {
273 let mut update_args = vec!["--package".to_owned(), name.clone()];
274
275 if let Some(version) = version {
276 update_args.push("--precise".to_owned());
277 update_args.push(version.clone());
278 }
279
280 update_args
281 }
282 }
283 }
284
285 /// Calls `cargo update` with arguments specific to the state of the current variant.
update(&self, manifest: &Path, cargo_bin: &Cargo) -> Result<()>286 pub(crate) fn update(&self, manifest: &Path, cargo_bin: &Cargo) -> Result<()> {
287 let manifest_dir = manifest.parent().unwrap();
288
289 // Simply invoke `cargo update`
290 let output = cargo_bin
291 .command()?
292 // Cargo detects config files based on `pwd` when running so
293 // to ensure user provided Cargo config files are used, it's
294 // critical to set the working directory to the manifest dir.
295 .current_dir(manifest_dir)
296 .arg("update")
297 .arg("--manifest-path")
298 .arg(manifest)
299 .args(self.get_update_args())
300 .output()
301 .with_context(|| {
302 format!(
303 "Error running cargo to update packages for manifest '{}'",
304 manifest.display()
305 )
306 })?;
307
308 if !output.status.success() {
309 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
310 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
311 bail!(format!("Failed to update lockfile: {}", output.status))
312 }
313
314 Ok(())
315 }
316 }
317
318 pub(crate) struct LockGenerator {
319 /// Interface to cargo.
320 cargo_bin: Cargo,
321 }
322
323 impl LockGenerator {
new(cargo_bin: Cargo) -> Self324 pub(crate) fn new(cargo_bin: Cargo) -> Self {
325 Self { cargo_bin }
326 }
327
328 #[tracing::instrument(name = "LockGenerator::generate", skip_all)]
generate( &self, manifest_path: &Path, existing_lock: &Option<PathBuf>, update_request: &Option<CargoUpdateRequest>, ) -> Result<cargo_lock::Lockfile>329 pub(crate) fn generate(
330 &self,
331 manifest_path: &Path,
332 existing_lock: &Option<PathBuf>,
333 update_request: &Option<CargoUpdateRequest>,
334 ) -> Result<cargo_lock::Lockfile> {
335 debug!("Generating Cargo Lockfile for {}", manifest_path.display());
336
337 let manifest_dir = manifest_path.parent().unwrap();
338 let generated_lockfile_path = manifest_dir.join("Cargo.lock");
339
340 if let Some(lock) = existing_lock {
341 debug!("Using existing lock {}", lock.display());
342 if !lock.exists() {
343 bail!(
344 "An existing lockfile path was provided but a file at '{}' does not exist",
345 lock.display()
346 )
347 }
348
349 // Install the file into the target location
350 if generated_lockfile_path.exists() {
351 fs::remove_file(&generated_lockfile_path)?;
352 }
353 fs::copy(lock, &generated_lockfile_path)?;
354
355 if let Some(request) = update_request {
356 request.update(manifest_path, &self.cargo_bin)?;
357 }
358
359 // Ensure the Cargo cache is up to date to simulate the behavior
360 // of having just generated a new one
361 let output = self
362 .cargo_bin
363 .command()?
364 // Cargo detects config files based on `pwd` when running so
365 // to ensure user provided Cargo config files are used, it's
366 // critical to set the working directory to the manifest dir.
367 .current_dir(manifest_dir)
368 .arg("fetch")
369 .arg("--manifest-path")
370 .arg(manifest_path)
371 .output()
372 .context(format!(
373 "Error running cargo to fetch crates '{}'",
374 manifest_path.display()
375 ))?;
376
377 if !output.status.success() {
378 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
379 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
380 bail!(format!(
381 "Failed to fetch crates for lockfile: {}",
382 output.status
383 ))
384 }
385 } else {
386 debug!("Generating new lockfile");
387 // Simply invoke `cargo generate-lockfile`
388 let output = self
389 .cargo_bin
390 .command()?
391 // Cargo detects config files based on `pwd` when running so
392 // to ensure user provided Cargo config files are used, it's
393 // critical to set the working directory to the manifest dir.
394 .current_dir(manifest_dir)
395 .arg("generate-lockfile")
396 .arg("--manifest-path")
397 .arg(manifest_path)
398 .output()
399 .context(format!(
400 "Error running cargo to generate lockfile '{}'",
401 manifest_path.display()
402 ))?;
403
404 if !output.status.success() {
405 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
406 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
407 bail!(format!("Failed to generate lockfile: {}", output.status))
408 }
409 }
410
411 cargo_lock::Lockfile::load(&generated_lockfile_path).context(format!(
412 "Failed to load lockfile: {}",
413 generated_lockfile_path.display()
414 ))
415 }
416 }
417
418 /// A generator which runs `cargo vendor` on a given manifest
419 pub(crate) struct VendorGenerator {
420 /// The path to a `cargo` binary
421 cargo_bin: Cargo,
422
423 /// The path to a `rustc` binary
424 rustc_bin: PathBuf,
425 }
426
427 impl VendorGenerator {
new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self428 pub(crate) fn new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self {
429 Self {
430 cargo_bin,
431 rustc_bin,
432 }
433 }
434 #[tracing::instrument(name = "VendorGenerator::generate", skip_all)]
generate(&self, manifest_path: &Path, output_dir: &Path) -> Result<()>435 pub(crate) fn generate(&self, manifest_path: &Path, output_dir: &Path) -> Result<()> {
436 debug!(
437 "Vendoring {} to {}",
438 manifest_path.display(),
439 output_dir.display()
440 );
441 let manifest_dir = manifest_path.parent().unwrap();
442
443 // Simply invoke `cargo generate-lockfile`
444 let output = self
445 .cargo_bin
446 .command()?
447 // Cargo detects config files based on `pwd` when running so
448 // to ensure user provided Cargo config files are used, it's
449 // critical to set the working directory to the manifest dir.
450 .current_dir(manifest_dir)
451 .arg("vendor")
452 .arg("--manifest-path")
453 .arg(manifest_path)
454 .arg("--locked")
455 .arg("--versioned-dirs")
456 .arg(output_dir)
457 .env("RUSTC", &self.rustc_bin)
458 .output()
459 .with_context(|| {
460 format!(
461 "Error running cargo to vendor sources for manifest '{}'",
462 manifest_path.display()
463 )
464 })?;
465
466 if !output.status.success() {
467 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
468 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
469 bail!(format!("Failed to vendor sources with: {}", output.status))
470 }
471
472 debug!("Done");
473 Ok(())
474 }
475 }
476
477 /// Feature resolver info about a given crate.
478 #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
479 pub(crate) struct CargoTreeEntry {
480 /// The set of features active on a given crate.
481 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
482 pub features: BTreeSet<String>,
483
484 /// The dependencies of a given crate based on feature resolution.
485 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
486 pub deps: BTreeSet<CrateId>,
487 }
488
489 impl CargoTreeEntry {
new() -> Self490 pub fn new() -> Self {
491 Self {
492 features: BTreeSet::new(),
493 deps: BTreeSet::new(),
494 }
495 }
496
is_empty(&self) -> bool497 pub fn is_empty(&self) -> bool {
498 self.features.is_empty() && self.deps.is_empty()
499 }
500 }
501
502 impl SelectableScalar for CargoTreeEntry {}
503
504 /// Feature and dependency metadata generated from [TreeResolver].
505 pub(crate) type TreeResolverMetadata = BTreeMap<CrateId, Select<CargoTreeEntry>>;
506
507 /// Generates metadata about a Cargo workspace tree which supplements the inaccuracies in
508 /// standard [Cargo metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html)
509 /// due lack of [Feature resolver 2](https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2)
510 /// support. This generator can be removed if the following is resolved:
511 /// <https://github.com/rust-lang/cargo/issues/9863>
512 pub(crate) struct TreeResolver {
513 /// The path to a `cargo` binary
514 cargo_bin: Cargo,
515 }
516
517 impl TreeResolver {
new(cargo_bin: Cargo) -> Self518 pub(crate) fn new(cargo_bin: Cargo) -> Self {
519 Self { cargo_bin }
520 }
521
522 /// Computes the set of enabled features for each target triplet for each crate.
523 #[tracing::instrument(name = "TreeResolver::generate", skip_all)]
generate( &self, pristine_manifest_path: &Path, target_triples: &BTreeSet<TargetTriple>, ) -> Result<TreeResolverMetadata>524 pub(crate) fn generate(
525 &self,
526 pristine_manifest_path: &Path,
527 target_triples: &BTreeSet<TargetTriple>,
528 ) -> Result<TreeResolverMetadata> {
529 debug!(
530 "Generating features for manifest {}",
531 pristine_manifest_path.display()
532 );
533
534 let (manifest_path_with_transitive_proc_macros, tempdir) = self
535 .copy_project_with_explicit_deps_on_all_transitive_proc_macros(pristine_manifest_path)
536 .context("Failed to copy project with proc macro deps made direct")?;
537
538 let mut target_triple_to_child = BTreeMap::new();
539 debug!("Spawning processes for {:?}", target_triples);
540 for target_triple in target_triples {
541 // We use `cargo tree` here because `cargo metadata` doesn't report
542 // back target-specific features (enabled with `resolver = "2"`).
543 // This is unfortunately a bit of a hack. See:
544 // - https://github.com/rust-lang/cargo/issues/9863
545 // - https://github.com/bazelbuild/rules_rust/issues/1662
546 let output = self
547 .cargo_bin
548 .command()?
549 .current_dir(tempdir.path())
550 .arg("tree")
551 .arg("--manifest-path")
552 .arg(&manifest_path_with_transitive_proc_macros)
553 .arg("--edges")
554 .arg("normal,build,dev")
555 .arg("--prefix=depth")
556 // https://doc.rust-lang.org/cargo/commands/cargo-tree.html#tree-formatting-options
557 .arg("--format=|{p}|{f}|")
558 .arg("--color=never")
559 .arg("--workspace")
560 .arg("--target")
561 .arg(target_triple.to_cargo())
562 .stdout(std::process::Stdio::piped())
563 .stderr(std::process::Stdio::piped())
564 .spawn()
565 .with_context(|| {
566 format!(
567 "Error spawning cargo in child process to compute features for target '{}', manifest path '{}'",
568 target_triple,
569 manifest_path_with_transitive_proc_macros.display()
570 )
571 })?;
572 target_triple_to_child.insert(target_triple, output);
573 }
574 let mut metadata: BTreeMap<CrateId, BTreeMap<TargetTriple, CargoTreeEntry>> =
575 BTreeMap::new();
576 for (target_triple, child) in target_triple_to_child.into_iter() {
577 let output = child
578 .wait_with_output()
579 .with_context(|| {
580 format!(
581 "Error running cargo in child process to compute features for target '{}', manifest path '{}'",
582 target_triple,
583 manifest_path_with_transitive_proc_macros.display()
584 )
585 })?;
586 if !output.status.success() {
587 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
588 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
589 bail!(format!("Failed to run cargo tree: {}", output.status))
590 }
591 debug!("Process complete for {}", target_triple);
592 for (crate_id, tree_data) in
593 parse_features_from_cargo_tree_output(output.stdout.lines())?
594 {
595 debug!(
596 "\tFor {}\n\t\tfeatures: {:?}\n\t\tdeps: {:?}",
597 crate_id, tree_data.features, tree_data.deps
598 );
599 metadata
600 .entry(crate_id.clone())
601 .or_default()
602 .insert(target_triple.clone(), tree_data);
603 }
604 }
605 let mut result = TreeResolverMetadata::new();
606 for (crate_id, tree_data) in metadata.into_iter() {
607 let common = CargoTreeEntry {
608 features: tree_data
609 .iter()
610 .fold(
611 None,
612 |common: Option<BTreeSet<String>>, (_, data)| match common {
613 Some(common) => {
614 Some(common.intersection(&data.features).cloned().collect())
615 }
616 None => Some(data.features.clone()),
617 },
618 )
619 .unwrap_or_default(),
620 deps: tree_data
621 .iter()
622 .fold(
623 None,
624 |common: Option<BTreeSet<CrateId>>, (_, data)| match common {
625 Some(common) => {
626 Some(common.intersection(&data.deps).cloned().collect())
627 }
628 None => Some(data.deps.clone()),
629 },
630 )
631 .unwrap_or_default(),
632 };
633 let mut select: Select<CargoTreeEntry> = Select::default();
634 for (target_triple, data) in tree_data {
635 let mut entry = CargoTreeEntry::new();
636 entry.features.extend(
637 data.features
638 .into_iter()
639 .filter(|f| !common.features.contains(f)),
640 );
641 entry
642 .deps
643 .extend(data.deps.into_iter().filter(|d| !common.deps.contains(d)));
644 if !entry.is_empty() {
645 select.insert(entry, Some(target_triple.to_bazel()));
646 }
647 }
648 if !common.is_empty() {
649 select.insert(common, None);
650 }
651 result.insert(crate_id, select);
652 }
653 Ok(result)
654 }
655
656 // Artificially inject all proc macros as dependency roots.
657 // Proc macros are built in the exec rather than target configuration.
658 // If we do cross-compilation, these will be different, and it will be important that we have resolved features and optional dependencies for the exec platform.
659 // If we don't treat proc macros as roots for the purposes of resolving, we may end up with incorrect platform-specific features.
660 //
661 // Example:
662 // If crate foo only uses a proc macro Linux,
663 // and that proc-macro depends on syn and requires the feature extra-traits,
664 // when we resolve on macOS we'll see we don't need the extra-traits feature of syn because the proc macro isn't used.
665 // But if we're cross-compiling for Linux from macOS, we'll build a syn, but because we're building it for macOS (because proc macros are exec-cfg dependencies),
666 // we'll build syn but _without_ the extra-traits feature (because our resolve told us it was Linux only).
667 //
668 // By artificially injecting all proc macros as root dependencies,
669 // it means we are forced to resolve the dependencies and features for those proc-macros on all platforms we care about,
670 // even if they wouldn't be used in some platform when cfg == exec.
671 //
672 // This is tested by the "keyring" example in examples/musl_cross_compiling - the keyring crate uses proc-macros only on Linux,
673 // and if we don't have this fake root injection, cross-compiling from Darwin to Linux won't work because features don't get correctly resolved for the exec=darwin case.
copy_project_with_explicit_deps_on_all_transitive_proc_macros( &self, pristine_manifest_path: &Path, ) -> Result<(PathBuf, tempfile::TempDir)>674 fn copy_project_with_explicit_deps_on_all_transitive_proc_macros(
675 &self,
676 pristine_manifest_path: &Path,
677 ) -> Result<(PathBuf, tempfile::TempDir)> {
678 let pristine_root = pristine_manifest_path.parent().unwrap();
679 let working_directory = tempfile::tempdir().context("Failed to make tempdir")?;
680 for file in std::fs::read_dir(pristine_root).context("Failed to read dir")? {
681 let source_path = file?.path();
682 let file_name = source_path.file_name().unwrap();
683 if file_name != "Cargo.toml" && file_name != "Cargo.lock" {
684 let destination = working_directory.path().join(file_name);
685 symlink(&source_path, &destination).with_context(|| {
686 format!(
687 "Failed to create symlink {:?} pointing at {:?}",
688 destination, source_path
689 )
690 })?;
691 }
692 }
693 std::fs::copy(
694 pristine_root.join("Cargo.lock"),
695 working_directory.path().join("Cargo.lock"),
696 )
697 .with_context(|| {
698 format!(
699 "Failed to copy Cargo.lock from {:?} to {:?}",
700 pristine_root,
701 working_directory.path()
702 )
703 })?;
704
705 let cargo_metadata = self
706 .cargo_bin
707 .metadata_command_with_options(pristine_manifest_path, vec!["--locked".to_owned()])?
708 .manifest_path(pristine_manifest_path)
709 .exec()
710 .context("Failed to run cargo metadata to list transitive proc macros")?;
711 let proc_macros = cargo_metadata
712 .packages
713 .iter()
714 .filter(|p| {
715 p.targets
716 .iter()
717 .any(|t| t.kind.iter().any(|k| k == "proc-macro"))
718 })
719 // Filter out any in-workspace proc macros, populate dependency details for non-in-workspace proc macros.
720 .filter_map(|pm| {
721 if let Some(source) = pm.source.as_ref() {
722 let mut detail = DependencyDetailWithOrd(cargo_toml::DependencyDetail {
723 package: Some(pm.name.clone()),
724 // Don't forcibly enable default features - if some other dependency enables them, they will still be enabled.
725 default_features: false,
726 ..cargo_toml::DependencyDetail::default()
727 });
728
729 let source = match Source::parse(&source.repr, pm.version.to_string()) {
730 Ok(source) => source,
731 Err(err) => {
732 return Some(Err(err));
733 }
734 };
735 source.populate_details(&mut detail.0);
736
737 Some(Ok((pm.name.clone(), detail)))
738 } else {
739 None
740 }
741 })
742 .collect::<Result<BTreeSet<_>>>()?;
743
744 let mut manifest =
745 cargo_toml::Manifest::from_path(pristine_manifest_path).with_context(|| {
746 format!(
747 "Failed to parse Cargo.toml file at {:?}",
748 pristine_manifest_path
749 )
750 })?;
751
752 // To add dependencies to a virtual workspace, we need to add them to a package inside the workspace,
753 // we can't just add them to the workspace directly.
754 if !proc_macros.is_empty() && manifest.package.is_none() {
755 if let Some(ref mut workspace) = &mut manifest.workspace {
756 if !workspace.members.contains(&".".to_owned()) {
757 workspace.members.push(".".to_owned());
758 }
759 manifest.package = Some(cargo_toml::Package::new(
760 "rules_rust_fake_proc_macro_root",
761 "0.0.0",
762 ));
763 }
764 if manifest.lib.is_none() && manifest.bin.is_empty() {
765 manifest.bin.push(cargo_toml::Product {
766 name: Some("rules_rust_fake_proc_macro_root_bin".to_owned()),
767 path: Some("/dev/null".to_owned()),
768 ..cargo_toml::Product::default()
769 })
770 }
771 }
772
773 let mut count_map: HashMap<_, u64> = HashMap::new();
774 for (dep_name, detail) in proc_macros {
775 let count = count_map.entry(dep_name.clone()).or_default();
776 manifest.dependencies.insert(
777 format!("rules_rust_fake_proc_macro_root_{}_{}", dep_name, count),
778 cargo_toml::Dependency::Detailed(Box::new(detail.0)),
779 );
780 *count += 1;
781 }
782 let manifest_path_with_transitive_proc_macros = working_directory.path().join("Cargo.toml");
783 crate::splicing::write_manifest(&manifest_path_with_transitive_proc_macros, &manifest)?;
784 Ok((manifest_path_with_transitive_proc_macros, working_directory))
785 }
786 }
787
788 // cargo_toml::DependencyDetail doesn't implement PartialOrd/Ord so can't be put in a sorted collection.
789 // Wrap it so we can sort things for stable orderings.
790 #[derive(Debug, PartialEq)]
791 struct DependencyDetailWithOrd(cargo_toml::DependencyDetail);
792
793 impl PartialOrd for DependencyDetailWithOrd {
partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering>794 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
795 Some(self.cmp(other))
796 }
797 }
798
799 impl Ord for DependencyDetailWithOrd {
cmp(&self, other: &Self) -> std::cmp::Ordering800 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
801 let cargo_toml::DependencyDetail {
802 version,
803 registry,
804 registry_index,
805 path,
806 inherited,
807 git,
808 branch,
809 tag,
810 rev,
811 features,
812 optional,
813 default_features,
814 package,
815 unstable: _,
816 } = &self.0;
817
818 version
819 .cmp(&other.0.version)
820 .then(registry.cmp(&other.0.registry))
821 .then(registry_index.cmp(&other.0.registry_index))
822 .then(path.cmp(&other.0.path))
823 .then(inherited.cmp(&other.0.inherited))
824 .then(git.cmp(&other.0.git))
825 .then(branch.cmp(&other.0.branch))
826 .then(tag.cmp(&other.0.tag))
827 .then(rev.cmp(&other.0.rev))
828 .then(features.cmp(&other.0.features))
829 .then(optional.cmp(&other.0.optional))
830 .then(default_features.cmp(&other.0.default_features))
831 .then(package.cmp(&other.0.package))
832 }
833 }
834
835 impl Eq for DependencyDetailWithOrd {}
836
837 #[derive(Debug, PartialEq, Eq)]
838 enum Source {
839 Registry {
840 registry: String,
841 version: String,
842 },
843 Git {
844 git: String,
845 rev: Option<String>,
846 branch: Option<String>,
847 tag: Option<String>,
848 },
849 }
850
851 impl Source {
parse(string: &str, version: String) -> Result<Source>852 fn parse(string: &str, version: String) -> Result<Source> {
853 let url: Url = Url::parse(string)?;
854 let original_scheme = url.scheme().to_owned();
855 let scheme_parts: Vec<_> = original_scheme.split('+').collect();
856 match &scheme_parts[..] {
857 // e.g. registry+https://github.com/rust-lang/crates.io-index
858 ["registry", scheme] => {
859 let new_url = set_url_scheme_despite_the_url_crate_not_wanting_us_to(&url, scheme)?;
860 Ok(Self::Registry {
861 registry: new_url,
862 version,
863 })
864 }
865 // e.g. git+https://github.com/serde-rs/serde.git?rev=9b868ef831c95f50dd4bde51a7eb52e3b9ee265a#9b868ef831c95f50dd4bde51a7eb52e3b9ee265a
866 ["git", scheme] => {
867 let mut query: HashMap<String, String> = url
868 .query_pairs()
869 .map(|(k, v)| (k.into_owned(), v.into_owned()))
870 .collect();
871
872 let mut url = url;
873 url.set_fragment(None);
874 url.set_query(None);
875 let new_url = set_url_scheme_despite_the_url_crate_not_wanting_us_to(&url, scheme)?;
876
877 Ok(Self::Git {
878 git: new_url,
879 rev: query.remove("rev"),
880 branch: query.remove("branch"),
881 tag: query.remove("tag"),
882 })
883 }
884 _ => {
885 anyhow::bail!(
886 "Couldn't parse source {:?}: Didn't recognise scheme",
887 string
888 );
889 }
890 }
891 }
892
populate_details(self, details: &mut cargo_toml::DependencyDetail)893 fn populate_details(self, details: &mut cargo_toml::DependencyDetail) {
894 match self {
895 Self::Registry { registry, version } => {
896 details.registry_index = Some(registry);
897 details.version = Some(version);
898 }
899 Self::Git {
900 git,
901 rev,
902 branch,
903 tag,
904 } => {
905 details.git = Some(git);
906 details.rev = rev;
907 details.branch = branch;
908 details.tag = tag;
909 }
910 }
911 }
912 }
913
set_url_scheme_despite_the_url_crate_not_wanting_us_to( url: &Url, new_scheme: &str, ) -> Result<String>914 fn set_url_scheme_despite_the_url_crate_not_wanting_us_to(
915 url: &Url,
916 new_scheme: &str,
917 ) -> Result<String> {
918 let (_old_scheme, new_url_without_scheme) = url.as_str().split_once(':').ok_or_else(|| {
919 anyhow::anyhow!(
920 "Cannot set schme of URL which doesn't contain \":\": {:?}",
921 url
922 )
923 })?;
924 Ok(format!("{new_scheme}:{new_url_without_scheme}"))
925 }
926
927 /// Parses the output of `cargo tree --format=|{p}|{f}|`. Other flags may be
928 /// 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, CargoTreeEntry>> where I: Iterator<Item = std::result::Result<S, E>>, S: AsRef<str>, E: std::error::Error + Sync + Send + 'static,929 fn parse_features_from_cargo_tree_output<I, S, E>(
930 lines: I,
931 ) -> Result<BTreeMap<CrateId, CargoTreeEntry>>
932 where
933 I: Iterator<Item = std::result::Result<S, E>>,
934 S: AsRef<str>,
935 E: std::error::Error + Sync + Send + 'static,
936 {
937 let mut tree_data = BTreeMap::<CrateId, CargoTreeEntry>::new();
938 let mut parents: Vec<CrateId> = Vec::new();
939 for line in lines {
940 let line = line?;
941 let line = line.as_ref();
942 if line.is_empty() {
943 continue;
944 }
945
946 let parts = line.split('|').collect::<Vec<_>>();
947 if parts.len() != 4 {
948 bail!("Unexpected line '{}'", line);
949 }
950 // We expect the crate id (parts[1]) to be either
951 // "<crate name> v<crate version>" or
952 // "<crate name> v<crate version> (<path>)"
953 // "<crate name> v<crate version> (proc-macro) (<path>)"
954 // https://github.com/rust-lang/cargo/blob/19f952f160d4f750d1e12fad2bf45e995719673d/src/cargo/ops/tree/mod.rs#L281
955 let crate_id_parts = parts[1].split(' ').collect::<Vec<_>>();
956 if crate_id_parts.len() < 2 && crate_id_parts.len() > 4 {
957 bail!(
958 "Unexpected crate id format '{}' when parsing 'cargo tree' output.",
959 parts[1]
960 );
961 }
962 let version_str = crate_id_parts[1].strip_prefix('v').ok_or_else(|| {
963 anyhow!(
964 "Unexpected crate version '{}' when parsing 'cargo tree' output.",
965 crate_id_parts[1]
966 )
967 })?;
968 let version = Version::parse(version_str).context("Failed to parse version")?;
969 let crate_id = CrateId::new(crate_id_parts[0].to_owned(), version);
970
971 // Update bookkeeping for dependency tracking.
972 let depth = parts[0]
973 .parse::<usize>()
974 .with_context(|| format!("Unexpected numeric value from cargo tree: {:?}", parts))?;
975 if (depth + 1) <= parents.len() {
976 // Drain parents until we get down to the right depth
977 let range = parents.len() - (depth + 1);
978 for _ in 0..range {
979 parents.pop();
980 }
981
982 // If the current parent does not have the same Crate ID, then
983 // it's likely we have moved to a different crate. This can happen
984 // in the following case
985 // ```
986 // ├── proc-macro2 v1.0.81
987 // │ └── unicode-ident v1.0.12
988 // ├── quote v1.0.36
989 // │ └── proc-macro2 v1.0.81 (*)
990 // ```
991 if parents.last() != Some(&crate_id) {
992 parents.pop();
993 parents.push(crate_id.clone());
994 }
995 } else {
996 // Start tracking the current crate as the new parent for any
997 // crates that represent a new depth in the dep tree.
998 parents.push(crate_id.clone());
999 }
1000
1001 // Attribute any dependency that is not the root to it's parent.
1002 if depth > 0 {
1003 // Access the last item in the list of parents.
1004 if let Some(parent) = parents.iter().rev().nth(1) {
1005 tree_data
1006 .entry(parent.clone())
1007 .or_default()
1008 .deps
1009 .insert(crate_id.clone());
1010 }
1011 }
1012
1013 let mut features = if parts[2].is_empty() {
1014 BTreeSet::new()
1015 } else {
1016 parts[2].split(',').map(str::to_owned).collect()
1017 };
1018 tree_data
1019 .entry(crate_id)
1020 .or_default()
1021 .features
1022 .append(&mut features);
1023 }
1024 Ok(tree_data)
1025 }
1026
1027 /// A helper function for writing Cargo metadata to a file.
write_metadata(path: &Path, metadata: &cargo_metadata::Metadata) -> Result<()>1028 pub(crate) fn write_metadata(path: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
1029 let content =
1030 serde_json::to_string_pretty(metadata).context("Failed to serialize Cargo Metadata")?;
1031
1032 fs::write(path, content).context("Failed to write metadata to disk")
1033 }
1034
1035 /// A helper function for deserializing Cargo metadata and lockfiles
load_metadata( metadata_path: &Path, ) -> Result<(cargo_metadata::Metadata, cargo_lock::Lockfile)>1036 pub(crate) fn load_metadata(
1037 metadata_path: &Path,
1038 ) -> Result<(cargo_metadata::Metadata, cargo_lock::Lockfile)> {
1039 // Locate the Cargo.lock file related to the metadata file.
1040 let lockfile_path = metadata_path
1041 .parent()
1042 .expect("metadata files should always have parents")
1043 .join("Cargo.lock");
1044 if !lockfile_path.exists() {
1045 bail!(
1046 "The metadata file at {} is not next to a `Cargo.lock` file.",
1047 metadata_path.display()
1048 )
1049 }
1050
1051 let content = fs::read_to_string(metadata_path)
1052 .with_context(|| format!("Failed to load Cargo Metadata: {}", metadata_path.display()))?;
1053
1054 let metadata =
1055 serde_json::from_str(&content).context("Unable to deserialize Cargo metadata")?;
1056
1057 let lockfile = cargo_lock::Lockfile::load(&lockfile_path)
1058 .with_context(|| format!("Failed to load lockfile: {}", lockfile_path.display()))?;
1059
1060 Ok((metadata, lockfile))
1061 }
1062
1063 #[cfg(test)]
1064 mod test {
1065 use super::*;
1066
1067 #[test]
deserialize_cargo_update_request_for_eager()1068 fn deserialize_cargo_update_request_for_eager() {
1069 for value in ["all", "full", "eager"] {
1070 let request = CargoUpdateRequest::from_str(value).unwrap();
1071
1072 assert_eq!(request, CargoUpdateRequest::Eager);
1073 }
1074 }
1075
1076 #[test]
deserialize_cargo_update_request_for_workspace()1077 fn deserialize_cargo_update_request_for_workspace() {
1078 for value in ["1", "true", "yes", "on", "workspace", "minimal"] {
1079 let request = CargoUpdateRequest::from_str(value).unwrap();
1080
1081 assert_eq!(request, CargoUpdateRequest::Workspace);
1082 }
1083 }
1084
1085 #[test]
deserialize_cargo_update_request_for_package()1086 fn deserialize_cargo_update_request_for_package() {
1087 let request = CargoUpdateRequest::from_str("cargo-bazel").unwrap();
1088
1089 assert_eq!(
1090 request,
1091 CargoUpdateRequest::Package {
1092 name: "cargo-bazel".to_owned(),
1093 version: None
1094 }
1095 );
1096 }
1097
1098 #[test]
deserialize_cargo_update_request_for_precise()1099 fn deserialize_cargo_update_request_for_precise() {
1100 let request = CargoUpdateRequest::from_str("[email protected]").unwrap();
1101
1102 assert_eq!(
1103 request,
1104 CargoUpdateRequest::Package {
1105 name: "[email protected]".to_owned(),
1106 version: None
1107 }
1108 );
1109 }
1110
1111 #[test]
deserialize_cargo_update_request_for_precise_pin()1112 fn deserialize_cargo_update_request_for_precise_pin() {
1113 let request = CargoUpdateRequest::from_str("[email protected]=4.5.6").unwrap();
1114
1115 assert_eq!(
1116 request,
1117 CargoUpdateRequest::Package {
1118 name: "[email protected]".to_owned(),
1119 version: Some("4.5.6".to_owned()),
1120 }
1121 );
1122 }
1123
1124 #[test]
parse_features_from_cargo_tree_output_prefix_none()1125 fn parse_features_from_cargo_tree_output_prefix_none() {
1126 let autocfg_id = CrateId {
1127 name: "autocfg".to_owned(),
1128 version: Version::new(1, 2, 0),
1129 };
1130 let chrono_id = CrateId {
1131 name: "chrono".to_owned(),
1132 version: Version::new(0, 4, 24),
1133 };
1134 let core_foundation_sys_id = CrateId {
1135 name: "core-foundation-sys".to_owned(),
1136 version: Version::new(0, 8, 6),
1137 };
1138 let cpufeatures_id = CrateId {
1139 name: "cpufeatures".to_owned(),
1140 version: Version::new(0, 2, 7),
1141 };
1142 let iana_time_zone_id = CrateId {
1143 name: "iana-time-zone".to_owned(),
1144 version: Version::new(0, 1, 60),
1145 };
1146 let libc_id = CrateId {
1147 name: "libc".to_owned(),
1148 version: Version::new(0, 2, 153),
1149 };
1150 let num_integer_id = CrateId {
1151 name: "num-integer".to_owned(),
1152 version: Version::new(0, 1, 46),
1153 };
1154 let num_traits_id = CrateId {
1155 name: "num-traits".to_owned(),
1156 version: Version::new(0, 2, 18),
1157 };
1158 let proc_macro2_id = CrateId {
1159 name: "proc-macro2".to_owned(),
1160 version: Version::new(1, 0, 81),
1161 };
1162 let quote_id = CrateId {
1163 name: "quote".to_owned(),
1164 version: Version::new(1, 0, 36),
1165 };
1166 let serde_derive_id = CrateId {
1167 name: "serde_derive".to_owned(),
1168 version: Version::new(1, 0, 152),
1169 };
1170 let syn_id = CrateId {
1171 name: "syn".to_owned(),
1172 version: Version::new(1, 0, 109),
1173 };
1174 let time_id = CrateId {
1175 name: "time".to_owned(),
1176 version: Version::new(0, 1, 45),
1177 };
1178 let tree_data_id = CrateId {
1179 name: "tree-data".to_owned(),
1180 version: Version::new(0, 1, 0),
1181 };
1182 let unicode_ident_id = CrateId {
1183 name: "unicode-ident".to_owned(),
1184 version: Version::new(1, 0, 12),
1185 };
1186
1187 // |tree-data v0.1.0 (/rules_rust/crate_universe/test_data/metadata/tree_data)||
1188 // ├── |chrono v0.4.24|clock,default,iana-time-zone,js-sys,oldtime,std,time,wasm-bindgen,wasmbind,winapi|
1189 // │ ├── |iana-time-zone v0.1.60|fallback|
1190 // │ │ └── |core-foundation-sys v0.8.6|default,link|
1191 // │ ├── |num-integer v0.1.46||
1192 // │ │ └── |num-traits v0.2.18|i128|
1193 // │ │ [build-dependencies]
1194 // │ │ └── |autocfg v1.2.0||
1195 // │ ├── |num-traits v0.2.18|i128| (*)
1196 // │ └── |time v0.1.45||
1197 // │ └── |libc v0.2.153|default,std|
1198 // ├── |cpufeatures v0.2.7||
1199 // │ └── |libc v0.2.153|default,std|
1200 // └── |serde_derive v1.0.152 (proc-macro)|default|
1201 // ├── |proc-macro2 v1.0.81|default,proc-macro|
1202 // │ └── |unicode-ident v1.0.12||
1203 // ├── |quote v1.0.36|default,proc-macro|
1204 // │ └── |proc-macro2 v1.0.81|default,proc-macro| (*)
1205 // └── |syn v1.0.109|clone-impls,default,derive,parsing,printing,proc-macro,quote|
1206 // ├── |proc-macro2 v1.0.81|default,proc-macro| (*)
1207 // ├── |quote v1.0.36|default,proc-macro| (*)
1208 // └── |unicode-ident v1.0.12||
1209 let output = parse_features_from_cargo_tree_output(
1210 vec![
1211 Ok::<&str, std::io::Error>(""), // Blank lines are ignored.
1212 Ok("0|tree-data v0.1.0 (/rules_rust/crate_universe/test_data/metadata/tree_data)||"),
1213 Ok("1|chrono v0.4.24|clock,default,iana-time-zone,js-sys,oldtime,std,time,wasm-bindgen,wasmbind,winapi|"),
1214 Ok("2|iana-time-zone v0.1.60|fallback|"),
1215 Ok("3|core-foundation-sys v0.8.6|default,link|"),
1216 Ok("2|num-integer v0.1.46||"),
1217 Ok("3|num-traits v0.2.18|i128|"),
1218 Ok("4|autocfg v1.2.0||"),
1219 Ok("2|num-traits v0.2.18|i128| (*)"),
1220 Ok("2|time v0.1.45||"),
1221 Ok("3|libc v0.2.153|default,std|"),
1222 Ok("1|cpufeatures v0.2.7||"),
1223 Ok("2|libc v0.2.153|default,std|"),
1224 Ok("1|serde_derive v1.0.152 (proc-macro)|default|"),
1225 Ok("2|proc-macro2 v1.0.81|default,proc-macro|"),
1226 Ok("3|unicode-ident v1.0.12||"),
1227 Ok("2|quote v1.0.36|default,proc-macro|"),
1228 Ok("3|proc-macro2 v1.0.81|default,proc-macro| (*)"),
1229 Ok("2|syn v1.0.109|clone-impls,default,derive,parsing,printing,proc-macro,quote|"),
1230 Ok("3|proc-macro2 v1.0.81|default,proc-macro| (*)"),
1231 Ok("3|quote v1.0.36|default,proc-macro| (*)"),
1232 Ok("3|unicode-ident v1.0.12||"),
1233 ]
1234 .into_iter()
1235 )
1236 .unwrap();
1237 assert_eq!(
1238 BTreeMap::from([
1239 (
1240 autocfg_id.clone(),
1241 CargoTreeEntry {
1242 features: BTreeSet::new(),
1243 deps: BTreeSet::new(),
1244 },
1245 ),
1246 (
1247 chrono_id.clone(),
1248 CargoTreeEntry {
1249 features: BTreeSet::from([
1250 "clock".to_owned(),
1251 "default".to_owned(),
1252 "iana-time-zone".to_owned(),
1253 "js-sys".to_owned(),
1254 "oldtime".to_owned(),
1255 "std".to_owned(),
1256 "time".to_owned(),
1257 "wasm-bindgen".to_owned(),
1258 "wasmbind".to_owned(),
1259 "winapi".to_owned(),
1260 ]),
1261 deps: BTreeSet::from([
1262 iana_time_zone_id.clone(),
1263 num_integer_id.clone(),
1264 num_traits_id.clone(),
1265 time_id.clone(),
1266 ]),
1267 }
1268 ),
1269 (
1270 core_foundation_sys_id.clone(),
1271 CargoTreeEntry {
1272 features: BTreeSet::from(["default".to_owned(), "link".to_owned()]),
1273 deps: BTreeSet::new(),
1274 }
1275 ),
1276 (
1277 cpufeatures_id.clone(),
1278 CargoTreeEntry {
1279 features: BTreeSet::new(),
1280 deps: BTreeSet::from([libc_id.clone()]),
1281 },
1282 ),
1283 (
1284 iana_time_zone_id,
1285 CargoTreeEntry {
1286 features: BTreeSet::from(["fallback".to_owned()]),
1287 deps: BTreeSet::from([core_foundation_sys_id]),
1288 }
1289 ),
1290 (
1291 libc_id.clone(),
1292 CargoTreeEntry {
1293 features: BTreeSet::from(["default".to_owned(), "std".to_owned()]),
1294 deps: BTreeSet::new(),
1295 }
1296 ),
1297 (
1298 num_integer_id,
1299 CargoTreeEntry {
1300 features: BTreeSet::new(),
1301 deps: BTreeSet::from([num_traits_id.clone()]),
1302 },
1303 ),
1304 (
1305 num_traits_id,
1306 CargoTreeEntry {
1307 features: BTreeSet::from(["i128".to_owned()]),
1308 deps: BTreeSet::from([autocfg_id]),
1309 }
1310 ),
1311 (
1312 proc_macro2_id.clone(),
1313 CargoTreeEntry {
1314 features: BTreeSet::from(["default".to_owned(), "proc-macro".to_owned()]),
1315 deps: BTreeSet::from([unicode_ident_id.clone()])
1316 }
1317 ),
1318 (
1319 quote_id.clone(),
1320 CargoTreeEntry {
1321 features: BTreeSet::from(["default".to_owned(), "proc-macro".to_owned()]),
1322 deps: BTreeSet::from([proc_macro2_id.clone()]),
1323 }
1324 ),
1325 (
1326 serde_derive_id.clone(),
1327 CargoTreeEntry {
1328 features: BTreeSet::from(["default".to_owned()]),
1329 deps: BTreeSet::from([
1330 proc_macro2_id.clone(),
1331 quote_id.clone(),
1332 syn_id.clone()
1333 ]),
1334 }
1335 ),
1336 (
1337 syn_id,
1338 CargoTreeEntry {
1339 features: BTreeSet::from([
1340 "clone-impls".to_owned(),
1341 "default".to_owned(),
1342 "derive".to_owned(),
1343 "parsing".to_owned(),
1344 "printing".to_owned(),
1345 "proc-macro".to_owned(),
1346 "quote".to_owned(),
1347 ]),
1348 deps: BTreeSet::from([proc_macro2_id, quote_id, unicode_ident_id.clone(),]),
1349 }
1350 ),
1351 (
1352 time_id,
1353 CargoTreeEntry {
1354 features: BTreeSet::new(),
1355 deps: BTreeSet::from([libc_id]),
1356 }
1357 ),
1358 (
1359 tree_data_id,
1360 CargoTreeEntry {
1361 features: BTreeSet::new(),
1362 deps: BTreeSet::from([chrono_id, cpufeatures_id, serde_derive_id,]),
1363 }
1364 ),
1365 (
1366 unicode_ident_id,
1367 CargoTreeEntry {
1368 features: BTreeSet::new(),
1369 deps: BTreeSet::new()
1370 }
1371 )
1372 ]),
1373 output,
1374 );
1375 }
1376
1377 #[test]
serde_cargo_tree_entry()1378 fn serde_cargo_tree_entry() {
1379 {
1380 let entry: CargoTreeEntry = serde_json::from_str("{}").unwrap();
1381 assert_eq!(CargoTreeEntry::new(), entry);
1382 }
1383 {
1384 let entry: CargoTreeEntry =
1385 serde_json::from_str(r#"{"features": ["default"]}"#).unwrap();
1386 assert_eq!(
1387 CargoTreeEntry {
1388 features: BTreeSet::from(["default".to_owned()]),
1389 deps: BTreeSet::new(),
1390 },
1391 entry
1392 );
1393 }
1394 {
1395 let entry: CargoTreeEntry =
1396 serde_json::from_str(r#"{"deps": ["common 1.2.3"]}"#).unwrap();
1397 assert_eq!(
1398 CargoTreeEntry {
1399 features: BTreeSet::new(),
1400 deps: BTreeSet::from([CrateId::new(
1401 "common".to_owned(),
1402 Version::new(1, 2, 3)
1403 )]),
1404 },
1405 entry
1406 );
1407 }
1408 {
1409 let entry: CargoTreeEntry =
1410 serde_json::from_str(r#"{"features": ["default"], "deps": ["common 1.2.3"]}"#)
1411 .unwrap();
1412 assert_eq!(
1413 CargoTreeEntry {
1414 features: BTreeSet::from(["default".to_owned()]),
1415 deps: BTreeSet::from([CrateId::new(
1416 "common".to_owned(),
1417 Version::new(1, 2, 3)
1418 )]),
1419 },
1420 entry
1421 );
1422 }
1423 }
1424
1425 #[test]
parse_registry_source()1426 fn parse_registry_source() {
1427 let source = Source::parse(
1428 "registry+https://github.com/rust-lang/crates.io-index",
1429 "1.0.1".to_owned(),
1430 )
1431 .unwrap();
1432 assert_eq!(
1433 source,
1434 Source::Registry {
1435 registry: "https://github.com/rust-lang/crates.io-index".to_owned(),
1436 version: "1.0.1".to_owned()
1437 }
1438 );
1439 }
1440
1441 #[test]
parse_git_source()1442 fn parse_git_source() {
1443 let source = Source::parse("git+https://github.com/serde-rs/serde.git?rev=9b868ef831c95f50dd4bde51a7eb52e3b9ee265a#9b868ef831c95f50dd4bde51a7eb52e3b9ee265a", "unused".to_owned()).unwrap();
1444 assert_eq!(
1445 source,
1446 Source::Git {
1447 git: "https://github.com/serde-rs/serde.git".to_owned(),
1448 rev: Some("9b868ef831c95f50dd4bde51a7eb52e3b9ee265a".to_owned()),
1449 branch: None,
1450 tag: None,
1451 }
1452 );
1453 }
1454 }
1455