code wiki / (root) / nxmake.nx

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}