Skip to main content

lux_lib/build/
treesitter_parser.rs

1use crate::build::backend::{BuildBackend, BuildInfo, RunBuildArgs};
2use crate::fs;
3use crate::lua_rockspec::TreesitterParserBuildSpec;
4use crate::lua_version::LuaVersionUnset;
5use crate::tree::InstallTree;
6use miette::Diagnostic;
7use std::num::ParseIntError;
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10use tracing::info_span;
11use tree_sitter_generate::GenerateError;
12
13const DEFAULT_GENERATE_ABI_VERSION: usize = tree_sitter::LANGUAGE_VERSION;
14
15#[derive(Error, Debug, Diagnostic)]
16#[non_exhaustive]
17pub enum Error {
18    #[error(transparent)]
19    #[diagnostic(transparent)]
20    LuaVersionUnset(#[from] LuaVersionUnset),
21    #[error("invalid TREE_SITTER_LANGUAGE_VERSION")]
22    #[diagnostic(help(
23        "check the value of the TREE_SITTER_LANGUAGE_VERSION environment variable."
24    ))]
25    ParseAbiVersion(#[from] ParseIntError),
26    #[error("error generating tree-sitter grammar")]
27    #[diagnostic(help("check the grammar source files for errors."))]
28    Generate(#[from] GenerateError),
29    #[error("error compiling the tree-sitter grammar: {0}")]
30    #[diagnostic(help("run `lx debug toolchains` to check available build tools."))]
31    TreesitterCompileError(String),
32    #[error(transparent)]
33    #[diagnostic(transparent)]
34    Fs(#[from] fs::Error),
35}
36
37impl BuildBackend for TreesitterParserBuildSpec {
38    type Err = Error;
39
40    #[tracing::instrument(name = "treesitter_parser::run", skip_all, level = "debug")]
41    async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
42    where
43        T: InstallTree,
44    {
45        let package = args.package;
46        let tree = args.tree;
47        let layout = tree.layout_for(package);
48        let build_dir = args.build_dir;
49        let build_dir = self
50            .location
51            .map(|dir| build_dir.join(dir))
52            .unwrap_or(build_dir.to_path_buf());
53        if self.generate {
54            let span = info_span!("Generating tree-sitter parser");
55            let _enter = span.enter();
56            let abi_version = match std::env::var("TREE_SITTER_LANGUAGE_VERSION") {
57                Ok(v) => v.parse()?,
58                Err(_) => DEFAULT_GENERATE_ABI_VERSION,
59            };
60            tracing::debug!("ABI version: {abi_version}");
61            let out_path: Option<PathBuf> = None;
62            let grammar_path: Option<PathBuf> = None;
63            let mut diagnostics = Vec::new();
64            tree_sitter_generate::generate_parser_in_directory(
65                &build_dir,
66                out_path,
67                grammar_path,
68                abi_version,
69                None,
70                None,
71                true,
72                tree_sitter_generate::OptLevel::default(),
73                &mut diagnostics,
74            )?;
75            for diagnostic in diagnostics {
76                tracing::debug!("tree-sitter-{}: {diagnostic}", &self.lang);
77            }
78        }
79        if self.parser {
80            build_parser(&build_dir, &layout.etc.join("parser"), &self.lang).await?;
81        }
82
83        let queries_dir = layout.etc.join("queries").join(&self.lang);
84        install_queries(&build_dir, &queries_dir, &self.lang, self.queries).await?;
85
86        Ok(BuildInfo::default())
87    }
88}
89
90fn is_query_file(path: &Path) -> bool {
91    path.extension().is_some_and(|ext| ext == "scm")
92}
93
94#[tracing::instrument(level = "trace")]
95async fn install_inline_queries(
96    queries_dir: &Path,
97    queries: std::collections::HashMap<PathBuf, String>,
98) -> Result<(), Error> {
99    fs::tokio::create_dir_all(queries_dir).await?;
100    for (path, content) in queries {
101        let dest = queries_dir.join(path);
102        fs::tokio::write(&dest, content).await?;
103    }
104    Ok(())
105}
106
107#[tracing::instrument(level = "trace")]
108async fn install_source_queries(
109    source_queries_dir: &Path,
110    queries_dir: &Path,
111) -> Result<(), Error> {
112    fs::tokio::create_dir_all(queries_dir).await?;
113    let mut entries = fs::tokio::read_dir(source_queries_dir).await?;
114    while let Some(entry) = entries
115        .next_entry()
116        .await
117        .map_err(|source| fs::Error::ReadDir {
118            path: source_queries_dir.to_path_buf(),
119            source,
120        })?
121    {
122        let path = entry.path();
123        if let Some(filename) = path.file_name().filter(|_| is_query_file(&path)) {
124            let dest = queries_dir.join(filename);
125            fs::tokio::copy(&path, &dest).await?;
126        }
127    }
128    Ok(())
129}
130
131#[tracing::instrument(level = "trace")]
132async fn install_queries(
133    build_dir: &Path,
134    queries_dir: &Path,
135    lang: &str,
136    queries: std::collections::HashMap<PathBuf, String>,
137) -> Result<(), Error> {
138    if !queries.is_empty() {
139        install_inline_queries(queries_dir, queries).await
140    } else {
141        let source_queries_dir = build_dir.join("queries");
142        let lang_queries_dir = source_queries_dir.join(lang);
143        if source_queries_dir.is_dir() && !lang_queries_dir.is_dir() {
144            install_source_queries(&source_queries_dir, queries_dir).await
145        } else {
146            Ok(())
147        }
148    }
149}
150
151#[tracing::instrument(
152    name = "Compiling tree-sitter parser",
153    level = "info",
154    skip_all,
155    fields(language = lang),
156)]
157async fn build_parser(build_dir: &Path, parser_dir: &Path, lang: &str) -> Result<(), Error> {
158    fs::tokio::create_dir_all(parser_dir).await?;
159    let loader = tree_sitter_loader::Loader::with_parser_lib_path(build_dir.to_path_buf());
160    let output_path = parser_dir.join(format!("{}.{}", lang, std::env::consts::DLL_EXTENSION));
161    // HACK(vhyrro): `tree-sitter-loader` will only use a temp directory instead of a
162    // lockfile if a `CROSS_RUNNER` env variable is set (why??). We should probably make a
163    // PR fixing this with a flag. This should make-do for now: theoretically, this could
164    // break since it's on an async thread, but in practice this will never be executed
165    // many times during the lifetime of the `lx` binary.
166    std::env::set_var("CROSS_RUNNER", "");
167    loader
168        .compile_parser_at_path(build_dir, output_path, &[])
169        .map_err(|err| Error::TreesitterCompileError(err.to_string()))
170}