cairo_toolchain_xtasks/
upgrade.rsuse anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use semver::Version;
use std::mem;
use std::path::PathBuf;
use std::sync::OnceLock;
use toml_edit::{DocumentMut, InlineTable, Value};
use xshell::{cmd, Shell};
#[derive(Parser)]
pub struct Args {
dep: DepName,
#[command(flatten)]
spec: Spec,
#[arg(long, default_value_t = false)]
dry_run: bool,
}
#[derive(ValueEnum, Copy, Clone, Debug)]
enum DepName {
Cairo,
#[value(name = "cairols")]
CairoLS,
}
#[derive(clap::Args, Clone, Default)]
#[group(required = true, multiple = true)]
struct Spec {
version: Option<Version>,
#[arg(short, long, conflicts_with = "branch")]
rev: Option<String>,
#[arg(short, long)]
branch: Option<String>,
#[arg(short, long, conflicts_with_all = ["rev", "branch"])]
path: Option<PathBuf>,
}
pub fn main(args: Args) -> Result<()> {
let sh = Shell::new()?;
let mut cargo_toml = sh.read_file("Cargo.toml")?.parse::<DocumentMut>()?;
edit_dependencies(&mut cargo_toml, "dependencies", &args);
edit_dependencies(&mut cargo_toml, "dev-dependencies", &args);
edit_dependencies(&mut cargo_toml, "workspace.dependencies", &args);
edit_patch(&mut cargo_toml, &args);
if !args.dry_run {
sh.write_file("Cargo.toml", cargo_toml.to_string())?;
cmd!(sh, "cargo fetch").run()?;
purge_unused_patches(&mut cargo_toml)?;
sh.write_file("Cargo.toml", cargo_toml.to_string())?;
cmd!(sh, "cargo xtask sync-version").run()?;
}
Ok(())
}
fn edit_dependencies(cargo_toml: &mut DocumentMut, table_path: &str, args: &Args) {
let Some(deps) = table_path
.split('.')
.try_fold(cargo_toml.as_item_mut(), |doc, key| doc.get_mut(key))
else {
return;
};
if deps.is_none() {
return;
}
let deps = deps.as_table_mut().unwrap();
for (_, dep) in deps.iter_mut().filter(|(key, _)| args.tool_owns_crate(key)) {
let dep = dep.as_value_mut().unwrap();
let mut new_dep = InlineTable::from_iter([(
"version",
match &args.spec.version {
Some(version) => Value::from(version.to_string()),
None => Value::from("*"),
},
)]);
copy_dependency_features(&mut new_dep, dep);
*dep = new_dep.into();
simplify_dependency_table(dep)
}
deps.fmt();
deps.sort_values();
eprintln!("[{table_path}]");
for (key, dep) in deps.iter().filter(|(key, _)| args.tool_owns_crate(key)) {
eprintln!("{key} = {dep}");
}
}
fn edit_patch(cargo_toml: &mut DocumentMut, args: &Args) {
let patch = cargo_toml["patch"].as_table_mut().unwrap()["crates-io"]
.as_table_mut()
.unwrap();
for crate_name in args.tool_crates() {
patch.remove(crate_name);
}
if args.spec.rev.is_some() || args.spec.branch.is_some() || args.spec.path.is_some() {
for &dep_name in args.tool_crates() {
let mut dep = InlineTable::new();
if args.spec.rev.is_some() || args.spec.branch.is_some() {
dep.insert("git", args.tool_repo().into());
}
if let Some(branch) = &args.spec.branch {
dep.insert("branch", branch.as_str().into());
}
if let Some(rev) = &args.spec.rev {
dep.insert("rev", rev.as_str().into());
}
if let Some(path) = &args.spec.path {
dep.insert(
"path",
path.join("crates")
.join(dep_name)
.to_string_lossy()
.into_owned()
.into(),
);
}
patch.insert(dep_name, dep.into());
}
}
patch.fmt();
patch.sort_values();
eprintln!("[patch.crates-io]");
for (key, dep) in patch.iter() {
eprintln!("{key} = {dep}");
}
}
impl Args {
fn tool_crates(&self) -> &'static [&'static str] {
static CAIRO_CACHE: OnceLock<Vec<&str>> = OnceLock::new();
match self.dep {
DepName::Cairo => CAIRO_CACHE.get_or_init(|| {
pull_cairo_packages_from_cairo_repository(&self.spec)
.unwrap()
.into_iter()
.map(|s| s.leak() as &str)
.collect()
}),
DepName::CairoLS => &["cairo-language-server"],
}
}
fn tool_owns_crate(&self, crate_name: &str) -> bool {
self.tool_crates().contains(&crate_name)
}
fn tool_repo(&self) -> &'static str {
match self.dep {
DepName::Cairo => "https://github.com/starkware-libs/cairo",
DepName::CairoLS => "https://github.com/software-mansion/cairols",
}
}
}
fn copy_dependency_features(dest: &mut InlineTable, src: &Value) {
if let Some(dep) = src.as_inline_table() {
if let Some(features) = dep.get("features") {
dest.insert("features", features.clone());
}
}
}
fn simplify_dependency_table(dep: &mut Value) {
*dep = match mem::replace(dep, false.into()) {
Value::InlineTable(mut table) => {
if table.len() == 1 {
table.remove("version").unwrap_or_else(|| table.into())
} else {
table.into()
}
}
dep => dep,
}
}
fn purge_unused_patches(cargo_toml: &mut DocumentMut) -> Result<()> {
let sh = Shell::new()?;
let cargo_lock = sh.read_file("Cargo.lock")?.parse::<DocumentMut>()?;
if let Some(unused_patches) = find_unused_patches(&cargo_lock) {
let patch = cargo_toml["patch"].as_table_mut().unwrap()["crates-io"]
.as_table_mut()
.unwrap();
patch.retain(|key, _| !unused_patches.contains(&key.to_owned()));
}
Ok(())
}
fn find_unused_patches(cargo_lock: &DocumentMut) -> Option<Vec<String>> {
Some(
cargo_lock
.get("patch")?
.get("unused")?
.as_array_of_tables()?
.iter()
.flat_map(|table| Some(table.get("name")?.as_str()?.to_owned()))
.collect(),
)
}
fn pull_cairo_packages_from_cairo_repository(spec: &Spec) -> Result<Vec<String>> {
let sh = Shell::new()?;
let release_crates_sh = if let Some(path) = &spec.path {
sh.read_file(path.join("scripts").join("release_crates.sh"))?
} else {
let rev = if let Some(version) = &spec.version {
format!("refs/tags/v{version}")
} else if let Some(rev) = &spec.rev {
rev.to_string()
} else if let Some(branch) = &spec.branch {
format!("refs/heads/{branch}")
} else {
"refs/heads/main".to_string()
};
let url = format!("https://raw.githubusercontent.com/starkware-libs/cairo/{rev}/scripts/release_crates.sh");
cmd!(sh, "curl -sSfL {url}").read()?
};
let Some((_, source_list)) = release_crates_sh.split_once("CRATES_TO_PUBLISH=(") else {
bail!("failed to extract start of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
};
let Some((source_list, _)) = source_list.split_once(")") else {
bail!("failed to extract end of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
};
let mut crates: Vec<String> = source_list
.split_whitespace()
.filter(|s| s.starts_with("cairo-lang-"))
.map(|s| s.into())
.collect();
crates.sort();
Ok(crates)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pull_cairo_packages_from_cairo_repository() {
let list = pull_cairo_packages_from_cairo_repository(&Spec::default()).unwrap();
assert!(!list.is_empty());
assert!(list.contains(&"cairo-lang-compiler".to_owned()));
assert!(!list.contains(&"cairo-test".to_owned()));
assert!(list.is_sorted());
}
}