commit e1178c0

shrub  ·  2026-11-02 21:11:35 +0000 UTC
parent e1178c0
init
3 files changed,  +203, -0
A README
A README
+12, -0
 1@@ -0,0 +1,12 @@
 2+rvb
 3+---
 4+
 5+rvb is a small build engine written in (and configured in) revo. due to revo being turing complete,
 6+rvb is a turing complete build engine, and you can use revo to express complex build logic in it,
 7+checking dependencies, configure-script style stuff, and so on.
 8+
 9+todo
10+----
11+[ ] extensions for handling specific languages in a more high level way(c, zig)
12+[ ] extensions for doing configure script style stuff easily(checking whats installed on the system)
13+[ ] be good enough to build revo itself(?...)
+179, -0
  1@@ -0,0 +1,179 @@
  2+const rules = {}
  3+
  4+pub fn task(name, deps, action) do
  5+  if (table.key?(rules, name)) panic("duplicate task:", name)
  6+  rules[name] = {deps = table.unique(deps), action = action, phony = :true}
  7+end
  8+
  9+# a file rule must create the file named by path
 10+pub fn rule(path, deps, action) do
 11+  if (table.key?(rules, path)) panic("duplicate task:", path)
 12+  rules[path] = {deps = table.unique(deps), action = action, phony = :false}
 13+end
 14+
 15+# return true when an output is missing or older than an input
 16+pub fn timestamp(output, inputs) do
 17+  if (not fs.exists?(output)) return :true
 18+  const output_time = fs.stat(output):unwrap().mtime
 19+  for input in inputs do
 20+    if (not fs.exists?(input)) panic("missing input:", input)
 21+    if (fs.stat(input):unwrap().mtime > output_time) return :true
 22+  end
 23+  return :false
 24+end
 25+
 26+fn visit(name, state, order) do
 27+  if (state[name] == :done) return :ok
 28+  if (state[name] == :visiting) panic("dep cycle:", name)
 29+
 30+  if not table.key?(rules, name) do
 31+    if (fs.exists?(name)) return :ok
 32+    panic("unknown task or missing input:", name)
 33+  end
 34+  const entry = rules[name]
 35+
 36+  state[name] = :visiting
 37+  for dep in entry.deps do
 38+    visit(dep, state, order)
 39+  end
 40+
 41+  order:push(name)
 42+  state[name] = :done
 43+end
 44+
 45+pub fn mkplan(names) do
 46+  const state = {}
 47+  const order = {}
 48+  for name in names do
 49+    visit(name, state, order)
 50+  end
 51+  return order
 52+end
 53+
 54+fn release(name, pending, followers, ready) do
 55+  for next in followers[name] do
 56+    pending[next] -= 1
 57+    if (pending[next] == 0) ready:push(next)
 58+  end
 59+end
 60+
 61+fn dirty(name, rebuilt) do
 62+  const entry = rules[name]
 63+  if (entry.phony) return :true
 64+  for dep in entry.deps do
 65+    if table.key?(rules, dep) do
 66+      if (rules[dep].phony or rebuilt[dep] == :true) return :true
 67+    end
 68+  end
 69+  return timestamp(name, entry.deps)
 70+end
 71+
 72+fn execute(name, completed) do
 73+  const action = rules[name].action
 74+  match action()
 75+  | {:err, reason} => do
 76+    print("failed:", name, reason)
 77+    exit(1)
 78+  end
 79+  | _ => :ok
 80+
 81+  if not rules[name].phony and not fs.exists?(name) do
 82+    print("failed: rule did not create output:", name)
 83+    exit(1)
 84+  end
 85+  send(completed, name)
 86+end
 87+
 88+pub fn kahn(plan, jobs) do
 89+  if (jobs < 1 or jobs != number.floor(jobs))
 90+    panic("jobs must be a positive integer")
 91+
 92+  const nodes = mkplan(plan)
 93+  const pending = {}
 94+  const followers = {}
 95+  const ready = {}
 96+  const rebuilt = {}
 97+  const handles = {}
 98+  const completed = chan(jobs)
 99+  const total = table.len(nodes)
100+
101+  for name in nodes do
102+    pending[name] = 0
103+    followers[name] = {}
104+  end
105+  for name in nodes do
106+    for dep in rules[name].deps do
107+      if table.key?(rules, dep) do
108+        pending[name] += 1
109+        const next = followers[dep]
110+        next:push(name)
111+      end
112+    end
113+    if (pending[name] == 0) ready:push(name)
114+  end
115+
116+  let ready_index = 0
117+  let active = 0
118+  let finished = 0
119+  let reported = 0
120+  while finished < total do
121+    while active < jobs do
122+      if ready_index >= table.len(ready) do
123+        break
124+      end
125+      const name = ready[ready_index]
126+      ready_index += 1
127+      reported += 1
128+      if dirty(name, rebuilt) do
129+        print(fmt("[%v/%v] %v", reported, total, name))
130+        handles[name] = spawn execute(name, completed)
131+        active += 1
132+      end else do
133+        print(fmt("[%v/%v] %v (up to date)", reported, total, name))
134+        rebuilt[name] = :false
135+        finished += 1
136+        release(name, pending, followers, ready)
137+      end
138+    end
139+
140+    if active > 0 do
141+      const name = recv(completed)
142+      join(handles[name])
143+      active -= 1
144+      finished += 1
145+      rebuilt[name] = :true
146+      release(name, pending, followers, ready)
147+    end else do
148+      if (finished < total) panic("dependency cycle in build plan")
149+    end
150+  end
151+  return :ok
152+end
153+
154+pub fn run(plan) do
155+  return kahn(plan, revo.threads())
156+end
157+
158+fn showout(text) do
159+  if (text == "") return :ok
160+
161+  if (text:ends_with?("\n"))
162+    print(text:sub(0, text:len() - 1))
163+  else
164+    print(text)
165+end
166+
167+pub fn cmd(args) do
168+  const label = table.join(args, " ")
169+
170+  match system(args)
171+  | {:ok, {out, err}} => do
172+    showout(out)
173+    showout(err)
174+  end
175+  | {:err, reason} => do
176+    print("failed:", label)
177+    print(reason)
178+    exit(1)
179+  end
180+end
+12, -0
 1@@ -0,0 +1,12 @@
 2+const build = import "./build.rv"
 3+
 4+build.task("prepare", {}, fn() do
 5+  build.cmd({"ls"})
 6+end)
 7+
 8+build.task("test", {"prepare"}, fn() do
 9+  build.cmd({"echo", "hi from the build!"})
10+end)
11+
12+const plan = build.mkplan({"test"})
13+build.run(plan)