xref: /wasmtime-44.0.1/scripts/publish.rs (revision cd4657b2)
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-error",
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-wizer",
88     "wasmtime-cli-flags",
89     "wasmtime-internal-explorer",
90     "wasmtime-cli",
91 ];
92 
93 // Anything **not** mentioned in this array is required to have an `=a.b.c`
94 // dependency requirement on it to enable breaking api changes even in "patch"
95 // releases since everything not mentioned here is just an organizational detail
96 // that no one else should rely on.
97 const PUBLIC_CRATES: &[&str] = &[
98     // These are actually public crates which we cannot break the API of in
99     // patch releases.
100     "wasmtime",
101     "wasmtime-wasi-io",
102     "wasmtime-wasi",
103     "wasmtime-wasi-tls",
104     "wasmtime-wasi-tls-nativetls",
105     "wasmtime-wasi-http",
106     "wasmtime-wasi-nn",
107     "wasmtime-wasi-config",
108     "wasmtime-wasi-keyvalue",
109     "wasmtime-wasi-threads",
110     "wasmtime-cli",
111     "wasmtime-wizer",
112     // All cranelift crates are considered "public" in that they can't have
113     // breaking API changes in patch releases.
114     "cranelift-srcgen",
115     "cranelift-assembler-x64-meta",
116     "cranelift-assembler-x64",
117     "cranelift-entity",
118     "cranelift-bforest",
119     "cranelift-bitset",
120     "cranelift-codegen-shared",
121     "cranelift-codegen-meta",
122     "cranelift-egraph",
123     "cranelift-control",
124     "cranelift-codegen",
125     "cranelift-reader",
126     "cranelift-serde",
127     "cranelift-module",
128     "cranelift-frontend",
129     "cranelift-native",
130     "cranelift-object",
131     "cranelift-interpreter",
132     "cranelift",
133     "cranelift-jit",
134     // This is a dependency of cranelift crates and as a result can't break in
135     // patch releases as well
136     "wasmtime-types",
137 ];
138 
139 const C_HEADER_PATH: &str = "./crates/c-api/include/wasmtime.h";
140 
141 struct Workspace {
142     version: String,
143 }
144 
145 struct Crate {
146     manifest: PathBuf,
147     name: String,
148     version: String,
149     publish: bool,
150 }
151 
152 fn main() {
153     let mut crates = Vec::new();
154     let root = read_crate(None, "./Cargo.toml".as_ref());
155     let ws = Workspace {
156         version: root.version.clone(),
157     };
158     crates.push(root);
159     find_crates("crates".as_ref(), &ws, &mut crates);
160     find_crates("cranelift".as_ref(), &ws, &mut crates);
161     find_crates("pulley".as_ref(), &ws, &mut crates);
162     find_crates("winch".as_ref(), &ws, &mut crates);
163 
164     let pos = CRATES_TO_PUBLISH
165         .iter()
166         .enumerate()
167         .map(|(i, c)| (*c, i))
168         .collect::<HashMap<_, _>>();
169     crates.sort_by_key(|krate| pos.get(&krate.name[..]));
170 
171     match &env::args().nth(1).expect("must have one argument")[..] {
172         name @ "bump" | name @ "bump-patch" => {
173             for krate in crates.iter() {
174                 bump_version(&krate, &crates, name == "bump-patch");
175             }
176             // update C API version in wasmtime.h
177             update_capi_version();
178             // update the lock file
179             run_cmd(Command::new("cargo").arg("fetch"));
180         }
181 
182         "publish" => {
183             // We have so many crates to publish we're frequently either
184             // rate-limited or we run into issues where crates can't publish
185             // successfully because they're waiting on the index entries of
186             // previously-published crates to propagate. This means we try to
187             // publish in a loop and we remove crates once they're successfully
188             // published. Failed-to-publish crates get enqueued for another try
189             // later on.
190             for _ in 0..10 {
191                 crates.retain(|krate| !publish(krate));
192 
193                 if crates.is_empty() {
194                     break;
195                 }
196 
197                 println!(
198                     "{} crates failed to publish, waiting for a bit to retry",
199                     crates.len(),
200                 );
201                 thread::sleep(Duration::from_secs(40));
202             }
203 
204             assert!(crates.is_empty(), "failed to publish all crates");
205 
206             println!("");
207             println!("===================================================================");
208             println!("");
209             println!("Don't forget to push a git tag for this release!");
210             println!("");
211             println!("    $ git tag vX.Y.Z");
212             println!("    $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z");
213         }
214 
215         "verify" => {
216             verify(&crates);
217         }
218 
219         s => panic!("unknown command: {}", s),
220     }
221 }
222 
223 fn cmd_output(cmd: &mut Command) -> Output {
224     eprintln!("Running: `{:?}`", cmd);
225     match cmd.output() {
226         Ok(o) => o,
227         Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
228     }
229 }
230 
231 fn cmd_status(cmd: &mut Command) -> ExitStatus {
232     eprintln!("Running: `{:?}`", cmd);
233     match cmd.status() {
234         Ok(s) => s,
235         Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
236     }
237 }
238 
239 fn run_cmd(cmd: &mut Command) {
240     let status = cmd_status(cmd);
241     assert!(
242         status.success(),
243         "Command `{:?}` exited with failure status: {}",
244         cmd,
245         status
246     );
247 }
248 
249 fn find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>) {
250     if dir.join("Cargo.toml").exists() {
251         let krate = read_crate(Some(ws), &dir.join("Cargo.toml"));
252         dst.push(krate);
253     }
254 
255     for entry in dir.read_dir().unwrap() {
256         let entry = entry.unwrap();
257         if entry.file_type().unwrap().is_dir() {
258             find_crates(&entry.path(), ws, dst);
259         }
260     }
261 }
262 
263 fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate {
264     let mut name = None;
265     let mut version = None;
266     let mut publish = true;
267     for line in fs::read_to_string(manifest).unwrap().lines() {
268         if name.is_none() && line.starts_with("name = \"") {
269             name = Some(
270                 line.replace("name = \"", "")
271                     .replace("\"", "")
272                     .trim()
273                     .to_string(),
274             );
275         }
276         if version.is_none() && line.starts_with("version = \"") {
277             version = Some(
278                 line.replace("version = \"", "")
279                     .replace("\"", "")
280                     .trim()
281                     .to_string(),
282             );
283         }
284         if let Some(ws) = ws {
285             if version.is_none() && line.starts_with("version.workspace = true") {
286                 version = Some(ws.version.clone());
287             }
288         }
289         if line.starts_with("publish = false") {
290             publish = false;
291         }
292     }
293     let name = name.unwrap();
294     let version = version.unwrap();
295     assert!(
296         !publish || CRATES_TO_PUBLISH.contains(&&name[..]),
297         "a crate must either be listed in `CRATES_TO_PUBLISH` or have `publish = false` \
298          in its `Cargo.toml`"
299     );
300     Crate {
301         manifest: manifest.to_path_buf(),
302         name,
303         version,
304         publish,
305     }
306 }
307 
308 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
309     println!("bumping `{}`...", krate.name);
310     let contents = fs::read_to_string(&krate.manifest).unwrap();
311     let next_version = |krate: &Crate| -> String {
312         if krate.publish {
313             bump(&krate.version, patch)
314         } else {
315             krate.version.clone()
316         }
317     };
318 
319     let mut new_manifest = String::new();
320     let mut is_deps = false;
321     for line in contents.lines() {
322         let mut rewritten = false;
323         if !is_deps && line.starts_with("version =") {
324             if krate.publish {
325                 println!("  {} => {}", krate.version, next_version(krate));
326                 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
327                 rewritten = true;
328             }
329         }
330 
331         is_deps = if line.starts_with("[") {
332             line.contains("dependencies")
333         } else {
334             is_deps
335         };
336 
337         for other in crates {
338             // If `other` isn't a published crate then it's not going to get a
339             // bumped version so we don't need to update anything in the
340             // manifest.
341             if !other.publish {
342                 continue;
343             }
344             if !is_deps
345                 || (!line.starts_with(&format!("{} ", other.name))
346                     && !(line.contains(&format!("package = '{}'", 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 !krate.publish {
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         verify_crates_io(krate);
554 
555         let mut cmd = Command::new("cargo");
556         cmd.arg("package")
557             .arg("--manifest-path")
558             .arg(&krate.manifest)
559             .env("CARGO_TARGET_DIR", "./target");
560         if krate.name.contains("wasi-nn") {
561             cmd.arg("--no-verify");
562         }
563         run_cmd(&mut cmd);
564         run_cmd(
565             Command::new("tar")
566                 .arg("xf")
567                 .arg(format!(
568                     "../target/package/{}-{}.crate",
569                     krate.name, krate.version
570                 ))
571                 .current_dir("./vendor"),
572         );
573         fs::write(
574             format!(
575                 "./vendor/{}-{}/.cargo-checksum.json",
576                 krate.name, krate.version
577             ),
578             "{\"files\":{}}",
579         )
580         .unwrap();
581     }
582 
583     fn verify_capi() {
584         let version = read_crate(None, "./Cargo.toml".as_ref()).version;
585 
586         let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
587         let major = iter.next().expect("major version");
588         let minor = iter.next().expect("minor version");
589         let patch = iter.next().expect("patch version");
590 
591         let mut count = 0;
592         let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
593         for line in contents.lines() {
594             if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) {
595                 count += 1;
596             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) {
597                 count += 1;
598             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) {
599                 count += 1;
600             } else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) {
601                 count += 1;
602             }
603         }
604 
605         assert!(
606             count == 4,
607             "invalid version macros in {}, should match \"{}\"",
608             C_HEADER_PATH,
609             version
610         );
611     }
612 
613     fn verify_crates_io(krate: &Crate) {
614         let name = &krate.name;
615         let Some(owners) = curl(&format!("https://crates.io/api/v1/crates/{name}/owners")) else {
616             panic!("failed to get owners for {name}", name = name);
617         };
618 
619         let assert_owner = |owner: &str| {
620             let owner_json = format!("\"{owner}\"");
621             if !owners.contains(&owner_json) {
622                 panic!(
623                     "
624 crate {name} is not owned by {owner}, please run:
625 
626     cargo owner -a {owner} {name}
627 ",
628                     name = name
629                 );
630             }
631         };
632 
633         // the wasmtime-publish github user
634         assert_owner("wasmtime-publish");
635         // the BA team which can publish crates
636         assert_owner("github:bytecodealliance:wasmtime-publish");
637     }
638 }
639