blob: dcded3b1a3a8d73b1d76058fd6ab4685cf09bb23 [file] [log] [blame]
//! This tool helps to capture the `Cargo.toml` files of a workspace.
//!
//! Run it by passing a list of workspaces to capture.
//! Use the `-f` flag to allow it to overwrite existing captures.
//! The workspace will be saved in a `.tgz` file in the `../workspaces` directory.
#![allow(clippy::disallowed_methods)]
#![allow(clippy::print_stderr)]
use flate2::{Compression, GzBuilder};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let force = std::env::args().any(|arg| arg == "-f");
let dest = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("workspaces");
if !dest.exists() {
panic!("expected {} to exist", dest.display());
}
for arg in std::env::args().skip(1).filter(|arg| !arg.starts_with("-")) {
let source_root = fs::canonicalize(arg).unwrap();
capture(&source_root, &dest, force);
}
}
fn capture(source_root: &Path, dest: &Path, force: bool) {
let name = Path::new(source_root.file_name().unwrap());
let mut dest_gz = PathBuf::from(dest);
dest_gz.push(name);
dest_gz.set_extension("tgz");
if dest_gz.exists() {
if !force {
panic!(
"dest {:?} already exists, use -f to force overwriting",
dest_gz
);
}
fs::remove_file(&dest_gz).unwrap();
}
let vcs_info = capture_vcs_info(source_root, force);
let dst = fs::File::create(&dest_gz).unwrap();
let encoder = GzBuilder::new()
.filename(format!("{}.tar", name.to_str().unwrap()))
.write(dst, Compression::best());
let mut ar = tar::Builder::new(encoder);
ar.mode(tar::HeaderMode::Deterministic);
if let Some(info) = &vcs_info {
add_ar_file(&mut ar, &name.join(".cargo_vcs_info.json"), info);
}
// Gather all local packages.
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(source_root.join("Cargo.toml"))
.features(cargo_metadata::CargoOpt::AllFeatures)
.exec()
.expect("cargo_metadata failed");
let mut found_root = false;
for package in &metadata.packages {
if package.source.is_some() {
continue;
}
let manifest_path = package.manifest_path.as_std_path();
copy_manifest(&manifest_path, &mut ar, name, &source_root);
found_root |= manifest_path == source_root.join("Cargo.toml");
}
if !found_root {
// A virtual workspace.
let contents = fs::read_to_string(source_root.join("Cargo.toml")).unwrap();
assert!(!contents.contains("[package]"));
add_ar_file(&mut ar, &name.join("Cargo.toml"), &contents);
}
let lock = fs::read_to_string(source_root.join("Cargo.lock")).unwrap();
add_ar_file(&mut ar, &name.join("Cargo.lock"), &lock);
let encoder = ar.into_inner().unwrap();
encoder.finish().unwrap();
eprintln!("created {}", dest_gz.display());
}
fn copy_manifest<W: std::io::Write>(
manifest_path: &Path,
ar: &mut tar::Builder<W>,
name: &Path,
source_root: &Path,
) {
let relative_path = manifest_path
.parent()
.unwrap()
.strip_prefix(source_root)
.expect("workspace member should be under workspace root");
let relative_path = name.join(relative_path);
let contents = fs::read_to_string(&manifest_path).unwrap();
let mut manifest: toml::Value = toml::from_str(&contents).unwrap();
let remove = |obj: &mut toml::Value, name| {
let table = obj.as_table_mut().unwrap();
if table.contains_key(name) {
table.remove(name);
}
};
remove(&mut manifest, "lib");
remove(&mut manifest, "bin");
remove(&mut manifest, "example");
remove(&mut manifest, "test");
remove(&mut manifest, "bench");
remove(&mut manifest, "profile");
if let Some(package) = manifest.get_mut("package") {
remove(package, "default-run");
}
let contents = toml::to_string(&manifest).unwrap();
add_ar_file(ar, &relative_path.join("Cargo.toml"), &contents);
add_ar_file(ar, &relative_path.join("src").join("lib.rs"), "");
}
fn add_ar_file<W: std::io::Write>(ar: &mut tar::Builder<W>, path: &Path, contents: &str) {
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::file());
header.set_mode(0o644);
header.set_size(contents.len() as u64);
header.set_mtime(123456789);
header.set_cksum();
ar.append_data(&mut header, path, contents.as_bytes())
.unwrap();
}
fn capture_vcs_info(ws_root: &Path, force: bool) -> Option<String> {
let maybe_git = |command: &str| {
Command::new("git")
.current_dir(ws_root)
.args(command.split_whitespace().collect::<Vec<_>>())
.output()
.expect("git should be installed")
};
assert!(ws_root.join("Cargo.toml").exists());
let relative = maybe_git("ls-files --full-name Cargo.toml");
if !relative.status.success() {
if !force {
panic!("git repository not detected, use -f to force");
}
return None;
}
let p = Path::new(std::str::from_utf8(&relative.stdout).unwrap().trim());
let relative = p.parent().unwrap();
if !force {
let has_changes = !maybe_git("diff-index --quiet HEAD .").status.success();
if has_changes {
panic!("git repo appears to have changes, use -f to force, or clean the repo");
}
}
let commit = maybe_git("rev-parse HEAD");
assert!(commit.status.success());
let commit = std::str::from_utf8(&commit.stdout).unwrap().trim();
let remote = maybe_git("remote get-url origin");
assert!(remote.status.success());
let remote = std::str::from_utf8(&remote.stdout).unwrap().trim();
let info = format!(
"{{\n \"git\": {{\n \"sha1\": \"{}\",\n \"remote\": \"{}\"\n }},\
\n \"path_in_vcs\": \"{}\"\n}}\n",
commit,
remote,
relative.display()
);
eprintln!("recording vcs info:\n{}", info);
Some(info)
}