Files
s6-lua/tools/lint.lua
T
JMR-dev cb48a676e9 Add Lua-ported s6/s6-rc service definitions
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.
2026-07-19 19:06:43 -05:00

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))