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
main()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
cmd_output(cmd: &mut Command) -> Output218 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
cmd_status(cmd: &mut Command) -> ExitStatus226 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
run_cmd(cmd: &mut Command)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
find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>)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
read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate258 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
bump_version(krate: &Crate, crates: &[Crate], patch: bool)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
update_capi_version()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.
bump(version: &str, patch_bump: bool) -> String419 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
publish(krate: &Crate) -> bool437 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
curl(url: &str) -> Option<String>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.
verify(crates: &[Crate])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