1 Commits

Author SHA1 Message Date
Axel Meyer
4047df286c dep-fetcher 0.1.2: multi-path resolution + accept untagged dev-state when manifest matches pin
- resolve_libs_dirs / resolve_modules_dirs return arrays of candidate
  paths (priority: opts override > opts.install_root-derived >
  engine.libs_dirs() / modules_dirs() > SPOREL_*_DIR env >
  install_root-derived fallback). first_existing walks the array to
  find the candidate that actually contains the requested manifest,
  so dev-tree layouts with libs and modules in different roots work
  without forcing the caller to pre-flatten.
- New case G: when describe fails because the repo has no tags at
  all (common during dev between tagged releases), accept the lib
  if its manifest.version matches the pin. Previously errored as
  "describe-fail" which made the pre-launch dep-check unusable in
  dev-tree runs.
- Dep pin: lib-core.git 0.1.0 -> 0.2.0 (token-auth + set_token).
2026-05-31 14:08:57 +02:00
2 changed files with 153 additions and 29 deletions

178
init.lua
View File

@@ -57,6 +57,85 @@ local function resolve_install_root(opts)
return os.getenv("SPOREL_INSTALL_ROOT") return os.getenv("SPOREL_INSTALL_ROOT")
end end
-- Slice 6.1: libs and modules can live in many roots simultaneously
-- (multi-path engine discovery, dev-tree SPOREL_*_DIR overrides). Each
-- resolver returns an ARRAY of candidate paths (priority-ordered);
-- callers iterate to find the first that contains the requested file.
--
-- Priority order:
-- 1. opts.libs_dir / opts.modules_dir explicit override (always wins)
-- 2. opts.install_root explicit override -> derive `<root>/{libs,modules}`
-- (tests pass install_root pointing at a fixture; the engine
-- bindings would otherwise return the real engine paths, masking
-- the fixture)
-- 3. engine.{libs,modules}_dirs() — runtime discovery candidates
-- 4. SPOREL_{LIBS,MODULES}_DIR env var
-- 5. Auto-resolved install_root -> `<root>/{libs,modules}` (last resort)
local function resolve_libs_dirs(opts, install_root)
local list = {}
if opts and opts.libs_dir then table.insert(list, opts.libs_dir) end
if opts and opts.install_root then
table.insert(list, opts.install_root .. "/libs")
end
if type(engine) == "table"
and type(engine.libs_dirs) == "function" then
local ok, dirs = pcall(engine.libs_dirs)
if ok and type(dirs) == "table" then
for _, d in ipairs(dirs) do
if type(d) == "string" and d ~= "" then
table.insert(list, d)
end
end
end
end
local env = os.getenv("SPOREL_LIBS_DIR")
if env and env ~= "" then table.insert(list, env) end
if install_root then table.insert(list, install_root .. "/libs") end
return list
end
local function resolve_modules_dirs(opts, install_root)
local list = {}
if opts and opts.modules_dir then table.insert(list, opts.modules_dir) end
if opts and opts.install_root then
table.insert(list, opts.install_root .. "/modules")
end
if type(engine) == "table"
and type(engine.modules_dirs) == "function" then
local ok, dirs = pcall(engine.modules_dirs)
if ok and type(dirs) == "table" then
for _, d in ipairs(dirs) do
if type(d) == "string" and d ~= "" then
table.insert(list, d)
end
end
end
end
local env = os.getenv("SPOREL_MODULES_DIR")
if env and env ~= "" then table.insert(list, env) end
if install_root then table.insert(list, install_root .. "/modules") end
return list
end
-- Helper: filesystem existence check via io.open. Used to pick the
-- first multi-path candidate that actually has the requested file.
local function file_exists(path)
local f = io.open(path, "rb")
if not f then return false end
f:close()
return true
end
-- Given a list of candidate dirs and a relative file-path, return the
-- first <dir>/<rel> that exists, or nil if none.
local function first_existing(dirs, rel_path)
for _, d in ipairs(dirs) do
local p = d .. "/" .. rel_path
if file_exists(p) then return p, d end
end
return nil, nil
end
local function resolve_gitea_base(opts) local function resolve_gitea_base(opts)
-- 1. explicit override -- 1. explicit override
if opts and opts.gitea_base then return opts.gitea_base end if opts and opts.gitea_base then return opts.gitea_base end
@@ -73,6 +152,8 @@ local function resolve_gitea_base(opts)
end end
M._resolve_install_root = resolve_install_root -- exposed for tests M._resolve_install_root = resolve_install_root -- exposed for tests
M._resolve_libs_dirs = resolve_libs_dirs -- exposed for tests
M._resolve_modules_dirs = resolve_modules_dirs -- exposed for tests
M._resolve_gitea_base = resolve_gitea_base -- exposed for tests M._resolve_gitea_base = resolve_gitea_base -- exposed for tests
-- ===================================================================== -- =====================================================================
@@ -312,9 +393,12 @@ local function id_to_path(lib_id)
end end
M._id_to_path = id_to_path M._id_to_path = id_to_path
-- Locate a lib's manifest under <install_root>/libs/<id-path>/. -- Find a lib's manifest across multiple candidate libs_dirs. Returns
local function lib_manifest_path(install_root, lib_id) -- (full_path, owning_libs_dir) for the first candidate that exists, or
return install_root .. "/libs/" .. id_to_path(lib_id) .. "/manifest.lib" -- (nil, nil) if absent everywhere.
local function find_lib_manifest(libs_dirs, lib_id)
return first_existing(libs_dirs,
id_to_path(lib_id) .. "/manifest.lib")
end end
-- Gitea-slug derivation. Default: "sporel-" + lib-id verbatim. -- Gitea-slug derivation. Default: "sporel-" + lib-id verbatim.
@@ -339,7 +423,7 @@ M._gitea_slug_for = gitea_slug_for
-- Each closure entry records the (source, lib_id, version) pin that the -- Each closure entry records the (source, lib_id, version) pin that the
-- closure walk observed; multi-pin entries for the same lib_id are kept -- closure walk observed; multi-pin entries for the same lib_id are kept
-- intact so detect_conflicts() can group them. -- intact so detect_conflicts() can group them.
local function walk_closure(module_manifest_path, install_root) local function walk_closure(module_manifest_path, libs_dirs)
local closure = {} local closure = {}
local visited = {} local visited = {}
@@ -361,7 +445,8 @@ local function walk_closure(module_manifest_path, install_root)
}) })
if not visited[d.id] then if not visited[d.id] then
visited[d.id] = true visited[d.id] = true
local m = read_manifest(lib_manifest_path(install_root, d.id)) local mpath = find_lib_manifest(libs_dirs, d.id)
local m = mpath and read_manifest(mpath) or nil
if m and m.deps then if m and m.deps then
for _, sub in ipairs(m.deps) do for _, sub in ipairs(m.deps) do
table.insert(queue, { source = m.id, dep = sub }) table.insert(queue, { source = m.id, dep = sub })
@@ -431,12 +516,24 @@ local function manifest_version_matches(lib_dir, expected_version)
return m ~= nil and m.version == expected_version return m ~= nil and m.version == expected_version
end end
local function run_state_check(unique_closure, install_root, local function run_state_check(unique_closure, libs_dirs,
gitea_base, warnings, errors) gitea_base, warnings, errors)
local git = require("lib-core.git") local git = require("lib-core.git")
for _, e in ipairs(unique_closure) do for _, e in ipairs(unique_closure) do
local lib_dir = install_root .. "/libs/" .. id_to_path(e.lib_id) -- Resolve the lib's home: first libs_dir that contains its
-- manifest. Falls back to the first candidate so clone targets
-- a deterministic location when the lib is missing entirely.
local _, owning_dir = find_lib_manifest(libs_dirs, e.lib_id)
local libs_dir = owning_dir or libs_dirs[1]
if not libs_dir then
table.insert(errors, {
lib_id = e.lib_id, kind = "config",
message = "no libs_dir candidate for state-check",
})
goto continue
end
local lib_dir = libs_dir .. "/" .. id_to_path(e.lib_id)
local url = gitea_base .. "/" .. gitea_slug_for(e.lib_id) .. ".git" local url = gitea_base .. "/" .. gitea_slug_for(e.lib_id) .. ".git"
local pinned = "v" .. e.version local pinned = "v" .. e.version
@@ -460,12 +557,22 @@ local function run_state_check(unique_closure, install_root,
else else
local desc, derr = git.describe(lib_dir) local desc, derr = git.describe(lib_dir)
if not desc then if not desc then
-- Case E: broken. -- describe failed. Two sub-cases:
table.insert(errors, { -- G — untagged dev-state: repo has no tags at all (common
lib_id = e.lib_id, -- when working on master between tagged releases).
kind = "describe-fail", -- If manifest.version matches pin, the install IS at
message = tostring(derr), -- the right code regardless of git refs → accept.
}) -- E — genuinely broken: manifest mismatch + no tags →
-- caller has no way to recover, surface error.
if manifest_version_matches(lib_dir, e.version) then
-- Case G: untagged but manifest authoritative — accept.
else
table.insert(errors, {
lib_id = e.lib_id,
kind = "describe-fail",
message = tostring(derr),
})
end
elseif desc == pinned then elseif desc == pinned then
-- Case B: clean exact match. -- Case B: clean exact match.
else else
@@ -497,6 +604,7 @@ local function run_state_check(unique_closure, install_root,
end end
end end
end end
::continue::
end end
end end
@@ -520,21 +628,23 @@ end
function M.ensure_for_module_at(module_manifest_path, opts) function M.ensure_for_module_at(module_manifest_path, opts)
opts = opts or {} opts = opts or {}
local install_root = resolve_install_root(opts) local install_root = resolve_install_root(opts)
if not install_root then local libs_dirs = resolve_libs_dirs(opts, install_root)
if #libs_dirs == 0 then
return { return {
ok = false, conflicts = {}, warnings = {}, ok = false, conflicts = {}, warnings = {},
errors = {{ errors = {{
lib_id = "<module>", lib_id = "<module>",
kind = "config", kind = "config",
message = "no install_root: pass opts.install_root, " .. message = "no libs_dir candidates: pass opts.libs_dir, set " ..
"set SPOREL_INSTALL_ROOT, or run under an engine " .. "SPOREL_LIBS_DIR / SPOREL_INSTALL_ROOT, or run " ..
"that provides engine.install_root()", "under an engine that provides engine.libs_dirs() / " ..
"engine.install_root()",
}}, }},
closure = {}, closure = {},
} }
end end
local closure, werr = walk_closure(module_manifest_path, install_root) local closure, werr = walk_closure(module_manifest_path, libs_dirs)
if not closure then if not closure then
return { return {
ok = false, conflicts = {}, warnings = {}, ok = false, conflicts = {}, warnings = {},
@@ -575,7 +685,7 @@ function M.ensure_for_module_at(module_manifest_path, opts)
local gitea_base = resolve_gitea_base(opts) local gitea_base = resolve_gitea_base(opts)
local warnings, errors = {}, {} local warnings, errors = {}, {}
run_state_check(unique, install_root, gitea_base, warnings, errors) run_state_check(unique, libs_dirs, gitea_base, warnings, errors)
return { return {
ok = (#errors == 0), ok = (#errors == 0),
@@ -592,22 +702,36 @@ end
function M.ensure_for_module(module_id, opts) function M.ensure_for_module(module_id, opts)
opts = opts or {} opts = opts or {}
local install_root = resolve_install_root(opts) local install_root = resolve_install_root(opts)
if not install_root then local modules_dirs = resolve_modules_dirs(opts, install_root)
if #modules_dirs == 0 then
return { return {
ok = false, conflicts = {}, warnings = {}, ok = false, conflicts = {}, warnings = {},
errors = {{ errors = {{
lib_id = "<module>", kind = "config", lib_id = "<module>", kind = "config",
message = "no install_root for ensure_for_module: " .. message = "no modules_dir candidates for ensure_for_module: " ..
"pass opts.install_root, set SPOREL_INSTALL_ROOT, " .. "pass opts.modules_dir, set SPOREL_MODULES_DIR / " ..
"or run under an engine providing " .. "SPOREL_INSTALL_ROOT, or run under an engine " ..
"engine.install_root()", "providing engine.modules_dirs() / engine.install_root()",
}}, }},
closure = {}, closure = {},
} }
end end
local path = install_root .. "/modules/" .. module_id -- Find the first candidate that actually has this module.
.. "/manifest.module" local path = first_existing(modules_dirs,
opts.install_root = install_root module_id .. "/manifest.module")
if not path then
return {
ok = false, conflicts = {}, warnings = {},
errors = {{
lib_id = "<module>", kind = "manifest-read",
message = "module '" .. module_id ..
"' manifest not found in any of " ..
tostring(#modules_dirs) .. " candidate dirs",
}},
closure = {},
}
end
if install_root then opts.install_root = install_root end
return M.ensure_for_module_at(path, opts) return M.ensure_for_module_at(path, opts)
end end

View File

@@ -1,8 +1,8 @@
{ {
"id": "lib-management.dep-fetcher", "id": "lib-management.dep-fetcher",
"version": "0.1.1", "version": "0.1.2",
"api_min": "0.1", "api_min": "0.1",
"deps": [ "deps": [
{"id": "lib-core.git", "version": "0.1.0"} {"id": "lib-core.git", "version": "0.2.0"}
] ]
} }