Skip to content

Commit 9eeece1

Browse files
committed
Auto merge of #6740 - ehuss:package-change-detection, r=alexcrichton
Stricter package change detection. This changes it so that when `cargo package` verifies that nothing has modified any source files that it hashes the files' contents instead of checking for mtime changes. mtimes are not always reliable. This also changes it so that it checks *all* files in the package file. Previously it skipped files based on search rules such as the `exclude` list. Closes #6717
2 parents 8ebf915 + 78a60bc commit 9eeece1

File tree

3 files changed

+83
-19
lines changed

3 files changed

+83
-19
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ termcolor = "1.0"
5959
toml = "0.4.2"
6060
url = "1.1"
6161
url_serde = "0.2.0"
62+
walkdir = "2.2"
6263
clap = "2.31.2"
6364
unicode-width = "0.1.5"
6465
openssl = { version = '0.10.11', optional = true }

src/cargo/ops/cargo_package.rs

+69-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fs::{self, File};
23
use std::io::prelude::*;
34
use std::io::SeekFrom;
@@ -439,7 +440,7 @@ fn run_verify(ws: &Workspace<'_>, tar: &FileLock, opts: &PackageOpts<'_>) -> Car
439440
let id = SourceId::for_path(&dst)?;
440441
let mut src = PathSource::new(&dst, id, ws.config());
441442
let new_pkg = src.root_package()?;
442-
let pkg_fingerprint = src.last_modified_file(&new_pkg)?;
443+
let pkg_fingerprint = hash_all(&dst)?;
443444
let ws = Workspace::ephemeral(new_pkg, config, None, true)?;
444445

445446
let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
@@ -465,21 +466,83 @@ fn run_verify(ws: &Workspace<'_>, tar: &FileLock, opts: &PackageOpts<'_>) -> Car
465466
)?;
466467

467468
// Check that `build.rs` didn't modify any files in the `src` directory.
468-
let ws_fingerprint = src.last_modified_file(ws.current()?)?;
469+
let ws_fingerprint = hash_all(&dst)?;
469470
if pkg_fingerprint != ws_fingerprint {
470-
let (_, path) = ws_fingerprint;
471+
let changes = report_hash_difference(&pkg_fingerprint, &ws_fingerprint);
471472
failure::bail!(
472473
"Source directory was modified by build.rs during cargo publish. \
473-
Build scripts should not modify anything outside of OUT_DIR. \
474-
Modified file: {}\n\n\
474+
Build scripts should not modify anything outside of OUT_DIR.\n\
475+
{}\n\n\
475476
To proceed despite this, pass the `--no-verify` flag.",
476-
path.display()
477+
changes
477478
)
478479
}
479480

480481
Ok(())
481482
}
482483

