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