Skip to content

Commit 0ec7281

Browse files
committed
Auto merge of #5886 - dekellum:record-package-git-head-3, r=alexcrichton
Generate .cargo_vcs_info.json and include in `cargo package` (take 2) Implements #5629 and supersedes #5786, with the following improvements: * With an upstream git2-rs fix (tracked #5823, validated and min version update in: #5858), no longer requires windows/unix split tests. * Per review in #5786, drops file system output and locks for .cargo_vcs_info.json. * Now uses serde `json!` macro for json production with appropriate escaping. * Now includes a test of the output json format. Possible followup: * Per discussion in #5786, we could improve reliability of both the VCS dirty check and the git head sha1 recording by preferring (and/or warning otherwise) the local repository bytes of each source file, at the same head commit. This makes the process more appropriately like an atomic snapshot, with no sentry file or other locking required. However given my lack of a window's license and dev setup, as exhibited by troubles of #5823, this feel intuitively like too much to attempt to change in one iteration here. I accept the "best effort" concept of this feature as suggested in #5786, and think it acceptable progress if merged in this form. @alexcrichton r? @joshtriplett cc
2 parents 677ef84 + 06dcc20 commit 0ec7281

File tree

3 files changed

+184
-32
lines changed

3 files changed

+184
-32
lines changed

src/cargo/ops/cargo_package.rs

+108-23
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::fs::{self, File};
2-
use std::io::SeekFrom;
32
use std::io::prelude::*;
4-
use std::path::{self, Path};
3+
use std::io::SeekFrom;
4+
use std::path::{self, Path, PathBuf};
55
use std::sync::Arc;
66

77
use flate2::read::GzDecoder;
88
use flate2::{Compression, GzBuilder};
99
use git2;
10+
use serde_json;
1011
use tar::{Archive, Builder, EntryType, Header};
1112

1213
use core::{Package, Source, SourceId, Workspace};
@@ -28,6 +29,8 @@ pub struct PackageOpts<'cfg> {
2829
pub registry: Option<String>,
2930
}
3031

