Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/scripts/publish.rs
1685 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
// 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-wasi-tls-nativetls",
84
"wasmtime-wast",
85
"wasmtime-internal-c-api-macros",
86
"wasmtime-c-api-impl",
87
"wasmtime-cli-flags",
88
"wasmtime-internal-explorer",
89
"wasmtime-cli",
90
];
91
92
// Anything **not** mentioned in this array is required to have an `=a.b.c`
93
// dependency requirement on it to enable breaking api changes even in "patch"
94
// releases since everything not mentioned here is just an organizational detail
95
// that no one else should rely on.
96
const PUBLIC_CRATES: &[&str] = &[
97
// These are actually public crates which we cannot break the API of in
98
// patch releases.
99
"wasmtime",
100
"wasmtime-wasi-io",
101
"wasmtime-wasi",
102
"wasmtime-wasi-tls",
103
"wasmtime-wasi-tls-nativetls",
104
"wasmtime-wasi-http",
105
"wasmtime-wasi-nn",
106
"wasmtime-wasi-config",
107
"wasmtime-wasi-keyvalue",
108
"wasmtime-wasi-threads",
109
"wasmtime-cli",
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
if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) {
251
dst.push(krate);
252
} else {
253
panic!("failed to find {:?} in whitelist or blacklist", krate.name);
254
}
255
}
256
257
for entry in dir.read_dir().unwrap() {
258
let entry = entry.unwrap();
259
if entry.file_type().unwrap().is_dir() {
260
find_crates(&entry.path(), ws, dst);
261
}
262
}
263
}
264
265
fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate {
266
let mut name = None;
267
let mut version = None;
268
let mut publish = true;
269
for line in fs::read_to_string(manifest).unwrap().lines() {
270
if name.is_none() && line.starts_with("name = \"") {
271
name = Some(
272
line.replace("name = \"", "")
273
.replace("\"", "")
274
.trim()
275
.to_string(),
276
);
277
}
278
if version.is_none() && line.starts_with("version = \"") {
279
version = Some(
280
line.replace("version = \"", "")
281
.replace("\"", "")
282
.trim()
283
.to_string(),
284
);
285
}
286
if let Some(ws) = ws {
287
if version.is_none() && line.starts_with("version.workspace = true") {
288
version = Some(ws.version.clone());
289
}
290
}
291
if line.starts_with("publish = false") {
292
publish = false;
293
}
294
}
295
let name = name.unwrap();
296
let version = version.unwrap();
297
Crate {
298
manifest: manifest.to_path_buf(),
299
name,
300
version,
301
publish,
302
}
303
}
304
305
fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
306
let contents = fs::read_to_string(&krate.manifest).unwrap();
307
let next_version = |krate: &Crate| -> String {
308
if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
309
bump(&krate.version, patch)
310
} else {
311
krate.version.clone()
312
}
313
};
314
315
let mut new_manifest = String::new();
316
let mut is_deps = false;
317
for line in contents.lines() {
318
let mut rewritten = false;
319
if !is_deps && line.starts_with("version =") {
320
if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
321
println!(
322
"bump `{}` {} => {}",
323
krate.name,
324
krate.version,
325
next_version(krate),
326
);
327
new_manifest.push_str(&line.replace(&krate.version, &next_version(krate)));
328
rewritten = true;
329
}
330
}
331
332
is_deps = if line.starts_with("[") {
333
line.contains("dependencies")
334
} else {
335
is_deps
336
};
337
338
for other in crates {
339
// If `other` isn't a published crate then it's not going to get a
340
// bumped version so we don't need to update anything in the
341
// manifest.
342
if !other.publish {
343
continue;
344
}
345
if !is_deps
346
|| (!line.starts_with(&format!("{} ", other.name))
347
&& !line.contains(&format!("package = '{}'", other.name)))
348
{
349
continue;
350
}
351
if !line.contains(&other.version) {
352
if !line.contains("version =") || !krate.publish {
353
continue;
354
}
355
panic!(
356
"{:?} has a dep on {} but doesn't list version {}",
357
krate.manifest, other.name, other.version
358
);
359
}
360
if krate.publish {
361
if PUBLIC_CRATES.contains(&other.name.as_str()) {
362
assert!(
363
!line.contains("\"="),
364
"{} should not have an exact version requirement on {}",
365
krate.name,
366
other.name
367
);
368
} else {
369
assert!(
370
line.contains("\"="),
371
"{} should have an exact version requirement on {}",
372
krate.name,
373
other.name
374
);
375
}
376
}
377
rewritten = true;
378
new_manifest.push_str(&line.replace(&other.version, &next_version(other)));
379
break;
380
}
381
if !rewritten {
382
new_manifest.push_str(line);
383
}
384
new_manifest.push_str("\n");
385
}
386
fs::write(&krate.manifest, new_manifest).unwrap();
387
}
388
389
fn update_capi_version() {
390
let version = read_crate(None, "./Cargo.toml".as_ref()).version;
391
392
let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
393
let major = iter.next().expect("major version");
394
let minor = iter.next().expect("minor version");
395
let patch = iter.next().expect("patch version");
396
397
let mut new_header = String::new();
398
let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
399
for line in contents.lines() {
400
if line.starts_with("#define WASMTIME_VERSION \"") {
401
new_header.push_str(&format!("#define WASMTIME_VERSION \"{version}\""));
402
} else if line.starts_with("#define WASMTIME_VERSION_MAJOR") {
403
new_header.push_str(&format!("#define WASMTIME_VERSION_MAJOR {major}"));
404
} else if line.starts_with("#define WASMTIME_VERSION_MINOR") {
405
new_header.push_str(&format!("#define WASMTIME_VERSION_MINOR {minor}"));
406
} else if line.starts_with("#define WASMTIME_VERSION_PATCH") {
407
new_header.push_str(&format!("#define WASMTIME_VERSION_PATCH {patch}"));
408
} else {
409
new_header.push_str(line);
410
}
411
new_header.push_str("\n");
412
}
413
414
fs::write(&C_HEADER_PATH, new_header).unwrap();
415
}
416
417
/// Performs a major version bump increment on the semver version `version`.
418
///
419
/// This function will perform a semver-major-version bump on the `version`
420
/// specified. This is used to calculate the next version of a crate in this
421
/// repository since we're currently making major version bumps for all our
422
/// releases. This may end up getting tweaked as we stabilize crates and start
423
/// doing more minor/patch releases, but for now this should do the trick.
424
fn bump(version: &str, patch_bump: bool) -> String {
425
let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
426
let major = iter.next().expect("major version");
427
let minor = iter.next().expect("minor version");
428
let patch = iter.next().expect("patch version");
429
430
if patch_bump {
431
return format!("{}.{}.{}", major, minor, patch + 1);
432
}
433
if major != 0 {
434
format!("{}.0.0", major + 1)
435
} else if minor != 0 {
436
format!("0.{}.0", minor + 1)
437
} else {
438
format!("0.0.{}", patch + 1)
439
}
440
}
441
442
fn publish(krate: &Crate) -> bool {
443
if !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) {
444
return true;
445
}
446
447
// First make sure the crate isn't already published at this version. This
448
// script may be re-run and there's no need to re-attempt previous work.
449
let Some(output) = curl(&format!(
450
"https://crates.io/api/v1/crates/{}/versions",
451
krate.name
452
)) else {
453
return false;
454
};
455
if output.contains(&format!("\"num\":\"{}\"", krate.version)) {
456
println!(
457
"skip publish {} because {} is already published",
458
krate.name, krate.version,
459
);
460
return true;
461
}
462
463
let status = cmd_status(
464
Command::new("cargo")
465
.arg("publish")
466
.current_dir(krate.manifest.parent().unwrap())
467
.arg("--no-verify"),
468
);
469
if !status.success() {
470
println!("FAIL: failed to publish `{}`: {}", krate.name, status);
471
return false;
472
}
473
474
// After we've published then make sure that the `wasmtime-publish` group is
475
// added to this crate for future publications. If it's already present
476
// though we can skip the `cargo owner` modification.
477
let Some(output) = curl(&format!(
478
"https://crates.io/api/v1/crates/{}/owners",
479
krate.name
480
)) else {
481
return false;
482
};
483
if output.contains("wasmtime-publish") {
484
println!(
485
"wasmtime-publish already listed as an owner of {}",
486
krate.name
487
);
488
return true;
489
}
490
491
// Note that the status is ignored here. This fails most of the time because
492
// the owner is already set and present, so we only want to add this to
493
// crates which haven't previously been published.
494
run_cmd(
495
Command::new("cargo")
496
.arg("owner")
497
.arg("-a")
498
.arg("github:bytecodealliance:wasmtime-publish")
499
.arg(&krate.name),
500
);
501
502
true
503
}
504
505
fn curl(url: &str) -> Option<String> {
506
let output = cmd_output(
507
Command::new("curl")
508
.arg("--user-agent")
509
.arg("bytecodealliance/wasmtime auto-publish script")
510
.arg(url),
511
);
512
if !output.status.success() {
513
println!("failed to curl: {}", output.status);
514
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
515
return None;
516
}
517
Some(String::from_utf8_lossy(&output.stdout).into())
518
}
519
520
// Verify the current tree is publish-able to crates.io. The intention here is
521
// that we'll run `cargo package` on everything which verifies the build as-if
522
// it were published to crates.io. This requires using an incrementally-built
523
// directory registry generated from `cargo vendor` because the versions
524
// referenced from `Cargo.toml` may not exist on crates.io.
525
fn verify(crates: &[Crate]) {
526
verify_capi();
527
528
if Path::new(".cargo").exists() {
529
panic!(
530
"`.cargo` already exists on the file system, remove it and then run the script again"
531
);
532
}
533
if Path::new("vendor").exists() {
534
panic!(
535
"`vendor` already exists on the file system, remove it and then run the script again"
536
);
537
}
538
539
let vendor = cmd_output(Command::new("cargo").arg("vendor").stderr(Stdio::inherit()));
540
assert!(vendor.status.success());
541
542
fs::create_dir_all(".cargo").unwrap();
543
fs::write(".cargo/config.toml", vendor.stdout).unwrap();
544
545
for krate in crates {
546
if !krate.publish {
547
continue;
548
}
549
verify_and_vendor(&krate);
550
}
551
552
fn verify_and_vendor(krate: &Crate) {
553
verify_crates_io(krate);
554
555
let mut cmd = Command::new("cargo");
556
cmd.arg("package")
557
.arg("--manifest-path")
558
.arg(&krate.manifest)
559
.env("CARGO_TARGET_DIR", "./target");
560
if krate.name.contains("wasi-nn") {
561
cmd.arg("--no-verify");
562
}
563
run_cmd(&mut cmd);
564
run_cmd(
565
Command::new("tar")
566
.arg("xf")
567
.arg(format!(
568
"../target/package/{}-{}.crate",
569
krate.name, krate.version
570
))
571
.current_dir("./vendor"),
572
);
573
fs::write(
574
format!(
575
"./vendor/{}-{}/.cargo-checksum.json",
576
krate.name, krate.version
577
),
578
"{\"files\":{}}",
579
)
580
.unwrap();
581
}
582
583
fn verify_capi() {
584
let version = read_crate(None, "./Cargo.toml".as_ref()).version;
585
586
let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
587
let major = iter.next().expect("major version");
588
let minor = iter.next().expect("minor version");
589
let patch = iter.next().expect("patch version");
590
591
let mut count = 0;
592
let contents = fs::read_to_string(C_HEADER_PATH).unwrap();
593
for line in contents.lines() {
594
if line.starts_with(&format!("#define WASMTIME_VERSION \"{version}\"")) {
595
count += 1;
596
} else if line.starts_with(&format!("#define WASMTIME_VERSION_MAJOR {major}")) {
597
count += 1;
598
} else if line.starts_with(&format!("#define WASMTIME_VERSION_MINOR {minor}")) {
599
count += 1;
600
} else if line.starts_with(&format!("#define WASMTIME_VERSION_PATCH {patch}")) {
601
count += 1;
602
}
603
}
604
605
assert!(
606
count == 4,
607
"invalid version macros in {}, should match \"{}\"",
608
C_HEADER_PATH,
609
version
610
);
611
}
612
613
fn verify_crates_io(krate: &Crate) {
614
let name = &krate.name;
615
let Some(owners) = curl(&format!("https://crates.io/api/v1/crates/{name}/owners")) else {
616
panic!("failed to get owners for {name}", name = name);
617
};
618
619
let assert_owner = |owner: &str| {
620
let owner_json = format!("\"{owner}\"");
621
if !owners.contains(&owner_json) {
622
panic!(
623
"
624
crate {name} is not owned by {owner}, please run:
625
626
cargo owner -a {owner} {name}
627
",
628
name = name
629
);
630
}
631
};
632
633
// the wasmtime-publish github user
634
assert_owner("wasmtime-publish");
635
// the BA team which can publish crates
636
assert_owner("github:bytecodealliance:wasmtime-publish");
637
}
638
}
639
640