Skip to content

Commit cee088b

Browse files
committed
Check if rerun-if-changed points to a directory.
1 parent dd41106 commit cee088b

File tree

4 files changed

+133
-13
lines changed

4 files changed

+133
-13
lines changed

src/cargo/core/compiler/fingerprint.rs

Lines changed: 1 addition & 1 deletion
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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,42 @@ 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| e.ok())
194+
.filter_map(|e| {
195+
if e.path_is_symlink() {
196+
// Use the mtime of both the symlink and its target, to
197+
// handle the case where the symlink is modified to a
198+
// different target.
199+
let sym_meta = std::fs::symlink_metadata(e.path()).ok()?;
200+
let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
201+
// Walkdir follows symlinks.
202+
match e.metadata() {
203+
Ok(target_meta) => {
204+
let target_mtime = FileTime::from_last_modification_time(&target_meta);
205+
Some(sym_mtime.max(target_mtime))
206+
}
207+
Err(_) => Some(sym_mtime),
208+
}
209+
} else {
210+
let meta = e.metadata().ok()?;
211+
Some(FileTime::from_last_modification_time(&meta))
212+
}
213+
})
214+
.max()
215+
.unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
216+
Ok(max_meta)
217+
}
218+
183219
/// Record the current time on the filesystem (using the filesystem's clock)
184220
/// using a file at the given directory. Returns the current time.
185221
pub fn set_invocation_time(path: &Path) -> CargoResult<FileTime> {

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

Lines changed: 7 additions & 10 deletions
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

Lines changed: 89 additions & 2 deletions
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)