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