nxmake.nx source
↩ module page · 492 lines · 18684 B
1// nxmake.nx -- sovereign build driver (replaces GNU make).
2//
3// Scope claim:
4// GNU make is Turing-complete and huge. We don't need most of it.
5// The typical Nishi build is: list of inputs -> one or more
6// command invocations -> an output file. Make's real
7// differentiator is MTIME-based staleness checking; we replicate
8// that with fs.nx's fstat.
9//
10// Replaces on the Nishi build path:
11// make / gmake / bmake — any POSIX make
12//
13// File format: plain-text stanzas in "nx.build":
14//
15// target: kernel.elf
16// inputs: boot.S trap.S src/sched_bootstrap.S nishi-kernel.s
17// cmd: ./nxld --base 0x80000000 -o kernel.elf <inputs>
18//
19// target: nishi-kernel.s
20// inputs: src/kmain.nx src/uart.nx ...
21// cmd: ./nxc2 --target asm --opt src/kmain.nx > $@
22//
23// Comments: lines starting with '#'. Blank lines separate stanzas.
24//
25// Subprocess-exec is intentionally absent from this seed. Windows
26// host (CreateProcessA) and Linux host (clone+execve) diverge at
27// the syscall layer and we don't have a portable abstraction yet.
28// Instead nxmake emits a build plan -- an ordered list of commands
29// and their staleness verdicts -- that a thin shim script runs.
30// This is honest: nxmake owns the dependency reasoning, the shim
31// owns the syscall specifics. Decoupled sovereignty.
32
33import "syscalls.nx"
34import "fs.nx"
35
36// ---------------------------------------------------------------
37// Storage layout
38// ---------------------------------------------------------------
39// Single mmap'd scratch carries:
40// rule records at offset 0, each 48 bytes (6 i64)
41// [target_off, target_len, n_inputs, inputs_base, cmd_off, cmd_len]
42// input records at offset MAX_RULES*48, each 16 bytes (2 i64)
43// [in_off, in_len]
44// line table follows, each 16 bytes (2 i64)
45// [line_off, line_len]
46//
47// This is a small enough problem to not bother with a real
48// allocator. Fixed caps are loud and easy to audit.
49
50const MAX_RULES: i64 = 64
51const MAX_INPUTS: i64 = 1024
52const MAX_LINES: i64 = 512
53const RULE_SIZE: i64 = 48
54const INPUT_SIZE: i64 = 16
55const LINE_SIZE: i64 = 16
56
57// Rule-field accessors (returns pointer into the rule record).
58func rule_target_off(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 0; return a as *i64 }
59func rule_target_len(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 8; return a as *i64 }
60func rule_n_inputs(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 16; return a as *i64 }
61func rule_inputs_at(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 24; return a as *i64 }
62func rule_cmd_off(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 32; return a as *i64 }
63func rule_cmd_len(rb: i64, i: i64) -> *i64 { let a: i64 = rb + i * RULE_SIZE + 40; return a as *i64 }
64
65func input_off_p(ib: i64, j: i64) -> *i64 { let a: i64 = ib + j * INPUT_SIZE + 0; return a as *i64 }
66func input_len_p(ib: i64, j: i64) -> *i64 { let a: i64 = ib + j * INPUT_SIZE + 8; return a as *i64 }
67
68func line_off_p(lb: i64, k: i64) -> *i64 { let a: i64 = lb + k * LINE_SIZE + 0; return a as *i64 }
69func line_len_p(lb: i64, k: i64) -> *i64 { let a: i64 = lb + k * LINE_SIZE + 8; return a as *i64 }
70
71// ---------------------------------------------------------------
72// String helpers
73// ---------------------------------------------------------------
74
75func nxmake_strlen(p: *u8) -> i64 {
76 var n: i64 = 0
77 while p[n] != 0 { n = n + 1 }
78 return n
79}
80
81// Compare a (buf, off, len) slice against a null-terminated keyword.
82// Returns 1 on match, 0 on mismatch. Lengths must match exactly.
83func slice_eq_cstr(buf: *u8, off: i64, len: i64, kw: *u8) -> i64 {
84 let kwl: i64 = nxmake_strlen(kw)
85 if len != kwl { return 0 }
86 var i: i64 = 0
87 while i < len {
88 if buf[off + i] != kw[i] { return 0 }
89 i = i + 1
90 }
91 return 1
92}
93
94// Does a slice start with `kw`? Returns 1 or 0.
95func slice_starts_with(buf: *u8, off: i64, len: i64, kw: *u8) -> i64 {
96 let kwl: i64 = nxmake_strlen(kw)
97 if len < kwl { return 0 }
98 var i: i64 = 0
99 while i < kwl {
100 if buf[off + i] != kw[i] { return 0 }
101 i = i + 1
102 }
103 return 1
104}
105
106// Write slice into null-terminated cstring in `out`.
107func slice_to_cstr(buf: *u8, off: i64, len: i64, out: *u8) -> i64 {
108 var i: i64 = 0
109 while i < len {
110 out[i] = buf[off + i]
111 i = i + 1
112 }
113 out[i] = 0
114 return len
115}
116
117// Trim leading whitespace (space, tab, CR). Returns new offset.
118func trim_left(buf: *u8, off: i64, end: i64) -> i64 {
119 var p: i64 = off
120 while p < end {
121 let c: i64 = buf[p]
122 if c == 0x20 { p = p + 1 } else {
123 if c == 0x09 { p = p + 1 } else {
124 if c == 0x0D { p = p + 1 } else { return p }
125 }
126 }
127 }
128 return p
129}
130
131// Trim trailing CR / whitespace from (off, len). Returns new len.
132func trim_right(buf: *u8, off: i64, len: i64) -> i64 {
133 var L: i64 = len
134 while L > 0 {
135 let c: i64 = buf[off + L - 1]
136 if c == 0x20 { L = L - 1 } else {
137 if c == 0x09 { L = L - 1 } else {
138 if c == 0x0D { L = L - 1 } else { return L }
139 }
140 }
141 }
142 return L
143}
144
145// ---------------------------------------------------------------
146// Line splitter
147// ---------------------------------------------------------------
148
149// Split buf[0..n] into lines (LF-separated). Writes line records
150// at line_base. Returns line count.
151func split_lines(buf: *u8, n: i64, line_base: i64) -> i64 {
152 var pos: i64 = 0
153 var lc: i64 = 0
154 var start: i64 = 0
155 while pos < n {
156 if buf[pos] == 0x0A {
157 if lc < MAX_LINES {
158 let po: *i64 = line_off_p(line_base, lc)
159 let pl: *i64 = line_len_p(line_base, lc)
160 *po = start
161 *pl = pos - start
162 lc = lc + 1
163 }
164 pos = pos + 1
165 start = pos
166 } else {
167 pos = pos + 1
168 }
169 }
170 if start < n {
171 if lc < MAX_LINES {
172 let po: *i64 = line_off_p(line_base, lc)
173 let pl: *i64 = line_len_p(line_base, lc)
174 *po = start
175 *pl = n - start
176 lc = lc + 1
177 }
178 }
179 return lc
180}
181
182// ---------------------------------------------------------------
183// Rule parser
184// ---------------------------------------------------------------
185//
186// Grammar (informal):
187// file := (comment | blank | stanza)*
188// stanza := target_line (indent inputs_line)? (indent cmd_line)?
189// target_line := "target:" WS path
190// inputs_line := "inputs:" WS path (WS path)*
191// cmd_line := "cmd:" WS text-to-EOL
192//
193// Returns number of rules parsed, or -1 on error.
194func parse_rules(
195 buf: *u8,
196 line_base: i64, lc: i64,
197 rule_base: i64,
198 input_base: i64
199) -> i64 {
200 var rc: i64 = 0
201 var ic: i64 = 0
202 var cur: i64 = -1
203 var k: i64 = 0
204 while k < lc {
205 let po: *i64 = line_off_p(line_base, k)
206 let pl: *i64 = line_len_p(line_base, k)
207 let l_off: i64 = *po
208 let l_len: i64 = *pl
209 // Trim.
210 let start: i64 = trim_left(buf, l_off, l_off + l_len)
211 let tail_len: i64 = (l_off + l_len) - start
212 let real_len: i64 = trim_right(buf, start, tail_len)
213 if real_len == 0 {
214 k = k + 1
215 } else {
216 let first: i64 = buf[start]
217 if first == 0x23 {
218 // comment; skip
219 k = k + 1
220 } else {
221 if slice_starts_with(buf, start, real_len, "target:") == 1 {
222 // new rule
223 if rc >= MAX_RULES { return -1 }
224 let v_off: i64 = trim_left(buf, start + 7, start + real_len)
225 let v_len: i64 = trim_right(buf, v_off, (start + real_len) - v_off)
226 let tof: *i64 = rule_target_off(rule_base, rc)
227 let tle: *i64 = rule_target_len(rule_base, rc)
228 let ninp: *i64 = rule_n_inputs(rule_base, rc)
229 let ibase: *i64 = rule_inputs_at(rule_base, rc)
230 let cof: *i64 = rule_cmd_off(rule_base, rc)
231 let cle: *i64 = rule_cmd_len(rule_base, rc)
232 *tof = v_off
233 *tle = v_len
234 *ninp = 0
235 *ibase = ic
236 *cof = 0
237 *cle = 0
238 cur = rc
239 rc = rc + 1
240 k = k + 1
241 } else {
242 if slice_starts_with(buf, start, real_len, "inputs:") == 1 {
243 if cur < 0 { return -1 }
244 let v_off0: i64 = trim_left(buf, start + 7, start + real_len)
245 let v_end: i64 = start + real_len
246 var p: i64 = v_off0
247 while p < v_end {
248 let s: i64 = trim_left(buf, p, v_end)
249 if s >= v_end { p = v_end } else {
250 // scan until ws
251 var q: i64 = s
252 while q < v_end {
253 let c: i64 = buf[q]
254 if c == 0x20 { q = v_end + 1 } else {
255 if c == 0x09 { q = v_end + 1 } else {
256 q = q + 1
257 }
258 }
259 }
260 let end_tok: i64 = q
261 var real_end: i64 = end_tok
262 if end_tok > v_end { real_end = q - 1 }
263 let tlen: i64 = real_end - s
264 if tlen > 0 {
265 if ic >= MAX_INPUTS { return -1 }
266 let iof: *i64 = input_off_p(input_base, ic)
267 let ilen: *i64 = input_len_p(input_base, ic)
268 *iof = s
269 *ilen = tlen
270 let ninp2: *i64 = rule_n_inputs(rule_base, cur)
271 let cnt: i64 = *ninp2
272 *ninp2 = cnt + 1
273 ic = ic + 1
274 }
275 p = real_end + 1
276 }
277 }
278 k = k + 1
279 } else {
280 if slice_starts_with(buf, start, real_len, "cmd:") == 1 {
281 if cur < 0 { return -1 }
282 let v_off2: i64 = trim_left(buf, start + 4, start + real_len)
283 let v_len2: i64 = (start + real_len) - v_off2
284 let cof2: *i64 = rule_cmd_off(rule_base, cur)
285 let cle2: *i64 = rule_cmd_len(rule_base, cur)
286 *cof2 = v_off2
287 *cle2 = v_len2
288 k = k + 1
289 } else {
290 // unknown line -> ignore (forward-compat)
291 k = k + 1
292 }
293 }
294 }
295 }
296 }
297 }
298 return rc
299}
300
301// ---------------------------------------------------------------
302// Staleness
303// ---------------------------------------------------------------
304
305// Get mtime of a null-terminated path. -1 if not found.
306// Uses the _raw syscall wrapper: the Result-returning fs_open_rd adds
307// allocation cost and we only need the bare fd number here; missing
308// files are an expected staleness-check outcome, not an error to
309// surface to callers.
310func nxmake_mtime(path: *u8) -> i64 {
311 let fd: i64 = fs_open_rd_raw(path)
312 if fd < 0 { return -1 }
313 let scratch: *u8 = sys_mmap(160)
314 let rc: i64 = __syscall(80, fd, scratch as i64, 0, 0, 0, 0)
315 sys_close(fd)
316 if rc < 0 { return -1 }
317 let sb: i64 = scratch as i64
318 let mt_addr: i64 = sb + 88
319 let mt: *i64 = mt_addr as *i64
320 return *mt
321}
322
323// Find rule index whose target matches (name_buf+name_off, name_len).
324// Returns -1 if no such rule.
325func find_rule(
326 buf: *u8, rule_base: i64, n_rules: i64,
327 name_off: i64, name_len: i64
328) -> i64 {
329 var i: i64 = 0
330 while i < n_rules {
331 let pof: *i64 = rule_target_off(rule_base, i)
332 let ple: *i64 = rule_target_len(rule_base, i)
333 let of: i64 = *pof
334 let le: i64 = *ple
335 if le == name_len {
336 var m: i64 = 1
337 var j: i64 = 0
338 while j < le {
339 if buf[of + j] != buf[name_off + j] { m = 0 }
340 j = j + 1
341 }
342 if m == 1 { return i }
343 }
344 i = i + 1
345 }
346 return -1
347}
348
349// Write a slice as a cstring into scratch, return mtime. -1 if absent.
350func slice_mtime(buf: *u8, off: i64, len: i64) -> i64 {
351 let scratch: *u8 = sys_mmap(1024)
352 slice_to_cstr(buf, off, len, scratch)
353 return nxmake_mtime(scratch)
354}
355
356// Is rule `ri` stale? 1 if yes, 0 if up-to-date.
357func is_stale(
358 buf: *u8, rule_base: i64, input_base: i64, ri: i64
359) -> i64 {
360 let ptof: *i64 = rule_target_off(rule_base, ri)
361 let ptle: *i64 = rule_target_len(rule_base, ri)
362 let t_mt: i64 = slice_mtime(buf, *ptof, *ptle)
363 if t_mt < 0 { return 1 }
364 let pnin: *i64 = rule_n_inputs(rule_base, ri)
365 let pibas: *i64 = rule_inputs_at(rule_base, ri)
366 let nin: i64 = *pnin
367 let ibas: i64 = *pibas
368 var j: i64 = 0
369 while j < nin {
370 let pio: *i64 = input_off_p(input_base, ibas + j)
371 let pil: *i64 = input_len_p(input_base, ibas + j)
372 let in_mt: i64 = slice_mtime(buf, *pio, *pil)
373 if in_mt > t_mt { return 1 }
374 j = j + 1
375 }
376 return 0
377}
378
379// ---------------------------------------------------------------
380// Walker
381// ---------------------------------------------------------------
382//
383// Depth-first traversal from the requested target. For each rule,
384// visit its dep-rules first, then emit "BUILD: <cmd>" if stale or
385// "SKIP: <target>" if fresh. Cycles are not checked (projects of
386// this scope don't need it; we add detection when we hit one).
387
388func emit_plan_for(
389 buf: *u8, rule_base: i64, input_base: i64, n_rules: i64,
390 ri: i64, out_fd: i64
391) -> i64 {
392 // Visit deps that are themselves rules.
393 let pnin: *i64 = rule_n_inputs(rule_base, ri)
394 let pibas: *i64 = rule_inputs_at(rule_base, ri)
395 let nin: i64 = *pnin
396 let ibas: i64 = *pibas
397 var j: i64 = 0
398 while j < nin {
399 let pio: *i64 = input_off_p(input_base, ibas + j)
400 let pil: *i64 = input_len_p(input_base, ibas + j)
401 let dep_ri: i64 = find_rule(buf, rule_base, n_rules, *pio, *pil)
402 if dep_ri >= 0 {
403 emit_plan_for(buf, rule_base, input_base, n_rules, dep_ri, out_fd)
404 }
405 j = j + 1
406 }
407 // Emit line for this rule.
408 let ptof: *i64 = rule_target_off(rule_base, ri)
409 let ptle: *i64 = rule_target_len(rule_base, ri)
410 let pcof: *i64 = rule_cmd_off(rule_base, ri)
411 let pcle: *i64 = rule_cmd_len(rule_base, ri)
412 let stale: i64 = is_stale(buf, rule_base, input_base, ri)
413 if stale == 1 {
414 sys_write(out_fd, "BUILD: " as *u8, 7)
415 sys_write(out_fd, (buf as i64 + *ptof) as *u8, *ptle)
416 sys_write(out_fd, "\n " as *u8, 8)
417 sys_write(out_fd, (buf as i64 + *pcof) as *u8, *pcle)
418 sys_write(out_fd, "\n" as *u8, 1)
419 } else {
420 sys_write(out_fd, "SKIP: " as *u8, 7)
421 sys_write(out_fd, (buf as i64 + *ptof) as *u8, *ptle)
422 sys_write(out_fd, "\n" as *u8, 1)
423 }
424 return 0
425}
426
427// ---------------------------------------------------------------
428// Entry
429// ---------------------------------------------------------------
430
431func main(argc: i64, argv: *i64) -> i64 {
432 var path: *u8 = "nx.build"
433 var goal: *u8 = 0 as *u8
434 if argc >= 2 { path = (argv[1]) as *u8 }
435 if argc >= 3 { goal = (argv[2]) as *u8 }
436 let len_raw: *u8 = sys_mmap(16)
437 let len: *i64 = len_raw as *i64
438 *len = 0
439 let buf: *u8 = sys_read_file(path, len)
440 if buf == (0 as *u8) {
441 sys_write(2, "nxmake: cannot open build file\n" as *u8, 31)
442 return 2
443 }
444 let n: i64 = *len
445 let rule_base_u: *u8 = sys_mmap(MAX_RULES * RULE_SIZE)
446 let input_base_u: *u8 = sys_mmap(MAX_INPUTS * INPUT_SIZE)
447 let line_base_u: *u8 = sys_mmap(MAX_LINES * LINE_SIZE)
448 let rule_base: i64 = rule_base_u as i64
449 let input_base: i64 = input_base_u as i64
450 let line_base: i64 = line_base_u as i64
451 let lc: i64 = split_lines(buf, n, line_base)
452 let n_rules: i64 = parse_rules(buf, line_base, lc, rule_base, input_base)
453 if n_rules < 0 {
454 sys_write(2, "nxmake: parse error\n" as *u8, 20)
455 return 3
456 }
457 // Choose goal: argv[2] if present, else the first rule.
458 var goal_ri: i64 = -1
459 if goal != (0 as *u8) {
460 let gl: i64 = nxmake_strlen(goal)
461 // Scan rules for a target name equal to `goal`.
462 var i: i64 = 0
463 while i < n_rules {
464 let pof: *i64 = rule_target_off(rule_base, i)
465 let ple: *i64 = rule_target_len(rule_base, i)
466 let of: i64 = *pof
467 let le: i64 = *ple
468 if le == gl {
469 var m: i64 = 1
470 var j: i64 = 0
471 while j < le {
472 if buf[of + j] != goal[j] { m = 0 }
473 j = j + 1
474 }
475 if m == 1 { goal_ri = i }
476 }
477 i = i + 1
478 }
479 if goal_ri < 0 {
480 sys_write(2, "nxmake: no rule for goal\n" as *u8, 25)
481 return 4
482 }
483 } else {
484 if n_rules > 0 { goal_ri = n_rules - 1 } // last rule = top
485 }
486 if goal_ri < 0 {
487 sys_write(2, "nxmake: empty build file\n" as *u8, 25)
488 return 5
489 }
490 emit_plan_for(buf, rule_base, input_base, n_rules, goal_ri, 1)
491 return 0
492}