Skip to main content

anza_xtask/commands/
update_crate.rs

1use {
2    crate::utils,
3    anyhow::Result,
4    clap::Args,
5    log::{debug, info},
6    std::{fs, path::PathBuf},
7    toml_edit::{value, DocumentMut, Item},
8};
9
10#[derive(Args)]
11pub struct CommandArgs {
12    #[arg(long, default_value = ".")]
13    pub root_path: PathBuf,
14    #[arg(long, short, required = true)]
15    pub package: String,
16    #[arg(long, required = true)]
17    pub from: String,
18    #[arg(long, required = true)]
19    pub to: String,
20    #[arg(long, default_value = "[]", value_delimiter = ',')]
21    pub exclude_paths: Vec<PathBuf>,
22}
23
24pub fn run(args: CommandArgs) -> Result<()> {
25    let all_cargo_tomls = utils::recursive_find_files(&args.root_path, "Cargo.toml", |_| true)?;
26    let dependency_targets: &[(&[&str], &str)] = &[
27        (&["workspace", "dependencies"], "workspace.dependencies"),
28        (&["dependencies"], "dependencies"),
29        (&["dev-dependencies"], "dev-dependencies"),
30    ];
31
32    'MAIN_LOOP: for cargo_toml in all_cargo_tomls {
33        info!("[{}]", cargo_toml.display());
34
35        for exclude_path in &args.exclude_paths {
36            if cargo_toml
37                .to_string_lossy()
38                .contains(exclude_path.to_string_lossy().as_ref())
39            {
40                info!("  ⏩ skipped (exclude path)");
41                continue 'MAIN_LOOP;
42            }
43        }
44
45        let content = fs::read_to_string(&cargo_toml)?;
46        let mut doc = content.parse::<DocumentMut>()?;
47        let mut need_to_write = false;
48
49        for (path, label) in dependency_targets {
50            if update_dependency_at(&mut doc, path, &args.package, &args.from, &args.to) {
51                need_to_write = true;
52                info!("  ✅ updated {label}");
53            }
54        }
55
56        if need_to_write {
57            fs::write(&cargo_toml, doc.to_string())?;
58        } else {
59            info!("  ⏩ skipped (no changes)");
60        }
61    }
62    Ok(())
63}
64
65fn update_dependency_at(
66    doc: &mut DocumentMut,
67    path: &[&str],
68    package: &str,
69    from: &str,
70    to: &str,
71) -> bool {
72    let mut table = doc.as_table_mut();
73
74    for key in path {
75        let Some(next_table) = table.get_mut(key).and_then(Item::as_table_mut) else {
76            return false;
77        };
78        table = next_table;
79    }
80
81    table
82        .get_mut(package)
83        .is_some_and(|dep_spec| update_dependency_spec(dep_spec, from, to))
84}
85
86fn update_dependency_spec(dep_spec: &mut Item, from: &str, to: &str) -> bool {
87    debug!("dep_spec: {dep_spec:?}");
88    if let Some(current_version) = dep_spec.as_str() {
89        if current_version == from || current_version == format!("={from}") {
90            let new_version = current_version.replace(from, to);
91            *dep_spec = value(&new_version);
92            return true;
93        }
94    } else if let Some(current_version) = dep_spec
95        .as_inline_table()
96        .and_then(|table| table.get("version").and_then(|version| version.as_str()))
97    {
98        if current_version == from || current_version == format!("={from}") {
99            let new_version = current_version.replace(from, to);
100            dep_spec["version"] = value(&new_version);
101            return true;
102        }
103    }
104
105    false
106}
107
108#[cfg(test)]
109mod tests {
110    use {super::*, toml_edit::Table};
111
112    #[test]
113    fn test_update_dependency_spec_string() {
114        // should update if the version matches
115        let mut dep_spec = value("1.2.3");
116        assert!(update_dependency_spec(&mut dep_spec, "1.2.3", "1.2.4"));
117        assert_eq!(dep_spec.as_str(), Some("1.2.4"));
118
119        // should not update if the version does not match
120        let mut dep_spec = value("1.2.3");
121        assert!(!update_dependency_spec(&mut dep_spec, "1.2.4", "1.2.5"));
122        assert_eq!(dep_spec.as_str(), Some("1.2.3"));
123
124        // should still update if the version is prefixed
125        let mut dep_spec = value("=1.2.3");
126        assert!(update_dependency_spec(&mut dep_spec, "1.2.3", "1.2.4"));
127        assert_eq!(dep_spec.as_str(), Some("=1.2.4"));
128    }
129
130    #[test]
131    fn test_update_dependency_spec_table() {
132        // should update if the version matches
133        let mut table = Table::default();
134        table["version"] = value("1.2.3");
135        let mut dep_spec: Item = value(table.into_inline_table());
136        assert!(update_dependency_spec(&mut dep_spec, "1.2.3", "1.2.4"));
137        assert_eq!(dep_spec["version"].as_str(), Some("1.2.4"));
138
139        // should not update if the version does not match
140        let mut table = Table::default();
141        table["version"] = value("1.2.3");
142        let mut dep_spec = table.into();
143        assert!(!update_dependency_spec(&mut dep_spec, "1.2.4", "1.2.5"));
144        assert_eq!(dep_spec["version"].as_str(), Some("1.2.3"));
145
146        // should still update if the version is prefixed
147        let mut table = Table::default();
148        table["version"] = value("=1.2.3".to_string());
149        let mut dep_spec: Item = value(table.into_inline_table());
150        assert!(update_dependency_spec(&mut dep_spec, "1.2.3", "1.2.4"));
151        assert_eq!(dep_spec["version"].as_str(), Some("=1.2.4"));
152    }
153}