blob: 051ed94e63fda863564bb6628a6aee096b35e816 [file] [edit]
use crate::util::data_structures::{HashMap, HashSet};
use std::cell::{Cell, RefCell};
use std::fmt::{self, Debug, Formatter};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::ops;
use crate::sources::IndexSummary;
use crate::sources::source::MaybePackage;
use crate::sources::source::QueryKind;
use crate::sources::source::Source;
use crate::util::GlobalContext;
use crate::util::errors::CargoResult;
use crate::util::important_paths::find_project_manifest_exact;
use crate::util::internal;
use crate::workspace::parser::read_manifest;
use crate::workspace::{Dependency, EitherManifest, Manifest, Package, PackageId, SourceId};
use anyhow::Context as _;
use cargo_util::paths;
use filetime::FileTime;
use gix::bstr::{BString, ByteVec};
use gix::dir::entry::Status;
use gix::index::entry::Stage;
use ignore::gitignore::GitignoreBuilder;
use tracing::{debug, info, trace, warn};
use walkdir::WalkDir;
/// A source that represents a package gathered at the root
/// path on the filesystem.
///
/// It also provides convenient methods like [`PathSource::list_files`] to
/// list all files in a package, given its ability to walk the filesystem.
pub struct PathSource<'gctx> {
/// The unique identifier of this source.
source_id: SourceId,
/// The root path of this source.
path: PathBuf,
/// The package discovered in this source, if any.
package: RefCell<Option<Option<Package>>>,
gctx: &'gctx GlobalContext,
}
impl<'gctx> PathSource<'gctx> {
/// Invoked with an absolute path to a directory that contains a `Cargo.toml`.
///
/// This source will only return the package at precisely the `path`
/// specified, and it will be an error if there's not a package at `path`.
pub fn new(path: &Path, source_id: SourceId, gctx: &'gctx GlobalContext) -> Self {
Self {
source_id,
path: path.to_path_buf(),
package: RefCell::new(None),
gctx,
}
}
/// Preloads a package for this source. The source is assumed that it has
/// yet loaded any other packages.
pub fn preload_with(pkg: Package, gctx: &'gctx GlobalContext) -> Self {
let source_id = pkg.package_id().source_id();
let path = pkg.root().to_owned();
Self {
source_id,
path,
package: RefCell::new(Some(Some(pkg))),
gctx,
}
}
/// Returns the root package, or an error if it is missing or failed to load.
pub fn root_package(&mut self) -> CargoResult<Package> {
trace!("root_package; source={:?}", self);
self.load()?;
match &*self.package.borrow() {
Some(Some(pkg)) => Ok(pkg.clone()),
Some(None) | None => Err(anyhow::format_err!(
"failed to read `{}`",
self.path.join("Cargo.toml").display()
)),
}
}
/// List all files relevant to building this package inside this source.
///
/// This function will use the appropriate methods to determine the
/// set of files underneath this source's directory which are relevant for
/// building `pkg`.
///
/// The basic assumption of this method is that all files in the directory
/// are relevant for building this package, but it also contains logic to
/// use other methods like `.gitignore`, `package.include`, or
/// `package.exclude` to filter the list of files.
#[tracing::instrument(skip_all)]
pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
list_files(pkg, self.gctx)
}
/// Gets the last modified file in a package.
fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
if self.package.borrow().is_none() {
return Err(internal(format!(
"BUG: source `{:?}` was not loaded",
self.path
)));
}
last_modified_file(&self.path, pkg, self.gctx)
}
/// Returns the root path of this source.
pub fn path(&self) -> &Path {
&self.path
}
/// Discovers packages inside this source if it hasn't yet done.
pub fn load(&self) -> CargoResult<()> {
let mut package = self.package.borrow_mut();
if package.is_none() {
*package = Some(self.read_package()?);
}
Ok(())
}
/// Reads the manifest. Returning `Ok(None)` if missing allows the resolver
/// to handle it as "not found" instead of an early IO error.
fn read_package(&self) -> CargoResult<Option<Package>> {
let path = self.path.join("Cargo.toml");
if !path.exists() {
return Ok(None);
}
Ok(Some(ops::read_package(&path, self.source_id, self.gctx)?))
}
}
impl<'gctx> Debug for PathSource<'gctx> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "the paths source")
}
}
#[async_trait::async_trait(?Send)]
impl<'gctx> Source for PathSource<'gctx> {
async fn query(
&self,
dep: &Dependency,
kind: QueryKind,
f: &mut dyn FnMut(IndexSummary),
) -> CargoResult<()> {
self.load()?;
if let Some(Some(p)) = &*self.package.borrow() {
let s = p.summary();
let matched = match kind {
QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(s),
QueryKind::AlternativeNames => true,
QueryKind::Normalized => dep.matches(s),
};
if matched {
f(IndexSummary::Candidate(s.clone()))
}
}
Ok(())
}
fn supports_checksums(&self) -> bool {
false
}
fn requires_precise(&self) -> bool {
false
}
fn source_id(&self) -> SourceId {
self.source_id
}
async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
trace!("getting packages; id={}", id);
self.load()?;
let pkg = self.package.borrow();
let pkg = pkg
.as_ref()
.and_then(|p| p.as_ref())
.filter(|pkg| pkg.package_id() == id);
pkg.cloned()
.map(MaybePackage::Ready)
.ok_or_else(|| internal(format!("failed to find {} in path source", id)))
}
async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
panic!("no download should have started")
}
fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
let (max, max_path) = self.last_modified_file(pkg)?;
// Note that we try to strip the prefix of this package to get a
// relative path to ensure that the fingerprint remains consistent
// across entire project directory renames.
let max_path = max_path.strip_prefix(&self.path).unwrap_or(&max_path);
Ok(format!("{} ({})", max, max_path.display()))
}
fn describe(&self) -> String {
match self.source_id.url().to_file_path() {
Ok(path) => path.display().to_string(),
Err(_) => self.source_id.to_string(),
}
}
fn invalidate_cache(&self) {
// Path source has no local cache.
}
fn set_quiet(&mut self, _quiet: bool) {
// Path source does not display status
}
}
/// A source that represents one or multiple packages gathered from a given root
/// path on the filesystem.
pub struct RecursivePathSource<'gctx> {
/// The unique identifier of this source.
source_id: SourceId,
/// The root path of this source.
path: PathBuf,
/// Whether this source has loaded all package information it may contain.
loaded: Cell<bool>,
/// Packages that this sources has discovered.
///
/// Tracking all packages for a given ID to warn on-demand for unused packages
packages: RefCell<HashMap<PackageId, Vec<Package>>>,
/// Avoid redundant unused package warnings
warned_duplicate: RefCell<HashSet<PackageId>>,
gctx: &'gctx GlobalContext,
}
impl<'gctx> RecursivePathSource<'gctx> {
/// Creates a new source which is walked recursively to discover packages.
///
/// This is similar to the [`PathSource::new`] method except that instead
/// of requiring a valid package to be present at `root` the folder is
/// walked entirely to crawl for packages.
///
/// Note that this should be used with care and likely shouldn't be chosen
/// by default!
pub fn new(root: &Path, source_id: SourceId, gctx: &'gctx GlobalContext) -> Self {
Self {
source_id,
path: root.to_path_buf(),
loaded: Cell::new(false),
packages: Default::default(),
warned_duplicate: Default::default(),
gctx,
}
}
/// Returns the packages discovered by this source. It may walk the
/// filesystem if package information haven't yet loaded.
pub fn read_packages(&mut self) -> CargoResult<Vec<Package>> {
self.load()?;
Ok(self
.packages
.borrow()
.iter()
.map(|(pkg_id, v)| {
first_package(
*pkg_id,
v,
&mut self.warned_duplicate.borrow_mut(),
self.gctx,
)
.clone()
})
.collect())
}
/// List all files relevant to building this package inside this source.
///
/// This function will use the appropriate methods to determine the
/// set of files underneath this source's directory which are relevant for
/// building `pkg`.
///
/// The basic assumption of this method is that all files in the directory
/// are relevant for building this package, but it also contains logic to
/// use other methods like `.gitignore`, `package.include`, or
/// `package.exclude` to filter the list of files.
pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathEntry>> {
list_files(pkg, self.gctx)
}
/// Gets the last modified file in a package.
fn last_modified_file(&self, pkg: &Package) -> CargoResult<(FileTime, PathBuf)> {
if !self.loaded.get() {
return Err(internal(format!(
"BUG: source `{:?}` was not loaded",
self.path
)));
}
last_modified_file(&self.path, pkg, self.gctx)
}
/// Returns the root path of this source.
pub fn path(&self) -> &Path {
&self.path
}
/// Discovers packages inside this source if it hasn't yet done.
pub fn load(&self) -> CargoResult<()> {
if !self.loaded.get() {
self.packages
.replace(read_packages(&self.path, self.source_id, self.gctx)?);
self.loaded.set(true);
}
Ok(())
}
}
impl<'gctx> Debug for RecursivePathSource<'gctx> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "the paths source")
}
}
#[async_trait::async_trait(?Send)]
impl<'gctx> Source for RecursivePathSource<'gctx> {
async fn query(
&self,
dep: &Dependency,
kind: QueryKind,
f: &mut dyn FnMut(IndexSummary),
) -> CargoResult<()> {
self.load()?;
for s in self
.packages
.borrow()
.iter()
.filter(|(pkg_id, _)| pkg_id.name() == dep.package_name())
.map(|(pkg_id, pkgs)| {
first_package(
*pkg_id,
pkgs,
&mut self.warned_duplicate.borrow_mut(),
self.gctx,
)
})
.map(|p| p.summary())
{
let matched = match kind {
QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(s),
QueryKind::AlternativeNames => true,
QueryKind::Normalized => dep.matches(s),
};
if matched {
f(IndexSummary::Candidate(s.clone()))
}
}
Ok(())
}
fn supports_checksums(&self) -> bool {
false
}
fn requires_precise(&self) -> bool {
false
}
fn source_id(&self) -> SourceId {
self.source_id
}
async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
trace!("getting packages; id={}", id);
self.load()?;
let pkgs = self.packages.borrow();
let pkg = pkgs.get(&id);
pkg.map(|pkgs| {
first_package(id, pkgs, &mut self.warned_duplicate.borrow_mut(), self.gctx).clone()
})
.map(MaybePackage::Ready)
.ok_or_else(|| internal(format!("failed to find {} in path source", id)))
}
async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
panic!("no download should have started")
}
fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
let (max, max_path) = self.last_modified_file(pkg)?;
// Note that we try to strip the prefix of this package to get a
// relative path to ensure that the fingerprint remains consistent
// across entire project directory renames.
let max_path = max_path.strip_prefix(&self.path).unwrap_or(&max_path);
Ok(format!("{} ({})", max, max_path.display()))
}
fn describe(&self) -> String {
match self.source_id.url().to_file_path() {
Ok(path) => path.display().to_string(),
Err(_) => self.source_id.to_string(),
}
}
fn invalidate_cache(&self) {
// Path source has no local cache.
}
fn set_quiet(&mut self, _quiet: bool) {
// Path source does not display status
}
}
/// Type that abstracts over [`gix::dir::entry::Kind`] and [`fs::FileType`].
#[derive(Debug, Clone, Copy)]
enum FileType {
File { maybe_symlink: bool },
Dir,
Symlink,
Other,
}
impl From<fs::FileType> for FileType {
fn from(value: fs::FileType) -> Self {
if value.is_file() {
FileType::File {
maybe_symlink: false,
}
} else if value.is_dir() {
FileType::Dir
} else if value.is_symlink() {
FileType::Symlink
} else {
FileType::Other
}
}
}
impl From<gix::dir::entry::Kind> for FileType {
fn from(value: gix::dir::entry::Kind) -> Self {
use gix::dir::entry::Kind;
match value {
Kind::Untrackable => FileType::Other,
Kind::File => FileType::File {
maybe_symlink: false,
},
Kind::Symlink => FileType::Symlink,
Kind::Directory | Kind::Repository => FileType::Dir,
}
}
}
/// [`PathBuf`] with extra metadata.
#[derive(Clone, Debug)]
pub struct PathEntry {
path: PathBuf,
ty: FileType,
/// Whether this path was visited when traversing a symlink directory.
under_symlink_dir: bool,
}
impl PathEntry {
pub fn into_path_buf(self) -> PathBuf {
self.path
}
/// Similar to [`std::path::Path::is_file`]
/// but doesn't follow the symbolic link nor make any system call
pub fn is_file(&self) -> bool {
matches!(self.ty, FileType::File { .. })
}
/// Similar to [`std::path::Path::is_dir`]
/// but doesn't follow the symbolic link nor make any system call
pub fn is_dir(&self) -> bool {
matches!(self.ty, FileType::Dir)
}
/// Similar to [`std::path::Path::is_symlink`]
/// but doesn't follow the symbolic link nor make any system call
///
/// If the path is not a symlink but under a symlink parent directory,
/// this will return false.
/// See [`PathEntry::is_symlink_or_under_symlink`] for an alternative.
pub fn is_symlink(&self) -> bool {
matches!(self.ty, FileType::Symlink)
}
/// Whether a path is a symlink or a path under a symlink directory.
///
/// Use [`PathEntry::is_symlink`] to get the exact file type of the path only.
pub fn is_symlink_or_under_symlink(&self) -> bool {
self.is_symlink() || self.under_symlink_dir
}
/// Whether this path might be a plain text symlink.
///
/// Git may check out symlinks as plain text files that contain the link texts,
/// when either `core.symlinks` is `false`, or on Windows.
pub fn maybe_plain_text_symlink(&self) -> bool {
matches!(
self.ty,
FileType::File {
maybe_symlink: true
}
)
}
}
impl std::ops::Deref for PathEntry {
type Target = Path;
fn deref(&self) -> &Self::Target {
self.path.as_path()
}
}
impl AsRef<PathBuf> for PathEntry {
fn as_ref(&self) -> &PathBuf {
&self.path
}
}
fn first_package<'p>(
pkg_id: PackageId,
pkgs: &'p Vec<Package>,
warned_duplicate: &mut HashSet<PackageId>,
gctx: &GlobalContext,
) -> &'p Package {
if pkgs.len() != 1 && warned_duplicate.insert(pkg_id) {
let ignored = pkgs[1..]
.iter()
// We can assume a package with publish = false isn't intended to be seen
// by users so we can hide the warning about those since the user is unlikely
// to care about those cases.
.filter(|pkg| pkg.publish().is_none())
.collect::<Vec<_>>();
if !ignored.is_empty() {
use std::fmt::Write as _;
let plural = if ignored.len() == 1 { "" } else { "s" };
let mut msg = String::new();
let _ = writeln!(&mut msg, "skipping duplicate package{plural} `{pkg_id}`:");
for ignored in ignored {
let manifest_path = ignored.manifest_path().display();
let _ = writeln!(&mut msg, " {manifest_path}");
}
let manifest_path = pkgs[0].manifest_path().display();
let _ = writeln!(&mut msg, "in favor of {manifest_path}");
let _ = gctx.shell().warn(msg);
}
}
&pkgs[0]
}
/// List all files relevant to building this package inside this source.
///
/// This function will use the appropriate methods to determine the
/// set of files underneath this source's directory which are relevant for
/// building `pkg`.
///
/// The basic assumption of this method is that all files in the directory
/// are relevant for building this package, but it also contains logic to
/// use other methods like `.gitignore`, `package.include`, or
/// `package.exclude` to filter the list of files.
pub fn list_files(pkg: &Package, gctx: &GlobalContext) -> CargoResult<Vec<PathEntry>> {
_list_files(pkg, gctx).with_context(|| {
format!(
"failed to determine list of files in {}",
pkg.root().display()
)
})
}
/// See [`PathSource::list_files`].
fn _list_files(pkg: &Package, gctx: &GlobalContext) -> CargoResult<Vec<PathEntry>> {
let root = pkg.root();
let no_include_option = pkg.manifest().include().is_empty();
let git_repo = if no_include_option {
discover_gix_repo(root)?
} else {
None
};
let mut exclude_builder = GitignoreBuilder::new(root);
if no_include_option && git_repo.is_none() {
// no include option and not git repo discovered (see rust-lang/cargo#7183).
exclude_builder.add_line(None, ".*")?;
}
for rule in pkg.manifest().exclude() {
exclude_builder.add_line(None, rule)?;
}
let ignore_exclude = exclude_builder.build()?;
let mut include_builder = GitignoreBuilder::new(root);
for rule in pkg.manifest().include() {
include_builder.add_line(None, rule)?;
}
let ignore_include = include_builder.build()?;
let ignore_should_package = |relative_path: &Path, is_dir: bool| {
// "Include" and "exclude" options are mutually exclusive.
if no_include_option {
!ignore_exclude
.matched_path_or_any_parents(relative_path, is_dir)
.is_ignore()
} else {
if is_dir {
// Generally, include directives don't list every
// directory (nor should they!). Just skip all directory
// checks, and only check files.
return true;
}
ignore_include
.matched_path_or_any_parents(relative_path, /* is_dir */ false)
.is_ignore()
}
};
let filter = |path: &Path, is_dir: bool| {
let Ok(relative_path) = path.strip_prefix(root) else {
return false;
};
let rel = relative_path.as_os_str();
if rel == "Cargo.lock" || rel == "Cargo.toml" {
return true;
}
ignore_should_package(relative_path, is_dir)
};
// Attempt Git-prepopulate only if no `include` (see rust-lang/cargo#4135).
if no_include_option {
if let Some(repo) = git_repo {
return list_files_gix(pkg, &repo, &filter, gctx);
}
}
let mut ret = Vec::new();
list_files_walk(pkg.root(), &mut ret, true, &filter, gctx)?;
Ok(ret)
}
/// Returns [`Some(gix::Repository)`](gix::Repository) if the discovered repository
/// (searched upwards from `root`) contains a tracked `<root>/Cargo.toml`.
/// Otherwise, the caller should fall back on full file list.
fn discover_gix_repo(root: &Path) -> CargoResult<Option<gix::Repository>> {
let repo = match gix::ThreadSafeRepository::discover(root) {
Ok(repo) => repo.to_thread_local(),
Err(e) => {
tracing::debug!(
"could not discover git repo at or above {}: {}",
root.display(),
e
);
return Ok(None);
}
};
let index = repo
.index_or_empty()
.with_context(|| format!("failed to open git index at {}", repo.path().display()))?;
let repo_root = repo.workdir().ok_or_else(|| {
anyhow::format_err!(
"did not expect repo at {} to be bare",
repo.path().display()
)
})?;
let repo_relative_path = match paths::strip_prefix_canonical(root, repo_root) {
Ok(p) => p,
Err(e) => {
warn!(
"cannot determine if path `{:?}` is in git repo `{:?}`: {:?}",
root, repo_root, e
);
return Ok(None);
}
};
let manifest_path = gix::path::join_bstr_unix_pathsep(
gix::path::to_unix_separators_on_windows(gix::path::into_bstr(repo_relative_path)),
"Cargo.toml",
);
if index.entry_index_by_path(&manifest_path).is_ok() {
return Ok(Some(repo));
}
// Package Cargo.toml is not in git, don't use git to guide our selection.
Ok(None)
}
/// Lists files relevant to building this package inside this source by
/// traversing the git working tree, while avoiding ignored files.
///
/// This looks into Git sub-repositories as well, resolving them to individual files.
/// Symlinks to directories will also be resolved, but walked as repositories if they
/// point to one to avoid picking up `.git` directories.
fn list_files_gix(
pkg: &Package,
repo: &gix::Repository,
filter: &dyn Fn(&Path, bool) -> bool,
gctx: &GlobalContext,
) -> CargoResult<Vec<PathEntry>> {
debug!("list_files_gix {}", pkg.package_id());
let options = repo
.dirwalk_options()?
.emit_untracked(gix::dir::walk::EmissionMode::Matching)
.emit_ignored(None)
.emit_tracked(true)
.recurse_repositories(false)
.symlinks_to_directories_are_ignored_like_directories(true)
.emit_empty_directories(false);
let index = repo.index_or_empty()?;
let root = repo
.workdir()
.ok_or_else(|| anyhow::format_err!("can't list files on a bare repository"))?;
assert!(
root.is_absolute(),
"BUG: paths used internally are absolute, and the repo inherits that"
);
let pkg_path = pkg.root();
let repo_relative_pkg_path = pkg_path.strip_prefix(root).unwrap_or(Path::new(""));
let target_prefix = gix::path::to_unix_separators_on_windows(gix::path::into_bstr(
repo_relative_pkg_path.join("target/"),
));
let package_prefix =
gix::path::to_unix_separators_on_windows(gix::path::into_bstr(repo_relative_pkg_path));
let pathspec = {
// Include the package root.
let mut include = BString::from(":(top)");
include.push_str(package_prefix.as_ref());
// Exclude the target directory.
let mut exclude = BString::from(":!(exclude,top)");
exclude.push_str(target_prefix.as_ref());
vec![include, exclude]
};
let mut files = Vec::<PathEntry>::new();
let mut subpackages_found = Vec::new();
for item in repo
.dirwalk_iter(index.clone(), pathspec, Default::default(), options)?
.filter(|res| {
// Don't include Cargo.lock if it is untracked. Packaging will
// generate a new one as needed.
// Also don't include untrackable directory entries, like FIFOs.
res.as_ref().map_or(true, |item| {
item.entry.disk_kind != Some(gix::dir::entry::Kind::Untrackable)
&& !(item.entry.status == Status::Untracked
&& item.entry.rela_path == "Cargo.lock")
})
})
.map(|res| {
res.map(|item| {
// Assumption: if a file tracked as a symlink in Git index, and
// the actual file type on disk is file, then it might be a
// plain text file symlink.
// There are exceptions like the file has changed from a symlink
// to a real text file, but hasn't been committed to Git index.
// Exceptions may be rare so we're okay with this now.
let maybe_plain_text_symlink = item.entry.index_kind
== Some(gix::dir::entry::Kind::Symlink)
&& item.entry.disk_kind == Some(gix::dir::entry::Kind::File);
(
item.entry.rela_path,
item.entry.disk_kind,
maybe_plain_text_symlink,
)
})
})
.chain(
// Append entries that might be tracked in `<pkg_root>/target/`.
index
.prefixed_entries(target_prefix.as_ref())
.unwrap_or_default()
.iter()
.filter(|entry| {
// probably not needed as conflicts prevent this to run, but let's be explicit.
entry.stage() == Stage::Unconflicted
})
.map(|entry| {
(
entry.path(&index).to_owned(),
// Do not trust what's recorded in the index, enforce checking the disk.
// This traversal is not part of a `status()`, and tracking things in `target/`
// is rare.
None,
false,
)
})
.map(Ok),
)
{
let (rela_path, kind, maybe_plain_text_symlink) = item?;
let file_path = root.join(gix::path::from_bstr(rela_path));
if file_path.file_name().and_then(|name| name.to_str()) == Some("Cargo.toml") {
// Keep track of all sub-packages found and also strip out all
// matches we've found so far. Note, though, that if we find
// our own `Cargo.toml`, we keep going.
let path = file_path.parent().unwrap();
if path != pkg_path {
debug!("subpackage found: {}", path.display());
files.retain(|p| !p.starts_with(path));
subpackages_found.push(path.to_path_buf());
continue;
}
}
// If this file is part of any other sub-package we've found so far,
// skip it.
if subpackages_found.iter().any(|p| file_path.starts_with(p)) {
continue;
}
let is_dir = kind.map_or(false, |kind| {
if kind == gix::dir::entry::Kind::Symlink {
// Symlinks must be checked to see if they point to a directory
// we should traverse.
file_path.is_dir()
} else {
kind.is_dir()
}
});
if is_dir {
// This could be a submodule, or a sub-repository. In any case, we prefer to walk
// it with git-support to leverage ignored files and to avoid pulling in entire
// .git repositories.
match gix::open(&file_path) {
Ok(sub_repo) => {
files.extend(list_files_gix(pkg, &sub_repo, filter, gctx)?);
}
Err(_) => {
list_files_walk(&file_path, &mut files, false, filter, gctx)?;
}
}
} else if (filter)(&file_path, is_dir) {
assert!(!is_dir);
trace!(" found {}", file_path.display());
let ty = match kind.map(Into::into) {
Some(FileType::File { .. }) => FileType::File {
maybe_symlink: maybe_plain_text_symlink,
},
Some(ty) => ty,
None => FileType::Other,
};
files.push(PathEntry {
path: file_path,
ty,
// Git index doesn't include files from symlink directory,
// symlink dirs are handled in `list_files_walk`.
under_symlink_dir: false,
});
}
}
return Ok(files);
}
/// Lists files relevant to building this package inside this source by
/// walking the filesystem from the package root path.
///
/// This is a fallback for [`list_files_gix`] when the package
/// is not tracked under a Git repository.
fn list_files_walk(
path: &Path,
ret: &mut Vec<PathEntry>,
is_root: bool,
filter: &dyn Fn(&Path, bool) -> bool,
gctx: &GlobalContext,
) -> CargoResult<()> {
let walkdir = WalkDir::new(path)
.follow_links(true)
// While this is the default, set it explicitly.
// We need walkdir to visit the directory tree in depth-first order,
// so we can ensure a path visited later be under a certain directory.
.contents_first(false)
.into_iter()
.filter_entry(|entry| {
let path = entry.path();
let at_root = is_root && entry.depth() == 0;
let is_dir = entry.file_type().is_dir();
if !at_root && !filter(path, is_dir) {
return false;
}
if !is_dir {
return true;
}
// Don't recurse into any sub-packages that we have.
if !at_root && path.join("Cargo.toml").exists() {
return false;
}
// Skip root Cargo artifacts.
if is_root
&& entry.depth() == 1
&& path.file_name().and_then(|s| s.to_str()) == Some("target")
{
return false;
}
true
});
let mut current_symlink_dir = None;
for entry in walkdir {
match entry {
Ok(entry) => {
let file_type = entry.file_type();
match current_symlink_dir.as_ref() {
Some(dir) if entry.path().starts_with(dir) => {
// Still walk under the same parent symlink dir, so keep it
}
Some(_) | None => {
// Not under any parent symlink dir, update the current one.
current_symlink_dir = if file_type.is_dir() && entry.path_is_symlink() {
Some(entry.path().to_path_buf())
} else {
None
};
}
}
if file_type.is_file() || file_type.is_symlink() {
// We follow_links(true) here so check if entry was created from a symlink
let ty = if entry.path_is_symlink() {
FileType::Symlink
} else {
file_type.into()
};
ret.push(PathEntry {
path: entry.into_path(),
ty,
// This rely on contents_first(false), which walks in depth-first order
under_symlink_dir: current_symlink_dir.is_some(),
});
}
}
Err(err) if err.loop_ancestor().is_some() => {
gctx.shell().warn(err)?;
}
Err(err) => match err.path() {
// If an error occurs with a path, filter it again.
// If it is excluded, Just ignore it in this case.
// See issue rust-lang/cargo#10917
Some(path) if !filter(path, path.is_dir()) => {}
// Otherwise, simply recover from it.
// Don't worry about error skipping here, the callers would
// still hit the IO error if they do access it thereafter.
Some(path) => ret.push(PathEntry {
path: path.to_path_buf(),
ty: FileType::Other,
under_symlink_dir: false,
}),
None => return Err(err.into()),
},
}
}
Ok(())
}
/// Gets the last modified file in a package.
fn last_modified_file(
path: &Path,
pkg: &Package,
gctx: &GlobalContext,
) -> CargoResult<(FileTime, PathBuf)> {
let mut max = FileTime::zero();
let mut max_path = PathBuf::new();
for file in list_files(pkg, gctx).with_context(|| {
format!(
"failed to determine the most recently modified file in {}",
pkg.root().display()
)
})? {
// An `fs::stat` error here is either because path is a
// broken symlink, a permissions error, or a race
// condition where this path was `rm`-ed -- either way,
// we can ignore the error and treat the path's `mtime`
// as `0`.
let mtime = paths::mtime(&file).unwrap_or_else(|_| FileTime::zero());
if mtime > max {
max = mtime;
max_path = file.into_path_buf();
}
}
trace!("last modified file {}: {}", path.display(), max);
Ok((max, max_path))
}
fn read_packages(
path: &Path,
source_id: SourceId,
gctx: &GlobalContext,
) -> CargoResult<HashMap<PackageId, Vec<Package>>> {
let mut all_packages = HashMap::default();
let mut visited = HashSet::<PathBuf>::default();
let mut errors = Vec::<anyhow::Error>::new();
trace!(
"looking for root package: {}, source_id={}",
path.display(),
source_id
);
walk(path, &mut |dir| {
trace!("looking for child package: {}", dir.display());
// Don't recurse into hidden/dot directories unless we're at the toplevel
if dir != path {
let name = dir.file_name().and_then(|s| s.to_str());
if name.map(|s| s.starts_with('.')) == Some(true) {
return Ok(false);
}
// Don't automatically discover packages across git submodules
if dir.join(".git").exists() {
return Ok(false);
}
}
// Don't ever look at target directories
if dir.file_name().and_then(|s| s.to_str()) == Some("target")
&& has_manifest(dir.parent().unwrap())
{
return Ok(false);
}
if has_manifest(dir) {
read_nested_packages(
dir,
&mut all_packages,
source_id,
gctx,
&mut visited,
&mut errors,
)?;
}
Ok(true)
})?;
if all_packages.is_empty() {
match errors.pop() {
Some(err) => Err(err),
None => {
if find_project_manifest_exact(path, "cargo.toml").is_ok() {
Err(anyhow::format_err!(
"could not find `Cargo.toml` in `{}`
help: found `cargo.toml`, consider renaming it to `Cargo.toml`",
path.display()
))
} else {
Err(anyhow::format_err!(
"could not find `Cargo.toml` in `{}`",
path.display()
))
}
}
}
} else {
Ok(all_packages)
}
}
fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
let mut nested_paths = Vec::new();
let normalized = manifest.normalized_toml();
let dependencies = normalized
.dependencies
.iter()
.chain(normalized.build_dependencies())
.chain(normalized.dev_dependencies())
.chain(
normalized
.target
.as_ref()
.into_iter()
.flat_map(|t| t.values())
.flat_map(|t| {
t.dependencies
.iter()
.chain(t.build_dependencies())
.chain(t.dev_dependencies())
}),
);
for dep_table in dependencies {
for dep in dep_table.values() {
let cargo_util_schemas::manifest::InheritableDependency::Value(dep) = dep else {
continue;
};
let cargo_util_schemas::manifest::TomlDependency::Detailed(dep) = dep else {
continue;
};
let Some(path) = dep.path.as_ref() else {
continue;
};
nested_paths.push(PathBuf::from(path.as_str()));
}
}
nested_paths
}
fn walk(path: &Path, callback: &mut dyn FnMut(&Path) -> CargoResult<bool>) -> CargoResult<()> {
if !callback(path)? {
trace!("not processing {}", path.display());
return Ok(());
}
// Ignore any permission denied errors because temporary directories
// can often have some weird permissions on them.
let dirs = match fs::read_dir(path) {
Ok(dirs) => dirs,
Err(ref e) if e.kind() == io::ErrorKind::PermissionDenied => return Ok(()),
Err(e) => {
let cx = format!("failed to read directory `{}`", path.display());
let e = anyhow::Error::from(e);
return Err(e.context(cx));
}
};
let mut dirs = dirs.collect::<Vec<_>>();
dirs.sort_unstable_by_key(|d| d.as_ref().ok().map(|d| d.file_name()));
for dir in dirs {
let dir = dir?;
if dir.file_type()?.is_dir() {
walk(&dir.path(), callback)?;
}
}
Ok(())
}
fn has_manifest(path: &Path) -> bool {
find_project_manifest_exact(path, "Cargo.toml").is_ok()
}
fn read_nested_packages(
path: &Path,
all_packages: &mut HashMap<PackageId, Vec<Package>>,
source_id: SourceId,
gctx: &GlobalContext,
visited: &mut HashSet<PathBuf>,
errors: &mut Vec<anyhow::Error>,
) -> CargoResult<()> {
if !visited.insert(path.to_path_buf()) {
return Ok(());
}
let manifest_path = find_project_manifest_exact(path, "Cargo.toml")?;
let manifest = match read_manifest(&manifest_path, source_id, gctx) {
Err(err) => {
// Ignore malformed manifests found on git repositories
//
// git source try to find and read all manifests from the repository
// but since it's not possible to exclude folders from this search
// it's safer to ignore malformed manifests to avoid
//
// TODO: Add a way to exclude folders?
info!(
"skipping malformed package found at `{}`",
path.to_string_lossy()
);
errors.push(err.into());
return Ok(());
}
Ok(tuple) => tuple,
};
let manifest = match manifest {
EitherManifest::Real(manifest) => manifest,
EitherManifest::Virtual(..) => return Ok(()),
};
let nested = nested_paths(&manifest);
let pkg = Package::new(manifest, &manifest_path);
let pkg_id = pkg.package_id();
all_packages.entry(pkg_id).or_default().push(pkg);
// Registry sources are not allowed to have `path=` dependencies because
// they're all translated to actual registry dependencies.
//
// We normalize the path here ensure that we don't infinitely walk around
// looking for crates. By normalizing we ensure that we visit this crate at
// most once.
//
// TODO: filesystem/symlink implications?
if !source_id.is_registry() {
for p in nested.iter() {
let path = paths::normalize_path(&path.join(p));
let result =
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
// Ignore broken manifests found on git repositories.
//
// A well formed manifest might still fail to load due to reasons
// like referring to a "path" that requires an extra build step.
//
// See https://github.com/rust-lang/cargo/issues/6822.
if let Err(err) = result {
if source_id.is_git() {
info!(
"skipping nested package found at `{}`: {:?}",
path.display(),
&err,
);
errors.push(err);
} else {
return Err(err);
}
}
}
}
Ok(())
}