commit b167d74

Emilia Smólska  ·  2026-08-08 00:47:38 +0000 UTC
parent b167d74
import
4 files changed,  +192, -0
A README
A main.c
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