diff --git a/.travis.yml b/.travis.yml index 708ae8d..a68b2b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,17 +15,17 @@ matrix: include: - name: rustfmt script: - - cargo fmt -- --check + - cargo fmt --all -- --check - name: clippy script: - - cargo clippy + - cargo clippy --all - name: build script: - - cargo build --release + - cargo build --all --release - name: test script: - - cargo test --release + - cargo test --all --release - name: test (1.31.0) rust: 1.31.0 script: - - cargo test --release + - cargo test --all --release diff --git a/Cargo.lock b/Cargo.lock index 34792ff..aaf0b16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,4 +3,11 @@ [[package]] name = "cargo-repro" version = "0.0.0" +dependencies = [ + "repro 0.0.0", +] + +[[package]] +name = "repro" +version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 27d0250..12c192a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ keywords = ["cargo", "deterministic", "reproducible", "security", "verifiable maintenance = { status = "experimental" } [dependencies] +repro = { version = "0", path = "repro" } + +[workspace] +members = [".", "repro"] diff --git a/repro/Cargo.toml b/repro/Cargo.toml new file mode 100644 index 0000000..5e06c9c --- /dev/null +++ b/repro/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "repro" +description = """ + Support crate for cargo-repro, a tool for building and verifying + Rust packages that are reproducible byte-for-byte using a + Cargo-driven workflow. + """ +version = "0.0.0" +authors = ["Rust Secure Code WG "] +edition = "2018" +license = "Apache-2.0 OR MIT" +readme = "README.md" +repository = "https://github.com/rust-secure-code/cargo-repro" +categories = ["command-line-utilities", "development-tools", "rust-patterns"] +keywords = ["cargo", "deterministic", "reproducible", "security", "verifiable"] + +[badges] +maintenance = { status = "experimental" } + +[dependencies] diff --git a/repro/src/builder.rs b/repro/src/builder.rs new file mode 100644 index 0000000..76cabcd --- /dev/null +++ b/repro/src/builder.rs @@ -0,0 +1,78 @@ +//! Rust project builder - wrapper for invoking Cargo + +use std::{ + ffi::OsString, + process::{Child, Command, ExitStatus}, +}; + +/// Name of the `cargo` executable +const CARGO_EXE: &str = "cargo"; + +/// Rust project builder +#[derive(Clone, Debug)] +pub struct Builder { + program: OsString, + args: Vec, +} + +impl Default for Builder { + fn default() -> Self { + Self::new(CARGO_EXE) + } +} + +impl Builder { + /// Create `Builder` that invokes the given command with the given arguments + pub fn new(program: S) -> Self + where + S: Into, + { + Self { + program: program.into(), + args: vec![], + } + } + + /// Append an argument to the set of arguments to run + pub fn arg(&mut self, arg: S) -> &mut Self + where + S: Into, + { + self.args.push(arg.into()); + self + } + + /// Append multiple arguments to the set of arguments to run + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: Into, + { + self.args.extend(args.into_iter().map(|a| a.into())); + self + } + + /// Run the given subcommand + pub fn run(&self) -> Process { + let child = Command::new(&self.program) + .args(&self.args) + .spawn() + .unwrap_or_else(|e| { + panic!("error running command: {}", e); + }); + + Process(child) + } +} + +/// Wrapper for the builder subprocess +pub struct Process(Child); + +impl Process { + /// Wait for the child to finish + pub fn wait(mut self) -> ExitStatus { + self.0 + .wait() + .unwrap_or_else(|e| panic!("couldn't get child's exit status: {}", e)) + } +} diff --git a/repro/src/lib.rs b/repro/src/lib.rs new file mode 100644 index 0000000..a5a0b4a --- /dev/null +++ b/repro/src/lib.rs @@ -0,0 +1,10 @@ +//! `repro` crate: perform and verify reproducible builds of Rust code + +#![forbid(unsafe_code)] +#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)] +#![doc( + html_logo_url = "https://avatars3.githubusercontent.com/u/44121472", + html_root_url = "https://docs.rs/repro/0.0.0" +)] + +pub mod builder; diff --git a/src/bin/cargo-repro/main.rs b/src/bin/cargo-repro/main.rs deleted file mode 100644 index 0b242ff..0000000 --- a/src/bin/cargo-repro/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! cargo-repro command-line utility - -#![deny( - warnings, - missing_docs, - trivial_casts, - trivial_numeric_casts, - unused_import_braces, - unused_qualifications -)] -#![forbid(unsafe_code)] - -fn main() { - cargo_repro::start(); -} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..bf96ae6 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,44 @@ +//! `cargo repro` subcommands + +pub mod build; +pub mod verify; + +use self::{build::BuildCommand, verify::VerifyCommand}; + +/// `cargo repro` subcommands +pub enum Command { + /// `cargo repro build` subcommand + Build(BuildCommand), + + /// `cargo repro verify` subcommand + Verify(VerifyCommand), +} + +impl Command { + /// Parse command to execute from CLI args + pub fn from_args(mut args: impl Iterator) -> Option { + // ARGV[0] is always the name of the executed binary + args.next().unwrap(); + + // Cargo passes `repro` as the first argument when invoking `cargo repro` + if args.next().as_ref().map(String::as_str) != Some("repro") { + return None; + } + + let command = match args.next().as_ref().map(String::as_str) { + Some("build") => Command::Build(BuildCommand::from_args(args)), + Some("verify") => Command::Verify(VerifyCommand::from_args(args)), + _ => return None, + }; + + Some(command) + } + + /// Run the parsed command + pub fn run(&self) { + match self { + Command::Build(build) => build.run(), + Command::Verify(verify) => verify.run(), + } + } +} diff --git a/src/commands/build.rs b/src/commands/build.rs new file mode 100644 index 0000000..c80f1ce --- /dev/null +++ b/src/commands/build.rs @@ -0,0 +1,48 @@ +//! `cargo repro build` subcommand + +use repro::builder::Builder; + +/// Cargo argument for a locked build. This is needed to ensure the build +/// is reproducible. +pub const LOCKED_ARG: &str = "--locked"; + +/// `cargo repro build` subcommand +pub struct BuildCommand { + /// Arguments passed to `cargo repro build` (to be passed to Cargo) + pub args: Vec, +} + +impl BuildCommand { + /// Initialize this command from the given arguments, which should *NOT* + /// include `["cargo", "repro", "build"]` + pub fn from_args(args: impl Iterator) -> Self { + Self { + args: args.collect(), + } + } + + /// Run this subcommand + // TODO(tarcieri): factor more of this logic into the `repro` crate? + pub fn run(&self) { + let mut builder = Builder::default(); + builder.arg("build"); + + // Add the `--locked` argument unless it's been specified explicitly + if !self.args.iter().any(|arg| arg.as_str() == LOCKED_ARG) { + builder.arg(LOCKED_ARG); + } + + builder.args(&self.args); + let exit_status = builder.run().wait(); + + if !exit_status.success() { + panic!( + "cargo exited with non-zero status: {}", + exit_status + .code() + .map(|code| code.to_string()) + .unwrap_or_else(|| "unknown".to_owned()) + ); + } + } +} diff --git a/src/commands/verify.rs b/src/commands/verify.rs new file mode 100644 index 0000000..2ecb0e6 --- /dev/null +++ b/src/commands/verify.rs @@ -0,0 +1,28 @@ +//! `cargo repro verify` subcommand + +/// `cargo repro verify` subcommand +pub struct VerifyCommand { + /// Arguments passed to `cargo repro verify` (to be passed to Cargo) + pub args: Vec, +} + +impl VerifyCommand { + /// Initialize this command from the given arguments, which should *NOT* + /// include `["cargo", "repro", "verify"]` + pub fn from_args(args: impl Iterator) -> Self { + Self { + args: args.collect(), + } + } + + /// Run this subcommand + pub fn run(&self) { + println!("cargo repro: build and verify byte-for-byte reproducible Rust packages"); + println!(); + println!("WORK IN PROGRESS: The 'verify' functionality of this tool is unimplemented."); + println!("If you are interested in contributing, please see the GitHub issues:"); + println!(); + println!(" https://github.com/rust-secure-code/cargo-repro/issues"); + println!(); + } +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 5559f28..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! cargo repro: build and verify byte-for-byte reproducible Rust packages - -#![forbid(unsafe_code)] -#![deny( - warnings, - missing_docs, - trivial_casts, - trivial_numeric_casts, - unused_import_braces, - unused_qualifications -)] -#![doc( - html_logo_url = "https://avatars3.githubusercontent.com/u/44121472", - html_root_url = "https://docs.rs/cargo-repro/0.0.0" -)] - -/// Start the utility -pub fn start() { - println!("cargo repro: build and verify byte-for-byte reproducible Rust packages"); - println!(); - println!("WORK IN PROGRESS: This utility does not yet provide any functionality"); - println!("If you are interested in contributing, please see the GitHub issues:"); - println!(); - println!(" https://github.com/rust-secure-code/cargo-repro/issues"); - println!(); -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..eb524c9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,31 @@ +//! cargo-repro: perform and verify reproducible builds of Rust code with Cargo + +#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)] +#![forbid(unsafe_code)] + +pub mod commands; + +use self::commands::Command; +use std::{env, process}; + +fn main() { + let command = Command::from_args(env::args()).unwrap_or_else(|| usage()); + command.run(); +} + +fn usage() -> ! { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + println!( + "{}\n", + env!("CARGO_PKG_DESCRIPTION") + .split_whitespace() + .collect::>() + .join(" ") + ); + + println!("SUBCOMMANDS:"); + println!(" build\tPerform a reproducible build of a Cargo project"); + println!(" verify\t(UNIMPLEMENTED) Verify a reproducible build"); + + process::exit(1); +}