1use crate::util::data_structures::{HashMap, HashSet};
2use std::cell::{Cell, RefCell};
3use std::fmt::{self, Debug, Formatter};
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7
8use crate::ops;
9use crate::sources::IndexSummary;
10use crate::sources::source::MaybePackage;
11use crate::sources::source::QueryKind;
12use crate::sources::source::Source;
13use crate::util::GlobalContext;
14use crate::util::VersionReqMatchMode;
15use crate::util::errors::CargoResult;
16use crate::util::important_paths::find_project_manifest_exact;
17use crate::util::internal;
18use crate::workspace::parser::read_manifest;
19use crate::workspace::{Dependency, EitherManifest, Manifest, Package, PackageId, SourceId};
20use anyhow::Context as _;
21use cargo_util::paths;
22use filetime::FileTime;
23use gix::bstr::{BString, ByteVec};
24use gix::dir::entry::Status;
25use gix::index::entry::Stage;
26use ignore::gitignore::GitignoreBuilder;
27use tracing::{debug, info, trace, warn};
28use walkdir::WalkDir;
29
30pub struct PathSource<'gctx> {
36 source_id: SourceId,
38 path: PathBuf,
40 package: RefCell<Option<Option<Package>>>,
42 gctx: &'gctx GlobalContext,
43}
44
45impl<'gctx> PathSource<'gctx> {
46 pub fn new(path: &Path, source_id: SourceId, gctx: &'gctx GlobalContext) -> Self {
51 Self {
52 source_id,
53 path: path.to_path_buf(),
54 package: RefCell::new(None),
55 gctx,
56 }
57 }
58
59 pub fn preload_with(pkg: Package, gctx: &'gctx GlobalContext) -> Self {
62 let source_id = pkg.package_id().source_id();
63 let path = pkg.root().to_owned();
64 Self {
65 source_id,
66 path,
67 package: RefCell::new(Some(Some(pkg))),
68 gctx,
69 }
70 }
71
72 pub fn root_package(&self) -> CargoResult<Package> {
74 trace!("root_package; source={:?}", self);
75
76 self.load()?;
77
78 match &*self.package.borrow() {
79 Some(Some(pkg)) => Ok(pkg.clone()),
80 Some(None) | None => Err(anyhow::format_err!(
81 "failed to read `{}`",
82 self.path.join("Cargo.toml").display()
83 )),
84 }
85 }
86
87 #[tracing::instrument(skip_all)]
98 pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
99 list_files(pkg, self.gctx)
100 }
101
102 fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
104 if self.package.borrow().is_none() {
105 return Err(internal(format!(
106 "BUG: source `{:?}` was not loaded",
107 self.path
108 )));
109 }
110 last_modified_file(&self.path, pkg, self.gctx)
111 }
112
113 pub fn path(&self) -> &Path {
115 &self.path
116 }
117
118 pub fn load(&self) -> CargoResult<()> {
120 let mut package = self.package.borrow_mut();
121 if package.is_none() {
122 *package = Some(self.read_package()?);
123 }
124
125 Ok(())
126 }
127
128 fn read_package(&self) -> CargoResult<Option<Package>> {
131 let path = self.path.join("Cargo.toml");
132 if !path.exists() {
133 return Ok(None);
134 }
135 Ok(Some(ops::read_package(&path, self.source_id, self.gctx)?))
136 }
137}
138
139impl<'gctx> Debug for PathSource<'gctx> {
140 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
141 write!(f, "the paths source")
142 }
143}
144
145#[async_trait::async_trait(?Send)]
146impl<'gctx> Source for PathSource<'gctx> {
147 async fn query(
148 &self,
149 dep: &Dependency,
150 kind: QueryKind,
151 f: &mut dyn FnMut(IndexSummary),
152 ) -> CargoResult<()> {
153 self.load()?;
154 if let Some(Some(p)) = &*self.package.borrow() {
155 let s = p.summary();
156 let matched = match kind {
157 QueryKind::Exact | QueryKind::RejectedVersions => {
158 dep.matches(s, VersionReqMatchMode::Default)
159 }
160 QueryKind::AlternativeNames => true,
161 QueryKind::Normalized => dep.matches(s, VersionReqMatchMode::Default),
162 };
163 if matched {
164 f(IndexSummary::Candidate(s.clone()))
165 }
166 }
167 Ok(())
168 }
169
170 fn supports_checksums(&self) -> bool {
171 false
172 }
173
174 fn requires_precise(&self) -> bool {
175 false
176 }
177
178 fn source_id(&self) -> SourceId {
179 self.source_id
180 }
181
182 async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
183 trace!("getting packages; id={}", id);
184 self.load()?;
185 let pkg = self.package.borrow();
186 let pkg = pkg
187 .as_ref()
188 .and_then(|p| p.as_ref())
189 .filter(|pkg| pkg.package_id() == id);
190 pkg.cloned()
191 .map(MaybePackage::Ready)
192 .ok_or_else(|| internal(format!("failed to find {} in path source", id)))
193 }
194
195 async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
196 panic!("no download should have started")
197 }
198
199 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
200 let (max, max_path) = self.last_modified_file(pkg)?;
201 let max_path = max_path.strip_prefix(&self.path).unwrap_or(&max_path);
205 Ok(format!("{} ({})", max, max_path.display()))
206 }
207
208 fn describe(&self) -> String {
209 match self.source_id.url().to_file_path() {
210 Ok(path) => path.display().to_string(),
211 Err(_) => self.source_id.to_string(),
212 }
213 }
214
215 fn invalidate_cache(&self) {
216 }
218
219 fn set_quiet(&mut self, _quiet: bool) {
220 }
222}
223
224pub struct RecursivePathSource<'gctx> {
227 source_id: SourceId,
229 path: PathBuf,
231 loaded: Cell<bool>,
233 packages: RefCell<HashMap<PackageId, Vec<Package>>>,
237 warned_duplicate: RefCell<HashSet<PackageId>>,
239 gctx: &'gctx GlobalContext,
240}
241
242impl<'gctx> RecursivePathSource<'gctx> {
243 pub fn new(root: &Path, source_id: SourceId, gctx: &'gctx GlobalContext) -> Self {
252 Self {
253 source_id,
254 path: root.to_path_buf(),
255 loaded: Cell::new(false),
256 packages: Default::default(),
257 warned_duplicate: Default::default(),
258 gctx,
259 }
260 }
261
262 pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
265 self.load()?;
266 Ok(self
267 .packages
268 .borrow()
269 .iter()
270 .map(|(pkg_id, v)| {
271 first_package(
272 *pkg_id,
273 v,
274 &mut self.warned_duplicate.borrow_mut(),
275 self.gctx,
276 )
277 .clone()
278 })
279 .collect())
280 }
281
282 pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
293 list_files(pkg, self.gctx)
294 }
295
296 fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
298 if !self.loaded.get() {
299 return Err(internal(format!(
300 "BUG: source `{:?}` was not loaded",
301 self.path
302 )));
303 }
304 last_modified_file(&self.path, pkg, self.gctx)
305 }
306
307 pub fn path(&self) -> &Path {
309 &self.path
310 }
311
312 pub fn load(&self) -> CargoResult<()> {
314 if !self.loaded.get() {
315 self.packages
316 .replace(read_packages(&self.path, self.source_id, self.gctx)?);
317 self.loaded.set(true);
318 }
319
320 Ok(())
321 }
322}
323
324impl<'gctx> Debug for RecursivePathSource<'gctx> {
325 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
326 write!(f, "the paths source")
327 }
328}
329
330#[async_trait::async_trait(?Send)]
331impl<'gctx> Source for RecursivePathSource<'gctx> {
332 async fn query(
333 &self,
334 dep: &Dependency,
335 kind: QueryKind,
336 f: &mut dyn FnMut(IndexSummary),
337 ) -> CargoResult<()> {
338 self.load()?;
339 for s in self
340 .packages
341 .borrow()
342 .iter()
343 .filter(|(pkg_id, _)| pkg_id.name() == dep.package_name())
344 .map(|(pkg_id, pkgs)| {
345 first_package(
346 *pkg_id,
347 pkgs,
348 &mut self.warned_duplicate.borrow_mut(),
349 self.gctx,
350 )
351 })
352 .map(|p| p.summary())
353 {
354 let matched = match kind {
355 QueryKind::Exact | QueryKind::RejectedVersions => {
356 dep.matches(s, VersionReqMatchMode::Default)
357 }
358 QueryKind::AlternativeNames => true,
359 QueryKind::Normalized => dep.matches(s, VersionReqMatchMode::Default),
360 };
361 if matched {
362 f(IndexSummary::Candidate(s.clone()))
363 }
364 }
365 Ok(())
366 }
367
368 fn supports_checksums(&self) -> bool {
369 false
370 }
371
372 fn requires_precise(&self) -> bool {
373 false
374 }
375
376 fn source_id(&self) -> SourceId {
377 self.source_id
378 }
379
380 async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
381 trace!("getting packages; id={}", id);
382 self.load()?;
383 let pkgs = self.packages.borrow();
384 let pkg = pkgs.get(&id);
385 pkg.map(|pkgs| {
386 first_package(id, pkgs, &mut self.warned_duplicate.borrow_mut(), self.gctx).clone()
387 })
388 .map(MaybePackage::Ready)
389 .ok_or_else(|| internal(format!("failed to find {} in path source", id)))
390 }
391
392 async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
393 panic!("no download should have started")
394 }
395
396 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
397 let (max, max_path) = self.last_modified_file(pkg)?;
398 let max_path = max_path.strip_prefix(&self.path).unwrap_or(&max_path);
402 Ok(format!("{} ({})", max, max_path.display()))
403 }
404
405 fn describe(&self) -> String {
406 match self.source_id.url().to_file_path() {
407 Ok(path) => path.display().to_string(),
408 Err(_) => self.source_id.to_string(),
409 }
410 }
411
412 fn invalidate_cache(&self) {
413 }
415
416 fn set_quiet(&mut self, _quiet: bool) {
417 }
419}
420
421#[derive(Debug, Clone, Copy)]
423enum FileType {
424 File { maybe_symlink: bool },
425 Dir,
426 Symlink,
427 Other,
428}
429
430impl From<fs::FileType> for FileType {
431 fn from(value: fs::FileType) -> Self {
432 if value.is_file() {
433 FileType::File {
434 maybe_symlink: false,
435 }
436 } else if value.is_dir() {
437 FileType::Dir
438 } else if value.is_symlink() {
439 FileType::Symlink
440 } else {
441 FileType::Other
442 }
443 }
444}
445
446impl From<gix::dir::entry::Kind> for FileType {
447 fn from(value: gix::dir::entry::Kind) -> Self {
448 use gix::dir::entry::Kind;
449 match value {
450 Kind::Untrackable => FileType::Other,
451 Kind::File => FileType::File {
452 maybe_symlink: false,
453 },
454 Kind::Symlink => FileType::Symlink,
455 Kind::Directory | Kind::Repository => FileType::Dir,
456 }
457 }
458}
459
460#[derive(Clone, Debug)]
462pub struct PathEntry {
463 path: PathBuf,
464 ty: FileType,
465 under_symlink_dir: bool,
467}
468
469impl PathEntry {
470 pub fn into_path_buf(self) -> PathBuf {
471 self.path
472 }
473
474 pub fn is_file(&self) -> bool {
477 matches!(self.ty, FileType::File { .. })
478 }
479
480 pub fn is_dir(&self) -> bool {
483 matches!(self.ty, FileType::Dir)
484 }
485
486 pub fn is_symlink(&self) -> bool {
493 matches!(self.ty, FileType::Symlink)
494 }
495
496 pub fn is_symlink_or_under_symlink(&self) -> bool {
500 self.is_symlink() || self.under_symlink_dir
501 }
502
503 pub fn maybe_plain_text_symlink(&self) -> bool {
508 matches!(
509 self.ty,
510 FileType::File {
511 maybe_symlink: true
512 }
513 )
514 }
515}
516
517impl std::ops::Deref for PathEntry {
518 type Target = Path;
519
520 fn deref(&self) -> &Self::Target {
521 self.path.as_path()
522 }
523}
524
525impl AsRef<PathBuf> for PathEntry {
526 fn as_ref(&self) -> &PathBuf {
527 &self.path
528 }
529}
530
531fn first_package<'p>(
532 pkg_id: PackageId,
533 pkgs: &'p Vec<Package>,
534 warned_duplicate: &mut HashSet<PackageId>,
535 gctx: &GlobalContext,
536) -> &'p Package {
537 if pkgs.len() != 1 && warned_duplicate.insert(pkg_id) {
538 let ignored = pkgs[1..]
539 .iter()
540 .filter(|pkg| pkg.publish().is_none())
544 .collect::<Vec<_>>();
545 if !ignored.is_empty() {
546 use std::fmt::Write as _;
547
548 let plural = if ignored.len() == 1 { "" } else { "s" };
549 let mut msg = String::new();
550 let _ = writeln!(&mut msg, "skipping duplicate package{plural} `{pkg_id}`:");
551 for ignored in ignored {
552 let manifest_path = ignored.manifest_path().display();
553 let _ = writeln!(&mut msg, " {manifest_path}");
554 }
555 let manifest_path = pkgs[0].manifest_path().display();
556 let _ = writeln!(&mut msg, "in favor of {manifest_path}");
557 let _ = gctx.shell().warn(msg);
558 }
559 }
560 &pkgs[0]
561}
562
563pub fn list_files(pkg: &Package, gctx: &GlobalContext) -> CargoResult<Vec<PathEntry>> {
574 _list_files(pkg, gctx).with_context(|| {
575 format!(
576 "failed to determine list of files in {}",
577 pkg.root().display()
578 )
579 })
580}
581
582fn _list_files(pkg: &Package, gctx: &GlobalContext) -> CargoResult<Vec<PathEntry>> {
584 let root = pkg.root();
585 let no_include_option = pkg.manifest().include().is_empty();
586 let git_repo = if no_include_option {
587 discover_gix_repo(root)?
588 } else {
589 None
590 };
591
592 let mut exclude_builder = GitignoreBuilder::new(root);
593 if no_include_option && git_repo.is_none() {
594 exclude_builder.add_line(None, ".*")?;
596 }
597 for rule in pkg.manifest().exclude() {
598 exclude_builder.add_line(None, rule)?;
599 }
600 let ignore_exclude = exclude_builder.build()?;
601
602 let mut include_builder = GitignoreBuilder::new(root);
603 for rule in pkg.manifest().include() {
604 include_builder.add_line(None, rule)?;
605 }
606 let ignore_include = include_builder.build()?;
607
608 let ignore_should_package = |relative_path: &Path, is_dir: bool| {
609 if no_include_option {
611 !ignore_exclude
612 .matched_path_or_any_parents(relative_path, is_dir)
613 .is_ignore()
614 } else {
615 if is_dir {
616 return true;
620 }
621 ignore_include
622 .matched_path_or_any_parents(relative_path, false)
623 .is_ignore()
624 }
625 };
626
627 let filter = |path: &Path, is_dir: bool| {
628 let Ok(relative_path) = path.strip_prefix(root) else {
629 return false;
630 };
631
632 let rel = relative_path.as_os_str();
633 if rel == "Cargo.lock" || rel == "Cargo.toml" {
634 return true;
635 }
636
637 ignore_should_package(relative_path, is_dir)
638 };
639
640 if no_include_option {
642 if let Some(repo) = git_repo {
643 return list_files_gix(pkg, &repo, &filter, gctx);
644 }
645 }
646 let mut ret = Vec::new();
647 list_files_walk(pkg.root(), &mut ret, true, &filter, gctx)?;
648 Ok(ret)
649}
650
651fn discover_gix_repo(root: &Path) -> CargoResult<Option<gix::Repository>> {
655 let repo = match gix::ThreadSafeRepository::discover(root) {
656 Ok(repo) => repo.to_thread_local(),
657 Err(e) => {
658 tracing::debug!(
659 "could not discover git repo at or above {}: {}",
660 root.display(),
661 e
662 );
663 return Ok(None);
664 }
665 };
666 let index = repo
667 .index_or_empty()
668 .with_context(|| format!("failed to open git index at {}", repo.path().display()))?;
669 let repo_root = repo.workdir().ok_or_else(|| {
670 anyhow::format_err!(
671 "did not expect repo at {} to be bare",
672 repo.path().display()
673 )
674 })?;
675 let repo_relative_path = match paths::strip_prefix_canonical(root, repo_root) {
676 Ok(p) => p,
677 Err(e) => {
678 warn!(
679 "cannot determine if path `{:?}` is in git repo `{:?}`: {:?}",
680 root, repo_root, e
681 );
682 return Ok(None);
683 }
684 };
685 let manifest_path = gix::path::join_bstr_unix_pathsep(
686 gix::path::to_unix_separators_on_windows(gix::path::into_bstr(repo_relative_path)),
687 "Cargo.toml",
688 );
689 if index.entry_index_by_path(&manifest_path).is_ok() {
690 return Ok(Some(repo));
691 }
692 Ok(None)
694}
695
696fn list_files_gix(
703 pkg: &Package,
704 repo: &gix::Repository,
705 filter: &dyn Fn(&Path, bool) -> bool,
706 gctx: &GlobalContext,
707) -> CargoResult<Vec<PathEntry>> {
708 debug!("list_files_gix {}", pkg.package_id());
709 let options = repo
710 .dirwalk_options()?
711 .emit_untracked(gix::dir::walk::EmissionMode::Matching)
712 .emit_ignored(None)
713 .emit_tracked(true)
714 .recurse_repositories(false)
715 .symlinks_to_directories_are_ignored_like_directories(true)
716 .emit_empty_directories(false);
717 let index = repo.index_or_empty()?;
718 let root = repo
719 .workdir()
720 .ok_or_else(|| anyhow::format_err!("can't list files on a bare repository"))?;
721 assert!(
722 root.is_absolute(),
723 "BUG: paths used internally are absolute, and the repo inherits that"
724 );
725
726 let pkg_path = pkg.root();
727 let repo_relative_pkg_path = pkg_path.strip_prefix(root).unwrap_or(Path::new(""));
728 let target_prefix = gix::path::to_unix_separators_on_windows(gix::path::into_bstr(
729 repo_relative_pkg_path.join("target/"),
730 ));
731 let package_prefix =
732 gix::path::to_unix_separators_on_windows(gix::path::into_bstr(repo_relative_pkg_path));
733
734 let pathspec = {
735 let mut include = BString::from(":(top)");
737 include.push_str(package_prefix.as_ref());
738
739 let mut exclude = BString::from(":!(exclude,top)");
741 exclude.push_str(target_prefix.as_ref());
742
743 vec![include, exclude]
744 };
745
746 let mut files = Vec::<PathEntry>::new();
747 let mut subpackages_found = Vec::new();
748 for item in repo
749 .dirwalk_iter(index.clone(), pathspec, Default::default(), options)?
750 .filter(|res| {
751 res.as_ref().map_or(true, |item| {
755 item.entry.disk_kind != Some(gix::dir::entry::Kind::Untrackable)
756 && !(item.entry.status == Status::Untracked
757 && item.entry.rela_path == "Cargo.lock")
758 })
759 })
760 .map(|res| {
761 res.map(|item| {
762 let maybe_plain_text_symlink = item.entry.index_kind
769 == Some(gix::dir::entry::Kind::Symlink)
770 && item.entry.disk_kind == Some(gix::dir::entry::Kind::File);
771 (
772 item.entry.rela_path,
773 item.entry.disk_kind,
774 maybe_plain_text_symlink,
775 )
776 })
777 })
778 .chain(
779 index
781 .prefixed_entries(target_prefix.as_ref())
782 .unwrap_or_default()
783 .iter()
784 .filter(|entry| {
785 entry.stage() == Stage::Unconflicted
787 })
788 .map(|entry| {
789 (
790 entry.path(&index).to_owned(),
791 None,
795 false,
796 )
797 })
798 .map(Ok),
799 )
800 {
801 let (rela_path, kind, maybe_plain_text_symlink) = item?;
802 let file_path = root.join(gix::path::from_bstr(rela_path));
803 if file_path.file_name().and_then(|name| name.to_str()) == Some("Cargo.toml") {
804 let path = file_path.parent().unwrap();
808 if path != pkg_path {
809 debug!("subpackage found: {}", path.display());
810 files.retain(|p| !p.starts_with(path));
811 subpackages_found.push(path.to_path_buf());
812 continue;
813 }
814 }
815
816 if subpackages_found.iter().any(|p| file_path.starts_with(p)) {
819 continue;
820 }
821
822 let is_dir = kind.map_or(false, |kind| {
823 if kind == gix::dir::entry::Kind::Symlink {
824 file_path.is_dir()
827 } else {
828 kind.is_dir()
829 }
830 });
831 if is_dir {
832 match gix::open(&file_path) {
836 Ok(sub_repo) => {
837 files.extend(list_files_gix(pkg, &sub_repo, filter, gctx)?);
838 }
839 Err(_) => {
840 list_files_walk(&file_path, &mut files, false, filter, gctx)?;
841 }
842 }
843 } else if (filter)(&file_path, is_dir) {
844 assert!(!is_dir);
845 trace!(" found {}", file_path.display());
846 let ty = match kind.map(Into::into) {
847 Some(FileType::File { .. }) => FileType::File {
848 maybe_symlink: maybe_plain_text_symlink,
849 },
850 Some(ty) => ty,
851 None => FileType::Other,
852 };
853 files.push(PathEntry {
854 path: file_path,
855 ty,
856 under_symlink_dir: false,
859 });
860 }
861 }
862
863 return Ok(files);
864}
865
866fn list_files_walk(
872 path: &Path,
873 ret: &mut Vec<PathEntry>,
874 is_root: bool,
875 filter: &dyn Fn(&Path, bool) -> bool,
876 gctx: &GlobalContext,
877) -> CargoResult<()> {
878 let walkdir = WalkDir::new(path)
879 .follow_links(true)
880 .contents_first(false)
884 .into_iter()
885 .filter_entry(|entry| {
886 let path = entry.path();
887 let at_root = is_root && entry.depth() == 0;
888 let is_dir = entry.file_type().is_dir();
889
890 if !at_root && !filter(path, is_dir) {
891 return false;
892 }
893
894 if !is_dir {
895 return true;
896 }
897
898 if !at_root && path.join("Cargo.toml").exists() {
900 return false;
901 }
902
903 if is_root
905 && entry.depth() == 1
906 && path.file_name().and_then(|s| s.to_str()) == Some("target")
907 {
908 return false;
909 }
910
911 true
912 });
913
914 let mut current_symlink_dir = None;
915 for entry in walkdir {
916 match entry {
917 Ok(entry) => {
918 let file_type = entry.file_type();
919
920 match current_symlink_dir.as_ref() {
921 Some(dir) if entry.path().starts_with(dir) => {
922 }
924 Some(_) | None => {
925 current_symlink_dir = if file_type.is_dir() && entry.path_is_symlink() {
927 Some(entry.path().to_path_buf())
928 } else {
929 None
930 };
931 }
932 }
933
934 if file_type.is_file() || file_type.is_symlink() {
935 let ty = if entry.path_is_symlink() {
937 FileType::Symlink
938 } else {
939 file_type.into()
940 };
941 ret.push(PathEntry {
942 path: entry.into_path(),
943 ty,
944 under_symlink_dir: current_symlink_dir.is_some(),
946 });
947 }
948 }
949 Err(err) if err.loop_ancestor().is_some() => {
950 gctx.shell().warn(err)?;
951 }
952 Err(err) => match err.path() {
953 Some(path) if !filter(path, path.is_dir()) => {}
957 Some(path) => ret.push(PathEntry {
961 path: path.to_path_buf(),
962 ty: FileType::Other,
963 under_symlink_dir: false,
964 }),
965 None => return Err(err.into()),
966 },
967 }
968 }
969
970 Ok(())
971}
972
973fn last_modified_file(
975 path: &Path,
976 pkg: &Package,
977 gctx: &GlobalContext,
978) -> CargoResult<(FileTime, PathBuf)> {
979 let mut max = FileTime::zero();
980 let mut max_path = PathBuf::new();
981 for file in list_files(pkg, gctx).with_context(|| {
982 format!(
983 "failed to determine the most recently modified file in {}",
984 pkg.root().display()
985 )
986 })? {
987 let mtime = paths::mtime(&file).unwrap_or_else(|_| FileTime::zero());
993 if mtime > max {
994 max = mtime;
995 max_path = file.into_path_buf();
996 }
997 }
998 trace!("last modified file {}: {}", path.display(), max);
999 Ok((max, max_path))
1000}
1001
1002fn read_packages(
1003 path: &Path,
1004 source_id: SourceId,
1005 gctx: &GlobalContext,
1006) -> CargoResult<HashMap<PackageId, Vec<Package>>> {
1007 let mut all_packages = HashMap::default();
1008 let mut visited = HashSet::<PathBuf>::default();
1009 let mut errors = Vec::<anyhow::Error>::new();
1010
1011 trace!(
1012 "looking for root package: {}, source_id={}",
1013 path.display(),
1014 source_id
1015 );
1016
1017 walk(path, &mut |dir| {
1018 trace!("looking for child package: {}", dir.display());
1019
1020 if dir != path {
1022 let name = dir.file_name().and_then(|s| s.to_str());
1023 if name.map(|s| s.starts_with('.')) == Some(true) {
1024 return Ok(false);
1025 }
1026
1027 if dir.join(".git").exists() {
1029 return Ok(false);
1030 }
1031 }
1032
1033 if dir.file_name().and_then(|s| s.to_str()) == Some("target")
1035 && has_manifest(dir.parent().unwrap())
1036 {
1037 return Ok(false);
1038 }
1039
1040 if has_manifest(dir) {
1041 read_nested_packages(
1042 dir,
1043 &mut all_packages,
1044 source_id,
1045 gctx,
1046 &mut visited,
1047 &mut errors,
1048 )?;
1049 }
1050 Ok(true)
1051 })?;
1052
1053 if all_packages.is_empty() {
1054 match errors.pop() {
1055 Some(err) => Err(err),
1056 None => {
1057 if find_project_manifest_exact(path, "cargo.toml").is_ok() {
1058 Err(anyhow::format_err!(
1059 "could not find `Cargo.toml` in `{}`
1060help: found `cargo.toml`, consider renaming it to `Cargo.toml`",
1061 path.display()
1062 ))
1063 } else {
1064 Err(anyhow::format_err!(
1065 "could not find `Cargo.toml` in `{}`",
1066 path.display()
1067 ))
1068 }
1069 }
1070 }
1071 } else {
1072 Ok(all_packages)
1073 }
1074}
1075
1076fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
1077 let mut nested_paths = Vec::new();
1078 let normalized = manifest.normalized_toml();
1079 let dependencies = normalized
1080 .dependencies
1081 .iter()
1082 .chain(normalized.build_dependencies())
1083 .chain(normalized.dev_dependencies())
1084 .chain(
1085 normalized
1086 .target
1087 .as_ref()
1088 .into_iter()
1089 .flat_map(|t| t.values())
1090 .flat_map(|t| {
1091 t.dependencies
1092 .iter()
1093 .chain(t.build_dependencies())
1094 .chain(t.dev_dependencies())
1095 }),
1096 );
1097 for dep_table in dependencies {
1098 for dep in dep_table.values() {
1099 let cargo_util_schemas::manifest::InheritableDependency::Value(dep) = dep else {
1100 continue;
1101 };
1102 let cargo_util_schemas::manifest::TomlDependency::Detailed(dep) = dep else {
1103 continue;
1104 };
1105 let Some(path) = dep.path.as_ref() else {
1106 continue;
1107 };
1108 nested_paths.push(PathBuf::from(path.as_str()));
1109 }
1110 }
1111 nested_paths
1112}
1113
1114fn walk(path: &Path, callback: &mut dyn FnMut(&Path) -> CargoResult<bool>) -> CargoResult<()> {
1115 if !callback(path)? {
1116 trace!("not processing {}", path.display());
1117 return Ok(());
1118 }
1119
1120 let dirs = match fs::read_dir(path) {
1123 Ok(dirs) => dirs,
1124 Err(ref e) if e.kind() == io::ErrorKind::PermissionDenied => return Ok(()),
1125 Err(e) => {
1126 let cx = format!("failed to read directory `{}`", path.display());
1127 let e = anyhow::Error::from(e);
1128 return Err(e.context(cx));
1129 }
1130 };
1131 let mut dirs = dirs.collect::<Vec<_>>();
1132 dirs.sort_unstable_by_key(|d| d.as_ref().ok().map(|d| d.file_name()));
1133 for dir in dirs {
1134 let dir = dir?;
1135 if dir.file_type()?.is_dir() {
1136 walk(&dir.path(), callback)?;
1137 }
1138 }
1139 Ok(())
1140}
1141
1142fn has_manifest(path: &Path) -> bool {
1143 find_project_manifest_exact(path, "Cargo.toml").is_ok()
1144}
1145
1146fn read_nested_packages(
1147 path: &Path,
1148 all_packages: &mut HashMap<PackageId, Vec<Package>>,
1149 source_id: SourceId,
1150 gctx: &GlobalContext,
1151 visited: &mut HashSet<PathBuf>,
1152 errors: &mut Vec<anyhow::Error>,
1153) -> CargoResult<()> {
1154 if !visited.insert(path.to_path_buf()) {
1155 return Ok(());
1156 }
1157
1158 let manifest_path = find_project_manifest_exact(path, "Cargo.toml")?;
1159
1160 let manifest = match read_manifest(&manifest_path, source_id, gctx) {
1161 Err(err) => {
1162 info!(
1170 "skipping malformed package found at `{}`",
1171 path.to_string_lossy()
1172 );
1173 errors.push(err.into());
1174 return Ok(());
1175 }
1176 Ok(tuple) => tuple,
1177 };
1178
1179 let manifest = match manifest {
1180 EitherManifest::Real(manifest) => manifest,
1181 EitherManifest::Virtual(..) => return Ok(()),
1182 };
1183 let nested = nested_paths(&manifest);
1184 let pkg = Package::new(manifest, &manifest_path);
1185
1186 let pkg_id = pkg.package_id();
1187 all_packages.entry(pkg_id).or_default().push(pkg);
1188
1189 if !source_id.is_registry() {
1198 for p in nested.iter() {
1199 let path = paths::normalize_path(&path.join(p));
1200 let result =
1201 read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
1202 if let Err(err) = result {
1209 if source_id.is_git() {
1210 info!(
1211 "skipping nested package found at `{}`: {:?}",
1212 path.display(),
1213 &err,
1214 );
1215 errors.push(err);
1216 } else {
1217 return Err(err);
1218 }
1219 }
1220 }
1221 }
1222
1223 Ok(())
1224}