commit b167d74
Emilia Smólska
·
2026-08-08 00:47:38 +0000 UTC
parent b167d74
import
4 files changed,
+192,
-0
A
README
+9,
-0
1@@ -0,0 +1,9 @@
2+this is a lil build system
3+
4+bugs: yes
5+features: missing
6+
7+todo:
8+- documentation
9+- shared libraries?
10+- pkg-config and shit
+3,
-0
1@@ -0,0 +1,3 @@
2+local neu = require("neutrino")(arg)
3+
4+neu.bin("main", { "main.c" })
A
main.c
+6,
-0
1@@ -0,0 +1,6 @@
2+#include <stdio.h>
3+
4+int main(void) {
5+ printf("hello, shrub\n");
6+ return 0;
7+}
+174,
-0
1@@ -0,0 +1,174 @@
2+-- neutrino - a simple and opinionated build system for C
3+
4+local neu = {}
5+
6+local function check(s)
7+ if not s:match("^[%u_]+$") then
8+ error(("name '%s' invalid"):format(s))
9+ end
10+end
11+
12+local function memq(elt, tbl)
13+ for _, x in pairs(tbl) do if elt == x then return true end end
14+ return false
15+end
16+
17+local bools = {}
18+function neu.bool(name)
19+ check(name)
20+ if bools[name] then
21+ error(("option '%s' redeclared"):format(name))
22+ end
23+ bools[name] = #(os.getenv(name) or "") > 0
24+ return bools[name]
25+end
26+
27+local enums = {}
28+function neu.enum(name, set)
29+ check(name)
30+ if enums[name] then
31+ error(("option '%s' redeclared"):format(name))
32+ end
33+ local val = os.getenv(name)
34+ if not val then enums[name] = set[1]
35+ elseif not memq(val, set) then
36+ local s = table.concat(set, "', '")
37+ error(("value '%s' not in { '%s' }"):format(val, s))
38+ else enums[name] = val end
39+ return enums[name]
40+end
41+
42+local strs = {}
43+function neu.str(name, dfl)
44+ check(name)
45+ if strs[name] then
46+ error(("option '%s' redeclared"):format(name))
47+ end
48+ strs[name] = os.getenv(name) or dfl
49+ return strs[name]
50+end
51+
52+local no_color = neu.bool("NO_COLOR")
53+local build_type = neu.enum("BUILD_TYPE", { "debug", "smol", "zomg" })
54+
55+local bn = assert(io.open("build.ninja", "w"))
56+
57+local function src2obj(s)
58+ return "obj/" .. s:gsub(".c$", ".o"):gsub("/", ".")
59+end
60+
61+local function writevar(name, value)
62+ bn:write(name, "=", value, "\n")
63+end
64+
65+local function writerule(name, tbl)
66+ bn:write("rule ", name, "\n")
67+ for k, v in pairs(tbl) do writevar(" " .. k, v, "\n") end
68+end
69+
70+local function writebuild(tgt, rule, prereq, impl)
71+ if type(prereq) == "table" then
72+ prereq = table.concat(prereq, " ")
73+ end
74+ if type(impl) == "table" then
75+ impl = table.concat(impl, " ")
76+ end
77+ impl = impl and "|" .. impl or ""
78+ bn:write("build ", tgt, ":", rule, " ", prereq, impl, "\n")
79+end
80+
81+local function aggr(name, rule, srcs, opts)
82+ opts = opts or {}
83+ opts.std = opts.std or "c17"
84+ local stds = { c17 = true, c23 = true }
85+ local objs = {}
86+ for i, x in ipairs(srcs) do objs[i] = src2obj(x) end
87+ writebuild("obj/" .. name, rule, objs)
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
100+end
101+
102+function neu.bin(bin, srcs, opts) aggr(bin, "ld", srcs, opts) end
103+
104+function neu.lib(lib, srcs, opts)
105+ aggr("lib" .. lib .. ".a", "ar", srcs, opts)
106+end
107+
108+if not package.searchpath then
109+ function package.searchpath(name, path)
110+ name = name:gsub("%.", "/"):gsub("%%", "%%%%")
111+ for t in path:gmatch("[^;]+") do
112+ local fn = t:gsub("%?", name)
113+ local fd = io.open(fn, "r")
114+ if fd and (fd:close() or true) then return fn end
115+ end
116+ return nil, "not found"
117+ end
118+end
119+
120+return function(argv)
121+ writevar("ar", neu.str("AR", "ar"))
122+ writevar("cc", neu.str("CC", "cc"))
123+ writevar(
124+ "cflags",
125+ table.concat({
126+ no_color and "-fno-diagnostics-color" or "-fdiagnostics-color",
127+ "-Wall -Wextra -Wpedantic",
128+ -- XXX: -s or strip when installing? should research more
129+ ({
130+ debug = "-g",
131+ smol = "-Os -s",
132+ zomg = "-O3 -s"
133+ })[build_type],
134+ "-fno-semantic-interposition",
135+ neu.str("CFLAGS", ""),
136+ }, " ")
137+ )
138+ writevar(
139+ "ldflags",
140+ "-static " .. neu.str("LDFLAGS", "")
141+ )
142+ writerule("ar", {
143+ command = "$ar rcs $out $in",
144+ description = "archiving $out",
145+ })
146+ writerule("cc", {
147+ command = "$cc $_cflags $cflags -MD -MF $out.d -c -o $out $in",
148+ depfile = "$out.d",
149+ deps = "gcc",
150+ description = "compiling $out",
151+ })
152+ writerule("ld", {
153+ command = "$cc $_ldflags $ldflags -o $out $in",
154+ description = "linking $out",
155+ })
156+ local ourpath = package.searchpath("neutrino", package.path)
157+ regencmd = {}
158+ for k, v in pairs(bools) do
159+ table.insert(regencmd, k .. "=" .. (v and "1" or ""))
160+ end
161+ for k, v in pairs(enums) do
162+ table.insert(regencmd, k .. "='" .. v .. "'")
163+ end
164+ for k, v in pairs(strs) do
165+ table.insert(regencmd, k .. "='" .. v .. "'")
166+ end
167+ table.insert(regencmd, argv[-1])
168+ table.insert(regencmd, "$in")
169+ writerule("regen", {
170+ command = table.concat(regencmd, " "),
171+ description = "regenerating $out",
172+ })
173+ writebuild("build.ninja", "regen", argv[0], ourpath)
174+ return neu
175+end