use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread;
use std::time::Duration;
const CRATES_TO_PUBLISH: &[&str] = &[
"wit-bindgen-core",
"wit-bindgen-c",
"wit-bindgen-cpp",
"wit-bindgen-rust",
"wit-bindgen-csharp",
"wit-bindgen-markdown",
"wit-bindgen-moonbit",
"wit-bindgen-go",
"wit-bindgen-rust-macro",
"wit-bindgen-rt",
"wit-bindgen",
"wit-bindgen-test",
"wit-bindgen-cli",
];
struct Workspace {
version: String,
}
struct Crate {
manifest: PathBuf,
name: String,
version: String,
publish: bool,
workspace_version: Option<String>,
}
fn main() {
let mut crates = Vec::new();
let root = read_crate(None, "./Cargo.toml".as_ref());
let ws = Workspace {
version: root.workspace_version.clone().unwrap(),
};
crates.push(root);
find_crates("crates".as_ref(), &ws, &mut crates);
let pos = CRATES_TO_PUBLISH
.iter()
.enumerate()
.map(|(i, c)| (*c, i))
.collect::<HashMap<_, _>>();
crates.sort_by_key(|krate| pos.get(&krate.name[..]));
match &env::args().nth(1).expect("must have one argument")[..] {
name @ "bump" | name @ "bump-patch" => {
for krate in crates.iter() {
bump_version(&krate, &crates, name == "bump-patch");
}
assert!(
Command::new("cargo")
.arg("fetch")
.status()
.unwrap()
.success()
);
}
"publish" => {
for _ in 0..10 {
crates.retain(|krate| !publish(krate));
if crates.is_empty() {
break;
}
println!(
"{} crates failed to publish, waiting for a bit to retry",
crates.len(),
);
thread::sleep(Duration::from_secs(40));
}
assert!(crates.is_empty(), "failed to publish all crates");
}
"verify" => {
verify(&crates);
}
s => panic!("unknown command: {}", s),
}
}
fn find_crates(dir: &Path, ws: &Workspace, dst: &mut Vec<Crate>) {
if dir.join("Cargo.toml").exists() {
let krate = read_crate(Some(ws), &dir.join("Cargo.toml"));
if !krate.publish || CRATES_TO_PUBLISH.iter().any(|c| krate.name == *c) {
dst.push(krate);
} else {
panic!("failed to find {:?} in whitelist or blacklist", krate.name);
}
}
for entry in dir.read_dir().unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
find_crates(&entry.path(), ws, dst);
}
}
}
fn read_crate(ws: Option<&Workspace>, manifest: &Path) -> Crate {
let mut name = None;
let mut version = None;
let mut workspace_version = None;
let mut publish = true;
let mut in_workspace = false;
for line in fs::read_to_string(manifest).unwrap().lines() {
if line.starts_with("[") {
in_workspace = line.starts_with("[workspace");
continue;
}
if name.is_none() && line.starts_with("name = \"") {
name = Some(
line.replace("name = \"", "")
.replace("\"", "")
.trim()
.to_string(),
);
}
if line.starts_with("version = \"") {
let dst = if in_workspace {
&mut workspace_version
} else {
&mut version
};
assert!(dst.is_none());
*dst = Some(
line.replace("version = \"", "")
.replace("\"", "")
.trim()
.to_string(),
);
}
if let Some(ws) = ws {
if version.is_none()
&& line.starts_with("version =")
&& line.contains("workspace = true")
{
version = Some(ws.version.clone());
}
}
if line.starts_with("publish = false") {
publish = false;
}
}
let name = name.unwrap();
let version = if !publish {
"0.0.0".to_string()
} else {
version.or(workspace_version.clone()).unwrap()
};
Crate {
manifest: manifest.to_path_buf(),
name,
version,
workspace_version,
publish,
}
}
fn bump_version(krate: &Crate, crates: &[Crate], patch: bool) {
let contents = fs::read_to_string(&krate.manifest).unwrap();
let next_version = |krate: &Crate| -> String {
if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
bump(
&krate.version,
if patch {
BumpKind::Patch
} else {
BumpKind::Major
},
)
} else {
krate.version.clone()
}
};
let mut new_manifest = String::new();
let mut is_deps = false;
let mut is_workspace = false;
for line in contents.lines() {
let mut rewritten = false;
if !is_deps && line.starts_with("version =") {
if CRATES_TO_PUBLISH.contains(&&krate.name[..]) {
println!(
"bump `{}` {} => {}",
krate.name,
krate.version,
next_version(krate),
);
let new_line = if is_workspace {
let ws_version = krate.workspace_version.as_ref().unwrap();
let next_version = bump(
ws_version,
if patch {
BumpKind::Patch
} else {
BumpKind::Major
},
);
line.replace(ws_version, &next_version)
} else {
line.replace(&krate.version, &next_version(krate))
};
new_manifest.push_str(&new_line);
rewritten = true;
}
}
if line.starts_with("[") {
is_deps = line.contains("dependencies");
is_workspace = line.contains("workspace");
}
for other in crates {
if !other.publish {
continue;
}
if !is_deps || !line.starts_with(&format!("{} ", other.name)) {
continue;
}
if !line.contains(&other.version) {
if !line.contains("version =") || !krate.publish {
continue;
}
panic!(
"{:?} has a dep on {} but doesn't list version {}",
krate.manifest, other.name, other.version
);
}
rewritten = true;
new_manifest.push_str(&line.replace(&other.version, &next_version(other)));
break;
}
if !rewritten {
new_manifest.push_str(line);
}
new_manifest.push_str("\n");
}
fs::write(&krate.manifest, new_manifest).unwrap();
}
enum BumpKind {
Major,
Patch,
}
fn bump(version: &str, bump: BumpKind) -> String {
let mut iter = version.split('.').map(|s| s.parse::<u32>().unwrap());
let major = iter.next().expect("major version");
let minor = iter.next().expect("minor version");
let patch = iter.next().expect("patch version");
match bump {
BumpKind::Patch => {
format!("{}.{}.{}", major, minor, patch + 1)
}
BumpKind::Major if major != 0 => {
format!("{}.0.0", major + 1)
}
BumpKind::Major if minor != 0 => {
format!("0.{}.0", minor + 1)
}
BumpKind::Major => {
format!("0.0.{}", patch + 1)
}
}
}
fn publish(krate: &Crate) -> bool {
if !CRATES_TO_PUBLISH.iter().any(|s| *s == krate.name) {
return true;
}
match curl(&format!("https://crates.io/api/v1/crates/{}", krate.name)) {
Some(output) => {
if output.contains(&format!("\"newest_version\":\"{}\"", krate.version)) {
println!(
"skip publish {} because {} is latest version",
krate.name, krate.version,
);
return true;
}
}
None => return false,
}
let status = Command::new("cargo")
.arg("publish")
.current_dir(krate.manifest.parent().unwrap())
.arg("--no-verify")
.status()
.expect("failed to run cargo");
if !status.success() {
println!("FAIL: failed to publish `{}`: {}", krate.name, status);
return false;
}
true
}
fn curl(url: &str) -> Option<String> {
let output = cmd_output(
Command::new("curl")
.arg("--user-agent")
.arg("bytecodealliance/wit-bindgen auto-publish script")
.arg(url),
);
if !output.status.success() {
println!("failed to curl: {}", output.status);
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
return None;
}
Some(String::from_utf8_lossy(&output.stdout).into())
}
fn verify(crates: &[Crate]) {
drop(fs::remove_dir_all(".cargo"));
drop(fs::remove_dir_all("vendor"));
let vendor = Command::new("cargo")
.arg("vendor")
.stderr(Stdio::inherit())
.output()
.unwrap();
assert!(vendor.status.success());
fs::create_dir_all(".cargo").unwrap();
fs::write(".cargo/config.toml", vendor.stdout).unwrap();
for krate in crates {
if !krate.publish {
continue;
}
verify_crates_io(krate);
verify_and_vendor(&krate);
}
fn verify_and_vendor(krate: &Crate) {
let mut cmd = Command::new("cargo");
cmd.arg("package")
.arg("--allow-dirty")
.arg("--manifest-path")
.arg(&krate.manifest)
.env("CARGO_TARGET_DIR", "./target");
let status = cmd.status().unwrap();
assert!(status.success(), "failed to verify {:?}", &krate.manifest);
let tar = Command::new("tar")
.arg("xf")
.arg(format!(
"../target/package/{}-{}.crate",
krate.name, krate.version
))
.current_dir("./vendor")
.status()
.unwrap();
assert!(tar.success());
fs::write(
format!(
"./vendor/{}-{}/.cargo-checksum.json",
krate.name, krate.version
),
"{\"files\":{}}",
)
.unwrap();
}
fn verify_crates_io(krate: &Crate) {
let name = &krate.name;
let Some(owners) = curl(&format!("https://crates.io/api/v1/crates/{name}/owners")) else {
panic!(
"
failed to get owners for {name}
If this crate does not exist on crates.io yet please ping wasm-tools maintainers
to add the crate on crates.io as a small shim. When doing so please remind them
that the trusted publishing workflow must be configured as well.
",
name = name,
);
};
if !owners.contains("\"id\":73222,") {
panic!(
"
crate {name} is not owned by wasmtime-publish, please run:
cargo owner -a wasmtime-publish {name}
",
name = name,
);
}
if owners.split("\"id\"").count() != 2 {
panic!(
"
crate {name} is not exclusively owned by wasmtime-publish
Please contact wasm-tools maintainers to ensure that `wasmtime-publish` is the
only listed owner of the crate.
",
name = name,
);
}
}
}
fn cmd_output(cmd: &mut Command) -> Output {
eprintln!("Running: `{:?}`", cmd);
match cmd.output() {
Ok(o) => o,
Err(e) => panic!("Failed to run `{:?}`: {}", cmd, e),
}
}