diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d22eb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +*.so diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed690c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: liblbuild_util.so + +liblbuild_util.so: util.c + $(CC) $(CFLAGS) -fPIC -shared util.c -o liblbuild_util.so + +$(DESTDIR)/usr/lib/liblbuild_util.so: liblbuild_util.so + install -D -m 0744 $< $@ + +$(DESTDIR)/usr/bin/lbuild: lbuild.lua + install -D -m 0755 $< $@ + +$(DESTDIR)/usr/share/lua/5.1/lbuild/%.lua: lbuild/%.lua + install -D -m 0744 $< $@ + +install: $(DESTDIR)/usr/lib/liblbuild_util.so $(DESTDIR)/usr/bin/lbuild \ + $(foreach l,$(shell ls lbuild),$(DESTDIR)/usr/share/lua/5.1/lbuild/$(l)) + +clean: + rm -f *.so example/*.o diff --git a/example/build.lua b/example/build.lua new file mode 100644 index 0000000..029ca67 --- /dev/null +++ b/example/build.lua @@ -0,0 +1,2 @@ +addcmd("hello.o", {"hello.c"}, {"/usr/bin/cc", "hello.c", "-o", "hello.o"}) +addcmd("hello", {}, {"/bin/echo", "hello", "world"}) diff --git a/example/hello.c b/example/hello.c new file mode 100644 index 0000000..bea32bc --- /dev/null +++ b/example/hello.c @@ -0,0 +1,5 @@ +#include + +int main(int argc, char **argv) { + printf("Hello world"); +} diff --git a/lbuild.lua b/lbuild.lua new file mode 100755 index 0000000..b674dbc --- /dev/null +++ b/lbuild.lua @@ -0,0 +1,36 @@ +#!/bin/luajit + +local ruletable = require("lbuild.ruletable") +local runner = require("lbuild.runner") + +local f = assert(io.open("build.lua", "rb")) +loadstring([[ +local runner = require("lbuild.runner") +local ruletable = require("lbuild.ruletable") +local add = function(...) + ruletable:add(...) +end +local addcmd = function(...) + ruletable:addcmd(...) +end +]] .. f:read("*all"))() + +local targs = {...} + +local n = #targs + +for _, r in ipairs(targs) do + ruletable:run(r):prom(function() + print("Done building " .. r) + n = n - 1 + if n == 0 then + os.exit() + end + end) +end + +if n == 0 then + print("Nothing to do") +end + +while true do runner:wait() end diff --git a/lbuild/cc.lua b/lbuild/cc.lua new file mode 100644 index 0000000..e553211 --- /dev/null +++ b/lbuild/cc.lua @@ -0,0 +1,81 @@ +--util for compiling c/c++ +--work in progress + +local runner = require("lbuild.runner") +local path = require("lbuild.path") +--local os lib +local os = os + +--the library table +local cc = {} + +--find cc to use +cc.CC = (os.getenv("CC") or ""):gmatch("^[ \n]+") +--find cxx to use +cc.CXX = (os.getenv("CXX") or ""):gmatch("^[ \n]+") +--load CFLAGS +cc.CFLAGS = (os.getenv("CFLAGS") or ""):gmatch("^[ \n]+") +--load CXXFLAGS +cc.CXXFLAGS = (os.getenv("CXXFLAGS") or ""):gmatch("^[ \n]+") + +--compiling a file with the -c option +--file, output file, extra args +function cc:ccc(file, outf, ...) + if outf == nil then + outf = path:stripext(file) .. ".o" + end + return runner:run(self.CC, "-c", table:unpack(self.CFLAGS), ..., "-o", outf) +end +function cc:cxxc(file, outf, ...) + if outf == nil then + outf = path:stripext(file) .. ".o" + end + return runner:run(self.CXX, "-c", table:unpack(self.CXXFLAGS), ..., "-o", outf) +end +--link objects together (mid-stage) +--syntax: output path, extra args (as a table), list of input objects (vararg) +function cc:partialld(outf, args, ...) + local inputs = {...} + return runner:run(self.CC, "-c", table:unpack(self.CFLAGS), ..., "-o", outf) +end +--link a finished output +--inputs: input file (multiple inputs possible as table), options table, output +--options: + --type: "executable", "shared" (default: executable) + --static: true, false (default: false) + --extraargs: string array (default: {}) +function cc:ld(input, output, options) + --select default options if missing + options = options or {} + options.type = options.type or "executable" + options.static = options.static or false + options.extraargs = options.extraargs or {} + if options.type == "shared" then + output = output or path:stripext(file) .. ".so" + else + output = output or path:stripext(file) .. ".o" + end + --check validity + if options.type ~= "executable" and options.type ~= "shared" then + error("bad cc type") + end + --check types + if type(static) ~= "boolean" then + error("static is not boolean") + end + if type(extraargs) ~= "table" then + error("extraargs is not table") + end + --generate args + local args = {} + if options.type == "shared" then + args[#args + 1] = "-shared" + end + if options.static then + args[#args + 1] = "-static" + end + --run thing + return runner:run(self.CC, table:unpack(args), table:unpack(self.CFLAGS), table:unpack(options.extraargs), ..., "-o", output) +end + +return cc diff --git a/lbuild/path.lua b/lbuild/path.lua new file mode 100644 index 0000000..3d55af2 --- /dev/null +++ b/lbuild/path.lua @@ -0,0 +1,9 @@ +--work in progress + +local path = {} + +function path:stripext(p) + return string.gsub(a, "%.+%a", "") +end + +return path diff --git a/lbuild/promise.lua b/lbuild/promise.lua new file mode 100644 index 0000000..ac41c67 --- /dev/null +++ b/lbuild/promise.lua @@ -0,0 +1,16 @@ +local p = {} + +p.__index = p + +function p:prom(success, fail) + if fail == nil then + fail = error + end + self.f(success, fail) +end + +local function promise(func) + return setmetatable({f = func}, p) +end + +return promise diff --git a/lbuild/ruletable.lua b/lbuild/ruletable.lua new file mode 100644 index 0000000..bbe0c3a --- /dev/null +++ b/lbuild/ruletable.lua @@ -0,0 +1,166 @@ +local path = require("lbuild.path") +local promise = require("lbuild.promise") +local runner = require("lbuild.runner") +local ffi = require("ffi") + +ffi.cdef[[ +int lbuild_util_isNewer(char *a, char *b); +char *strerror(int errnum); +bool exists(const char path[]); +]] + +local lbuild_util = ffi.load("liblbuild_util.so") + +local ruletbl = {} +local ruletable = ruletbl +ruletable.gen = {} +ruletable.rules = {} + +--add a rule +--rule: table + --rule.deps: a list or a function which returns a promise to a list + --rule.run: a function which returns a promise +function ruletbl:add(rule) + self.rules[rule.name] = rule +end + +--add a rule which executes a command +--name is the name of the rule +--deps are the dependencies of the command +--cmd is the command to run + --can be a table (argv) or a string + --if a string, is translated to sh -c cmd +function ruletbl:addcmd(name, deps, cmd) + local rule = {name = name} + function rule:deps() + return deps + end + if type(cmd) == "string" then + cmd = {"sh", "-c", cmd} + end + function rule:run() + return runner:run(unpack(cmd)) + end + self:add(rule) +end + +--add a rule generator +--genfunc is a func which takes one argument (a name) and returns a rule +--if the name does not match the generator pattern, genfunc should return nil +--unmatched names will be passed to the next generator +function ruletbl:addgenerator(genfunc) + self.gen[#self.gen + 1] = genfunc +end + +--returns whether file a is newer than b +function ruletbl:cmptime(a, b) + local res = lbuild_util:lbuild_util_isNewer(a, b) + if res == 1 then + return true + else + return false + end +end + +--run a dependency (as a promise) +--value of promise is a boolean indicating whether the dep is newer or not +function ruletbl:rundep(r, dep) + local rt = self + return promise(function(success, failure) + rt:run(dep):prom(function() + success(rt:cmptime(dep, r)) + end, failure) + end) +end + +--run a rule (returns a promise) +function ruletbl:run(name) + local rt = self + --get rule + local rule = self.rules[name] + if rule == nil then --rule not found - try to generate it + local n = #self.gen + while rule == nil and n ~= 0 do + rule = self.gen[n](name) + n = n - 1 + end + if rule == nil then --it wasn't found and could not be generated + error("Rule " .. name .. " does not exist") + end + --add new rule to lookup table + self.rules[name] = rule + end + return promise(function(success) + if rule.state ~= nil then + --if rule is already running (or done) dont start it again + if rule.state == "done" then + success() + else + if rule.cb ~= nil then + rule.cb = {} + end + rule.cb[#rule.cb + 1] = success + end + else --otherwise start it + --get dependencies + rule.state = "deplookup" + rule.cb = {success} + local depcb = function(deps) + rule.state = "deps" + local runcb = function() + for _, cb in ipairs(rule.cb) do + cb() + end + end + local n = #deps + 1 + local ischng = false + local startcb = function(chng) + if chng then + rule.state = "done" + runcb() + else + rule:run():prom(runcb) + end + end + local depfcb = function(chng) + n = n - 1 + ischng = ischng or chng + if n == 0 then + startcb(ischng) + end + end + for _, dep in ipairs(deps) do + rt:run(dep):prom(depfcb) + end + depfcb(false) + end + local dps = rule:deps() + if dps == nil then + depcb({}) + elseif dps.prom ~= nil then + dps:prom(depcb) + else + depcb(dps) + end + end + end) +end + +--add generator for rules for files that already exist +ruletable:addgenerator(function(name) + if lbuild_util.exists(name) then + local rule = {} + function rule:deps() + return nil + end + function rule:run() + return promise(function(s, f) + s() + end) + end + return rule + end + return nil +end) + +return ruletable diff --git a/lbuild/runner.lua b/lbuild/runner.lua new file mode 100644 index 0000000..5827baf --- /dev/null +++ b/lbuild/runner.lua @@ -0,0 +1,97 @@ +local ffi = require("ffi") +local lbuild_util = ffi.load("liblbuild_util.so") +local promise = require("lbuild.promise") + +ffi.cdef[[ +typedef int32_t pid_t; +pid_t lbuild_util_fexec(const char *argv[]); +unsigned int sleep(unsigned int seconds); +struct wpid_result { + pid_t pid; + int exitcode; +}; +struct wpid_result wpid(); +]] + +local runner = { + pt = {}, + queue = {}, + n = 0, + maxn = 1, +} + +local function ex(r, op) + --print command + print(table.concat(op.argv, " ")) + --run thing + local argv = ffi.new("const char*[?]", #op.argv + 1, op.argv) + argv[#op.argv] = nil + local pid = lbuild_util.lbuild_util_fexec(argv) + --handle fork failure + if pid == -1 then + op.fail("failed to fork: " .. ffi.errno()) + return + end + --update info + op.pid = pid + --add to lookup table + r.pt[pid] = op + r.n = r.n + 1 + return pid +end + +--waitpid wrapper +local function wpid() + local res = lbuild_util.wpid() + return res.pid, res.exitcode +end + +function runner:run(...) + local argv = {...} + local q = self.queue + return promise(function(success, failure) + if #argv == 0 then + failure("No command") + return + end + --create op + local op = { + argv = argv, + finish = success, + fail = failure, + } + -- not actually a queue - just stick it anywhere + q[#q + 1] = op + end) +end + +function runner:upd() + while self.n < self.maxn and #self.queue > 0 do + ex(self, self.queue[#self.queue]) + self.queue[#self.queue] = nil + end +end + +function runner:wait() + self:upd() + if self.n > 0 then + local pid, code = wpid() + local op = self.pt[pid] + if code == 0 then + op.finish() + else + op.fail(code) + end + self.pt[pid] = nil + op.pid = nil + else + error("Nothing running") + end +end + +function runner:setmaxn(n) + self.maxn = n + self:upd() +end + +return runner diff --git a/util.c b/util.c new file mode 100644 index 0000000..5b8b3ae --- /dev/null +++ b/util.c @@ -0,0 +1,58 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//fork/exec with child set to sudoku when the parent dies +pid_t lbuild_util_fexec(char *const argv[]) { + int fret = fork(); + switch(fret) { + case -1: + return -1; + case 0: + //prctl(PR_SET_PDEATHSIG, SIGKILL); + execvpe(argv[0], argv, environ); + printf("Exec fail: %s\n", strerror(errno)); + exit(65); + default: + return fret; + } +} + +int lbuild_util_isNewer(char *a, char *b) { + struct stat sa, sb; + if(stat(a, &sa) != 0) { + return -1; + } + if(stat(b, &sb) != 0) { + return -1; + } + if(sa.st_mtim.tv_sec >= sb.st_mtim.tv_sec) { + return 1; + } + return 0; +} + +bool exists(const char path[]) { + return access(path, R_OK) == 0; +} + +struct wpid_result { + pid_t pid; + int exitcode; +}; + +struct wpid_result wpid() { + struct wpid_result res; + int status; + res.pid = waitpid(-1, &status, 0); + res.exitcode = WEXITSTATUS(status); + return res; +}