121 lines
3.5 KiB
Lua
121 lines
3.5 KiB
Lua
-- indexer.lua -- generates packages/index.lua from all package.lua manifests.
|
|
--
|
|
-- Scans <output>/packages/ for every subdirectory containing a package.lua.
|
|
-- Each manifest is loaded in a sandboxed environment (no io, os, etc.) and
|
|
-- its name, version, and summary are extracted. The resulting index is
|
|
-- written as a Lua list, sorted by package name.
|
|
|
|
local indexer = {}
|
|
|
|
local path = require("path")
|
|
local log = require("log")
|
|
|
|
---------------------------------------------------------------------------
|
|
-- Sandboxed manifest loading
|
|
---------------------------------------------------------------------------
|
|
|
|
-- Load a package.lua file and extract { name, version, summary }.
|
|
-- Runs in a locked-down environment — the manifest cannot access io, os,
|
|
-- require, or any global state. Only pure table constructors are allowed.
|
|
local function load_manifest_meta(filepath)
|
|
local f, err = io.open(filepath, "rb")
|
|
if not f then return nil, err end
|
|
local src = f:read("*a")
|
|
f:close()
|
|
|
|
local env = {}
|
|
local chunk
|
|
if setfenv then
|
|
chunk, err = loadstring(src, "@" .. filepath)
|
|
if chunk then setfenv(chunk, env) end
|
|
else
|
|
chunk, err = load(src, "@" .. filepath, "t", env)
|
|
end
|
|
if not chunk then
|
|
return nil, ("%s: syntax error: %s"):format(filepath, tostring(err))
|
|
end
|
|
|
|
local ok, raw = pcall(chunk)
|
|
if not ok or type(raw) ~= "table" then
|
|
return nil, ("%s: must return a table"):format(filepath)
|
|
end
|
|
|
|
local meta = {
|
|
name = type(raw.name) == "string" and raw.name or nil,
|
|
version = type(raw.version) == "string" and raw.version or nil,
|
|
summary = type(raw.summary) == "string" and raw.summary or nil,
|
|
}
|
|
|
|
if not meta.name then
|
|
return nil, ("%s: missing or invalid name"):format(filepath)
|
|
end
|
|
|
|
return meta
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- Index generation
|
|
---------------------------------------------------------------------------
|
|
|
|
-- Scan <packages_dir> for all subdirectories containing package.lua and
|
|
-- write a sorted index.lua. Returns the path to the written index.
|
|
function indexer.generate(packages_dir)
|
|
if not path.exists(packages_dir) then
|
|
log.warn("packages directory not found: " .. packages_dir)
|
|
return nil
|
|
end
|
|
|
|
log.step("generating index for " .. packages_dir)
|
|
|
|
-- Collect all package.lua files.
|
|
local f = io.popen("find " .. path.quote(packages_dir)
|
|
.. " -mindepth 2 -maxdepth 2 -name package.lua"
|
|
.. " -type f 2>/dev/null | sort")
|
|
if not f then
|
|
error(("cannot scan %s"):format(packages_dir), 0)
|
|
end
|
|
|
|
local entries = {}
|
|
for filepath in f:lines() do
|
|
local meta, err = load_manifest_meta(filepath)
|
|
if meta then
|
|
entries[#entries + 1] = meta
|
|
else
|
|
log.warn(tostring(err))
|
|
end
|
|
end
|
|
f:close()
|
|
|
|
-- Sort by name.
|
|
table.sort(entries, function(a, b) return a.name < b.name end)
|
|
|
|
-- Write index.lua
|
|
local index_path = path.join(packages_dir, "index.lua")
|
|
local lines = { "return {" }
|
|
|
|
for _, e in ipairs(entries) do
|
|
lines[#lines + 1] = string.format(
|
|
' { name = %q, version = %q, summary = %q },',
|
|
e.name,
|
|
e.version or "",
|
|
e.summary or ""
|
|
)
|
|
end
|
|
|
|
lines[#lines + 1] = "}"
|
|
|
|
local tmp = index_path .. ".tmp"
|
|
local out, err = io.open(tmp, "wb")
|
|
if not out then
|
|
error(("cannot write %s: %s"):format(tmp, tostring(err)), 0)
|
|
end
|
|
out:write(table.concat(lines, "\n") .. "\n")
|
|
out:close()
|
|
|
|
os.rename(tmp, index_path)
|
|
log.ok(("index updated (%d packages)"):format(#entries))
|
|
return index_path
|
|
end
|
|
|
|
return indexer
|