Skip to content

Commit c587c68

Browse files
committed
Basic functionality (and initial repro crate)
Adds extremely basic functionality for invoking `cargo build --locked` as `cargo repro build`. Though this may seem fairly pointless in and of itself, the goal of a followup commit would be to collect environmental information during this step (OS/release, rustc/cargo version, CWD, environment variables, git commit, C/C++ compiler versions if applicable) and use that during the verification process to detect and highlight mismatches. This commit attempts to split the CLI app (i.e. `cargo-repro`) from a library-level crate containing the core functionality (ala `cargo-audit` and the `rustsec` crate), in case there is interest in driving these sorts of builds from external tooling.
1 parent 9340d98 commit c587c68

File tree

12 files changed

+275
-46
lines changed

12 files changed

+275
-46
lines changed

.travis.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ matrix:
1515
include:
1616
- name: rustfmt
1717
script:
18-
- cargo fmt -- --check
18+
- cargo fmt --all -- --check
1919
- name: clippy
2020
script:
21-
- cargo clippy
21+
- cargo clippy --all
2222
- name: build
2323
script:
24-
- cargo build --release
24+
- cargo build --all --release
2525
- name: test
2626
script:
27-
- cargo test --release
27+
- cargo test --all --release
2828
- name: test (1.31.0)
2929
rust: 1.31.0
3030
script:
31-
- cargo test --release
31+
- cargo test --all --release

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ keywords = ["cargo", "deterministic", "reproducible", "security", "verifiable
1717
maintenance = { status = "experimental" }
1818

1919
[dependencies]
20+
repro = { version = "0", path = "repro" }
21+
22+
[workspace]
23+
members = [".", "repro"]

repro/Cargo.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "repro"
3+
description = """
4+
Support crate for cargo-repro, a tool for building and verifying
5+
Rust packages that are reproducible byte-for-byte using a
6+
Cargo-driven workflow.
7+
"""
8+
version = "0.0.0"
9+
authors = ["Rust Secure Code WG <[email protected]>"]
10+
edition = "2018"
11+
license = "Apache-2.0 OR MIT"
12+
readme = "README.md"
13+
repository = "https://github.com/rust-secure-code/cargo-repro"
14+
categories = ["command-line-utilities", "development-tools", "rust-patterns"]
15+
keywords = ["cargo", "deterministic", "reproducible", "security", "verifiable"]
16+
17+
[badges]
18+
maintenance = { status = "experimental" }
19+
20+
[dependencies]

repro/src/builder.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Rust project builder - wrapper for invoking Cargo
2+
3+
use std::{
4+
ffi::OsString,
5+
process::{Child, Command, ExitStatus},
6+
};
7+
8+
/// Name of the `cargo` executable
9+
const CARGO_EXE: &str = "cargo";
10+
11+
/// Rust project builder
12+
#[derive(Clone, Debug)]
13+
pub struct Builder {
14+
program: OsString,
15+
args: Vec<OsString>,
16+
}
17+
18+
impl Default for Builder {
19+
fn default() -> Self {
20+
Self::new(CARGO_EXE)
21+
}
22+
}
23+
24+
impl Builder {
25+
/// Create `Builder` that invokes the given command with the given arguments
26+
pub fn new<S>(program: S) -> Self
27+
where
28+
S: Into<OsString>,
29+
{
30+
Self {
31+
program: program.into(),
32+
args: vec![],
33+
}
34+
}
35+
36+
/// Append an argument to the set of arguments to run
37+
pub fn arg<S>(&mut self, arg: S) -> &mut Self
38+
where
39+
S: Into<OsString>,
40+
{
41+
self.args.push(arg.into());
42+
self
43+
}
44+
45+
/// Append multiple arguments to the set of arguments to run
46+
pub fn args<I, S>(&mut self, args: I) -> &mut Self
47+
where
48+
I: IntoIterator<Item = S>,
49+
S: Into<OsString>,
50+
{
51+
self.args.extend(args.into_iter().map(|a| a.into()));
52+
self
53+
}
54+
55+
/// Run the given subcommand
56+
pub fn run(&self) -> Process {
57+
let child = Command::new(&self.program)
58+
.args(&self.args)
59+
.spawn()
60+
.unwrap_or_else(|e| {
61+
panic!("error running command: {}", e);
62+
});
63+
64+
Process(child)
65+
}
66+
}
67+
68+
/// Wrapper for the builder subprocess
69+
pub struct Process(Child);
70+
71+
impl Process {
72+
/// Wait for the child to finish
73+
pub fn wait(mut self) -> ExitStatus {
74+
self.0
75+
.wait()
76+
.unwrap_or_else(|e| panic!("couldn't get child's exit status: {}", e))
77+
}
78+
}

repro/src/lib.rs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! `repro` crate: perform and verify reproducible builds of Rust code
2+
3+
#![forbid(unsafe_code)]
4+
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
5+
#![doc(
6+
html_logo_url = "https://avatars3.githubusercontent.com/u/44121472",
7+
html_root_url = "https://docs.rs/repro/0.0.0"
8+
)]
9+
10+
pub mod builder;

src/bin/cargo-repro/main.rs

-15
This file was deleted.

