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