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 }