mistress
neutrino.lua
1-- neutrino - a simple and opinionated build system for C
2
3local neu = {}
4
5local function check(s)
6 assert(s:match("^[%u_]+$"), ("name '%s' invalid"):format(s))
7end
8
9local function memq(elt, tbl)
10 for _, x in pairs(tbl) do if elt == x then return true end end
11 return false
12end
13
14local bools = {}
15function neu.bool(name)
16 check(name)
17 assert(not bools[name], ("option '%s' redeclared"):format(name))
18 bools[name] = #(os.getenv(name) or "") > 0
19 return bools[name]
20end
21
22local enums = {}
23function neu.enum(name, set)
24 check(name)
25 assert(not enums[name], ("option '%s' redeclared"):format(name))
26 local val = os.getenv(name)
27 if not val then enums[name] = set[1]
28 else
29 assert(memq(val, set), ("value '%s' not in { '%s' }")
30 :format(val, table.concat(set, " ")))
31 enums[name] = val
32 end
33 return enums[name]
34end
35
36local strs = {}
37function neu.str(name, dfl)
38 check(name)
39 assert(not strs[name], ("option '%s' redeclared"):format(name))
40 strs[name] = os.getenv(name) or dfl
41 return strs[name]
42end
43
44local no_color = neu.bool("NO_COLOR")
45local build_type = neu.enum("BUILD_TYPE", { "debug", "smol", "zomg" })
46
47local bn = assert(io.open("build.ninja", "w"))
48
49local function src2obj(s)
50 return "obj/" .. s:gsub(".c$", ".o"):gsub("/", ".")
51end
52
53local function writevar(name, value)
54 bn:write(name, "=", value, "\n")
55end
56
57local function writerule(name, tbl)
58 bn:write("rule ", name, "\n")
59 for k, v in pairs(tbl) do writevar(" " .. k, v, "\n") end
60end
61
62local function writebuild(tgt, rule, prereq, ideps)
63 if type(prereq) == "table" then
64 prereq = table.concat(prereq, " ")
65 end
66 if type(ideps) == "table" then
67 ideps = table.concat(ideps, " ")
68 end
69 ideps = ideps and "|" .. ideps or ""
70 bn:write("build ", tgt, ":", rule, " ", prereq, ideps, "\n")
71end
72
73local depmeta = {}
74local function blessdep(tbl) return setmetatable(tbl, depmeta) end
75local function isdep(tbl) return getmetatable(tbl) == depmeta end
76
77local function aggr(name, rule, srcs, opts, ideps, vars)
78 opts = opts or {}
79 opts.std = opts.std or "c17"
80 local stds = { c17 = true, c23 = true }
81 local objs = {}
82 local lf = {}
83 for i, x in ipairs(srcs) do objs[i] = src2obj(x) end
84 writebuild("obj/" .. name, rule, objs, ideps)
85 if vars then
86 for k, v in pairs(vars) do writevar(" " .. k, v) end
87 end
88 for i = 1, #srcs do
89 writebuild(objs[i], "cc", srcs[i])
90 local cf = {}
91 if stds[opts.std] then table.insert(cf, "-std=" .. opts.std)
92 else error(("std '%s' invalid"):format(opts.std)) end
93 if opts.includedirs then
94 for _, d in ipairs(opts.includedirs) do
95 table.insert(cf, "-I'" .. d .. "'")
96 end
97 end
98 writevar(" _cflags", table.concat(cf, " "))
99 end
100end
101
102function neu.bin(bin, srcs, opts)
103 local ideps = {}
104 local lf = {}
105 if opts.deps then
106 for _, d in ipairs(opts.deps) do
107 assert(isdep(d), "deps contain invalid object")
108 if d.file then table.insert(ideps, "obj/" .. d.file) end
109 table.insert(lf, d.ldflags)
110 end
111 end
112 aggr(bin, "ld", srcs, opts, ideps, {
113 _ldflags = table.concat(lf, " "),
114 })
115end
116
117function neu.lib(lib, srcs, opts)
118 local fn = table.concat({ "lib", lib, ".a" })
119 aggr(fn, "ar", srcs, opts)
120 return blessdep({ file = fn, ldflags = "-Lobj -l" .. lib })
121end
122
123if not package.searchpath then
124 function package.searchpath(name, path)
125 name = name:gsub("%.", "/"):gsub("%%", "%%%%")
126 for t in path:gmatch("[^;]+") do
127 local fn = t:gsub("%?", name)
128 local fd = io.open(fn, "r")
129 if fd and (fd:close() or true) then return fn end
130 end
131 return nil, "not found"
132 end
133end
134
135return function(argv)
136 writevar("builddir", "obj")
137 writevar("ar", neu.str("AR", "ar"))
138 writevar("cc", neu.str("CC", "cc"))
139 writevar(
140 "cflags",
141 table.concat({
142 no_color and "-fno-diagnostics-color" or "-fdiagnostics-color",
143 "-Wall -Wextra -Wpedantic",
144 ({
145 debug = "-g",
146 smol = "-Os",
147 zomg = "-O3"
148 })[build_type],
149 "-fno-semantic-interposition",
150 neu.str("CFLAGS", ""),
151 }, " ")
152 )
153 writevar(
154 "ldflags",
155 table.concat({
156 "-static",
157 -- XXX: -s or strip when installing? should research more
158 build_type == "debug" and "" or "-s",
159 neu.str("LDFLAGS", ""),
160 }, " ")
161 )
162 writerule("ar", {
163 command = "$ar rcs $out $in",
164 description = "archiving $out",
165 })
166 writerule("cc", {
167 command = "$cc $_cflags $cflags -MD -MF $out.d -c -o $out $in",
168 depfile = "$out.d",
169 deps = "gcc",
170 description = "compiling $out",
171 })
172 writerule("ld", {
173 command = "$cc $_ldflags $ldflags -o $out $in",
174 description = "linking $out",
175 })
176 local ourpath = package.searchpath("neutrino", package.path)
177 local regencmd = {}
178 for k, v in pairs(bools) do
179 table.insert(regencmd, k .. "=" .. (v and "1" or ""))
180 end
181 for k, v in pairs(enums) do
182 table.insert(regencmd, k .. "='" .. v .. "'")
183 end
184 for k, v in pairs(strs) do
185 table.insert(regencmd, k .. "='" .. v .. "'")
186 end
187 table.insert(regencmd, argv[-1])
188 table.insert(regencmd, "$in")
189 writerule("regen", {
190 command = table.concat(regencmd, " "),
191 description = "regenerating $out",
192 })
193 writebuild("build.ninja", "regen", argv[0], ourpath)
194 return neu
195end