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-wast", 84 "wasmtime-internal-c-api-macros", 85 "wasmtime-c-api-impl", 86 "wasmtime-cli-flags", 87 "wasmtime-internal-explorer", 88 "wasmtime-cli", 89 ]; 90 91 // Anything **not** mentioned in this array is required to have an `=a.b.c` 92 // dependency requirement on it to enable breaking api changes even in "patch" 93 // releases since everything not mentioned here is just an organizational detail 94 // that no one else should rely on. 95 const PUBLIC_CRATES: &[&str] = &[ 96 // These are actually public crates which we cannot break the API of in 97 // patch releases. 98 "wasmtime", 99 "wasmtime-wasi-io", 100 "wasmtime-wasi", 101 "wasmtime-wasi-tls", 102 "wasmtime-wasi-http", 103 "wasmtime-wasi-nn", 104 "wasmtime-wasi-config", 105 "wasmtime-wasi-keyvalue", 106 "wasmtime-wasi-threads", 107 "wasmtime-cli", 108 // All cranelift crates are considered "public" in that they can't have 109 // breaking API changes in patch releases. 110 "cranelift-srcgen", 111 "cranelift-assembler-x64-meta", 112 "cranelift-assembler-x64", 113 "cranelift-entity", 114 "cranelift-bforest", 115 "cranelift-bitset", 116 "cranelift-codegen-shared", 117 "cranelift-codegen-meta", 118 "cranelift-egraph", 119 "cranelift-control", 120 "cranelift-codegen", 121 "cranelift-reader", 122 "cranelift-serde", 123 "cranelift-module", 124 "cranelift-frontend", 125 "cranelift-native", 126 "cranelift-object", 127 "cranelift-interpreter", 128 "cranelift", 129 "cranelift-jit", 130 // This is a dependency of cranelift crates and as a result can't break in 131 // patch releases as well 132 "wasmtime-types", 133 ]; 134 135 const C_HEADER_PATH: &str = "./crates/c-api/include/wasmtime.h"; 136 137 struct Workspace { 138 version: String, 139 } 140 141 struct Crate { 142 manifest: PathBuf, 143 name: String, 144 version: String, 145 publish: bool, 146 } 147 148 fn main() { 149 let mut crates = Vec::new(); 150 let root = read_crate(None, "./Cargo.toml".as_ref()); 151 let ws = Workspace { 152 version: root.version.clone(), 153 }; 154 crates.push(root); 155 find_crates("crates".as_ref(), &ws, &mut crates); 156 find_crates("cranelift".as_ref(), &ws, &mut crates); 157 find_crates("pulley".as_ref(), &ws, &mut crates); 158 find_crates("winch".as_ref(), &ws, &mut crates); 159 160 let pos = CRATES_TO_PUBLISH 161 .iter() 162 .enumerate() 163 .map(|(i, c)| (*c, i)) 164 .collect::<HashMap<_, _>>(); 165 crates.sort_by_key(|krate| pos.get(&krate.name[..])); 166 167 match &env::args().nth(1).expect("must have one argument")[..] { 168 name @ "bump" | name @ "bump-patch" => { 169 for krate in crates.iter() { 170 bump_version(&krate, &crates, name == "bump-patch"); 171 } 172 // update C API version in wasmtime.h 173 update_capi_version(); 174 // update the lock file 175 run_cmd(Command::new("cargo").arg("fetch")); 176 } 177 178 "publish" => { 179 // We have so many crates to publish we're frequently either 180 // rate-limited or we run into issues where crates can't publish 181 // successfully because they're waiting on the index entries of 182 // previously-published crates to propagate. This means we try to 183 // publish in a loop and we remove crates once they're successfully 184 // published. Failed-to-publish crates get enqueued for another try 185 // later on. 186 for _ in 0..10 { 187 crates.retain(|krate| !publish(krate)); 188 189 if crates.is_empty() { 190 break; 191 } 192 193 println!( 194 "{} crates failed to publish, waiting for a bit to retry", 195 crates.len(), 196 ); 197 thread::sleep(Duration::from_secs(40)); 198 } 199 200 assert!(crates.is_empty(), "failed to publish all crates"); 201 202 println!(""); 203 println!("==================================================================="); 204 println!(""); 205 println!("Don't forget to push a git tag for this release!"); 206 println!(""); 207 println!(" $ git tag vX.Y.Z"); 208 println!(" $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z"); 209 } 210 211 "verify" => { 212 verify(&crates); 213 } 214 215 s => panic!("unknown command: {}", s), 216 } 217 } 218 219 fn cmd_output(cmd: &mut Command) -> Output { 220 eprintln!("Running: `{:?}`", cmd); 221 match cmd.output() { 222 Ok(o) => o, 223 Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e), 224 } 225 } 226 227 fn cmd_status(cmd: &mut Command) -> ExitStatus { 228 eprintln!("Running: `{:?}`", cmd); 229 match cmd.status() { 230 Ok(s) => s, 231 Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e), 232 } 233 } 234 235 fn run_cmd(cmd: &mut Command) { 236 let status = cmd_status(cmd); 237 assert!( 238 status.success(), 239 "Command `{:?}` exited with failure status: {}", 240 cmd, 241 status 242 ); 243 } 244 245 fn find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>) { 246 if dir.join("Cargo.toml").exists() { 247 let krate = read_crate(Some(ws), &dir.join("Cargo.toml")); 248 if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) { 249 dst.push(krate); 250 } else { 251 panic!("failed to find {:?} in whitelist or blacklist", krate.name); 252 } 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 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 let contents = fs::read_to_string(&krate.manifest).unwrap(); 305 let next_version = |krate: &Crate| -> String { 306 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 307 bump(&krate.version, patch) 308 } else { 309 krate.version.clone() 310 } 311 }; 312 313 let mut new_manifest = String::new(); 314 let mut is_deps = false; 315 for line in contents.lines() { 316 let mut rewritten = false; 317 if !is_deps && line.starts_with("version =") { 318 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 319 println!( 320 "bump `{}` {} => {}", 321 krate.name, 322 krate.version, 323 next_version(krate), 324 ); 325 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate))); 326 rewritten = true; 327 } 328 } 329 330 is_deps = if line.starts_with("[") { 331 line.contains("dependencies") 332 } else { 333 is_deps 334 }; 335 336 for other in crates { 337 // If `other` isn't a published crate then it's not going to get a 338 // bumped version so we don't need to update anything in the 339 // manifest. 340 if !other.publish { 341 continue; 342 } 343 if !is_deps 344 || (!line.starts_with(&format!("{} ", 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 !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) { 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 output = cmd_output(Command::new("curl").arg(&format!( 448 "https://crates.io/api/v1/crates/{}/versions", 449 krate.name 450 ))); 451 if output.status.success() 452 && String::from_utf8_lossy(&output.stdout) 453 .contains(&format!("\"num\":\"{}\"", krate.version)) 454 { 455 println!( 456 "skip publish {} because {} is already published", 457 krate.name, krate.version, 458 ); 459 return true; 460 } 461 462 let status = cmd_status( 463 Command::new("cargo") 464 .arg("publish") 465 .current_dir(krate.manifest.parent().unwrap()) 466 .arg("--no-verify"), 467 ); 468 if !status.success() { 469 println!("FAIL: failed to publish `{}`: {}", krate.name, status); 470 return false; 471 } 472 473 // After we've published then make sure that the `wasmtime-publish` group is 474 // added to this crate for future publications. If it's already present 475 // though we can skip the `cargo owner` modification. 476 let output = cmd_output(Command::new("curl").arg(&format!( 477 "https://crates.io/api/v1/crates/{}/owners", 478 krate.name 479 ))); 480 if output.status.success() 481 && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish") 482 { 483 println!( 484 "wasmtime-publish already listed as an owner of {}", 485 krate.name 486 ); 487 return true; 488 } 489 490 // Note that the status is ignored here. This fails most of the time because 491 // the owner is already set and present, so we only want to add this to 492 // crates which haven't previously been published. 493 run_cmd( 494 Command::new("cargo") 495 .arg("owner") 496 .arg("-a") 497 .arg("github:bytecodealliance:wasmtime-publish") 498 .arg(&krate.name), 499 ); 500 501 true 502 } 503 504 // Verify the current tree is publish-able to crates.io. The intention here is 505 // that we'll run `cargo package` on everything which verifies the build as-if 506 // it were published to crates.io. This requires using an incrementally-built 507 // directory registry generated from `cargo vendor` because the versions 508 // referenced from `Cargo.toml` may not exist on crates.io. 509 fn verify(crates: &[Crate]) { 510 verify_capi(); 511 512 if Path::new(".cargo").exists() { 513 panic!( 514 "`.cargo` already exists on the file system, remove it and then run the script again" 515 ); 516 } 517 if Path::new("vendor").exists() { 518 panic!( 519 "`vendor` already exists on the file system, remove it and then run the script again" 520 ); 521 } 522 523 let vendor = cmd_output(Command::new("cargo").arg("vendor").stderr(Stdio::inherit())); 524 assert!(vendor.status.success()); 525 526 fs::create_dir_all(".cargo").unwrap(); 527 fs::write(".cargo/config.toml", vendor.stdout).unwrap(); 528 529 for krate in crates { 530 if !krate.publish { 531 continue; 532 } 533 verify_and_vendor(&krate); 534 } 535 536 fn verify_and_vendor(krate: &Crate) { 537 let mut cmd = Command::new("cargo"); 538 cmd.arg("package") 539 .arg("--manifest-path") 540 .arg(&krate.manifest) 541 .env("CARGO_TARGET_DIR", "./target"); 542 if krate.name.contains("wasi-nn") { 543 cmd.arg("--no-verify"); 544 } 545 run_cmd(&mut cmd); 546 run_cmd( 547 Command::new("tar") 548 .arg("xf") 549 .arg(format!( 550 "../target/package/{}-{}.crate", 551 krate.name, krate.version 552 )) 553 .current_dir("./vendor"), 554 ); 555 fs::write( 556 format!( 557 "./vendor/{}-{}/.cargo-checksum.json", 558 krate.name, krate.version 559 ), 560 "{\"files\":{}}", 561 ) 562 .unwrap(); 563 } 564 565 fn verify_capi() { 566 let version = read_crate(None, "./Cargo.toml".as_ref()).version; 567 568 let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap()); 569 let major = iter.next().expect("major version"); 570 let minor = iter.next().expect("minor version"); 571 let patch = iter.next().expect("patch version"); 572 573 let mut count = 0; 574 let contents = fs::read_to_string(C_HEADER_PATH).unwrap(); 575 for line in contents.lines() { 576 if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) { 577 count += 1; 578 } else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) { 579 count += 1; 580 } else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) { 581 count += 1; 582 } else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) { 583 count += 1; 584 } 585 } 586 587 assert!( 588 count == 4, 589 "invalid version macros in {}, should match \"{}\"", 590 C_HEADER_PATH, 591 version 592 ); 593 } 594 } 595