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