Skip to main content

cargo/sources/
path.rs

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
30/// A source that represents a package gathered at the root
31/// path on the filesystem.
32///
33/// It also provides convenient methods like [`PathSource::list_files`] to
34/// list all files in a package, given its ability to walk the filesystem.
35pub struct PathSource<'gctx> {
36    /// The unique identifier of this source.
37    source_id: SourceId,
38    /// The root path of this source.
39    path: PathBuf,
40    /// The package discovered in this source, if any.
41    package: RefCell<Option<Option<Package>>>,
42    gctx: &'gctx GlobalContext,
43}
44
45impl<'gctx> PathSource<'gctx> {
46    /// Invoked with an absolute path to a directory that contains a `Cargo.toml`.
47    ///
48    /// This source will only return the package at precisely the `path`
49    /// specified, and it will be an error if there's not a package at `path`.
50    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    /// Preloads a package for this source. The source is assumed that it has
60    /// yet loaded any other packages.
61    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    /// Returns the root package, or an error if it is missing or failed to load.
73    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    /// List all files relevant to building this package inside this source.
88    ///
89    /// This function will use the appropriate methods to determine the
90    /// set of files underneath this source's directory which are relevant for
91    /// building `pkg`.
92    ///
93    /// The basic assumption of this method is that all files in the directory
94    /// are relevant for building this package, but it also contains logic to
95    /// use other methods like `.gitignore`, `package.include`, or
96    /// `package.exclude` to filter the list of files.
97    #[tracing::instrument(skip_all)]
98    pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
99        list_files(pkg, self.gctx)
100    }
101
102    /// Gets the last modified file in a package.
103    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    /// Returns the root path of this source.
114    pub fn path(&self) -> &Path {
115        &self.path
116    }
117
118    /// Discovers packages inside this source if it hasn't yet done.
119    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    /// Reads the manifest. Returning `Ok(None)` if missing allows the resolver
129    /// to handle it as "not found" instead of an early IO error.
130    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        // Note that we try to strip the prefix of this package to get a
202        // relative path to ensure that the fingerprint remains consistent
203        // across entire project directory renames.
204        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        // Path source has no local cache.
217    }
218
219    fn set_quiet(&mut self, _quiet: bool) {
220        // Path source does not display status
221    }
222}
223
224/// A source that represents one or multiple packages gathered from a given root
225/// path on the filesystem.
226pub struct RecursivePathSource<'gctx> {
227    /// The unique identifier of this source.
228    source_id: SourceId,
229    /// The root path of this source.
230    path: PathBuf,
231    /// Whether this source has loaded all package information it may contain.
232    loaded: Cell<bool>,
233    /// Packages that this sources has discovered.
234    ///
235    /// Tracking all packages for a given ID to warn on-demand for unused packages
236    packages: RefCell<HashMap<PackageId, Vec<Package>>>,
237    /// Avoid redundant unused package warnings
238    warned_duplicate: RefCell<HashSet<PackageId>>,
239    gctx: &'gctx GlobalContext,
240}
241
242impl<'gctx> RecursivePathSource<'gctx> {
243    /// Creates a new source which is walked recursively to discover packages.
244    ///
245    /// This is similar to the [`PathSource::new`] method except that instead
246    /// of requiring a valid package to be present at `root` the folder is
247    /// walked entirely to crawl for packages.
248    ///
249    /// Note that this should be used with care and likely shouldn't be chosen
250    /// by default!
251    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    /// Returns the packages discovered by this source. It may walk the
263    /// filesystem if package information haven't yet loaded.
264    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    /// List all files relevant to building this package inside this source.
283    ///
284    /// This function will use the appropriate methods to determine the
285    /// set of files underneath this source's directory which are relevant for
286    /// building `pkg`.
287    ///
288    /// The basic assumption of this method is that all files in the directory
289    /// are relevant for building this package, but it also contains logic to
290    /// use other methods like `.gitignore`, `package.include`, or
291    /// `package.exclude` to filter the list of files.
292    pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
293        list_files(pkg, self.gctx)
294    }
295
296    /// Gets the last modified file in a package.
297    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    /// Returns the root path of this source.
308    pub fn path(&self) -> &Path {
309        &self.path
310    }
311
312    /// Discovers packages inside this source if it hasn't yet done.
313    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        // Note that we try to strip the prefix of this package to get a
399        // relative path to ensure that the fingerprint remains consistent
400        // across entire project directory renames.
401        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        // Path source has no local cache.
414    }
415
416    fn set_quiet(&mut self, _quiet: bool) {
417        // Path source does not display status
418    }
419}
420
421/// Type that abstracts over [`gix::dir::entry::Kind`] and [`fs::FileType`].
422#[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/// [`PathBuf`] with extra metadata.
461#[derive(Clone, Debug)]
462pub struct PathEntry {
463    path: PathBuf,
464    ty: FileType,
465    /// Whether this path was visited when traversing a symlink directory.
466    under_symlink_dir: bool,
467}
468
469impl PathEntry {
470    pub fn into_path_buf(self) -> PathBuf {
471        self.path
472    }
473
474    /// Similar to [`std::path::Path::is_file`]
475    /// but doesn't follow the symbolic link nor make any system call
476    pub fn is_file(&self) -> bool {
477        matches!(self.ty, FileType::File { .. })
478    }
479
480    /// Similar to [`std::path::Path::is_dir`]
481    /// but doesn't follow the symbolic link nor make any system call
482    pub fn is_dir(&self) -> bool {
483        matches!(self.ty, FileType::Dir)
484    }
485
486    /// Similar to [`std::path::Path::is_symlink`]
487    /// but doesn't follow the symbolic link nor make any system call
488    ///
489    /// If the path is not a symlink but under a symlink parent directory,
490    /// this will return false.
491    /// See [`PathEntry::is_symlink_or_under_symlink`] for an alternative.
492    pub fn is_symlink(&self) -> bool {
493        matches!(self.ty, FileType::Symlink)
494    }
495
496    /// Whether a path is a symlink or a path under a symlink directory.
497    ///
498    /// Use [`PathEntry::is_symlink`] to get the exact file type of the path only.
499    pub fn is_symlink_or_under_symlink(&self) -> bool {
500        self.is_symlink() || self.under_symlink_dir
501    }
502
503    /// Whether this path might be a plain text symlink.
504    ///
505    /// Git may check out symlinks as plain text files that contain the link texts,
506    /// when either `core.symlinks` is `false`, or on Windows.
507    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            // We can assume a package with publish = false isn't intended to be seen
541            // by users so we can hide the warning about those since the user is unlikely
542            // to care about those cases.
543            .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
563/// List all files relevant to building this package inside this source.
564///
565/// This function will use the appropriate methods to determine the
566/// set of files underneath this source's directory which are relevant for
567/// building `pkg`.
568///
569/// The basic assumption of this method is that all files in the directory
570/// are relevant for building this package, but it also contains logic to
571/// use other methods like `.gitignore`, `package.include`, or
572/// `package.exclude` to filter the list of files.
573pub 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
582/// See [`PathSource::list_files`].
583fn _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        // no include option and not git repo discovered (see rust-lang/cargo#7183).
595        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        // "Include" and "exclude" options are mutually exclusive.
610        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                // Generally, include directives don't list every
617                // directory (nor should they!). Just skip all directory
618                // checks, and only check files.
619                return true;
620            }
621            ignore_include
622                .matched_path_or_any_parents(relative_path, /* is_dir */ 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    // Attempt Git-prepopulate only if no `include` (see rust-lang/cargo#4135).
641    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
651/// Returns [`Some(gix::Repository)`](gix::Repository) if the discovered repository
652/// (searched upwards from `root`) contains a tracked `<root>/Cargo.toml`.
653/// Otherwise, the caller should fall back on full file list.
654fn 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    // Package Cargo.toml is not in git, don't use git to guide our selection.
693    Ok(None)
694}
695
696/// Lists files relevant to building this package inside this source by
697/// traversing the git working tree, while avoiding ignored files.
698///
699/// This looks into Git sub-repositories as well, resolving them to individual files.
700/// Symlinks to directories will also be resolved, but walked as repositories if they
701/// point to one to avoid picking up `.git` directories.
702fn 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        // Include the package root.
736        let mut include = BString::from(":(top)");
737        include.push_str(package_prefix.as_ref());
738
739        // Exclude the target directory.
740        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            // Don't include Cargo.lock if it is untracked. Packaging will
752            // generate a new one as needed.
753            // Also don't include untrackable directory entries, like FIFOs.
754            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                // Assumption: if a file tracked as a symlink in Git index, and
763                // the actual file type on disk is file, then it might be a
764                // plain text file symlink.
765                // There are exceptions like the file has changed from a symlink
766                // to a real text file, but hasn't been committed to Git index.
767                // Exceptions may be rare so we're okay with this now.
768                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            // Append entries that might be tracked in `<pkg_root>/target/`.
780            index
781                .prefixed_entries(target_prefix.as_ref())
782                .unwrap_or_default()
783                .iter()
784                .filter(|entry| {
785                    // probably not needed as conflicts prevent this to run, but let's be explicit.
786                    entry.stage() == Stage::Unconflicted
787                })
788                .map(|entry| {
789                    (
790                        entry.path(&index).to_owned(),
791                        // Do not trust what's recorded in the index, enforce checking the disk.
792                        // This traversal is not part of a `status()`, and tracking things in `target/`
793                        // is rare.
794                        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            // Keep track of all sub-packages found and also strip out all
805            // matches we've found so far. Note, though, that if we find
806            // our own `Cargo.toml`, we keep going.
807            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 this file is part of any other sub-package we've found so far,
817        // skip it.
818        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                // Symlinks must be checked to see if they point to a directory
825                // we should traverse.
826                file_path.is_dir()
827            } else {
828                kind.is_dir()
829            }
830        });
831        if is_dir {
832            // This could be a submodule, or a sub-repository. In any case, we prefer to walk
833            // it with git-support to leverage ignored files and to avoid pulling in entire
834            // .git repositories.
835            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                // Git index doesn't include files from symlink directory,
857                // symlink dirs are handled in `list_files_walk`.
858                under_symlink_dir: false,
859            });
860        }
861    }
862
863    return Ok(files);
864}
865
866/// Lists files relevant to building this package inside this source by
867/// walking the filesystem from the package root path.
868///
869/// This is a fallback for [`list_files_gix`] when the package
870/// is not tracked under a Git repository.
871fn 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        // While this is the default, set it explicitly.
881        // We need walkdir to visit the directory tree in depth-first order,
882        // so we can ensure a path visited later be under a certain directory.
883        .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            // Don't recurse into any sub-packages that we have.
899            if !at_root && path.join("Cargo.toml").exists() {
900                return false;
901            }
902
903            // Skip root Cargo artifacts.
904            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                        // Still walk under the same parent symlink dir, so keep it
923                    }
924                    Some(_) | None => {
925                        // Not under any parent symlink dir, update the current one.
926                        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                    // We follow_links(true) here so check if entry was created from a symlink
936                    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                        // This rely on contents_first(false), which walks in depth-first order
945                        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                // If an error occurs with a path, filter it again.
954                // If it is excluded, Just ignore it in this case.
955                // See issue rust-lang/cargo#10917
956                Some(path) if !filter(path, path.is_dir()) => {}
957                // Otherwise, simply recover from it.
958                // Don't worry about error skipping here, the callers would
959                // still hit the IO error if they do access it thereafter.
960                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
973/// Gets the last modified file in a package.
974fn 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        // An `fs::stat` error here is either because path is a
988        // broken symlink, a permissions error, or a race
989        // condition where this path was `rm`-ed -- either way,
990        // we can ignore the error and treat the path's `mtime`
991        // as `0`.
992        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        // Don't recurse into hidden/dot directories unless we're at the toplevel
1021        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            // Don't automatically discover packages across git submodules
1028            if dir.join(".git").exists() {
1029                return Ok(false);
1030            }
1031        }
1032
1033        // Don't ever look at target directories
1034        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    // Ignore any permission denied errors because temporary directories
1121    // can often have some weird permissions on them.
1122    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            // Ignore malformed manifests found on git repositories
1163            //
1164            // git source try to find and read all manifests from the repository
1165            // but since it's not possible to exclude folders from this search
1166            // it's safer to ignore malformed manifests to avoid
1167            //
1168            // TODO: Add a way to exclude folders?
1169            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    // Registry sources are not allowed to have `path=` dependencies because
1190    // they're all translated to actual registry dependencies.
1191    //
1192    // We normalize the path here ensure that we don't infinitely walk around
1193    // looking for crates. By normalizing we ensure that we visit this crate at
1194    // most once.
1195    //
1196    // TODO: filesystem/symlink implications?
1197    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            // Ignore broken manifests found on git repositories.
1203            //
1204            // A well formed manifest might still fail to load due to reasons
1205            // like referring to a "path" that requires an extra build step.
1206            //
1207            // See https://github.com/rust-lang/cargo/issues/6822.
1208            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}