Skip to content

Commit a3c2627

Browse files
committed
Auto merge of #8973 - ehuss:rerun-if-directory, r=alexcrichton
Check if rerun-if-changed points to a directory. This changes it so that if a build script emits `cargo:rerun-if-changed` pointing to a directory, then Cargo will scan the entire directory for changes (instead of just looking at the mtime of the directory itself). I think this is more useful, as otherwise build scripts have to recreate this logic. I've tried to make it semi-intelligent in the face of symbolic links. It checks the mtime of the link and its target, and follows the link if it points to a directory. There are a few other edge cases. For example, if it doesn't have permission for a directory, it will skip it. I think this is relatively reasonable, though it's hard to say for sure.
2 parents 8917837 + 4c68a4a commit a3c2627

File tree

4 files changed

+178
-13
lines changed

4 files changed

+178
-13
lines changed

src/cargo/core/compiler/fingerprint.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,7 @@ where
17001700
let path_mtime = match mtime_cache.entry(path.to_path_buf()) {
17011701
Entry::Occupied(o) => *o.get(),
17021702
Entry::Vacant(v) => {
1703-
let mtime = match paths::mtime(path) {
1703+
let mtime = match paths::mtime_recursive(path) {
17041704
Ok(mtime) => mtime,
17051705
Err(..) => return Some(StaleItem::MissingFile(path.to_path_buf())),
17061706
};

src/cargo/util/paths.rs

+81
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,87 @@ pub fn mtime(path: &Path) -> CargoResult<FileTime> {
180180
Ok(FileTime::from_last_modification_time(&meta))
181181
}
182182

183+
/// Returns the maximum mtime of the given path, recursing into
184+
/// subdirectories, and following symlinks.
185+
pub fn mtime_recursive(path: &Path) -> CargoResult<FileTime> {
186+
let meta = fs::metadata(path).chain_err(|| format!("failed to stat `{}`", path.display()))?;
187+
if !meta.is_dir() {
188+
return Ok(FileTime::from_last_modification_time(&meta));
189+
}
190+
let max_meta = walkdir::WalkDir::new(path)
191+
.follow_links(true)
192+
.into_iter()
193+
.filter_map(|e| match e {
194+
Ok(e) => Some(e),
195+
Err(e) => {
196+
// Ignore errors while walking. If Cargo can't access it, the
197+
// build script probably can't access it, either.
198+
log::debug!("failed to determine mtime while walking directory: {}", e);
199+
None
200+
}
201+
})
202+
.filter_map(|e| {
203+
if e.path_is_symlink() {
204+
// Use the mtime of both the symlink and its target, to
205+
// handle the case where the symlink is modified to a
206+
// different target.
207+
let sym_meta = match std::fs::symlink_metadata(e.path()) {
208+
Ok(m) => m,
209+
Err(err) => {
210+
// I'm not sure when this is really possible (maybe a
211+
// race with unlinking?). Regardless, if Cargo can't
212+
// read it, the build script probably can't either.
213+
log::debug!(
214+
"failed to determine mtime while fetching symlink metdata of {}: {}",
215+
e.path().display(),
216+
err
217+
);
218+
return None;
219+
}
220+
};
221+
let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
222+
// Walkdir follows symlinks.
223+
match e.metadata() {
224+
Ok(target_meta) => {
225+
let target_mtime = FileTime::from_last_modification_time(&target_meta);
226+
Some(sym_mtime.max(target_mtime))
227+
}
228+
Err(err) => {
229+
// Can't access the symlink target. If Cargo can't
230+
// access it, the build script probably can't access
231+
// it either.
232+
log::debug!(
233+
"failed to determine mtime of symlink target for {}: {}",
234+
e.path().display(),
235+
err
236+
);
237+
Some(sym_mtime)
238+
}
239+
}
240+
} else {
241+
let meta = match e.metadata() {
242+
Ok(m) => m,
243+
Err(err) => {
244+
// I'm not sure when this is really possible (maybe a
245+
// race with unlinking?). Regardless, if Cargo can't
246+
// read it, the build script probably can't either.
247+
log::debug!(
248+
"failed to determine mtime while fetching metadata of {}: {}",
249+
e.path().display(),
250+
err
251+
);
252+
return None;
253+
}
254+
};
255+
Some(FileTime::from_last_modification_time(&meta))
256+
}
257+
})
258+
.max()
259+
// or_else handles the case where there are no files in the directory.
260+
.unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
261+
Ok(max_meta)
262+
}
263+
183264
/// Record the current time on the filesystem (using the filesystem's clock)
184265
/// using a file at the given directory. Returns the current time.
185266
pub fn set_invocation_time(path: &Path) -> CargoResult<FileTime> {

src/doc/src/reference/build-scripts.md

+7-10
Original file line numberDiff line numberDiff line change
@@ -258,19 +258,16 @@ filesystem last-modified "mtime" timestamp to determine if the file has
258258
changed. It compares against an internal cached timestamp of when the build
259259
script last ran.
260260

261-
If the path points to a directory, it does *not* automatically traverse the
262-
directory for changes. Only the mtime change of the directory itself is
263-
considered (which corresponds to some types of changes within the directory,
264-
depending on platform). To request a re-run on any changes within an entire
265-
directory, print a line for the directory and separate lines for everything
266-
inside it, recursively.
261+
If the path points to a directory, it will scan the entire directory for
262+
any modifications.
267263

268264
If the build script inherently does not need to re-run under any circumstance,
269265
then emitting `cargo:rerun-if-changed=build.rs` is a simple way to prevent it
270-
from being re-run. Cargo automatically handles whether or not the script
271-
itself needs to be recompiled, and of course the script will be re-run after
272-
it has been recompiled. Otherwise, specifying `build.rs` is redundant and
273-
unnecessary.
266+
from being re-run (otherwise, the default if no `rerun-if` instructions are
267+
emitted is to scan the entire package directory for changes). Cargo
268+
automatically handles whether or not the script itself needs to be recompiled,
269+
and of course the script will be re-run after it has been recompiled.
270+
Otherwise, specifying `build.rs` is redundant and unnecessary.
274271

275272
<a id="rerun-if-env-changed"></a>
276273
#### `cargo:rerun-if-env-changed=NAME`

tests/testsuite/build_script.rs

+89-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use std::thread;
88
use cargo::util::paths::remove_dir_all;
99
use cargo_test_support::paths::CargoPathExt;
1010
use cargo_test_support::registry::Package;
11-
use cargo_test_support::{basic_manifest, cross_compile, project};
12-
use cargo_test_support::{rustc_host, sleep_ms, slow_cpu_multiplier};
11+
use cargo_test_support::{basic_manifest, cross_compile, is_coarse_mtime, project};
12+
use cargo_test_support::{rustc_host, sleep_ms, slow_cpu_multiplier, symlink_supported};
1313

1414
#[cargo_test]
1515
fn custom_build_script_failed() {
@@ -4071,3 +4071,90 @@ fn dev_dep_with_links() {
40714071
.build();
40724072
p.cargo("check --tests").run()
40734073
}
4074+
4075+
#[cargo_test]
4076+
fn rerun_if_directory() {
4077+
if !symlink_supported() {
4078+
return;
4079+
}
4080+
4081+
// rerun-if-changed of a directory should rerun if any file in the directory changes.
4082+
let p = project()
4083+
.file("Cargo.toml", &basic_manifest("foo", "0.1.0"))
4084+
.file("src/lib.rs", "")
4085+
.file(
4086+
"build.rs",
4087+
r#"
4088+
fn main() {
4089+
println!("cargo:rerun-if-changed=somedir");
4090+
}
4091+
"#,
4092+
)
4093+
.build();
4094+
4095+
let dirty = || {
4096+
p.cargo("check")
4097+
.with_stderr(
4098+
"[COMPILING] foo [..]\n\
4099+
[FINISHED] [..]",
4100+
)
4101+
.run();
4102+
};
4103+
4104+
let fresh = || {
4105+
p.cargo("check").with_stderr("[FINISHED] [..]").run();
4106+
};
4107+
4108+
// Start with a missing directory.
4109+
dirty();
4110+
// Because the directory doesn't exist, it will trigger a rebuild every time.
4111+
// https://github.com/rust-lang/cargo/issues/6003
4112+
dirty();
4113+
4114+
if is_coarse_mtime() {
4115+
sleep_ms(1000);
4116+
}
4117+
4118+
// Empty directory.
4119+
fs::create_dir(p.root().join("somedir")).unwrap();
4120+
dirty();
4121+
fresh();
4122+
4123+
if is_coarse_mtime() {
4124+
sleep_ms(1000);
4125+
}
4126+
4127+
// Add a file.
4128+
p.change_file("somedir/foo", "");
4129+
p.change_file("somedir/bar", "");
4130+
dirty();
4131+
fresh();
4132+
4133+
if is_coarse_mtime() {
4134+
sleep_ms(1000);
4135+
}
4136+
4137+
// Add a symlink.
4138+
p.symlink("foo", "somedir/link");
4139+
dirty();
4140+
fresh();
4141+
4142+
if is_coarse_mtime() {
4143+
sleep_ms(1000);
4144+
}
4145+
4146+
// Move the symlink.
4147+
fs::remove_file(p.root().join("somedir/link")).unwrap();
4148+
p.symlink("bar", "somedir/link");
4149+
dirty();
4150+
fresh();
4151+
4152+
if is_coarse_mtime() {
4153+
sleep_ms(1000);
4154+
}
4155+
4156+
// Remove a file.
4157+
fs::remove_file(p.root().join("somedir/foo")).unwrap();
4158+
dirty();
4159+
fresh();
4160+
}

0 commit comments

Comments
 (0)