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