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