lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

release_qualification.rs (6883B)


      1 use std::{ffi::OsString, fs, path::Path, process::Command};
      2 
      3 use serde::Deserialize;
      4 
      5 #[derive(Debug, Deserialize)]
      6 struct Catalog {
      7     package: Vec<Package>,
      8 }
      9 
     10 #[derive(Debug, Deserialize)]
     11 struct Package {
     12     name: String,
     13     state: String,
     14     groups: Vec<String>,
     15 }
     16 
     17 #[derive(Debug, PartialEq, Eq)]
     18 struct CargoInvocation {
     19     args: Vec<String>,
     20 }
     21 
     22 pub fn run_feature_matrix(workspace_root: &Path) -> Result<(), String> {
     23     for invocation in feature_matrix(workspace_root)? {
     24         eprintln!("cargo {}", invocation.args.join(" "));
     25         let mut command = Command::new("cargo");
     26         command
     27             .args(&invocation.args)
     28             .current_dir(workspace_root)
     29             .env("RUSTFLAGS", rustflags_with_warnings_denied());
     30         let status = command
     31             .status()
     32             .map_err(|error| format!("failed to start cargo: {error}"))?;
     33         if !status.success() {
     34             return Err(format!(
     35                 "public feature qualification failed: cargo {}",
     36                 invocation.args.join(" ")
     37             ));
     38         }
     39     }
     40     Ok(())
     41 }
     42 
     43 fn rustflags_with_warnings_denied() -> OsString {
     44     let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
     45     if !rustflags.is_empty() {
     46         rustflags.push(" ");
     47     }
     48     rustflags.push("-Dwarnings");
     49     rustflags
     50 }
     51 
     52 fn feature_matrix(workspace_root: &Path) -> Result<Vec<CargoInvocation>, String> {
     53     let path = workspace_root.join("contracts/crates/catalog.v2.toml");
     54     let raw = fs::read_to_string(&path)
     55         .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
     56     let catalog = toml::from_str::<Catalog>(&raw)
     57         .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
     58     let mut packages = catalog
     59         .package
     60         .into_iter()
     61         .filter(|package| {
     62             package.state == "active" && package.groups.iter().any(|group| group == "public_native")
     63         })
     64         .collect::<Vec<_>>();
     65     packages.sort_by(|left, right| left.name.cmp(&right.name));
     66     if packages.len() != 19 {
     67         return Err(format!(
     68             "library feature qualification requires exactly 19 public packages, found {}",
     69             packages.len()
     70         ));
     71     }
     72 
     73     let mut invocations = Vec::new();
     74     let feature_map = package_feature_map(workspace_root)?;
     75     for package in packages {
     76         let features = feature_map
     77             .get(&package.name)
     78             .ok_or_else(|| format!("cargo metadata omitted features for {}", package.name))?;
     79         invocations.push(check_invocation(&package.name, None, true));
     80         invocations.push(check_invocation(&package.name, None, false));
     81         for feature in features {
     82             invocations.push(check_invocation(&package.name, Some(feature), true));
     83         }
     84         invocations.push(CargoInvocation {
     85             args: vec![
     86                 "check".to_owned(),
     87                 "-p".to_owned(),
     88                 package.name,
     89                 "--all-targets".to_owned(),
     90                 "--all-features".to_owned(),
     91                 "--locked".to_owned(),
     92             ],
     93         });
     94     }
     95     Ok(invocations)
     96 }
     97 
     98 fn package_feature_map(
     99     workspace_root: &Path,
    100 ) -> Result<std::collections::BTreeMap<String, Vec<String>>, String> {
    101     let output = Command::new("cargo")
    102         .args(["metadata", "--format-version", "1", "--locked", "--no-deps"])
    103         .current_dir(workspace_root)
    104         .output()
    105         .map_err(|error| format!("failed to start cargo metadata: {error}"))?;
    106     if !output.status.success() {
    107         return Err("locked cargo metadata failed".to_owned());
    108     }
    109     let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)
    110         .map_err(|error| format!("failed to parse cargo metadata: {error}"))?;
    111     let packages = metadata
    112         .get("packages")
    113         .and_then(serde_json::Value::as_array)
    114         .ok_or_else(|| "cargo metadata omitted packages".to_owned())?;
    115     packages
    116         .iter()
    117         .map(|package| {
    118             let name = package
    119                 .get("name")
    120                 .and_then(serde_json::Value::as_str)
    121                 .ok_or_else(|| "cargo metadata package omitted name".to_owned())?;
    122             let mut features = package
    123                 .get("features")
    124                 .and_then(serde_json::Value::as_object)
    125                 .ok_or_else(|| format!("cargo metadata omitted features for {name}"))?
    126                 .keys()
    127                 .filter(|feature| feature.as_str() != "default")
    128                 .cloned()
    129                 .collect::<Vec<_>>();
    130             features.sort_unstable();
    131             Ok((name.to_owned(), features))
    132         })
    133         .collect()
    134 }
    135 
    136 fn check_invocation(
    137     package: &str,
    138     feature: Option<&str>,
    139     no_default_features: bool,
    140 ) -> CargoInvocation {
    141     let mut args = vec!["check".to_owned(), "-p".to_owned(), package.to_owned()];
    142     if no_default_features {
    143         args.push("--lib".to_owned());
    144         args.push("--no-default-features".to_owned());
    145     } else {
    146         args.push("--all-targets".to_owned());
    147     }
    148     if let Some(feature) = feature {
    149         args.push("--features".to_owned());
    150         args.push(feature.to_owned());
    151     }
    152     args.push("--locked".to_owned());
    153     CargoInvocation { args }
    154 }
    155 
    156 #[cfg(test)]
    157 mod tests {
    158     use super::{check_invocation, feature_matrix};
    159 
    160     #[test]
    161     fn current_catalog_generates_the_complete_library_feature_matrix() {
    162         let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
    163             .parent()
    164             .and_then(std::path::Path::parent)
    165             .expect("workspace root");
    166         let matrix = feature_matrix(root).expect("feature matrix");
    167         assert!(matrix.len() > 68);
    168         assert!(matrix.iter().any(|entry| {
    169             entry
    170                 .args
    171                 .windows(2)
    172                 .any(|pair| pair == ["--features", "blossom"])
    173                 && entry
    174                     .args
    175                     .windows(2)
    176                     .any(|pair| pair == ["-p", "radroots_nostr"])
    177         }));
    178         assert!(matrix.iter().any(|entry| {
    179             entry
    180                 .args
    181                 .windows(2)
    182                 .any(|pair| pair == ["-p", "radroots_geonames"])
    183                 && entry.args.iter().any(|arg| arg == "--all-features")
    184         }));
    185     }
    186 
    187     #[test]
    188     fn individual_features_never_enable_defaults_implicitly() {
    189         let invocation = check_invocation("radroots_event", Some("knowledge"), true);
    190         assert!(
    191             invocation
    192                 .args
    193                 .iter()
    194                 .any(|arg| arg == "--no-default-features")
    195         );
    196         assert_eq!(
    197             invocation
    198                 .args
    199                 .windows(2)
    200                 .find(|pair| pair[0] == "--features"),
    201             Some(&["--features".to_owned(), "knowledge".to_owned()][..])
    202         );
    203     }
    204 }