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