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-asm-macros", 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-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 // 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 if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) { 251 dst.push(krate); 252 } else { 253 panic!("failed to find {:?} in whitelist or blacklist", krate.name); 254 } 255 } 256 257 for entry in dir.read_dir().unwrap() { 258 let entry = entry.unwrap(); 259 if entry.file_type().unwrap().is_dir() { 260 find_crates(&entry.path(), ws, dst); 261 } 262 } 263 } 264 265 fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate { 266 let mut name = None; 267 let mut version = None; 268 let mut publish = true; 269 for line in fs::read_to_string(manifest).unwrap().lines() { 270 if name.is_none() && line.starts_with("name = \"") { 271 name = Some( 272 line.replace("name = \"", "") 273 .replace("\"", "") 274 .trim() 275 .to_string(), 276 ); 277 } 278 if version.is_none() && line.starts_with("version = \"") { 279 version = Some( 280 line.replace("version = \"", "") 281 .replace("\"", "") 282 .trim() 283 .to_string(), 284 ); 285 } 286 if let Some(ws) = ws { 287 if version.is_none() && line.starts_with("version.workspace = true") { 288 version = Some(ws.version.clone()); 289 } 290 } 291 if line.starts_with("publish = false") { 292 publish = false; 293 } 294 } 295 let name = name.unwrap(); 296 let version = version.unwrap(); 297 Crate { 298 manifest: manifest.to_path_buf(), 299 name, 300 version, 301 publish, 302 } 303 } 304 305 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) { 306 let contents = fs::read_to_string(&krate.manifest).unwrap(); 307 let next_version = |krate: &Crate| -> String { 308 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 309 bump(&krate.version, patch) 310 } else { 311 krate.version.clone() 312 } 313 }; 314 315 let mut new_manifest = String::new(); 316 let mut is_deps = false; 317 for line in contents.lines() { 318 let mut rewritten = false; 319 if !is_deps && line.starts_with("version =") { 320 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 321 println!( 322 "bump `{}` {} => {}", 323 krate.name, 324 krate.version, 325 next_version(krate), 326 ); 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 { 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 !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) { 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 let mut cmd = Command::new("cargo"); 554 cmd.arg("package") 555 .arg("--manifest-path") 556 .arg(&krate.manifest) 557 .env("CARGO_TARGET_DIR", "./target"); 558 if krate.name.contains("wasi-nn") { 559 cmd.arg("--no-verify"); 560 } 561 run_cmd(&mut cmd); 562 run_cmd( 563 Command::new("tar") 564 .arg("xf") 565 .arg(format!( 566 "../target/package/{}-{}.crate", 567 krate.name, krate.version 568 )) 569 .current_dir("./vendor"), 570 ); 571 fs::write( 572 format!( 573 "./vendor/{}-{}/.cargo-checksum.json", 574 krate.name, krate.version 575 ), 576 "{\"files\":{}}", 577 ) 578 .unwrap(); 579 } 580 581 fn verify_capi() { 582 let version = read_crate(None, "./Cargo.toml".as_ref()).version; 583 584 let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap()); 585 let major = iter.next().expect("major version"); 586 let minor = iter.next().expect("minor version"); 587 let patch = iter.next().expect("patch version"); 588 589 let mut count = 0; 590 let contents = fs::read_to_string(C_HEADER_PATH).unwrap(); 591 for line in contents.lines() { 592 if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) { 593 count += 1; 594 } else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) { 595 count += 1; 596 } else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) { 597 count += 1; 598 } else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) { 599 count += 1; 600 } 601 } 602 603 assert!( 604 count == 4, 605 "invalid version macros in {}, should match \"{}\"", 606 C_HEADER_PATH, 607 version 608 ); 609 } 610 } 611