1 //! Helper script to publish the wasmtime and cranelift suites of crates 2 //! 3 //! See documentation in `docs/contributing-release-process.md` for more 4 //! information, but in a nutshell: 5 //! 6 //! * `./publish bump` - bump crate versions in-tree 7 //! * `./publish verify` - verify crates can be published to crates.io 8 //! * `./publish publish` - actually publish crates to crates.io 9 10 use std::collections::HashMap; 11 use std::env; 12 use std::fs; 13 use std::path::{Path, PathBuf}; 14 use std::process::{Command, Stdio}; 15 use std::thread; 16 use std::time::Duration; 17 18 // note that this list must be topologically sorted by dependencies 19 const CRATES_TO_PUBLISH: &[&str] = &[ 20 // cranelift 21 "cranelift-isle", 22 "cranelift-entity", 23 "wasmtime-types", 24 "cranelift-bforest", 25 "cranelift-codegen-shared", 26 "cranelift-codegen-meta", 27 "cranelift-codegen", 28 "cranelift-reader", 29 "cranelift-serde", 30 "cranelift-module", 31 "cranelift-preopt", 32 "cranelift-frontend", 33 "cranelift-wasm", 34 "cranelift-native", 35 "cranelift-object", 36 "cranelift-interpreter", 37 "cranelift", 38 "cranelift-jit", 39 // wiggle 40 "wiggle-generate", 41 "wiggle-macro", 42 // wasmtime 43 "wasmtime-jit-debug", 44 "wasmtime-fiber", 45 "wasmtime-environ", 46 "wasmtime-runtime", 47 "wasmtime-cranelift", 48 "wasmtime-jit", 49 "wasmtime-cache", 50 "wasmtime", 51 // wasi-common/wiggle 52 "wiggle", 53 "wasi-common", 54 "wasi-cap-std-sync", 55 "wasi-tokio", 56 // other misc wasmtime crates 57 "wasmtime-wasi", 58 "wasmtime-wasi-nn", 59 "wasmtime-wasi-crypto", 60 "wasmtime-wast", 61 "wasmtime-cli-flags", 62 "wasmtime-cli", 63 ]; 64 65 // Anything **not** mentioned in this array is required to have an `=a.b.c` 66 // dependency requirement on it to enable breaking api changes even in "patch" 67 // releases since everything not mentioned here is just an organizational detail 68 // that no one else should rely on. 69 const PUBLIC_CRATES: &[&str] = &[ 70 // just here to appease the script because these are submodules of this 71 // repository. 72 "wasi-crypto", 73 "witx", 74 // these are actually public crates which we cannot break the API of in 75 // patch releases. 76 "wasmtime", 77 "wasmtime-wasi", 78 "wasmtime-wasi-nn", 79 "wasmtime-wasi-crypto", 80 "wasmtime-cli", 81 // all cranelift crates are considered "public" in that they can't 82 // have breaking API changes in patch releases 83 "cranelift-entity", 84 "cranelift-bforest", 85 "cranelift-codegen-shared", 86 "cranelift-codegen-meta", 87 "cranelift-codegen", 88 "cranelift-reader", 89 "cranelift-serde", 90 "cranelift-module", 91 "cranelift-preopt", 92 "cranelift-frontend", 93 "cranelift-wasm", 94 "cranelift-native", 95 "cranelift-object", 96 "cranelift-interpreter", 97 "cranelift", 98 "cranelift-jit", 99 // This is a dependency of cranelift crates and as a result can't break in 100 // patch releases as well 101 "wasmtime-types", 102 ]; 103 104 struct Crate { 105 manifest: PathBuf, 106 name: String, 107 version: String, 108 publish: bool, 109 } 110 111 fn main() { 112 let mut crates = Vec::new(); 113 crates.push(read_crate("./Cargo.toml".as_ref())); 114 find_crates("crates".as_ref(), &mut crates); 115 find_crates("cranelift".as_ref(), &mut crates); 116 117 let pos = CRATES_TO_PUBLISH 118 .iter() 119 .enumerate() 120 .map(|(i, c)| (*c, i)) 121 .collect::<HashMap<_, _>>(); 122 crates.sort_by_key(|krate| pos.get(&krate.name[..])); 123 124 match &env::args().nth(1).expect("must have one argument")[..] { 125 name @ "bump" | name @ "bump-patch" => { 126 for krate in crates.iter() { 127 bump_version(&krate, &crates, name == "bump-patch"); 128 } 129 // update the lock file 130 assert!(Command::new("cargo") 131 .arg("fetch") 132 .status() 133 .unwrap() 134 .success()); 135 } 136 137 "publish" => { 138 // We have so many crates to publish we're frequently either 139 // rate-limited or we run into issues where crates can't publish 140 // successfully because they're waiting on the index entries of 141 // previously-published crates to propagate. This means we try to 142 // publish in a loop and we remove crates once they're successfully 143 // published. Failed-to-publish crates get enqueued for another try 144 // later on. 145 for _ in 0..5 { 146 crates.retain(|krate| !publish(krate)); 147 148 if crates.is_empty() { 149 break; 150 } 151 152 println!( 153 "{} crates failed to publish, waiting for a bit to retry", 154 crates.len(), 155 ); 156 thread::sleep(Duration::from_secs(20)); 157 } 158 159 assert!(crates.is_empty(), "failed to publish all crates"); 160 161 println!(""); 162 println!("==================================================================="); 163 println!(""); 164 println!("Don't forget to push a git tag for this release!"); 165 println!(""); 166 println!(" $ git tag vX.Y.Z"); 167 println!(" $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z"); 168 } 169 170 "verify" => { 171 verify(&crates); 172 } 173 174 s => panic!("unknown command: {}", s), 175 } 176 } 177 178 fn find_crates(dir: &Path, dst: &mut Vec<Crate>) { 179 if dir.join("Cargo.toml").exists() { 180 let krate = read_crate(&dir.join("Cargo.toml")); 181 if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) { 182 dst.push(krate); 183 } else { 184 panic!("failed to find {:?} in whitelist or blacklist", krate.name); 185 } 186 } 187 188 for entry in dir.read_dir().unwrap() { 189 let entry = entry.unwrap(); 190 if entry.file_type().unwrap().is_dir() { 191 find_crates(&entry.path(), dst); 192 } 193 } 194 } 195 196 fn read_crate(manifest: &Path) -> Crate { 197 let mut name = None; 198 let mut version = None; 199 let mut publish = true; 200 for line in fs::read_to_string(manifest).unwrap().lines() { 201 if name.is_none() && line.starts_with("name = \"") { 202 name = Some( 203 line.replace("name = \"", "") 204 .replace("\"", "") 205 .trim() 206 .to_string(), 207 ); 208 } 209 if version.is_none() && line.starts_with("version = \"") { 210 version = Some( 211 line.replace("version = \"", "") 212 .replace("\"", "") 213 .trim() 214 .to_string(), 215 ); 216 } 217 if line.starts_with("publish = false") { 218 publish = false; 219 } 220 } 221 let name = name.unwrap(); 222 let version = version.unwrap(); 223 if ["witx", "witx-cli", "wasi-crypto"].contains(&&name[..]) { 224 publish = false; 225 } 226 Crate { 227 manifest: manifest.to_path_buf(), 228 name, 229 version, 230 publish, 231 } 232 } 233 234 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) { 235 let contents = fs::read_to_string(&krate.manifest).unwrap(); 236 let next_version = |krate: &Crate| -> String { 237 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 238 bump(&krate.version, patch) 239 } else { 240 krate.version.clone() 241 } 242 }; 243 244 let mut new_manifest = String::new(); 245 let mut is_deps = false; 246 for line in contents.lines() { 247 let mut rewritten = false; 248 if !is_deps && line.starts_with("version =") { 249 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 250 println!( 251 "bump `{}` {} => {}", 252 krate.name, 253 krate.version, 254 next_version(krate), 255 ); 256 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate))); 257 rewritten = true; 258 } 259 } 260 261 is_deps = if line.starts_with("[") { 262 line.contains("dependencies") 263 } else { 264 is_deps 265 }; 266 267 for other in crates { 268 // If `other` isn't a published crate then it's not going to get a 269 // bumped version so we don't need to update anything in the 270 // manifest. 271 if !other.publish { 272 continue; 273 } 274 if !is_deps || !line.starts_with(&format!("{} ", other.name)) { 275 continue; 276 } 277 if !line.contains(&other.version) { 278 if !line.contains("version =") || !krate.publish { 279 continue; 280 } 281 panic!( 282 "{:?} has a dep on {} but doesn't list version {}", 283 krate.manifest, other.name, other.version 284 ); 285 } 286 if krate.publish { 287 if PUBLIC_CRATES.contains(&other.name.as_str()) { 288 assert!( 289 !line.contains("\"="), 290 "{} should not have an exact version requirement on {}", 291 krate.name, 292 other.name 293 ); 294 } else { 295 assert!( 296 line.contains("\"="), 297 "{} should have an exact version requirement on {}", 298 krate.name, 299 other.name 300 ); 301 } 302 } 303 rewritten = true; 304 new_manifest.push_str(&line.replace(&other.version, &next_version(other))); 305 break; 306 } 307 if !rewritten { 308 new_manifest.push_str(line); 309 } 310 new_manifest.push_str("\n"); 311 } 312 fs::write(&krate.manifest, new_manifest).unwrap(); 313 } 314 315 /// Performs a major version bump increment on the semver version `version`. 316 /// 317 /// This function will perform a semver-major-version bump on the `version` 318 /// specified. This is used to calculate the next version of a crate in this 319 /// repository since we're currently making major version bumps for all our 320 /// releases. This may end up getting tweaked as we stabilize crates and start 321 /// doing more minor/patch releases, but for now this should do the trick. 322 fn bump(version: &str, patch_bump: bool) -> String { 323 let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap()); 324 let major = iter.next().expect("major version"); 325 let minor = iter.next().expect("minor version"); 326 let patch = iter.next().expect("patch version"); 327 328 if patch_bump { 329 return format!("{}.{}.{}", major, minor, patch + 1); 330 } 331 if major != 0 { 332 format!("{}.0.0", major + 1) 333 } else if minor != 0 { 334 format!("0.{}.0", minor + 1) 335 } else { 336 format!("0.0.{}", patch + 1) 337 } 338 } 339 340 fn publish(krate: &Crate) -> bool { 341 if !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) { 342 return true; 343 } 344 345 // First make sure the crate isn't already published at this version. This 346 // script may be re-run and there's no need to re-attempt previous work. 347 let output = Command::new("curl") 348 .arg(&format!("https://crates.io/api/v1/crates/{}", krate.name)) 349 .output() 350 .expect("failed to invoke `curl`"); 351 if output.status.success() 352 && String::from_utf8_lossy(&output.stdout) 353 .contains(&format!("\"newest_version\":\"{}\"", krate.version)) 354 { 355 println!( 356 "skip publish {} because {} is latest version", 357 krate.name, krate.version, 358 ); 359 return true; 360 } 361 362 let status = Command::new("cargo") 363 .arg("publish") 364 .current_dir(krate.manifest.parent().unwrap()) 365 .arg("--no-verify") 366 .status() 367 .expect("failed to run cargo"); 368 if !status.success() { 369 println!("FAIL: failed to publish `{}`: {}", krate.name, status); 370 return false; 371 } 372 373 // After we've published then make sure that the `wasmtime-publish` group is 374 // added to this crate for future publications. If it's already present 375 // though we can skip the `cargo owner` modification. 376 let output = Command::new("curl") 377 .arg(&format!( 378 "https://crates.io/api/v1/crates/{}/owners", 379 krate.name 380 )) 381 .output() 382 .expect("failed to invoke `curl`"); 383 if output.status.success() 384 && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish") 385 { 386 println!( 387 "wasmtime-publish already listed as an owner of {}", 388 krate.name 389 ); 390 return true; 391 } 392 393 // Note that the status is ignored here. This fails most of the time because 394 // the owner is already set and present, so we only want to add this to 395 // crates which haven't previously been published. 396 let status = Command::new("cargo") 397 .arg("owner") 398 .arg("-a") 399 .arg("github:bytecodealliance:wasmtime-publish") 400 .arg(&krate.name) 401 .status() 402 .expect("failed to run cargo"); 403 if !status.success() { 404 panic!( 405 "FAIL: failed to add wasmtime-publish as owner `{}`: {}", 406 krate.name, status 407 ); 408 } 409 410 true 411 } 412 413 // Verify the current tree is publish-able to crates.io. The intention here is 414 // that we'll run `cargo package` on everything which verifies the build as-if 415 // it were published to crates.io. This requires using an incrementally-built 416 // directory registry generated from `cargo vendor` because the versions 417 // referenced from `Cargo.toml` may not exist on crates.io. 418 fn verify(crates: &[Crate]) { 419 drop(fs::remove_dir_all(".cargo")); 420 drop(fs::remove_dir_all("vendor")); 421 let vendor = Command::new("cargo") 422 .arg("vendor") 423 .stderr(Stdio::inherit()) 424 .output() 425 .unwrap(); 426 assert!(vendor.status.success()); 427 428 fs::create_dir_all(".cargo").unwrap(); 429 fs::write(".cargo/config.toml", vendor.stdout).unwrap(); 430 431 // Vendor witx which wasn't vendored because it's a path dependency, but 432 // it'll need to be in our directory registry for crates that depend on it. 433 let witx = crates 434 .iter() 435 .find(|c| c.name == "witx" && c.manifest.iter().any(|p| p == "wasi-common")) 436 .unwrap(); 437 verify_and_vendor(&witx); 438 439 // Vendor wasi-crypto which is also a path dependency 440 let wasi_crypto = crates.iter().find(|c| c.name == "wasi-crypto").unwrap(); 441 verify_and_vendor(&wasi_crypto); 442 443 for krate in crates { 444 if !krate.publish { 445 continue; 446 } 447 verify_and_vendor(&krate); 448 } 449 450 fn verify_and_vendor(krate: &Crate) { 451 let mut cmd = Command::new("cargo"); 452 cmd.arg("package") 453 .arg("--manifest-path") 454 .arg(&krate.manifest) 455 .env("CARGO_TARGET_DIR", "./target"); 456 if krate.name == "witx" || krate.name.contains("wasi-nn") { 457 cmd.arg("--no-verify"); 458 } 459 let status = cmd.status().unwrap(); 460 assert!(status.success(), "failed to verify {:?}", &krate.manifest); 461 let tar = Command::new("tar") 462 .arg("xf") 463 .arg(format!( 464 "../target/package/{}-{}.crate", 465 krate.name, krate.version 466 )) 467 .current_dir("./vendor") 468 .status() 469 .unwrap(); 470 assert!(tar.success()); 471 fs::write( 472 format!( 473 "./vendor/{}-{}/.cargo-checksum.json", 474 krate.name, krate.version 475 ), 476 "{\"files\":{}}", 477 ) 478 .unwrap(); 479 } 480 } 481