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