cairo_toolchain_xtasks/
upgrade.rs1use anyhow::{Result, bail};
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::{Shell, cmd};
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"].ensure_table()["crates-io"].ensure_table();
124
125 for crate_name in args.tool_crates() {
127 patch.remove(crate_name);
128 }
129
130 if args.spec.rev.is_some() || args.spec.branch.is_some() || args.spec.path.is_some() {
132 for &dep_name in args.tool_crates() {
135 let mut dep = InlineTable::new();
136
137 if args.spec.rev.is_some() || args.spec.branch.is_some() {
139 dep.insert("git", args.tool_repo().into());
140 }
141
142 if let Some(branch) = &args.spec.branch {
143 dep.insert("branch", branch.as_str().into());
144 }
145
146 if let Some(rev) = &args.spec.rev {
147 dep.insert("rev", rev.as_str().into());
148 }
149
150 if let Some(path) = &args.spec.path {
154 dep.insert(
155 "path",
156 path.join("crates")
157 .join(dep_name)
158 .to_string_lossy()
159 .into_owned()
160 .into(),
161 );
162 }
163
164 patch.insert(dep_name, dep.into());
165 }
166 }
167
168 patch.fmt();
169 patch.sort_values();
170
171 eprintln!("[patch.crates-io]");
172 for (key, dep) in patch.iter() {
173 eprintln!("{key} = {dep}");
174 }
175}
176
177impl Args {
178 fn tool_crates(&self) -> &'static [&'static str] {
179 static CAIRO_CACHE: OnceLock<Vec<&str>> = OnceLock::new();
180 match self.dep {
181 DepName::Cairo => CAIRO_CACHE.get_or_init(|| {
182 pull_cairo_packages_from_cairo_repository(&self.spec)
183 .unwrap()
184 .into_iter()
185 .map(|s| s.leak() as &str)
186 .collect()
187 }),
188 DepName::CairoLS => &["cairo-language-server"],
189 DepName::CairoLint => &["cairo-lint"],
190 DepName::StwoCairo => &["stwo_cairo_prover", "stwo-cairo-adapter"],
191 }
192 }
193
194 fn tool_owns_crate(&self, crate_name: &str) -> bool {
195 self.tool_crates().contains(&crate_name)
196 }
197
198 fn tool_repo(&self) -> &'static str {
199 match self.dep {
200 DepName::Cairo => "https://github.com/starkware-libs/cairo",
201 DepName::CairoLS => "https://github.com/software-mansion/cairols",
202 DepName::CairoLint => "https://github.com/software-mansion/cairo-lint",
203 DepName::StwoCairo => "https://github.com/starkware-libs/stwo-cairo",
204 }
205 }
206}
207
208fn copy_dependency_features(dest: &mut InlineTable, src: &Value) {
210 if let Some(dep) = src.as_inline_table() {
211 if let Some(features) = dep.get("features") {
212 dest.insert("features", features.clone());
213 }
214 }
215}
216
217fn simplify_dependency_table(dep: &mut Value) {
219 *dep = match mem::replace(dep, false.into()) {
220 Value::InlineTable(mut table) => {
221 if table.len() == 1 {
222 table.remove("version").unwrap_or_else(|| table.into())
223 } else {
224 table.into()
225 }
226 }
227
228 dep => dep,
229 }
230}
231
232fn purge_unused_patches(cargo_toml: &mut DocumentMut) -> Result<()> {
238 let sh = Shell::new()?;
239 let cargo_lock = sh.read_file("Cargo.lock")?.parse::<DocumentMut>()?;
240
241 if let Some(unused_patches) = find_unused_patches(&cargo_lock)
242 && let Some(patch) = cargo_toml["patch"].as_table_mut()
243 && let Some(patch) = patch["crates-io"].as_table_mut()
244 {
245 patch.retain(|key, _| !unused_patches.contains(&key.to_owned()));
247 }
248
249 Ok(())
250}
251
252fn find_unused_patches(cargo_lock: &DocumentMut) -> Option<Vec<String>> {
254 Some(
255 cargo_lock
256 .get("patch")?
257 .get("unused")?
258 .as_array_of_tables()?
259 .iter()
260 .flat_map(|table| Some(table.get("name")?.as_str()?.to_owned()))
261 .collect(),
262 )
263}
264
265fn pull_cairo_packages_from_cairo_repository(spec: &Spec) -> Result<Vec<String>> {
270 let sh = Shell::new()?;
271
272 let release_crates_sh = if let Some(path) = &spec.path {
273 sh.read_file(path.join("scripts").join("release_crates.sh"))?
274 } else {
275 let rev = if let Some(version) = &spec.version {
276 format!("refs/tags/v{version}")
277 } else if let Some(rev) = &spec.rev {
278 rev.to_string()
279 } else if let Some(branch) = &spec.branch {
280 format!("refs/heads/{branch}")
281 } else {
282 "refs/heads/main".to_string()
283 };
284 let url = format!(
285 "https://raw.githubusercontent.com/starkware-libs/cairo/{rev}/scripts/release_crates.sh"
286 );
287 cmd!(sh, "curl -sSfL {url}").read()?
288 };
289
290 let Some((_, source_list)) = release_crates_sh.split_once("CRATES_TO_PUBLISH=(") else {
291 bail!("failed to extract start of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
292 };
293 let Some((source_list, _)) = source_list.split_once(")") else {
294 bail!("failed to extract end of `CRATES_TO_PUBLISH` from `scripts/release_crates.sh`");
295 };
296
297 let mut crates: Vec<String> = source_list
298 .split_whitespace()
299 .filter(|s| s.starts_with("cairo-lang-"))
300 .map(|s| s.into())
301 .collect();
302 crates.sort();
303 Ok(crates)
304}
305
306trait ItemEx {
307 fn ensure_table(&mut self) -> &mut toml_edit::Table;
308}
309
310impl ItemEx for toml_edit::Item {
311 fn ensure_table(&mut self) -> &mut toml_edit::Table {
312 if !self.is_table() {
313 let mut table = toml_edit::Table::new();
314 table.set_implicit(true);
315 *self = table.into();
316 }
317 self.as_table_mut().unwrap()
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn test_pull_cairo_packages_from_cairo_repository() {
327 let list = pull_cairo_packages_from_cairo_repository(&Spec::default()).unwrap();
328 assert!(!list.is_empty());
329 assert!(list.contains(&"cairo-lang-compiler".to_owned()));
330 assert!(!list.contains(&"cairo-test".to_owned()));
331 assert!(list.is_sorted());
332 }
333}