lib

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

api_qualification.rs (5576B)


      1 use std::{collections::BTreeSet, fs, path::Path, process::Command};
      2 
      3 use serde::Deserialize;
      4 
      5 #[derive(Debug, Deserialize)]
      6 struct Contract {
      7     schema_version: u16,
      8     baseline_revision: String,
      9     tool: String,
     10     minimum_tool_version: String,
     11     release_type: String,
     12     feature_policy: String,
     13     packages: Vec<String>,
     14     baseline_override: Vec<BaselineOverride>,
     15 }
     16 
     17 #[derive(Debug, Deserialize)]
     18 struct BaselineOverride {
     19     package: String,
     20     revision: String,
     21 }
     22 
     23 pub fn run(root: &Path) -> Result<(), String> {
     24     let contract = load(root)?;
     25     validate(&contract, 19)?;
     26     verify_tool(&contract)?;
     27     verify_revision(root, &contract.baseline_revision)?;
     28     for baseline in &contract.baseline_override {
     29         verify_revision(root, &baseline.revision)?;
     30     }
     31     for package in &contract.packages {
     32         let args = invocation(&contract, package);
     33         eprintln!("cargo {}", args.join(" "));
     34         let status = Command::new("cargo")
     35             .args(&args)
     36             .env("CARGO_PROFILE_DEV_DEBUG", "none")
     37             .current_dir(root)
     38             .status()
     39             .map_err(|error| format!("failed to start cargo-semver-checks: {error}"))?;
     40         if !status.success() {
     41             return Err(format!("public API qualification failed for {package}"));
     42         }
     43     }
     44     Ok(())
     45 }
     46 
     47 fn load(root: &Path) -> Result<Contract, String> {
     48     let path = root.join("contracts/releases/api_semver.toml");
     49     let raw = fs::read_to_string(&path)
     50         .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
     51     toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
     52 }
     53 
     54 fn validate(contract: &Contract, expected_packages: usize) -> Result<(), String> {
     55     if contract.schema_version != 1
     56         || contract.tool != "cargo-semver-checks"
     57         || contract.release_type != "major"
     58         || contract.feature_policy != "all"
     59         || contract.baseline_revision.len() < 7
     60     {
     61         return Err("invalid public API qualification contract".to_owned());
     62     }
     63     let unique = contract.packages.iter().collect::<BTreeSet<_>>();
     64     if unique.len() != expected_packages || unique.len() != contract.packages.len() {
     65         return Err(format!(
     66             "public API contract requires exactly {expected_packages} unique packages"
     67         ));
     68     }
     69     let overrides = contract
     70         .baseline_override
     71         .iter()
     72         .map(|baseline| baseline.package.as_str())
     73         .collect::<BTreeSet<_>>();
     74     if overrides != BTreeSet::from(["radroots", "radroots_sdk"])
     75         || contract
     76             .baseline_override
     77             .iter()
     78             .any(|baseline| baseline.revision.len() != 40)
     79     {
     80         return Err(
     81             "public API baseline overrides must identify the two imported front doors".to_owned(),
     82         );
     83     }
     84     Ok(())
     85 }
     86 
     87 fn verify_tool(contract: &Contract) -> Result<(), String> {
     88     let output = Command::new("cargo")
     89         .args(["semver-checks", "--version"])
     90         .output()
     91         .map_err(|error| format!("failed to start cargo-semver-checks: {error}"))?;
     92     if !output.status.success() {
     93         return Err("cargo-semver-checks is required".to_owned());
     94     }
     95     let stdout = String::from_utf8_lossy(&output.stdout);
     96     let installed = stdout
     97         .split_whitespace()
     98         .find_map(|value| semver::Version::parse(value).ok())
     99         .ok_or_else(|| format!("could not parse cargo-semver-checks version: {stdout}"))?;
    100     let minimum = semver::Version::parse(&contract.minimum_tool_version)
    101         .map_err(|error| format!("invalid minimum tool version: {error}"))?;
    102     if installed < minimum {
    103         return Err(format!(
    104             "cargo-semver-checks {} or newer is required, found {installed}",
    105             contract.minimum_tool_version
    106         ));
    107     }
    108     Ok(())
    109 }
    110 
    111 fn verify_revision(root: &Path, revision: &str) -> Result<(), String> {
    112     let status = Command::new("git")
    113         .args(["cat-file", "-e", &format!("{revision}^{{commit}}")])
    114         .current_dir(root)
    115         .status()
    116         .map_err(|error| format!("failed to inspect API baseline revision: {error}"))?;
    117     if status.success() {
    118         Ok(())
    119     } else {
    120         Err(format!("API baseline revision {revision} is unavailable"))
    121     }
    122 }
    123 
    124 fn invocation(contract: &Contract, package: &str) -> Vec<String> {
    125     let baseline = contract
    126         .baseline_override
    127         .iter()
    128         .find(|baseline| baseline.package == package)
    129         .map_or(contract.baseline_revision.as_str(), |baseline| {
    130             baseline.revision.as_str()
    131         });
    132     vec![
    133         "semver-checks".to_owned(),
    134         "check-release".to_owned(),
    135         "--package".to_owned(),
    136         package.to_owned(),
    137         "--baseline-rev".to_owned(),
    138         baseline.to_owned(),
    139         "--all-features".to_owned(),
    140         "--release-type".to_owned(),
    141         contract.release_type.clone(),
    142     ]
    143 }
    144 
    145 #[cfg(test)]
    146 mod tests {
    147     use super::{invocation, load, validate};
    148 
    149     #[test]
    150     fn current_contract_covers_all_library_packages() {
    151         let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
    152             .parent()
    153             .and_then(std::path::Path::parent)
    154             .expect("workspace root");
    155         let contract = load(root).expect("contract");
    156         validate(&contract, 19).expect("valid contract");
    157         let invocation = invocation(&contract, "radroots_core");
    158         assert!(invocation.contains(&"--all-features".to_owned()));
    159         assert!(invocation.ends_with(&["--release-type".to_owned(), "major".to_owned()]));
    160     }
    161 }