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