Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/scripts/publish.rs
3052 views
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
"wasmtime-internal-core",
21
"cranelift-bitset",
22
// pulley
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-control",
35
"cranelift-codegen",
36
"cranelift-reader",
37
"cranelift-serde",
38
"cranelift-module",
39
"cranelift-frontend",
40
"cranelift-native",
41
"cranelift-object",
42
"cranelift-interpreter",
43
"wasmtime-internal-component-util",
44
"wasmtime-environ",
45
"wasmtime-internal-jit-icache-coherence",
46
// Wasmtime unwinder, used by both `cranelift-jit` (optionally) and filetests, and by Wasmtime.
47
"wasmtime-internal-unwinder",
48
// Cranelift crates that use Wasmtime unwinder.
49
"cranelift-jit",
50
"cranelift",
51
// wiggle
52
"wiggle-generate",
53
"wiggle-macro",
54
// wasmtime
55
"wasmtime-internal-versioned-export-macros",
56
"wasmtime-internal-wit-bindgen",
57
"wasmtime-internal-component-macro",
58
"wasmtime-internal-jit-debug",
59
"wasmtime-internal-fiber",
60
"wasmtime-internal-wmemcheck",
61
"wasmtime-internal-cranelift",
62
"wasmtime-internal-cache",
63
"winch-codegen",
64
"wasmtime-internal-winch",
65
"wasmtime",
66
// wasi-common/wiggle
67
"wiggle",
68
"wasi-common",
69
// other misc wasmtime crates
70
"wasmtime-wasi-io",
71
"wasmtime-wasi",
72
"wasmtime-wasi-http",
73
"wasmtime-wasi-nn",
74
"wasmtime-wasi-config",
75
"wasmtime-wasi-keyvalue",
76
"wasmtime-wasi-threads",
77
"wasmtime-wasi-tls",
78
"wasmtime-wasi-tls-nativetls",
79
"wasmtime-wasi-tls-openssl",
80
"wasmtime-wast",
81
"wasmtime-internal-c-api-macros",
82
"wasmtime-c-api-impl",
83
"wasmtime-wizer",
84
"wasmtime-cli-flags",
85
"wasmtime-internal-explorer",
86
"wasmtime-internal-debugger",
87
"wasmtime-cli",
88
];
89
90
// Anything **not** mentioned in this array is required to have an `=a.b.c`
91
// dependency requirement on it to enable breaking api changes even in "patch"
92
// releases since everything not mentioned here is just an organizational detail
93
// that no one else should rely on.
94
const PUBLIC_CRATES: &[&str] = &[
95
// These are actually public crates which we cannot break the API of in
96
// patch releases.
97
"wasmtime",
98
"wasmtime-wasi-io",
99
"wasmtime-wasi",
100
"wasmtime-wasi-tls",
101
"wasmtime-wasi-tls-nativetls",
102
"wasmtime-wasi-tls-openssl",
103
"wasmtime-wasi-http",
104
"wasmtime-wasi-nn",
105
"wasmtime-wasi-config",
106
"wasmtime-wasi-keyvalue",
107
"wasmtime-wasi-threads",
108
"wasmtime-cli",
109
"wasmtime-wizer",
110
// All cranelift crates are considered "public" in that they can't have
111
// breaking API changes in patch releases.
112
"cranelift-srcgen",
113
"cranelift-assembler-x64-meta",
114
"cranelift-assembler-x64",
115
"cranelift-entity",
116
"cranelift-bforest",
117
"cranelift-bitset",
118
"cranelift-codegen-shared",
119
"cranelift-codegen-meta",
120
"cranelift-egraph",
121
"cranelift-control",
122
"cranelift-codegen",
123
"cranelift-reader",
124
"cranelift-serde",
125
"cranelift-module",
126
"cranelift-frontend",
127
"cranelift-native",
128
"cranelift-object",
129
"cranelift-interpreter",
130
"cranelift",
131
"cranelift-jit",
132
// This is a dependency of cranelift crates and as a result can't break in
133
// patch releases as well
134
"wasmtime-types",
135
];
136
137
const C_HEADER_PATH: &str = "./crates/c-api/include/wasmtime.h";
138
139
struct Workspace {
140
version: String,
141
}
142
143
struct Crate {
144
manifest: PathBuf,
145
name: String,
146
version: String,
147
publish: bool,
148
}
149
150
fn main() {
151
let mut crates = Vec::new();
152
let root = read_crate(None, "./Cargo.toml".as_ref());
153
let ws = Workspace {
154
version: root.version.clone(),
155
};
156
crates.push(root);
157
find_crates("crates".as_ref(), &ws, &mut crates);
158
find_crates("cranelift".as_ref(), &ws, &mut crates);
159
find_crates("pulley".as_ref(), &ws, &mut crates);
160
find_crates("winch".as_ref(), &ws, &mut crates);
161
162
let pos = CRATES_TO_PUBLISH
163
.iter()
164
.enumerate()
165
.map(|(i, c)| (*c, i))
166
.collect::<HashMap<_, _>>();
167
crates.sort_by_key(|krate| pos.get(&krate.name[..]));
168
169
match &env::args().nth(1).expect("must have one argument")[..] {
170
name @ "bump" | name @ "bump-patch" => {
171
for krate in crates.iter() {
172
bump_version(&krate, &crates, name == "bump-patch");
173
}
174
// update C API version in wasmtime.h
175
update_capi_version();
176
// update the lock file
177
run_cmd(Command::new("cargo").arg("fetch"));
178
}
179
180
"publish" => {
181
// We have so many crates to publish we're frequently either
182
// rate-limited or we run into issues where crates can't publish
183
// successfully because they're waiting on the index entries of
184
// previously-published crates to propagate. This means we try to
185
// publish in a loop and we remove crates once they're successfully
186
// published. Failed-to-publish crates get enqueued for another try
187
// later on.
188
for _ in 0..10 {
189
crates.retain(|krate| !publish(krate));
190
191
if crates.is_empty() {
192
break;
193
}
194
195
println!(
196
"{} crates failed to publish, waiting for a bit to retry",
197
crates.len(),
198
);
199
thread::sleep(Duration::from_secs(40));
200
}
201
202
assert!(crates.is_empty(), "failed to publish all crates");
203
204
println!("");
205
println!("===================================================================");
206
println!("");
207
println!("Don't forget to push a git tag for this release!");
208
println!("");
209
println!(" $ git tag vX.Y.Z");
210
println!(" $ git push [email protected]:bytecodealliance/wasmtime.git vX.Y.Z");
211
}
212
213
"verify" => {
214
verify(&crates);
215
}
216
217
s => panic!("unknown command: {}", s),
218
}
219
}
220
221
fn cmd_output(cmd: &mut Command) -> Output {
222
eprintln!("Running: `{:?}`", cmd);
223
match cmd.output() {
224
Ok(o) => o,
225
Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
226
}
227
}
228
229
fn cmd_status(cmd: &mut Command) -> ExitStatus {
230
eprintln!("Running: `{:?}`", cmd);
231
match cmd.status() {
232
Ok(s) => s,
233
Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
234
}
235
}
236
237
fn run_cmd(cmd: &mut Command) {
238
let status = cmd_status(cmd);
239
assert!(
240
status.success(),
241
"Command `{:?}` exited with failure status: {}",
242
cmd,
243
status
244
);
245
}
246
247
fn find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>) {
248
if dir.join("Cargo.toml").exists() {
249
let krate = read_crate(Some(ws), &dir.join("Cargo.toml"));
250
dst.push(krate);
251
}
252
253
for entry in dir.read_dir().unwrap() {
254
let entry = entry.unwrap();
255
if entry.file_type().unwrap().is_dir() {
256
find_crates(&entry.path(), ws, dst);
257
}
258
}
259
}
260
261
fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate {
262
let mut name = None;
263
let mut version = None;
264
let mut publish = true;
265
for line in fs::read_to_string(manifest).unwrap().lines() {
266
if name.is_none() && line.starts_with("name = \"") {
267
name = Some(
268
line.replace("name = \"", "")
269
.replace("\"", "")
270
.trim()
271
.to_string(),
272
);
273
}
274
if version.is_none() && line.starts_with("version = \"") {
275
version = Some(
276
line.replace("version = \"", "")
277
.replace("\"", "")
278
.trim()
279
.to_string(),
280
);
281
}
282
if let Some(ws) = ws {
283
if version.is_none() && line.starts_with("version.workspace = true") {
284
version = Some(ws.version.clone());
285
}
286
}
287
if line.starts_with("publish = false") {
288
publish = false;
289
}
290
}
291
let name = name.unwrap();
292
let version = version.unwrap();
293
assert!(
294
!publish || CRATES_TO_PUBLISH.contains(&&name[..]),
295
"a crate must either be listed in `CRATES_TO_PUBLISH` or have `publish = false` \
296
in its `Cargo.toml`"
297
);
298
Crate {
299
manifest: manifest.to_path_buf(),
300
name,
301
version,
302
publish,
303
}
304
}
305
306
fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
307
println!("bumping `{}`...", krate.name);
308
let contents = fs::read_to_string(&krate.manifest).unwrap();
309
let next_version = |krate: &Crate| -> String {
310
if krate.publish {
311
bump(&krate.version, patch)
312
} else {
313
krate.version.clone()
314
}
315
};
316
317
let mut new_manifest = String::new();
318
let mut is_deps = false;
319
for line in contents.lines() {
320
let mut rewritten = false;
321
if !is_deps && line.starts_with("version =") {
322
if krate.publish {
323
println!(" {} => {}", krate.version, next_version(krate));
324
new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
325
rewritten = true;
326
}
327
}
328
329
is_deps = if line.starts_with("[") {
330
line.contains("dependencies")
331
} else {
332
is_deps
333
};
334
335
for other in crates {
336
// If `other` isn't a published crate then it's not going to get a
337
// bumped version so we don't need to update anything in the
338
// manifest.
339
if !other.publish {
340
continue;
341
}
342
if !is_deps
343
|| (!line.starts_with(&format!("{} ", other.name))
344
&& !(line.contains(&format!("package = '{}'", 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 !krate.publish {
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 Some(output) = curl(&format!(
448
"https://crates.io/api/v1/crates/{}/versions",
449
krate.name
450
)) else {
451
return false;
452
};
453
if output.contains(&format!("\"num\":\"{}\"", krate.version)) {
454
println!(
455
"skip publish {} because {} is already published",
456
krate.name, krate.version,
457
);
458
return true;
459
}
460
461
let status = cmd_status(
462
Command::new("cargo")
463
.arg("publish")
464
.current_dir(krate.manifest.parent().unwrap())
465
.arg("--no-verify"),
466
);
467
if !status.success() {
468
println!("FAIL: failed to publish `{}`: {}", krate.name, status);
469
return false;
470
}
471
472
true
473
}
474
475
fn curl(url: &str) -> Option<String> {
476
let output = cmd_output(
477
Command::new("curl")
478
.arg("--user-agent")
479
.arg("bytecodealliance/wasmtime auto-publish script")
480
.arg(url),
481
);
482
if !output.status.success() {
483
println!("failed to curl: {}", output.status);
484
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
485
return None;
486
}
487
Some(String::from_utf8_lossy(&output.stdout).into())
488
}
489
490
// Verify the current tree is publish-able to crates.io. The intention here is
491
// that we'll run `cargo package` on everything which verifies the build as-if
492
// it were published to crates.io. This requires using an incrementally-built
493
// directory registry generated from `cargo vendor` because the versions
494
// referenced from `Cargo.toml` may not exist on crates.io.
495
fn verify(crates: &[Crate]) {
496
verify_capi();
497
498
if Path::new(".cargo").exists() {
499
panic!(
500
"`.cargo` already exists on the file system, remove it and then run the script again"
501
);
502
}
503
if Path::new("vendor").exists() {
504
panic!(
505
"`vendor` already exists on the file system, remove it and then run the script again"
506
);
507
}
508
509
let vendor = cmd_output(Command::new("cargo").arg("vendor").stderr(Stdio::inherit()));
510
assert!(vendor.status.success());
511
512
fs::create_dir_all(".cargo").unwrap();
513
fs::write(".cargo/config.toml", vendor.stdout).unwrap();
514
515
for krate in crates {
516
if !krate.publish {
517
continue;
518
}
519
verify_and_vendor(&krate);
520
}
521
522
fn verify_and_vendor(krate: &Crate) {
523
verify_crates_io(krate);
524
525
let mut cmd = Command::new("cargo");
526
cmd.arg("package")
527
.arg("--manifest-path")
528
.arg(&krate.manifest)
529
.env("CARGO_TARGET_DIR", "./target");
530
if krate.name.contains("wasi-nn") {
531
cmd.arg("--no-verify");
532
}
533
run_cmd(&mut cmd);
534
run_cmd(
535
Command::new("tar")
536
.arg("xf")
537
.arg(format!(
538
"../target/package/{}-{}.crate",
539
krate.name, krate.version
540
))
541
.current_dir("./vendor"),
542
);
543
fs::write(
544
format!(
545
"./vendor/{}-{}/.cargo-checksum.json",
546
krate.name, krate.version
547
),
548
"{\"files\":{}}",
549
)
550
.unwrap();
551
}
552
553
fn verify_capi() {
554
let version = read_crate(None, "./Cargo.toml".as_ref()).version;
555
556
let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
557
let major = iter.next().expect("major version");
558
let minor = iter.next().expect("minor version");
559
let patch = iter.next().expect("patch version");
560
561
let mut count = 0;
562
let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
563
for line in contents.lines() {
564
if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) {
565
count += 1;
566
} else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) {
567
count += 1;
568
} else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) {
569
count += 1;
570
} else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) {
571
count += 1;
572
}
573
}
574
575
assert!(
576
count == 4,
577
"invalid version macros in {}, should match \"{}\"",
578
C_HEADER_PATH,
579
version
580
);
581
}
582
583
fn verify_crates_io(krate: &Crate) {
584
let name = &krate.name;
585
let Some(owners) = curl(&format!("https://crates.io/api/v1/crates/{name}/owners")) else {
586
panic!(
587
"
588
failed to get owners for {name}
589
590
If this crate does not exist on crates.io yet please visit
591
592
https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
593
594
and follow the instructions there
595
",
596
name = name,
597
);
598
};
599
600
// This is the id of the `wasmtime-publish` user on crates.io
601
if !owners.contains("\"id\":73222,") {
602
panic!(
603
"
604
crate {name} is not owned by wasmtime-publish, please visit:
605
606
https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
607
608
and follow the instructions there
609
",
610
name = name,
611
);
612
}
613
614
// TODO: waiting for trusted publishing to be proven to work before
615
// activating this.
616
if false && owners.split("\"id\"").count() != 2 {
617
panic!(
618
"
619
crate {name} is not exclusively owned by wasmtime-publish, please visit:
620
621
https://docs.wasmtime.dev/contributing-coding-guidelines.html#adding-crates
622
623
and follow the instructions there
624
",
625
name = name,
626
);
627
}
628
}
629
}
630
631