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