xref: /wasmtime-44.0.1/scripts/publish.rs (revision dbaaa92f)
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