cairo_toolchain_xtasks/
upgrade.rs1use anyhow::{bail, Result};
4use clap::{Parser, ValueEnum};
5use semver::Version;
6use std::mem;
7use std::path::PathBuf;
8use std::sync::OnceLock;
9use toml_edit::{DocumentMut, InlineTable, Value};
10use xshell::{cmd, Shell};
11
12#[derive(Parser)]
14pub struct Args {
15 dep: DepName,
17
18 #[command(flatten)]
19 spec: Spec,
20
21 #[arg(long, default_value_t = false)]
23 dry_run: bool,
24}
25
26#[derive(ValueEnum, Copy, Clone, Debug)]
27enum DepName {
28 Cairo,
29 #[value(name = "cairols")]
30 CairoLS,
31 #[value(name = "cairolint")]
32 CairoLint,
33 #[value(name = "stwo-cairo")]
34 StwoCairo,
35}
36
37#[derive(clap::Args, Clone, Default)]
38#[group(required = true, multiple = true)]
39struct Spec {
40 version: Option<Version>,
42
43 #[arg(short, long, conflicts_with = "branch")]
45 rev: Option<String>,
46
47 #[arg(short, long)]
49 branch: Option<String>,
50
51 #[arg(short, long, conflicts_with_all = ["rev", "branch"])]
55 path: Option<PathBuf>,
56}
57
58pub fn main(args: Args) -> Result<()> {
59 let sh = Shell::new()?;
60
61 let mut cargo_toml = sh.read_file("Cargo.toml")?.parse::<DocumentMut>()?;
62
63 edit_dependencies(&mut cargo_toml, "dependencies", &args);
64 edit_dependencies(&mut cargo_toml, "dev-dependencies", &args);
65 edit_dependencies(&mut cargo_toml, "workspace.dependencies", &args);
66 edit_patch(&mut cargo_toml, &args);
67
68 if !args.dry_run {
69 sh.write_file("Cargo.toml", cargo_toml.to_string())?;
70
71 cmd!(sh, "cargo fetch").run()?;
72
73 purge_unused_patches(&mut cargo_toml)?;
74 sh.write_file("Cargo.toml", cargo_toml.to_string())?;
75
76 cmd!(sh, "cargo xtask sync-version").run()?;
77 }
78
79 Ok(())
80}
81
82fn edit_dependencies(cargo_toml: &mut DocumentMut, table_path: &str, args: &Args) {
83 let Some(deps) = table_path
84 .split('.')
85 .try_fold(cargo_toml.as_item_mut(), |doc, key| doc.get_mut(key))
86 else {
87 return;
88 };
89 if deps.is_none() {
90 return;
91 }
92 let deps = deps.as_table_mut().unwrap();
93
94 for (_, dep) in deps.iter_mut().filter(|(key, _)| args.tool_owns_crate(key)) {
95 let dep = dep.as_value_mut().unwrap();
96
97 let mut new_dep = InlineTable::from_iter([(
100 "version",
101 match &args.spec.version {
102 Some(version) => Value::from(version.to_string()),
103 None => Value::from("*"),
104 },
105 )]);
106
107 copy_dependency_features(&mut new_dep, dep);
108
109 *dep = new_dep.into();
110 simplify_dependency_table(dep)
111 }
112
113 deps.fmt();
114 deps.sort_values();
115
116 eprintln!("[{table_path}]");
117 for (key, dep) in deps.iter().filter(|(key, _)| args.tool_owns_crate(key)) {
118 eprintln!("{key} = {dep}");
119 }
120}
121
122fn edit_patch(cargo_toml: &mut DocumentMut, args: &Args) {
123 let patch = cargo_toml["patch"].as_table_mut().unwrap()["crates-io"]
124 .as_table_mut()
125 .unwrap();
126
127 for crate_name in args.tool_crates() {
129 patch.remove(crate_name);
130 }
131
132 if args.spec.rev.is_some() || args.spec.branch.is_some() || args.spec.path.is_some() {
134 for &dep_name in args.tool_crates() {
137 let mut dep = InlineTable::new();
138
139 if args.spec.rev.is_some() || args.spec.branch.is_some() {
141 dep.insert("git", args.tool_repo().into());
142 }
143
144 if let Some(branch) = &args.spec.branch {
145 dep.insert("branch", branch.as_str().into());
146 }
147
148 if let Some(rev) = &args.spec.rev {
149 dep.insert("rev", rev.as_str().into());
150 }
151
152 if let Some(path) = &args.spec.path {
156 dep.insert(
157 "path",
158 path.join("crates")
159 .join(dep_name)
160 .to_string_lossy()
161 .into_owned()
162 .into(),
163 );
164 }
165
166 patch.insert(dep_name, dep.into());
167 }
168 }
169
170 patch.fmt();
171 patch.sort_values();
172
173 eprintln!("[patch.crates-io]");
174 for (key, dep) in patch.iter() {
175 eprintln!("{key} = {dep}");
176 }
177}
178
179impl Args {
180 fn tool_crates(&self) -> &'static [&'static str] {
181 static CAIRO_CACHE: OnceLock<Vec<&str>> = OnceLock::new();
182 match self.dep {
183 DepName::Cairo => CAIRO_CACHE.get_or_init(|| {
184 pull_cairo_packages_from_cairo_repository(&self.spec)
185 .unwrap()
186 .into_iter()
187 .map(|s| s.leak() as &str)
188 .collect()
189 }),
190 DepName::CairoLS => &["cairo-language-server"],
191 DepName::CairoLint => &["cairo-lint"],
192 DepName::StwoCairo => &["stwo_cairo_prover", "stwo-cairo-adapter"],
193 }
194 }
195
196 fn tool_owns_crate(&self, crate_name: &str) -> bool {
197 self.tool_crates().contains(&crate_name)
198 }
199
200 fn tool_repo(&self) -> &'static str {
201 match self.dep {
202 DepName::Cairo => "https://github.com/starkware-libs/cairo",
203 DepName::CairoLS => "https://github.com/software-mansion/cairols",
204 DepName::CairoLint => "https://github.com/software-mansion/cairo-lint",
205 DepName::StwoCairo => "https://github.com/starkware-libs/stwo-cairo",
206 }
207 }
208}
209
210fn copy_dependency_features(dest: &mut InlineTable, src: &Value) {
212 if let Some(dep) = src.as_inline_table() {
213 if let Some(features) = dep.get("features") {
214 dest.insert("features", features.clone());
215 }
216 }
217}
218
219fn simplify_dependency_table(dep: &mut Value) {
221 *dep = match mem::replace(dep, false.into()) {
222 Value::InlineTable(mut table) => {
223 if table.len() == 1 {
224 table.remove("version").unwrap_or_else(|| table.into())
225 } else {
226 table.into()
227 }
228 }
229
230 dep => dep,
231 }
232}
233
234fn purge_unused_patches(cargo_toml: &mut DocumentMut) -> Result<()> {
240 let sh = Shell::new()?;
241 let cargo_lock = sh.read_file("Cargo.lock")?.parse::<DocumentMut>()?;
242
243 if let Some(unused_patches) = find_unused_patches(&cargo_lock) {
244 let patch = cargo_toml["patch"].as_table_mut().unwrap()["crates-io"]
245 .as_table_mut()
246 .unwrap();
247
248 patch.retain(|key, _| !unused_patches.contains(&key.to_owned()));
250 }
251
252 Ok(())
253}
254
255fn find_unused_patches(cargo_lock: &DocumentMut) -> Option<Vec<String>> {
257 Some(
258 cargo_lock
259 .get("patch")?
260 .get("unused")?
261 .as_array_of_tables()?
262 .iter()
263 .flat_map(|table| Some(table.get("name")?.as_str()?.to_owned()))
264 .collect(),
265 )
266}
267
268fn pull_cairo_packages_from_cairo_repository(spec: &Spec) -> Result<Vec<String>> {
273 let sh = Shell::new()?;
274
275 let release_crates_sh = if let Some(path) = &spec.path {
276 sh.read_file(path.join("scripts").join("release_crates.sh"))?
277 } else {
278 let rev = if let Some(version) = &spec.version {
279 format!("refs/tags/v{version}")
280 } else if let Some(rev) = &spec.rev {
281 rev.to_string()
282 } else if let Some(branch) = &spec.branch {
283 format!("refs/heads/{branch}")
284 } else {
285 "refs/heads/main".to_string()
286 };
287 let url = format!("https://raw.githubusercontent.com/starkware-libs/cairo/{rev}/scripts/release_crates.sh");
288 cmd!(sh, "curl -sSfL {url}").read()?
289 };
290
291 let Some((_, source_list)) = release_crates_sh.split_once("CRATES_TO_PUBLISH=(") else {
292 bail!("failed to extract start of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
293 };
294 let Some((source_list, _)) = source_list.split_once(")") else {
295 bail!("failed to extract end of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
296 };
297
298 let mut crates: Vec<String> = source_list
299 .split_whitespace()
300 .filter(|s| s.starts_with("cairo-lang-"))
301 .map(|s| s.into())
302 .collect();
303 crates.sort();
304 Ok(crates)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_pull_cairo_packages_from_cairo_repository() {
313 let list = pull_cairo_packages_from_cairo_repository(&Spec::default()).unwrap();
314 assert!(!list.is_empty());
315 assert!(list.contains(&"cairo-lang-compiler".to_owned()));
316 assert!(!list.contains(&"cairo-test".to_owned()));
317 assert!(list.is_sorted());
318 }
319}