-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathautogen.lua
More file actions
361 lines (318 loc) · 11.6 KB
/
autogen.lua
File metadata and controls
361 lines (318 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
---
-- autogen.lua — generate Lua bindings for kernel constants.
--
-- Two kbuild passes drive the pipeline; both rely on a single cpp-based
-- enumeration step:
--
-- specs
-- │
-- ▼
-- autogen/dump_N.c (one per unique header)
-- │ kbuild: cpp -E -dD
-- ▼
-- autogen/dump_N.pp (directives preserved, enums expanded)
-- │ scan for prefix in #define lines + enum bodies
-- ▼
-- autogen/extract.c (#include + DEFINE(name, name) per candidate)
-- │ kbuild: cc -S
-- ▼
-- autogen/extract.s (`.ascii "->sym val"` per DEFINE, asm-offsets pattern)
-- │
-- ▼
-- autogen/linux/<module>.lua + autogen/lunatik/config.lua
--
-- Each phase lives in its own table: `enumerate` produces candidate names,
-- `extract` resolves them to integer values, `emit` writes the Lua output.
-- `util` holds file/shell helpers shared across phases.
--
-- @script autogen
-- @usage lua5.4 autogen.lua <KBUILD> <KERNEL_RELEASE> <LUNATIK_MODULES>
local KBUILD, KERNEL, LUNATIK_MODULES = arg[1], arg[2], arg[3]
local ROOT = os.getenv("PWD") or error(arg[0] .. ": PWD not set")
local BASE = ROOT .. "/autogen"
local specs = dofile(BASE .. "/specs.lua")
local BANNER = [[
/*
* AUTO-GENERATED by autogen.lua. DO NOT EDIT.
*/
]]
local util = {}
--- Print a formatted error to stderr and exit with failure.
-- @tparam string fmt format string
-- @param ... format arguments
function util.die(fmt, ...)
io.stderr:write(arg[0], ": ", fmt:format(...), "\n")
os.exit(1)
end
--- Run a shell command; die on non-zero exit.
-- @tparam string fmt format string
-- @param ... format arguments
function util.run(fmt, ...)
local cmd = fmt:format(...)
if os.execute(cmd) ~= true then
util.die("command failed: %s", cmd)
end
end
--- Read an entire file into a string.
-- @tparam string path
-- @treturn string contents
function util.slurp(path)
local f <close>, err = io.open(path, "r")
if not f then util.die("cannot read %s: %s", path, err) end
return f:read("a")
end
local function unchanged(path, text)
local f <close> = io.open(path, "r")
return f and f:read("a") == text
end
--- Write a string to a file. No-op if its current contents match,
-- so downstream tools (notably kbuild) can skip rebuilds.
-- @tparam string path
-- @tparam string text
function util.spit(path, text)
if unchanged(path, text) then return end
local f <close>, err = io.open(path, "w")
if not f then util.die("cannot write %s: %s", path, err) end
f:write(text)
end
--- Return the elements of a set, sorted.
-- @tparam {[any]=true,...} set
-- @treturn {any,...}
function util.sorted(set)
local list = {}
for k in pairs(set) do table.insert(list, k) end
table.sort(list)
return list
end
--- Invoke kbuild in `autogen/` to build the given whitespace-separated
-- target list. Writes `targets.mk` with `always-y := <targets>` first so
-- kbuild's pattern rules resolve them.
-- @tparam string targets
function util.run_kbuild(targets)
util.spit(BASE .. "/targets.mk", "always-y := " .. targets .. "\n")
util.run("make -C %s M=%s >&2", KBUILD, BASE)
end
local enumerate = {}
--- Plan one dump per unique spec header.
-- Each dump has `.header` (the kernel header) and `.name` (the file stem
-- used for `dump_N.c` / `dump_N.pp`). Mutates each spec with `.dump` set
-- to its dump record, for later lookup by `enumerate.candidates`.
-- @treturn {{header=string, name=string},...} dumps, in first-seen order
function enumerate.plan()
local by_header, dumps = {}, {}
for _, spec in ipairs(specs) do
local dump = by_header[spec.header]
if not dump then
dump = { header = spec.header, name = "dump_" .. (#dumps + 1) }
table.insert(dumps, dump)
by_header[spec.header] = dump
end
spec.dump = dump
end
return dumps
end
--- Write one `dump_N.c` stub per dump. Each contains only the `#include`;
-- kbuild's cpp does the rest.
-- @tparam {table,...} dumps
function enumerate.write_stubs(dumps)
for _, dump in ipairs(dumps) do
util.spit(("%s/%s.c"):format(BASE, dump.name),
BANNER .. ("#include <%s>\n"):format(dump.header))
end
end
-- Is the #define value an integer constant expression?
-- Reject if any function-like macro invocation (`IDENT(...)`) appears --
-- those expand to code, not compile-time integers. Then strip hex literals
-- (legitimately carry a lowercase 'x') and reject remaining lowercase,
-- which signals casts, sizeof, function pointers, etc.
local function is_integer_expr(value)
if value:match("[%a_][%w_]*%s*%(") then return false end
return not value:gsub("0[xX][%x]+[uUlL]*", ""):match("[a-z]")
end
--- Extract candidate names from a spec's preprocessed dump file.
-- Two sources: #define lines with integer values, and identifiers appearing
-- inside `enum { ... }` bodies (enum values are always integer constants).
-- `spec.exclude`, if set, drops names starting with the given longer
-- prefix (string) or any of the given prefixes (table). Useful when a
-- shorter prefix shadows a nested spec (e.g. `NF_BR_` also matches
-- `NF_BR_PRI_*`) or to drop non-syscall aliases like `__NR_syscalls`.
-- `spec.include`, if set, keeps only names whose suffix (after the
-- prefix) matches one of the listed items -- useful when a spec should
-- emit only a curated subset of a broad prefix.
-- @tparam table spec
-- @treturn {string,...} names matching `spec.prefix`, sorted
function enumerate.candidates(spec)
local text = util.slurp(("%s/%s.pp"):format(BASE, spec.dump.name))
local seen = {}
local define_pattern = "^#define%s+(" .. spec.prefix .. "[%w_]+)%s+(.+)$"
for line in text:gmatch("[^\n]+") do
local name, value = line:match(define_pattern)
if name and is_integer_expr(value) then
seen[name] = true
end
end
local member_pattern = "%f[%w_](" .. spec.prefix .. "[%w_]+)%f[^%w_]"
for body in text:gmatch("enum[^{]*{(.-)}") do
for name in body:gmatch(member_pattern) do
seen[name] = true
end
end
if spec.exclude then
local prefixes = type(spec.exclude) == "table" and spec.exclude or { spec.exclude }
for name in pairs(seen) do
for _, prefix in ipairs(prefixes) do
if name:sub(1, #prefix) == prefix then
seen[name] = nil
break
end
end
end
end
if spec.include then
local keep = {}
for _, suffix in ipairs(spec.include) do keep[spec.prefix .. suffix] = true end
for name in pairs(seen) do
if not keep[name] then seen[name] = nil end
end
end
return util.sorted(seen)
end
local extract = {}
--- Write `extract.c`: include every unique header and emit a
-- `DEFINE(name, name)` for each candidate. The resulting `.s` file carries
-- resolved integer values as assembly immediates (see linux/kbuild.h).
-- @tparam {table,...} dumps
-- @tparam {[table]={string,...},...} candidates keyed by spec
function extract.write(dumps, candidates)
local parts = { BANNER, "#include <linux/kbuild.h>\n" }
for _, dump in ipairs(dumps) do
table.insert(parts, ("#include <%s>\n"):format(dump.header))
end
table.insert(parts, "\nint main(void)\n{\n")
for _, spec in ipairs(specs) do
table.insert(parts, ('\tCOMMENT("module %s %s");\n'):format(spec.module, spec.prefix))
for _, name in ipairs(candidates[spec]) do
table.insert(parts, ("\tDEFINE(%s, %s);\n"):format(name, name))
end
end
table.insert(parts, "\treturn 0;\n}\n")
util.spit(BASE .. "/extract.c", table.concat(parts))
end
--- Parse `extract.s` into module records grouped by top-level name.
-- The asm-offsets `DEFINE()` macro emits `.ascii "->sym val name"`;
-- `COMMENT()` emits `.ascii "->#..."`. We use the latter to delimit
-- modules, and group records by their first dotted segment so each
-- top maps to the set of records that will share one output file.
-- @treturn {[string]={table,...},...} module records keyed by top-level name
-- @treturn {string,...} top-level names in first-seen order
function extract.parse()
local modules, order = {}, {}
local current
for line in io.lines(BASE .. "/extract.s") do
local ascii = line:match('%.ascii%s*"(.-)"')
if ascii then
local name, prefix = ascii:match('^%-%>#module%s+(%S+)%s+(%S+)$')
if name then
local top = name:match("^[^.]+")
if not modules[top] then
modules[top] = {}
table.insert(order, top)
end
current = { name = name, prefix = prefix, entries = {} }
table.insert(modules[top], current)
else
local sym, val = ascii:match('^%-%>(%S+)%s+%$?(%-?%d+)%s+%S+$')
local prefix_len = current and #current.prefix
if sym and prefix_len and sym:sub(1, prefix_len) == current.prefix then
table.insert(current.entries, {
key = sym:sub(prefix_len + 1),
value = val,
})
end
end
end
end
return modules, order
end
local emit = {}
-- Collect intermediate sub-paths needed to host nested sub-tables
-- (e.g. `nf.br` so `nf.br.pri` can be assigned to). Paths that are
-- themselves specs are skipped -- their own `write_submodule` will
-- init them.
local function intermediate_paths(mods)
local specs, needs = {}, {}
for _, mod in ipairs(mods) do
specs[mod.name] = true
local parts = {}
for p in mod.name:gmatch("[^.]+") do table.insert(parts, p) end
for i = 2, #parts - 1 do
needs[table.concat(parts, ".", 1, i)] = true
end
end
for name in pairs(specs) do needs[name] = nil end
return util.sorted(needs)
end
-- Write one sub-table block: init line (unless this is the top itself)
-- followed by sorted entries.
local function write_submodule(out, mod, top)
out:write("\n")
if mod.name ~= top then out:write(mod.name, " = {}\n") end
table.sort(mod.entries, function(a, b) return a.key < b.key end)
for _, e in ipairs(mod.entries) do
out:write(mod.name, '["', e.key, '"]\t= ', e.value, "\n")
end
end
--- Write one Lua file per top-level group. Specs whose `module` name shares
-- a first segment (e.g. `nf.inet`, `nf.ip.pri`) merge into a single file
-- (`linux/nf.lua`) with nested sub-tables, giving callers a single
-- `require("linux.nf")` handle.
-- @tparam string top top-level group name (e.g. `"eth"`, `"nf"`)
-- @tparam {table,...} mods module records produced by `extract.parse`
function emit.module(top, mods)
local out <close> = assert(io.open(("%s/linux/%s.lua"):format(BASE, top), "w"))
out:write("-- auto-generated, do not edit\n")
out:write("-- kernel: ", KERNEL, "\n\n")
out:write("local ", top, " = {}\n")
for _, path in ipairs(intermediate_paths(mods)) do
out:write(path, " = {}\n")
end
table.sort(mods, function(a, b) return a.name < b.name end)
for _, mod in ipairs(mods) do
write_submodule(out, mod, top)
end
out:write("\nreturn ", top, "\n\n")
end
--- Write autogen/lunatik/config.lua with kernel version and module list.
function emit.config()
local out <close> = assert(io.open(BASE .. "/lunatik/config.lua", "w"))
out:write("-- auto-generated, do not edit\n")
out:write("-- kernel: ", KERNEL, "\n\n")
out:write("local config = {}\n\n")
out:write('config.kernel_version = "', KERNEL, '"\n')
out:write('config.modules = {\n\t"lunatik",\n')
for m in LUNATIK_MODULES:gmatch("%S+") do
out:write('\t"lua', m:lower(), '",\n')
end
out:write('\t"lunatik_run"\n}\n\nreturn config\n\n')
end
if not (KBUILD and KERNEL and LUNATIK_MODULES) then
util.die("usage: lua5.4 %s <KBUILD> <KERNEL_RELEASE> <LUNATIK_MODULES>", arg[0])
end
local dumps = enumerate.plan()
enumerate.write_stubs(dumps)
local dump_targets = {}
for _, dump in ipairs(dumps) do
table.insert(dump_targets, dump.name .. ".pp")
end
util.run_kbuild(table.concat(dump_targets, " "))
local candidates = {}
for _, spec in ipairs(specs) do
candidates[spec] = enumerate.candidates(spec)
end
extract.write(dumps, candidates)
util.run_kbuild("extract.s")
local modules, order = extract.parse()
for _, name in ipairs(order) do
emit.module(name, modules[name])
end
emit.config()