code wiki / _hdl_build / nx_sov_build_run.nx

nx_sov_build_run.nx source

↩ module page · 532 lines · 36152 B

1// nx_sov_build_run.nx -- FULLY SOVEREIGN, REUSABLE build runner (operator: "no sh and no c build, 2// from the hardware layer up"). Generalizes nx_retire_gcc_orchestrator from hardcoded targets to ANY 3// module passed as argv[1]. The build path is bits-up sovereign end to end: 4// nx_cc_sovereign.elf <module>.nx -> _build/<module>.s (the team's SELF-HOSTED compiler) 5// nxasm_x86_main.elf <module>.s -> _build/<module>.sov.elf (the team's x86-64 assembler+linker) 6// _build/<module>.sov.elf (run it; exit = its exit) 7// ⚠OUTPUT PATH IS _build/, RELATIVE TO CWD -- NOT /tmp/. These three lines said /tmp/ until 2026-07-30, 8// left stale by the flock change that moved artifacts to a per-target _build/<name>.lock+.s+.sov.elf so 9// concurrent sweeps stop overwriting each other mid-build. A doc that disagrees with the code is a defect, 10// not a nit: it is why callers hunt for the artifact, and it hid a REAL brick hazard -- nx_hostctl 11// cmd_buildrun (nx_hostctl.nx:3089) still reads /tmp/<name>.sov.elf, so this runner and that supervisor 12// MUST be shipped as a matched pair or every /api/build in the ecosystem fails "no /tmp/<name>.sov.elf". 13// NO gcc, NO bash, NO .sh anywhere. Orchestration is NishiLang sys_fork/dup3/execve/wait4. 14// Recompile-retry guards the known-good compiler's empty-.s nondeterminism. Usage: 15// nx_sov_build_run.elf <module-basename-in-runtime/_hdl_build> 16// license_tier: ORIGINAL Reuses the _run spine from nx_retire_gcc_orchestrator. 17import "nx_syscalls.nx" 18import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 19 20// TOOLCHAIN NAMES -- the binaries that BUILD everything else, so they may never be installed as a 21// side effect of a build (see the refusal at the install site; seq1464). 22// ⚠BOTH COMPILER NAMES: the module is `nx_compile_x86` but it ships as `nx_cc_sovereign.elf`, so a 23// guard that knew only one name would leave the other door open -- which is exactly the door the 24// regressed compiler came through. `nx_sov_build_run` is listed because a runner that can overwrite 25// ITSELF mid-build is the same hazard pointed inward. 26func sbr_name_is(a: *u8, b: *u8) -> i64 { 27 var i: i64 = 0 28 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 29 if b[i] != (0 as u8) { return 0 } 30 return 1 31} 32func sbr_is_toolchain(name: *u8) -> i64 { 33 if sbr_name_is(name, "nx_compile_x86" as *u8) == 1 { return 1 } 34 if sbr_name_is(name, "nx_cc_sovereign" as *u8) == 1 { return 1 } 35 if sbr_name_is(name, "nxasm_x86_main" as *u8) == 1 { return 1 } 36 if sbr_name_is(name, "nxasm_x86" as *u8) == 1 { return 1 } 37 if sbr_name_is(name, "nx_sov_build_run" as *u8) == 1 { return 1 } 38 return 0 39} 40const SBR_MAGIC_4096: i64 = 4096 // read-chunk size (folded from the buildroot branch's rule-11 sweep, 2026-07-29 merge) 41 42const SBR_OK: i64 = 0 43const SBR_USAGE: i64 = 2 44const SBR_COMPILE_FAIL: i64 = 3 45const SBR_ASM_FAIL: i64 = 4 46const SBR_ADMIT_REFUSED: i64 = 6 // build admission said no; distinct from a compile/assemble failure 47 // (3 and 4 raw would COLLIDE with COMPILE_FAIL/ASM_FAIL -- a retry-later 48 // signal must never decode as a broken build) 49const SBR_CANON_REFUSED: i64 = 7 // tree-canon divergence: building would compile a FORKED copy 50const SBR_MIN_ASM_BYTES: i64 = 128 // empty/failed .s is ~0-byte header; the smallest real program (_min42) is 540B 51const SBR_MAX_RETRIES: i64 = 12 52 53func sbr_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 54// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 55// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 56// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 57// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 58func sbr_putn(v: i64) -> i64 { nxi_out(v); return 0 } 59 60// append NUL-terminated s into dst at off; return new offset (no NUL written) 61func sbr_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 62 63// bounded byte-range equality on (base,offset,len) pairs -- conf/manifest lines are not NUL-terminated 64func sbr_ceq_at(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 { 65 if al != bl { return 0 } 66 var i: i64 = 0 67 while i < al { if a[ao+i] != b[bo+i] { return 0 } i = i + 1 } 68 return 1 69} 70 71func sbr_exitcode(status: i64) -> i64 { return (status >> 8) & 0xff } 72 73// fork + optional stdout/stderr redirect + execve; parent waits; returns child's WEXITSTATUS, 74// OR 128+signal if the child died to a signal (shell convention). Without the signal check a 75// SEGFAULTED tool decodes as rc=0 = silent fake success (caught live 2026-06-09: nxasm segfaulted 76// on the 2.4MB compiler .s, the runner reported the assemble step OK, and the missing ELF 77// surfaced later as a confusing execve-127). Composes [[feedback-evidence-driven-live-grading]]. 78// redir_err>=0 -> child stderr (e.g. /dev/null to mute nx_cc's symbol-table dump). 79func sbr_run(path: *u8, argv: *i64, envp: *i64, redir_out: i64, redir_err: i64) -> i64 { 80 let pid: i64 = sys_fork() 81 if pid == 0 { 82 if redir_out >= 0 { sys_dup3(redir_out, 1, 0) } 83 if redir_err >= 0 { sys_dup3(redir_err, 2, 0) } 84 sys_execve(path, argv, envp) 85 sys_exit(127) 86 } 87 let st: *i64 = sys_mmap(16) as *i64 88 sys_wait4(pid, st, 0) 89 let sig: i64 = st[0] & 0x7f 90 if sig != 0 { return 128 + sig } 91 return sbr_exitcode(st[0]) 92} 93 94// byte-size of a file (open, read in chunks, count) -- the determinism check for the empty-.s guard. 95func sbr_filesize(path: *u8) -> i64 { 96 let fd: i64 = sys_openat_rd(path) 97 if fd < 0 { return 0 } 98 let buf: *u8 = sys_mmap(SBR_MAGIC_4096) 99 var total: i64 = 0 100 var n: i64 = sys_read(fd, buf, SBR_MAGIC_4096) 101 while n > 0 { total = total + n; n = sys_read(fd, buf, SBR_MAGIC_4096) } 102 sys_close(fd) 103 return total 104} 105 106func main(argc: i64, argv: *i64) -> i64 { 107 if argc < 2 { sbr_puts("usage: nx_sov_build_run <module-basename> [--build-only|--install] [args-forwarded-to-program...]\n" as *u8); sys_exit(SBR_USAGE); return SBR_USAGE } 108 let name: *u8 = argv[1] as *u8 109 110 // FLAGS (argv[2]): --build-only (--b*) = skip the run; --install (--i*) = FORCE-install /tmp/<name>.sov.elf 111 // to _offc/<name>.elf even with NO existing twin (makes a reusable TOOL permanent on first build) + skip run. 112 // Default = refresh-IF-PRESENT (LM-026) + run. Opt-in so throwaway gates/probes never clutter _offc. 113 // Everything AFTER the optional flag is FORWARDED to the built program (fwd_start below). 114 var force_install: i64 = 0 115 var skip_run: i64 = 0 116 var fwd_start: i64 = 2 117 if argc >= 3 { 118 let fl: *u8 = argv[2] as *u8 119 if fl[0] == (45 as u8) { if fl[1] == (45 as u8) { 120 if fl[2] == (98 as u8) { skip_run = 1 } // --build-only 121 if fl[2] == (105 as u8) { force_install = 1; skip_run = 1 } // --install 122 fwd_start = 3 // a --flag is the lane's, never forwarded 123 } } 124 } 125 126 let src: *u8 = sys_mmap(512); var o: i64 = 0 127 o = sbr_cat(src, o, "runtime/_hdl_build/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 128 // fall back to runtime/<name>.nx if the module isn't under _hdl_build (additive; the older slice stack lives in runtime/) 129 let chk: i64 = sys_openat_rd(src) 130 if chk < 0 { 131 o = 0 132 o = sbr_cat(src, o, "runtime/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 133 } 134 if chk >= 0 { sys_close(chk) } 135 // 2nd fallback: nxasm/<name>.nx -- so the TOOLCHAIN itself (assembler/linker) is built 136 // through THIS runner's retry guard, never direct 1-shot (2026-06-09 lesson: a direct 137 // unguarded nxasm rebuild handed back a nondeterministic miscompile that exited pre-pass0). 138 let chk2: i64 = sys_openat_rd(src) 139 if chk2 < 0 { 140 o = 0 141 o = sbr_cat(src, o, "nxasm/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 142 } 143 if chk2 >= 0 { sys_close(chk2) } 144 // 3rd fallback: runtime/wiki/<name>.nx -- the internal wiki engine (nx_wiki_main) and 145 // its render organs live here, NOT in runtime/ or _hdl_build/. Mirrors the fallbacks 146 // above exactly (reopen src; if still missing, rewrite to the next candidate path). 147 // Added 2026-06-15 (a wiki organ otherwise mis-compiled on a bogus nxasm/<name>.nx path 148 // and surfaced as a misleading "COMPILE-FAIL (empty .s)"). 149 let chk3: i64 = sys_openat_rd(src) 150 if chk3 < 0 { 151 o = 0 152 o = sbr_cat(src, o, "runtime/wiki/" as *u8); o = sbr_cat(src, o, name); o = sbr_cat(src, o, ".nx" as *u8); src[o] = 0 as u8 153 } 154 if chk3 >= 0 { sys_close(chk3) } 155 // FINAL existence verdict: if NONE of the candidate paths resolved to a real file, fail 156 // LOUD with SOURCE-NOT-FOUND rather than running the compiler on a non-existent path 157 // (which produces an empty .s and the misleading "COMPILE-FAIL" above). Fail-fast at the 158 // boundary (Cardinal 12 + Cardinal 20: a build that can't find its source must say so). 159 let chkf: i64 = sys_openat_rd(src) 160 if chkf < 0 { 161 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 162 sbr_puts(": SOURCE-NOT-FOUND (probed runtime/_hdl_build/, runtime/, nxasm/, runtime/wiki/)\n" as *u8) 163 sys_exit(SBR_USAGE); return SBR_USAGE 164 } 165 sys_close(chkf) 166 // BUILD OUTPUT LIVES IN THE REPO, NOT /tmp (operator 2026-07-27). WSL's /tmp is wiped 167 // between sessions, which silently deleted built ELFs mid-run and made scripts that 168 // referenced them fail in confusing ways (a documented recurring trap). `_build/` is 169 // repo-local and durable; mkdir is idempotent (EEXIST is fine, hence the ignored rc). 170 sys_mkdir("_build\x00" as *u8, 493) 171 // PER-TARGET BUILD LOCK (seq1260, 2026-07-29): two concurrent builds of the SAME target used to 172 // interleave on _build/<name>.{s,sov.elf} -- one overwrote the other's artifact MID-SWEEP and a 173 // BD control curve read garbage (the /api/build shared-capture class at the local tier). Blocking 174 // flock on _build/<name>.lock serializes per target, held through compile->assemble->install and 175 // RELEASED before the run phase (a long-running target must never wedge future rebuilds; an 176 // already-running process keeps its inode across the atomic rename, so rebuild-under-run is safe). 177 // Cross-target builds never contend; process exit releases (crash-safe); the lock file is NEVER 178 // unlinked (the documented lock-file law). 179 let lockpath: *u8 = sys_mmap(256); o = 0 180 o = sbr_cat(lockpath, o, "_build/" as *u8); o = sbr_cat(lockpath, o, name); o = sbr_cat(lockpath, o, ".lock" as *u8); lockpath[o] = 0 as u8 181 let lockfd: i64 = sys_openat_wr(lockpath, 0x1a4) 182 if lockfd >= 0 { sys_flock(lockfd, SYS_LOCK_EX) } 183 let spath: *u8 = sys_mmap(256); o = 0 184 o = sbr_cat(spath, o, "_build/" as *u8); o = sbr_cat(spath, o, name); o = sbr_cat(spath, o, ".s" as *u8); spath[o] = 0 as u8 185 let elfpath: *u8 = sys_mmap(256); o = 0 186 o = sbr_cat(elfpath, o, "_build/" as *u8); o = sbr_cat(elfpath, o, name); o = sbr_cat(elfpath, o, ".sov.elf" as *u8); elfpath[o] = 0 as u8 187 // FAIL-LOUD (seq132, 2026-07-19): remove any STALE _build/<name>.sov.elf from a prior build BEFORE 188 // compiling, so a failed build can never leave the previous working binary behind to be staged as a 189 // false "BUILT" (the masking trap that cost a 3-reship cascade). 190 // 191 // ROOT FIX seq363 (2026-07-30): this used to TRUNCATE the stale artifact to 0 bytes via O_TRUNC and 192 // leave it there. That achieved the anti-staleness goal but replaced one fail-open with a weaker one: 193 // after a COMPILE-FAIL a 0-byte file REMAINED on disk, so `does the elf exist?` -- the check every 194 // naive caller actually writes -- still answered YES for a build that produced nothing. Measured 195 // 2026-07-30: a deliberately-broken target left _build/<t>.sov.elf at 0 bytes, and separately a PIPE 196 // swallows this runner's exit code, so neither the artifact nor `$?` was trustworthy. 197 // 198 // UNLINK instead. The invariant becomes the one callers already assume: THE ARTIFACT EXISTS IF AND 199 // ONLY IF THE BUILD SUCCEEDED. Downstream ELF-magic/size guards (nx_hostctl cmd_buildrun, /api/promote, 200 // md_tc_elf_size) all still hold -- this makes them belt-and-suspenders rather than load-bearing. 201 // Success path is unchanged: the atomic rename below creates the file fresh. 202 sys_unlinkat(elfpath) 203 204 // SOVEREIGN compiler (self-hosted), NOT nx_cc_known_good (= the C 205 // bootstrap, FORBIDDEN on the build path per operator law -- C is 206 // for benchmarking only). Fixed 2026-06-09 during C2-deploy. 207 let compiler: *u8 = "_offc/nx_cc_sovereign.elf" as *u8 208 // the CURRENT (post-cl-shift-fix, 2026-06-09) sovereign assembler. The older _offc/nxasm_x86.elf 209 // (2026-05-30) systemically mis-encodes registers->r15 and segfaults even ret-42 -- do NOT use it. 210 let asm_tool: *u8 = "_offc/nxasm_x86_main.elf" as *u8 211 212 let envp: *i64 = sys_mmap(8*4) as *i64 213 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0 214 215 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) // mute nx_cc's symbol-table dump 216 217 // ---- BUILD ADMISSION (2026-08-01): consult nx_build_admit BEFORE forking the compiler. 218 // MEASURED THE NIGHT THIS LANDED: load average 132.58, 334MB available of 36GB, four concurrent 219 // compiles -- builds were being OOM-killed mid-flight for every seat sharing this host, and two of 220 // mine died that way. nx_build_admit has existed since 2026-07-30 and NOTHING called it: its own 221 // header names ma_do_build as the filed rung, and THIS runner -- the path agents actually invoke -- 222 // was never wired either. A COMPILE IS A HEAVY LAUNCH. 223 // Refuse LOUDLY rather than pile on and be killed. That is the memfloor law already on the board: 224 // REFUSE, NEVER SILENTLY SHRINK. A build that never starts is recoverable; a wedged host is not. 225 // 0 GRANT | 3 DENY-LOAD (below the memory floor) | 4 QUEUE (above the load ceiling) 226 // 5 CANNOT-MEASURE (unreadable /proc -> fail-CLOSED by construction, never wave through) 227 // FAIL-OPEN ON ABSENCE ONLY: a missing admitter makes execve return 127 and we proceed, so a tree 228 // without it still builds -- but any verdict it DOES return is obeyed. Absence is not permission. 229 let sbr_admit: *u8 = "_build/nx_build_admit.sov.elf" as *u8 230 // THRESHOLDS ARE EXPLICIT, AND ONLY THE CAUSAL ONE IS ARMED (2026-08-01). 231 // MEMORY FLOOR 512MB IS ARMED: it is the DIRECTLY MEASURED cause -- builds were OOM-killed 232 // tonight at 334MB available, which is below this floor, so this check would have refused 233 // exactly the builds that died instead of letting them pile on and be killed. 234 // LOAD CEILING IS DELIBERATELY DISABLED (1000000 centiload = load 10000, unreachable) because 235 // it is UNCALIBRATED for this host: the admitter default is 400 (load 4.00) while this Synology 236 // IDLES at 17.62/24.80/36.19 and only wedged at 132.58. Arming it at the default would refuse 237 // every build for every seat -- including this runner own rebuild, a self-bricking guard. 238 // Picking a number that merely produces the outcome I want would be FITTING, NOT MODELLING. 239 // TO ARM IT: sample /proc/loadavg over a representative window, set the ceiling above the normal 240 // distribution and below the wedge point, and record the measurement that justifies the number. 241 let sbr_aargv: *i64 = sys_mmap(8*6) as *i64 242 sbr_aargv[0] = sbr_admit as i64 243 sbr_aargv[1] = "check" as *u8 as i64 244 // 1024 NOT 512: this MATCHES ma_do_build / md_exec_build_admit, which passes check 1024 100000. 245 // Two call sites carrying two different floors is the rule-11 defect in miniature -- one 246 // threshold, one source. mgmt is the REFERENCE because its value was chosen after the 247 // 2026-07-20 host-wedge incident. If this number ever moves, move BOTH. 248 sbr_aargv[2] = "1024" as *u8 as i64 249 sbr_aargv[3] = "1000000" as *u8 as i64 250 sbr_aargv[4] = 0 251 let sbr_arc: i64 = sbr_run(sbr_admit, sbr_aargv, envp, 0 - 1, 0 - 1) 252 // EXIT IS SBR_ADMIT_REFUSED(6) FOR ALL THREE DENIALS (restored in the 2026-08-03 fork merge -- 253 // the NAS branch had regressed to returning the admitter's raw 3/4/5, which COLLIDE with 254 // COMPILE_FAIL(3)/ASM_FAIL(4): a wait-and-retry signal decoding as a broken build). The REASON 255 // stays in the message; the code says only "admission refused, not a build failure". 256 if sbr_arc == 3 { 257 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=3 DENY-MEM -- available memory is BELOW THE FLOOR. Compiling now risks wedging the HOST, not just this build. Wait for headroom and retry.\n" as *u8) 258 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 259 } 260 if sbr_arc == 4 { 261 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=4 QUEUE -- 1-minute load is ABOVE THE CEILING. Hand this to nx_orchestrate wait-for-opening and come back.\n" as *u8) 262 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 263 } 264 if sbr_arc == 5 { 265 sbr_puts("[nx_sov_build_run] REFUSED-BUILD-ADMIT rc=5 CANNOT-MEASURE -- /proc unreadable, so admission FAILS CLOSED by construction. A guard that cannot measure must refuse, never wave through.\n" as *u8) 266 sys_exit(SBR_ADMIT_REFUSED); return SBR_ADMIT_REFUSED 267 } 268 if sbr_arc != 0 { 269 sbr_puts("[nx_sov_build_run] BUILD ADMISSION DID NOT RUN (rc=" as *u8); sbr_putn(sbr_arc) 270 sbr_puts(", expected _build/nx_build_admit.sov.elf). PROCEEDING UNGUARDED -- this build is not protected against wedging the host. Build nx_build_admit to close this.\n" as *u8) 271 } 272 273 // ---- TREE-CANON ADMISSION (2026-08-03, debt 1785622769): a build must never compile a copy 274 // that diverges from canon. ../knowledge/tree_canon.conf (cwd is buildroot; the conf lives at 275 // the nishihost root) lists tree-root-relative paths that MUST be byte-identical across every 276 // tree that can build them. Row grammar: '#' comment, blank ignored, '!path' = FREEZE EVERY 277 // BUILD while divergent (shared infrastructure -- a fork there miscompiles everything 278 // downstream), 'path' = refuse only the build OF THAT FILE (a lane's own divergence must not 279 // block unrelated targets). The other tree's sizes come from the last pushed authoring 280 // manifest (../knowledge/status/treecanon_laptop.mf, produced by nx_treediff on that tree). 281 // SIZE IS A SCREEN, NOT PROOF OF IDENTITY (nx_treediff's own declared bound) -- equal-size 282 // rewrites pass here; nx_treecanon_gate stays the tree-level instrument. 283 // ⚠the manifest is AS FRESH AS ITS LAST PUSH: a stale one can false-pass a scoped row. The 284 // authoring loop owns pushing it after edits; this check is the floor, not the ceiling. 285 // FAIL DIRECTION SPLIT, same design as build admission above: fixtures ABSENT -> PROCEED LOUD 286 // (a tree without them still builds; a silent unwired guard is how guards die); fixtures 287 // PRESENT and a governed row diverges -> REFUSE, fail CLOSED, exit SBR_CANON_REFUSED(7). 288 let cn_conf_l: *i64 = sys_mmap(16) as *i64 289 let cn_conf: *u8 = sys_read_file("../knowledge/tree_canon.conf" as *u8, cn_conf_l) 290 let cn_mf_l: *i64 = sys_mmap(16) as *i64 291 let cn_mf: *u8 = sys_read_file("../knowledge/status/treecanon_laptop.mf" as *u8, cn_mf_l) 292 if (cn_conf as i64) == 0 { 293 sbr_puts("[nx_sov_build_run] TREE-CANON NOT CHECKED: ../knowledge/tree_canon.conf absent. PROCEEDING UNGUARDED against cross-tree divergence.\n" as *u8) 294 } 295 if (cn_conf as i64) != 0 { if (cn_mf as i64) == 0 { 296 sbr_puts("[nx_sov_build_run] TREE-CANON NOT CHECKED: ../knowledge/status/treecanon_laptop.mf absent -- generate with nx_treediff on the authoring tree and push it. PROCEEDING UNGUARDED.\n" as *u8) 297 } } 298 if (cn_conf as i64) != 0 { if (cn_mf as i64) != 0 { 299 // the canon rows are relative to the runtime root; src is "runtime/<rel>" for every probe 300 // path except the nxasm/ fallback (toolchain sources are not canon-governed rows today) 301 var cn_isrt: i64 = 1 302 let cn_rt: *u8 = "runtime/" as *u8 303 var cn_i: i64 = 0 304 while cn_i < 8 { if src[cn_i] != cn_rt[cn_i] { cn_isrt = 0 } cn_i = cn_i + 1 } 305 var cn_srclen: i64 = 0 306 while src[cn_srclen] != (0 as u8) { cn_srclen = cn_srclen + 1 } 307 let cn_path: *u8 = sys_mmap(512) 308 let cn_cl: i64 = cn_conf_l[0] 309 let cn_ml: i64 = cn_mf_l[0] 310 var cn_p: i64 = 0 311 while cn_p < cn_cl { 312 var cn_e: i64 = cn_p 313 var cn_go: i64 = 1 314 while cn_go == 1 { 315 if cn_e >= cn_cl { cn_go = 0 } 316 if cn_go == 1 { if cn_conf[cn_e] == (10 as u8) { cn_go = 0 } } 317 if cn_go == 1 { cn_e = cn_e + 1 } 318 } 319 var cn_len: i64 = cn_e - cn_p 320 if cn_len > 0 { if cn_conf[cn_p + cn_len - 1] == (13 as u8) { cn_len = cn_len - 1 } } // CRLF-tolerant 321 var cn_frozen: i64 = 0 322 var cn_ps: i64 = cn_p 323 if cn_len > 0 { if cn_conf[cn_ps] == (33 as u8) { cn_frozen = 1; cn_ps = cn_ps + 1; cn_len = cn_len - 1 } } 324 var cn_live: i64 = 0 325 if cn_len > 0 { if cn_conf[cn_ps] != (35 as u8) { cn_live = 1 } } 326 if cn_live == 1 { 327 // relevant = frozen row, OR the row IS the file this build compiles 328 var cn_rel: i64 = cn_frozen 329 if cn_isrt == 1 { if sbr_ceq_at(src, 8, cn_srclen - 8, cn_conf, cn_ps, cn_len) == 1 { cn_rel = 1 } } 330 if cn_rel == 1 { 331 var cq: i64 = 0 332 cq = sbr_cat(cn_path, cq, "runtime/" as *u8) 333 var ck: i64 = 0 334 while ck < cn_len { cn_path[cq] = cn_conf[cn_ps + ck]; cq = cq + 1; ck = ck + 1 } 335 cn_path[cq] = 0 as u8 336 let cn_nas: i64 = sbr_filesize(cn_path) 337 // find the row in the authoring manifest: lines are "<bytes> <relpath>" 338 var cn_found: i64 = 0 339 var cn_lap: i64 = 0 - 1 340 var mp: i64 = 0 341 while mp < cn_ml { 342 var me: i64 = mp 343 var mg: i64 = 1 344 while mg == 1 { 345 if me >= cn_ml { mg = 0 } 346 if mg == 1 { if cn_mf[me] == (10 as u8) { mg = 0 } } 347 if mg == 1 { me = me + 1 } 348 } 349 var ml2: i64 = me - mp 350 if ml2 > 0 { if cn_mf[mp + ml2 - 1] == (13 as u8) { ml2 = ml2 - 1 } } 351 // split at the first space 352 var ms: i64 = 0 - 1 353 var mi: i64 = 0 354 while mi < ml2 { if ms < 0 { if cn_mf[mp + mi] == (32 as u8) { ms = mi } } mi = mi + 1 } 355 if ms > 0 { 356 if sbr_ceq_at(cn_mf, mp + ms + 1, ml2 - ms - 1, cn_conf, cn_ps, cn_len) == 1 { 357 cn_found = 1 358 var mv: i64 = 0 359 var md: i64 = 0 360 while md < ms { mv = mv * 10 + ((cn_mf[mp + md] as i64) - 48); md = md + 1 } 361 cn_lap = mv 362 } 363 } 364 mp = me + 1 365 } 366 var cn_div: i64 = 0 367 if cn_found == 0 { cn_div = 1 } 368 if cn_found == 1 { if cn_lap != cn_nas { cn_div = 1 } } 369 if cn_nas == 0 { cn_div = 1 } 370 if cn_div == 1 { 371 sbr_puts("[nx_sov_build_run] REFUSED-TREE-CANON rc=7: canon path " as *u8) 372 sbr_puts(cn_path) 373 sbr_puts(" DIVERGES across trees (this tree=" as *u8); sbr_putn(cn_nas) 374 sbr_puts("B, authoring manifest=" as *u8); sbr_putn(cn_lap) 375 sbr_puts("B; -1 = absent). Building now would compile a FORKED copy -- the exact class that applied the unfreed-mmap fix twice. Remedy: body-diff and converge the file in BOTH trees (merge per file, never wholesale), regenerate + push knowledge/status/treecanon_laptop.mf, then rebuild.\n" as *u8) 376 sys_exit(SBR_CANON_REFUSED); return SBR_CANON_REFUSED 377 } 378 } 379 } 380 cn_p = cn_e + 1 381 } 382 } } 383 384 // ---- compile+ASSEMBLE with recompile-retry until the .s ASSEMBLES CLEANLY (compiler nondeterminism guard). 385 // ROOT FIX 2026-06-25: the old guard stopped at the first NON-EMPTY .s -- but nx_cc's nondeterminism also 386 // TRUNCATES (a non-empty .s missing the trailing `main` -> nxasm rc=102 "UNDEFINED label: main", caught x3 387 // assembling nx_raci_sov / nx_pattern_library). So success now REQUIRES rc_a==0 from nxasm, not merely 388 // non-empty bytes -> truncation-nondeterminism becomes reliability. The assemble lands in tmpelf (atomic 389 // rename after the loop). If a real DETERMINISTIC miscompile, the retries exhaust + we LOUD-fail saying so. ---- 390 let tmpelf: *u8 = sys_mmap(256); o = 0 391 o = sbr_cat(tmpelf, o, "_build/_sbr_asm_out." as *u8); o = sbr_cat(tmpelf, o, name); tmpelf[o] = 0 as u8 392 var asmbytes: i64 = 0 393 var rc_a: i64 = 0 - 1 394 var built: i64 = 0 395 var tries: i64 = 0 396 while tries < SBR_MAX_RETRIES { 397 let sfd: i64 = sys_openat_wr(spath, 0x1a4) 398 let cc: *i64 = sys_mmap(8*4) as *i64 399 cc[0] = compiler as i64; cc[1] = src as i64; cc[2] = 0 400 let rc_c: i64 = sbr_run(compiler, cc, envp, sfd, devnull) 401 sys_close(sfd) 402 asmbytes = sbr_filesize(spath) 403 if rc_c == 0 { if asmbytes > SBR_MIN_ASM_BYTES { 404 let aa2: *i64 = sys_mmap(8*4) as *i64 405 aa2[0] = asm_tool as i64; aa2[1] = spath as i64; aa2[2] = tmpelf as i64; aa2[3] = 0 406 rc_a = sbr_run(asm_tool, aa2, envp, 0 - 1, 0 - 1) 407 if rc_a == 0 { built = 1; tries = SBR_MAX_RETRIES } 408 } } 409 if tries != SBR_MAX_RETRIES { tries = tries + 1 } 410 } 411 if built == 0 { 412 if asmbytes <= SBR_MIN_ASM_BYTES { 413 // ROOT-CAUSE SURFACE (2026-07-06): the retry loop mutes nx_cc's stderr, so a plain source error 414 // (undefined function / parse / arg-count) hid behind this opaque "empty .s" (cost a ~15-build blind 415 // hunt). Re-run the compiler ONCE with stderr VISIBLE so the REAL diagnostic always prints. Same 416 // one-look answer as _offc/nx_ccdiag.elf, now automatic on every failure. 417 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": COMPILE-FAIL (empty .s) -- nx_cc says:\n---- nx_cc stderr ----\n" as *u8) 418 let dcc: *i64 = sys_mmap(8*4) as *i64 419 dcc[0] = compiler as i64; dcc[1] = src as i64; dcc[2] = 0 420 sbr_run(compiler, dcc, envp, devnull, 0 - 1) // stdout->devnull, stderr VISIBLE = the real cause 421 sbr_puts("----------------------\n(usually an undefined fn / missing import / parse error -- NOT a codegen crash; fix the source above)\n" as *u8) 422 sys_exit(SBR_COMPILE_FAIL); return SBR_COMPILE_FAIL 423 } 424 if rc_a > 128 { 425 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": NXASM-CRASHED sig=" as *u8); sbr_putn(rc_a - 128); sbr_puts(" (assembler died on this .s -> capacity/robustness bug)\n" as *u8) 426 sys_exit(SBR_ASM_FAIL); return SBR_ASM_FAIL 427 } 428 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": NXASM-FAIL rc=" as *u8); sbr_putn(rc_a) 429 sbr_puts(" after recompile-retries -- DETERMINISTIC (not the nondeterministic-truncation class). rc=102 = nxasm 'UNDEFINED label: main' = USUALLY a no-main LIBRARY built standalone -> run its _gate/importer (the one with main), NOT the lib. Else a real deterministic compiler/source miscompile.\n" as *u8) 430 sys_exit(SBR_ASM_FAIL); return SBR_ASM_FAIL 431 } 432 433 // CORRECTED 2026-06-25: the old comment here claimed the rc=6/102 "ctx-x-output-path" class was 434 // "non-reproducible / nxasm outpath codegen CLEAN". That was WRONG -- the real rc=102 is nxasm's pass-2 435 // "UNDEFINED label: main" (axc_label_resolve), i.e. nx_cc emitted a TRUNCATED .s missing `main`. It is the 436 // COMPILER truncation-nondeterminism, not the output path. The retry loop above now requires a clean assemble, 437 // so a truncated .s no longer escapes. tmpelf holds the good ELF -> atomic rename install (temp+rename kept 438 // for atomicity = never overwrite a running binary mid-write). 439 sys_renameat(tmpelf, elfpath) 440 441 // ---- LM-026 ROOT FIX: refresh the INSTALLED artifact if one exists ----------------------------- 442 // Historical trap (cost a full session on the MMU rung): this runner builds to /tmp/<name>.sov.elf, 443 // but gates/organs fork _offc/<name>.elf -- so a rebuild left _offc STALE and source edits silently 444 // never took effect. Now, on a SUCCESSFUL build, if _offc/<name>.elf ALREADY EXISTS (i.e. <name> is 445 // an installed artifact that something forks), atomically refresh it from the fresh build. This is 446 // refresh-IF-PRESENT only: throwaway gate/probe builds (run straight from /tmp, no _offc twin) never 447 // get a spurious _offc entry. Atomic (write .sbrtmp + renameat) = no torn binary, retires LM-026 at 448 // the source. The reactive nx_offc_install guardrail stays as the belt-and-suspenders detector. 449 let offcpath: *u8 = sys_mmap(256); o = 0 450 o = sbr_cat(offcpath, o, "_offc/" as *u8); o = sbr_cat(offcpath, o, name); o = sbr_cat(offcpath, o, ".elf" as *u8); offcpath[o] = 0 as u8 451 let oex: i64 = sys_openat_rd(offcpath) 452 var do_install: i64 = 0 453 if oex >= 0 { sys_close(oex); do_install = 1 } // refresh-IF-PRESENT (LM-026) 454 if force_install == 1 { do_install = 1 } // --install: install even with NO twin (make a tool permanent) 455 // ⚠⚠TOOLCHAIN IS NEVER AUTO-INSTALLED (rule 26, added 2026-07-30 after seq1464; RESTORED in the 456 // 2026-08-03 fork merge -- the NAS branch had dropped this entire guard, so any successful build 457 // of a toolchain binary silently replaced the deployed one, canary-less, even under --build-only). 458 // MEASURED INCIDENT: buildroot/_offc/nx_cc_sovereign.elf was found LIVE at 520103 bytes -- the 459 // output of a rebuild made while buildroot was stale -- instead of the 543126 that had been 460 // deliberately promoted. The hub was therefore compiling EVERY organ with a compiler whose 461 // optimiser was 11712 source-bytes short, and its .s came out 16430B larger from identical 462 // sources. Nobody chose that; a build installed it. 463 // 464 // THE INVARIANT: refresh-IF-PRESENT is right for ordinary organs (it retires LM-026 staleness) and 465 // CATASTROPHIC for the toolchain, because the toolchain is what BUILT the thing being installed -- 466 // a bad build then replaces the compiler that produced it and every subsequent build inherits the 467 // damage, with no promote, no canary and no .prev. That is a self-modifying build path. 468 // The toolchain has exactly ONE lawful update route: /api/promote_toolchain, which ELF-validates, 469 // banks .prev, chmod +x, CANARY-COMPILES AND RUNS, and auto-rolls-back. This guard makes that route 470 // the only one BY CONSTRUCTION rather than by convention. 471 // ★Applies to --install too: an explicit flag must not be able to brick the toolchain either. 472 if sbr_is_toolchain(name) == 1 { 473 if do_install == 1 { 474 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name) 475 sbr_puts(": REFUSED to auto-install a TOOLCHAIN binary into _offc (rule 26 / seq1464).\n" as *u8) 476 sbr_puts(" The build artifact is staged and usable; promote it deliberately via\n" as *u8) 477 sbr_puts(" POST /api/promote_toolchain (ELF-validated, .prev-banked, canary-run, auto-rollback).\n" as *u8) 478 } 479 do_install = 0 480 } 481 if do_install == 1 { 482 let il: *i64 = sys_mmap(16) as *i64 483 let ib: *u8 = sys_read_file(elfpath, il) 484 if (ib as i64) != 0 { 485 let otmp: *u8 = sys_mmap(256); var oo: i64 = 0 486 oo = sbr_cat(otmp, oo, "_offc/" as *u8); oo = sbr_cat(otmp, oo, name); oo = sbr_cat(otmp, oo, ".elf.sbrtmp" as *u8); otmp[oo] = 0 as u8 487 let ofd: i64 = sys_openat_wr(otmp, 493) // 0755 = executable (works for refresh AND force-install-new) 488 if ofd >= 0 { sys_write(ofd, ib, il[0]); sys_close(ofd); sys_renameat(otmp, offcpath) } 489 if force_install == 1 { sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": INSTALLED -> _offc/" as *u8); sbr_puts(name); sbr_puts(".elf (--install)\n" as *u8) } 490 } 491 } 492 493 // release the per-target build lock HERE -- artifacts are final (atomic renames done); the run 494 // phase below must not hold it (a daemon target would wedge every future rebuild of itself). 495 if lockfd >= 0 { sys_flock(lockfd, SYS_LOCK_UN); sys_close(lockfd) } 496 497 // ---- build-only mode: argv[2] starting "--b" (--build-only) SKIPS the run, so a sovereign 498 // bring-up/supervisor can build a dependency without triggering its side effects (e.g. a worker 499 // would otherwise start a live download). The build path above is byte-for-byte unchanged; 500 // this only short-circuits before the run. Default (no 2nd arg) behaves exactly as before. ---- 501 // --build-only OR --install both SKIP the run (skip_run was set from argv[2] at the top). 502 if skip_run == 1 { 503 // ARTIFACT PATH IS PART OF THE OUTPUT CONTRACT (2026-07-30). --build-only used to print only 504 // "asm=NNNNB", so the caller was told the build SUCCEEDED but not WHERE the ELF landed -- and the 505 // header comments still said /tmp/<name>.sov.elf while the code has written _build/<name>.sov.elf 506 // since the flock change. Every caller therefore had to go hunting (measured: 3 shell round-trips 507 // to locate one artifact). A tool that produces a file MUST say where it put it; the run path 508 // below already prints elf=<path>, so build-only was the odd one out. Same line shape as the run 509 // branch so existing log scrapers see a superset, never a changed field (rule 19). 510 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": asm=" as *u8); sbr_putn(asmbytes) 511 sbr_puts("B SOVEREIGN build (nx_cc->nxasm_x86, no gcc, no run) elf=" as *u8); sbr_puts(elfpath) 512 sbr_puts("\n" as *u8) 513 sys_exit(SBR_OK); return SBR_OK 514 } 515 516 // ---- run the sovereign ELF WITH FORWARDED ARGS; report; exit with its code ---- 517 // ARG-FORWARDING (2026-07-14 debt-eat): argv[fwd_start..argc) pass through to the built program, so a 518 // daemon/tool builds AND launches with its runtime args in one command (e.g. a cap-secret path). Before 519 // this, extra args were silently DROPPED -- caught live when the self-gated git server ran AUTH OFF. 520 let rr: *i64 = sys_mmap(8 * 20) as *i64 521 rr[0] = elfpath as i64 522 var ra: i64 = 1 523 var ai: i64 = fwd_start 524 while ai < argc { if ra < 19 { rr[ra] = argv[ai]; ra = ra + 1 } ai = ai + 1 } 525 rr[ra] = 0 526 let rc_r: i64 = sbr_run(elfpath, rr, envp, 0 - 1, 0 - 1) 527 sbr_puts("[nx_sov_build_run] " as *u8); sbr_puts(name); sbr_puts(": asm=" as *u8); sbr_putn(asmbytes) 528 sbr_puts("B SOVEREIGN(nx_cc->nxasm_x86, no gcc) elf=" as *u8); sbr_puts(elfpath) 529 sbr_puts(" run-exit=" as *u8); sbr_putn(rc_r); sbr_puts("\n" as *u8) 530 sys_exit(rc_r) 531 return rc_r 532}