commit e1178c0
shrub
·
2026-11-02 21:11:35 +0000 UTC
parent e1178c0
init
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(?...)
A
build.rv
+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
A
test.rvb
+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)