tree_sitter_installer/
parser_installer.rs1use anyhow::{anyhow, Context, Result};
2use std::{
3 fs::read,
4 io::{BufRead, BufReader},
5 path::{Path, PathBuf},
6 process::{Command, Output},
7 str::from_utf8,
8};
9
10const BUILD_CMD: &str = "cargo rustc --crate-type=dylib --release";
11
12pub fn get_compiled_lib_path(library_name: &str, install_dir: &Path) -> PathBuf {
13 install_dir.join("target").join("release").join(format!(
14 "{}{}{}",
15 std::env::consts::DLL_PREFIX,
16 library_name,
17 std::env::consts::DLL_SUFFIX
18 ))
19}
20
21pub fn is_installed_at(library_name: &str, install_dir: &Path) -> bool {
22 get_compiled_lib_path(library_name, install_dir).exists()
23}
24
25#[derive(Debug)]
26pub enum InstallationStatus {
27 Downloading(String),
28 Patching,
29 Compiling(String),
30}
31
32pub fn install_parser(
33 download_cmd: &str,
34 library_name: &str,
35 install_dir: &Path,
36 mut report_progress: Option<impl FnMut(InstallationStatus)>,
37) -> Result<PathBuf> {
38 if let Some(report_progress) = &mut report_progress {
39 report_progress(InstallationStatus::Downloading("Start".to_string()));
40 }
41 download_parser(download_cmd, install_dir, &mut report_progress)?;
42 if let Some(report_progress) = &mut report_progress {
43 report_progress(InstallationStatus::Downloading("Done".to_string()));
44 }
45
46 if let Some(report_progress) = &mut report_progress {
47 report_progress(InstallationStatus::Patching);
48 }
49 disable_language_fn_mangle(install_dir).context("failed to disable language fn mangle")?;
50
51 if let Some(report_progress) = &mut report_progress {
52 report_progress(InstallationStatus::Compiling("Start".to_string()));
53 }
54 compile_parser(install_dir, &mut report_progress)?;
55
56 Ok(get_compiled_lib_path(library_name, install_dir))
57}
58
59fn _run_cmd_with_progress(
60 cmd: &mut Command,
61 report_progress: &mut Option<impl FnMut(String)>,
62) -> Result<Output> {
63 let mut child = cmd
64 .stdout(std::process::Stdio::piped())
65 .stderr(std::process::Stdio::piped())
66 .spawn()
67 .context("failed to start cmd")?;
68
69 let stdout = child.stdout.take().context("failed to read stdout")?;
70 let stderr = child.stderr.take().context("failed to read stdout")?;
71
72 let mut out_lines = BufReader::new(stdout).lines();
73 let mut error_lines = BufReader::new(stderr).lines();
74
75 while child.try_wait()?.is_none() {
76 while let Some(Ok(line)) = out_lines.next() {
77 if let Some(report_progress) = report_progress {
78 report_progress(line);
79 }
80 }
81 while let Some(Ok(line)) = error_lines.next() {
82 if let Some(report_progress) = report_progress {
83 report_progress(line);
84 }
85 }
86 }
87
88 while let Some(Ok(line)) = out_lines.next() {
90 if let Some(report_progress) = report_progress {
91 report_progress(line);
92 }
93 }
94 while let Some(Ok(line)) = error_lines.next() {
95 if let Some(report_progress) = report_progress {
96 report_progress(line);
97 }
98 }
99
100 child.wait_with_output().context("failed to run cmd")
101}
102
103fn download_parser(
104 download_cmd: &str,
105 target_dir: &Path,
106 report_progress: &mut Option<impl FnMut(InstallationStatus)>,
107) -> Result<()> {
108 let cmd = download_cmd
109 .split_ascii_whitespace()
110 .next()
111 .context("got empty download command")?;
112 let args: Vec<_> = download_cmd.split_ascii_whitespace().skip(1).collect();
113
114 if let Some(report_progress) = report_progress {
115 report_progress(InstallationStatus::Downloading(format!(
116 "Running command '{} {}'",
117 download_cmd,
118 target_dir.to_string_lossy()
119 )))
120 }
121
122 let mut outputs = vec![];
123 let mut report_progress = report_progress.as_mut().map(|report_progress| {
124 |line: String| {
125 outputs.push(line.clone());
126 report_progress(InstallationStatus::Downloading(line));
127 }
128 });
129
130 let output = _run_cmd_with_progress(
131 Command::new(cmd).args(args).arg(target_dir),
132 &mut report_progress,
133 )?;
134
135 if !output.status.success() {
136 Err(anyhow!("failed to download parser: {}", outputs.join("\n")))
137 } else {
138 Ok(())
139 }
140}
141
142fn disable_language_fn_mangle(parser_dir: &Path) -> Result<()> {
143 let lib_file = parser_dir.join("bindings").join("rust").join("lib.rs");
144 let lib_file_buf = read(&lib_file).context("failed to read lib.rs file")?;
145 let lib_file_src = from_utf8(&lib_file_buf).context("failed to decode lib.rs content")?;
146
147 let pattern = "pub fn language";
148 let no_mangle_fn_locations = lib_file_src
149 .match_indices(pattern)
150 .map(|(byte, _)| byte)
151 .collect::<Vec<_>>();
152
153 let mut new_lib_file_src = lib_file_src.to_string();
154
155 for loc in no_mangle_fn_locations.into_iter().rev() {
156 new_lib_file_src.insert_str(loc, "#[no_mangle]\n")
157 }
158
159 std::fs::write(lib_file, new_lib_file_src).context("failed to write new lib file src")
160}
161
162fn compile_parser(
163 parser_dir: &Path,
164 report_progress: &mut Option<impl FnMut(InstallationStatus)>,
165) -> Result<()> {
166 let cmd = BUILD_CMD.split_ascii_whitespace().next().unwrap();
167 let args: Vec<_> = BUILD_CMD.split_ascii_whitespace().skip(1).collect();
168
169 let mut outputs = vec![];
170 let mut report_progress = report_progress.as_mut().map(|report_progress| {
171 |line: String| {
172 outputs.push(line.clone());
173 report_progress(InstallationStatus::Compiling(line));
174 }
175 });
176
177 let output = _run_cmd_with_progress(
178 Command::new(cmd).args(args).current_dir(parser_dir),
179 &mut report_progress,
180 )?;
181
182 if !output.status.success() {
183 Err(anyhow!("failed to compile parser: {}", outputs.join("\n")))
184 } else {
185 Ok(())
186 }
187}