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, 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 // cranelift 21 "cranelift-isle", 22 "cranelift-entity", 23 "wasmtime-types", 24 "cranelift-bforest", 25 "cranelift-codegen-shared", 26 "cranelift-codegen-meta", 27 "cranelift-codegen", 28 "cranelift-reader", 29 "cranelift-serde", 30 "cranelift-module", 31 "cranelift-preopt", 32 "cranelift-frontend", 33 "cranelift-wasm", 34 "cranelift-native", 35 "cranelift-object", 36 "cranelift-interpreter", 37 "cranelift", 38 "cranelift-jit", 39 // wiggle 40 "wiggle-generate", 41 "wiggle-macro", 42 // wasmtime 43 "wasmtime-asm-macros", 44 "wasmtime-component-util", 45 "wasmtime-component-macro", 46 "wasmtime-jit-debug", 47 "wasmtime-fiber", 48 "wasmtime-environ", 49 "wasmtime-runtime", 50 "wasmtime-cranelift", 51 "wasmtime-jit", 52 "wasmtime-cache", 53 "wasmtime", 54 // wasi-common/wiggle 55 "wiggle", 56 "wasi-common", 57 "wasi-cap-std-sync", 58 "wasi-tokio", 59 // other misc wasmtime crates 60 "wasmtime-wasi", 61 "wasmtime-wasi-nn", 62 "wasmtime-wasi-crypto", 63 "wasmtime-wast", 64 "wasmtime-cli-flags", 65 "wasmtime-cli", 66 ]; 67 68 // Anything **not** mentioned in this array is required to have an `=a.b.c` 69 // dependency requirement on it to enable breaking api changes even in "patch" 70 // releases since everything not mentioned here is just an organizational detail 71 // that no one else should rely on. 72 const PUBLIC_CRATES: &[&str] = &[ 73 // just here to appease the script because these are submodules of this 74 // repository. 75 "wasi-crypto", 76 "witx", 77 // these are actually public crates which we cannot break the API of in 78 // patch releases. 79 "wasmtime", 80 "wasmtime-wasi", 81 "wasmtime-wasi-nn", 82 "wasmtime-wasi-crypto", 83 "wasmtime-cli", 84 // all cranelift crates are considered "public" in that they can't 85 // have breaking API changes in patch releases 86 "cranelift-entity", 87 "cranelift-bforest", 88 "cranelift-codegen-shared", 89 "cranelift-codegen-meta", 90 "cranelift-codegen", 91 "cranelift-reader", 92 "cranelift-serde", 93 "cranelift-module", 94 "cranelift-preopt", 95 "cranelift-frontend", 96 "cranelift-wasm", 97 "cranelift-native", 98 "cranelift-object", 99 "cranelift-interpreter", 100 "cranelift", 101 "cranelift-jit", 102 // This is a dependency of cranelift crates and as a result can't break in 103 // patch releases as well 104 "wasmtime-types", 105 ]; 106 107 struct Crate { 108 manifest: PathBuf, 109 name: String, 110 version: String, 111 publish: bool, 112 } 113 114 fn main() { 115 let mut crates = Vec::new(); 116 crates.push(read_crate("./Cargo.toml".as_ref())); 117 find_crates("crates".as_ref(), &mut crates); 118 find_crates("cranelift".as_ref(), &mut crates); 119 120 let pos = CRATES_TO_PUBLISH 121 .iter() 122 .enumerate() 123 .map(|(i, c)| (*c, i)) 124 .collect::<HashMap<_, _>>(); 125 crates.sort_by_key(|krate| pos.get(&krate.name[..])); 126 127 match &env::args().nth(1).expect("must have one argument")[..] { 128 name @ "bump" | name @ "bump-patch" => { 129 for krate in crates.iter() { 130 bump_version(&krate, &crates, name == "bump-patch"); 131 } 132 // update the lock file 133 assert!(Command::new("cargo") 134 .arg("fetch") 135 .status() 136 .unwrap() 137 .success()); 138 } 139 140 "publish" => { 141 // We have so many crates to publish we're frequently either 142 // rate-limited or we run into issues where crates can't publish 143 // successfully because they're waiting on the index entries of 144 // previously-published crates to propagate. This means we try to 145 // publish in a loop and we remove crates once they're successfully 146 // published. Failed-to-publish crates get enqueued for another try 147 // later on. 148 for _ in 0..5 { 149 crates.retain(|krate| !publish(krate)); 150 151 if crates.is_empty() { 152 break; 153 } 154 155 println!( 156 "{} crates failed to publish, waiting for a bit to retry", 157 crates.len(), 158 ); 159 thread::sleep(Duration::from_secs(20)); 160 } 161 162 assert!(crates.is_empty(), "failed to publish all crates"); 163 164 println!(""); 165 println!("==================================================================="); 166 println!(""); 167 println!("Don't forget to push a git tag for this release!"); 168 println!(""); 169 println!(" $ git tag vX.Y.Z"); 170 println!(" $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z"); 171 } 172 173 "verify" => { 174 verify(&crates); 175 } 176 177 s => panic!("unknown command: {}", s), 178 } 179 } 180 181 fn find_crates(dir: &Path, dst: &mut Vec<Crate>) { 182 if dir.join("Cargo.toml").exists() { 183 let krate = read_crate(&dir.join("Cargo.toml")); 184 if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) { 185 dst.push(krate); 186 } else { 187 panic!("failed to find {:?} in whitelist or blacklist", krate.name); 188 } 189 } 190 191 for entry in dir.read_dir().unwrap() { 192 let entry = entry.unwrap(); 193 if entry.file_type().unwrap().is_dir() { 194 find_crates(&entry.path(), dst); 195 } 196 } 197 } 198 199 fn read_crate(manifest: &Path) -> Crate { 200 let mut name = None; 201 let mut version = None; 202 let mut publish = true; 203 for line in fs::read_to_string(manifest).unwrap().lines() { 204 if name.is_none() && line.starts_with("name = \"") { 205 name = Some( 206 line.replace("name = \"", "") 207 .replace("\"", "") 208 .trim() 209 .to_string(), 210 ); 211 } 212 if version.is_none() && line.starts_with("version = \"") { 213 version = Some( 214 line.replace("version = \"", "") 215 .replace("\"", "") 216 .trim() 217 .to_string(), 218 ); 219 } 220 if line.starts_with("publish = false") { 221 publish = false; 222 } 223 } 224 let name = name.unwrap(); 225 let version = version.unwrap(); 226 if ["witx", "witx-cli", "wasi-crypto"].contains(&&name[..]) { 227 publish = false; 228 } 229 Crate { 230 manifest: manifest.to_path_buf(), 231 name, 232 version, 233 publish, 234 } 235 } 236 237 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) { 238 let contents = fs::read_to_string(&krate.manifest).unwrap(); 239 let next_version = |krate: &Crate| -> String { 240 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 241 bump(&krate.version, patch) 242 } else { 243 krate.version.clone() 244 } 245 }; 246 247 let mut new_manifest = String::new(); 248 let mut is_deps = false; 249 for line in contents.lines() { 250 let mut rewritten = false; 251 if !is_deps && line.starts_with("version =") { 252 if CRATES_TO_PUBLISH.contains(&&krate.name[..]) { 253 println!( 254 "bump `{}` {} => {}", 255 krate.name, 256 krate.version, 257 next_version(krate), 258 ); 259 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate))); 260 rewritten = true; 261 } 262 } 263 264 is_deps = if line.starts_with("[") { 265 line.contains("dependencies") 266 } else { 267 is_deps 268 }; 269 270 for other in crates { 271 // If `other` isn't a published crate then it's not going to get a 272 // bumped version so we don't need to update anything in the 273 // manifest. 274 if !other.publish { 275 continue; 276 } 277 if !is_deps || !line.starts_with(&format!("{} ", other.name)) { 278 continue; 279 } 280 if !line.contains(&other.version) { 281 if !line.contains("version =") || !krate.publish { 282 continue; 283 } 284 panic!( 285 "{:?} has a dep on {} but doesn't list version {}", 286 krate.manifest, other.name, other.version 287 ); 288 } 289 if krate.publish { 290 if PUBLIC_CRATES.contains(&other.name.as_str()) { 291 assert!( 292 !line.contains("\"="), 293 "{} should not have an exact version requirement on {}", 294 krate.name, 295 other.name 296 ); 297 } else { 298 assert!( 299 line.contains("\"="), 300 "{} should have an exact version requirement on {}", 301 krate.name, 302 other.name 303 ); 304 } 305 } 306 rewritten = true; 307 new_manifest.push_str(&line.replace(&other.version, &next_version(other))); 308 break; 309 } 310 if !rewritten { 311 new_manifest.push_str(line); 312 } 313 new_manifest.push_str("\n"); 314 } 315 fs::write(&krate.manifest, new_manifest).unwrap(); 316 } 317 318 /// Performs a major version bump increment on the semver version `version`. 319 /// 320 /// This function will perform a semver-major-version bump on the `version` 321 /// specified. This is used to calculate the next version of a crate in this 322 /// repository since we're currently making major version bumps for all our 323 /// releases. This may end up getting tweaked as we stabilize crates and start 324 /// doing more minor/patch releases, but for now this should do the trick. 325 fn bump(version: &str, patch_bump: bool) -> String { 326 let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap()); 327 let major = iter.next().expect("major version"); 328 let minor = iter.next().expect("minor version"); 329 let patch = iter.next().expect("patch version"); 330 331 if patch_bump { 332 return format!("{}.{}.{}", major, minor, patch + 1); 333 } 334 if major != 0 { 335 format!("{}.0.0", major + 1) 336 } else if minor != 0 { 337 format!("0.{}.0", minor + 1) 338 } else { 339 format!("0.0.{}", patch + 1) 340 } 341 } 342 343 fn publish(krate: &Crate) -> bool { 344 if !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) { 345 return true; 346 } 347 348 // First make sure the crate isn't already published at this version. This 349 // script may be re-run and there's no need to re-attempt previous work. 350 let output = Command::new("curl") 351 .arg(&format!("https://crates.io/api/v1/crates/{}", krate.name)) 352 .output() 353 .expect("failed to invoke `curl`"); 354 if output.status.success() 355 && String::from_utf8_lossy(&output.stdout) 356 .contains(&format!("\"newest_version\":\"{}\"", krate.version)) 357 { 358 println!( 359 "skip publish {} because {} is latest version", 360 krate.name, krate.version, 361 ); 362 return true; 363 } 364 365 let status = Command::new("cargo") 366 .arg("publish") 367 .current_dir(krate.manifest.parent().unwrap()) 368 .arg("--no-verify") 369 .status() 370 .expect("failed to run cargo"); 371 if !status.success() { 372 println!("FAIL: failed to publish `{}`: {}", krate.name, status); 373 return false; 374 } 375 376 // After we've published then make sure that the `wasmtime-publish` group is 377 // added to this crate for future publications. If it's already present 378 // though we can skip the `cargo owner` modification. 379 let output = Command::new("curl") 380 .arg(&format!( 381 "https://crates.io/api/v1/crates/{}/owners", 382 krate.name 383 )) 384 .output() 385 .expect("failed to invoke `curl`"); 386 if output.status.success() 387 && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish") 388 { 389 println!( 390 "wasmtime-publish already listed as an owner of {}", 391 krate.name 392 ); 393 return true; 394 } 395 396 // Note that the status is ignored here. This fails most of the time because 397 // the owner is already set and present, so we only want to add this to 398 // crates which haven't previously been published. 399 let status = Command::new("cargo") 400 .arg("owner") 401 .arg("-a") 402 .arg("github:bytecodealliance:wasmtime-publish") 403 .arg(&krate.name) 404 .status() 405 .expect("failed to run cargo"); 406 if !status.success() { 407 panic!( 408 "FAIL: failed to add wasmtime-publish as owner `{}`: {}", 409 krate.name, status 410 ); 411 } 412 413 true 414 } 415 416 // Verify the current tree is publish-able to crates.io. The intention here is 417 // that we'll run `cargo package` on everything which verifies the build as-if 418 // it were published to crates.io. This requires using an incrementally-built 419 // directory registry generated from `cargo vendor` because the versions 420 // referenced from `Cargo.toml` may not exist on crates.io. 421 fn verify(crates: &[Crate]) { 422 drop(fs::remove_dir_all(".cargo")); 423 drop(fs::remove_dir_all("vendor")); 424 let vendor = Command::new("cargo") 425 .arg("vendor") 426 .stderr(Stdio::inherit()) 427 .output() 428 .unwrap(); 429 assert!(vendor.status.success()); 430 431 fs::create_dir_all(".cargo").unwrap(); 432 fs::write(".cargo/config.toml", vendor.stdout).unwrap(); 433 434 // Vendor witx which wasn't vendored because it's a path dependency, but 435 // it'll need to be in our directory registry for crates that depend on it. 436 let witx = crates 437 .iter() 438 .find(|c| c.name == "witx" && c.manifest.iter().any(|p| p == "wasi-common")) 439 .unwrap(); 440 verify_and_vendor(&witx); 441 442 // Vendor wasi-crypto which is also a path dependency 443 let wasi_crypto = crates.iter().find(|c| c.name == "wasi-crypto").unwrap(); 444 verify_and_vendor(&wasi_crypto); 445 446 for krate in crates { 447 if !krate.publish { 448 continue; 449 } 450 verify_and_vendor(&krate); 451 } 452 453 fn verify_and_vendor(krate: &Crate) { 454 let mut cmd = Command::new("cargo"); 455 cmd.arg("package") 456 .arg("--manifest-path") 457 .arg(&krate.manifest) 458 .env("CARGO_TARGET_DIR", "./target"); 459 if krate.name == "witx" || krate.name.contains("wasi-nn") { 460 cmd.arg("--no-verify"); 461 } 462 let status = cmd.status().unwrap(); 463 assert!(status.success(), "failed to verify {:?}", &krate.manifest); 464 let tar = Command::new("tar") 465 .arg("xf") 466 .arg(format!( 467 "../target/package/{}-{}.crate", 468 krate.name, krate.version 469 )) 470 .current_dir("./vendor") 471 .status() 472 .unwrap(); 473 assert!(tar.success()); 474 fs::write( 475 format!( 476 "./vendor/{}-{}/.cargo-checksum.json", 477 krate.name, krate.version 478 ), 479 "{\"files\":{}}", 480 ) 481 .unwrap(); 482 } 483 } 484