diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs index 2056f8cf6a5..5c9bf73a0ee 100644 --- a/src/cargo/ops/cargo_package.rs +++ b/src/cargo/ops/cargo_package.rs @@ -484,6 +484,23 @@ fn timestamp() -> u64 { .as_secs() } +fn canonicalize_header(header: &mut Header) { + // Let's not include information about the user or their system here. + header.set_username("root").unwrap(); + header.set_groupname("root").unwrap(); + header.set_uid(0); + header.set_gid(0); + header.set_device_major(0).unwrap(); + header.set_device_minor(0).unwrap(); + + let mode = if header.mode().unwrap() & 0o100 != 0 { + 0o755 + } else { + 0o644 + }; + header.set_mode(mode); +} + fn tar( ws: &Workspace<'_>, ar_files: Vec, @@ -524,6 +541,8 @@ fn tar( format!("could not learn metadata for: `{}`", disk_path.display()) })?; header.set_metadata(&metadata); + header.set_mtime(time); + canonicalize_header(&mut header); header.set_cksum(); ar.append_data(&mut header, &ar_path, &mut file) .chain_err(|| { @@ -540,6 +559,7 @@ fn tar( header.set_mode(0o644); header.set_mtime(time); header.set_size(contents.len() as u64); + canonicalize_header(&mut header); header.set_cksum(); ar.append_data(&mut header, &ar_path, contents.as_bytes()) .chain_err(|| format!("could not archive source file `{}`", rel_str))?; diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index 96b6d8a18ce..d3900ef65ae 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -6,8 +6,10 @@ use cargo_test_support::registry::{self, Package}; use cargo_test_support::{ basic_manifest, cargo_process, git, path2url, paths, project, symlink_supported, t, }; +use flate2::read::GzDecoder; use std::fs::{self, read_to_string, File}; use std::path::Path; +use tar::Archive; #[cargo_test] fn simple() { @@ -1917,3 +1919,44 @@ src/main.rs )) .run(); } + +#[cargo_test] +fn reproducible_output() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + exclude = ["*.txt"] + license = "MIT" + description = "foo" + "#, + ) + .file("src/main.rs", r#"fn main() { println!("hello"); }"#) + .build(); + + // Timestamp is arbitrary and is the same used by git format-patch. + p.cargo("package") + .env("SOURCE_DATE_EPOCH", "1000684800") + .run(); + assert!(p.root().join("target/package/foo-0.0.1.crate").is_file()); + + let f = File::open(&p.root().join("target/package/foo-0.0.1.crate")).unwrap(); + let decoder = GzDecoder::new(f); + let mut archive = Archive::new(decoder); + for ent in archive.entries().unwrap() { + let ent = ent.unwrap(); + let header = ent.header(); + assert_eq!(header.mode().unwrap(), 0o644); + assert_eq!(header.uid().unwrap(), 0); + assert_eq!(header.gid().unwrap(), 0); + assert_eq!(header.mtime().unwrap(), 1000684800); + assert_eq!(header.username().unwrap().unwrap(), "root"); + assert_eq!(header.groupname().unwrap().unwrap(), "root"); + assert_eq!(header.device_major().unwrap().unwrap(), 0); + assert_eq!(header.device_minor().unwrap().unwrap(), 0); + } +}