tree_sitter_cli/
version.rs1use 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(()); };
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(()); };
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}