diff options
Diffstat (limited to 'bootstrap.tl')
-rwxr-xr-x | bootstrap.tl | 518 |
1 files changed, 518 insertions, 0 deletions
diff --git a/bootstrap.tl b/bootstrap.tl new file mode 100755 index 00000000..5017f117 --- /dev/null +++ b/bootstrap.tl | |||
@@ -0,0 +1,518 @@ | |||
1 | #!/usr/bin/env -S -- tl run | ||
2 | |||
3 | local PLATFORM = arg[1] or "unix" | ||
4 | |||
5 | local lfs = require("lfs") | ||
6 | |||
7 | local dependencies = { | ||
8 | "md5", | ||
9 | "lua-zlib", | ||
10 | "lua-bz2", | ||
11 | "luafilesystem", | ||
12 | "luasocket", | ||
13 | "luasec", | ||
14 | "miniposix", | ||
15 | } | ||
16 | |||
17 | local c_module_exceptions: {string:{string}} = { | ||
18 | ["ssl"] = { "ssl.core", "ssl.context", "ssl.x509", "ssl.config" }, | ||
19 | } | ||
20 | |||
21 | local rockspec_locations: {string:string} = { | ||
22 | ["md5"] = "vendor/md5/rockspec/md5-1.2-1.rockspec", | ||
23 | ["lua-zlib"] = "vendor/lua-zlib/lua-zlib-1.1-0.rockspec", | ||
24 | ["lua-bz2"] = "vendor/lua-bz2/lua-bz2-0.2.1-1.rockspec", | ||
25 | ["luafilesystem"] = "vendor/luafilesystem/luafilesystem-scm-1.rockspec", | ||
26 | ["luasocket"] = "vendor/luasocket/luasocket-scm-3.rockspec", | ||
27 | ["luasec"] = "vendor/luasec/luasec-1.3.2-1.rockspec", | ||
28 | ["miniposix"] = "vendor/miniposix/miniposix-dev-1.rockspec", | ||
29 | } | ||
30 | |||
31 | -------------------------------------------------------------------------------- | ||
32 | -- Utilities | ||
33 | -------------------------------------------------------------------------------- | ||
34 | |||
35 | local hexdump: function(string): string | ||
36 | do | ||
37 | local numtab = {} | ||
38 | for i = 0, 255 do | ||
39 | numtab[string.char(i)] = ("%-3d,"):format(i) | ||
40 | end | ||
41 | hexdump = function(str: string): string | ||
42 | return (str:gsub(".", numtab):gsub(("."):rep(80), "%0\n")) | ||
43 | end | ||
44 | end | ||
45 | |||
46 | local function apply_template(template: string, variables: {string:string}): string | ||
47 | return (template:gsub("$%(([^)]*)%)", variables)) | ||
48 | end | ||
49 | |||
50 | local function reindent_c(input: string): string | ||
51 | local out = {} | ||
52 | local indent = 0 | ||
53 | local previous_is_blank = true | ||
54 | for line in input:gmatch("([^\n]*)") do | ||
55 | line = line:match("^[ \t]*(.-)[ \t]*$") | ||
56 | |||
57 | local is_blank = (#line == 0) | ||
58 | local do_print = | ||
59 | (not is_blank) or | ||
60 | (not previous_is_blank and indent == 0) | ||
61 | |||
62 | if line:match("^[})]") then | ||
63 | indent = indent - 1 | ||
64 | if indent < 0 then indent = 0 end | ||
65 | end | ||
66 | if do_print then | ||
67 | table.insert(out, string.rep(" ", indent)) | ||
68 | table.insert(out, line) | ||
69 | table.insert(out, "\n") | ||
70 | end | ||
71 | if line:match("[{(]$") then | ||
72 | indent = indent + 1 | ||
73 | end | ||
74 | |||
75 | previous_is_blank = is_blank | ||
76 | end | ||
77 | return table.concat(out) | ||
78 | end | ||
79 | |||
80 | local function sortedpairs<K, V>(tbl: {K: V}): function(): (K, V) | ||
81 | local keys = {} | ||
82 | for k, _ in pairs(tbl) do | ||
83 | table.insert(keys, k) | ||
84 | end | ||
85 | table.sort(keys, function(a: K, b: K): boolean | ||
86 | if a is integer and not b is integer then | ||
87 | return true | ||
88 | elseif not a is integer and b is integer then | ||
89 | return false | ||
90 | end | ||
91 | return (a as integer) < (b as integer) | ||
92 | end) | ||
93 | local i = 1 | ||
94 | return function(): (K, V) | ||
95 | local key = keys[i] | ||
96 | i = i + 1 | ||
97 | return key, tbl[key] | ||
98 | end | ||
99 | end | ||
100 | |||
101 | local function mkdir_p(dirname: string) | ||
102 | local a, b = dirname:match("(.*)/(.*)") | ||
103 | if a and b then | ||
104 | mkdir_p(a) | ||
105 | end | ||
106 | lfs.mkdir(dirname) | ||
107 | end | ||
108 | |||
109 | local function dirname(filename: string): string | ||
110 | return (filename:gsub("[^/]*$", "")) | ||
111 | end | ||
112 | |||
113 | local function write_template(filename: string, template: string, variables: {string:string}) | ||
114 | local fd = assert(io.open(filename, "wb")) | ||
115 | local text = apply_template(template, variables) | ||
116 | if filename:match("%.[ch]$") then | ||
117 | text = reindent_c(text) | ||
118 | end | ||
119 | fd:write(text) | ||
120 | fd:close() | ||
121 | end | ||
122 | |||
123 | local function find_files(dir_name: string): (function(): string) | ||
124 | local iter_stack: {lfs.DirObj} = {} | ||
125 | local dir_stack: {string} = { dir_name } | ||
126 | local iter, dd = lfs.dir(dir_name) | ||
127 | return function(): string | ||
128 | while true do | ||
129 | local s = iter(dd) | ||
130 | if s ~= "." and s ~= ".." then | ||
131 | if not s then | ||
132 | if #iter_stack == 0 then | ||
133 | return nil | ||
134 | end | ||
135 | table.remove(dir_stack) | ||
136 | dd = table.remove(iter_stack) | ||
137 | else | ||
138 | local d = dir_stack[#dir_stack] | ||
139 | local pathname = d .. "/" .. s, "mode" | ||
140 | if lfs.attributes(pathname, "mode") ~= "directory" then | ||
141 | return pathname | ||
142 | else | ||
143 | table.insert(iter_stack, dd) | ||
144 | iter, dd = lfs.dir(pathname) | ||
145 | table.insert(dir_stack, pathname) | ||
146 | end | ||
147 | end | ||
148 | end | ||
149 | end | ||
150 | end | ||
151 | end | ||
152 | |||
153 | -------------------------------------------------------------------------------- | ||
154 | -- Collect data based on rockspecs in vendor/ | ||
155 | -------------------------------------------------------------------------------- | ||
156 | |||
157 | local record BuildEntry | ||
158 | sources: {string} | ||
159 | incdirs: {string} | ||
160 | defines: {string} | ||
161 | end | ||
162 | |||
163 | local record Data | ||
164 | record LuaEntry | ||
165 | dep: string | ||
166 | mod: string | ||
167 | source: string | ||
168 | |||
169 | h_file: string | ||
170 | length: integer | ||
171 | end | ||
172 | record CEntry | ||
173 | dep: string | ||
174 | mod: string | ||
175 | build: BuildEntry | ||
176 | |||
177 | a_file: string | ||
178 | end | ||
179 | |||
180 | luas: {LuaEntry} | ||
181 | cs: {CEntry} | ||
182 | end | ||
183 | |||
184 | local function add_lua(data: Data, dep: string, mod: string, source: string) | ||
185 | table.insert(data.luas, { dep = dep, mod = mod, source = source }) | ||
186 | end | ||
187 | |||
188 | local function add_c(data: Data, dep: string, mod: string, build: BuildEntry) | ||
189 | table.insert(data.cs, { dep = dep, mod = mod, build = build }) | ||
190 | end | ||
191 | |||
192 | local record Rockspec | ||
193 | record Build | ||
194 | record Install | ||
195 | lua: {(string | integer): string} | ||
196 | end | ||
197 | |||
198 | type: string | ||
199 | modules: {string: (string | BuildEntry)} | ||
200 | install: Install | ||
201 | platforms: {string: Build} | ||
202 | end | ||
203 | |||
204 | build: Build | ||
205 | end | ||
206 | |||
207 | local function load_rockspec(dep: string, rockspec_filename: string): Rockspec | ||
208 | local rockspec: Rockspec = {} | ||
209 | local fd = io.open(rockspec_filename) | ||
210 | local source = fd:read("*a") | ||
211 | fd:close() | ||
212 | local chunk = assert(load(source, dep, "t", rockspec as {any:any})) | ||
213 | chunk() | ||
214 | return rockspec | ||
215 | end | ||
216 | |||
217 | local function process_build_entry(data: Data, dep: string, mod: string, modt: string|BuildEntry) | ||
218 | if modt is string then | ||
219 | if modt:match("lua$") then | ||
220 | add_lua(data, dep, mod, "vendor/" .. dep .. "/" .. modt) | ||
221 | else | ||
222 | add_c(data, dep, mod, { sources = { modt } }) | ||
223 | end | ||
224 | elseif modt is BuildEntry then | ||
225 | add_c(data, dep, mod, modt) | ||
226 | end | ||
227 | end | ||
228 | |||
229 | local function process_build_modules(data: Data, dep: string, entries: {string: (string|BuildEntry)}) | ||
230 | for mod, modt in sortedpairs(entries) do | ||
231 | process_build_entry(data, dep, mod, modt) | ||
232 | end | ||
233 | end | ||
234 | |||
235 | local function process_build_install(data: Data, dep: string, entries: {(string|integer): string}) | ||
236 | for mod, modt in sortedpairs(entries) do | ||
237 | if mod is integer then | ||
238 | assert(modt is string) | ||
239 | mod = modt:gsub("^src/", ""):gsub("^lua/", ""):gsub("%.[^.]*$", ""):gsub("/", ".") | ||
240 | end | ||
241 | assert(mod is string) | ||
242 | |||
243 | process_build_entry(data, dep, mod, modt) | ||
244 | end | ||
245 | end | ||
246 | |||
247 | local function process_build(data: Data, dep: string, build: Rockspec.Build) | ||
248 | if build.modules then | ||
249 | process_build_modules(data, dep, build.modules) | ||
250 | end | ||
251 | if build.install and build.install.lua then | ||
252 | process_build_install(data, dep, build.install.lua) | ||
253 | end | ||
254 | end | ||
255 | |||
256 | local function process(data: Data, dep: string, rockspec_filename: string) | ||
257 | local rockspec = load_rockspec(dep, rockspec_filename) | ||
258 | |||
259 | assert(rockspec.build.type == "builtin") | ||
260 | |||
261 | if rockspec.build.modules or rockspec.build.install then | ||
262 | process_build(data, dep, rockspec.build) | ||
263 | end | ||
264 | |||
265 | if rockspec.build.platforms then | ||
266 | if rockspec.build.platforms[PLATFORM] then | ||
267 | process_build(data, dep, rockspec.build.platforms[PLATFORM]) | ||
268 | end | ||
269 | end | ||
270 | end | ||
271 | |||
272 | -------------------------------------------------------------------------------- | ||
273 | -- Generate entries for Makefile-based build | ||
274 | -------------------------------------------------------------------------------- | ||
275 | |||
276 | local function global_name(mod: string): string | ||
277 | return "luarocks_gen_" .. mod:gsub("%.", "_") | ||
278 | end | ||
279 | |||
280 | local function generate(input_filename: string, entry: Data.LuaEntry): string | ||
281 | local fd = assert(io.open(input_filename, "rb")) | ||
282 | local content = fd:read("*a"):gsub("^#![^\n]+\n", "") | ||
283 | fd:close() | ||
284 | |||
285 | entry.length = #content | ||
286 | |||
287 | return apply_template([[ | ||
288 | /* automatically generated by bootstrap.tl */ | ||
289 | |||
290 | static const unsigned char $(global)[] = { | ||
291 | $(code) | ||
292 | }; | ||
293 | ]], { | ||
294 | global = global_name(entry.mod), | ||
295 | code = hexdump(content), | ||
296 | }) | ||
297 | end | ||
298 | |||
299 | local function generate_all_luas(luas: {Data.LuaEntry}) | ||
300 | for _, entry in ipairs(luas) do | ||
301 | local filename = "gen/lua/" .. entry.mod:gsub("%.", "/") .. ".h" | ||
302 | mkdir_p(dirname(filename)) | ||
303 | |||
304 | local fd = assert(io.open(filename, "wb")) | ||
305 | fd:write(reindent_c(generate(entry.source, entry))) | ||
306 | entry.h_file = filename | ||
307 | fd:close() | ||
308 | end | ||
309 | |||
310 | local includes = {} | ||
311 | local array = {} | ||
312 | for _, entry in ipairs(luas) do | ||
313 | table.insert(includes, ("#include \"%s\""):format(entry.h_file)) | ||
314 | table.insert(array, apply_template([[ | ||
315 | { | ||
316 | .module_name = "$(mod)", | ||
317 | .source_name = "$(source)", | ||
318 | .length = $(length), | ||
319 | .code = $(global), | ||
320 | }, | ||
321 | ]], { | ||
322 | mod = entry.mod, | ||
323 | dep = entry.dep, | ||
324 | source = entry.source, | ||
325 | length = tostring(entry.length), | ||
326 | global = global_name(entry.mod), | ||
327 | })) | ||
328 | end | ||
329 | |||
330 | table.insert(array, apply_template([[ | ||
331 | { | ||
332 | .module_name = NULL, | ||
333 | .source_name = NULL, | ||
334 | .length = 0, | ||
335 | .code = NULL, | ||
336 | }, | ||
337 | ]], {})) | ||
338 | |||
339 | write_template("gen/gen.h", [[ | ||
340 | /* automatically generated by bootstrap.tl */ | ||
341 | |||
342 | $(includes) | ||
343 | |||
344 | static const Gen GEN[] = { | ||
345 | $(array) | ||
346 | }; | ||
347 | ]], { | ||
348 | array = table.concat(array, "\n"), | ||
349 | includes = table.concat(includes, "\n"), | ||
350 | }) | ||
351 | end | ||
352 | |||
353 | local function get_flag_list(flag: string, entries: {string}, parent: string): string | ||
354 | local out = {} | ||
355 | for _, entry in ipairs(entries) do | ||
356 | table.insert(out, flag .. " " .. parent .. entry) | ||
357 | end | ||
358 | return table.concat(out, " ") | ||
359 | end | ||
360 | |||
361 | local function generate_makefile_entry(entry: Data.CEntry, seen: {string:boolean}, dirs: {string:boolean}): string | ||
362 | local out = {} | ||
363 | assert(entry.build.sources) | ||
364 | |||
365 | local incdirs = "" | ||
366 | if entry.build.incdirs then | ||
367 | incdirs = get_flag_list("-I", entry.build.incdirs, "vendor/" .. entry.dep .. "/") | ||
368 | end | ||
369 | local defines = "" | ||
370 | if entry.build.defines then | ||
371 | defines = get_flag_list("-D", entry.build.defines, "") | ||
372 | end | ||
373 | |||
374 | local objects = {} | ||
375 | for _, f in ipairs(entry.build.sources) do | ||
376 | local file = "vendor/" .. entry.dep .. "/" .. f | ||
377 | local obj_file = "target/objects/" .. entry.dep .. "/" .. file:gsub("%.c$", ".o") | ||
378 | table.insert(objects, obj_file) | ||
379 | |||
380 | if not seen[file] then | ||
381 | local d = dirname(obj_file) | ||
382 | if not dirs[d] then | ||
383 | dirs[d] = true | ||
384 | end | ||
385 | |||
386 | -- the pipe indicates an order-only prerequisite | ||
387 | -- (https://www.gnu.org/software/make/manual/make.html#Prerequisite-Types) | ||
388 | table.insert(out, ("%s: %s | %s"):format(obj_file, file, d)) | ||
389 | table.insert(out, ("\t$(CC) -c -o %s %s %s %s"):format(obj_file, file, incdirs, defines)) | ||
390 | seen[file] = true | ||
391 | end | ||
392 | end | ||
393 | |||
394 | local a_file = "target/libraries/" .. entry.mod:gsub("%.", "/") .. ".a" | ||
395 | entry.a_file = a_file | ||
396 | |||
397 | local d = dirname(a_file) | ||
398 | if not dirs[d] then | ||
399 | dirs[d] = true | ||
400 | end | ||
401 | |||
402 | local object_list = table.concat(objects, " ") | ||
403 | table.insert(out, ("%s: %s | %s"):format(a_file, object_list, d)) | ||
404 | --table.insert(out, ("\t$(MKDIR) -p %s"):format(dirname(a_file))) | ||
405 | table.insert(out, ("\t$(AR) rcu %s %s"):format(a_file, object_list)) | ||
406 | table.insert(out, "") | ||
407 | |||
408 | return table.concat(out, "\n") | ||
409 | end | ||
410 | |||
411 | local function process_c_entry(mod: string, externs: {string}, declares: {string}) | ||
412 | if c_module_exceptions[mod] then | ||
413 | for _, m in ipairs(c_module_exceptions[mod]) do | ||
414 | process_c_entry(m, externs, declares) | ||
415 | end | ||
416 | return | ||
417 | end | ||
418 | |||
419 | local cfunc = "luaopen_" .. mod:gsub("%.", "_") | ||
420 | |||
421 | table.insert(externs, ("extern int %s(lua_State* L);"):format(cfunc)) | ||
422 | |||
423 | table.insert(declares, ("lua_pushcfunction(L, %s);"):format(cfunc)) | ||
424 | table.insert(declares, ("lua_setfield(L, -2, \"%s\");"):format(mod)) | ||
425 | end | ||
426 | |||
427 | local makefile_vendor_template = [[ | ||
428 | # automatically generated by bootstrap.tl | ||
429 | |||
430 | VENDOR_LIBS = $(a_files) | ||
431 | |||
432 | $(entries) | ||
433 | |||
434 | ]] | ||
435 | |||
436 | local function generate_all_cs(cs: {Data.CEntry}) | ||
437 | local seen = {} | ||
438 | local dirs = {} | ||
439 | |||
440 | local entries = {} | ||
441 | for _, entry in ipairs(cs) do | ||
442 | table.insert(entries, generate_makefile_entry(entry, seen, dirs)) | ||
443 | end | ||
444 | |||
445 | table.insert(entries, "") | ||
446 | for d, _ in sortedpairs(dirs) do | ||
447 | table.insert(entries, ("%s:"):format(d)) | ||
448 | table.insert(entries, ("\t$(MKDIR) -p %s"):format(d)) | ||
449 | end | ||
450 | |||
451 | local a_files = {} | ||
452 | for _, entry in ipairs(cs) do | ||
453 | table.insert(a_files, entry.a_file) | ||
454 | end | ||
455 | |||
456 | write_template("Makefile.vendor", makefile_vendor_template, { | ||
457 | entries = table.concat(entries, "\n"), | ||
458 | a_files = table.concat(a_files, " "), | ||
459 | }) | ||
460 | |||
461 | local externs = {} | ||
462 | local declares = {} | ||
463 | for _, entry in ipairs(cs) do | ||
464 | process_c_entry(entry.mod, externs, declares) | ||
465 | end | ||
466 | |||
467 | write_template("gen/libraries.h", [[ | ||
468 | /* automatically generated by bootstrap.tl */ | ||
469 | |||
470 | $(externs) | ||
471 | |||
472 | static void declare_libraries(lua_State* L) { | ||
473 | lua_getglobal(L, "package"); /* package */ | ||
474 | lua_getfield(L, -1, "preload"); /* package package.preload */ | ||
475 | $(declares) | ||
476 | lua_settop(L, 0); /* */ | ||
477 | } | ||
478 | ]], { | ||
479 | externs = table.concat(externs, "\n"), | ||
480 | declares = table.concat(declares, "\n"), | ||
481 | }) | ||
482 | end | ||
483 | |||
484 | local function generate_main_h() | ||
485 | local fd = assert(io.open("gen/main.h", "wb")) | ||
486 | fd:write(reindent_c(generate("src/bin/luarocks", { | ||
487 | mod = "main" | ||
488 | }))) | ||
489 | fd:close() | ||
490 | end | ||
491 | |||
492 | -------------------------------------------------------------------------------- | ||
493 | -- Main operation | ||
494 | -------------------------------------------------------------------------------- | ||
495 | |||
496 | local data: Data = { | ||
497 | luas = {}, | ||
498 | cs = {}, | ||
499 | } | ||
500 | |||
501 | for file in find_files("src") do | ||
502 | if file:match("%.lua$") then | ||
503 | table.insert(data.luas, { | ||
504 | dep = "luarocks", | ||
505 | mod = file:gsub("^src/(.*).lua", "%1"):gsub("/", "."), | ||
506 | source = file, | ||
507 | }) | ||
508 | end | ||
509 | end | ||
510 | |||
511 | for _, dep in ipairs(dependencies) do | ||
512 | print(dep) | ||
513 | process(data, dep, rockspec_locations[dep]) | ||
514 | end | ||
515 | |||
516 | generate_all_luas(data.luas) | ||
517 | generate_all_cs(data.cs) | ||
518 | generate_main_h() | ||