tree_sitter_cli/
version.rs

1use std::{fs, path::PathBuf, process::Command};
2
3use anyhow::{anyhow, Context, Result};
4use regex::Regex;
5use tree_sitter_loader::TreeSitterJSON;
6
7pub struct Version {
8    pub version: String,
9    pub current_dir: PathBuf,
10}
11
12impl Version {
13    #[must_use]
14    pub const fn new(version: String, current_dir: PathBuf) -> Self {
15        Self {
16            version,
17            current_dir,
18        }
19    }
20
21    pub fn run(self) -> Result<()> {
22        let tree_sitter_json = self.current_dir.join("tree-sitter.json");
23
24        let tree_sitter_json =
25            serde_json::from_str::<TreeSitterJSON>(&fs::read_to_string(tree_sitter_json)?)?;
26
27        let is_multigrammar = tree_sitter_json.grammars.len() > 1;
28
29        self.update_treesitter_json().with_context(|| {
30            format!(
31                "Failed to update tree-sitter.json at {}",
32                self.current_dir.display()
33            )
34        })?;
35        self.update_cargo_toml().with_context(|| {
36            format!(
37                "Failed to update Cargo.toml at {}",
38                self.current_dir.display()
39            )
40        })?;
41        self.update_package_json().with_context(|| {
42            format!(
43                "Failed to update package.json at {}",
44                self.current_dir.display()
45            )
46        })?;
47        self.update_makefile(is_multigrammar).with_context(|| {
48            format!(
49                "Failed to update Makefile at {}",
50                self.current_dir.display()
51            )
52        })?;
53        self.update_cmakelists_txt().with_context(|| {
54            format!(
55                "Failed to update CMakeLists.txt at {}",
56                self.current_dir.display()
57            )
58        })?;
59        self.update_pyproject_toml().with_context(|| {
60            format!(
61                "Failed to update pyproject.toml at {}",
62                self.current_dir.display()
63            )
64        })?;
65
66        Ok(())
67    }
68
69    fn update_treesitter_json(&self) -> Result<()> {
70        let tree_sitter_json = &fs::read_to_string(self.current_dir.join("tree-sitter.json"))?;
71
72        let tree_sitter_json = tree_sitter_json
73            .lines()
74            .map(|line| {
75                if line.contains("\"version\":") {
76                    let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len();
77                    let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1;
78                    let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
79
80                    format!(
81                        "{}{}{}",
82                        &line[..start_quote],
83                        self.version,
84                        &line[end_quote..]
85                    )
86                } else {
87                    line.to_string()
88                }
89            })
90            .collect::<Vec<_>>()
91            .join("\n")
92            + "\n";
93
94        fs::write(self.current_dir.join("tree-sitter.json"), tree_sitter_json)?;
95
96        Ok(())
97    }
98
99    fn update_cargo_toml(&self) -> Result<()> {
100        if !self.current_dir.join("Cargo.toml").exists() {
101            return Ok(());
102        }
103
104        let cargo_toml = fs::read_to_string(self.current_dir.join("Cargo.toml"))?;
105
106        let cargo_toml = cargo_toml
107            .lines()
108            .map(|line| {
109                if line.starts_with("version =") {
110                    format!("version = \"{}\"", self.version)
111                } else {
112                    line.to_string()
113                }
114            })
115            .collect::<Vec<_>>()
116            .join("\n")
117            + "\n";
118
119        fs::write(self.current_dir.join("Cargo.toml"), cargo_toml)?;
120
121        if self.current_dir.join("Cargo.lock").exists() {
122            let Ok(cmd) = Command::new("cargo")
123                .arg("generate-lockfile")
124                .arg("--offline")
125                .current_dir(&self.current_dir)
126                .output()
127            else {
128                return Ok(()); // cargo is not `executable`, ignore
129            };
130
131            if !cmd.status.success() {
132                let stderr = String::from_utf8_lossy(&cmd.stderr);
133                return Err(anyhow!(
134                    "Failed to run `cargo generate-lockfile`:\n{stderr}"
135                ));
136            }
137        }
138
139        Ok(())
140    }
141
142    fn update_package_json(&self) -> Result<()> {
143        if !self.current_dir.join("package.json").exists() {
144            return Ok(());
145        }
146
147        let package_json = &fs::read_to_string(self.current_dir.join("package.json"))?;
148
149        let package_json = package_json
150            .lines()
151            .map(|line| {
152                if line.contains("\"version\":") {
153                    let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len();
154                    let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1;
155                    let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
156
157                    format!(
158                        "{}{}{}",
159                        &line[..start_quote],
160                        self.version,
161                        &line[end_quote..]
162                    )
163                } else {
164                    line.to_string()
165                }
166            })
167            .collect::<Vec<_>>()
168            .join("\n")
169            + "\n";
170
171        fs::write(self.current_dir.join("package.json"), package_json)?;
172
173        if self.current_dir.join("package-lock.json").exists() {
174            let Ok(cmd) = Command::new("npm")
175                .arg("install")
176                .arg("--package-lock-only")
177                .current_dir(&self.current_dir)
178                .output()
179            else {
180                return Ok(()); // npm is not `executable`, ignore
181            };
182
183            if !cmd.status.success() {
184                let stderr = String::from_utf8_lossy(&cmd.stderr);
185                return Err(anyhow!("Failed to run `npm install`:\n{stderr}"));
186            }
187        }
188
189        Ok(())
190    }
191
192    fn update_makefile(&self, is_multigrammar: bool) -> Result<()> {
193        let makefile = if is_multigrammar {
194            if !self.current_dir.join("common").join("common.mak").exists() {
195                return Ok(());
196            }
197
198            fs::read_to_string(self.current_dir.join("Makefile"))?
199        } else {
200            if !self.current_dir.join("Makefile").exists() {
201                return Ok(());
202            }
203
204            fs::read_to_string(self.current_dir.join("Makefile"))?
205        };
206
207        let makefile = makefile
208            .lines()
209            .map(|line| {
210                if line.starts_with("VERSION") {
211                    format!("VERSION := {}", self.version)
212                } else {
213                    line.to_string()
214                }
215            })
216            .collect::<Vec<_>>()
217            .join("\n")
218            + "\n";
219
220        fs::write(self.current_dir.join("Makefile"), makefile)?;
221
222        Ok(())
223    }
224
225    fn update_cmakelists_txt(&self) -> Result<()> {
226        if !self.current_dir.join("CMakeLists.txt").exists() {
227            return Ok(());
228        }
229
230        let cmake = fs::read_to_string(self.current_dir.join("CMakeLists.txt"))?;
231
232        let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)?;
233        let cmake = re.replace(&cmake, format!(r#"$1"{}""#, self.version));
234
235        fs::write(self.current_dir.join("CMakeLists.txt"), cmake.as_bytes())?;
236
237        Ok(())
238    }
239
240    fn update_pyproject_toml(&self) -> Result<()> {
241        if !self.current_dir.join("pyproject.toml").exists() {
242            return Ok(());
243        }
244
245        let pyproject_toml = fs::read_to_string(self.current_dir.join("pyproject.toml"))?;
246
247        let pyproject_toml = pyproject_toml
248            .lines()
249            .map(|line| {
250                if line.starts_with("version =") {
251                    format!("version = \"{}\"", self.version)
252                } else {
253                    line.to_string()
254                }
255            })
256            .collect::<Vec<_>>()
257            .join("\n")
258            + "\n";
259
260        fs::write(self.current_dir.join("pyproject.toml"), pyproject_toml)?;
261
262        Ok(())
263    }
264}