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