//! Validated, shell-free execution of the release CI matrix. use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; use std::fmt; use std::fs::{self, OpenOptions}; use std::io::Write as _; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; mod api_surface; mod artifact; mod ci_gate; mod dependency; mod documentation; mod provenance; mod release_candidate; pub use api_surface::{audit_api_surface, write_api_baseline}; pub use artifact::audit_artifacts; pub use ci_gate::{audit_consolidated_ci, run_release_gate, run_required_gate}; pub use dependency::audit_dependencies; pub use documentation::{audit_documentation, write_documentation_report}; pub use provenance::{audit_provenance, write_provenance_reports}; pub use release_candidate::audit_release_candidate; pub const MATRIX_PATH: &str = "ci/release-matrix.json"; const WORKFLOW_PATH: &str = ".gitea/workflows/release.yml"; const REQUIRED_PROFILES: [&str; 7] = [ "linux-msrv-portable", "linux-stable-default", "linux-stable-minimal", "linux-stable-features", "linux-stable-release-surface", "windows-stable-portable", "macos-stable-portable", ]; const REQUIRED_FEATURE_SETS: [&str; 13] = [ "all-features", "dds-bc67", "default", "docs", "examples", "jpeg2000", "no-default-features", "real-audio", "rust-j2k", "rust-skia", "skia", "tests", "vorbis", ]; const REQUIRED_FEATURES: [(&str, &str); 9] = [ ("libremetaverse", "dds-bc67"), ("libremetaverse", "jpeg2000"), ("libremetaverse", "rust-j2k"), ("libremetaverse", "vorbis"), ("libremetaverse-imaging", "jpeg2000"), ("libremetaverse-imaging", "rust-j2k"), ("libremetaverse-imaging-skia", "skia"), ("libremetaverse-imaging-skia", "rust-skia"), ("libremetaverse-voice-webrtc", "real-audio"), ]; #[derive(Debug)] pub struct MatrixError(String); impl MatrixError { fn new(message: impl Into) -> Self { Self(message.into()) } } impl fmt::Display for MatrixError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } impl std::error::Error for MatrixError {} impl From for MatrixError { fn from(error: std::io::Error) -> Self { Self::new(error.to_string()) } } impl From for MatrixError { fn from(error: serde_json::Error) -> Self { Self::new(error.to_string()) } } pub type Result = std::result::Result; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ReleaseMatrix { pub schema: u32, pub msrv: String, pub current: String, pub native_prerequisites: Vec, pub manual_gates: Vec, pub profiles: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NativePrerequisite { pub id: String, pub version: String, pub purpose: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Profile { pub id: String, pub platform: String, pub host: String, pub execution: String, pub toolchain: String, pub target: String, pub feature_sets: Vec, pub prerequisites: Vec, pub commands: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CargoInvocation { pub label: String, pub args: Vec, } #[derive(Debug, Serialize)] struct Evidence<'a> { schema: u32, profile: &'a str, platform: &'a str, host: &'a str, execution: &'a str, target: &'a str, requested_toolchain: &'a str, rustc: String, cargo: String, source_commit: String, recorded_unix_seconds: u64, feature_sets: &'a [String], native_prerequisites: Vec<&'a NativePrerequisite>, commands: &'a [CargoInvocation], completed_commands: usize, status: &'static str, } #[must_use] pub fn workspace_root(start: &Path) -> Option { start.ancestors().find_map(|directory| { let manifest = directory.join("Cargo.toml"); fs::read_to_string(&manifest) .ok() .filter(|contents| contents.contains("[workspace]")) .map(|_| directory.to_path_buf()) }) } /// Loads the canonical release matrix from a workspace root. /// /// # Errors /// /// Returns an error when the matrix cannot be read or is not valid JSON. pub fn load(root: &Path) -> Result { let bytes = fs::read(root.join(MATRIX_PATH))?; Ok(serde_json::from_slice(&bytes)?) } /// Validates the matrix, Cargo feature declarations, and Gitea workflow coverage. /// /// # Errors /// /// Returns an error when any release invariant is missing or inconsistent. pub fn audit(root: &Path, matrix: &ReleaseMatrix) -> Result<()> { audit_shape(matrix)?; audit_cargo_features(root)?; audit_workflows(root, matrix)?; Ok(()) } /// Validates the platform, toolchain, feature, and command shape of a matrix. /// /// # Errors /// /// Returns an error when a required profile, prerequisite, or gate is absent. pub fn audit_shape(matrix: &ReleaseMatrix) -> Result<()> { if matrix.schema != 1 { return Err(MatrixError::new("release matrix schema must be 1")); } if matrix.msrv != "1.96.0" || matrix.current != "stable" { return Err(MatrixError::new( "release matrix must cover Rust 1.96.0 and current stable", )); } let prerequisite_ids = unique( matrix .native_prerequisites .iter() .map(|prerequisite| prerequisite.id.as_str()), "native prerequisite", )?; if matrix.native_prerequisites.iter().any(|prerequisite| { prerequisite.version.trim().is_empty() || prerequisite.purpose.trim().is_empty() }) { return Err(MatrixError::new( "native prerequisites require exact versions and purposes", )); } let manual = matrix.manual_gates.join(" ").to_ascii_lowercase(); for boundary in ["live grid", "physical audio", "proprietary"] { if !manual.contains(boundary) { return Err(MatrixError::new(format!( "manual gate boundary is missing {boundary}" ))); } } let profile_ids = unique( matrix.profiles.iter().map(|profile| profile.id.as_str()), "profile", )?; for required in REQUIRED_PROFILES { if !profile_ids.contains(required) { return Err(MatrixError::new(format!( "required profile {required} is missing" ))); } } let (platforms, toolchains, feature_sets) = audit_profiles(matrix, &prerequisite_ids)?; audit_release_surface(matrix)?; if platforms != BTreeSet::from(["linux", "macos", "windows"]) || !toolchains.contains("stable") || !toolchains.contains("1.96.0") { return Err(MatrixError::new( "matrix must cover Linux, macOS, Windows, MSRV, and stable", )); } for required in REQUIRED_FEATURE_SETS { if !feature_sets.contains(required) { return Err(MatrixError::new(format!( "feature/test surface {required} is missing" ))); } } Ok(()) } fn audit_release_surface(matrix: &ReleaseMatrix) -> Result<()> { let profile = matrix .profiles .iter() .find(|profile| profile.id == "linux-stable-release-surface") .ok_or_else(|| MatrixError::new("release-surface profile is missing"))?; let builds_programs = profile.commands.iter().any(|invocation| { invocation .args .first() .is_some_and(|argument| argument == "build") && invocation .args .windows(2) .any(|pair| pair == ["-p", "libremetaverse-programs"]) && invocation.args.iter().any(|argument| argument == "--bins") }); if !builds_programs { return Err(MatrixError::new( "release-surface profile must build the shipped example-program binaries", )); } Ok(()) } fn audit_profiles<'a>( matrix: &'a ReleaseMatrix, prerequisite_ids: &BTreeSet<&str>, ) -> Result<(BTreeSet<&'a str>, BTreeSet<&'a str>, BTreeSet<&'a str>)> { let mut platforms = BTreeSet::new(); let mut toolchains = BTreeSet::new(); let mut feature_sets = BTreeSet::new(); for profile in &matrix.profiles { if profile.host != "ubuntu-latest" { return Err(MatrixError::new(format!( "profile {} violates the ubuntu-latest Gitea constraint", profile.id ))); } if !matches!(profile.platform.as_str(), "linux" | "windows" | "macos") { return Err(MatrixError::new(format!( "profile {} has an unsupported platform", profile.id ))); } let expected_execution = if profile.platform == "linux" { "native" } else { "cross" }; if profile.execution != expected_execution { return Err(MatrixError::new(format!( "profile {} must use {expected_execution} execution", profile.id ))); } if profile.target.trim().is_empty() || profile.commands.is_empty() { return Err(MatrixError::new(format!( "profile {} needs a target and at least one command", profile.id ))); } for prerequisite in &profile.prerequisites { if !prerequisite_ids.contains(prerequisite.as_str()) { return Err(MatrixError::new(format!( "profile {} references unknown prerequisite {prerequisite}", profile.id ))); } } for invocation in &profile.commands { audit_invocation(profile, invocation)?; } platforms.insert(profile.platform.as_str()); toolchains.insert(profile.toolchain.as_str()); feature_sets.extend(profile.feature_sets.iter().map(String::as_str)); } Ok((platforms, toolchains, feature_sets)) } fn unique<'a>(values: impl Iterator, kind: &str) -> Result> { let mut unique = BTreeSet::new(); for value in values { if value.trim().is_empty() || !unique.insert(value) { return Err(MatrixError::new(format!( "{kind} identifiers must be nonempty and unique" ))); } } Ok(unique) } fn audit_invocation(profile: &Profile, invocation: &CargoInvocation) -> Result<()> { let Some(subcommand) = invocation.args.first().map(String::as_str) else { return Err(MatrixError::new(format!( "profile {} contains an empty Cargo command", profile.id ))); }; if !matches!(subcommand, "check" | "test" | "doc" | "build") { return Err(MatrixError::new(format!( "profile {} uses disallowed Cargo subcommand {subcommand}", profile.id ))); } if invocation.label.trim().is_empty() || !invocation .args .iter() .any(|argument| argument == "--locked") || !invocation.args.windows(2).any(|pair| pair == ["-j", "1"]) { return Err(MatrixError::new(format!( "profile {} commands need labels, --locked, and -j 1", profile.id ))); } if profile.execution == "cross" && !invocation .args .windows(2) .any(|pair| pair == ["--target", profile.target.as_str()]) { return Err(MatrixError::new(format!( "cross profile {} command does not select its target", profile.id ))); } Ok(()) } fn audit_cargo_features(root: &Path) -> Result<()> { let output = Command::new(cargo_program()) .args(["metadata", "--no-deps", "--format-version", "1"]) .current_dir(root) .output()?; if !output.status.success() { return Err(MatrixError::new( "cargo metadata failed during matrix audit", )); } let metadata: Value = serde_json::from_slice(&output.stdout)?; let packages = metadata["packages"] .as_array() .ok_or_else(|| MatrixError::new("cargo metadata has no package array"))?; let feature_map = packages .iter() .filter_map(|package| Some((package["name"].as_str()?, package["features"].as_object()?))) .collect::>(); for (package, feature) in REQUIRED_FEATURES { if !feature_map .get(package) .is_some_and(|features| features.contains_key(feature)) { return Err(MatrixError::new(format!( "Cargo feature {package}/{feature} is missing" ))); } } let defaults = feature_map .get("libremetaverse") .and_then(|features| features.get("default")) .and_then(Value::as_array) .ok_or_else(|| MatrixError::new("libremetaverse default feature set is missing"))?; if defaults.iter().any(|feature| { matches!( feature.as_str(), Some("jpeg2000" | "vorbis" | "dep:vorbis_rs") ) }) { return Err(MatrixError::new( "default consumers must not enable native OpenJPEG or Vorbis adapters", )); } let default_tree = Command::new(cargo_program()) .args([ "tree", "--locked", "-p", "libremetaverse", "--edges", "normal,build", "--prefix", "none", ]) .current_dir(root) .output()?; if !default_tree.status.success() { return Err(MatrixError::new( "cargo tree failed during default dependency audit", )); } let default_tree = String::from_utf8_lossy(&default_tree.stdout); for forbidden in [ "libremetaverse-openjpeg ", "vorbis_rs ", "ogg_next_sys ", "aotuv_lancer_vorbis_sys ", ] { if default_tree.lines().any(|line| line.starts_with(forbidden)) { return Err(MatrixError::new(format!( "default consumer dependency graph unexpectedly contains {forbidden}" ))); } } Ok(()) } fn audit_workflows(root: &Path, matrix: &ReleaseMatrix) -> Result<()> { let workflow = fs::read_to_string(root.join(WORKFLOW_PATH))?; if matrix.profiles.is_empty() || !workflow.contains("release-gate") || !workflow.contains("~/.cargo/registry") || !workflow.contains("~/.cargo/git") { return Err(MatrixError::new( "release workflow must invoke the Rust gate and cache Cargo downloads", )); } audit_consolidated_ci(root)?; let workflows = root.join(".gitea/workflows"); for entry in fs::read_dir(workflows)? { let path = entry?.path(); if !matches!( path.extension().and_then(OsStr::to_str), Some("yml" | "yaml") ) { continue; } let contents = fs::read_to_string(&path)?.to_ascii_lowercase(); if contents.contains("runs-on: windows") || contents.contains("runs-on: macos") { return Err(MatrixError::new(format!( "{} violates the ubuntu-only Gitea runner policy", path.display() ))); } } Ok(()) } /// Executes one profile from an empty target directory and writes its evidence record. /// /// # Errors /// /// Returns an error when the profile is unknown, its toolchain is incorrect, the /// target directory is not clean, a command fails, or evidence cannot be written. pub fn run(root: &Path, matrix: &ReleaseMatrix, profile_id: &str, evidence: &Path) -> Result<()> { audit(root, matrix)?; let profile = matrix .profiles .iter() .find(|profile| profile.id == profile_id) .ok_or_else(|| MatrixError::new(format!("unknown CI profile {profile_id}")))?; if evidence.exists() { return Err(MatrixError::new(format!( "{} already exists; preserve or remove it before running the profile", evidence.display() ))); } let rustc = version("rustc")?; let cargo = version(cargo_program())?; if profile.toolchain != "stable" && rustc .split_whitespace() .nth(1) .is_none_or(|version| version != profile.toolchain) { return Err(MatrixError::new(format!( "profile {} requires rustc {}, found {rustc}", profile.id, profile.toolchain ))); } let target_directory = root.join("target/ci").join(&profile.id); if target_directory.exists() { return Err(MatrixError::new(format!( "{} already exists; clean it before rerunning so cache cannot mask the build", target_directory.display() ))); } let source_commit = command_line("git", ["rev-parse", "--verify", "HEAD"], root)?; let prerequisites = profile .prerequisites .iter() .map(|id| { matrix .native_prerequisites .iter() .find(|prerequisite| prerequisite.id == *id) .ok_or_else(|| MatrixError::new(format!("unknown prerequisite {id}"))) }) .collect::>>()?; let mut completed = 0; let mut failure = None; for invocation in &profile.commands { println!("ci-matrix {}: {}", profile.id, invocation.label); let status = Command::new(cargo_program()) .args(&invocation.args) .current_dir(root) .env("CARGO_INCREMENTAL", "0") .env("CARGO_TARGET_DIR", &target_directory) .stdin(Stdio::null()) .status()?; if status.success() { completed += 1; } else { failure = Some(status); break; } } let status = if failure.is_none() { "ok" } else { "failed" }; write_evidence( evidence, &Evidence { schema: 1, profile: &profile.id, platform: &profile.platform, host: &profile.host, execution: &profile.execution, target: &profile.target, requested_toolchain: &profile.toolchain, rustc, cargo, source_commit, recorded_unix_seconds: SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| MatrixError::new("system clock predates Unix epoch"))? .as_secs(), feature_sets: &profile.feature_sets, native_prerequisites: prerequisites, commands: &profile.commands, completed_commands: completed, status, }, )?; if let Some(status) = failure { return Err(MatrixError::new(format!( "profile {} failed with {}", profile.id, describe_status(status) ))); } Ok(()) } fn version(program: impl AsRef) -> Result { command_line(program, ["--version"], Path::new(".")) } fn command_line( program: impl AsRef, args: impl IntoIterator>, directory: &Path, ) -> Result { let output = Command::new(program) .args(args) .current_dir(directory) .output()?; if !output.status.success() { return Err(MatrixError::new("version/provenance command failed")); } String::from_utf8(output.stdout) .map(|value| value.trim().to_owned()) .map_err(|_| MatrixError::new("version/provenance output was not UTF-8")) } fn cargo_program() -> std::ffi::OsString { std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()) } fn write_evidence(path: &Path, evidence: &Evidence<'_>) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let mut file = OpenOptions::new().write(true).create_new(true).open(path)?; serde_json::to_writer_pretty(&mut file, evidence)?; file.write_all(b"\n")?; file.sync_all()?; Ok(()) } fn describe_status(status: ExitStatus) -> String { status.code().map_or_else( || "termination by signal".to_owned(), |code| format!("exit {code}"), ) } #[cfg(test)] mod tests { use super::*; #[test] fn checked_in_matrix_has_complete_shape() { let matrix: ReleaseMatrix = serde_json::from_str(include_str!("../../../ci/release-matrix.json")).unwrap(); audit_shape(&matrix).unwrap(); } #[test] fn runner_rejects_unknown_profiles_before_execution() { let root = workspace_root(Path::new(env!("CARGO_MANIFEST_DIR"))).unwrap(); let matrix = load(&root).unwrap(); let evidence = std::env::temp_dir().join("metacrate-ci-matrix-unknown.json"); assert!(run(&root, &matrix, "not-a-profile", &evidence).is_err()); assert!(!evidence.exists()); } }