use crate::PRELUDE_REGISTRY_KEY;
use mlua::{Function, Lua, Table, Value};
use mlua_pkg::Resolver;
use mlua_pkg::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs, SymlinkAwareSandbox};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
pub use mlua_pkg;
pub struct TealResolver {
sandbox: Box<dyn SandboxedFs>,
root: Option<PathBuf>,
path_added: AtomicBool,
module_separator: char,
}
impl TealResolver {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let root = root.into();
Ok(Self {
sandbox: Box::new(FsSandbox::new(&root)?),
root: Some(root),
path_added: AtomicBool::new(false),
module_separator: '.',
})
}
pub fn new_symlink_aware(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let root = root.into();
Ok(Self {
sandbox: Box::new(SymlinkAwareSandbox::new(&root)?),
root: Some(root),
path_added: AtomicBool::new(false),
module_separator: '.',
})
}
pub fn with_sandbox(sandbox: impl SandboxedFs + 'static, root: Option<PathBuf>) -> Self {
Self {
sandbox: Box::new(sandbox),
root,
path_added: AtomicBool::new(false),
module_separator: '.',
}
}
pub fn with_module_separator(mut self, sep: char) -> Self {
self.module_separator = sep;
self
}
fn prelude(lua: &Lua) -> mlua::Result<Table> {
lua.named_registry_value::<Table>(PRELUDE_REGISTRY_KEY)
.map_err(|_| mlua::Error::external(
"htl::pkg::TealResolver: this Lua has no htl prelude (create it with Htl::new / Htl::from_lua)",
))
}
fn ensure_checker_path(&self, lua: &Lua, h: &Table) -> mlua::Result<()> {
if self.path_added.swap(true, Ordering::Relaxed) {
return Ok(());
}
if let Some(root) = &self.root {
let f: Function = h.get("add_path")?;
f.call::<()>(root.to_string_lossy().as_ref())?;
}
let _ = lua;
Ok(())
}
fn has_lua_sibling(&self, relative: &str) -> bool {
for cand in [format!("{relative}.lua"), format!("{relative}/init.lua")] {
if let Ok(Some(_)) = self.sandbox.read(Path::new(&cand)) {
return true;
}
}
false
}
fn load_teal(&self, lua: &Lua, h: &Table, src: &str, resolved: &Path, name: &str) -> mlua::Result<Value> {
let gen_fn: Function = h.get("gen_string")?;
let (code, info): (Option<String>, Table) = gen_fn.call((src, resolved.to_string_lossy().as_ref()))?;
let Some(code) = code else {
let errors: Table = info.get("errors")?;
let msgs: Vec<String> = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
return Err(mlua::Error::external(TealResolveError::TypeCheck {
module: name.to_string(),
errors: msgs,
}));
};
let chunk = lua
.load(code)
.set_name(format!("@{}", resolved.display()))
.into_function()?;
chunk.call::<Value>((name, resolved.to_string_lossy().as_ref()))
}
}
#[derive(Debug, Clone)]
pub struct Project {
pub root: PathBuf,
pub manifest: PathBuf,
pub lockfile: PathBuf,
pub pkgs_dir: PathBuf,
pub vendored: PathBuf,
}
pub const MANIFEST_NAME: &str = "mlua-pkg.toml";
pub const LOCKFILE_NAME: &str = "mlua-pkg.lock";
impl Project {
pub fn find(start: &Path) -> Option<Self> {
let mut dir = if start.is_dir() { start.to_path_buf() } else { crate::parent_dir(start) };
if let Ok(abs) = std::fs::canonicalize(&dir) {
dir = abs;
}
loop {
let manifest = dir.join(MANIFEST_NAME);
if manifest.is_file() {
return Some(Self::at(&dir));
}
if !dir.pop() {
return None;
}
}
}
pub fn at(root: &Path) -> Self {
let pkgs_dir = match std::env::var("MLUA_PKG_DIR") {
Ok(p) if !p.is_empty() => PathBuf::from(p),
_ if root.join("target").is_dir() => root.join("target").join("mlua-pkgs"),
_ => root.join(".mlua-pkgs"),
};
Self {
root: root.to_path_buf(),
manifest: root.join(MANIFEST_NAME),
lockfile: root.join(LOCKFILE_NAME),
vendored: pkgs_dir.join("vendored"),
pkgs_dir,
}
}
pub fn installed(&self) -> bool {
self.lockfile.is_file()
}
pub fn teal_resolver(&self) -> Result<TealResolver, InitError> {
let _ = std::fs::create_dir_all(&self.vendored);
TealResolver::new_symlink_aware(&self.vendored)
}
pub fn vendored_resolver(&self) -> anyhow::Result<mlua_pkg::resolvers::VendoredResolver> {
if self.installed() {
Ok(mlua_pkg::resolvers::VendoredResolver::from_lockfile(&self.lockfile, &self.vendored)?)
} else {
let _ = std::fs::create_dir_all(&self.vendored);
Ok(mlua_pkg::resolvers::VendoredResolver::new(&self.vendored)?)
}
}
pub fn registry(&self) -> anyhow::Result<mlua_pkg::Registry> {
let mut reg = mlua_pkg::Registry::new();
reg.add(self.teal_resolver()?);
reg.add(self.vendored_resolver()?);
Ok(reg)
}
}
impl crate::Htl {
pub fn apply_project(&self, p: &Project) -> anyhow::Result<()> {
let _ = std::fs::create_dir_all(&p.vendored);
self.add_path(&p.vendored)?;
Ok(())
}
}
#[derive(Debug)]
pub enum TealResolveError {
TypeCheck { module: String, errors: Vec<String> },
Read { module: String, source: ReadError },
}
impl std::fmt::Display for TealResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TypeCheck { module, errors } => {
write!(f, "Teal type check failed for module '{module}':")?;
for e in errors {
write!(f, "\n {e}")?;
}
Ok(())
}
Self::Read { module, source } => write!(f, "reading module '{module}': {source}"),
}
}
}
impl std::error::Error for TealResolveError {}
impl Resolver for TealResolver {
fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<Value>> {
let relative = name.replace(self.module_separator, "/");
let candidates = [
(format!("{relative}.tl"), false),
(format!("{relative}/init.tl"), false),
(format!("{relative}.d.tl"), true),
];
let h = match Self::prelude(lua) {
Ok(h) => h,
Err(e) => return Some(Err(e)),
};
if let Err(e) = self.ensure_checker_path(lua, &h) {
return Some(Err(e));
}
for (candidate, type_only) in &candidates {
match self.sandbox.read(Path::new(candidate)) {
Ok(Some(file)) => {
if *type_only {
if self.has_lua_sibling(&relative) {
return None;
}
return Some(lua.create_table().map(Value::Table));
}
return Some(self.load_teal(lua, &h, &file.content, &file.resolved_path, name));
}
Ok(None) => continue,
Err(source) => {
return Some(Err(mlua::Error::external(TealResolveError::Read {
module: name.to_string(),
source,
})));
}
}
}
None
}
}