tree_sitter_installer/
parser_installer.rs

1use 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    // final lines
89    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}