src/commands.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! `cargo repro` subcommands
2+
3+
pub mod build;
4+
pub mod verify;
5+
6+
use self::{build::BuildCommand, verify::VerifyCommand};
7+
8+
/// `cargo repro` subcommands
9+
pub enum Command {
10+
/// `cargo repro build` subcommand
11+
Build(BuildCommand),
12+
13+
/// `cargo repro verify` subcommand
14+
Verify(VerifyCommand),
15+
}
16+
17+
impl Command {
18+
/// Parse command to execute from CLI args
19+
pub fn from_args(mut args: impl Iterator<Item = String>) -> Option<Self> {
20+
// ARGV[0] is always the name of the executed binary
21+
args.next().unwrap();
22+
23+
// Cargo passes `repro` as the first argument when invoking `cargo repro`
24+
if args.next().as_ref().map(String::as_str) != Some("repro") {
25+
return None;
26+
}
27+
28+
let command = match args.next().as_ref().map(String::as_str) {
29+
Some("build") => Command::Build(BuildCommand::from_args(args)),
30+
Some("verify") => Command::Verify(VerifyCommand::from_args(args)),
31+
_ => return None,
32+
};
33+
34+
Some(command)
35+
}
36+
37+
/// Run the parsed command
38+
pub fn run(&self) {
39+
match self {
40+
Command::Build(build) => build.run(),
41+
Command::Verify(verify) => verify.run(),
42+
}
43+
}
44+
}

src/commands/build.rs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! `cargo repro build` subcommand
2+
3+
use repro::builder::Builder;
4+
5+
/// Cargo argument for a locked build. This is needed to ensure the build
6+
/// is reproducible.
7+
pub const LOCKED_ARG: &str = "--locked";
8+
9+
/// `cargo repro build` subcommand
10+
pub struct BuildCommand {
11+
/// Arguments passed to `cargo repro build` (to be passed to Cargo)
12+
pub args: Vec<String>,
13+
}
14+
15+
impl BuildCommand {
16+
/// Initialize this command from the given arguments, which should *NOT*
17+
/// include `["cargo", "repro", "build"]`
18+
pub fn from_args(args: impl Iterator<Item = String>) -> Self {
19+
Self {
20+
args: args.collect(),
21+
}
22+
}
23+
24+
/// Run this subcommand
25+
// TODO(tarcieri): factor more of this logic into the `repro` crate?
26+
pub fn run(&self) {
27+
let mut builder = Builder::default();
28+
builder.arg("build");
29+
30+
// Add the `--locked` argument unless it's been specified explicitly
31+
if !self.args.iter().any(|arg| arg.as_str() == LOCKED_ARG) {
32+
builder.arg(LOCKED_ARG);
33+
}
34+
35+
builder.args(&self.args);
36+
let exit_status = builder.run().wait();
37+
38+
if !exit_status.success() {
39+
panic!(
40+
"cargo exited with non-zero status: {}",
41+
exit_status
42+
.code()
43+
.map(|code| code.to_string())
44+
.unwrap_or_else(|| "unknown".to_owned())
45+
);
46+
}
47+
}
48+
}

src/commands/verify.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! `cargo repro verify` subcommand
2+
3+
/// `cargo repro verify` subcommand
4+
pub struct VerifyCommand {
5+
/// Arguments passed to `cargo repro verify` (to be passed to Cargo)
6+
pub args: Vec<String>,
7+
}
8+
9+
impl VerifyCommand {
10+
/// Initialize this command from the given arguments, which should *NOT*
11+
/// include `["cargo", "repro", "verify"]`
12+
pub fn from_args(args: impl Iterator<Item = String>) -> Self {
13+
Self {
14+
args: args.collect(),
15+
}
16+
}
17+
18+
/// Run this subcommand
19+
pub fn run(&self) {
20+
println!("cargo repro: build and verify byte-for-byte reproducible Rust packages");
21+
println!();
22+
println!("WORK IN PROGRESS: The 'verify' functionality of this tool is unimplemented.");
23+
println!("If you are interested in contributing, please see the GitHub issues:");
24+
println!();
25+
println!(" https://github.com/rust-secure-code/cargo-repro/issues");
26+
println!();
27+
}
28+
}

src/lib.rs

-26
This file was deleted.

src/main.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! cargo-repro: perform and verify reproducible builds of Rust code with Cargo
2+
3+
#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)]
4+
#![forbid(unsafe_code)]
5+
6+
pub mod commands;
7+
8+
use self::commands::Command;
9+
use std::{env, process};
10+
11+
fn main() {
12+
let command = Command::from_args(env::args()).unwrap_or_else(|| usage());
13+
command.run();
14+
}
15+
16+
fn usage() -> ! {
17+
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
18+
println!(
19+
"{}\n",
20+
env!("CARGO_PKG_DESCRIPTION")
21+
.split_whitespace()
22+
.collect::<Vec<_>>()
23+
.join(" ")
24+
);
25+
26+
println!("SUBCOMMANDS:");
27+
println!(" build\tPerform a reproducible build of a Cargo project");
28+
println!(" verify\t(UNIMPLEMENTED) Verify a reproducible build");
29+
30+
process::exit(1);
31+
}

0 commit comments

Comments
 (0)