xref: /wasmtime-44.0.1/scripts/publish.rs (revision d3087487)
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, 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     // cranelift
21     "cranelift-isle",
22     "cranelift-entity",
23     "wasmtime-types",
24     "cranelift-bforest",
25     "cranelift-codegen-shared",
26     "cranelift-codegen-meta",
27     "cranelift-codegen",
28     "cranelift-reader",
29     "cranelift-serde",
30     "cranelift-module",
31     "cranelift-preopt",
32     "cranelift-frontend",
33     "cranelift-wasm",
34     "cranelift-native",
35     "cranelift-object",
36     "cranelift-interpreter",
37     "cranelift",
38     "cranelift-jit",
39     // wiggle
40     "wiggle-generate",
41     "wiggle-macro",
42     // wasmtime
43     "wasmtime-jit-debug",
44     "wasmtime-fiber",
45     "wasmtime-environ",
46     "wasmtime-runtime",
47     "wasmtime-cranelift",
48     "wasmtime-jit",
49     "wasmtime-cache",
50     "wasmtime",
51     // wasi-common/wiggle
52     "wiggle",
53     "wasi-common",
54     "wasi-cap-std-sync",
55     "wasi-tokio",
56     // other misc wasmtime crates
57     "wasmtime-wasi",
58     "wasmtime-wasi-nn",
59     "wasmtime-wasi-crypto",
60     "wasmtime-wast",
61     "wasmtime-cli-flags",
62     "wasmtime-cli",
63 ];
64 
65 // Anything **not** mentioned in this array is required to have an `=a.b.c`
66 // dependency requirement on it to enable breaking api changes even in "patch"
67 // releases since everything not mentioned here is just an organizational detail
68 // that no one else should rely on.
69 const PUBLIC_CRATES: &[&str] = &[
70     // just here to appease the script because these are submodules of this
71     // repository.
72     "wasi-crypto",
73     "witx",
74     // these are actually public crates which we cannot break the API of in
75     // patch releases.
76     "wasmtime",
77     "wasmtime-wasi",
78     "wasmtime-wasi-nn",
79     "wasmtime-wasi-crypto",
80     "wasmtime-cli",
81     // all cranelift crates are considered "public" in that they can't
82     // have breaking API changes in patch releases
83     "cranelift-entity",
84     "cranelift-bforest",
85     "cranelift-codegen-shared",
86     "cranelift-codegen-meta",
87     "cranelift-codegen",
88     "cranelift-reader",
89     "cranelift-serde",
90     "cranelift-module",
91     "cranelift-preopt",
92     "cranelift-frontend",
93     "cranelift-wasm",
94     "cranelift-native",
95     "cranelift-object",
96     "cranelift-interpreter",
97     "cranelift",
98     "cranelift-jit",
99     // This is a dependency of cranelift crates and as a result can't break in
100     // patch releases as well
101     "wasmtime-types",
102 ];
103 
104 struct Crate {
105     manifest: PathBuf,
106     name: String,
107     version: String,
108     publish: bool,
109 }
110 
111 fn main() {
112     let mut crates = Vec::new();
113     crates.push(read_crate("./Cargo.toml".as_ref()));
114     find_crates("crates".as_ref(), &mut crates);
115     find_crates("cranelift".as_ref(), &mut crates);
116 
117     let pos = CRATES_TO_PUBLISH
118         .iter()
119         .enumerate()
120         .map(|(i, c)| (*c, i))
121         .collect::<HashMap<_, _>>();
122     crates.sort_by_key(|krate| pos.get(&krate.name[..]));
123 
124     match &env::args().nth(1).expect("must have one argument")[..] {
125         name @ "bump" | name @ "bump-patch" => {
126             for krate in crates.iter() {
127                 bump_version(&krate, &crates, name == "bump-patch");
128             }
129             // update the lock file
130             assert!(Command::new("cargo")
131                 .arg("fetch")
132                 .status()
133                 .unwrap()
134                 .success());
135         }
136 
137         "publish" => {
138             // We have so many crates to publish we're frequently either
139             // rate-limited or we run into issues where crates can't publish
140             // successfully because they're waiting on the index entries of
141             // previously-published crates to propagate. This means we try to
142             // publish in a loop and we remove crates once they're successfully
143             // published. Failed-to-publish crates get enqueued for another try
144             // later on.
145             for _ in 0..5 {
146                 crates.retain(|krate| !publish(krate));
147 
148                 if crates.is_empty() {
149                     break;
150                 }
151 
152                 println!(
153                     "{} crates failed to publish, waiting for a bit to retry",
154                     crates.len(),
155                 );
156                 thread::sleep(Duration::from_secs(20));
157             }
158 
159             assert!(crates.is_empty(), "failed to publish all crates");
160 
161             println!("");
162             println!("===================================================================");
163             println!("");
164             println!("Don't forget to push a git tag for this release!");
165             println!("");
166             println!("    $ git tag vX.Y.Z");
167             println!("    $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z");
168         }
169 
170         "verify" => {
171             verify(&crates);
172         }
173 
174         s => panic!("unknown command: {}", s),
175     }
176 }
177 
178 fn find_crates(dir: &Path, dst: &mut Vec<Crate>) {
179     if dir.join("Cargo.toml").exists() {
180         let krate = read_crate(&dir.join("Cargo.toml"));
181         if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) {
182             dst.push(krate);
183         } else {
184             panic!("failed to find {:?} in whitelist or blacklist", krate.name);
185         }
186     }
187 
188     for entry in dir.read_dir().unwrap() {
189         let entry = entry.unwrap();
190         if entry.file_type().unwrap().is_dir() {
191             find_crates(&entry.path(), dst);
192         }
193     }
194 }
195 
196 fn read_crate(manifest: &Path) -> Crate {
197     let mut name = None;
198     let mut version = None;
199     let mut publish = true;
200     for line in fs::read_to_string(manifest).unwrap().lines() {
201         if name.is_none() && line.starts_with("name = \"") {
202             name = Some(
203                 line.replace("name = \"", "")
204                     .replace("\"", "")
205                     .trim()
206                     .to_string(),
207             );
208         }
209         if version.is_none() && line.starts_with("version = \"") {
210             version = Some(
211                 line.replace("version = \"", "")
212                     .replace("\"", "")
213                     .trim()
214                     .to_string(),
215             );
216         }
217         if line.starts_with("publish = false") {
218             publish = false;
219         }
220     }
221     let name = name.unwrap();
222     let version = version.unwrap();
223     if ["witx", "witx-cli", "wasi-crypto"].contains(&&name[..]) {
224         publish = false;
225     }
226     Crate {
227         manifest: manifest.to_path_buf(),
228         name,
229         version,
230         publish,
231     }
232 }
233 
234 fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
235     let contents = fs::read_to_string(&krate.manifest).unwrap();
236     let next_version = |krate: &Crate| -> String {
237         if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
238             bump(&krate.version, patch)
239         } else {
240             krate.version.clone()
241         }
242     };
243 
244     let mut new_manifest = String::new();
245     let mut is_deps = false;
246     for line in contents.lines() {
247         let mut rewritten = false;
248         if !is_deps && line.starts_with("version =") {
249             if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
250                 println!(
251                     "bump `{}` {} => {}",
252                     krate.name,
253                     krate.version,
254                     next_version(krate),
255                 );
256                 new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
257                 rewritten = true;
258             }
259         }
260 
261         is_deps = if line.starts_with("[") {
262             line.contains("dependencies")
263         } else {
264             is_deps
265         };
266 
267         for other in crates {
268             // If `other` isn't a published crate then it's not going to get a
269             // bumped version so we don't need to update anything in the
270             // manifest.
271             if !other.publish {
272                 continue;
273             }
274             if !is_deps || !line.starts_with(&format!("{} ", other.name)) {
275                 continue;
276             }
277             if !line.contains(&other.version) {
278                 if !line.contains("version =") || !krate.publish {
279                     continue;
280                 }
281                 panic!(
282                     "{:?} has a dep on {} but doesn't list version {}",
283                     krate.manifest, other.name, other.version
284                 );
285             }
286             if krate.publish {
287                 if PUBLIC_CRATES.contains(&other.name.as_str()) {
288                     assert!(
289                         !line.contains("\"="),
290                         "{} should not have an exact version requirement on {}",
291                         krate.name,
292                         other.name
293                     );
294                 } else {
295                     assert!(
296                         line.contains("\"="),
297                         "{} should have an exact version requirement on {}",
298                         krate.name,
299                         other.name
300                     );
301                 }
302             }
303             rewritten = true;
304             new_manifest.push_str(&line.replace(&other.version, &next_version(other)));
305             break;
306         }
307         if !rewritten {
308             new_manifest.push_str(line);
309         }
310         new_manifest.push_str("\n");
311     }
312     fs::write(&krate.manifest, new_manifest).unwrap();
313 }
314 
315 /// Performs a major version bump increment on the semver version `version`.
316 ///
317 /// This function will perform a semver-major-version bump on the `version`
318 /// specified. This is used to calculate the next version of a crate in this
319 /// repository since we're currently making major version bumps for all our
320 /// releases. This may end up getting tweaked as we stabilize crates and start
321 /// doing more minor/patch releases, but for now this should do the trick.
322 fn bump(version: &str, patch_bump: bool) -> String {
323     let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
324     let major = iter.next().expect("major version");
325     let minor = iter.next().expect("minor version");
326     let patch = iter.next().expect("patch version");
327 
328     if patch_bump {
329         return format!("{}.{}.{}", major, minor, patch + 1);
330     }
331     if major != 0 {
332         format!("{}.0.0", major + 1)
333     } else if minor != 0 {
334         format!("0.{}.0", minor + 1)
335     } else {
336         format!("0.0.{}", patch + 1)
337     }
338 }
339 
340 fn publish(krate: &Crate) -> bool {
341     if !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) {
342         return true;
343     }
344 
345     // First make sure the crate isn't already published at this version. This
346     // script may be re-run and there's no need to re-attempt previous work.
347     let output = Command::new("curl")
348         .arg(&format!("https://crates.io/api/v1/crates/{}", krate.name))
349         .output()
350         .expect("failed to invoke `curl`");
351     if output.status.success()
352         && String::from_utf8_lossy(&output.stdout)
353             .contains(&format!("\"newest_version\":\"{}\"", krate.version))
354     {
355         println!(
356             "skip publish {} because {} is latest version",
357             krate.name, krate.version,
358         );
359         return true;
360     }
361 
362     let status = Command::new("cargo")
363         .arg("publish")
364         .current_dir(krate.manifest.parent().unwrap())
365         .arg("--no-verify")
366         .status()
367         .expect("failed to run cargo");
368     if !status.success() {
369         println!("FAIL: failed to publish `{}`: {}", krate.name, status);
370         return false;
371     }
372 
373     // After we've published then make sure that the `wasmtime-publish` group is
374     // added to this crate for future publications. If it's already present
375     // though we can skip the `cargo owner` modification.
376     let output = Command::new("curl")
377         .arg(&format!(
378             "https://crates.io/api/v1/crates/{}/owners",
379             krate.name
380         ))
381         .output()
382         .expect("failed to invoke `curl`");
383     if output.status.success()
384         && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish")
385     {
386         println!(
387             "wasmtime-publish already listed as an owner of {}",
388             krate.name
389         );
390         return true;
391     }
392 
393     // Note that the status is ignored here. This fails most of the time because
394     // the owner is already set and present, so we only want to add this to
395     // crates which haven't previously been published.
396     let status = Command::new("cargo")
397         .arg("owner")
398         .arg("-a")
399         .arg("github:bytecodealliance:wasmtime-publish")
400         .arg(&krate.name)
401         .status()
402         .expect("failed to run cargo");
403     if !status.success() {
404         panic!(
405             "FAIL: failed to add wasmtime-publish as owner `{}`: {}",
406             krate.name, status
407         );
408     }
409 
410     true
411 }
412 
413 // Verify the current tree is publish-able to crates.io. The intention here is
414 // that we'll run `cargo package` on everything which verifies the build as-if
415 // it were published to crates.io. This requires using an incrementally-built
416 // directory registry generated from `cargo vendor` because the versions
417 // referenced from `Cargo.toml` may not exist on crates.io.
418 fn verify(crates: &[Crate]) {
419     drop(fs::remove_dir_all(".cargo"));
420     drop(fs::remove_dir_all("vendor"));
421     let vendor = Command::new("cargo")
422         .arg("vendor")
423         .stderr(Stdio::inherit())
424         .output()
425         .unwrap();
426     assert!(vendor.status.success());
427 
428     fs::create_dir_all(".cargo").unwrap();
429     fs::write(".cargo/config.toml", vendor.stdout).unwrap();
430 
431     // Vendor witx which wasn't vendored because it's a path dependency, but
432     // it'll need to be in our directory registry for crates that depend on it.
433     let witx = crates
434         .iter()
435         .find(|c| c.name == "witx" && c.manifest.iter().any(|p| p == "wasi-common"))
436         .unwrap();
437     verify_and_vendor(&witx);
438 
439     // Vendor wasi-crypto which is also a path dependency
440     let wasi_crypto = crates.iter().find(|c| c.name == "wasi-crypto").unwrap();
441     verify_and_vendor(&wasi_crypto);
442 
443     for krate in crates {
444         if !krate.publish {
445             continue;
446         }
447         verify_and_vendor(&krate);
448     }
449 
450     fn verify_and_vendor(krate: &Crate) {
451         let mut cmd = Command::new("cargo");
452         cmd.arg("package")
453             .arg("--manifest-path")
454             .arg(&krate.manifest)
455             .env("CARGO_TARGET_DIR", "./target");
456         if krate.name == "witx" || krate.name.contains("wasi-nn") {
457             cmd.arg("--no-verify");
458         }
459         let status = cmd.status().unwrap();
460         assert!(status.success(), "failed to verify {:?}", &krate.manifest);
461         let tar = Command::new("tar")
462             .arg("xf")
463             .arg(format!(
464                 "../target/package/{}-{}.crate",
465                 krate.name, krate.version
466             ))
467             .current_dir("./vendor")
468             .status()
469             .unwrap();
470         assert!(tar.success());
471         fs::write(
472             format!(
473                 "./vendor/{}-{}/.cargo-checksum.json",
474                 krate.name, krate.version
475             ),
476             "{\"files\":{}}",
477         )
478         .unwrap();
479     }
480 }
481