use std::collections::HashMap;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::RwLock;
use cc::Build;
use lazy_static::lazy_static;
use libloading::Library;
use tree_sitter::Language;
use walkdir::WalkDir;
use crate::Error;
use crate::queries::{has_extension, language_name};
lazy_static! {
static ref LOADED_LANGUAGES: RwLock<HashMap<PathBuf, (Library, Language)>> = RwLock::new(HashMap::new());
}
pub fn dyload_language(path: impl AsRef<Path>) -> Result<Language, Error> {
let path = path.as_ref();
if let Some((_, language)) = LOADED_LANGUAGES.read().unwrap().get(path) {
return Ok(*language);
}
let (library, language) = dyload_new_language(path)?;
LOADED_LANGUAGES.write().unwrap().insert(path.to_path_buf(), (library, language));
Ok(language)
}
fn dyload_new_language(path: &Path) -> Result<(Library, Language), Error> {
let dylib_path = dylib_path(path);
let symbol_name = language_name(path)?;
build_dylib_if_needed(path, &dylib_path)?;
eprintln!("Dynamically loading {}...", symbol_name);
unsafe {
let dylib = Library::new(&dylib_path.canonicalize()?).map_err(Error::LoadDylibFailed)?;
let language_fn = dylib
.get::<fn() -> Language>(symbol_name.as_bytes())
.map_err(Error::LoadDylibSymbolFailed)?;
let language = language_fn();
Ok((dylib, language))
}
}
fn build_dylib_if_needed(path: &Path, dylib_path: &Path) -> Result<(), Error> {
if !dylib_path.exists() {
build_dylib(path, dylib_path)?;
}
if !dylib_path.exists() {
return Err(Error::MissingDylib);
}
Ok(())
}
fn dylib_path(path: &Path) -> PathBuf {
let mut path = path.join("target/c-release-so/libtree-sitter");
if cfg!(target_os = "macos") {
path.set_extension("dylib");
} else if cfg!(target_os = "windows") {
path.set_extension("dll");
} else {
path.set_extension("so");
}
path
}
fn build_dylib(path: &Path, dylib_path: &Path) -> Result<(), Error> {
let dylib_dir = dylib_path.parent().unwrap();
create_dir_all(dylib_dir)?;
let src_dir = path.join("src");
eprintln!("Building {}...", dylib_path.display());
let sources = src_dir.read_dir()?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| has_extension(p, "c"));
Build::new()
.host(env!("HOST"))
.target(env!("TARGET"))
.opt_level_str(env!("OPT_LEVEL"))
.debug(env!("DEBUG") == "true")
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs")
.include(&src_dir)
.files(sources)
.shared_flag(true)
.cargo_metadata(false)
.out_dir(&dylib_dir)
.try_compile("tree-sitter")?;
if cfg!(target_os = "macos") {
eprintln!("Dynamic linking {}...", dylib_path.display());
let status = Command::new("/usr/bin/clang")
.args(["-dynamiclib", "-undefined", "error", "-o"])
.arg(&dylib_path)
.args(find_object_files_in(dylib_dir))
.status()
.map_err(Error::LinkDylibCmdFailed)?;
if !status.success() {
return Err(Error::LinkDylibFailed { exit_status: status });
}
}
Ok(())
}
fn find_object_files_in(dir: &Path) -> impl Iterator<Item=PathBuf> {
WalkDir::new(dir)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_file())
.filter(|entry| has_extension(entry.path(), "o"))
.map(|entry| entry.into_path())
}