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::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
29/// A source that represents a package gathered at the root
30/// path on the filesystem.
31///
32/// It also provides convenient methods like [`PathSource::list_files`] to
33/// list all files in a package, given its ability to walk the filesystem.
34pub struct PathSource<'gctx> {
35    /// The unique identifier of this source.
36    source_id: SourceId,
37    /// The root path of this source.
38    path: PathBuf,
39    /// The package discovered in this source, if any.
40    package: RefCell<Option<Option<Package>>>,
41    gctx: &'gctx GlobalContext,
42}
43
44impl<'gctx> PathSource<'gctx> {
45    /// Invoked with an absolute path to a directory that contains a `Cargo.toml`.
46    ///
47    /// This source will only return the package at precisely the `path`
48    /// specified, and it will be an error if there's not a package at `path`.
49    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    /// Preloads a package for this source. The source is assumed that it has
59    /// yet loaded any other packages.
60    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    /// Returns the root package, or an error if it is missing or failed to load.
72    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    /// List all files relevant to building this package inside this source.
87    ///
88    /// This function will use the appropriate methods to determine the
89    /// set of files underneath this source's directory which are relevant for
90    /// building `pkg`.
91    ///
92    /// The basic assumption of this method is that all files in the directory
93    /// are relevant for building this package, but it also contains logic to
94    /// use other methods like `.gitignore`, `package.include`, or
95    /// `package.exclude` to filter the list of files.
96    #[tracing::instrument(skip_all)]
97    pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
98        list_files(pkg, self.gctx)
99    }
100
101    /// Gets the last modified file in a package.
102    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    /// Returns the root path of this source.
113    pub fn path(&self) -> &Path {
114        &self.path
115    }
116
117    /// Discovers packages inside this source if it hasn't yet done.
118    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    /// Reads the manifest. Returning `Ok(None)` if missing allows the resolver
128    /// to handle it as "not found" instead of an early IO error.
129    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        // Note that we try to strip the prefix of this package to get a
199        // relative path to ensure that the fingerprint remains consistent
200        // across entire project directory renames.
201        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        // Path source has no local cache.
214    }
215
216    fn set_quiet(&mut self, _quiet: bool) {
217        // Path source does not display status
218    }
219}
220
221/// A source that represents one or multiple packages gathered from a given root
222/// path on the filesystem.
223pub struct RecursivePathSource<'gctx> {
224    /// The unique identifier of this source.
225    source_id: SourceId,
226    /// The root path of this source.
227    path: PathBuf,
228    /// Whether this source has loaded all package information it may contain.
229    loaded: Cell<bool>,
230    /// Packages that this sources has discovered.
231    ///
232    /// Tracking all packages for a given ID to warn on-demand for unused packages
233    packages: RefCell<HashMap<PackageId, Vec<Package>>>,
234    /// Avoid redundant unused package warnings
235    warned_duplicate: RefCell<HashSet<PackageId>>,
236    gctx: &'gctx GlobalContext,
237}
238
239impl<'gctx> RecursivePathSource<'gctx> {
240    /// Creates a new source which is walked recursively to discover packages.
241    ///
242    /// This is similar to the [`PathSource::new`] method except that instead
243    /// of requiring a valid package to be present at `root` the folder is
244    /// walked entirely to crawl for packages.
245    ///
246    /// Note that this should be used with care and likely shouldn't be chosen
247    /// by default!
248    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    /// Returns the packages discovered by this source. It may walk the
260    /// filesystem if package information haven't yet loaded.
261    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    /// List all files relevant to building this package inside this source.
280    ///
281    /// This function will use the appropriate methods to determine the
282    /// set of files underneath this source's directory which are relevant for
283    /// building `pkg`.
284    ///
285    /// The basic assumption of this method is that all files in the directory
286    /// are relevant for building this package, but it also contains logic to
287    /// use other methods like `.gitignore`, `package.include`, or
288    /// `package.exclude` to filter the list of files.
289    pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
290        list_files(pkg, self.gctx)
291    }
292
293    /// Gets the last modified file in a package.
294    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    /// Returns the root path of this source.
305    pub fn path(&self) -> &Path {
306        &self.path
307    }
308
309    /// Discovers packages inside this source if it hasn't yet done.
310    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        // Note that we try to strip the prefix of this package to get a
394        // relative path to ensure that the fingerprint remains consistent
395        // across entire project directory renames.
396        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        // Path source has no local cache.
409    }
410
411    fn set_quiet(&mut self, _quiet: bool) {
412        // Path source does not display status
413    }
414}
415
416/// Type that abstracts over [`gix::dir::entry::Kind`] and [`fs::FileType`].
417#[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/// [`PathBuf`] with extra metadata.
456#[derive(Clone, Debug)]
457pub struct PathEntry {
458    path: PathBuf,
459    ty: FileType,
460    /// Whether this path was visited when traversing a symlink directory.
461    under_symlink_dir: bool,
462}
463
464impl PathEntry {
465    pub fn into_path_buf(self) -> PathBuf {
466        self.path
467    }
468
469    /// Similar to [`std::path::Path::is_file`]
470    /// but doesn't follow the symbolic link nor make any system call
471    pub fn is_file(&self) -> bool {
472        matches!(self.ty, FileType::File { .. })
473    }
474
475    /// Similar to [`std::path::Path::is_dir`]
476    /// but doesn't follow the symbolic link nor make any system call
477    pub fn is_dir(&self) -> bool {
478        matches!(self.ty, FileType::Dir)
479    }
480
481    /// Similar to [`std::path::Path::is_symlink`]
482    /// but doesn't follow the symbolic link nor make any system call
483    ///
484    /// If the path is not a symlink but under a symlink parent directory,
485    /// this will return false.
486    /// See [`PathEntry::is_symlink_or_under_symlink`] for an alternative.
487    pub fn is_symlink(&self) -> bool {
488        matches!(self.ty, FileType::Symlink)
489    }
490
491    /// Whether a path is a symlink or a path under a symlink directory.
492    ///
493    /// Use [`PathEntry::is_symlink`] to get the exact file type of the path only.
494    pub fn is_symlink_or_under_symlink(&self) -> bool {
495        self.is_symlink() || self.under_symlink_dir
496    }
497
498    /// Whether this path might be a plain text symlink.
499    ///
500    /// Git may check out symlinks as plain text files that contain the link texts,
501    /// when either `core.symlinks` is `false`, or on Windows.
502    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            // We can assume a package with publish = false isn't intended to be seen
536            // by users so we can hide the warning about those since the user is unlikely
537            // to care about those cases.
538            .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
558/// List all files relevant to building this package inside this source.
559///
560/// This function will use the appropriate methods to determine the
561/// set of files underneath this source's directory which are relevant for
562/// building `pkg`.
563///
564/// The basic assumption of this method is that all files in the directory
565/// are relevant for building this package, but it also contains logic to
566/// use other methods like `.gitignore`, `package.include`, or
567/// `package.exclude` to filter the list of files.
568pub 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
577/// See [`PathSource::list_files`].
578fn _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        // no include option and not git repo discovered (see rust-lang/cargo#7183).
590        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        // "Include" and "exclude" options are mutually exclusive.
605        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                // Generally, include directives don't list every
612                // directory (nor should they!). Just skip all directory
613                // checks, and only check files.
614                return true;
615            }
616            ignore_include
617                .matched_path_or_any_parents(relative_path, /* is_dir */ 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    // Attempt Git-prepopulate only if no `include` (see rust-lang/cargo#4135).
636    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
646/// Returns [`Some(gix::Repository)`](gix::Repository) if the discovered repository
647/// (searched upwards from `root`) contains a tracked `<root>/Cargo.toml`.
648/// Otherwise, the caller should fall back on full file list.
649fn 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    // Package Cargo.toml is not in git, don't use git to guide our selection.
688    Ok(None)
689}
690
691/// Lists files relevant to building this package inside this source by
692/// traversing the git working tree, while avoiding ignored files.
693///
694/// This looks into Git sub-repositories as well, resolving them to individual files.
695/// Symlinks to directories will also be resolved, but walked as repositories if they
696/// point to one to avoid picking up `.git` directories.
697fn 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        // Include the package root.
731        let mut include = BString::from(":(top)");
732        include.push_str(package_prefix.as_ref());
733
734        // Exclude the target directory.
735        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            // Don't include Cargo.lock if it is untracked. Packaging will
747            // generate a new one as needed.
748            // Also don't include untrackable directory entries, like FIFOs.
749            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                // Assumption: if a file tracked as a symlink in Git index, and
758                // the actual file type on disk is file, then it might be a
759                // plain text file symlink.
760                // There are exceptions like the file has changed from a symlink
761                // to a real text file, but hasn't been committed to Git index.
762                // Exceptions may be rare so we're okay with this now.
763                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            // Append entries that might be tracked in `<pkg_root>/target/`.
775            index
776                .prefixed_entries(target_prefix.as_ref())
777                .unwrap_or_default()
778                .iter()
779                .filter(|entry| {
780                    // probably not needed as conflicts prevent this to run, but let's be explicit.
781                    entry.stage() == Stage::Unconflicted
782                })
783                .map(|entry| {
784                    (
785                        entry.path(&index).to_owned(),
786                        // Do not trust what's recorded in the index, enforce checking the disk.
787                        // This traversal is not part of a `status()`, and tracking things in `target/`
788                        // is rare.
789                        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            // Keep track of all sub-packages found and also strip out all
800            // matches we've found so far. Note, though, that if we find
801            // our own `Cargo.toml`, we keep going.
802            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 this file is part of any other sub-package we've found so far,
812        // skip it.
813        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                // Symlinks must be checked to see if they point to a directory
820                // we should traverse.
821                file_path.is_dir()
822            } else {
823                kind.is_dir()
824            }
825        });
826        if is_dir {
827            // This could be a submodule, or a sub-repository. In any case, we prefer to walk
828            // it with git-support to leverage ignored files and to avoid pulling in entire
829            // .git repositories.
830            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                // Git index doesn't include files from symlink directory,
852                // symlink dirs are handled in `list_files_walk`.
853                under_symlink_dir: false,
854            });
855        }
856    }
857
858    return Ok(files);
859}
860
861/// Lists files relevant to building this package inside this source by
862/// walking the filesystem from the package root path.
863///
864/// This is a fallback for [`list_files_gix`] when the package
865/// is not tracked under a Git repository.
866fn 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        // While this is the default, set it explicitly.
876        // We need walkdir to visit the directory tree in depth-first order,
877        // so we can ensure a path visited later be under a certain directory.
878        .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            // Don't recurse into any sub-packages that we have.
894            if !at_root && path.join("Cargo.toml").exists() {
895                return false;
896            }
897
898            // Skip root Cargo artifacts.
899            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                        // Still walk under the same parent symlink dir, so keep it
918                    }
919                    Some(_) | None => {
920                        // Not under any parent symlink dir, update the current one.
921                        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                    // We follow_links(true) here so check if entry was created from a symlink
931                    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                        // This rely on contents_first(false), which walks in depth-first order
940                        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                // If an error occurs with a path, filter it again.
949                // If it is excluded, Just ignore it in this case.
950                // See issue rust-lang/cargo#10917
951                Some(path) if !filter(path, path.is_dir()) => {}
952                // Otherwise, simply recover from it.
953                // Don't worry about error skipping here, the callers would
954                // still hit the IO error if they do access it thereafter.
955                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
968/// Gets the last modified file in a package.
969fn 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        // An `fs::stat` error here is either because path is a
983        // broken symlink, a permissions error, or a race
984        // condition where this path was `rm`-ed -- either way,
985        // we can ignore the error and treat the path's `mtime`
986        // as `0`.
987        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        // Don't recurse into hidden/dot directories unless we're at the toplevel
1016        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            // Don't automatically discover packages across git submodules
1023            if dir.join(".git").exists() {
1024                return Ok(false);
1025            }
1026        }
1027
1028        // Don't ever look at target directories
1029        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    // Ignore any permission denied errors because temporary directories
1116    // can often have some weird permissions on them.
1117    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            // Ignore malformed manifests found on git repositories
1158            //
1159            // git source try to find and read all manifests from the repository
1160            // but since it's not possible to exclude folders from this search
1161            // it's safer to ignore malformed manifests to avoid
1162            //
1163            // TODO: Add a way to exclude folders?
1164            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    // Registry sources are not allowed to have `path=` dependencies because
1185    // they're all translated to actual registry dependencies.
1186    //
1187    // We normalize the path here ensure that we don't infinitely walk around
1188    // looking for crates. By normalizing we ensure that we visit this crate at
1189    // most once.
1190    //
1191    // TODO: filesystem/symlink implications?
1192    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            // Ignore broken manifests found on git repositories.
1198            //
1199            // A well formed manifest might still fail to load due to reasons
1200            // like referring to a "path" that requires an extra build step.
1201            //
1202            // See https://github.com/rust-lang/cargo/issues/6822.
1203            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}