target_qualification.rs (7936B)
1 use std::{fs, path::Path, process::Command}; 2 3 use serde::Deserialize; 4 5 #[derive(Debug, Deserialize)] 6 struct TargetMatrix { 7 schema_version: u32, 8 msrv_toolchain: String, 9 current_toolchain: String, 10 operating_system: Vec<OperatingSystem>, 11 } 12 13 #[derive(Debug, Deserialize, PartialEq, Eq)] 14 struct OperatingSystem { 15 name: String, 16 target: String, 17 cross_compiler: Option<String>, 18 cross_cflags: Option<String>, 19 } 20 21 #[derive(Debug, Deserialize)] 22 struct Catalog { 23 package: Vec<CatalogPackage>, 24 } 25 26 #[derive(Debug, Deserialize)] 27 struct CatalogPackage { 28 name: String, 29 state: String, 30 groups: Vec<String>, 31 } 32 33 pub fn run(workspace_root: &Path) -> Result<(), String> { 34 let (matrix, packages) = load(workspace_root)?; 35 for toolchain in [&matrix.msrv_toolchain, &matrix.current_toolchain] { 36 run_command( 37 workspace_root, 38 "rustup", 39 &[ 40 "run", 41 toolchain, 42 "cargo", 43 "check", 44 "--workspace", 45 "--all-targets", 46 "--locked", 47 ], 48 )?; 49 } 50 for operating_system in matrix.operating_system { 51 for package in &packages { 52 run_target_command( 53 workspace_root, 54 &operating_system, 55 &[ 56 "check", 57 "-p", 58 package, 59 "--lib", 60 "--target", 61 &operating_system.target, 62 "--locked", 63 ], 64 )?; 65 } 66 } 67 Ok(()) 68 } 69 70 fn load(workspace_root: &Path) -> Result<(TargetMatrix, Vec<String>), String> { 71 let matrix_path = workspace_root.join("contracts/releases/target_matrix.toml"); 72 let matrix = toml::from_str::<TargetMatrix>(&read(&matrix_path)?) 73 .map_err(|error| format!("failed to parse {}: {error}", matrix_path.display()))?; 74 validate(&matrix)?; 75 76 let catalog_path = workspace_root.join("contracts/crates/catalog.v2.toml"); 77 let catalog = toml::from_str::<Catalog>(&read(&catalog_path)?) 78 .map_err(|error| format!("failed to parse {}: {error}", catalog_path.display()))?; 79 let mut packages = catalog 80 .package 81 .into_iter() 82 .filter(|package| { 83 package.state == "active" && package.groups.iter().any(|group| group == "public_native") 84 }) 85 .map(|package| package.name) 86 .collect::<Vec<_>>(); 87 packages.sort(); 88 if packages.len() != 19 { 89 return Err(format!( 90 "target qualification requires exactly 19 public packages, found {}", 91 packages.len() 92 )); 93 } 94 Ok((matrix, packages)) 95 } 96 97 fn read(path: &Path) -> Result<String, String> { 98 fs::read_to_string(path).map_err(|error| format!("failed to read {}: {error}", path.display())) 99 } 100 101 fn validate(matrix: &TargetMatrix) -> Result<(), String> { 102 if matrix.schema_version != 1 { 103 return Err(format!( 104 "target matrix schema_version must be 1, found {}", 105 matrix.schema_version 106 )); 107 } 108 if matrix.msrv_toolchain != "1.97.1" || matrix.current_toolchain != "stable" { 109 return Err("target matrix must retain MSRV 1.97.1 and current stable".to_owned()); 110 } 111 let expected = [ 112 ( 113 "linux", 114 "x86_64-unknown-linux-gnu", 115 Some("zig cc"), 116 Some("-target x86_64-linux-gnu"), 117 ), 118 ("macos", "aarch64-apple-darwin", None, None), 119 ]; 120 if matrix.operating_system.len() != expected.len() 121 || expected.iter().any(|(name, target, compiler, cflags)| { 122 !matrix.operating_system.iter().any(|entry| { 123 entry.name == *name 124 && entry.target == *target 125 && entry.cross_compiler.as_deref() == *compiler 126 && entry.cross_cflags.as_deref() == *cflags 127 }) 128 }) 129 { 130 return Err( 131 "target matrix must contain the exact Linux x86_64 and macOS aarch64 triples" 132 .to_owned(), 133 ); 134 } 135 Ok(()) 136 } 137 138 fn run_target_command( 139 workspace_root: &Path, 140 operating_system: &OperatingSystem, 141 args: &[&str], 142 ) -> Result<(), String> { 143 eprintln!("cargo {}", args.join(" ")); 144 let mut command = Command::new("cargo"); 145 command.args(args).current_dir(workspace_root); 146 let target_key = operating_system.target.replace('-', "_"); 147 if let Some(compiler) = operating_system.cross_compiler.as_deref() { 148 command.env(format!("CC_{target_key}"), compiler); 149 } 150 if let Some(cflags) = operating_system.cross_cflags.as_deref() { 151 command.env(format!("CFLAGS_{target_key}"), cflags); 152 } 153 let status = command 154 .status() 155 .map_err(|error| format!("failed to start cargo: {error}"))?; 156 if status.success() { 157 Ok(()) 158 } else { 159 Err(format!( 160 "target qualification failed: cargo {}", 161 args.join(" ") 162 )) 163 } 164 } 165 166 fn run_command(workspace_root: &Path, program: &str, args: &[&str]) -> Result<(), String> { 167 eprintln!("{program} {}", args.join(" ")); 168 let status = Command::new(program) 169 .args(args) 170 .current_dir(workspace_root) 171 .status() 172 .map_err(|error| format!("failed to start {program}: {error}"))?; 173 if status.success() { 174 Ok(()) 175 } else { 176 Err(format!( 177 "target qualification failed: {program} {}", 178 args.join(" ") 179 )) 180 } 181 } 182 183 #[cfg(test)] 184 mod tests { 185 use super::{OperatingSystem, TargetMatrix, load, validate}; 186 187 #[test] 188 fn current_contract_selects_exact_toolchains_targets_and_packages() { 189 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 190 .parent() 191 .and_then(std::path::Path::parent) 192 .expect("workspace root"); 193 let (matrix, packages) = load(root).expect("target matrix"); 194 assert_eq!(matrix.msrv_toolchain, "1.97.1"); 195 assert_eq!(matrix.current_toolchain, "stable"); 196 assert_eq!(matrix.operating_system.len(), 2); 197 assert_eq!( 198 matrix 199 .operating_system 200 .iter() 201 .map(|entry| (entry.name.as_str(), entry.target.as_str())) 202 .collect::<Vec<_>>(), 203 [ 204 ("linux", "x86_64-unknown-linux-gnu"), 205 ("macos", "aarch64-apple-darwin"), 206 ] 207 ); 208 assert_eq!(packages.len(), 19); 209 } 210 211 #[test] 212 fn unsupported_production_targets_are_rejected() { 213 let mut matrix = TargetMatrix { 214 schema_version: 1, 215 msrv_toolchain: "1.97.1".to_owned(), 216 current_toolchain: "stable".to_owned(), 217 operating_system: vec![ 218 OperatingSystem { 219 name: "linux".to_owned(), 220 target: "x86_64-unknown-linux-gnu".to_owned(), 221 cross_compiler: Some("zig cc".to_owned()), 222 cross_cflags: Some("-target x86_64-linux-gnu".to_owned()), 223 }, 224 OperatingSystem { 225 name: "macos".to_owned(), 226 target: "aarch64-apple-darwin".to_owned(), 227 cross_compiler: None, 228 cross_cflags: None, 229 }, 230 ], 231 }; 232 for (name, target) in [ 233 ("windows", "x86_64-pc-windows-gnu"), 234 ("macos", "x86_64-apple-darwin"), 235 ("linux", "aarch64-unknown-linux-gnu"), 236 ] { 237 matrix.operating_system.push(OperatingSystem { 238 name: name.to_owned(), 239 target: target.to_owned(), 240 cross_compiler: None, 241 cross_cflags: None, 242 }); 243 assert!(validate(&matrix).is_err(), "accepted {target}"); 244 matrix.operating_system.pop(); 245 } 246 } 247 }