//! Helper script to publish this repository's suites of crates //! //! In a nutshell //! //! * `./publish bump` - bump crate versions as a major release //! * `./publish bump-patch` - bump crate versions as a patch release //! * `./publish publish` - actually publish crates to crates.io //! * `./publish verify` - verify that crates can be published, like a dry run use std::collections::HashMap; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::thread; use std::time::Duration; // note that this list must be topologically sorted by dependencies const CRATES_TO_PUBLISH: &[&str] = &["wasm-component-ld"]; struct Workspace { version: String, } struct Crate { manifest: PathBuf, name: String, version: String, publish: bool, workspace_version: Option, } fn main() { let mut crates = Vec::new(); let root = read_crate(None, "./Cargo.toml".as_ref()); crates.push(root); let pos = CRATES_TO_PUBLISH .iter() .enumerate() .map(|(i, c)| (*c, i)) .collect::>(); 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"); } // update the lock file assert!(Command::new("cargo") .arg("fetch") .status() .unwrap() .success()); } "publish" => { // We have so many crates to publish we're frequently either // rate-limited or we run into issues where crates can't publish // successfully because they're waiting on the index entries of // previously-published crates to propagate. This means we try to // publish in a loop and we remove crates once they're successfully // published. Failed-to-publish crates get enqueued for another try // later on. 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 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.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.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` isn't a published crate then it's not going to get a // bumped version so we don't need to update anything in the // manifest. 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, #[allow(dead_code)] Minor, Patch, } /// Performs a major version bump increment on the semver version `version`. /// /// This function will perform a semver-major-version bump on the `version` /// specified. This is used to calculate the next version of a crate in this /// repository since we're currently making major version bumps for all our /// releases. This may end up getting tweaked as we stabilize crates and start /// doing more minor/patch releases, but for now this should do the trick. fn bump(version: &str, bump: BumpKind) -> String { let mut iter = version.split('.').map(|s| s.parse::().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::Minor => { format!("{}.{}.0", major, minor + 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; } // First make sure the crate isn't already published at this version. This // script may be re-run and there's no need to re-attempt previous work. let output = Command::new("curl") .arg(&format!("https://crates.io/api/v1/crates/{}", krate.name)) .output() .expect("failed to invoke `curl`"); if output.status.success() && String::from_utf8_lossy(&output.stdout) .contains(&format!("\"newest_version\":\"{}\"", krate.version)) { println!( "skip publish {} because {} is latest version", krate.name, krate.version, ); return true; } 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; } // // After we've published then make sure that the `wasmtime-publish` group is // // added to this crate for future publications. If it's already present // // though we can skip the `cargo owner` modification. // let output = Command::new("curl") // .arg(&format!( // "https://crates.io/api/v1/crates/{}/owners", // krate.name // )) // .output() // .expect("failed to invoke `curl`"); // if output.status.success() // && String::from_utf8_lossy(&output.stdout).contains("wasmtime-publish") // { // println!( // "wasmtime-publish already listed as an owner of {}", // krate.name // ); // return true; // } // // Note that the status is ignored here. This fails most of the time because // // the owner is already set and present, so we only want to add this to // // crates which haven't previously been published. // let status = Command::new("cargo") // .arg("owner") // .arg("-a") // .arg("github:bytecodealliance:wasmtime-publish") // .arg(&krate.name) // .status() // .expect("failed to run cargo"); // if !status.success() { // panic!( // "FAIL: failed to add wasmtime-publish as owner `{}`: {}", // krate.name, status // ); // } true } // Verify the current tree is publish-able to crates.io. The intention here is // that we'll run `cargo package` on everything which verifies the build as-if // it were published to crates.io. This requires using an incrementally-built // directory registry generated from `cargo vendor` because the versions // referenced from `Cargo.toml` may not exist on crates.io. 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_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(); } }