algocline_engine/variant_pkg.rs
1use std::path::{Path, PathBuf};
2
3use mlua_pkg::{
4 resolvers::{FsResolver, PrefixResolver},
5 Registry, Resolver,
6};
7
8use crate::resolver_factory::make_resolver;
9
10/// A variant-scoped package pinned to an explicit `(name, pkg_dir)` mapping.
11///
12/// Sourced from `alc.local.toml` (worktree-scoped, gitignored). Unlike the
13/// global `~/.algocline/packages/` layout — where `FsResolver(parent_dir)`
14/// implicitly maps `require("X")` to `<parent>/X/init.lua` via directory
15/// names — variant entries declare the require name explicitly so the on-disk
16/// directory name does not have to match.
17///
18/// Resolution rules (built by [`register_variant_pkgs`]):
19/// - `require("{name}")` → `{pkg_dir}/init.lua`
20/// - `require("{name}.{sub}")` → `{pkg_dir}/{sub}.lua` or `{pkg_dir}/{sub}/init.lua`
21#[derive(Clone, Debug)]
22pub struct VariantPkg {
23 pub name: String,
24 pub pkg_dir: PathBuf,
25}
26
27impl VariantPkg {
28 pub fn new(name: impl Into<String>, pkg_dir: impl Into<PathBuf>) -> Self {
29 Self {
30 name: name.into(),
31 pkg_dir: pkg_dir.into(),
32 }
33 }
34}
35
36/// Resolver for `require("{name}")` — the package root of a variant pkg.
37///
38/// `resolve()` reads `{init_path}` from disk each time it fires. Within a
39/// single Lua session, `package.loaded` caches the first result, so this
40/// resolver runs at most once per name per session. Freshness across edits
41/// comes from the fact that each `alc_run` builds a fresh session VM: the
42/// disk read at resolve time guarantees the new session sees the current
43/// `init.lua` content. Submodule lookups (`{name}.{sub}`) are delegated to
44/// a separate `PrefixResolver(name, FsResolver(pkg_dir))`.
45struct VariantRootResolver {
46 name: String,
47 init_path: PathBuf,
48}
49
50impl Resolver for VariantRootResolver {
51 fn resolve(&self, lua: &mlua::Lua, req: &str) -> Option<mlua::Result<mlua::Value>> {
52 if req != self.name {
53 return None;
54 }
55 let content = match std::fs::read_to_string(&self.init_path) {
56 Ok(c) => c,
57 Err(e) => {
58 // `{:?}` preserves non-UTF-8 bytes in the path for debugging;
59 // `display()` would replace them with U+FFFD and obscure the
60 // actual offending filename when the OS locale is misaligned.
61 return Some(Err(mlua::Error::external(format!(
62 "variant pkg '{}': failed to read {:?}: {e}",
63 self.name, self.init_path
64 ))));
65 }
66 };
67 Some(
68 lua.load(content)
69 .set_name(self.init_path.display().to_string())
70 .eval(),
71 )
72 }
73}
74
75/// Build a sandboxed `FsResolver` for a variant pkg's submodule lookups.
76///
77/// Delegates to the crate-level [`make_resolver`] factory so the sandbox
78/// policy (default `SymlinkAwareSandbox`, strict under `ALC_PKG_STRICT=1`)
79/// stays identical to the resolvers `Executor` and `alc.fork` construct.
80fn make_submodule_resolver(pkg_dir: &Path) -> Option<FsResolver> {
81 make_resolver(pkg_dir)
82}
83
84/// Register both the root resolver and the submodule prefix resolver for
85/// each variant pkg. Variant pkgs should be inserted before any global
86/// library resolvers so that `alc.local.toml` overrides win over
87/// `~/.algocline/packages/`.
88pub(crate) fn register_variant_pkgs(reg: &mut Registry, variant_pkgs: &[VariantPkg]) {
89 for vp in variant_pkgs {
90 let init_path = vp.pkg_dir.join("init.lua");
91 reg.add(VariantRootResolver {
92 name: vp.name.clone(),
93 init_path,
94 });
95 match make_submodule_resolver(&vp.pkg_dir) {
96 Some(inner) => {
97 reg.add(PrefixResolver::new(vp.name.clone(), inner));
98 }
99 None => {
100 tracing::warn!(
101 "variant pkg '{}': sandbox init failed for {}, submodules disabled",
102 vp.name,
103 vp.pkg_dir.display()
104 );
105 }
106 }
107 }
108}