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