Skip to content

Commit ef3cbfc

Browse files
committed
Auto merge of #8095 - ehuss:paths-walking, r=alexcrichton
Updates to path source walking. This is a collection of loosely related changes to path source walking: * Add more context to error messages. * Allow `package.exclude` patterns to match directories. Previously, the walker would recurse into the directory, and skip every file. Instead, just skip the whole directory. This can be helpful if the directory is not readable, or otherwise want to avoid walking. * Don't require `Cargo.toml` to be in root of a git repo in order to use git to guide the selection. I'm not sure I understand the original reasoning that (any) `Cargo.toml` had to reside next to the `.git` directory. The last is a moderately risky change, since it's hard to predict how this might affect more complex project layouts or new interactions with `.gitignore` that didn't exist before. Also, I'm wondering if it should just ignore if it fails to open the repo instead of emitting an error? Closes #1729 Closes #6188 Closes #8092
2 parents 457b47d + 25715e4 commit ef3cbfc

File tree

6 files changed

+195
-70
lines changed

6 files changed

+195
-70
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pretty_env_logger = { version = "0.4", optional = true }
3232
anyhow = "1.0"
3333
filetime = "0.2.9"
3434
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
35-
git2 = "0.13.1"
35+
git2 = "0.13.5"
3636
git2-curl = "0.14.0"
3737
glob = "0.3.0"
3838
hex = "0.4"
@@ -44,7 +44,7 @@ jobserver = "0.1.21"
4444
lazycell = "1.2.0"
4545
libc = "0.2"
4646
log = "0.4.6"
47-
libgit2-sys = "0.12.1"
47+
libgit2-sys = "0.12.5"
4848
memchr = "2.1.3"
4949
num_cpus = "1.0"
5050
opener = "0.4"