32+
static VCS_INFO_FILE: &'static str = ".cargo_vcs_info.json";
33+
3134
pub fn package(ws: &Workspace, opts: &PackageOpts) -> CargoResult<Option<FileLock>> {
3235
ops::resolve_ws(ws)?;
3336
let pkg = ws.current()?;
@@ -42,6 +45,19 @@ pub fn package(ws: &Workspace, opts: &PackageOpts) -> CargoResult<Option<FileLoc
4245

4346
verify_dependencies(pkg)?;
4447

48+
// `list_files` outputs warnings as a side effect, so only do it once.
49+
let src_files = src.list_files(pkg)?;
50+
51+
// Make sure a VCS info file is not included in source, regardless of if
52+
// we produced the file above, and in particular if we did not.
53+
check_vcs_file_collision(pkg, &src_files)?;
54+
55+
// Check (git) repository state, getting the current commit hash if not
56+
// dirty. This will `bail!` if dirty, unless allow_dirty. Produce json
57+
// info for any sha1 (HEAD revision) returned.
58+
let vcs_info = check_repo_state(pkg, &src_files, &config, opts.allow_dirty)?
59+
.map(|h| json!({"git":{"sha1": h}}));
60+
4561
if opts.list {
4662
let root = pkg.root();
4763
let mut list: Vec<_> = src.list_files(pkg)?
@@ -51,17 +67,16 @@ pub fn package(ws: &Workspace, opts: &PackageOpts) -> CargoResult<Option<FileLoc
5167
if include_lockfile(pkg) {
5268
list.push("Cargo.lock".into());
5369
}
70+
if vcs_info.is_some() {
71+
list.push(Path::new(VCS_INFO_FILE).to_path_buf());
72+
}
5473
list.sort_unstable();
5574
for file in list.iter() {
5675
println!("{}", file.display());
5776
}
5877
return Ok(None);
5978
}
6079

61-
if !opts.allow_dirty {
62-
check_not_dirty(pkg, &src, &config)?;
63-
}
64-
6580
let filename = format!("{}-{}.crate", pkg.name(), pkg.version());
6681
let dir = ws.target_dir().join("package");
6782
let mut dst = {
@@ -77,7 +92,7 @@ pub fn package(ws: &Workspace, opts: &PackageOpts) -> CargoResult<Option<FileLoc
7792
.shell()
7893
.status("Packaging", pkg.package_id().to_string())?;
7994
dst.file().set_len(0)?;
80-
tar(ws, &src, dst.file(), &filename)
95+
tar(ws, &src_files, vcs_info.as_ref(), dst.file(), &filename)
8196
.chain_err(|| format_err!("failed to prepare local package for uploading"))?;
8297
if opts.verify {
8398
dst.seek(SeekFrom::Start(0))?;
@@ -152,7 +167,16 @@ fn verify_dependencies(pkg: &Package) -> CargoResult<()> {
152167
Ok(())
153168
}
154169

155-
fn check_not_dirty(p: &Package, src: &PathSource, config: &Config) -> CargoResult<()> {
170+
// Check if the package source is in a *git* DVCS repository. If *git*, and
171+
// the source is *dirty* (e.g. has uncommited changes) and not `allow_dirty`
172+
// then `bail!` with an informative message. Otherwise return the sha1 hash of
173+
// the current *HEAD* commit, or `None` if *dirty*.
174+
fn check_repo_state(
175+
p: &Package,
176+
src_files: &[PathBuf],
177+
config: &Config,
178+
allow_dirty: bool
179+
) -> CargoResult<Option<String>> {
156180
if let Ok(repo) = git2::Repository::discover(p.root()) {
157181
if let Some(workdir) = repo.workdir() {
158182
debug!("found a git repo at {:?}", workdir);
@@ -164,7 +188,7 @@ fn check_not_dirty(p: &Package, src: &PathSource, config: &Config) -> CargoResul
164188
"found (git) Cargo.toml at {:?} in workdir {:?}",
165189
path, workdir
166190
);
167-
return git(p, src, &repo);
191+
return git(p, src_files, &repo, allow_dirty);
168192
}
169193
}
170194
config.shell().verbose(|shell| {
@@ -182,11 +206,16 @@ fn check_not_dirty(p: &Package, src: &PathSource, config: &Config) -> CargoResul
182206

183207
// No VCS with a checked in Cargo.toml found. so we don't know if the
184208
// directory is dirty or not, so we have to assume that it's clean.
185-
return Ok(());
186-
187-
fn git(p: &Package, src: &PathSource, repo: &git2::Repository) -> CargoResult<()> {
209+
return Ok(None);
210+
211+
fn git(
212+
p: &Package,
213+
src_files: &[PathBuf],
214+
repo: &git2::Repository,
215+
allow_dirty: bool
216+
) -> CargoResult<Option<String>> {
188217
let workdir = repo.workdir().unwrap();
189-
let dirty = src.list_files(p)?
218+
let dirty = src_files
190219
.iter()
191220
.filter(|file| {
192221
let relative = file.strip_prefix(workdir).unwrap();
@@ -204,20 +233,46 @@ fn check_not_dirty(p: &Package, src: &PathSource, config: &Config) -> CargoResul
204233
})
205234
.collect::<Vec<_>>();
206235
if dirty.is_empty() {
207-
Ok(())
236+
let rev_obj = repo.revparse_single("HEAD")?;
237+
Ok(Some(rev_obj.id().to_string()))
208238
} else {
209-
bail!(
210-
"{} files in the working directory contain changes that were \
211-
not yet committed into git:\n\n{}\n\n\
212-
to proceed despite this, pass the `--allow-dirty` flag",
213-
dirty.len(),
214-
dirty.join("\n")
215-
)
239+
if !allow_dirty {
240+
bail!(
241+
"{} files in the working directory contain changes that were \
242+
not yet committed into git:\n\n{}\n\n\
243+
to proceed despite this, pass the `--allow-dirty` flag",
244+
dirty.len(),
245+
dirty.join("\n")
246+
)
247+
}
248+
Ok(None)
216249
}
217250
}
218251
}
219252

220-
fn tar(ws: &Workspace, src: &PathSource, dst: &File, filename: &str) -> CargoResult<()> {
253+
// Check for and `bail!` if a source file matches ROOT/VCS_INFO_FILE, since
254+
// this is now a cargo reserved file name, and we don't want to allow
255+
// forgery.
256+
fn check_vcs_file_collision(pkg: &Package, src_files: &[PathBuf]) -> CargoResult<()> {
257+
let root = pkg.root();
258+
let vcs_info_path = Path::new(VCS_INFO_FILE);
259+
let collision = src_files.iter().find(|&p| {
260+
util::without_prefix(&p, root).unwrap() == vcs_info_path
261+
});
262+
if collision.is_some() {
263+
bail!("Invalid inclusion of reserved file name \
264+
{} in package source", VCS_INFO_FILE);
265+
}
266+
Ok(())
267+
}
268+
269+
fn tar(
270+
ws: &Workspace,
271+
src_files: &[PathBuf],
272+
vcs_info: Option<&serde_json::Value>,
273+
dst: &File,
274+
filename: &str
275+
) -> CargoResult<()> {
221276
// Prepare the encoder and its header
222277
let filename = Path::new(filename);
223278
let encoder = GzBuilder::new()
@@ -229,7 +284,7 @@ fn tar(ws: &Workspace, src: &PathSource, dst: &File, filename: &str) -> CargoRes
229284
let pkg = ws.current()?;
230285
let config = ws.config();
231286
let root = pkg.root();
232-
for file in src.list_files(pkg)?.iter() {
287+
for file in src_files.iter() {
233288
let relative = util::without_prefix(file, root).unwrap();
234289
check_filename(relative)?;
235290
let relative = relative.to_str().ok_or_else(|| {
@@ -297,6 +352,36 @@ fn tar(ws: &Workspace, src: &PathSource, dst: &File, filename: &str) -> CargoRes
297352
}
298353
}
299354

355+
if let Some(ref json) = vcs_info {
356+
let filename: PathBuf = Path::new(VCS_INFO_FILE).into();
357+
debug_assert!(check_filename(&filename).is_ok());
358+
let fnd = filename.display();
359+
config
360+
.shell()
361+
.verbose(|shell| shell.status("Archiving", &fnd))?;
362+
let path = format!(
363+
"{}-{}{}{}",
364+
pkg.name(),
365+
pkg.version(),
366+
path::MAIN_SEPARATOR,
367+
fnd
368+
);
369+
let mut header = Header::new_ustar();
370+
header.set_path(&path).chain_err(|| {
371+
format!("failed to add to archive: `{}`", fnd)
372+
})?;
373+
let json = format!("{}\n", serde_json::to_string_pretty(json)?);
374+
let mut header = Header::new_ustar();
375+
header.set_path(&path)?;
376+
header.set_entry_type(EntryType::file());
377+
header.set_mode(0o644);
378+
header.set_size(json.len() as u64);
379+
header.set_cksum();
380+
ar.append(&header, json.as_bytes()).chain_err(|| {
381+
internal(format!("could not archive source file `{}`", fnd))
382+
})?;
383+
}
384+
300385
if include_lockfile(pkg) {
301386
let toml = paths::read(&ws.root().join("Cargo.lock"))?;
302387
let path = format!(

tests/testsuite/package.rs

+72-9
Original file line numberDiff line numberDiff line change
@@ -155,38 +155,64 @@ See http://doc.crates.io/manifest.html#package-metadata for more info.
155155
#[test]
156156
fn package_verbose() {
157157
let root = paths::root().join("all");
158-
let p = git::repo(&root)
158+
let repo = git::repo(&root)
159159
.file("Cargo.toml", &basic_manifest("foo", "0.0.1"))
160160
.file("src/main.rs", "fn main() {}")
161161
.file("a/Cargo.toml", &basic_manifest("a", "0.0.1"))
162162
.file("a/src/lib.rs", "")
163163
.build();
164-
assert_that(cargo_process("build").cwd(p.root()), execs());
164+
assert_that(cargo_process("build").cwd(repo.root()), execs());
165165

166166
println!("package main repo");
167167
assert_that(
168-
cargo_process("package -v --no-verify").cwd(p.root()),
168+
cargo_process("package -v --no-verify").cwd(repo.root()),
169169
execs().with_stderr(
170170
"\
171171
[WARNING] manifest has no description[..]
172172
See http://doc.crates.io/manifest.html#package-metadata for more info.
173173
[PACKAGING] foo v0.0.1 ([..])
174174
[ARCHIVING] [..]
175175
[ARCHIVING] [..]
176+
[ARCHIVING] .cargo_vcs_info.json
176177
",
177178
),
178179
);
179180

181+
let f = File::open(&repo.root().join("target/package/foo-0.0.1.crate")).unwrap();
182+
let mut rdr = GzDecoder::new(f);
183+
let mut contents = Vec::new();
184+
rdr.read_to_end(&mut contents).unwrap();
185+
let mut ar = Archive::new(&contents[..]);
186+
let mut entry = ar.entries().unwrap()
187+
.map(|f| f.unwrap())
188+
.find(|e| e.path().unwrap().ends_with(".cargo_vcs_info.json"))
189+
.unwrap();
190+
let mut contents = String::new();
191+
entry.read_to_string(&mut contents).unwrap();
192+
assert_eq!(
193+
&contents[..],
194+
&*format!(
195+
r#"{{
196+
"git": {{
197+
"sha1": "{}"
198+
}}
199+
}}
200+
"#,
201+
repo.revparse_head()
202+
)
203+
);
204+
180205
println!("package sub-repo");
181206
assert_that(
182-
cargo_process("package -v --no-verify").cwd(p.root().join("a")),
207+
cargo_process("package -v --no-verify").cwd(repo.root().join("a")),
183208
execs().with_stderr(
184209
"\
185210
[WARNING] manifest has no description[..]
186211
See http://doc.crates.io/manifest.html#package-metadata for more info.
187212
[PACKAGING] a v0.0.1 ([..])
188-
[ARCHIVING] [..]
189-
[ARCHIVING] [..]
213+
[ARCHIVING] Cargo.toml
214+
[ARCHIVING] src/lib.rs
215+
[ARCHIVING] .cargo_vcs_info.json
190216
",
191217
),
192218
);
@@ -214,6 +240,42 @@ See http://doc.crates.io/manifest.html#package-metadata for more info.
214240
);
215241
}
216242

243+
#[test]
244+
fn vcs_file_collision() {
245+
let p = project().build();
246+
let _ = git::repo(&paths::root().join("foo"))
247+
.file(
248+
"Cargo.toml",
249+
r#"
250+
[project]
251+
name = "foo"
252+
description = "foo"
253+
version = "0.0.1"
254+
authors = []
255+
license = "MIT"
256+
documentation = "foo"
257+
homepage = "foo"
258+
repository = "foo"
259+
exclude = ["*.no-existe"]
260+
"#)
261+
.file(
262+
"src/main.rs",
263+
r#"
264+
fn main() {}
265+
"#)
266+
.file(".cargo_vcs_info.json", "foo")
267+
.build();
268+
assert_that(
269+
p.cargo("package").arg("--no-verify"),
270+
execs().with_status(101).with_stderr(&format!(
271+
"\
272+
[ERROR] Invalid inclusion of reserved file name .cargo_vcs_info.json \
273+
in package source
274+
",
275+
)),
276+
);
277+
}
278+
217279
#[test]
218280
fn path_dependency_no_version() {
219281
let p = project()
@@ -320,8 +382,6 @@ fn exclude() {
320382
"\
321383
[WARNING] manifest has no description[..]
322384
See http://doc.crates.io/manifest.html#package-metadata for more info.
323-
[WARNING] No (git) Cargo.toml found at `[..]` in workdir `[..]`
324-
[PACKAGING] foo v0.0.1 ([..])
325385
[WARNING] [..] file `dir_root_1/some_dir/file` WILL be excluded [..]
326386
See [..]
327387
[WARNING] [..] file `dir_root_2/some_dir/file` WILL be excluded [..]
@@ -334,6 +394,8 @@ See [..]
334394
See [..]
335395
[WARNING] [..] file `some_dir/file_deep_1` WILL be excluded [..]
336396
See [..]
397+
[WARNING] No (git) Cargo.toml found at `[..]` in workdir `[..]`
398+
[PACKAGING] foo v0.0.1 ([..])
337399
[ARCHIVING] [..]
338400
[ARCHIVING] [..]
339401
[ARCHIVING] [..]
@@ -483,7 +545,7 @@ fn no_duplicates_from_modified_tracked_files() {
483545
.unwrap();
484546
assert_that(cargo_process("build").cwd(p.root()), execs());
485547
assert_that(
486-
cargo_process("package --list").cwd(p.root()),
548+
cargo_process("package --list --allow-dirty").cwd(p.root()),
487549
execs().with_stdout(
488550
"\
489551
Cargo.toml
@@ -1188,6 +1250,7 @@ fn package_lockfile_git_repo() {
11881250
p.cargo("package -l").masquerade_as_nightly_cargo(),
11891251
execs().with_stdout(
11901252
"\
1253+
.cargo_vcs_info.json
11911254
Cargo.lock
11921255
Cargo.toml
11931256
src/main.rs

tests/testsuite/support/git.rs

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ impl Repository {
7474
pub fn url(&self) -> Url {
7575
path2url(self.0.workdir().unwrap().to_path_buf())
7676
}
77+
78+
pub fn revparse_head(&self) -> String {
79+
self.0.revparse_single("HEAD").expect("revparse HEAD").id().to_string()
80+
}
7781
}
7882

7983
pub fn new<F>(name: &str, callback: F) -> Result<Project, ProcessError>

0 commit comments

Comments
 (0)