484+
fn hash_all(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
485+
fn wrap(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
486+
let mut result = HashMap::new();
487+
let walker = walkdir::WalkDir::new(path).into_iter();
488+
for entry in walker.filter_entry(|e| {
489+
!(e.depth() == 1 && (e.file_name() == "target" || e.file_name() == "Cargo.lock"))
490+
}) {
491+
let entry = entry?;
492+
let file_type = entry.file_type();
493+
if file_type.is_file() {
494+
let contents = fs::read(entry.path())?;
495+
let hash = util::hex::hash_u64(&contents);
496+
result.insert(entry.path().to_path_buf(), hash);
497+
} else if file_type.is_symlink() {
498+
let hash = util::hex::hash_u64(&fs::read_link(entry.path())?);
499+
result.insert(entry.path().to_path_buf(), hash);
500+
}
501+
}
502+
Ok(result)
503+
}
504+
let result = wrap(path).chain_err(|| format!("failed to verify output at {:?}", path))?;
505+
Ok(result)
506+
}
507+
508+
fn report_hash_difference(
509+
orig: &HashMap<PathBuf, u64>,
510+
after: &HashMap<PathBuf, u64>,
511+
) -> String {
512+
let mut changed = Vec::new();
513+
let mut removed = Vec::new();
514+
for (key, value) in orig {
515+
match after.get(key) {
516+
Some(after_value) => {
517+
if value != after_value {
518+
changed.push(key.to_string_lossy());
519+
}
520+
}
521+
None => removed.push(key.to_string_lossy()),
522+
}
523+
}
524+
let mut added: Vec<_> = after
525+
.keys()
526+
.filter(|key| !orig.contains_key(*key))
527+
.map(|key| key.to_string_lossy())
528+
.collect();
529+
let mut result = Vec::new();
530+
if !changed.is_empty() {
531+
changed.sort_unstable();
532+
result.push(format!("Changed: {}", changed.join("\n\t")));
533+
}
534+
if !added.is_empty() {
535+
added.sort_unstable();
536+
result.push(format!("Added: {}", added.join("\n\t")));
537+
}
538+
if !removed.is_empty() {
539+
removed.sort_unstable();
540+
result.push(format!("Removed: {}", removed.join("\n\t")));
541+
}
542+
assert!(!result.is_empty(), "unexpected empty change detection");
543+
result.join("\n")
544+
}
545+
483546
// It can often be the case that files of a particular name on one platform
484547
// can't actually be created on another platform. For example files with colons
485548
// in the name are allowed on Unix but not on Windows.

tests/testsuite/package.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ use std::fs::File;
33
use std::io::prelude::*;
44
use std::path::Path;
55

6+
use crate::support::cargo_process;
67
use crate::support::registry::Package;
78
use crate::support::{
89
basic_manifest, git, is_nightly, path2url, paths, project, publish::validate_crate_contents,
910
registry,
1011
};
11-
use crate::support::{cargo_process, sleep_ms};
1212
use git2;
1313

1414
#[test]
@@ -1201,27 +1201,24 @@ fn lock_file_and_workspace() {
12011201
fn do_not_package_if_src_was_modified() {
12021202
let p = project()
12031203
.file("src/main.rs", r#"fn main() { println!("hello"); }"#)
1204+
.file("foo.txt", "")
1205+
.file("bar.txt", "")
12041206
.file(
12051207
"build.rs",
12061208
r#"
1207-
use std::fs::File;
1208-
use std::io::Write;
1209+
use std::fs;
12091210
12101211
fn main() {
1211-
let mut file = File::create("src/generated.txt").expect("failed to create file");
1212-
file.write_all(b"Hello, world of generated files.").expect("failed to write");
1212+
fs::write("src/generated.txt",
1213+
"Hello, world of generated files."
1214+
).expect("failed to create file");
1215+
fs::remove_file("foo.txt").expect("failed to remove");
1216+
fs::write("bar.txt", "updated content").expect("failed to update");
12131217
}
12141218
"#,
12151219
)
12161220
.build();
12171221

1218-
if cfg!(target_os = "macos") {
1219-
// MacOS has 1s resolution filesystem.
1220-
// If src/main.rs is created within 1s of src/generated.txt, then it
1221-
// won't trigger the modification check.
1222-
sleep_ms(1000);
1223-
}
1224-
12251222
p.cargo("package")
12261223
.with_status(101)
12271224
.with_stderr_contains(
@@ -1230,7 +1227,10 @@ error: failed to verify package tarball
12301227
12311228
Caused by:
12321229
Source directory was modified by build.rs during cargo publish. \
1233-
Build scripts should not modify anything outside of OUT_DIR. Modified file: [..]src/generated.txt
1230+
Build scripts should not modify anything outside of OUT_DIR.
1231+
Changed: [CWD]/target/package/foo-0.0.1/bar.txt
1232+
Added: [CWD]/target/package/foo-0.0.1/src/generated.txt
1233+
Removed: [CWD]/target/package/foo-0.0.1/foo.txt
12341234
12351235
To proceed despite this, pass the `--no-verify` flag.",
12361236
)

0 commit comments

Comments
 (0)