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