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