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