use std::{
collections::HashMap,
ffi::OsStr,
io::Write,
path::PathBuf,
process::{Command, Output, Stdio},
};
use auditable_serde::{DependencyKind, VersionInfo};
use cargo_metadata::{
camino::{Utf8Path, Utf8PathBuf},
Artifact, TargetKind,
};
const EXE: &str = env!("CARGO_BIN_EXE_cargo-auditable");
const CARGO: &str = env!("CARGO");
fn run_cargo_auditable<P>(
cargo_toml_path: P,
args: &[&str],
env: &[(&str, &OsStr)],
sbom: bool,
) -> HashMap<String, Vec<Utf8PathBuf>>
where
P: AsRef<OsStr>,
{
let status = Command::new(CARGO)
.arg("clean")
.arg("--manifest-path")
.arg(&cargo_toml_path)
.status()
.unwrap();
assert!(status.success(), "Failed to invoke `cargo clean`!");
let mut command = Command::new(EXE);
command
.arg("auditable")
.arg("build")
.arg("--release")
.arg("--manifest-path")
.arg(&cargo_toml_path)
.arg("--message-format=json");
if sbom {
command.arg("-Z").arg("sbom");
command.env("CARGO_BUILD_SBOM", "true");
command.env("RUSTC_BOOTSTRAP", "1");
}
command.args(args);
if let Ok(target) = std::env::var("AUDITABLE_TEST_TARGET") {
if args.iter().all(|arg| !arg.starts_with("--target")) {
command.arg(format!("--target={target}"));
}
}
for (name, value) in env {
command.env(name, value);
}
let output = command
.stderr(Stdio::inherit())
.stdout(Stdio::piped())
.output()
.unwrap();
ensure_build_succeeded(&output);
let mut bins = HashMap::new();
std::str::from_utf8(&output.stdout)
.unwrap()
.lines()
.flat_map(|line: &str| {
let mut binaries = vec![];
if let Ok(artifact) = serde_json::from_str::<Artifact>(line) {
let member = artifact
.package_id
.to_string()
.split(' ')
.next()
.unwrap()
.to_string();
if let Some(executable) = artifact.executable {
binaries.push((member, executable));
} else if artifact
.target
.kind
.iter()
.any(|kind| *kind == TargetKind::CDyLib)
{
artifact
.filenames
.into_iter()
.filter(|f| {
f.extension() == Some("dylib")
|| f.extension() == Some("so")
|| f.extension() == Some("dll")
})
.for_each(|f| {
binaries.push((member.clone(), f));
});
}
}
binaries
})
.for_each(|(package, binary)| {
bins.entry(pkgid_to_bin_name(&package))
.or_insert(Vec::new())
.push(binary);
});
bins
}
fn pkgid_to_bin_name(pkgid: &str) -> String {
pkgid
.rsplit_once('/')
.unwrap()
.1
.split_once('#')
.unwrap()
.0
.to_owned()
}
fn ensure_build_succeeded(output: &Output) {
if !output.status.success() {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
handle.write_all(&output.stdout).unwrap();
handle.write_all(&output.stderr).unwrap();
handle.flush().unwrap();
panic!("Build with `cargo auditable` failed");
}
}
fn get_dependency_info(binary: &Utf8Path) -> VersionInfo {
auditable_info::audit_info_from_file(binary.as_std_path(), Default::default()).unwrap()
}
#[test]
fn test_cargo_auditable_workspaces() {
test_cargo_auditable_workspaces_inner(false);
test_cargo_auditable_workspaces_inner(true);
}
fn test_cargo_auditable_workspaces_inner(sbom: bool) {
let workspace_cargo_toml =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/workspace/Cargo.toml");
let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
assert!(!bins.contains_key("library_crate"));
let binary_and_cdylib_crate_bins = bins.get("binary_and_cdylib_crate").unwrap();
match std::env::var("AUDITABLE_TEST_TARGET") {
Ok(target) if target.contains("musl") => assert!(!binary_and_cdylib_crate_bins.is_empty()),
_ => assert_eq!(binary_and_cdylib_crate_bins.len(), 2),
}
for binary in binary_and_cdylib_crate_bins {
let dep_info = get_dependency_info(binary);
eprintln!("{binary} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 2);
assert!(dep_info.packages.iter().any(|p| p.name == "library_crate"));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "binary_and_cdylib_crate"));
}
let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0];
let dep_info = get_dependency_info(crate_with_features_bin);
eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 2);
assert!(dep_info.packages.iter().any(|p| p.name == "library_crate"));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "crate_with_features"));
let bins = run_cargo_auditable(
&workspace_cargo_toml,
&["--features", "binary_and_cdylib_crate"],
&[],
sbom,
);
let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0];
let dep_info = get_dependency_info(crate_with_features_bin);
eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 3);
assert!(dep_info.packages.iter().any(|p| p.name == "library_crate"));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "crate_with_features"));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "binary_and_cdylib_crate"));
let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-default-features"], &[], sbom);
let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0];
let dep_info = get_dependency_info(crate_with_features_bin);
eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "crate_with_features"));
}
#[test]
fn test_self_hosting() {
test_self_hosting_inner(false);
test_self_hosting_inner(true);
}
fn test_self_hosting_inner(sbom: bool) {
let workspace_cargo_toml =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../rust-audit-info/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Self-hosting binary map: {bins:?}");
let bin = &bins.get("rust-audit-info").unwrap()[0];
let dep_info = get_dependency_info(bin);
eprintln!("{bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() > 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "rust-audit-info"));
}
#[test]
fn test_lto() {
test_lto_inner(false);
test_lto_inner(true);
}
fn test_lto_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/lto_binary_crate/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("LTO binary map: {bins:?}");
let lto_binary_crate_bin = &bins.get("lto_binary_crate").unwrap()[0];
let dep_info = get_dependency_info(lto_binary_crate_bin);
eprintln!("{lto_binary_crate_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "lto_binary_crate"));
}
#[test]
fn test_lto_stripped() {
test_lto_stripped_inner(false);
test_lto_stripped_inner(true);
}
fn test_lto_stripped_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/lto_stripped_binary/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Stripped binary map: {bins:?}");
let lto_stripped_binary_bin = &bins.get("lto_stripped_binary").unwrap()[0];
let dep_info = get_dependency_info(lto_stripped_binary_bin);
eprintln!("{lto_stripped_binary_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "lto_stripped_binary"));
}
#[test]
fn test_bin_and_lib_in_one_crate() {
test_bin_and_lib_in_one_crate_inner(false);
test_bin_and_lib_in_one_crate_inner(true);
}
fn test_bin_and_lib_in_one_crate_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/lib_and_bin_crate/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &["--bin=some_binary"], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
let lib_and_bin_crate_bin = &bins.get("lib_and_bin_crate").unwrap()[0];
let dep_info = get_dependency_info(lib_and_bin_crate_bin);
eprintln!("{lib_and_bin_crate_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "lib_and_bin_crate"));
}
#[test]
fn test_build_script() {
test_build_script_inner(false);
test_build_script_inner(true);
}
fn test_build_script_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/crate_with_build_script/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
let crate_with_build_script_bin = &bins.get("crate_with_build_script").unwrap()[0];
let dep_info = get_dependency_info(crate_with_build_script_bin);
eprintln!("{crate_with_build_script_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "crate_with_build_script"));
}
#[test]
fn test_platform_specific_deps() {
test_platform_specific_deps_inner(false);
test_platform_specific_deps_inner(true);
}
fn test_platform_specific_deps_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/platform_specific_deps/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
let test_target = std::env::var("AUDITABLE_TEST_TARGET");
if test_target.is_err() || !test_target.unwrap().starts_with("m68k") {
let bin = &bins.get("with_platform_dep").unwrap()[0];
let dep_info = get_dependency_info(bin);
eprintln!("{bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(!dep_info
.packages
.iter()
.any(|p| p.name == "should_not_be_included"));
}
}
#[test]
fn test_build_then_runtime_dep() {
test_build_then_runtime_dep_inner(false);
test_build_then_runtime_dep_inner(true);
}
fn test_build_then_runtime_dep_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/build_then_runtime_dep/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0];
let dep_info = get_dependency_info(toplevel_crate_bin);
eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 3);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "build_dep" && p.kind == DependencyKind::Build));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "runtime_dep_of_build_dep" && p.kind == DependencyKind::Build));
}
#[test]
fn test_runtime_then_build_dep() {
test_runtime_then_build_dep_inner(false);
test_runtime_then_build_dep_inner(true);
}
fn test_runtime_then_build_dep_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/runtime_then_build_dep/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Test fixture binary map: {bins:?}");
let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0];
let dep_info = get_dependency_info(toplevel_crate_bin);
eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 3);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build));
}
#[test]
fn test_custom_rustc_path() {
test_custom_rustc_path_inner(false);
test_custom_rustc_path_inner(true);
}
fn test_custom_rustc_path_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/custom_rustc_path/Cargo.toml");
let rustc_path = which::which("rustc").unwrap();
let bins = run_cargo_auditable(
workspace_cargo_toml,
&[],
&[("RUSTC", rustc_path.as_ref())],
sbom,
);
eprintln!("Test fixture binary map: {bins:?}");
let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0];
let dep_info = get_dependency_info(toplevel_crate_bin);
eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 3);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build));
}
#[test]
fn test_workspace_member_version_info() {
let mut command = Command::new(EXE);
command.env("CARGO_PRIMARY_PACKAGE", "true");
command.args(["rustc", "-vV"]);
let status = command.status().unwrap();
assert!(status.success());
}
#[test]
fn test_wasm() {
test_wasm_inner(false);
test_wasm_inner(true);
}
fn test_wasm_inner(sbom: bool) {
let workspace_cargo_toml =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wasm_crate/Cargo.toml");
run_cargo_auditable(
workspace_cargo_toml,
&["--target=wasm32-unknown-unknown"],
&[],
sbom,
);
let dep_info = get_dependency_info(
"tests/fixtures/wasm_crate/target/wasm32-unknown-unknown/release/wasm_crate.wasm".into(),
);
eprintln!("wasm_crate.wasm dependency info: {dep_info:?}");
assert_eq!(dep_info.packages.len(), 16);
}
#[test]
fn test_path_not_equal_name() {
test_path_not_equal_name_inner(false);
test_path_not_equal_name_inner(true);
}
fn test_path_not_equal_name_inner(sbom: bool) {
let cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/path_not_equal_name/foo/Cargo.toml");
let bins = run_cargo_auditable(cargo_toml, &[], &[], sbom);
let foo_bin = &bins.get("foo").unwrap()[0];
let dep_info = get_dependency_info(foo_bin);
eprintln!("{foo_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 3);
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "bar" && p.kind == DependencyKind::Runtime));
assert!(dep_info
.packages
.iter()
.any(|p| p.name == "baz" && p.kind == DependencyKind::Runtime));
}
#[test]
fn test_proc_macro() {
test_proc_macro_inner(false);
test_proc_macro_inner(true);
}
fn test_proc_macro_inner(sbom: bool) {
let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/proc-macro-dependency/Cargo.toml");
let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom);
eprintln!("Proc macro binary map: {bins:?}");
let binary = &bins.get("proc-macro-dependency").unwrap()[0];
let dep_info = get_dependency_info(binary);
eprintln!("{binary} dependency info: {dep_info:?}");
let serde_derive_info = dep_info
.packages
.iter()
.find(|p| p.name == "serde_derive")
.expect("Could not find 'serde_derive' in the embedded dependency list!");
assert_eq!(serde_derive_info.kind, DependencyKind::Build);
let syn_info = dep_info
.packages
.iter()
.find(|p| p.name == "syn")
.expect("Could not find 'syn' in the embedded dependency list!");
assert_eq!(syn_info.kind, DependencyKind::Build);
}
#[test]
fn test_bare_linker() {
test_bare_linker_inner(false);
test_bare_linker_inner(true);
}
fn test_bare_linker_inner(sbom: bool) {
let cargo_toml =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bare_linker/Cargo.toml");
let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/bare_linker/.cargo/config.toml");
let bins = run_cargo_auditable(
cargo_toml,
&["--config", config_path.to_str().unwrap()],
&[],
sbom,
);
eprintln!("Test fixture binary map: {bins:?}");
let bare_linker_bin = &bins.get("bare_linker").unwrap()[0];
let dep_info = get_dependency_info(bare_linker_bin);
eprintln!("{bare_linker_bin} dependency info: {dep_info:?}");
assert!(dep_info.packages.len() == 1);
assert!(dep_info.packages.iter().any(|p| p.name == "bare_linker"));
}