1use std::{
5 cell::RefCell,
6 collections::{BTreeMap, HashMap, HashSet},
7 path::PathBuf,
8 rc::Rc,
9};
10
11use cargo_lock::{Lockfile, Version};
12use cargo_metadata::{camino::Utf8PathBuf, DependencyKind, MetadataCommand, PackageId};
13use target_spec::{Platform, TargetSpec};
14use tracing::{instrument, trace};
15
16use crate::Error;
17
18use super::Source;
19
20mod visitor;
21
22pub use visitor::Visitor;
23
24#[derive(Debug, PartialEq, Clone)]
26pub struct Package {
27 pub(super) name: String,
28 pub(super) version: Version,
29 pub(super) source: Source,
30 pub(super) lib_name: Option<String>,
31 pub(super) lib_path: Option<Utf8PathBuf>,
32 pub(super) build_path: Option<Utf8PathBuf>,
33 pub(super) proc_macro: bool,
34
35 pub(super) features: HashMap<String, Vec<String>>,
37
38 pub(super) enabled_features: HashSet<String>,
40 pub(super) dependencies: Vec<Dependency>,
41 pub(super) build_dependencies: Vec<Dependency>,
42 pub(super) edition: String,
43}
44
45#[derive(Debug, PartialEq, Clone)]
53pub struct Dependency {
54 pub(super) name: String,
55 pub(super) package: Rc<RefCell<Package>>,
56 pub(super) optional: bool,
57 pub(super) uses_default_features: bool,
58 pub(super) features: Vec<String>,
59}
60
61impl Package {
62 pub fn from_current_dir(path: impl Into<PathBuf>) -> Result<Self, Error> {
64 let platform = Platform::current()?;
65
66 let metadata = MetadataCommand::new()
67 .current_dir(path)
68 .other_options(vec![
69 "--filter-platform".to_string(),
70 platform.triple_str().to_string(),
71 ])
72 .exec()?;
73 let lock_file = metadata.workspace_root.join("Cargo.lock");
74 let lock_file = Lockfile::load(lock_file)?;
75
76 trace!(?platform, ?metadata, ?lock_file, "have metadata");
77
78 let packages =
79 BTreeMap::from_iter(metadata.packages.iter().map(|p| (p.id.clone(), p.clone())));
80 let nodes = BTreeMap::from_iter(
81 metadata
82 .resolve
83 .as_ref()
84 .expect("metadata to have a resolve section")
85 .nodes
86 .iter()
87 .map(|n| (n.id.clone(), n.clone())),
88 );
89 let checksums = BTreeMap::from_iter(lock_file.packages.iter().filter_map(|p| {
90 p.checksum.as_ref().map(|checksum| {
91 (
92 (p.name.to_string(), p.version.to_string()),
93 checksum.to_string(),
94 )
95 })
96 }));
97
98 let root_id = metadata
99 .resolve
100 .as_ref()
101 .expect("metadata to have a resolve section")
102 .root
103 .as_ref()
104 .expect("a root from metadata")
105 .clone();
106
107 let mut resolved_packages = Default::default();
108
109 Ok(Self::get_package(
110 root_id,
111 &packages,
112 &nodes,
113 &checksums,
114 &mut resolved_packages,
115 &platform,
116 ))
117 }
118
119 fn get_package(
122 id: PackageId,
123 packages: &BTreeMap<PackageId, cargo_metadata::Package>,
124 nodes: &BTreeMap<PackageId, cargo_metadata::Node>,
125 checksums: &BTreeMap<(String, String), String>,
126 resolved_packages: &mut BTreeMap<PackageId, Rc<RefCell<Package>>>,
127 platform: &Platform,
128 ) -> Self {
129 let node = nodes.get(&id).expect("node to exist").clone();
130 let package = packages.get(&id).expect("package to exist");
131
132 trace!(
133 package.name,
134 ?package.features,
135 ?node,
136 "found package and node"
137 );
138
139 let features = package.features.clone();
140 let package_dependencies: Vec<_> = package
141 .dependencies
142 .iter()
143 .filter(|d| d.kind == DependencyKind::Normal)
144 .cloned()
145 .collect();
146 let package_build_dependencies: Vec<_> = package
147 .dependencies
148 .iter()
149 .filter(|d| d.kind == DependencyKind::Build)
150 .cloned()
151 .collect();
152
153 let dependencies = node
154 .dependencies
155 .iter()
156 .filter_map(|id| {
157 Dependency::get_dependency(
158 id,
159 &package_dependencies,
160 packages,
161 nodes,
162 checksums,
163 resolved_packages,
164 platform,
165 )
166 })
167 .collect();
168 let build_dependencies = node
169 .dependencies
170 .iter()
171 .filter_map(|id| {
172 Dependency::get_dependency(
173 id,
174 &package_build_dependencies,
175 packages,
176 nodes,
177 checksums,
178 resolved_packages,
179 platform,
180 )
181 })
182 .collect();
183
184 let package_path: PathBuf = package.manifest_path.parent().unwrap().into();
186
187 let (lib_path, lib_name) = package
188 .targets
189 .iter()
190 .find(|t| {
191 t.kind.iter().any(|k| {
192 matches!(
193 k.as_str(),
194 "lib" | "cdylib" | "dylib" | "rlib" | "proc-macro"
195 )
196 })
197 })
198 .map(|t| {
199 (
200 t.src_path
201 .strip_prefix(&package_path)
202 .unwrap() .to_path_buf(),
204 t.name.clone(),
205 )
206 })
207 .unzip();
208 let build_path = package
209 .targets
210 .iter()
211 .find(|t| t.kind.iter().any(|k| k == "custom-build"))
212 .map(|t| {
213 t.src_path
214 .strip_prefix(&package_path)
215 .unwrap() .to_path_buf()
217 });
218 let proc_macro = package
219 .targets
220 .iter()
221 .any(|t| t.kind.iter().any(|k| k == "proc-macro"));
222
223 let source = if package.source.is_some() {
224 let checksum = checksums
225 .get(&(package.name.to_string(), package.version.to_string()))
226 .expect("to have a checksum");
227 Source::CratesIo(checksum.to_string())
228 } else {
229 Source::Local(package_path)
230 };
231
232 Self {
233 name: package.name.clone(),
234 version: package.version.clone(),
235 source,
236 lib_name,
237 lib_path,
238 build_path,
239 proc_macro,
240 dependencies,
241 build_dependencies,
242 features,
243 enabled_features: Default::default(),
244 edition: package.edition.to_string(),
245 }
246 }
247
248 pub fn resolve(&mut self) {
251 self.visit(&mut visitor::ResolveVisitor);
252 }
253
254 fn visit(&mut self, visitor: &mut impl visitor::Visitor) {
256 visitor.visit(self);
257 }
258
259 pub fn dependencies_iter(&self) -> impl Iterator<Item = &Dependency> {
261 self.dependencies
262 .iter()
263 .chain(self.build_dependencies.iter())
264 }
265
266 pub fn dependencies_iter_mut(&mut self) -> impl Iterator<Item = &mut Dependency> {
268 self.dependencies
269 .iter_mut()
270 .chain(self.build_dependencies.iter_mut())
271 }
272}
273
274impl Dependency {
275 #[instrument(skip_all, fields(%id))]
278 fn get_dependency(
279 id: &PackageId,
280 parent_dependencies: &[cargo_metadata::Dependency],
281 packages: &BTreeMap<PackageId, cargo_metadata::Package>,
282 nodes: &BTreeMap<PackageId, cargo_metadata::Node>,
283 checksums: &BTreeMap<(String, String), String>,
284 resolved_packages: &mut BTreeMap<PackageId, Rc<RefCell<Package>>>,
285 platform: &Platform,
286 ) -> Option<Self> {
287 let package = match resolved_packages.get(id) {
288 Some(package) => Rc::clone(package),
289 None => {
290 let package = RefCell::new(Package::get_package(
291 id.clone(),
292 packages,
293 nodes,
294 checksums,
295 resolved_packages,
296 platform,
297 ))
298 .into();
299
300 resolved_packages.insert(id.clone(), Rc::clone(&package));
301
302 package
303 }
304 };
305
306 let name = package.borrow().name.clone();
308 let version = package.borrow().version.clone();
309
310 let dependencies: Vec<_> = parent_dependencies
314 .iter()
315 .filter(|d| d.name == name)
316 .filter(|d| d.req.matches(&version))
317 .filter(|d| match &d.target {
318 Some(target_spec) => {
319 let target_spec = TargetSpec::new(target_spec.to_string()).unwrap();
321
322 target_spec.eval(platform).unwrap_or(false)
323 }
324 None => true,
325 })
326 .collect();
327
328 if dependencies.is_empty() {
331 return None;
332 }
333
334 let mut optional = true;
336 let mut uses_default_features = false;
337 let mut features: Vec<String> = Default::default();
338 let mut dependency_name: String = Default::default();
339 let mut dependency_rename = None;
340
341 for dependency in dependencies {
342 if !dependency.optional {
343 optional = false;
344 }
345
346 if dependency.uses_default_features {
347 uses_default_features = true;
348 }
349
350 features.extend(dependency.features.iter().cloned());
352
353 if dependency_rename.is_none() && dependency.rename.is_some() {
354 dependency_rename = dependency.rename.clone();
355 }
356
357 if dependency_name.is_empty() {
358 dependency_name = dependency.name.clone();
359 }
360 }
361
362 if let Some(dependency_rename) = dependency_rename {
363 dependency_name = dependency_rename;
364 };
365
366 trace!(
367 name,
368 dependency_name,
369 optional,
370 uses_default_features,
371 ?features,
372 "done with dependency"
373 );
374
375 Some(Self {
376 name: dependency_name,
377 package,
378 optional,
379 uses_default_features,
380 features,
381 })
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use std::{cell::RefCell, collections::HashMap, path::PathBuf, str::FromStr};
388
389 use crate::models::cargo::{Dependency, Package};
390
391 use pretty_assertions::assert_eq;
392
393 #[test]
394 fn simple_package() {
395 let path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR"))
396 .unwrap()
397 .join("tests")
398 .join("simple");
399
400 let package = Package::from_current_dir(path.clone()).unwrap();
401
402 assert_eq!(
403 package,
404 Package {
405 name: "simple".to_string(),
406 source: path.into(),
407 lib_name: None,
408 lib_path: None,
409 build_path: None,
410 proc_macro: false,
411 version: "0.1.0".parse().unwrap(),
412 dependencies: vec![Dependency {
413 name: "itoa".to_string(),
414 package: RefCell::new(Package {
415 name: "itoa".to_string(),
416 version: "1.0.6".parse().unwrap(),
417 source: "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
418 .into(),
419 lib_name: Some("itoa".to_string()),
420 lib_path: Some("src/lib.rs".into()),
421 build_path: None,
422 proc_macro: false,
423 dependencies: Default::default(),
424 build_dependencies: Default::default(),
425 features: HashMap::from([(
426 "no-panic".to_string(),
427 vec!["dep:no-panic".to_string()]
428 )]),
429 enabled_features: Default::default(),
430 edition: "2018".to_string(),
431 })
432 .into(),
433 optional: false,
434 uses_default_features: true,
435 features: Default::default(),
436 },],
437 build_dependencies: vec![Dependency {
438 name: "arbitrary".to_string(),
439 package: RefCell::new(Package {
440 name: "arbitrary".to_string(),
441 version: "1.3.0".parse().unwrap(),
442 source: "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
443 .into(),
444 lib_name: Some("arbitrary".to_string()),
445 lib_path: Some("src/lib.rs".into()),
446 build_path: None,
447 proc_macro: false,
448 dependencies: Default::default(),
449 build_dependencies: Default::default(),
450 features: HashMap::from([
451 ("derive".to_string(), vec!["derive_arbitrary".to_string()]),
452 (
453 "derive_arbitrary".to_string(),
454 vec!["dep:derive_arbitrary".to_string()]
455 ),
456 ]),
457 enabled_features: Default::default(),
458 edition: "2018".to_string(),
459 })
460 .into(),
461 optional: false,
462 uses_default_features: true,
463 features: Default::default(),
464 },],
465 features: Default::default(),
466 enabled_features: Default::default(),
467 edition: "2021".to_string(),
468 }
469 );
470 }
471
472 #[test]
473 fn workspace() {
474 let workspace = PathBuf::from_str(env!("CARGO_MANIFEST_DIR"))
475 .unwrap()
476 .join("tests")
477 .join("workspace");
478 let path = workspace.join("parent");
479
480 let package = Package::from_current_dir(path.clone()).unwrap();
481
482 assert_eq!(
483 package,
484 Package {
485 name: "parent".to_string(),
486 version: "0.1.0".parse().unwrap(),
487 source: path.into(),
488 lib_name: None,
489 lib_path: None,
490 build_path: None,
491 proc_macro: false,
492 dependencies: vec![
493 Dependency {
494 name: "child".to_string(),
495 package: RefCell::new(Package {
496 name: "child".to_string(),
497 version: "0.1.0".parse().unwrap(),
498 source: workspace.join("child").into(),
499 lib_name: Some("child".to_string()),
500 lib_path: Some("src/lib.rs".into()),
501 build_path: None,
502 proc_macro: false,
503 dependencies: vec![
504 Dependency {
505 name: "fnv".to_string(),
506 package: RefCell::new(Package {
507 name: "fnv".to_string(),
508 version: "1.0.7".parse().unwrap(),
509 source: "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1".into(),
510 lib_name: Some("fnv".to_string()),
511 lib_path: Some("lib.rs".into()),
512 build_path: None,
513 proc_macro: false,
514 dependencies: Default::default(),
515 build_dependencies: Default::default(),
516 features: HashMap::from([
517 ("default".to_string(), vec!["std".to_string()]),
518 ("std".to_string(), vec![]),
519 ]),
520 enabled_features: Default::default(),
521 edition: "2015".to_string(),
522 })
523 .into(),
524 optional: false,
525 uses_default_features: true,
526 features: Default::default(),
527 },
528 Dependency {
529 name: "itoa".to_string(),
530 package: RefCell::new(Package {
531 name: "itoa".to_string(),
532 version: "1.0.6".parse().unwrap(),
533 source: "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6".into(),
534 lib_name: Some("itoa".to_string()),
535 lib_path: Some("src/lib.rs".into()),
536 build_path: None,
537 proc_macro: false,
538 dependencies: Default::default(),
539 build_dependencies: Default::default(),
540 features: HashMap::from([(
541 "no-panic".to_string(),
542 vec!["dep:no-panic".to_string()]
543 )]),
544 enabled_features: Default::default(),
545 edition: "2018".to_string(),
546 })
547 .into(),
548 optional: false,
549 uses_default_features: true,
550 features: Default::default(),
551 },
552 Dependency {
553 name: "libc".to_string(),
554 package: RefCell::new(Package {
555 name: "libc".to_string(),
556 version: "0.2.144".parse().unwrap(),
557 source: "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1".into(),
558 lib_name: Some("libc".to_string()),
559 lib_path: Some("src/lib.rs".into()),
560 build_path: Some("build.rs".into()),
561 proc_macro: false,
562 dependencies: Default::default(),
563 build_dependencies: Default::default(),
564 features: HashMap::from([
565 ("std".to_string(), vec![]),
566 ("default".to_string(), vec!["std".to_string()]),
567 ("use_std".to_string(), vec!["std".to_string()]),
568 ("extra_traits".to_string(), vec![]),
569 ("align".to_string(), vec![]),
570 (
571 "rustc-dep-of-std".to_string(),
572 vec![
573 "align".to_string(),
574 "rustc-std-workspace-core".to_string()
575 ]
576 ),
577 ("const-extern-fn".to_string(), vec![]),
578 (
579 "rustc-std-workspace-core".to_string(),
580 vec!["dep:rustc-std-workspace-core".to_string()]
581 ),
582 ]),
583 enabled_features: Default::default(),
584 edition: "2015".to_string(),
585 })
586 .into(),
587 optional: false,
588 uses_default_features: true,
589 features: Default::default(),
590 },
591 Dependency {
592 name: "new_name".to_string(),
593 package: RefCell::new(Package {
594 name: "rename".to_string(),
595 version: "0.1.0".parse().unwrap(),
596 source: workspace.join("rename").into(),
597 lib_name: Some("lib_rename".to_string()),
598 lib_path: Some("src/lib.rs".into()),
599 build_path: None,
600 proc_macro: false,
601 dependencies: Default::default(),
602 build_dependencies: Default::default(),
603 features: Default::default(),
604 enabled_features: Default::default(),
605 edition: "2021".to_string(),
606 })
607 .into(),
608 optional: true,
609 uses_default_features: true,
610 features: Default::default(),
611 },
612 Dependency {
613 name: "rustversion".to_string(),
614 package: RefCell::new(Package {
615 name: "rustversion".to_string(),
616 version: "1.0.12".parse().unwrap(),
617 source: "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06".into(),
618 lib_name: Some("rustversion".to_string()),
619 lib_path: Some("src/lib.rs".into()),
620 build_path: Some("build/build.rs".into()),
621 proc_macro: true,
622 dependencies: Default::default(),
623 build_dependencies: Default::default(),
624 features: Default::default(),
625 enabled_features: Default::default(),
626 edition: "2018".to_string(),
627 })
628 .into(),
629 optional: false,
630 uses_default_features: true,
631 features: Default::default(),
632 },
633 ],
634 build_dependencies: Default::default(),
635 features: HashMap::from([
636 (
637 "default".to_string(),
638 vec!["one".to_string(), "two".to_string()]
639 ),
640 ("one".to_string(), vec!["new_name".to_string()]),
641 ("two".to_string(), vec![]),
642 ("new_name".to_string(), vec!["dep:new_name".to_string()]),
643 ]),
644 enabled_features: Default::default(),
645 edition: "2021".to_string(),
646 })
647 .into(),
648 optional: false,
649 uses_default_features: false,
650 features: vec!["one".to_string()],
651 },
652 Dependency {
653 name: "itoa".to_string(),
654 package: RefCell::new(Package {
655 name: "itoa".to_string(),
656 version: "0.4.8".parse().unwrap(),
657 source: "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4".into(),
658 lib_name: Some("itoa".to_string()),
659 lib_path: Some("src/lib.rs".into()),
660 build_path: None,
661 proc_macro: false,
662 dependencies: Default::default(),
663 build_dependencies: Default::default(),
664 features: HashMap::from([
665 ("default".to_string(), vec!["std".to_string()]),
666 ("std".to_string(), vec![]),
667 ("i128".to_string(), vec![]),
668 ]),
669 enabled_features: Default::default(),
670 edition: "2015".to_string(),
671 })
672 .into(),
673 optional: false,
674 uses_default_features: true,
675 features: Default::default(),
676 },
677 Dependency {
678 name: "libc".to_string(),
679 package: RefCell::new(Package {
680 name: "libc".to_string(),
681 version: "0.2.144".parse().unwrap(),
682 source: "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1".into(),
683 lib_name: Some("libc".to_string()),
684 lib_path: Some("src/lib.rs".into()),
685 build_path: Some("build.rs".into()),
686 proc_macro: false,
687 dependencies: Default::default(),
688 build_dependencies: Default::default(),
689 features: HashMap::from([
690 ("std".to_string(), vec![]),
691 ("default".to_string(), vec!["std".to_string()]),
692 ("use_std".to_string(), vec!["std".to_string()]),
693 ("extra_traits".to_string(), vec![]),
694 ("align".to_string(), vec![]),
695 (
696 "rustc-dep-of-std".to_string(),
697 vec![
698 "align".to_string(),
699 "rustc-std-workspace-core".to_string()
700 ]
701 ),
702 ("const-extern-fn".to_string(), vec![]),
703 (
704 "rustc-std-workspace-core".to_string(),
705 vec!["dep:rustc-std-workspace-core".to_string()]
706 ),
707 ]),
708 enabled_features: Default::default(),
709 edition: "2015".to_string(),
710 })
711 .into(),
712 optional: false,
713 uses_default_features: true,
714 features: Default::default(),
715 },
716 Dependency {
717 name: "targets".to_string(),
718 package: RefCell::new(Package {
719 name: "targets".to_string(),
720 version: "0.1.0".parse().unwrap(),
721 source: workspace.join("targets").into(),
722 lib_name: Some("targets".to_string()),
723 lib_path: Some("src/lib.rs".into()),
724 build_path: None,
725 proc_macro: false,
726 dependencies: Default::default(),
727 build_dependencies: Default::default(),
728 features: HashMap::from([
729 ("unix".to_string(), vec![]),
730 ("windows".to_string(), vec![]),
731 ]),
732 enabled_features: Default::default(),
733 edition: "2021".to_string(),
734 })
735 .into(),
736 optional: false,
737 uses_default_features: true,
738 features: vec!["unix".to_string()],
739 },
740 ],
741 build_dependencies: Default::default(),
742 features: Default::default(),
743 enabled_features: Default::default(),
744 edition: "2021".to_string(),
745 }
746 );
747 }
748}