code wiki / (root) / nx_nxmake.nx

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}