Ports the community s6/s6-rc service catalog (early-boot mounts, udev,
dbus, session/power, networking, display managers, peripheral daemons)
to Lua-based service definitions.
- src/lua/s6lua.lua: shared helper library (exec, logging, fstab
parsing, oneshot shell runner, readiness notification)
- src/native/s6exec.c: minimal execvp() binding so longrun `run`
scripts can replace their own process image with the real daemon,
since stock Lua has no exec() and os.execute("exec ...") does not
solve it (the interpreter stays alive as an unsupervised parent)
- tools/catalog.lua + gen-services.lua: table-driven generator for
the structurally identical -srv/-log daemon pairs and agetty ttys
- tools/lint.lua, check-syntax.ps1, set-exec-bits.ps1: local
verification (no live s6/Linux target available this session)
- services/: 50 service directories (14 early-boot oneshots, 12
daemon srv/log pairs, 6 agetty ttys, boot/mount/setup/ttys/
graphical/default bundles)
Authored and syntax/structurally validated only. Native module is
unbuilt and daemon flags are unverified against Artix's real catalog
(gitea was unreachable during research) -- see docs/CONVENTIONS.md.
132 lines
3.5 KiB
Lua
132 lines
3.5 KiB
Lua
-- Pure-Lua structural linter over services/. Validates that every service
|
|
-- directory has the files its `type` requires, that every
|
|
-- dependencies.d/contents.d entry names a real sibling service, and that
|
|
-- producer-for/consumer-for pairs are mutually consistent.
|
|
--
|
|
-- No lfs/posix available, so directory listing shells out to `dir /b` on
|
|
-- Windows or `ls -1a` on Unix.
|
|
--
|
|
-- Usage: lua55 tools/lint.lua
|
|
|
|
local function is_windows()
|
|
return package.config:sub(1, 1) == "\\"
|
|
end
|
|
|
|
local function list_dir(path)
|
|
local cmd
|
|
if is_windows() then
|
|
cmd = 'dir /b "' .. path:gsub("/", "\\") .. '" 2>nul'
|
|
else
|
|
cmd = 'ls -1a "' .. path .. '" 2>/dev/null'
|
|
end
|
|
local entries = {}
|
|
local p = io.popen(cmd)
|
|
if not p then
|
|
return entries
|
|
end
|
|
for line in p:lines() do
|
|
if line ~= "." and line ~= ".." and line ~= "" then
|
|
table.insert(entries, line)
|
|
end
|
|
end
|
|
p:close()
|
|
return entries
|
|
end
|
|
|
|
local function read_file(path)
|
|
local f = io.open(path, "r")
|
|
if not f then
|
|
return nil
|
|
end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
return content
|
|
end
|
|
|
|
local function trim(s)
|
|
return s:match("^%s*(.-)%s*$")
|
|
end
|
|
|
|
local script_dir = arg[0]:match("^(.*)[/\\][^/\\]*$") or "."
|
|
local repo_root = script_dir .. "/.."
|
|
local services_dir = repo_root .. "/services"
|
|
|
|
local errors = {}
|
|
local function fail(fmt, ...)
|
|
table.insert(errors, string.format(fmt, ...))
|
|
end
|
|
|
|
local names = list_dir(services_dir)
|
|
local svc_type = {}
|
|
|
|
for _, name in ipairs(names) do
|
|
local dir = services_dir .. "/" .. name
|
|
local t = read_file(dir .. "/type")
|
|
if not t then
|
|
fail("%s: missing 'type' file", name)
|
|
else
|
|
t = trim(t)
|
|
svc_type[name] = t
|
|
if t ~= "oneshot" and t ~= "longrun" and t ~= "bundle" then
|
|
fail("%s: type must be oneshot|longrun|bundle, got %q", name, t)
|
|
elseif t == "oneshot" and not read_file(dir .. "/up") then
|
|
fail("%s: oneshot missing 'up'", name)
|
|
elseif t == "longrun" and not read_file(dir .. "/run") then
|
|
fail("%s: longrun missing 'run'", name)
|
|
elseif t == "bundle" and #list_dir(dir .. "/contents.d") == 0 then
|
|
fail("%s: bundle has empty/missing contents.d", name)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function exists(name)
|
|
return svc_type[name] ~= nil
|
|
end
|
|
|
|
for _, name in ipairs(names) do
|
|
local dir = services_dir .. "/" .. name
|
|
|
|
for _, dep in ipairs(list_dir(dir .. "/dependencies.d")) do
|
|
if not exists(dep) then
|
|
fail("%s: dependencies.d references unknown service %q", name, dep)
|
|
end
|
|
end
|
|
|
|
for _, member in ipairs(list_dir(dir .. "/contents.d")) do
|
|
if not exists(member) then
|
|
fail("%s: contents.d references unknown service %q", name, member)
|
|
end
|
|
end
|
|
|
|
local producer = read_file(dir .. "/producer-for")
|
|
if producer then
|
|
producer = trim(producer)
|
|
if not exists(producer) then
|
|
fail("%s: producer-for references unknown service %q", name, producer)
|
|
else
|
|
local consumer = read_file(services_dir .. "/" .. producer .. "/consumer-for")
|
|
if not consumer or trim(consumer) ~= name then
|
|
fail("%s: producer-for %s but %s's consumer-for does not point back", name, producer, producer)
|
|
end
|
|
if not read_file(services_dir .. "/" .. producer .. "/pipeline-name") then
|
|
fail("%s: logger %s missing pipeline-name", name, producer)
|
|
end
|
|
end
|
|
end
|
|
|
|
local nfd = read_file(dir .. "/notification-fd")
|
|
if nfd and not trim(nfd):match("^%d+$") then
|
|
fail("%s: notification-fd must be a bare integer, got %q", name, trim(nfd))
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
for _, e in ipairs(errors) do
|
|
io.stderr:write("LINT: " .. e .. "\n")
|
|
end
|
|
io.stderr:write(string.format("%d lint error(s)\n", #errors))
|
|
os.exit(1)
|
|
end
|
|
|
|
print(string.format("lint OK: %d service directories checked", #names))
|