1use crate::PRELUDE_REGISTRY_KEY;
21use mlua::{Function, Lua, Table, Value};
22use mlua_pkg::Resolver;
23use mlua_pkg::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs, SymlinkAwareSandbox};
24use std::path::{Path, PathBuf};
25use std::sync::atomic::{AtomicBool, Ordering};
26
27pub use mlua_pkg;
28
29pub struct TealResolver {
32 sandbox: Box<dyn SandboxedFs>,
33 root: Option<PathBuf>,
34 path_added: AtomicBool,
35 module_separator: char,
36}
37
38impl TealResolver {
39 pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
41 let root = root.into();
42 Ok(Self {
43 sandbox: Box::new(FsSandbox::new(&root)?),
44 root: Some(root),
45 path_added: AtomicBool::new(false),
46 module_separator: '.',
47 })
48 }
49
50 pub fn new_symlink_aware(root: impl Into<PathBuf>) -> Result<Self, InitError> {
52 let root = root.into();
53 Ok(Self {
54 sandbox: Box::new(SymlinkAwareSandbox::new(&root)?),
55 root: Some(root),
56 path_added: AtomicBool::new(false),
57 module_separator: '.',
58 })
59 }
60
61 pub fn with_sandbox(sandbox: impl SandboxedFs + 'static, root: Option<PathBuf>) -> Self {
64 Self {
65 sandbox: Box::new(sandbox),
66 root,
67 path_added: AtomicBool::new(false),
68 module_separator: '.',
69 }
70 }
71
72 pub fn with_module_separator(mut self, sep: char) -> Self {
73 self.module_separator = sep;
74 self
75 }
76
77 fn prelude(lua: &Lua) -> mlua::Result<Table> {
78 lua.named_registry_value::<Table>(PRELUDE_REGISTRY_KEY)
79 .map_err(|_| mlua::Error::external(
80 "htl::pkg::TealResolver: this Lua has no htl prelude (create it with Htl::new / Htl::from_lua)",
81 ))
82 }
83
84 fn ensure_checker_path(&self, lua: &Lua, h: &Table) -> mlua::Result<()> {
87 if self.path_added.swap(true, Ordering::Relaxed) {
88 return Ok(());
89 }
90 if let Some(root) = &self.root {
91 let f: Function = h.get("add_path")?;
92 f.call::<()>(root.to_string_lossy().as_ref())?;
93 }
94 let _ = lua;
95 Ok(())
96 }
97
98 fn has_lua_sibling(&self, relative: &str) -> bool {
99 for cand in [format!("{relative}.lua"), format!("{relative}/init.lua")] {
100 if let Ok(Some(_)) = self.sandbox.read(Path::new(&cand)) {
101 return true;
102 }
103 }
104 false
105 }
106
107 fn load_teal(&self, lua: &Lua, h: &Table, src: &str, resolved: &Path, name: &str) -> mlua::Result<Value> {
108 let gen_fn: Function = h.get("gen_string")?;
109 let (code, info): (Option<String>, Table) = gen_fn.call((src, resolved.to_string_lossy().as_ref()))?;
110 let Some(code) = code else {
111 let errors: Table = info.get("errors")?;
112 let msgs: Vec<String> = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
113 return Err(mlua::Error::external(TealResolveError::TypeCheck {
114 module: name.to_string(),
115 errors: msgs,
116 }));
117 };
118 let chunk = lua
119 .load(code)
120 .set_name(format!("@{}", resolved.display()))
121 .into_function()?;
122 chunk.call::<Value>((name, resolved.to_string_lossy().as_ref()))
123 }
124}
125
126#[derive(Debug, Clone)]
134pub struct Project {
135 pub root: PathBuf,
136 pub manifest: PathBuf,
137 pub lockfile: PathBuf,
138 pub pkgs_dir: PathBuf,
139 pub vendored: PathBuf,
140}
141
142pub const MANIFEST_NAME: &str = "mlua-pkg.toml";
143pub const LOCKFILE_NAME: &str = "mlua-pkg.lock";
144
145impl Project {
146 pub fn find(start: &Path) -> Option<Self> {
148 let mut dir = if start.is_dir() { start.to_path_buf() } else { crate::parent_dir(start) };
149 if let Ok(abs) = std::fs::canonicalize(&dir) {
150 dir = abs;
151 }
152 loop {
153 let manifest = dir.join(MANIFEST_NAME);
154 if manifest.is_file() {
155 return Some(Self::at(&dir));
156 }
157 if !dir.pop() {
158 return None;
159 }
160 }
161 }
162
163 pub fn at(root: &Path) -> Self {
165 let pkgs_dir = match std::env::var("MLUA_PKG_DIR") {
166 Ok(p) if !p.is_empty() => PathBuf::from(p),
167 _ if root.join("target").is_dir() => root.join("target").join("mlua-pkgs"),
168 _ => root.join(".mlua-pkgs"),
169 };
170 Self {
171 root: root.to_path_buf(),
172 manifest: root.join(MANIFEST_NAME),
173 lockfile: root.join(LOCKFILE_NAME),
174 vendored: pkgs_dir.join("vendored"),
175 pkgs_dir,
176 }
177 }
178
179 pub fn installed(&self) -> bool {
181 self.lockfile.is_file()
182 }
183
184 pub fn teal_resolver(&self) -> Result<TealResolver, InitError> {
187 let _ = std::fs::create_dir_all(&self.vendored);
188 TealResolver::new_symlink_aware(&self.vendored)
189 }
190
191 pub fn vendored_resolver(&self) -> anyhow::Result<mlua_pkg::resolvers::VendoredResolver> {
193 if self.installed() {
194 Ok(mlua_pkg::resolvers::VendoredResolver::from_lockfile(&self.lockfile, &self.vendored)?)
195 } else {
196 let _ = std::fs::create_dir_all(&self.vendored);
197 Ok(mlua_pkg::resolvers::VendoredResolver::new(&self.vendored)?)
198 }
199 }
200
201 pub fn registry(&self) -> anyhow::Result<mlua_pkg::Registry> {
204 let mut reg = mlua_pkg::Registry::new();
205 reg.add(self.teal_resolver()?);
206 reg.add(self.vendored_resolver()?);
207 Ok(reg)
208 }
209}
210
211impl crate::Htl {
212 pub fn apply_project(&self, p: &Project) -> anyhow::Result<()> {
215 let _ = std::fs::create_dir_all(&p.vendored);
216 self.add_path(&p.vendored)?;
217 Ok(())
218 }
219}
220
221#[derive(Debug)]
223pub enum TealResolveError {
224 TypeCheck { module: String, errors: Vec<String> },
225 Read { module: String, source: ReadError },
226}
227
228impl std::fmt::Display for TealResolveError {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 match self {
231 Self::TypeCheck { module, errors } => {
232 write!(f, "Teal type check failed for module '{module}':")?;
233 for e in errors {
234 write!(f, "\n {e}")?;
235 }
236 Ok(())
237 }
238 Self::Read { module, source } => write!(f, "reading module '{module}': {source}"),
239 }
240 }
241}
242
243impl std::error::Error for TealResolveError {}
244
245impl Resolver for TealResolver {
246 fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<Value>> {
247 let relative = name.replace(self.module_separator, "/");
248 let candidates = [
249 (format!("{relative}.tl"), false),
250 (format!("{relative}/init.tl"), false),
251 (format!("{relative}.d.tl"), true),
252 ];
253 let h = match Self::prelude(lua) {
254 Ok(h) => h,
255 Err(e) => return Some(Err(e)),
256 };
257 if let Err(e) = self.ensure_checker_path(lua, &h) {
258 return Some(Err(e));
259 }
260 for (candidate, type_only) in &candidates {
261 match self.sandbox.read(Path::new(candidate)) {
262 Ok(Some(file)) => {
263 if *type_only {
264 if self.has_lua_sibling(&relative) {
268 return None;
269 }
270 return Some(lua.create_table().map(Value::Table));
272 }
273 return Some(self.load_teal(lua, &h, &file.content, &file.resolved_path, name));
274 }
275 Ok(None) => continue,
276 Err(source) => {
277 return Some(Err(mlua::Error::external(TealResolveError::Read {
278 module: name.to_string(),
279 source,
280 })));
281 }
282 }
283 }
284 None
285 }
286}