auditable_build/
lib.rs

1#![forbid(unsafe_code)]
2
3//! This is a companion crate to [`auditable`](https://docs.rs/auditable/) to be used as a build dependency.
4//!
5//! This crate is responsible for collecting the dependecy data. It exists as a separate crate purely for technical reasons.
6//! Please refer to [`auditable`](https://docs.rs/auditable/) crate for documentation.
7
8use std::{env, path::{Path, PathBuf}, fs::File, io::Write};
9use std::{convert::TryFrom, collections::HashSet};
10use auditable_serde::VersionInfo;
11use miniz_oxide::deflate::compress_to_vec_zlib;
12use cargo_metadata::{Metadata, MetadataCommand};
13
14/// Run this in your build.rs to collect dependency info and make it avaible to `inject_dependency_list!` macro
15pub fn collect_dependency_list() {
16    let version_info = VersionInfo::try_from(&get_metadata()).unwrap();
17    let json = serde_json::to_string(&version_info).unwrap();
18    let compressed_json = compress_to_vec_zlib(json.as_bytes(), choose_compression_level());
19    let output_file_path = output_file_path();
20    write_dependency_info(&compressed_json, &output_file_path);
21    export_dependency_file_path(&output_file_path);
22}
23
24fn output_file_path() -> std::path::PathBuf {
25    let out_dir = env::var("OUT_DIR").unwrap();
26    let dest_dir = Path::new(&out_dir);
27    dest_dir.join("dependency-list.json.zlib")
28}
29
30fn write_dependency_info(data: &[u8], path: &Path) {
31    let f = File::create(path).unwrap();
32    let mut writer = std::io::BufWriter::new(f);
33    writer.write_all(data).unwrap();
34}
35
36fn export_dependency_file_path(path: &Path) {
37    // Required because there's no cross-platform way to use `include_bytes!`
38    // on a file from the build dir other than this. I've tried lots of them.
39    // See https://github.com/rust-lang/rust/issues/75075
40    println!("cargo:rustc-env=RUST_AUDIT_DEPENDENCY_FILE_LOCATION={}", path.display());
41}
42
43fn choose_compression_level() -> u8 {
44    let build_profile = env::var("PROFILE").unwrap();
45    match build_profile.as_str() {
46        "debug" => 1,
47        "release" => 7, // not 9 because this also affects speed of incremental builds
48        _ => panic!("Unknown build profile: {}", &build_profile)
49    }
50}
51
52fn get_metadata() -> Metadata {
53    let mut metadata_command = metadata_command();
54    let mut features = enabled_features();
55    // feature "default" is explicitly passed to build scripts but there is no "all" feature
56    if let Some(index) = features.iter().position(|x| x.as_str() == "default") {
57        features.remove(index);
58    } else {
59        metadata_command.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
60    }
61    metadata_command.features(cargo_metadata::CargoOpt::SomeFeatures(features));
62    metadata_command.exec().unwrap()
63}
64
65fn metadata_command() -> MetadataCommand {
66    // MetadataCommand::new() automatically reads the $CARGO env var
67    // that Cargo sets for build scripts, so we don't have to pass it explicitly
68    let mut cmd = MetadataCommand::new();
69    let cargo_toml_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml");
70    cmd.manifest_path(cargo_toml_path);
71    cmd.other_options(vec!["--filter-platform=".to_owned() + &env::var("TARGET").unwrap()]);
72    cmd
73}
74
75fn enabled_features() -> Vec<String> {
76    let mut result = Vec::new();
77    // Cargo irreparably mangles the feature list when passing it to the build script
78    // (in particular, case and distinction between `-` and `_` are lost, see
79    // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
80    // so we have to reconsruct it by calling cargo-metadata and filtering features
81    // that we know exist against the mangled list of *enabled* features from env variables
82    let enabled_uppercase_features = enabled_uppercase_features();
83    let dry_run_metadata = metadata_command().exec().unwrap();
84    // we can safely unwrap here because resolve is only missing if called with --no-deps,
85    // and root package is only missing in a virtual workspace, from which you can't run a build script
86    let root_package_id = dry_run_metadata.resolve.unwrap().root.unwrap();
87    let root_package = dry_run_metadata.packages.iter().filter(|p| p.id == root_package_id).next().unwrap();
88    for (feature, _implied_features) in root_package.features.iter() {
89        let mangled_feature = feature.to_ascii_uppercase().replace("-", "_");
90        if enabled_uppercase_features.contains(&mangled_feature) {
91            result.push(feature.clone());
92        }
93    }
94    result
95}
96
97fn enabled_uppercase_features() -> HashSet<String> {
98    let mut features = HashSet::new();
99    for (var_name, _value) in env::vars().filter(|(name, _value)| {
100        name.len() > "CARGO_FEATURE_".len() && name.starts_with("CARGO_FEATURE_")
101    }) {
102        features.insert(var_name.strip_prefix("CARGO_FEATURE_").unwrap().to_owned());
103    }
104    features
105}