xref: /wasmtime-44.0.1/scripts/publish.rs (revision bda02c19)
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-wasi-tls-nativetls",
79     "wasmtime-wasi-tls-openssl",
80     "wasmtime-wast",
81     "wasmtime-internal-c-api-macros",
82     "wasmtime-c-api-impl",
83     "wasmtime-wizer",
84     "wasmtime-cli-flags",
85     "wasmtime-internal-explorer",
86     "wasmtime-internal-debugger",
87     "wasmtime-cli",
88 ];
89 
90 // Anything **not** mentioned in this array is required to have an `=a.b.c`
91 // dependency requirement on it to enable breaking api changes even in "patch"
92 // releases since everything not mentioned here is just an organizational detail
93 // that no one else should rely on.
94 const PUBLIC_CRATES: &[&str] = &[
95     // These are actually public crates which we cannot break the API of in
96     // patch releases.
97     "wasmtime",
98     "wasmtime-wasi-io",
99     "wasmtime-wasi",
100     "wasmtime-wasi-tls",
101     "wasmtime-wasi-tls-nativetls",
102     "wasmtime-wasi-tls-openssl",
103     "wasmtime-wasi-http",
104     "wasmtime-wasi-nn",
105     "wasmtime-wasi-config",
106     "wasmtime-wasi-keyvalue",
107     "wasmtime-wasi-threads",
108     "wasmtime-cli",
109     "wasmtime-wizer",
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         dst.push(krate);
251     }
252 
253     for entry in dir.read_dir().unwrap() {
254         let entry = entry.unwrap();
255         if entry.file_type().unwrap().is_dir() {
256             find_crates(&entry.path(), ws, dst);
257         }
258     }
259 }
260 
261 fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate {
262     let mut name = None;
263     let mut version = None;
264     let mut publish = true;
265     for line in fs::read_to_string(manifest).unwrap().lines() {
266         if name.is_none() && line.starts_with("name = \"") {
267             name = Some(
268                 line.replace("name = \"", "")
269                     .replace("\"", "")
270                     .trim()
271                     .to_string(),
272             );
273         }
274         if version.is_none() && line.starts_with("version = \"") {
275             version = Some(
276                 line.replace("version = \"", "")
277                     .replace("\"", "")
278                     .trim()
279                     .to_string(),
280             );
281         }
282         if let Some(ws) = ws {
283             if version.is_none() && line.starts_with("version.workspace = true") {
284                 version = Some(ws.version.clone());
285             }
286         }
287         if line.starts_with("publish = false") {
288             publish = false;
289         }
290     }
291     let name = name.unwrap();
292     let version = version.unwrap();
293     assert!(
294         !publish || CRATES_TO_PUBLISH.contains(&&name[..]),
295         "a crate must either be listed in `CRATES_TO_PUBLISH` or have `publish = false` \
296          in its `Cargo.toml`"
297     );
298     Crate {
299         manifest: manifest.to_path_buf(),
300         name,
301         version,
302         publish,
303     }
304 }
305 
306 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
307     println!("bumping `{}`...", krate.name);
308     let contents = fs::read_to_string(&krate.manifest).unwrap();
309     let next_version = |krate: &Crate| -> String {
310         if krate.publish {
311             bump(&krate.version, patch)
312         } else {
313             krate.version.clone()
314         }
315     };
316 
317     let mut new_manifest = String::new();
318     let mut is_deps = false;
319     for line in contents.lines() {
320         let mut rewritten = false;
321         if !is_deps && line.starts_with("version =") {
322             if krate.publish {
323                 println!("  {} => {}", krate.version, next_version(krate));
324                 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
325                 rewritten = true;
326             }
327         }
328 
329         is_deps = if line.starts_with("[") {
330             line.contains("dependencies")
331         } else {
332             is_deps
333         };
334 
335         for other in crates {
336             // If `other` isn't a published crate then it's not going to get a
337             // bumped version so we don't need to update anything in the
338             // manifest.
339             if !other.publish {
340                 continue;
341             }
342             if !is_deps
343                 || (!line.starts_with(&format!("{} ", other.name))
344                     && !(line.contains(&format!("package = '{}'", other.name))
345                         || line.contains(&format!("package = \"{}\"", other.name))))
346             {
347                 continue;
348             }
349             if !line.contains(&other.version) {
350                 if !line.contains("version =") || !krate.publish {
351                     continue;
352                 }
353                 panic!(
354                     "{:?} has a dep on {} but doesn't list version {}",
355                     krate.manifest, other.name, other.version
356                 );
357             }
358             if krate.publish {
359                 if PUBLIC_CRATES.contains(&other.name.as_str()) {
360                     assert!(
361                         !line.contains("\"="),
362                         "{} should not have an exact version requirement on {}",
363                         krate.name,
364                         other.name
365                     );
366                 } else {
367                     assert!(
368                         line.contains("\"="),
369                         "{} should have an exact version requirement on {}",
370                         krate.name,
371                         other.name
372                     );
373                 }
374             }
375             rewritten = true;
376             new_manifest.push_str(&line.replace(&other.version, &next_version(other)));
377             break;
378         }
379         if !rewritten {
380             new_manifest.push_str(line);
381         }
382         new_manifest.push_str("\n");
383     }
384     fs::write(&krate.manifest, new_manifest).unwrap();
385 }
386 
387 fn update_capi_version() {
388     let version = read_crate(None, "./Cargo.toml".as_ref()).version;
389 
390     let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
391     let major = iter.next().expect("major version");
392     let minor = iter.next().expect("minor version");
393     let patch = iter.next().expect("patch version");
394 
395     let mut new_header = String::new();
396     let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
397     for line in contents.lines() {
398         if line.starts_with("#define WASMTIME_VERSION \"") {
399             new_header.push_str(&format!("#define WASMTIME_VERSION \"{version}\""));
400         } else if line.starts_with("#define WASMTIME_VERSION_MAJOR") {
401             new_header.push_str(&format!("#define WASMTIME_VERSION_MAJOR {major}"));
402         } else if line.starts_with("#define WASMTIME_VERSION_MINOR") {
403             new_header.push_str(&format!("#define WASMTIME_VERSION_MINOR {minor}"));
404         } else if line.starts_with("#define WASMTIME_VERSION_PATCH") {
405             new_header.push_str(&format!("#define WASMTIME_VERSION_PATCH {patch}"));
406         } else {
407             new_header.push_str(line);
408         }
409         new_header.push_str("\n");
410     }
411 
412     fs::write(&C_HEADER_PATH, new_header).unwrap();
413 }
414 
415 /// Performs a major version bump increment on the semver version `version`.
416 ///
417 /// This function will perform a semver-major-version bump on the `version`
418 /// specified. This is used to calculate the next version of a crate in this
419 /// repository since we're currently making major version bumps for all our
420 /// releases. This may end up getting tweaked as we stabilize crates and start
421 /// doing more minor/patch releases, but for now this should do the trick.
422 fn bump(version: &str, patch_bump: bool) -> String {
423     let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
424     let major = iter.next().expect("major version");
425     let minor = iter.next().expect("minor version");
426     let patch = iter.next().expect("patch version");
427 
428     if patch_bump {
429         return format!("{}.{}.{}", major, minor, patch + 1);
430     }
431     if major != 0 {
432         format!("{}.0.0", major + 1)
433     } else if minor != 0 {
434         format!("0.{}.0", minor + 1)
435     } else {
436         format!("0.0.{}", patch + 1)
437     }
438 }
439 
440 fn publish(krate: &Crate) -> bool {
441     if !krate.publish {
442         return true;
443     }
444 
445     // First make sure the crate isn't already published at this version. This
446     // script may be re-run and there's no need to re-attempt previous work.
447     let Some(output) = curl(&format!(
448         "https://crates.io/api/v1/crates/{}/versions",
449         krate.name
450     )) else {
451         return false;
452     };
453     if output.contains(&format!("\"num\":\"{}\"", krate.version)) {
454         println!(
455             "skip publish {} because {} is already published",
456             krate.name, krate.version,
457         );
458         return true;
459     }
460 
461     let status = cmd_status(
462         Command::new("cargo")
463             .arg("publish")
464             .current_dir(krate.manifest.parent().unwrap())
465             .arg("--no-verify"),
466     );
467     if !status.success() {
468         println!("FAIL: failed to publish `{}`: {}", krate.name, status);
469         return false;
470     }
471 
472     true
473 }
474 
475 fn curl(url: &str) -> Option<String> {
476     let output = cmd_output(
477         Command::new("curl")
478             .arg("--user-agent")
479             .arg("bytecodealliance/wasmtime auto-publish script")
480             .arg(url),
481     );
482     if !output.status.success() {
483         println!("failed to curl: {}", output.status);
484         println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
485         return None;
486     }
487     Some(String::from_utf8_lossy(&output.stdout).into())
488 }
489 
490 // Verify the current tree is publish-able to crates.io. The intention here is
491 // that we'll run `cargo package` on everything which verifies the build as-if
492 // it were published to crates.io. This requires using an incrementally-built
493 // directory registry generated from `cargo vendor` because the versions
494 // referenced from `Cargo.toml` may not exist on crates.io.
495 fn verify(crates: &[Crate]) {
496     verify_capi();
497 
498     if Path::new(".cargo").exists() {
499         panic!(
500             "`.cargo` already exists on the file system, remove it and then run the script again"
501         );
502     }
503     if Path::new("vendor").exists() {
504         panic!(
505             "`vendor` already exists on the file system, remove it and then run the script again"
506         );
507     }
508 
509     let vendor = cmd_output(Command::new("cargo").arg("vendor").stderr(Stdio::inherit()));
510     assert!(vendor.status.success());
511 
512     fs::create_dir_all(".cargo").unwrap();
513     fs::write(".cargo/config.toml", vendor.stdout).unwrap();
514 
515     for krate in crates {
516         if !krate.publish {
517             continue;
518         }
519         verify_and_vendor(&krate);
520     }
521 
522     fn verify_and_vendor(krate: &Crate) {
523         verify_crates_io(krate);
524 
525         let mut cmd = Command::new("cargo");
526         cmd.arg("package")
527             .arg("--manifest-path")
528             .arg(&krate.manifest)
529             .env("CARGO_TARGET_DIR", "./target");
530         if krate.name.contains("wasi-nn") {
531             cmd.arg("--no-verify");
532         }
533         run_cmd(&mut cmd);
534         run_cmd(
535             Command::new("tar")
536                 .arg("xf")
537                 .arg(format!(
538                     "../target/package/{}-{}.crate",
539                     krate.name, krate.version
540                 ))
541                 .current_dir("./vendor"),
542         );
543         fs::write(
544             format!(
545                 "./vendor/{}-{}/.cargo-checksum.json",
546                 krate.name, krate.version
547             ),
548             "{\"files\":{}}",
549         )
550         .unwrap();
551     }
552 
553     fn verify_capi() {
554         let version = read_crate(None, "./Cargo.toml".as_ref()).version;
555 
556         let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
557         let major = iter.next().expect("major version");
558         let minor = iter.next().expect("minor version");
559         let patch = iter.next().expect("patch version");
560 
561         let mut count = 0;
562         let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
563         for line in contents.lines() {
564             if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) {
565                 count += 1;
566             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) {
567                 count += 1;
568             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) {
569                 count += 1;
570             } else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) {
571                 count += 1;
572             }
573         }
574 
575         assert!(
576             count == 4,
577             "invalid version macros in {}, should match \"{}\"",
578             C_HEADER_PATH,
579             version
580         );
581     }
582 
583     fn verify_crates_io(krate: &Crate) {
584         let name = &krate.name;
585         let Some(owners) = curl(&format!("https://crates.io/api/v1/crates/{name}/owners")) else {
586             panic!(
587                 "
588 failed to get owners for {name}
589 
590 If this crate does not exist on crates.io yet please visit
591 
592   https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
593 
594 and follow the instructions there
595 ",
596                 name = name,
597             );
598         };
599 
600         // This is the id of the `wasmtime-publish` user on crates.io
601         if !owners.contains("\"id\":73222,") {
602             panic!(
603                 "
604 crate {name} is not owned by wasmtime-publish, please visit:
605 
606   https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
607 
608 and follow the instructions there
609 ",
610                 name = name,
611             );
612         }
613 
614         // TODO: waiting for trusted publishing to be proven to work before
615         // activating this.
616         if false && owners.split("\"id\"").count() != 2 {
617             panic!(
618                 "
619 crate {name} is not exclusively owned by wasmtime-publish, please visit:
620 
621   https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
622 
623 and follow the instructions there
624 ",
625                 name = name,
626             );
627         }
628     }
629 }
630