xref: /wasmtime-44.0.1/scripts/publish.rs (revision cfe17cb1)
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-asm-macros",
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-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-http",
103     "wasmtime-wasi-nn",
104     "wasmtime-wasi-config",
105     "wasmtime-wasi-keyvalue",
106     "wasmtime-wasi-threads",
107     "wasmtime-cli",
108     // All cranelift crates are considered "public" in that they can't have
109     // breaking API changes in patch releases.
110     "cranelift-srcgen",
111     "cranelift-assembler-x64-meta",
112     "cranelift-assembler-x64",
113     "cranelift-entity",
114     "cranelift-bforest",
115     "cranelift-bitset",
116     "cranelift-codegen-shared",
117     "cranelift-codegen-meta",
118     "cranelift-egraph",
119     "cranelift-control",
120     "cranelift-codegen",
121     "cranelift-reader",
122     "cranelift-serde",
123     "cranelift-module",
124     "cranelift-frontend",
125     "cranelift-native",
126     "cranelift-object",
127     "cranelift-interpreter",
128     "cranelift",
129     "cranelift-jit",
130     // This is a dependency of cranelift crates and as a result can't break in
131     // patch releases as well
132     "wasmtime-types",
133 ];
134 
135 const C_HEADER_PATH: &str = "./crates/c-api/include/wasmtime.h";
136 
137 struct Workspace {
138     version: String,
139 }
140 
141 struct Crate {
142     manifest: PathBuf,
143     name: String,
144     version: String,
145     publish: bool,
146 }
147 
148 fn main() {
149     let mut crates = Vec::new();
150     let root = read_crate(None, "./Cargo.toml".as_ref());
151     let ws = Workspace {
152         version: root.version.clone(),
153     };
154     crates.push(root);
155     find_crates("crates".as_ref(), &ws, &mut crates);
156     find_crates("cranelift".as_ref(), &ws, &mut crates);
157     find_crates("pulley".as_ref(), &ws, &mut crates);
158     find_crates("winch".as_ref(), &ws, &mut crates);
159 
160     let pos = CRATES_TO_PUBLISH
161         .iter()
162         .enumerate()
163         .map(|(i, c)| (*c, i))
164         .collect::<HashMap<_, _>>();
165     crates.sort_by_key(|krate| pos.get(&krate.name[..]));
166 
167     match &env::args().nth(1).expect("must have one argument")[..] {
168         name @ "bump" | name @ "bump-patch" => {
169             for krate in crates.iter() {
170                 bump_version(&krate, &crates, name == "bump-patch");
171             }
172             // update C API version in wasmtime.h
173             update_capi_version();
174             // update the lock file
175             run_cmd(Command::new("cargo").arg("fetch"));
176         }
177 
178         "publish" => {
179             // We have so many crates to publish we're frequently either
180             // rate-limited or we run into issues where crates can't publish
181             // successfully because they're waiting on the index entries of
182             // previously-published crates to propagate. This means we try to
183             // publish in a loop and we remove crates once they're successfully
184             // published. Failed-to-publish crates get enqueued for another try
185             // later on.
186             for _ in 0..10 {
187                 crates.retain(|krate| !publish(krate));
188 
189                 if crates.is_empty() {
190                     break;
191                 }
192 
193                 println!(
194                     "{} crates failed to publish, waiting for a bit to retry",
195                     crates.len(),
196                 );
197                 thread::sleep(Duration::from_secs(40));
198             }
199 
200             assert!(crates.is_empty(), "failed to publish all crates");
201 
202             println!("");
203             println!("===================================================================");
204             println!("");
205             println!("Don't forget to push a git tag for this release!");
206             println!("");
207             println!("    $ git tag vX.Y.Z");
208             println!("    $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z");
209         }
210 
211         "verify" => {
212             verify(&crates);
213         }
214 
215         s => panic!("unknown command: {}", s),
216     }
217 }
218 
219 fn cmd_output(cmd: &mut Command) -> Output {
220     eprintln!("Running: `{:?}`", cmd);
221     match cmd.output() {
222         Ok(o) => o,
223         Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
224     }
225 }
226 
227 fn cmd_status(cmd: &mut Command) -> ExitStatus {
228     eprintln!("Running: `{:?}`", cmd);
229     match cmd.status() {
230         Ok(s) => s,
231         Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
232     }
233 }
234 
235 fn run_cmd(cmd: &mut Command) {
236     let status = cmd_status(cmd);
237     assert!(
238         status.success(),
239         "Command `{:?}` exited with failure status: {}",
240         cmd,
241         status
242     );
243 }
244 
245 fn find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>) {
246     if dir.join("Cargo.toml").exists() {
247         let krate = read_crate(Some(ws), &dir.join("Cargo.toml"));
248         if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) {
249             dst.push(krate);
250         } else {
251             panic!("failed to find {:?} in whitelist or blacklist", krate.name);
252         }
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     Crate {
296         manifest: manifest.to_path_buf(),
297         name,
298         version,
299         publish,
300     }
301 }
302 
303 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
304     let contents = fs::read_to_string(&krate.manifest).unwrap();
305     let next_version = |krate: &Crate| -> String {
306         if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
307             bump(&krate.version, patch)
308         } else {
309             krate.version.clone()
310         }
311     };
312 
313     let mut new_manifest = String::new();
314     let mut is_deps = false;
315     for line in contents.lines() {
316         let mut rewritten = false;
317         if !is_deps && line.starts_with("version =") {
318             if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
319                 println!(
320                     "bump `{}` {} => {}",
321                     krate.name,
322                     krate.version,
323                     next_version(krate),
324                 );
325                 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
326                 rewritten = true;
327             }
328         }
329 
330         is_deps = if line.starts_with("[") {
331             line.contains("dependencies")
332         } else {
333             is_deps
334         };
335 
336         for other in crates {
337             // If `other` isn't a published crate then it's not going to get a
338             // bumped version so we don't need to update anything in the
339             // manifest.
340             if !other.publish {
341                 continue;
342             }
343             if !is_deps
344                 || (!line.starts_with(&format!("{} ", 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 !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) {
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 output = cmd_output(Command::new("curl").arg(&format!(
448         "https://crates.io/api/v1/crates/{}/versions",
449         krate.name
450     )));
451     if output.status.success()
452         && String::from_utf8_lossy(&output.stdout)
453             .contains(&format!("\"num\":\"{}\"", krate.version))
454     {
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 output = cmd_output(Command::new("curl").arg(&format!(
477         "https://crates.io/api/v1/crates/{}/owners",
478         krate.name
479     )));
480     if output.status.success()
481         && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish")
482     {
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 // Verify the current tree is publish-able to crates.io. The intention here is
505 // that we'll run `cargo package` on everything which verifies the build as-if
506 // it were published to crates.io. This requires using an incrementally-built
507 // directory registry generated from `cargo vendor` because the versions
508 // referenced from `Cargo.toml` may not exist on crates.io.
509 fn verify(crates: &[Crate]) {
510     verify_capi();
511 
512     if Path::new(".cargo").exists() {
513         panic!(
514             "`.cargo` already exists on the file system, remove it and then run the script again"
515         );
516     }
517     if Path::new("vendor").exists() {
518         panic!(
519             "`vendor` already exists on the file system, remove it and then run the script again"
520         );
521     }
522 
523     let vendor = cmd_output(Command::new("cargo").arg("vendor").stderr(Stdio::inherit()));
524     assert!(vendor.status.success());
525 
526     fs::create_dir_all(".cargo").unwrap();
527     fs::write(".cargo/config.toml", vendor.stdout).unwrap();
528 
529     for krate in crates {
530         if !krate.publish {
531             continue;
532         }
533         verify_and_vendor(&krate);
534     }
535 
536     fn verify_and_vendor(krate: &Crate) {
537         let mut cmd = Command::new("cargo");
538         cmd.arg("package")
539             .arg("--manifest-path")
540             .arg(&krate.manifest)
541             .env("CARGO_TARGET_DIR", "./target");
542         if krate.name.contains("wasi-nn") {
543             cmd.arg("--no-verify");
544         }
545         run_cmd(&mut cmd);
546         run_cmd(
547             Command::new("tar")
548                 .arg("xf")
549                 .arg(format!(
550                     "../target/package/{}-{}.crate",
551                     krate.name, krate.version
552                 ))
553                 .current_dir("./vendor"),
554         );
555         fs::write(
556             format!(
557                 "./vendor/{}-{}/.cargo-checksum.json",
558                 krate.name, krate.version
559             ),
560             "{\"files\":{}}",
561         )
562         .unwrap();
563     }
564 
565     fn verify_capi() {
566         let version = read_crate(None, "./Cargo.toml".as_ref()).version;
567 
568         let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
569         let major = iter.next().expect("major version");
570         let minor = iter.next().expect("minor version");
571         let patch = iter.next().expect("patch version");
572 
573         let mut count = 0;
574         let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
575         for line in contents.lines() {
576             if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) {
577                 count += 1;
578             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) {
579                 count += 1;
580             } else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) {
581                 count += 1;
582             } else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) {
583                 count += 1;
584             }
585         }
586 
587         assert!(
588             count == 4,
589             "invalid version macros in {}, should match \"{}\"",
590             C_HEADER_PATH,
591             version
592         );
593     }
594 }
595