src/cargo/core/compiler/fingerprint.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,12 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
12091209
let target_root = target_root(cx);
12101210
let local = if unit.mode.is_doc() {
12111211
// rustdoc does not have dep-info files.
1212-
let fingerprint = pkg_fingerprint(cx.bcx, &unit.pkg)?;
1212+
let fingerprint = pkg_fingerprint(cx.bcx, &unit.pkg).chain_err(|| {
1213+
format!(
1214+
"failed to determine package fingerprint for documenting {}",
1215+
unit.pkg
1216+
)
1217+
})?;
12131218
vec![LocalFingerprint::Precalculated(fingerprint)]
12141219
} else {
12151220
let dep_info = dep_info_loc(cx, unit);
@@ -1270,7 +1275,18 @@ fn calculate_run_custom_build(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoRes
12701275
// the whole crate.
12711276
let (gen_local, overridden) = build_script_local_fingerprints(cx, unit);
12721277
let deps = &cx.build_explicit_deps[unit];
1273-
let local = (gen_local)(deps, Some(&|| pkg_fingerprint(cx.bcx, &unit.pkg)))?.unwrap();
1278+
let local = (gen_local)(
1279+
deps,
1280+
Some(&|| {
1281+
pkg_fingerprint(cx.bcx, &unit.pkg).chain_err(|| {
1282+
format!(
1283+
"failed to determine package fingerprint for build script for {}",
1284+
unit.pkg
1285+
)
1286+
})
1287+
}),
1288+
)?
1289+
.unwrap();
12741290
let output = deps.build_script_output.clone();
12751291

12761292
// Include any dependencies of our execution, which is typically just the

src/cargo/sources/path.rs

Lines changed: 84 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ impl<'cfg> PathSource<'cfg> {
9696
/// are relevant for building this package, but it also contains logic to
9797
/// use other methods like .gitignore to filter the list of files.
9898
pub fn list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
99+
self._list_files(pkg).chain_err(|| {
100+
format!(
101+
"failed to determine list of files in {}",
102+
pkg.root().display()
103+
)
104+
})
105+
}
106+
107+
fn _list_files(&self, pkg: &Package) -> CargoResult<Vec<PathBuf>> {
99108
let root = pkg.root();
100109
let no_include_option = pkg.manifest().include().is_empty();
101110

@@ -111,17 +120,21 @@ impl<'cfg> PathSource<'cfg> {
111120
}
112121
let ignore_include = include_builder.build()?;
113122

114-
let ignore_should_package = |relative_path: &Path| -> CargoResult<bool> {
123+
let ignore_should_package = |relative_path: &Path, is_dir: bool| -> CargoResult<bool> {
115124
// "Include" and "exclude" options are mutually exclusive.
116125
if no_include_option {
117-
match ignore_exclude
118-
.matched_path_or_any_parents(relative_path, /* is_dir */ false)
119-
{
126+
match ignore_exclude.matched_path_or_any_parents(relative_path, is_dir) {
120127
Match::None => Ok(true),
121128
Match::Ignore(_) => Ok(false),
122129
Match::Whitelist(_) => Ok(true),
123130
}
124131
} else {
132+
if is_dir {
133+
// Generally, include directives don't list every
134+
// directory (nor should they!). Just skip all directory
135+
// checks, and only check files.
136+
return Ok(true);
137+
}
125138
match ignore_include
126139
.matched_path_or_any_parents(relative_path, /* is_dir */ false)
127140
{
@@ -132,7 +145,7 @@ impl<'cfg> PathSource<'cfg> {
132145
}
133146
};
134147

135-
let mut filter = |path: &Path| -> CargoResult<bool> {
148+
let mut filter = |path: &Path, is_dir: bool| -> CargoResult<bool> {
136149
let relative_path = path.strip_prefix(root)?;
137150

138151
let rel = relative_path.as_os_str();
@@ -142,13 +155,13 @@ impl<'cfg> PathSource<'cfg> {
142155
return Ok(true);
143156
}
144157

145-
ignore_should_package(relative_path)
158+
ignore_should_package(relative_path, is_dir)
146159
};
147160

148161
// Attempt Git-prepopulate only if no `include` (see rust-lang/cargo#4135).
149162
if no_include_option {
150-
if let Some(result) = self.discover_git_and_list_files(pkg, root, &mut filter) {
151-
return result;
163+
if let Some(result) = self.discover_git_and_list_files(pkg, root, &mut filter)? {
164+
return Ok(result);
152165
}
153166
// no include option and not git repo discovered (see rust-lang/cargo#7183).
154167
return self.list_files_walk_except_dot_files_and_dirs(pkg, &mut filter);
@@ -162,50 +175,48 @@ impl<'cfg> PathSource<'cfg> {
162175
&self,
163176
pkg: &Package,
164177
root: &Path,
165-
filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
166-
) -> Option<CargoResult<Vec<PathBuf>>> {
167-
// If this package is in a Git repository, then we really do want to
168-
// query the Git repository as it takes into account items such as
169-
// `.gitignore`. We're not quite sure where the Git repository is,
170-
// however, so we do a bit of a probe.
171-
//
172-
// We walk this package's path upwards and look for a sibling
173-
// `Cargo.toml` and `.git` directory. If we find one then we assume that
174-
// we're part of that repository.
175-
let mut cur = root;
176-
loop {
177-
if cur.join("Cargo.toml").is_file() {
178-
// If we find a Git repository next to this `Cargo.toml`, we still
179-
// check to see if we are indeed part of the index. If not, then
180-
// this is likely an unrelated Git repo, so keep going.
181-
if let Ok(repo) = git2::Repository::open(cur) {
182-
let index = match repo.index() {
183-
Ok(index) => index,
184-
Err(err) => return Some(Err(err.into())),
185-
};
186-
let path = root.strip_prefix(cur).unwrap().join("Cargo.toml");
187-
if index.get_path(&path, 0).is_some() {
188-
return Some(self.list_files_git(pkg, &repo, filter));
189-
}
190-
}
191-
}
192-
// Don't cross submodule boundaries.
193-
if cur.join(".git").is_dir() {
194-
break;
195-
}
196-
match cur.parent() {
197-
Some(parent) => cur = parent,
198-
None => break,
178+
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
179+
) -> CargoResult<Option<Vec<PathBuf>>> {
180+
let repo = match git2::Repository::discover(root) {
181+
Ok(repo) => repo,
182+
Err(e) => {
183+
log::debug!(
184+
"could not discover git repo at or above {}: {}",
185+
root.display(),
186+
e
187+
);
188+
return Ok(None);
199189
}
190+
};
191+
let index = repo
192+
.index()
193+
.chain_err(|| format!("failed to open git index at {}", repo.path().display()))?;
194+
let repo_root = repo.workdir().ok_or_else(|| {
195+
anyhow::format_err!(
196+
"did not expect repo at {} to be bare",
197+
repo.path().display()
198+
)
199+
})?;
200+
let repo_relative_path = root.strip_prefix(repo_root).chain_err(|| {
201+
format!(
202+
"expected git repo {} to be parent of package {}",
203+
repo.path().display(),
204+
root.display()
205+
)
206+
})?;
207+
let manifest_path = repo_relative_path.join("Cargo.toml");
208+
if index.get_path(&manifest_path, 0).is_some() {
209+
return Ok(Some(self.list_files_git(pkg, &repo, filter)?));
200210
}
201-
None
211+
// Package Cargo.toml is not in git, don't use git to guide our selection.
212+
Ok(None)
202213
}
203214

204215
fn list_files_git(
205216
&self,
206217
pkg: &Package,
207218
repo: &git2::Repository,
208-
filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
219+
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
209220
) -> CargoResult<Vec<PathBuf>> {
210221
warn!("list_files_git {}", pkg.package_id());
211222
let index = repo.index()?;
@@ -289,7 +300,10 @@ impl<'cfg> PathSource<'cfg> {
289300
continue;
290301
}
291302

292-
if is_dir.unwrap_or_else(|| file_path.is_dir()) {
303+
// `is_dir` is None for symlinks. The `unwrap` checks if the
304+
// symlink points to a directory.
305+
let is_dir = is_dir.unwrap_or_else(|| file_path.is_dir());
306+
if is_dir {
293307
warn!(" found submodule {}", file_path.display());
294308
let rel = file_path.strip_prefix(root)?;
295309
let rel = rel.to_str().ok_or_else(|| {
@@ -307,7 +321,8 @@ impl<'cfg> PathSource<'cfg> {
307321
PathSource::walk(&file_path, &mut ret, false, filter)?;
308322
}
309323
}
310-
} else if (*filter)(&file_path)? {
324+
} else if (*filter)(&file_path, is_dir)? {
325+
assert!(!is_dir);
311326
// We found a file!
312327
warn!(" found {}", file_path.display());
313328
ret.push(file_path);
@@ -338,29 +353,28 @@ impl<'cfg> PathSource<'cfg> {
338353
fn list_files_walk_except_dot_files_and_dirs(
339354
&self,
340355
pkg: &Package,
341-
filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
356+
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
342357
) -> CargoResult<Vec<PathBuf>> {
343358
let root = pkg.root();
344359
let mut exclude_dot_files_dir_builder = GitignoreBuilder::new(root);
345360
exclude_dot_files_dir_builder.add_line(None, ".*")?;
346361
let ignore_dot_files_and_dirs = exclude_dot_files_dir_builder.build()?;
347362

348-
let mut filter_ignore_dot_files_and_dirs = |path: &Path| -> CargoResult<bool> {
349-
let relative_path = path.strip_prefix(root)?;
350-
match ignore_dot_files_and_dirs
351-
.matched_path_or_any_parents(relative_path, /* is_dir */ false)
352-
{
353-
Match::Ignore(_) => Ok(false),
354-
_ => filter(path),
355-
}
356-
};
363+
let mut filter_ignore_dot_files_and_dirs =
364+
|path: &Path, is_dir: bool| -> CargoResult<bool> {
365+
let relative_path = path.strip_prefix(root)?;
366+
match ignore_dot_files_and_dirs.matched_path_or_any_parents(relative_path, is_dir) {
367+
Match::Ignore(_) => Ok(false),
368+
_ => filter(path, is_dir),
369+
}
370+
};
357371
self.list_files_walk(pkg, &mut filter_ignore_dot_files_and_dirs)
358372
}
359373

360374
fn list_files_walk(
361375
&self,
362376
pkg: &Package,
363-
filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
377+
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
364378
) -> CargoResult<Vec<PathBuf>> {
365379
let mut ret = Vec::new();
366380
PathSource::walk(pkg.root(), &mut ret, true, filter)?;
@@ -371,12 +385,14 @@ impl<'cfg> PathSource<'cfg> {
371385
path: &Path,
372386
ret: &mut Vec<PathBuf>,
373387
is_root: bool,
374-
filter: &mut dyn FnMut(&Path) -> CargoResult<bool>,
388+
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
375389
) -> CargoResult<()> {
376-
if !path.is_dir() {
377-
if (*filter)(path)? {
378-
ret.push(path.to_path_buf());
379-
}
390+
let is_dir = path.is_dir();
391+
if !is_root && !(*filter)(path, is_dir)? {
392+
return Ok(());
393+
}
394+
if !is_dir {
395+
ret.push(path.to_path_buf());
380396
return Ok(());
381397
}
382398
// Don't recurse into any sub-packages that we have.
@@ -415,7 +431,12 @@ impl<'cfg> PathSource<'cfg> {
415431

416432
let mut max = FileTime::zero();
417433
let mut max_path = PathBuf::new();
418-
for file in self.list_files(pkg)? {
434+
for file in self.list_files(pkg).chain_err(|| {
435+
format!(
436+
"failed to determine the most recently modified file in {}",
437+
pkg.root().display()
438+
)
439+
})? {
419440
// An `fs::stat` error here is either because path is a
420441
// broken symlink, a permissions error, or a race
421442
// condition where this path was `rm`-ed -- either way,

tests/testsuite/build_script.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3977,26 +3977,60 @@ fn links_interrupted_can_restart() {
39773977
fn build_script_scan_eacces() {
39783978
// build.rs causes a scan of the whole project, which can be a problem if
39793979
// a directory is not accessible.
3980+
use cargo_test_support::git;
39803981
use std::os::unix::fs::PermissionsExt;
3982+
39813983
let p = project()
39823984
.file("src/lib.rs", "")
39833985
.file("build.rs", "fn main() {}")
39843986
.file("secrets/stuff", "")
39853987
.build();
39863988
let path = p.root().join("secrets");
39873989
fs::set_permissions(&path, fs::Permissions::from_mode(0)).unwrap();
3988-
// "Caused by" is a string from libc such as the following:
3990+
// The last "Caused by" is a string from libc such as the following:
39893991
// Permission denied (os error 13)
39903992
p.cargo("build")
39913993
.with_stderr(
39923994
"\
3993-
[ERROR] cannot read \"[..]/foo/secrets\"
3995+
[ERROR] failed to determine package fingerprint for build script for foo v0.0.1 ([..]/foo)
3996+
3997+
Caused by:
3998+
failed to determine the most recently modified file in [..]/foo
3999+
4000+
Caused by:
4001+
failed to determine list of files in [..]/foo
4002+
4003+
Caused by:
4004+
cannot read \"[..]/foo/secrets\"
39944005
39954006
Caused by:
39964007
[..]
39974008
",
39984009
)
39994010
.with_status(101)
40004011
.run();
4012+
4013+
// Try `package.exclude` to skip a directory.
4014+
p.change_file(
4015+
"Cargo.toml",
4016+
r#"
4017+
[package]
4018+
name = "foo"
4019+
version = "0.0.1"
4020+
exclude = ["secrets"]
4021+
"#,
4022+
);
4023+
p.cargo("build").run();
4024+
4025+
// Try with git. This succeeds because the git status walker ignores
4026+
// directories it can't access.
4027+
p.change_file("Cargo.toml", &basic_manifest("foo", "0.0.1"));
4028+
p.build_dir().rm_rf();
4029+
let repo = git::init(&p.root());
4030+
git::add(&repo);
4031+
git::commit(&repo);
4032+
p.cargo("build").run();
4033+
4034+
// Restore permissions so that the directory can be deleted.
40014035
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
40024036
}

0 commit comments

Comments
 (0)