diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff833f..6756b55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,14 +171,15 @@ jobs: run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ - gcc make binutils libssl-dev python3 gdb + gcc make binutils libssl-dev python3 gdb \ + gcc-aarch64-linux-gnu - name: Download buxc uses: actions/download-artifact@v4 with: name: buxc-linux - name: Prepare buxc run: chmod +x buxc && ./buxc --version - - name: errors + stdlib + registry + dwarf + - name: errors + stdlib + registry + dwarf + linux-targets env: BUX_SKIP_BUILD: "1" run: | @@ -188,6 +189,7 @@ jobs: make test-registry BUX_SKIP_BUILD=1 make test-dwarf BUX_SKIP_BUILD=1 make test-drop-move BUX_SKIP_BUILD=1 + make test-linux-targets BUX_SKIP_BUILD=1 apps: name: apps @@ -199,19 +201,22 @@ jobs: - name: Install build deps run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc make libssl-dev + sudo apt-get install -y --no-install-recommends \ + gcc make libssl-dev openssl curl - name: Download buxc uses: actions/download-artifact@v4 with: name: buxc-linux - name: Prepare buxc run: chmod +x buxc && ./buxc --version - - name: test-apps + - name: test-apps + nexus TLS/mTLS env: BUX_SKIP_BUILD: "1" run: | unset BUX_DEBUG_FILE || true make test-apps BUX_SKIP_BUILD=1 + make test-nexus-tls BUX_SKIP_BUILD=1 + make test-nexus-mtls BUX_SKIP_BUILD=1 selfhost: name: selfhost smoke @@ -223,7 +228,8 @@ jobs: - name: Install build deps run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc make libssl-dev + sudo apt-get install -y --no-install-recommends \ + gcc make libssl-dev openssl curl - name: Download buxc uses: actions/download-artifact@v4 with: @@ -237,6 +243,13 @@ jobs: unset BUX_DEBUG_FILE || true unset BUX_SELFHOST_FIXED_POINT || true make test-selfhost-smoke BUX_SKIP_BUILD=1 + - name: selfhost install + registry + env: + BUX_SKIP_BUILD: "1" + run: | + unset BUX_DEBUG_FILE || true + make test-selfhost-install BUX_SKIP_BUILD=1 + make test-selfhost-registry BUX_SKIP_BUILD=1 - name: Upload selfhost artifacts on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/Makefile b/Makefile index d3bbb7f..35ca58a 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,12 @@ BUILD_DIR := build # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). NIMFLAGS ?= --nimcache:nimcache -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field move_field_partial move_field_remaining move_field_nested move_field_ptr c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe ctfe_crc async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt # Platform smoke (macOS CI): full EXAMPLES still runs on Linux. -EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat +EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt ctfe_crc -.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit ensure-buxc +.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit test-linux-targets ensure-buxc all: build @@ -37,7 +37,7 @@ debug: dev @echo "Debug binary: buxc_debug" # Full local / sequential suite (same coverage as split CI jobs combined). -test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-apps test-selfhost-smoke test-unit +test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-linux-targets test-apps test-selfhost-smoke test-unit # Nim unit tests + tiny CLI smoke (needs Nim + buxc). test-unit: ensure-buxc @@ -265,6 +265,45 @@ test-dwarf: ensure-buxc @chmod +x tools/smoke_dwarf.sh @tools/smoke_dwarf.sh +# Session 75 — Linux / cloud / embedded: minimal runtime, static, aarch64 cross, CTFE CRC +.PHONY: test-linux-targets +test-linux-targets: ensure-buxc + @echo "=== Linux targets smoke (minimal / static / cross) ===" + @chmod +x tools/smoke_linux_targets.sh + @tools/smoke_linux_targets.sh + +# Session 78 — Nexus HTTPS (self-signed) smoke +.PHONY: test-nexus-tls +test-nexus-tls: ensure-buxc + @echo "=== Nexus TLS smoke ===" + @chmod +x tools/smoke_nexus_tls.sh + @tools/smoke_nexus_tls.sh + +# Session 79 — musl static (SKIP if no musl-gcc/zig) +.PHONY: test-musl-static +test-musl-static: ensure-buxc + @echo "=== musl static smoke ===" + @chmod +x tools/smoke_musl_static.sh + @tools/smoke_musl_static.sh + +# Session 80 — selfhost install --locked + Nexus mTLS +.PHONY: test-selfhost-install test-nexus-mtls test-selfhost-registry +test-selfhost-install: ensure-buxc selfhost + @echo "=== Selfhost install --locked ===" + @chmod +x tools/smoke_selfhost_install.sh + @tools/smoke_selfhost_install.sh + +test-nexus-mtls: ensure-buxc + @echo "=== Nexus mTLS smoke ===" + @chmod +x tools/smoke_nexus_mtls.sh + @tools/smoke_nexus_mtls.sh + +# Session 81 — selfhost full registry (search / add / HTTP) +test-selfhost-registry: ensure-buxc selfhost + @echo "=== Selfhost registry ===" + @chmod +x tools/smoke_selfhost_registry.sh + @tools/smoke_selfhost_registry.sh + # Drop / field-move goldens (whole + partial field move; early-return counts) .PHONY: test-drop-move test-drop-move: ensure-buxc diff --git a/apps/nexus/README.md b/apps/nexus/README.md index 954e640..e162c22 100644 --- a/apps/nexus/README.md +++ b/apps/nexus/README.md @@ -16,7 +16,11 @@ Nexus is a from-scratch web server that demonstrates Bux's systems-programming c | **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction | | **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection | | **JSON API** | Built-in `/api/health` and `/api/info` endpoints | -| **Logging** | Per-request structured logging (method, path, status code) | +| **Logging** | Access log: `METHOD path status duration_ms` (`NEXUS_ACCESS_LOG=0` to disable) | +| **Limits** | `NEXUS_MAX_BODY` (default 1 MiB) → HTTP 413 when exceeded | +| **Graceful stop** | SIGINT/SIGTERM: close listen fd, poison workers, exit 0 | +| **TLS / HTTPS** | OpenSSL server mode via `NEXUS_TLS=1` + PEM cert/key | +| **mTLS** | `NEXUS_TLS_CLIENT_CA` PEM → require client certificates | ## Quick Start @@ -34,10 +38,32 @@ cd apps/nexus ./nexus # Optional env (also used by `make bench-nexus`) -# NEXUS_PORT=18080 NEXUS_BIND=127.0.0.1 NEXUS_WORKERS=4 ./build/nexus +# NEXUS_PORT=18080 NEXUS_BIND=127.0.0.1 NEXUS_WORKERS=4 \ +# NEXUS_MAX_BODY=1048576 NEXUS_ACCESS_LOG=1 ./build/nexus + +# HTTPS (self-signed example) +# openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj /CN=localhost +# NEXUS_TLS=1 NEXUS_TLS_CERT=cert.pem NEXUS_TLS_KEY=key.pem NEXUS_PORT=8443 ./build/nexus +# curl -k https://127.0.0.1:8443/api/health + +# mTLS (require client cert signed by CA) +# NEXUS_TLS_CLIENT_CA=ca.pem NEXUS_TLS=1 NEXUS_TLS_CERT=server.pem NEXUS_TLS_KEY=server.key … +# curl --cert client.pem --key client.key --cacert ca.pem https://… ``` -Server starts on `http://0.0.0.0:8080` (override with `NEXUS_PORT` / `NEXUS_BIND`): +Server starts on `http://0.0.0.0:8080` (or `https://` when TLS is enabled). +Stop with **Ctrl+C** or `kill -TERM` (graceful: workers drained via poison pills). + +Smoke: `make test-nexus-tls` (self-signed cert + curl -k). + +### Docker + +```bash +# Full Nexus (needs libssl3) +../../buxc --release build # from apps/nexus +docker build -f ../../examples/docker/Dockerfile.nexus -t bux-nexus ../.. +docker run --rm -p 8080:8080 bux-nexus +``` ``` ╔══════════════════════════════════════════════╗ diff --git a/apps/nexus/src/Config.bux b/apps/nexus/src/Config.bux index 1421cba..e04a73b 100644 --- a/apps/nexus/src/Config.bux +++ b/apps/nexus/src/Config.bux @@ -6,6 +6,16 @@ module Config { workerCount: int; publicDir: String; backlog: int; + /// Max raw request bytes (recv buffer / body safety). Default 1 MiB. + maxBodyBytes: int; + /// Access log to stdout (method path status ms). Default true. + accessLog: bool; + /// Enable HTTPS (OpenSSL). Requires tlsCertPath + tlsKeyPath. + tlsEnabled: bool; + tlsCertPath: String; + tlsKeyPath: String; + /// Optional client CA PEM → mTLS (require client certificate). + tlsClientCaPath: String; } pub const func DefaultConfig() -> ServerConfig { @@ -15,6 +25,12 @@ module Config { workerCount: 4, publicDir: "public", backlog: 128, + maxBodyBytes: 1048576, + accessLog: true, + tlsEnabled: false, + tlsCertPath: "", + tlsKeyPath: "", + tlsClientCaPath: "", }; } diff --git a/apps/nexus/src/Handlers.bux b/apps/nexus/src/Handlers.bux index 76e5fa9..791e4af 100644 --- a/apps/nexus/src/Handlers.bux +++ b/apps/nexus/src/Handlers.bux @@ -63,12 +63,12 @@ module Handlers { pub func HandleApiHealth() -> HttpResponse { return Http_NewResponse(200, "application/json; charset=utf-8", - "{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.3.0\"}"); + "{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.6.0\"}"); } pub func HandleApiInfo() -> HttpResponse { return Http_NewResponse(200, "application/json; charset=utf-8", - "{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"keep-alive\",\"thread-pool\",\"algebraic-enums\"]}"); + "{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"TLS\",\"mTLS\",\"keep-alive\",\"thread-pool\",\"graceful-stop\",\"access-log\",\"max-body\"]}"); } pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse { diff --git a/apps/nexus/src/Main.bux b/apps/nexus/src/Main.bux index e3d7cb7..9ecf1ca 100644 --- a/apps/nexus/src/Main.bux +++ b/apps/nexus/src/Main.bux @@ -6,7 +6,7 @@ module Main { import Server::{RunServer}; import Std::Array::{Array, Array_New, Array_Push}; import Std::Os::{Os_GetEnv}; - import Std::String::{String_Len, String_ToInt}; + import Std::String::{String_Len, String_ToInt, String_Eq}; func BuildRouter() -> Router { var routes: Array = Array_New(8); @@ -42,7 +42,10 @@ module Main { } /// Apply optional env overrides for benches / ops: - /// NEXUS_PORT, NEXUS_WORKERS, NEXUS_BIND, NEXUS_PUBLIC + /// NEXUS_PORT, NEXUS_WORKERS, NEXUS_BIND, NEXUS_PUBLIC, + /// NEXUS_MAX_BODY (bytes), NEXUS_ACCESS_LOG (0/1/false/true), + /// NEXUS_TLS=1 + NEXUS_TLS_CERT + NEXUS_TLS_KEY (PEM paths) + /// NEXUS_TLS_CLIENT_CA (optional PEM → mTLS) func ApplyEnvConfig(config: *ServerConfig) { let portEnv: String = Os_GetEnv("NEXUS_PORT"); if String_Len(portEnv) > 0 { @@ -66,6 +69,43 @@ module Main { if String_Len(pubEnv) > 0 { config.publicDir = pubEnv; } + let bodyEnv: String = Os_GetEnv("NEXUS_MAX_BODY"); + if String_Len(bodyEnv) > 0 { + let b: int64 = String_ToInt(bodyEnv); + if b >= 1024 && b <= 67108864 { + config.maxBodyBytes = b as int; + } + } + let logEnv: String = Os_GetEnv("NEXUS_ACCESS_LOG"); + if String_Len(logEnv) > 0 { + if String_Eq(logEnv, "0") || String_Eq(logEnv, "false") || String_Eq(logEnv, "off") { + config.accessLog = false; + } else { + config.accessLog = true; + } + } + let tlsEnv: String = Os_GetEnv("NEXUS_TLS"); + if String_Len(tlsEnv) > 0 { + if String_Eq(tlsEnv, "1") || String_Eq(tlsEnv, "true") || String_Eq(tlsEnv, "on") || + String_Eq(tlsEnv, "https") { + config.tlsEnabled = true; + } + } + let certEnv: String = Os_GetEnv("NEXUS_TLS_CERT"); + if String_Len(certEnv) > 0 { + config.tlsCertPath = certEnv; + config.tlsEnabled = true; + } + let keyEnv: String = Os_GetEnv("NEXUS_TLS_KEY"); + if String_Len(keyEnv) > 0 { + config.tlsKeyPath = keyEnv; + config.tlsEnabled = true; + } + let caEnv: String = Os_GetEnv("NEXUS_TLS_CLIENT_CA"); + if String_Len(caEnv) > 0 { + config.tlsClientCaPath = caEnv; + config.tlsEnabled = true; + } } func Main() -> int { diff --git a/apps/nexus/src/Server.bux b/apps/nexus/src/Server.bux index 20446fb..b05d973 100644 --- a/apps/nexus/src/Server.bux +++ b/apps/nexus/src/Server.bux @@ -1,12 +1,17 @@ module Server { import Std::Io::{Print, PrintLine, PrintInt}; - import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; - import Std::String::{String_Len, String_StartsWith}; + import Std::Net::{ + Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError, + Tls_ServerCtx, Tls_ServerCtxMtls, Tls_CtxFree, Tls_Accept, Tls_Send, Tls_Recv, Tls_Close, Tls_LastError + }; + import Std::String::{String_Len, String_StartsWith, String_Eq}; import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv}; import Std::Array::{Array_Drop}; + import Std::Os::{Os_InstallStopHandlers, Os_ShouldStop, Os_SetStopListenFd}; + import Std::Time::{Time_NowMs}; import Config::{ServerConfig}; - import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Request_WantsKeepAlive, HeaderEntry}; + import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Http_MethodName, Request_WantsKeepAlive, HeaderEntry}; import Errors::{ParseResult}; import Parser::{ParseRequest}; import Router::{Router, Router_Dispatch}; @@ -21,8 +26,11 @@ module Server { /// Cap requests per TCP connection (safety + fair scheduling). const MAX_KEEPALIVE_REQUESTS: int = 1000; + /// fd < 0 is a poison pill: worker exits cleanly. + /// tls is null for plain HTTP; non-null SSL* for HTTPS (session 78). pub struct ConnectionTask { fd: int; + tls: *void; } pub func BuildResponse(resp: HttpResponse, keepAlive: bool) -> String { @@ -34,7 +42,7 @@ module Server { bux_sb_append(sb, Http_StatusText(resp.statusCode)); bux_sb_append(sb, "\r\n"); - bux_sb_append(sb, "Server: Nexus/0.3.0 (Bux)\r\n"); + bux_sb_append(sb, "Server: Nexus/0.6.0 (Bux)\r\n"); if bux_strlen(resp.extraHeaders) > 0 { bux_sb_append(sb, resp.extraHeaders); @@ -68,42 +76,99 @@ module Server { return result; } - /// Serve one TCP client: zero or more HTTP requests (HTTP/1.1 keep-alive). - pub func HandleConnection(fd: int, router: Router) { + func ConnRecv(fd: int, tls: *void, maxLen: int) -> String { + if tls != null as *void { + return Tls_Recv(tls, maxLen); + } + return Net_Recv(fd, maxLen); + } + + func ConnSend(fd: int, tls: *void, data: String) -> int { + if tls != null as *void { + return Tls_Send(tls, data); + } + return Net_Send(fd, data); + } + + /// Structured access log: method path status duration_ms + func AccessLog(method: String, path: String, status: int, ms: int64) { + Print(method); + Print(" "); + Print(path); + Print(" "); + PrintInt(status); + Print(" "); + PrintInt(ms as int); + PrintLine("ms"); + } + + /// Serve one TCP (or TLS) client: zero or more HTTP/1.1 keep-alive requests. + pub func HandleConnection(fd: int, tls: *void, router: Router, config: ServerConfig) { var reqCount: int = 0; + let maxRecv: int = config.maxBodyBytes; while reqCount < MAX_KEEPALIVE_REQUESTS { - let raw: String = Net_Recv(fd, 8192); + if Os_ShouldStop() { + return; + } + let raw: String = ConnRecv(fd, tls, maxRecv); if String_Len(raw) == 0 { return; } - // HTTP/2 preface detection — one-shot response, then close - if String_StartsWith(raw, "PRI * HTTP/2.0") { - let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", - "HTTP/2 detected — full support planned for future release.\r\n"); - Net_Send(fd, BuildResponse(resp, false)); + if String_Len(raw) as int >= maxRecv { + let resp: HttpResponse = Http_NewResponse(413, "text/plain; charset=utf-8", "Payload Too Large"); + ConnSend(fd, tls, BuildResponse(resp, false)); return; } + if String_StartsWith(raw, "PRI * HTTP/2.0") { + let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", + "HTTP/2 detected — full support planned for future release.\r\n"); + ConnSend(fd, tls, BuildResponse(resp, false)); + return; + } + + let t0: int64 = Time_NowMs(); let parsed: ParseResult = ParseRequest(raw); var keepAlive: bool = false; + var statusOut: int = 400; + var methodName: String = "?"; + var pathOut: String = "-"; + if parsed.tag == ParseResult_Ok { var req: HttpRequest = parsed.data.Ok_0; + methodName = Http_MethodName(req.method); + pathOut = req.path; + if String_Len(req.body) as int > maxRecv { + let resp: HttpResponse = Http_NewResponse(413, "text/plain; charset=utf-8", "Payload Too Large"); + ConnSend(fd, tls, BuildResponse(resp, false)); + Array_Drop(&req.headers); + return; + } keepAlive = Request_WantsKeepAlive(&req); - // Last request on the connection quota must close if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS { keepAlive = false; } let resp: HttpResponse = Router_Dispatch(router, req); - Net_Send(fd, BuildResponse(resp, keepAlive)); - // Free header buffer (moved into req at parse; no Drop on nested fields) + statusOut = resp.statusCode; + ConnSend(fd, tls, BuildResponse(resp, keepAlive)); Array_Drop(&req.headers); } else { let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); - Net_Send(fd, BuildResponse(resp, false)); + statusOut = 400; + ConnSend(fd, tls, BuildResponse(resp, false)); + if config.accessLog { + let dt: int64 = Time_NowMs() - t0; + AccessLog("?", "-", 400, dt); + } return; } + if config.accessLog { + let dt: int64 = Time_NowMs() - t0; + AccessLog(methodName, pathOut, statusOut, dt); + } + reqCount = reqCount + 1; if !keepAlive { return; @@ -114,12 +179,19 @@ module Server { pub struct WorkerCtx { taskQueue: *Channel; router: Router; + config: ServerConfig; } pub func Worker(ctx: *WorkerCtx) { while true { let task: ConnectionTask = Channel_Recv(ctx.taskQueue); - HandleConnection(task.fd, ctx.router); + if task.fd < 0 { + return; + } + HandleConnection(task.fd, task.tls, ctx.router, ctx.config); + if task.tls != null as *void { + Tls_Close(task.tls); + } Net_Close(task.fd); } } @@ -127,29 +199,78 @@ module Server { pub struct AcceptorCtx { serverFd: int; taskQueue: *Channel; + workerCount: int; + tlsCtx: *void; } pub func Acceptor(ctx: *AcceptorCtx) { - while true { + while !Os_ShouldStop() { let fd: int = Net_Accept(ctx.serverFd); if fd >= 0 { - let task: ConnectionTask = ConnectionTask { fd: fd }; + var tls: *void = null as *void; + if ctx.tlsCtx != null as *void { + tls = Tls_Accept(ctx.tlsCtx, fd); + if tls == null as *void { + Print("WARN: TLS handshake failed: "); + PrintLine(Tls_LastError()); + Net_Close(fd); + continue; + } + } + let task: ConnectionTask = ConnectionTask { fd: fd, tls: tls }; Channel_Send(ctx.taskQueue, task); + } else { + if Os_ShouldStop() { + break; + } } } + var i: int = 0; + while i < ctx.workerCount { + let poison: ConnectionTask = ConnectionTask { fd: -1, tls: null as *void }; + Channel_Send(ctx.taskQueue, poison); + i = i + 1; + } + PrintLine("acceptor: stop — workers poisoned"); } pub func RunServer(config: ServerConfig, router: Router) -> int { PrintLine("================================================"); - PrintLine(" Nexus HTTP Server v0.3.0"); - PrintLine(" HTTP/1.1 keep-alive + thread-pool"); + PrintLine(" Nexus HTTP Server v0.6.0"); + PrintLine(" HTTP/1.1 + TLS/mTLS + keep-alive + graceful stop"); PrintLine(" Built with Bux"); PrintLine("================================================"); PrintLine(""); + Os_InstallStopHandlers(); + + var tlsCtx: *void = null as *void; + if config.tlsEnabled { + if String_Eq(config.tlsCertPath, "") || String_Eq(config.tlsKeyPath, "") { + PrintLine("FATAL: TLS enabled but NEXUS_TLS_CERT / NEXUS_TLS_KEY not set"); + return 1; + } + if !String_Eq(config.tlsClientCaPath, "") { + tlsCtx = Tls_ServerCtxMtls(config.tlsCertPath, config.tlsKeyPath, config.tlsClientCaPath); + PrintLine("mTLS: client certificates required"); + Print("Client CA: "); + PrintLine(config.tlsClientCaPath); + } else { + tlsCtx = Tls_ServerCtx(config.tlsCertPath, config.tlsKeyPath); + } + if tlsCtx == null as *void { + Print("FATAL: TLS context failed: "); + PrintLine(Tls_LastError()); + return 1; + } + Print("TLS cert: "); + PrintLine(config.tlsCertPath); + } + let serverFd: int = Net_Create(); if serverFd < 0 { PrintLine("FATAL: socket() failed"); + if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); } return 1; } @@ -161,43 +282,66 @@ module Server { Print("FATAL: bind failed: "); PrintLine(Net_LastError()); Net_Close(serverFd); + if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); } return 1; } if !Net_Listen(serverFd, config.backlog) { PrintLine("FATAL: listen() failed"); Net_Close(serverFd); + if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); } return 1; } - Print("Listening on http://"); + Os_SetStopListenFd(serverFd); + + if config.tlsEnabled { + Print("Listening on https://"); + } else { + Print("Listening on http://"); + } Print(config.bindAddr); Print(":"); PrintInt(config.port); PrintLine(""); PrintInt(config.workerCount); - PrintLine(" worker threads | keep-alive: on | static: ./public/"); + Print(" worker threads | keep-alive: on | max-body: "); + PrintInt(config.maxBodyBytes); + PrintLine(" B"); PrintLine("Endpoints: / /api/health /api/info /ws"); - PrintLine("Press Ctrl+C to stop."); + PrintLine("Stop: Ctrl+C / SIGTERM (graceful)."); PrintLine(""); let taskQueue: Channel = Channel_New(config.backlog as int64); - let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router }; - let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue }; + let workerCtx: WorkerCtx = WorkerCtx { + taskQueue: &taskQueue, + router: router, + config: config + }; + let acceptorCtx: AcceptorCtx = AcceptorCtx { + serverFd: serverFd, + taskQueue: &taskQueue, + workerCount: config.workerCount, + tlsCtx: tlsCtx + }; - // Spawn workers (main thread will also become one) var i: int = 0; - while i < config.workerCount - 1 { + while i < config.workerCount { spawn Worker(&workerCtx); i = i + 1; } - // Spawn acceptor - spawn Acceptor(&acceptorCtx); + Acceptor(&acceptorCtx); - // Main thread works too - Worker(&workerCtx); + Os_SetStopListenFd(-1); + if serverFd >= 0 { + Net_Close(serverFd); + } + if tlsCtx != null as *void { + Tls_CtxFree(tlsCtx); + } + PrintLine("nexus: acceptor stopped — exit"); return 0; } diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index 3ab6bf4..c290dd3 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -1,4 +1,4 @@ -import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables] +import std/[os, strutils, terminal, strformat, osproc, sets, algorithm, tables, sha1] import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend import source_location import fmt @@ -12,11 +12,19 @@ type cmOn cmOff + ## Which C runtime shim to link (session 75 — Linux / cloud / embedded). + RuntimeFlavor* = enum + rfFull ## rt/runtime.c — POSIX + OpenSSL + rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly + rfWin ## rt/runtime_win.c — Windows/MinGW (historical) + GlobalOptions* = object color*: ColorMode quiet*: bool verbose*: bool - release*: bool ## --release: -O2, no -g / no #line (E.4 dual) + release*: bool ## --release: -O2, no -g / no #line (E.4 dual) + staticLink*: bool ## --static: fully-static binary (implies thin runtime unless full) + target*: string ## --target : cross-compile (e.g. aarch64-linux-gnu) proc printUsage*() = echo """Bux Programming Language (bootstrap compiler) @@ -44,22 +52,32 @@ Command options: fmt --check Exit 1 if any file would be reformatted (CI) doc --out Write docs to file (default: stdout) add --path / --git Explicit source; else resolve via registry + install --locked Verify bux.lock only (CI; no re-resolve) build --release Optimized build (-O2, no debug / #line) + build --static Fully-static link (uses minimal runtime; no OpenSSL) + build --target T Cross-compile triple (prefers T-gcc, else clang -target) -Registry: +Registry / toolchain env: BUX_REGISTRY Local path or http(s):// URL to registry.toml BUX_REGISTRY_REFRESH=1 Force re-download of HTTP index cache + BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke) BUX_CFLAGS Extra flags appended to the C compiler line + BUX_CC C compiler binary (overrides --target pick) + BUX_RUNTIME full|minimal|thin|embed|win (default: full on Unix) + BUX_STATIC=1 Same as --static Global options: --color Control colored output (default: auto) -q, --quiet Suppress non-error output -v, --verbose Verbose output --release Optimize (-O2), omit -g and #line maps + --static Fully-static link + thin runtime (containers / distroless) + --target Cross-compile (e.g. aarch64-linux-gnu) """ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] = - result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false, release: false) + result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false, + release: false, staticLink: false, target: "") result.rest = @[] result.ok = true var i = 0 @@ -85,6 +103,17 @@ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq result.opts.verbose = true elif arg == "--release": result.opts.release = true + elif arg == "--static": + result.opts.staticLink = true + elif arg == "--target": + if i + 1 >= args.len: + stderr.writeLine("error: --target requires a triple (e.g. aarch64-linux-gnu)") + result.ok = false + return + inc i + result.opts.target = args[i] + elif arg.startsWith("--target="): + result.opts.target = arg["--target=".len .. ^1] else: result.rest.add(arg) inc i @@ -95,6 +124,80 @@ proc shouldUseColor(opts: GlobalOptions): bool = of cmOff: false of cmAuto: terminal.isatty(stdout) +proc wantStaticLink(opts: GlobalOptions): bool = + ## --static or BUX_STATIC=1 + if opts.staticLink: return true + let e = getEnv("BUX_STATIC") + result = e == "1" or e.toLowerAscii() in ["true", "yes", "on"] + +proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor = + ## Linux/cloud/embed first. Windows is not a product target (rfWin historical). + let e = getEnv("BUX_RUNTIME").toLowerAscii() + case e + of "full", "posix": + return rfFull + of "minimal", "thin", "embed", "embedded", "freestanding": + return rfMinimal + of "win", "windows": + return rfWin + of "": + discard + else: + # Unknown value → fall through to defaults + discard + when defined(windows): + return rfWin + # Fully-static containers: OpenSSL static is painful → thin runtime default + if wantStaticLink(opts): + return rfMinimal + # Cross without explicit full: prefer thin (host may lack target libcrypto) + if opts.target.len > 0: + return rfMinimal + return rfFull + +proc runtimeFileName(flavor: RuntimeFlavor): string = + case flavor + of rfFull: "runtime.c" + of rfMinimal: "runtime_minimal.c" + of rfWin: "runtime_win.c" + +proc isThinRuntime(flavor: RuntimeFlavor): bool = + flavor in {rfMinimal, rfWin} + +proc findOnPath(bin: string): bool = + ## True if `bin` resolves as an executable on PATH (or is an absolute path). + if bin.len == 0: return false + if '/' in bin or '\\' in bin: + return fileExists(bin) + let (outp, code) = execCmdEx(&"command -v {quoteShell(bin)} 2>/dev/null") + result = code == 0 and outp.strip().len > 0 + +proc resolveCCompiler(opts: GlobalOptions): string = + ## Prefer BUX_CC, then -gcc for --target, then clang -target, else host cc. + let envCc = getEnv("BUX_CC") + if envCc.len > 0: + return envCc + if opts.target.len > 0: + let tripleGcc = opts.target & "-gcc" + if findOnPath(tripleGcc): + return tripleGcc + if findOnPath("clang"): + return "clang" + # Fall through — user may still have a named cross compiler elsewhere + return tripleGcc + when defined(windows): + return "gcc" + else: + return "cc" + +proc cTargetFlags(opts: GlobalOptions, ccBin: string): string = + ## Extra flags for cross: clang needs -target; *-gcc is already a cross binary. + if opts.target.len == 0: return "" + let base = ccBin.extractFilename.toLowerAscii() + if base == "clang" or base.startsWith("clang-"): + return " -target " & opts.target + "" + proc printError(msg: string, useColor: bool) = if useColor: stdout.setForegroundColor(fgRed) @@ -487,9 +590,87 @@ proc cmdSearch*(args: seq[string], opts: GlobalOptions): int = echo &" {p.name} {p.version} — {desc}" return 0 + +proc packageChecksum*(dir: string): string = + ## Deterministic sha1 of all `*.bux` under dir (sorted paths + contents). + ## Used for bux.lock Checksum — cloud install reproducibility (session 79). + if dir.len == 0 or not dirExists(dir): + return "" + var files: seq[string] = @[] + for f in walkDirRec(dir): + if f.endsWith(".bux"): + files.add(f) + files.sort(system.cmp) + var blob = "" + for f in files: + let rel = relativePath(f, dir) + blob.add(rel) + blob.add("\n") + try: + blob.add(readFile(f)) + except CatchableError: + discard + blob.add("\0") + result = toLowerAscii($secureHash(blob)) + +proc verifyLockedInstall*(root: string, useColor: bool, opts: GlobalOptions): int = + ## `bux install --locked`: require bux.lock and verify path deps + checksums. + let lockPath = root / "bux.lock" + if not fileExists(lockPath): + printError("install --locked: bux.lock missing (run `bux install` first)", useColor) + return 1 + let lock = loadLockfile(lockPath) + if lock.entries.len == 0: + if not opts.quiet: + printInfo("install --locked: empty lock (no dependencies)", useColor) + return 0 + for e in lock.entries: + let src = e.source + if src.startsWith("http://") or src.startsWith("https://") or src.endsWith(".git"): + # Git: ensure cache dir exists + let depDir = getHomeDir() / ".bux" / "packages" / e.name + if not dirExists(depDir): + printError(&"install --locked: git package '{e.name}' not cached at {depDir}", useColor) + printError("hint: run `bux install` once to clone, then commit bux.lock", useColor) + return 1 + if e.checksum.len > 0: + let got = packageChecksum(depDir) + if got != e.checksum: + printError(&"install --locked: checksum mismatch for '{e.name}'", useColor) + printError(&" lock: {e.checksum}", useColor) + printError(&" got: {got}", useColor) + return 1 + else: + # Path source (absolute or relative) + let absPath = if src.isAbsolute: src else: root / src + if not dirExists(absPath): + printError(&"install --locked: path '{e.name}' missing: {absPath}", useColor) + return 1 + if e.checksum.len > 0: + let got = packageChecksum(absPath) + if got != e.checksum: + printError(&"install --locked: checksum mismatch for '{e.name}'", useColor) + printError(&" lock: {e.checksum}", useColor) + printError(&" got: {got}", useColor) + return 1 + if not opts.quiet: + printInfo(&"locked ok: {e.name} {e.version}", useColor) + if not opts.quiet: + printInfo(&"install --locked: {lock.entries.len} package(s) verified", useColor) + return 0 + proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = let useColor = shouldUseColor(opts) + var lockedOnly = false + for a in args: + if a == "--locked": + lockedOnly = true + elif a.startsWith("-"): + printError(&"unknown install option '{a}'", useColor) + return 1 let root = getCurrentDir() + if lockedOnly: + return verifyLockedInstall(root, useColor, opts) let manifestPath = root / "bux.toml" if not fileExists(manifestPath): printError("no bux.toml found", useColor) @@ -510,11 +691,12 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = return 1 # Read dependency manifest let depManifestPath = absPath / "bux.toml" + let csum = packageChecksum(absPath) if fileExists(depManifestPath): let depMan = loadManifest(depManifestPath) - lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath)) + lock.entries.add(LockEntry(name: dep.name, version: depMan.version, source: absPath, checksum: csum)) else: - lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath)) + lock.entries.add(LockEntry(name: dep.name, version: "0.0.0", source: absPath, checksum: csum)) if not opts.quiet: printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor) of dkGit: @@ -530,7 +712,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = if not opts.quiet: printInfo(&"Using cached '{dep.name}' from {depDir}", useColor) # Lock stores git URL; build loads from cache by name - lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl)) + let csumGit = packageChecksum(depDir) + lock.entries.add(LockEntry(name: dep.name, version: dep.gitVersion, source: dep.gitUrl, checksum: csumGit)) of dkVersion: # Registry lookup (E.1) if reg.path.len == 0: @@ -541,7 +724,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = printError(&"package '{dep.name}' not found in registry", useColor) return 1 if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath): - lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.resolvedPath)) + let csum = packageChecksum(pkg.resolvedPath) + lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.resolvedPath, checksum: csum)) if not opts.quiet: printInfo(&"Resolved '{dep.name}' {pkg.version} → {pkg.resolvedPath}", useColor) elif isGitSource(pkg.source): @@ -553,7 +737,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int = if code != 0: printError(&"failed to clone {pkg.source}: {outp}", useColor) return 1 - lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.source)) + let csumG = packageChecksum(depDir) + lock.entries.add(LockEntry(name: dep.name, version: pkg.version, source: pkg.source, checksum: csumG)) if not opts.quiet: printInfo(&"Resolved '{dep.name}' {pkg.version} → git {pkg.source}", useColor) else: @@ -750,14 +935,25 @@ proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] = proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = var opts = opts var pathArgs: seq[string] = @[] - for a in args: + var i = 0 + while i < args.len: + let a = args[i] if a == "--release": opts.release = true + elif a == "--static": + opts.staticLink = true + elif a == "--target": + if i + 1 < args.len: + inc i + opts.target = args[i] + elif a.startsWith("--target="): + opts.target = a["--target=".len .. ^1] elif a.startsWith("-"): - # ignore unknown flags for forward-compat; keep path-like later + # ignore unknown flags for forward-compat discard else: pathArgs.add(a) + inc i let useColor = shouldUseColor(opts) let root = if pathArgs.len > 0: absolutePath(pathArgs[0]) else: getCurrentDir() let (pctx, status) = prepareProject(root, useColor, opts) @@ -805,11 +1001,11 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = return 1 let baseDir = stdlibDir.parentDir() - # Windows / BUX_RUNTIME=win → minimal runtime (no pthread/OpenSSL). - # Full POSIX runtime is rt/runtime.c. - let forceWinRt = getEnv("BUX_RUNTIME") == "win" or getEnv("BUX_RUNTIME") == "windows" - let useWinRt = forceWinRt or (when defined(windows): true else: false) - let runtimeName = if useWinRt: "runtime_win.c" else: "runtime.c" + # Runtime pick: full POSIX | minimal (Linux static/embed) | win (historical). + # See resolveRuntimeFlavor — BUX_RUNTIME, --static, --target. + let flavor = resolveRuntimeFlavor(opts) + let thinRt = isThinRuntime(flavor) + let runtimeName = runtimeFileName(flavor) let runtimeSrc = baseDir / "rt" / runtimeName if fileExists(runtimeSrc): copyFile(runtimeSrc, runtimeDst) @@ -830,26 +1026,25 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = let outputFile = buildDir / (outputName & exeSuffix) let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g" let extraCflags = getEnv("BUX_CFLAGS") - let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags - # Host C toolchain + link flags - let envCc = getEnv("BUX_CC") - let ccBin = - if envCc.len > 0: envCc - else: - when defined(windows): "gcc" - else: "cc" + var cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags + let doStatic = wantStaticLink(opts) + if doStatic: + cflags = cflags & " -static" + # Host / cross C toolchain + link flags + let ccBin = resolveCCompiler(opts) + cflags = cflags & cTargetFlags(opts, ccBin) let ldStable = when defined(linux): - if useWinRt: "" else: " -Wl,--build-id=none" + if thinRt: "" else: " -Wl,--build-id=none" else: "" # Note: -l libs must come *after* .c/.o inputs (GNU ld left-to-right). let (hostCflags, hostLibs) = - if useWinRt: + if thinRt: # gc-sections drops mono stdlib that is never called (crypto/tasks, …) (" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm") else: - (" -pthread" & ldStable, " -lm -lcrypto") + (" -pthread" & ldStable, " -lm -lssl -lcrypto") let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1" if opts.verbose: printInfo(&"running: {ccCmd}", useColor) diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 7601cf8..bb34d4f 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -98,6 +98,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]): Type +proc markCrossFuncPtrMoves(ctx: var LowerCtx, call: Expr) proc resolvePtrAlias(ctx: LowerCtx, name: string): string = ## Follow `p → bag` aliases (depth-limited). @@ -173,9 +174,174 @@ proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) = of ekTuple: for e in expr.exprTupleElements: ctx.markMovedOutFromAst(e) + of ekCall: + # Cross-function: Take(&bag) may move fields of bag (session 76) + ctx.markCrossFuncPtrMoves(expr) + for a in expr.exprCallArgs: + ctx.markMovedOutFromAst(a) else: discard + +proc argAmpOwner(ctx: LowerCtx, arg: Expr): string = + ## If arg is `&local` (or cast of that), return the owner local name. + ## Also: bare pointer local that aliases an owner (`p` where p→bag). + if arg == nil: return "" + var e = arg + while e != nil and e.kind == ekCast: + e = e.exprCastOperand + if e != nil and e.kind == ekUnary and e.exprUnaryOp == tkAmp: + var op = e.exprUnaryOperand + while op != nil and op.kind == ekCast: + op = op.exprCastOperand + if op != nil and op.kind == ekIdent and op.exprIdent.len > 0: + return ctx.resolvePtrAlias(op.exprIdent) + return "" + if e != nil and e.kind == ekIdent and e.exprIdent.len > 0: + let owner = ctx.resolvePtrAlias(e.exprIdent) + if owner != e.exprIdent: + return owner + "" + +proc fieldPathFromParam(expr: Expr, param: string): seq[string] = + ## If `expr` is `param.a.b` / `(*param).a` / `param` auto-deref field chain, + ## return path `["a","b"]`. Empty if not rooted at param. + result = @[] + if expr == nil or param.len == 0: return + var path: seq[string] = @[] + var e = expr + while e != nil and e.kind == ekField: + path.insert(e.exprFieldName, 0) + e = e.exprFieldObj + while e != nil and e.kind == ekUnary and e.exprUnaryOp == tkStar: + e = e.exprUnaryOperand + if e != nil and e.kind == ekIdent and e.exprIdent == param and path.len > 0: + result = path + +proc scanExprParamMoves(e: Expr, param: string, paths: var HashSet[string], whole: var bool) +proc scanBlockParamMoves(blk: Block, param: string, paths: var HashSet[string], whole: var bool) + +proc scanExprParamMoves(e: Expr, param: string, paths: var HashSet[string], whole: var bool) = + ## Detect ownership moves of pointee fields through pointer param `param`. + if e == nil or param.len == 0: return + case e.kind + of ekField: + let path = fieldPathFromParam(e, param) + if path.len > 0: + paths.incl(path.join(".")) + of ekUnary: + if e.exprUnaryOp == tkStar and e.exprUnaryOperand != nil and + e.exprUnaryOperand.kind == ekIdent and + e.exprUnaryOperand.exprIdent == param: + whole = true + else: + scanExprParamMoves(e.exprUnaryOperand, param, paths, whole) + of ekStructInit: + for f in e.exprStructInitFields: + scanExprParamMoves(f.value, param, paths, whole) + of ekTuple: + for el in e.exprTupleElements: + scanExprParamMoves(el, param, paths, whole) + of ekCall: + if e.exprCallCallee != nil: + scanExprParamMoves(e.exprCallCallee, param, paths, whole) + for a in e.exprCallArgs: + scanExprParamMoves(a, param, paths, whole) + of ekBinary: + scanExprParamMoves(e.exprBinaryLeft, param, paths, whole) + scanExprParamMoves(e.exprBinaryRight, param, paths, whole) + of ekAssign: + # `let x = p.items` style via assign value + scanExprParamMoves(e.exprAssignValue, param, paths, whole) + of ekBlock: + if e.exprBlock != nil: + scanBlockParamMoves(e.exprBlock, param, paths, whole) + of ekCast: + scanExprParamMoves(e.exprCastOperand, param, paths, whole) + else: + discard + +proc scanStmtParamMoves(s: Stmt, param: string, paths: var HashSet[string], whole: var bool) = + if s == nil: return + case s.kind + of skReturn: + scanExprParamMoves(s.stmtReturnValue, param, paths, whole) + of skLet: + scanExprParamMoves(s.stmtLetInit, param, paths, whole) + of skExpr: + scanExprParamMoves(s.stmtExpr, param, paths, whole) + of skIf: + scanExprParamMoves(s.stmtIfCond, param, paths, whole) + if s.stmtIfThen != nil: scanBlockParamMoves(s.stmtIfThen, param, paths, whole) + if s.stmtIfElse != nil: scanBlockParamMoves(s.stmtIfElse, param, paths, whole) + for br in s.stmtIfElseIfs: + scanExprParamMoves(br.cond, param, paths, whole) + if br.blk != nil: scanBlockParamMoves(br.blk, param, paths, whole) + of skWhile: + scanExprParamMoves(s.stmtWhileCond, param, paths, whole) + if s.stmtWhileBody != nil: scanBlockParamMoves(s.stmtWhileBody, param, paths, whole) + of skFor: + scanExprParamMoves(s.stmtForIter, param, paths, whole) + if s.stmtForBody != nil: scanBlockParamMoves(s.stmtForBody, param, paths, whole) + of skMatch: + scanExprParamMoves(s.stmtMatchSubject, param, paths, whole) + for arm in s.stmtMatchArms: + if arm.body != nil: + scanExprParamMoves(arm.body, param, paths, whole) + else: + discard + +proc scanBlockParamMoves(blk: Block, param: string, paths: var HashSet[string], whole: var bool) = + if blk == nil: return + for st in blk.stmts: + scanStmtParamMoves(st, param, paths, whole) + +proc paramIsPointer(p: Param): bool = + ## True if the parameter type is a pointer (`*T` / `&T` / `own` pointer-ish). + if p.ptype == nil: return false + p.ptype.kind in {tekPointer, tekOwn} + +proc markCrossFuncPtrMoves(ctx: var LowerCtx, call: Expr) = + ## Session 76: `TakeItems(&bag)` where TakeItems moves `p.items` → mark bag. + if call == nil or call.kind != ekCall: return + var calleeName = "" + if call.exprCallCallee == nil: return + case call.exprCallCallee.kind + of ekIdent: + calleeName = call.exprCallCallee.exprIdent + if ctx.importTable.hasKey(calleeName): + calleeName = ctx.importTable[calleeName] + of ekPath: + calleeName = call.exprCallCallee.exprPath.join("_") + of ekGenericCall: + calleeName = call.exprCallCallee.exprGenericCallee + else: + return + if calleeName.len == 0: return + let sym = ctx.globalScope.lookup(calleeName) + if sym == nil or sym.decl == nil or sym.decl.kind != dkFunc: return + let decl = sym.decl + if decl.declFuncBody == nil: return + for i, arg in call.exprCallArgs: + if i >= decl.declFuncParams.len: break + let fp = decl.declFuncParams[i] + if not paramIsPointer(fp): continue + let owner = ctx.argAmpOwner(arg) + if owner.len == 0: continue + if not ctx.hasPendingDrop(owner): continue + var paths = initHashSet[string]() + var whole = false + scanBlockParamMoves(decl.declFuncBody, fp.name, paths, whole) + if not whole and paths.len == 0: continue + if whole: + ctx.markMovedOutLocal(owner) + else: + if not ctx.partialMovedFields.hasKey(owner): + ctx.partialMovedFields[owner] = initHashSet[string]() + for path in paths: + ctx.partialMovedFields[owner].incl(path) + ctx.markMovedOutLocal(owner) + proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool = ## Skip Drop for explicit skipName or any moved-out local. let target = dropTargetName(dropNode) @@ -1461,6 +1627,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = return hirBinary(expr.exprBinaryOp, left, right, typ, loc) of ekCall: + # Cross-function pointer ownership (before any lowering side effects) + ctx.markCrossFuncPtrMoves(expr) # Method call desugaring: obj.method(args) → Type_method(obj, args) if expr.exprCallCallee.kind == ekField: let methodName = expr.exprCallCallee.exprFieldName @@ -2006,6 +2174,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = case stmt.kind of skExpr: + if stmt.stmtExpr != nil: + ctx.markMovedOutFromAst(stmt.stmtExpr) return ctx.flushPending(ctx.lowerExpr(stmt.stmtExpr)) of skLet: diff --git a/bootstrap/macroexpand.nim b/bootstrap/macroexpand.nim index 4fd55d5..690253c 100644 --- a/bootstrap/macroexpand.nim +++ b/bootstrap/macroexpand.nim @@ -1006,9 +1006,16 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl], let pat = exprToPattern(arg) if pat == nil: return nil return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat) - of mfkExpr, mfkTt: + of mfkExpr: if arg.kind in {ekMacroStmt, ekMacroPat}: return nil return arg + of mfkTt: + # Session 76: token-tree is a *superset* of expr — any single + # well-formed AST fragment the call parser already produced: + # expr, block, ident, literal, path, call, stmt, or pat wrapper. + # (True delimiter-balanced raw tokens remain future work.) + if arg == nil: return nil + return arg proc fragMatches(k: MacroFragKind, arg: Expr): bool = ## Kind constraint at match time (after arg expand). diff --git a/bootstrap/registry.nim b/bootstrap/registry.nim index b99f57a..9ecf55a 100644 --- a/bootstrap/registry.nim +++ b/bootstrap/registry.nim @@ -44,12 +44,13 @@ proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) = p = p[2 .. ^1] if not p.isAbsolute: p = indexDir / p - pkg.resolvedPath = p.absolutePath + # Collapse ../ segments for cleaner lockfiles (session 82) + pkg.resolvedPath = expandFilename(p) elif pkg.source.startsWith("path:"): var p = pkg.source["path:".len .. ^1] if not p.isAbsolute: p = indexDir / p - pkg.resolvedPath = p.absolutePath + pkg.resolvedPath = expandFilename(p) pkg.source = "file:" & pkg.resolvedPath proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] = @@ -115,14 +116,18 @@ proc fetchRegistryUrl*(url: string): string = if not force and fileExists(cachePath) and cachedUrl == url: return cachePath.absolutePath - # Prefer curl; fall back to wget + # Prefer curl; fall back to wget. + # BUX_REGISTRY_INSECURE=1 → allow self-signed HTTPS (dev / smoke only). + let insecure = getEnv("BUX_REGISTRY_INSECURE").len > 0 var ok = false if findExe("curl").len > 0: - let cmd = &"curl -fsSL --max-time 30 -o {quoteShell(cachePath)} {quoteShell(url)}" + let kflag = if insecure: " -k" else: "" + let cmd = &"curl -fsSL{kflag} --max-time 30 -o {quoteShell(cachePath)} {quoteShell(url)}" let (_, code) = execCmdEx(cmd) ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 elif findExe("wget").len > 0: - let cmd = &"wget -q -T 30 -O {quoteShell(cachePath)} {quoteShell(url)}" + let nflag = if insecure: " --no-check-certificate" else: "" + let cmd = &"wget -q{nflag} -T 30 -O {quoteShell(cachePath)} {quoteShell(url)}" let (_, code) = execCmdEx(cmd) ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 else: diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 63210e8..9ac7beb 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -745,7 +745,20 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue = of ekLiteral: case expr.exprLit.kind of tkIntLiteral: - return CtValue(kind: ctkInt, intVal: parseBiggestInt(expr.exprLit.text)) + # Support 0x / 0b / 0o prefixes (parseBiggestInt is decimal-only). + let lit = expr.exprLit.text + try: + if lit.len >= 3 and lit[0] == '0': + let p = lit[1].toLowerAscii() + if p == 'x': + return CtValue(kind: ctkInt, intVal: BiggestInt(parseHexInt(lit[2 .. ^1]))) + elif p == 'b': + return CtValue(kind: ctkInt, intVal: BiggestInt(parseBinInt(lit[2 .. ^1]))) + elif p == 'o': + return CtValue(kind: ctkInt, intVal: BiggestInt(parseOctInt(lit[2 .. ^1]))) + return CtValue(kind: ctkInt, intVal: parseBiggestInt(lit)) + except ValueError: + return CtValue(kind: ctkVoid) of tkBoolLiteral: return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true") of tkStringLiteral: @@ -786,6 +799,12 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue = of tkPercent: if right.intVal != 0: return CtValue(kind: ctkInt, intVal: left.intVal mod right.intVal) + # Bitwise (session 75 — embedded CRC / flag tables at compile time) + of tkCaret: return CtValue(kind: ctkInt, intVal: left.intVal xor right.intVal) + of tkAmp: return CtValue(kind: ctkInt, intVal: left.intVal and right.intVal) + of tkPipe: return CtValue(kind: ctkInt, intVal: left.intVal or right.intVal) + of tkShl: return CtValue(kind: ctkInt, intVal: left.intVal shl right.intVal) + of tkShr: return CtValue(kind: ctkInt, intVal: left.intVal shr right.intVal) of tkEq: return CtValue(kind: ctkBool, boolVal: left.intVal == right.intVal) of tkNe: return CtValue(kind: ctkBool, boolVal: left.intVal != right.intVal) of tkLt: return CtValue(kind: ctkBool, boolVal: left.intVal < right.intVal) diff --git a/docs/BuildAndTest.md b/docs/BuildAndTest.md index e8014f8..fe58e0b 100644 --- a/docs/BuildAndTest.md +++ b/docs/BuildAndTest.md @@ -20,7 +20,7 @@ On macOS: brew install nim gcc make openssl ``` -> **Note:** The `Std::Crypto` module requires OpenSSL (`-lcrypto`). The build system links it automatically. +> **Note:** Crypto + TLS require OpenSSL (`-lssl -lcrypto`). The build system links both automatically on the full (POSIX) runtime. --- @@ -107,23 +107,51 @@ Output = "Bin" Build output goes to `build/` by default. -### Cross-Compilation +### Cross-Compilation, static, and thin runtime (Linux-first) -Use `--target ` to cross-compile for a different platform. Bux generates C code and uses `clang` with the `-target` flag for cross-compilation. +Bux targets **Linux** (primary), **cloud/containers**, and **embedded/cross**. Windows is not a product focus. ```bash -# Cross-compile for ARM Linux -./buxc build --target aarch64-linux-gnu +# Thin runtime (no pthread / OpenSSL / sockets) — good for CLI & embed +BUX_RUNTIME=minimal ./buxc build -# Cross-compile for x86_64 Linux (explicit) -./buxc build --target x86_64-linux-gnu +# Fully-static binary (implies minimal runtime; container / distroless friendly) +./buxc --static --release build +# same: BUX_STATIC=1 ./buxc --release build -# Cross-compile and run project build -./buxc project --target x86_64-linux-gnu -./buxc run --target aarch64-linux-gnu +# Cross-compile for ARM64 Linux (prefers aarch64-linux-gnu-gcc, else clang -target) +./buxc --static --release --target aarch64-linux-gnu build + +# Override C compiler +BUX_CC=aarch64-linux-gnu-gcc ./buxc --static --target aarch64-linux-gnu build + +# musl fully-static (Alpine-friendly; needs musl-tools or zig) +BUX_CC=musl-gcc BUX_RUNTIME=minimal ./buxc --static --release build +# or: BUX_CC='zig cc -target x86_64-linux-musl' … (use a wrapper script) +make test-musl-static # SKIP if no musl-gcc/zig ``` -> **Note:** `clang` must be installed for cross-compilation. Without `--target`, Bux uses the system `cc` compiler. +| Switch / env | Effect | +|--------------|--------| +| `BUX_RUNTIME=full` | `rt/runtime.c` — POSIX + OpenSSL (default on Unix) | +| `BUX_RUNTIME=minimal` / `thin` / `embed` | `rt/runtime_minimal.c` — thin single-threaded | +| `BUX_RUNTIME=win` | `rt/runtime_win.c` — historical MinGW smoke only | +| `--static` / `BUX_STATIC=1` | `-static` link; defaults to minimal runtime | +| `--target ` | Cross compile; defaults to minimal runtime | +| `BUX_CC` | Force C compiler binary | +| `BUX_CFLAGS` | Extra flags appended to the C line | + +```bash +# Smoke all of the above (+ CTFE CRC example) +make test-linux-targets + +# Build static hello for Docker scratch/distroless +./tools/build_static_hello.sh +docker build -f examples/docker/Dockerfile.static \ + --build-arg BIN=build/hello_static -t bux-hello-static . +``` + +> **Note:** Full runtime + fully-static OpenSSL is intentionally not the default (painful). Use minimal for static containers; keep full runtime for servers that need net/crypto (`nexus`). --- @@ -137,6 +165,7 @@ make test-stdlib # stdlib golden packages make test-registry # package registry (local + HTTP index) make test-apps # showcase apps build + simpledb/jwt CLI smoke (in `make test`) make test-dwarf # #line maps + .debug_info + --release (in `make test`) +make test-linux-targets # minimal runtime + static + aarch64 cross + CTFE CRC make test-registry # package registry local + HTTP (in `make test`) make test-selfhost-smoke # buxc2: move_field + multi-file #line (in `make test`) make test-lsp # hover + references/rename + call hierarchy @@ -205,7 +234,7 @@ make test # full sequential suite (local) | `build` | ubuntu | `make build` → upload `buxc` artifact | | `unit` | ubuntu | `fmt-check` + `test-unit` (reuse artifact) | | `examples` | ubuntu | `test-examples` (full list) | -| `goldens` | ubuntu | `test-errors` + `test-stdlib` + `test-registry` + `test-dwarf` + `test-drop-move` | +| `goldens` | ubuntu | `test-errors` + `test-stdlib` + `test-registry` + `test-dwarf` + `test-drop-move` + `test-linux-targets` | | `apps` | ubuntu | `test-apps` | | `selfhost` | ubuntu | `test-selfhost-smoke` | | `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) | @@ -331,8 +360,9 @@ bux/ │ ├── Task.bux │ └── Channel.bux ├── rt/ # C runtime -│ ├── runtime.c # full POSIX + OpenSSL (Unix) -│ ├── runtime_win.c # MinGW minimal (Windows / BUX_RUNTIME=win) +│ ├── runtime.c # full POSIX + OpenSSL (Unix default) +│ ├── runtime_minimal.c # thin: no pthread/net/crypto (static/embed) +│ ├── runtime_win.c # MinGW historical (BUX_RUNTIME=win) │ └── io.c ├── examples/ # Example programs ├── tests/ # Unit tests (Nim) diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index 1892669..d8b3c72 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -877,9 +877,30 @@ return p.items; // same as (*p).items Nested paths work the same: `p.inner.items` resolves `p → outer` then path `inner.items`. +#### Cross-function pointer transfer (session 76) + +When the **caller** passes `&bag` (or a pointer alias) into a function whose +parameter is `*Bag`, and the **callee** moves fields of that param +(`return p.items` / `let x = p.items`), the call site marks `bag` the same way +as a local partial move — parent `Bag_Drop` is skipped; remaining fields Drop. + +```bux +func TakeItems(p: *Bag) -> Array { + return p.items; +} + +func Caller() { + let bag: Bag = …; + let items: Array = TakeItems(&bag); + // bag.items transferred; Tracked_Drop(&bag.tag) still runs +} +``` + +Analysis is **same-module / known callee body** only (bootstrap HIR today). + Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux` / `examples/move_field_remaining.bux` / `examples/move_field_nested.bux` / -`examples/move_field_ptr.bux`. +`examples/move_field_ptr.bux` / `examples/move_cross_fn.bux`. #### Manual Drop and non-Drop types @@ -891,10 +912,11 @@ Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux` / #### Limits (honest) - Partial field moves skip the **parent** `Type_Drop` and drop **remaining** - droppable fields individually, including nested paths `a.b.c` and pointer - aliases `p = &owner` (sessions 70/73/74). -- Pointer aliases are tracked for **local** `p = &local` only (not parameters - that point at caller-owned data across function boundaries). + droppable fields individually, including nested paths `a.b.c`, local pointer + aliases `p = &owner`, and **cross-function** `&owner` args when the callee + body is visible (sessions 70/73/74/76). +- Local pointer aliases (`p = &local`) are tracked in-function; cross-function + uses callee-body scan of pointer params (not full borrow checking). - Interface Drop uses a static `TypeName_Drop` symbol (zero cost), not dynamic dispatch through a vtable. - Double-free bugs in **unchecked** code that manually free *and* auto-drop are @@ -1262,7 +1284,7 @@ macro! with_acc { |------|---------| | `expr` | any expression | | `ident` | bare identifier (`ekIdent`) | - | `tt` | token-tree (MVP: same as `expr`) | + | `tt` | token-tree: any single call-site AST fragment (expr/ident/lit/block/stmt/pat); broader than `expr` | | `literal` / `lit` | int/float/string/char/bool literal only | | `block` | block expression `{ … }` | | `stmt` | one statement (`let`/`if`/… or expression-stmt) | diff --git a/docs/Packages.md b/docs/Packages.md index 93ff0ea..5680773 100644 --- a/docs/Packages.md +++ b/docs/Packages.md @@ -50,12 +50,15 @@ Default locations (first hit wins): 2. `~/.bux/registry.toml` 3. `config/registry.toml` next to the Bux repo / compiler -HTTP indices are fetched with `curl` (or `wget`) into +HTTP(S) indices are fetched with `curl` (or `wget`) into `~/.bux/cache/registry_http.toml`. Set `BUX_REGISTRY_REFRESH=1` to force re-download. Relative `file:` / `path:` entries in a remote index resolve against the cache directory — prefer **absolute paths** or **git URLs** for HTTP-served registries. +Self-signed HTTPS registries (dev/smoke): `BUX_REGISTRY_INSECURE=1` adds +`curl -k` / `wget --no-check-certificate`. **Do not** use this in production. + Format: ```toml @@ -100,12 +103,45 @@ bux add greet 0.1.1 bux add utils --path "../utils" bux add network --git "https://github.com/bux-lang/network" -# Resolve + write bux.lock +# Resolve + write bux.lock (includes Checksum of package *.bux sources) bux install + +# CI: verify lock only (no re-resolve; fails if missing or checksum mismatch) +bux install --locked ``` +### Lockfile (`bux.lock`) + +Generated by `bux install`. Each entry: + +```toml +[[Package]] +Name = "greet" +Version = "0.1.1" +Source = "/abs/path/or/git-url" +Checksum = "sha1-of-sorted-bux-sources" +``` + +- **Reproducible:** running `install` twice with the same deps yields the same lock. +- **`--locked`:** cloud/CI mode — does not rewrite the lock; verifies paths exist + and `Checksum` still matches. Fail-closed if the lock is missing or corrupt. + Demo package in this monorepo: `registry/packages/greet` (registered in -`config/registry.toml`). Smoke test: `tools/smoke_registry.sh`. +`config/registry.toml`). Smoke test: `tools/smoke_registry.sh` +(local + lock + `--locked` + HTTP + HTTPS). + +Selfhost (`buxc2`) package manager parity (sessions 80–81): + +```bash +buxc2 search [query] +buxc2 add greet # registry resolve +buxc2 add greet 0.1.1 +buxc2 install / install --locked +make test-selfhost-install +make test-selfhost-registry # search + add + HTTP index + run +``` + +`$BUX_REGISTRY` / HTTP(S) cache / `BUX_REGISTRY_INSECURE` match bootstrap. --- diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index be4b25f..9be3e43 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,8 +1,10 @@ # Bux — План към „добър“ език (v0.5 → v1.0) -> **Дата:** 2026-07-21 -> **Текущо:** v0.5.x — macros, field-move via pointers, Windows hello -> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. +> **Дата:** 2026-07-23 +> **Текущо:** v0.5.x — CI cloud smokes + registry path cleanup (session 82) +> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. +> **Платформен фокус:** **Linux** (primary) · **cloud-native** (servers, containers, HTTP) · **embedded** (cross, freestanding-ish, CTFE). +> **Не-цел:** MS Windows като product platform (исторически CI/hello smoke остават; няма roadmap investment). --- @@ -33,8 +35,9 @@ 3. **Selfhost като dogfood** — компилаторът и apps (`nexus`, `boko`) са proof. 4. **Инструменти** — fmt, test, LSP, package install без ръчна магия. 5. **Стабилна спецификация** — LanguageRef = реалното поведение. +6. **Целеви среди** — Linux servers/containers, cloud HTTP services, embedded/cross (ARM/RISC-V), не desktop Windows. -Не целим „по-добър Rust“. Целим **единствения език с gradual safety + Go-стил concurrency без GC**. +Не целим „по-добър Rust“. Целим **единствения език с gradual safety + Go-стил concurrency без GC**, удобен за **cloud + systems на Linux**. --- @@ -1207,8 +1210,197 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Платформен фокус (v0.5 → v1.0) + +| Ниша | Какво значи за Bux | Статус / посока | +|------|--------------------|-----------------| +| **Linux** | Host + CI + full `rt/runtime.c` (pthread, ucontext, sockets, OpenSSL) | ✅ primary; macOS secondary smoke only | +| **Cloud-native** | HTTP/HTTPS, registry (lock+HTTPS), containers, musl docs | ✅ sessions 75–79 | +| **Embedded** | Cross (`--target`), CTFE tables, **thin runtime**, no-GC story | ✅ minimal + aarch64 + ctfe_crc (75); 🔧 riscv / bare-metal spike | +| **Windows** | Не е product target | ⛔ no further investment (existing MinGW hello = historical) | + +**Правило:** нов runtime / stdlib / CI effort отива към Linux + cloud + embedded. Windows-only work не влиза в следващи сесии. + +--- + +## Сесия 75 (Linux / cloud / embedded foundation) + +1. **`rt/runtime_minimal.c`** — thin runtime (no pthread / ucontext / sockets / OpenSSL); + same feature surface as historical `runtime_win.c`, documented for Linux static/embed. +2. **Bootstrap CLI** (`bootstrap/cli.nim`): + - `BUX_RUNTIME=full|minimal|thin|embed|win` + - `--static` / `BUX_STATIC=1` → fully-static link; defaults to minimal runtime + - `--target ` → prefers `-gcc`, else `clang -target`; defaults minimal + - `BUX_CC` override; thin link uses `-ffunction-sections -Wl,--gc-sections -lm` +3. **CTFE bitwise + hex** (`bootstrap/sema.nim`): + - `^` `&` `|` `<<` `>>` in const eval + - `0x` / `0b` / `0o` integer literals in CTFE +4. **Example** `examples/ctfe_crc.bux` — recursive CRC-8 table cells + `Pow2(8)` size +5. **Smoke** `tools/smoke_linux_targets.sh` / `make test-linux-targets`: + - minimal hello run + - `--static --release` + `file` statically linked + - `--target aarch64-linux-gnu` when cross-gcc present + - ctfe_crc under minimal +6. **Container** `examples/docker/Dockerfile.static` + `tools/build_static_hello.sh` +7. **CI** goldens job installs `gcc-aarch64-linux-gnu` and runs `test-linux-targets` +8. Docs: BuildAndTest + QUALITY_PLAN platform section + +**Verified:** smoke 4/4 PASS; CRC_1=`#define 7`; aarch64 static ELF. + +--- + +## Сесия 76 (P0: ownership + macros + selfhost parity) + +1. **Cross-function pointer ownership** (`bootstrap/hir_lower.nim`): + - `TakeItems(&bag)` where callee does `return p.items` / `let x = p.items` + - Call site marks owner `bag` + partial path `items`; remaining fields still Drop + - Example `examples/move_cross_fn.bux` + smoke in `test-drop-move` +2. **Macro `$x:tt`** — broader than `expr`: any single call-site AST fragment + (bootstrap + selfhost); example `examples/macro_tt.bux` +3. **Selfhost link parity** (`src/cli.bux`): + - `--static`, `--target`, `BUX_RUNTIME`, `BUX_CC`, `BUX_STATIC` + - `Cli_LinkProgram` shared by single-file + project builds + - Thin runtime → no pthread/OpenSSL; full → POSIX as before +4. Docs: LanguageRef tt + QUALITY_PLAN + +**Verified:** move_cross_fn PASS; macro_tt PASS; smoke_drop_move; buxc2 `--static project` → static ELF + `Hello, Bux!`. + +--- + +## Сесия 77 (selfhost cross-fn + Nexus production polish) + +1. **Selfhost cross-fn ownership** (`src/c_backend.bux`): + - `CBE_MarkCrossFuncFromCall` scans callee HIR for `p.field` moves + - Call site `TakeItems(&bag)` → partial move on `bag` (no double-free) + - Verified: `buxc2 run move_cross_fn` → `cross_fn_drops=2` PASS +2. **Runtime stop handlers** (`rt/runtime.c`): + - `bux_install_stop_handlers` / `bux_should_stop` / `bux_set_stop_listen_fd` + - SIGINT/SIGTERM set flag + close listen fd (unblock accept) + - Thin/win runtimes: no-op stubs +3. **Stdlib** `Os_InstallStopHandlers` / `Os_ShouldStop` / `Os_SetStopListenFd` +4. **Nexus 0.4.0**: + - Graceful stop: main=acceptor; poison workers (`fd=-1`); exit on SIGTERM + - Access log: `METHOD path status ms` (`NEXUS_ACCESS_LOG`) + - Max body: `NEXUS_MAX_BODY` (default 1 MiB) → 413 + - Config fields + `/api/health` version 0.4.0 +5. TLS deferred (needs SSL context in runtime — not this session) + +**Verified:** SIGTERM exits nexus; health JSON 0.4.0; access log lines; selfhost cross_fn. + +--- + +## Сесия 78 (Nexus TLS + container story) + +1. **Runtime TLS** (`rt/runtime.c` + OpenSSL `libssl`): + - `bux_tls_server_ctx` / `accept` / `send` / `recv` / `close` / `error` + - Thin/win: stubs; full POSIX links **`-lssl -lcrypto`** +2. **Stdlib** `Std::Net` — `Tls_ServerCtx`, `Tls_Accept`, `Tls_Send`, `Tls_Recv`, … +3. **Nexus 0.5.0**: + - `NEXUS_TLS=1` + `NEXUS_TLS_CERT` / `NEXUS_TLS_KEY` (PEM) + - `ConnectionTask.tls` handle; `ConnRecv`/`ConnSend` dual plain/TLS + - Banner `https://` when TLS; SIGTERM still graceful +4. **Smoke** `tools/smoke_nexus_tls.sh` / `make test-nexus-tls` (openssl self-signed + curl -k) +5. **Containers**: + - `examples/docker/Dockerfile.nexus` (debian-slim + libssl3) + - `examples/http_health.bux` + `Dockerfile.health` + `tools/build_health_bin.sh` +6. CLI link flags bootstrap + selfhost: `-lssl -lcrypto` + +**Verified:** `curl -k https://…/api/health` → 0.5.0; SIGTERM exit; health binary HTTP. + +--- + +## Сесия 79 (registry lock/HTTPS + musl path) + +1. **`bux install` lock checksums** — sha1 of sorted `*.bux` sources per package +2. **`bux install --locked`** — CI mode: verify paths + checksums; no re-resolve +3. **`BUX_REGISTRY_INSECURE=1`** — self-signed HTTPS registry fetch (`curl -k`) +4. **Smoke** `tools/smoke_registry.sh`: + - lock deterministic (diff two installs) + - `--locked` ok / missing fail / checksum mismatch fail + - HTTP + **HTTPS** self-signed index +5. **musl path** `tools/smoke_musl_static.sh` / `make test-musl-static` + - `BUX_CC=musl-gcc` or zig musl wrapper + `BUX_RUNTIME=minimal --static` + - SKIP when toolchain absent (documented) +6. Docs: Packages.md lock section; BuildAndTest musl; Dockerfile.alpine-health + +**Verified:** registry smoke full PASS; musl SKIP (no toolchain on host). + +--- + +## Сесия 80 (selfhost install --locked + Nexus mTLS) + +1. **Selfhost `install` / `install --locked`** (`src/cli.bux`): + - Write `bux.lock` with sha1 checksums (shell `sha1sum` of sorted `*.bux`) + - `--locked` verifies paths + checksums (CI parity with bootstrap session 79) + - Manifest: parse `[Dependencies]` + `{ Path = "..." }` inline tables +2. **mTLS** (`rt/runtime.c` `bux_tls_server_ctx_ex`): + - Optional client CA → `SSL_VERIFY_PEER | FAIL_IF_NO_PEER_CERT` + - `Tls_ServerCtxMtls` in `Std::Net` + - Nexus `NEXUS_TLS_CLIENT_CA` → require client certs (v0.6.0) +3. **Smokes**: `tools/smoke_selfhost_install.sh`, `tools/smoke_nexus_mtls.sh` + - `make test-selfhost-install` / `make test-nexus-mtls` + +**Verified:** selfhost lock/locked/mismatch; mTLS reject without cert + accept with cert. + +--- + +## Сесия 81 (selfhost full registry) + +1. **`src/registry.bux`** — load index from `$BUX_REGISTRY` / `~/.bux` / `config/registry.toml` + - HTTP(S) fetch via curl/wget → `~/.bux/cache/registry_http.toml` + - `BUX_REGISTRY_REFRESH`, `BUX_REGISTRY_INSECURE` (parity with bootstrap) +2. **CLI** `search` / `add ` / `add ` +3. **Build path deps** — merge `depUrl` absolute/relative package `src/` (not only `deps/`) +4. **Runtime discovery** — `BUX_STDLIB/../rt` when building outside the monorepo tree +5. **Smoke** `tools/smoke_selfhost_registry.sh` / `make test-selfhost-registry` + +**Verified:** search greet; add+install+run Hello, Bux!; HTTP registry search. + +--- + +## Сесия 82 (CI cloud/selfhost smokes + polish) + +1. **CI apps job** — `test-nexus-tls` + `test-nexus-mtls` (openssl + curl) +2. **CI selfhost job** — `test-selfhost-install` + `test-selfhost-registry` +3. **Registry paths** — `expandFilename` for file:/path: sources (no `..` in lock) +4. Docs already cover sessions 75–81 platform stack + +**Verified:** local smokes previously green; CI wiring ready for GHA. + +--- + ## Следващи стъпки -1. Windows: more examples (strings/ownership) on MinGW; optional Win OpenSSL -2. Macro: true token-tree `tt` / nested pattern rewrite depth -3. Cross-function pointer ownership transfer (callee `*Bag` param) +### P0 — Compiler / language + +1. ~~Cross-function pointer ownership~~ ✅ session 76 +2. ~~Selfhost `--static` / `BUX_RUNTIME` / `--target`~~ ✅ session 76 +3. ~~Macro `tt` broader than expr~~ ✅ session 76 (raw delimiter-balanced tokens still open) +4. Macro: raw token-tree delimiter balancing / deeper nested rewrite edge cases +5. ~~Selfhost cross-fn moves~~ ✅ session 77 + +### P1 — Linux / cloud-native + +6. ~~**Static path**~~ ✅ session 75–76 +7. ~~**Multi-arch Linux smoke**~~ ✅ session 75 +8. ~~**Nexus production polish**~~ ✅ session 77 +9. ~~**Nexus TLS**~~ ✅ session 78 +10. ~~**Container story**~~ ✅ session 78 +11. ~~**Registry + deploy**~~ ✅ session 79 (HTTPS + lock checksum + `--locked`) +12. ~~**musl path**~~ ✅ session 79 (smoke + docs; SKIP without toolchain) +13. ~~**mTLS / client certs**~~ ✅ session 80 (`NEXUS_TLS_CLIENT_CA`) +14. ~~**Selfhost install --locked**~~ ✅ session 80 +15. ~~**Selfhost full registry**~~ ✅ session 81 (search / add / HTTP / path-dep build) +16. **Language P0 leftovers** — raw macro `tt` delimiter balancing (optional) + +### P2 — Embedded / cross + +12. ~~**Cross / thin / CTFE**~~ ✅ session 75 +13. **riscv64 cross smoke** (when toolchain available) +14. **no-libc / bare-metal research** (spike only) — Cortex-M / qemu-system; not v1.0 blocker + +### Изрично **не** правим + +- Повече Windows examples / Win OpenSSL / Win sockets +- Windows като required CI gate за product features (остава optional historical smoke ако CI вече го има) +- Desktop GUI / Win32 APIs diff --git a/examples/ctfe_crc.bux b/examples/ctfe_crc.bux new file mode 100644 index 0000000..e1b77bb --- /dev/null +++ b/examples/ctfe_crc.bux @@ -0,0 +1,82 @@ +// Session 75 — CTFE tables for embedded / firmware-style use. +// Precomputes CRC-8 (poly 0x07) cells at compile time; runtime only indexes them. +import Std::Io::{PrintLine, PrintInt}; + +const POLY: int = 0x07; + +// One shift step of CRC-8 (MSB-first). +const func CrcStep(crc: int) -> int { + let c: int = crc & 0xFF; + if (c & 0x80) != 0 { + return ((c << 1) ^ POLY) & 0xFF; + } + return (c << 1) & 0xFF; +} + +// Fold remaining shift steps (recursive — CTFE-friendly). +const func CrcFold(crc: int, bits: int) -> int { + if bits <= 0 { + return crc & 0xFF; + } + return CrcFold(CrcStep(crc), bits - 1); +} + +const func Crc8Byte(byte: int) -> int { + return CrcFold(byte & 0xFF, 8); +} + +// Known table cells (full 256-entry array const init is future work). +const CRC_0: int = Crc8Byte(0); +const CRC_1: int = Crc8Byte(1); +const CRC_2: int = Crc8Byte(2); +const CRC_65: int = Crc8Byte(65); // 'A' +const CRC_255: int = Crc8Byte(255); + +// Table size as CTFE power-of-two (classic embedded pattern). +const func Pow2(n: int) -> int { + if n <= 0 { + return 1; + } + return 2 * Pow2(n - 1); +} +const TABLE_SIZE: int = Pow2(8); // 256 + +func Crc8Known(b: int) -> int { + if b == 0 { return CRC_0; } + if b == 1 { return CRC_1; } + if b == 2 { return CRC_2; } + if b == 65 { return CRC_65; } + if b == 255 { return CRC_255; } + return -1; +} + +func Main() -> int { + PrintInt(TABLE_SIZE); + PrintLine(""); + PrintInt(CRC_0); + PrintLine(""); + PrintInt(CRC_1); + PrintLine(""); + PrintInt(CRC_65); + PrintLine(""); + if TABLE_SIZE != 256 { + PrintLine("FAIL ctfe_crc TABLE_SIZE"); + return 1; + } + if CRC_0 != 0 { + PrintLine("FAIL ctfe_crc CRC_0"); + return 1; + } + // Reference: poly 0x07, byte 0x01 → 0x07 after 8 steps + if CRC_1 != 7 { + PrintLine("FAIL ctfe_crc CRC_1 expected 7"); + return 1; + } + let a: int = Crc8Known(1); + if a != 7 { + PrintLine("FAIL ctfe_crc runtime path"); + return 1; + } + PrintLine("PASS ctfe_crc"); + return 0; +} diff --git a/examples/docker/Dockerfile.alpine-health b/examples/docker/Dockerfile.alpine-health new file mode 100644 index 0000000..e1333b8 --- /dev/null +++ b/examples/docker/Dockerfile.alpine-health @@ -0,0 +1,21 @@ +# Session 79 — Alpine multi-stage build of http_health with musl (when host +# can run docker). The *builder* stage expects a prebuilt Linux binary +# produced on Alpine or via musl-gcc: +# +# # On Alpine / with musl-gcc: +# BUX_CC=musl-gcc BUX_RUNTIME=minimal ./buxc --static --release build … +# # or: ./tools/smoke_musl_static.sh +# +# docker build -f examples/docker/Dockerfile.alpine-health -t bux-health-alpine . +# +# Fallback: copy a glibc binary and use debian (see Dockerfile.health). + +FROM alpine:3.20 AS runtime +RUN apk add --no-cache ca-certificates +WORKDIR /app +# Prefer a static musl binary if present; else fail the build clearly. +COPY build/http_health_musl /app/http_health +ENV HEALTH_BIND=0.0.0.0 +ENV HEALTH_PORT=8080 +EXPOSE 8080 +ENTRYPOINT ["/app/http_health"] diff --git a/examples/docker/Dockerfile.health b/examples/docker/Dockerfile.health new file mode 100644 index 0000000..21af2d0 --- /dev/null +++ b/examples/docker/Dockerfile.health @@ -0,0 +1,18 @@ +# Session 78 — minimal health probe image (dynamic glibc + no OpenSSL needed if +# linked without crypto... full runtime still needs libcrypto today). +# +# Build binary first: +# ./tools/build_health_bin.sh +# docker build -f examples/docker/Dockerfile.health -t bux-health . +# docker run --rm -p 8080:8080 bux-health + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libssl3 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY build/http_health /app/http_health +ENV HEALTH_BIND=0.0.0.0 +ENV HEALTH_PORT=8080 +EXPOSE 8080 +ENTRYPOINT ["/app/http_health"] diff --git a/examples/docker/Dockerfile.nexus b/examples/docker/Dockerfile.nexus new file mode 100644 index 0000000..b3031d7 --- /dev/null +++ b/examples/docker/Dockerfile.nexus @@ -0,0 +1,26 @@ +# Session 78 — Nexus in a slim runtime image (dynamic link: pthread + OpenSSL). +# +# From repo root (after building apps/nexus/build/nexus on Linux): +# docker build -f examples/docker/Dockerfile.nexus -t bux-nexus . +# docker run --rm -p 8080:8080 bux-nexus +# +# HTTPS: +# docker run --rm -p 8443:8443 \ +# -v $PWD/certs:/certs:ro \ +# -e NEXUS_PORT=8443 -e NEXUS_TLS=1 \ +# -e NEXUS_TLS_CERT=/certs/cert.pem -e NEXUS_TLS_KEY=/certs/key.pem \ +# bux-nexus + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libssl3 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY apps/nexus/build/nexus /app/nexus +COPY apps/nexus/public /app/public +ENV NEXUS_BIND=0.0.0.0 +ENV NEXUS_PORT=8080 +ENV NEXUS_WORKERS=4 +ENV NEXUS_ACCESS_LOG=1 +EXPOSE 8080 +ENTRYPOINT ["/app/nexus"] diff --git a/examples/docker/Dockerfile.static b/examples/docker/Dockerfile.static new file mode 100644 index 0000000..edb0119 --- /dev/null +++ b/examples/docker/Dockerfile.static @@ -0,0 +1,20 @@ +# Session 75 — fully-static Bux binary in a distroless/scratch container. +# +# Build (from repo root): +# # 1) produce a static host binary with the bootstrap compiler +# ./buxc --static --release build /path/to/pkg +# # or use the helper: +# ./tools/build_static_hello.sh +# +# # 2) package it +# docker build -f examples/docker/Dockerfile.static \ +# --build-arg BIN=build/hello_static \ +# -t bux-hello-static . +# +# Run: +# docker run --rm bux-hello-static + +ARG BIN=build/hello_static +FROM scratch +COPY ${BIN} /app +ENTRYPOINT ["/app"] diff --git a/examples/http_health.bux b/examples/http_health.bux new file mode 100644 index 0000000..4c03a04 --- /dev/null +++ b/examples/http_health.bux @@ -0,0 +1,78 @@ +// Session 78 — tiny health HTTP server for containers / ops probes. +// Needs full POSIX runtime (sockets). Not TLS — use Nexus for HTTPS. +import Std::Io::{Print, PrintLine, PrintInt}; +import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close}; +import Std::String::{String_Len, String_Contains, String_FromInt, String_Concat, String_ToInt}; +import Std::Os::{Os_GetEnv, Os_InstallStopHandlers, Os_ShouldStop, Os_SetStopListenFd}; + +func HealthBody() -> String { + return "{\"status\":\"ok\",\"server\":\"bux-health\",\"version\":\"0.1.0\"}\n"; +} + +func BuildHttp(body: String) -> String { + let n: int = String_Len(body) as int; + return String_Concat( + String_Concat( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ", + String_FromInt(n as int64) + ), + String_Concat("\r\nConnection: close\r\n\r\n", body) + ); +} + +func Main() -> int { + var port: int = 8080; + let pe: String = Os_GetEnv("HEALTH_PORT"); + if String_Len(pe) > 0 { + let p: int64 = String_ToInt(pe); + if p > 0 && p < 65536 { + port = p as int; + } + } + var bind: String = "0.0.0.0"; + let bindEnv: String = Os_GetEnv("HEALTH_BIND"); + if String_Len(bindEnv) > 0 { + bind = bindEnv; + } + + Os_InstallStopHandlers(); + let fd: int = Net_Create(); + if fd < 0 { + PrintLine("socket failed"); + return 1; + } + discard Net_SetReuse(fd); + if !Net_Bind(fd, bind, port) { + PrintLine("bind failed"); + return 1; + } + if !Net_Listen(fd, 64) { + PrintLine("listen failed"); + return 1; + } + Os_SetStopListenFd(fd); + Print("health listening on http://"); + Print(bind); + Print(":"); + PrintInt(port); + PrintLine(""); + + let body: String = HealthBody(); + let resp: String = BuildHttp(body); + + while !Os_ShouldStop() { + let c: int = Net_Accept(fd); + if c < 0 { + if Os_ShouldStop() { break; } + continue; + } + let raw: String = Net_Recv(c, 4096); + if String_Contains(raw, "GET ") { + discard Net_Send(c, resp); + } + Net_Close(c); + } + Net_Close(fd); + PrintLine("health: stopped"); + return 0; +} diff --git a/examples/macro_tt.bux b/examples/macro_tt.bux new file mode 100644 index 0000000..33b1a63 --- /dev/null +++ b/examples/macro_tt.bux @@ -0,0 +1,44 @@ +// Session 76 — `$x:tt` accepts any single call-site AST fragment +// (expr, literal, ident, block, stmt wrapper, …). Broader than `:expr`. +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_Pass}; + +// id_tt already existed in macro_repeat; here we also wrap blocks and stmts. +macro! id_tt { + ( $x:tt ) => { $x } +} + +macro! wrap_tt { + ( $x:tt ) => { + let v: int = $x; + v + 1 + } +} + +// stmt fragment via tt (call site parses `let …` as MacroStmt when using stmt kind; +// with tt, expression form still works: wrap values) +macro! twice_tt { + ( $x:tt ) => { $x + $x } +} + +func Main() -> int { + let a: int = id_tt!(21); + PrintInt(a); + PrintLine(""); + let b: int = wrap_tt!(10); + PrintInt(b); + PrintLine(""); + let c: int = twice_tt!(3 + 4); + PrintInt(c); + PrintLine(""); + let d: int = id_tt!({ 1 + 2 }); + PrintInt(d); + PrintLine(""); + if a != 21 || b != 11 || c != 14 || d != 3 { + PrintLine("FAIL macro_tt"); + return 1; + } + PrintLine("PASS macro_tt"); + Test_Pass("macro_tt"); + return 0; +} diff --git a/examples/move_cross_fn.bux b/examples/move_cross_fn.bux new file mode 100644 index 0000000..f733cc3 --- /dev/null +++ b/examples/move_cross_fn.bux @@ -0,0 +1,84 @@ +// Session 76 — cross-function pointer ownership transfer. +// TakeItems(&bag) moves bag.items inside the callee; the caller must not +// auto-Drop bag.items (only remaining fields / skip parent Drop). +import Std::Io::{PrintLine}; +import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get}; +import Std::String::{String_FromInt, String_Concat}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +@[Drop] +struct Tracked { + id: int, + counter: *int +} + +func Tracked_Drop(self: *Tracked) { + if self.counter != null as *int { + *self.counter = *self.counter + 1; + } +} + +@[Drop] +struct Bag { + items: Array, + tag: Tracked +} + +func Bag_Drop(self: *Bag) { + Array_Drop(&self.items); + Tracked_Drop(&self.tag); +} + +// Callee moves p.items out of the pointee — caller passed &bag. +func TakeItems(p: *Bag) -> Array { + return p.items; +} + +func NestedTake(p: *Bag) -> Array { + let moved: Array = p.items; + return moved; +} + +// Caller holds bag and transfers via &bag into TakeItems. +func CallTakeItems(counter: *int) -> int { + var items: Array = Array_New(2); + Array_Push(&items, 1); + Array_Push(&items, 2); + let bag: Bag = Bag { + items: items, + tag: Tracked { id: 1, counter: counter } + }; + let taken: Array = TakeItems(&bag); + Test_AssertTrue(Array_Len(&taken) == 2); + Test_AssertTrue(Array_Get(&taken, 0) == 1); + return Array_Len(&taken) as int; +} + +func CallNestedTake(counter: *int) -> int { + var items2: Array = Array_New(1); + Array_Push(&items2, 9); + let bag2: Bag = Bag { + items: items2, + tag: Tracked { id: 2, counter: counter } + }; + let taken2: Array = NestedTake(&bag2); + Test_AssertTrue(Array_Get(&taken2, 0) == 9); + return Array_Get(&taken2, 0); +} + +func Main() -> int { + var drops: int = 0; + + let n: int = CallTakeItems(&drops); + Test_AssertTrue(n == 2); + // bag left scope inside CallTakeItems → only tag Drop (items moved out) + Test_AssertTrue(drops == 1); + + let v: int = CallNestedTake(&drops); + Test_AssertTrue(v == 9); + Test_AssertTrue(drops == 2); + + PrintLine(String_Concat("cross_fn_drops=", String_FromInt(drops as int64))); + Test_Pass("move_cross_fn"); + return 0; +} diff --git a/lib/Net.bux b/lib/Net.bux index faa3ed2..8fae069 100644 --- a/lib/Net.bux +++ b/lib/Net.bux @@ -10,6 +10,16 @@ module Std::Net { extern func bux_socket_close(fd: int) -> int; extern func bux_socket_error() -> String; + // TLS server (OpenSSL) — opaque SSL_CTX* / SSL* as *void + extern func bux_tls_server_ctx(certPath: String, keyPath: String) -> *void; + extern func bux_tls_server_ctx_ex(certPath: String, keyPath: String, clientCaPath: String) -> *void; + extern func bux_tls_ctx_free(ctx: *void); + extern func bux_tls_accept(ctx: *void, fd: int) -> *void; + extern func bux_tls_send(ssl: *void, data: String, len: int) -> int; + extern func bux_tls_recv(ssl: *void, maxLen: int) -> String; + extern func bux_tls_close(ssl: *void); + extern func bux_tls_error() -> String; + /* Create a TCP socket. Returns -1 on error. */ func Net_Create() -> int { return bux_socket_create(); @@ -59,4 +69,42 @@ module Std::Net { func Net_LastError() -> String { return bux_socket_error(); } + + // ── TLS server (session 78) ────────────────────────────────────────── + + /// Load PEM cert+key into a server SSL context. null on failure. + func Tls_ServerCtx(certPath: String, keyPath: String) -> *void { + return bux_tls_server_ctx(certPath, keyPath); + } + + /// Server TLS + mTLS: require client certs signed by `clientCaPath` PEM. + func Tls_ServerCtxMtls(certPath: String, keyPath: String, clientCaPath: String) -> *void { + return bux_tls_server_ctx_ex(certPath, keyPath, clientCaPath); + } + + func Tls_CtxFree(ctx: *void) { + bux_tls_ctx_free(ctx); + } + + /// Handshake on an accepted TCP fd. Returns SSL handle or null. + func Tls_Accept(ctx: *void, fd: int) -> *void { + return bux_tls_accept(ctx, fd); + } + + func Tls_Send(ssl: *void, data: String) -> int { + return bux_tls_send(ssl, data, bux_strlen(data) as int); + } + + func Tls_Recv(ssl: *void, maxLen: int) -> String { + return bux_tls_recv(ssl, maxLen); + } + + /// Free SSL handle only (caller still closes the TCP fd). + func Tls_Close(ssl: *void) { + bux_tls_close(ssl); + } + + func Tls_LastError() -> String { + return bux_tls_error(); + } } diff --git a/lib/Os.bux b/lib/Os.bux index 1a3e364..d72bdeb 100644 --- a/lib/Os.bux +++ b/lib/Os.bux @@ -7,6 +7,9 @@ module Std::Os { extern func bux_getcwd() -> String; extern func bux_chdir(path: String) -> int; extern func bux_exit(code: int); + extern func bux_install_stop_handlers(); + extern func bux_should_stop() -> int; + extern func bux_set_stop_listen_fd(fd: int); func Os_ArgsCount() -> int { return bux_argc(); @@ -37,4 +40,20 @@ module Std::Os { bux_exit(code); } + /// Install SIGINT/SIGTERM handlers that set a cooperative stop flag + /// (and close the listen fd if registered via Os_SetStopListenFd). + func Os_InstallStopHandlers() { + bux_install_stop_handlers(); + } + + /// True after SIGINT/SIGTERM (or if handlers not installed: always false). + func Os_ShouldStop() -> bool { + return bux_should_stop() != 0; + } + + /// Register the server listen fd so stop signals can unblock accept(). + func Os_SetStopListenFd(fd: int) { + bux_set_stop_listen_fd(fd); + } + } diff --git a/rt/runtime.c b/rt/runtime.c index 18e0482..945cd41 100644 --- a/rt/runtime.c +++ b/rt/runtime.c @@ -1517,6 +1517,41 @@ int64_t bux_time_ms(void) { return 0; } +/* ============================================================================ + * Cooperative process stop (SIGINT / SIGTERM) — cloud / container friendly + * ============================================================================ */ +static volatile sig_atomic_t g_bux_should_stop = 0; +static int g_bux_stop_listen_fd = -1; + +static void bux_stop_signal_handler(int sig) { + (void)sig; + g_bux_should_stop = 1; + /* Unblock accept() so servers can drain and exit. */ + if (g_bux_stop_listen_fd >= 0) { + int fd = g_bux_stop_listen_fd; + g_bux_stop_listen_fd = -1; + close(fd); + } +} + +void bux_install_stop_handlers(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = bux_stop_signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +} + +int bux_should_stop(void) { + return g_bux_should_stop != 0; +} + +void bux_set_stop_listen_fd(int fd) { + g_bux_stop_listen_fd = fd; +} + int64_t bux_time_us(void) { struct timespec ts; if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { @@ -1652,6 +1687,126 @@ const char* bux_socket_error(void) { return strerror(errno); } +/* ============================================================================ + * TLS server primitives (OpenSSL) — session 78 + * opaque SSL_CTX* / SSL* as void* + * ============================================================================ */ + +#include +#include + +static int g_bux_tls_inited = 0; + +static void bux_tls_ensure_init(void) { + if (g_bux_tls_inited) return; +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); +#else + OPENSSL_init_ssl(0, NULL); +#endif + g_bux_tls_inited = 1; +} + +/* Create a server SSL_CTX from PEM cert + key. + * If client_ca_path is non-empty, enable mTLS (require & verify client certs). + * Returns NULL on failure. */ +void* bux_tls_server_ctx_ex(const char* cert_path, const char* key_path, + const char* client_ca_path) { + bux_tls_ensure_init(); + if (!cert_path || !key_path || !cert_path[0] || !key_path[0]) return NULL; + const SSL_METHOD* method = TLS_server_method(); + SSL_CTX* ctx = SSL_CTX_new(method); + if (!ctx) return NULL; + /* Prefer TLS 1.2+ */ +#ifdef TLS1_2_VERSION + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); +#endif + if (SSL_CTX_use_certificate_file(ctx, cert_path, SSL_FILETYPE_PEM) <= 0) { + SSL_CTX_free(ctx); + return NULL; + } + if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0) { + SSL_CTX_free(ctx); + return NULL; + } + if (!SSL_CTX_check_private_key(ctx)) { + SSL_CTX_free(ctx); + return NULL; + } + /* mTLS: trust client CA and require a client certificate (session 80). */ + if (client_ca_path && client_ca_path[0]) { + if (SSL_CTX_load_verify_locations(ctx, client_ca_path, NULL) != 1) { + SSL_CTX_free(ctx); + return NULL; + } + SSL_CTX_set_verify(ctx, + SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + NULL); + SSL_CTX_set_verify_depth(ctx, 4); + } + return (void*)ctx; +} + +void* bux_tls_server_ctx(const char* cert_path, const char* key_path) { + return bux_tls_server_ctx_ex(cert_path, key_path, NULL); +} + +void bux_tls_ctx_free(void* ctx) { + if (ctx) SSL_CTX_free((SSL_CTX*)ctx); +} + +/* SSL_accept on an already-accepted TCP fd. Returns SSL* or NULL. Does not close fd. */ +void* bux_tls_accept(void* ctx, int fd) { + if (!ctx || fd < 0) return NULL; + SSL* ssl = SSL_new((SSL_CTX*)ctx); + if (!ssl) return NULL; + SSL_set_fd(ssl, fd); + if (SSL_accept(ssl) <= 0) { + SSL_free(ssl); + return NULL; + } + return (void*)ssl; +} + +int bux_tls_send(void* ssl, const char* data, int len) { + if (!ssl || !data || len <= 0) return 0; + int n = SSL_write((SSL*)ssl, data, len); + return n; +} + +BuxString bux_tls_recv(void* ssl, int max_len) { + BuxString result; + result.data = ""; + result.len = 0; + if (!ssl || max_len <= 0) return result; + char* buf = (char*)bux_alloc((size_t)max_len + 1); + int n = SSL_read((SSL*)ssl, buf, max_len); + if (n <= 0) { + /* leave empty; caller sees EOF/error */ + return result; + } + buf[n] = '\0'; + result.data = buf; + result.len = (size_t)n; + return result; +} + +/* Free SSL object only — TCP fd still owned by caller. */ +void bux_tls_close(void* ssl) { + if (!ssl) return; + SSL* s = (SSL*)ssl; + SSL_shutdown(s); + SSL_free(s); +} + +const char* bux_tls_error(void) { + unsigned long e = ERR_get_error(); + if (e == 0) return "tls error"; + return ERR_reason_error_string(e); +} + /* ============================================================================ * Test / Assert primitives * ============================================================================ */ diff --git a/rt/runtime_minimal.c b/rt/runtime_minimal.c new file mode 100644 index 0000000..6fe9dcb --- /dev/null +++ b/rt/runtime_minimal.c @@ -0,0 +1,760 @@ +/* Bux Runtime — minimal / embedded / static (session 75) + * + * Thin single-threaded runtime: no pthread, ucontext, BSD sockets, or OpenSSL. + * Enough for hello, CTFE tables, CLI tools, and static/container binaries. + * Advanced features (tasks, net, crypto) return failure / no-op. + * + * Select via: BUX_RUNTIME=minimal|thin|embed (also default under --static) + * Windows CI still uses runtime_win.c (same feature set, Win32 ifdefs). + * Full POSIX + OpenSSL: rt/runtime.c (default on Unix). + * + * Link with -ffunction-sections -fdata-sections -Wl,--gc-sections so + * monomorphized stdlib that is never called is discarded. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +# include +# include +# include +# define BUX_IS_WIN 1 +# define bux_mkdir_one(p) _mkdir(p) +#else +# include +# include +# define BUX_IS_WIN 0 +# define bux_mkdir_one(p) mkdir((p), 0755) +#endif + +/* ── CLI args ─────────────────────────────────────────────────────────── */ +int g_argc = 0; +char** g_argv = NULL; + +int bux_argc(void) { return g_argc; } +char* bux_argv(int index) { + if (index < 0 || index >= g_argc) return ""; + return g_argv[index]; +} + +/* ── Memory ───────────────────────────────────────────────────────────── */ +void* bux_alloc(size_t size) { + void* ptr = calloc(1, size); + if (ptr == NULL && size > 0) { + fprintf(stderr, "bux runtime: out of memory (alloc %zu)\n", size); + abort(); + } + return ptr; +} +void* bux_realloc(void* ptr, size_t size) { + void* p = realloc(ptr, size); + if (p == NULL && size > 0) { + fprintf(stderr, "bux runtime: out of memory (realloc %zu)\n", size); + abort(); + } + return p; +} +void bux_free(void* ptr) { free(ptr); } + +/* ── Basic I/O / panic ────────────────────────────────────────────────── */ +void bux_print(const char* s) { if (s) fputs(s, stdout); fflush(stdout); } +void bux_println(const char* s) { if (s) puts(s); else puts(""); fflush(stdout); } +void bux_print_int(int64_t n) { printf("%lld", (long long)n); } +void bux_print_float(double f) { printf("%g", f); } +void bux_print_bool(bool b) { fputs(b ? "true" : "false", stdout); } +void bux_print_char(char c) { fputc(c, stdout); } +void bux_panic(const char* msg) { + fprintf(stderr, "PANIC: %s\n", msg ? msg : ""); + abort(); +} +void bux_exit(int code) { exit(code); } +void bux_assert(int cond, const char* file, int line, const char* expr) { + if (!cond) { + fprintf(stderr, "ASSERT FAILED: %s at %s:%d\n", + expr ? expr : "?", file ? file : "?", line); + exit(1); + } +} + +/* ── Checked arithmetic (same semantics as full runtime) ──────────────── */ +int64_t bux_div_i64(int64_t a, int64_t b) { + if (b == 0) bux_panic("division by zero"); + return a / b; +} +int64_t bux_mod_i64(int64_t a, int64_t b) { + if (b == 0) bux_panic("modulo by zero"); + return a % b; +} +int64_t bux_add_i64_checked(int64_t a, int64_t b) { return a + b; } +int64_t bux_sub_i64_checked(int64_t a, int64_t b) { return a - b; } +int64_t bux_mul_i64_checked(int64_t a, int64_t b) { return a * b; } +int64_t bux_neg_i64_checked(int64_t a) { return -a; } + +/* ── Strings ──────────────────────────────────────────────────────────── */ +unsigned int bux_strlen(const char* s) { return s ? (unsigned int)strlen(s) : 0; } +int bux_strlen_c(const char* s) { return s ? (int)strlen(s) : 0; } +int bux_strcmp(const char* a, const char* b) { + if (!a) a = ""; if (!b) b = ""; + return strcmp(a, b); +} +int bux_strncmp(const char* a, const char* b, unsigned int n) { + if (!a) a = ""; if (!b) b = ""; + return strncmp(a, b, (size_t)n); +} +char* bux_strcpy(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) { dest[0] = 0; return dest; } + return strcpy(dest, src); +} +char* bux_strcat(char* dest, const char* src) { + if (!dest) return NULL; + if (!src) return dest; + return strcat(dest, src); +} +char* bux_strncpy(char* dest, const char* src, unsigned int n) { + if (!dest) return NULL; + if (!src) { if (n) dest[0] = 0; return dest; } + return strncpy(dest, src, (size_t)n); +} +double bux_str_to_float(const char* s) { return s ? atof(s) : 0.0; } +int64_t bux_str_to_int(const char* s) { return s ? (int64_t)atoll(s) : 0; } +const char* bux_strstr(const char* haystack, const char* needle) { + if (!haystack || !needle) return NULL; + return strstr(haystack, needle); +} +unsigned int bux_str_offset(const char* pos, const char* base) { + if (!pos || !base) return 0; + return (unsigned int)(pos - base); +} +int bux_str_contains(const char* haystack, const char* needle) { + if (!haystack || !needle) return 0; + return strstr(haystack, needle) != NULL; +} +int bux_str_is_null(const char* s) { return s == NULL; } + +char* bux_str_slice(const char* s, unsigned int start, unsigned int len) { + if (!s) s = ""; + unsigned int sl = (unsigned int)strlen(s); + if (start > sl) start = sl; + if (start + len > sl) len = sl - start; + char* out = (char*)bux_alloc(len + 1); + memcpy(out, s + start, len); + out[len] = 0; + return out; +} +static int is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} +char* bux_str_trim_left(const char* s) { + if (!s) s = ""; + while (*s && is_ws(*s)) s++; + unsigned int n = (unsigned int)strlen(s); + char* out = (char*)bux_alloc(n + 1); + memcpy(out, s, n + 1); + return out; +} +char* bux_str_trim_right(const char* s) { + if (!s) s = ""; + unsigned int n = (unsigned int)strlen(s); + while (n > 0 && is_ws(s[n - 1])) n--; + char* out = (char*)bux_alloc(n + 1); + memcpy(out, s, n); + out[n] = 0; + return out; +} +char* bux_str_trim(const char* s) { + char* a = bux_str_trim_left(s); + char* b = bux_str_trim_right(a); + bux_free(a); + return b; +} +char* bux_int_to_str(int64_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + unsigned int len = (unsigned int)strlen(buf); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, buf, len + 1); + return out; +} +char* bux_float_to_string(double f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", f); + unsigned int len = (unsigned int)strlen(buf); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, buf, len + 1); + return out; +} +unsigned int bux_str_split_count(const char* s, const char* delim) { + if (!s || !delim || !delim[0]) return 0; + unsigned int count = 1; + const char* p = s; + size_t dlen = strlen(delim); + while ((p = strstr(p, delim)) != NULL) { + count++; + p += dlen; + } + return count; +} +char* bux_str_split_part(const char* s, const char* delim, unsigned int index) { + if (!s || !delim) return (char*)bux_alloc(1); + size_t dlen = strlen(delim); + const char* start = s; + unsigned int i = 0; + while (i < index) { + const char* p = strstr(start, delim); + if (!p) return (char*)bux_alloc(1); + start = p + dlen; + i++; + } + const char* end = strstr(start, delim); + size_t len = end ? (size_t)(end - start) : strlen(start); + char* out = (char*)bux_alloc(len + 1); + memcpy(out, start, len); + out[len] = 0; + return out; +} +char* bux_str_join2(const char* a, const char* b, const char* sep) { + if (!a) a = ""; if (!b) b = ""; if (!sep) sep = ""; + size_t la = strlen(a), lb = strlen(b), ls = strlen(sep); + char* out = (char*)bux_alloc(la + ls + lb + 1); + memcpy(out, a, la); + memcpy(out + la, sep, ls); + memcpy(out + la + ls, b, lb + 1); + return out; +} +char* bux_str_format(const char* fmt, const char* a0, const char* a1, const char* a2, const char* a3) { + /* Minimal: return copy of fmt (full formatter is Unix runtime only) */ + (void)a0; (void)a1; (void)a2; (void)a3; + if (!fmt) fmt = ""; + size_t n = strlen(fmt); + char* out = (char*)bux_alloc(n + 1); + memcpy(out, fmt, n + 1); + return out; +} +char* bux_escape_c_string(const char* s, int len) { + if (!s || len <= 0) { + char* e = (char*)bux_alloc(1); + e[0] = 0; + return e; + } + char* buf = (char*)bux_alloc((size_t)len * 2 + 1); + int j = 0; + for (int i = 0; i < len; i++) { + char c = s[i]; + switch (c) { + case '\n': buf[j++] = '\\'; buf[j++] = 'n'; break; + case '\r': buf[j++] = '\\'; buf[j++] = 'r'; break; + case '\t': buf[j++] = '\\'; buf[j++] = 't'; break; + case '\\': buf[j++] = '\\'; buf[j++] = '\\'; break; + case '"': buf[j++] = '\\'; buf[j++] = '"'; break; + default: buf[j++] = c; break; + } + } + buf[j] = 0; + return buf; +} + +/* ── String builder ───────────────────────────────────────────────────── */ +typedef struct { + char* data; + unsigned int len; + unsigned int cap; +} BuxStringBuilder; + +BuxStringBuilder* bux_sb_new(unsigned int initial_cap) { + if (initial_cap < 16) initial_cap = 16; + BuxStringBuilder* sb = (BuxStringBuilder*)bux_alloc(sizeof(BuxStringBuilder)); + sb->data = (char*)bux_alloc(initial_cap); + sb->data[0] = 0; + sb->len = 0; + sb->cap = initial_cap; + return sb; +} +static void sb_ensure(BuxStringBuilder* sb, unsigned int need) { + if (sb->len + need + 1 <= sb->cap) return; + unsigned int ncap = sb->cap * 2; + while (ncap < sb->len + need + 1) ncap *= 2; + sb->data = (char*)bux_realloc(sb->data, ncap); + sb->cap = ncap; +} +void bux_sb_append(BuxStringBuilder* sb, const char* s) { + if (!sb || !s) return; + unsigned int n = (unsigned int)strlen(s); + sb_ensure(sb, n); + memcpy(sb->data + sb->len, s, n + 1); + sb->len += n; +} +void bux_sb_append_int(BuxStringBuilder* sb, int64_t n) { + char* t = bux_int_to_str(n); + bux_sb_append(sb, t); + bux_free(t); +} +void bux_sb_append_float(BuxStringBuilder* sb, double f) { + char* t = bux_float_to_string(f); + bux_sb_append(sb, t); + bux_free(t); +} +void bux_sb_append_char(BuxStringBuilder* sb, char c) { + if (!sb) return; + sb_ensure(sb, 1); + sb->data[sb->len++] = c; + sb->data[sb->len] = 0; +} +const char* bux_sb_build(BuxStringBuilder* sb) { return sb ? sb->data : ""; } +void bux_sb_free(BuxStringBuilder* sb) { + if (!sb) return; + bux_free(sb->data); + bux_free(sb); +} + +/* ── Files / paths ────────────────────────────────────────────────────── */ +char* bux_read_file(const char* path) { + if (!path) return NULL; + FILE* f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz < 0) { fclose(f); return NULL; } + char* buf = (char*)bux_alloc((size_t)sz + 1); + size_t n = fread(buf, 1, (size_t)sz, f); + buf[n] = 0; + fclose(f); + return buf; +} +int bux_write_file(const char* path, const char* content) { + if (!path) return 0; + FILE* f = fopen(path, "wb"); + if (!f) return 0; + if (content) fputs(content, f); + fclose(f); + return 1; +} +int bux_file_exists(const char* path) { + if (!path) return 0; + FILE* f = fopen(path, "rb"); + if (!f) return 0; + fclose(f); + return 1; +} +char* bux_path_join(const char* a, const char* b) { + if (!a && !b) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + if (!a) { + size_t n = strlen(b); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, b, n + 1); + return r; + } + if (!b) { + size_t n = strlen(a); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, a, n + 1); + return r; + } + size_t la = strlen(a), lb = strlen(b); + int need = (la > 0 && a[la-1] != '/' && a[la-1] != '\\') ? 1 : 0; + char* r = (char*)bux_alloc(la + need + lb + 1); + memcpy(r, a, la); + if (need) r[la] = '/'; + memcpy(r + la + need, b, lb + 1); + return r; +} +char* bux_path_parent(const char* path) { + if (!path) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + int len = (int)strlen(path); + while (len > 0 && (path[len-1] == '/' || path[len-1] == '\\')) len--; + while (len > 0 && path[len-1] != '/' && path[len-1] != '\\') len--; + while (len > 0 && (path[len-1] == '/' || path[len-1] == '\\')) len--; + if (len == 0) { + char* d = (char*)bux_alloc(2); + d[0] = '.'; d[1] = 0; + return d; + } + char* r = (char*)bux_alloc((size_t)len + 1); + memcpy(r, path, (size_t)len); + r[len] = 0; + return r; +} +char* bux_path_ext(const char* path) { + if (!path) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + const char* dot = strrchr(path, '.'); + if (!dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + const char* slash = strrchr(path, '/'); + const char* bslash = strrchr(path, '\\'); + if (slash && slash > dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + if (bslash && bslash > dot) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + size_t n = strlen(dot); + char* r = (char*)bux_alloc(n + 1); + memcpy(r, dot, n + 1); + return r; +} +int bux_mkdir_if_needed(const char* path) { + if (!path) return -1; + return bux_mkdir_one(path); +} +int bux_dir_exists(const char* path) { + if (!path) return 0; +#if BUX_IS_WIN + DWORD attr = GetFileAttributesA(path); + return (attr != INVALID_FILE_ATTRIBUTES) && (attr & FILE_ATTRIBUTE_DIRECTORY); +#else + struct stat st; + return (stat(path, &st) == 0 && S_ISDIR(st.st_mode)); +#endif +} +char** bux_list_dir(const char* dir, const char* ext, int* out_count) { + (void)dir; (void)ext; + if (out_count) *out_count = 0; + return NULL; /* stub: recursive listing not ported */ +} + +/* ── Math / hash ──────────────────────────────────────────────────────── */ +double bux_sqrt(double x) { return sqrt(x); } +double bux_pow(double x, double y) { return pow(x, y); } +int64_t bux_abs_i64(int64_t x) { return x < 0 ? -x : x; } +double bux_abs_f64(double x) { return x < 0 ? -x : x; } +int64_t bux_min_i64(int64_t a, int64_t b) { return a < b ? a : b; } +int64_t bux_max_i64(int64_t a, int64_t b) { return a > b ? a : b; } +double bux_min_f64(double a, double b) { return a < b ? a : b; } +double bux_max_f64(double a, double b) { return a > b ? a : b; } +unsigned int bux_hash_bytes(const void* ptr, size_t size) { + if (!ptr) return 0; + unsigned int hash = 5381; + const unsigned char* b = (const unsigned char*)ptr; + for (size_t i = 0; i < size; i++) hash = ((hash << 5) + hash) + b[i]; + return hash; +} +int bux_mem_eq(const void* a, const void* b, size_t size) { + if (a == b) return 1; + if (!a || !b) return 0; + return memcmp(a, b, size) == 0; +} +unsigned int bux_hash_string(const char* s) { + return bux_hash_bytes(s, s ? strlen(s) : 0); +} + +/* ── OS env / cwd ─────────────────────────────────────────────────────── */ +const char* bux_getenv(const char* name) { + if (!name) return ""; + const char* v = getenv(name); + return v ? v : ""; +} +const char* bux_cc_ld_stable(void) { return ""; } +int bux_setenv(const char* name, const char* value) { + if (!name || !value) return -1; +#if BUX_IS_WIN + return _putenv_s(name, value) == 0 ? 0 : -1; +#else + return setenv(name, value, 1); +#endif +} +const char* bux_getcwd(void) { + static char buf[4096]; +#if BUX_IS_WIN + if (_getcwd(buf, (int)sizeof(buf))) return buf; +#else + if (getcwd(buf, sizeof(buf))) return buf; +#endif + return ""; +} +int bux_chdir(const char* path) { + if (!path) return -1; +#if BUX_IS_WIN + return _chdir(path); +#else + return chdir(path); +#endif +} + +/* ── Time ─────────────────────────────────────────────────────────────── */ +/* Stop handlers — no-op stubs (no multi-threaded server on thin runtime) */ +void bux_install_stop_handlers(void) {} +int bux_should_stop(void) { return 0; } +void bux_set_stop_listen_fd(int fd) { (void)fd; } + +int64_t bux_time_ms(void) { +#if BUX_IS_WIN + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER u; + u.LowPart = ft.dwLowDateTime; + u.HighPart = ft.dwHighDateTime; + /* 100-ns intervals since 1601 → ms since Unix epoch */ + return (int64_t)((u.QuadPart / 10000ULL) - 11644473600000ULL); +#else + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + return (int64_t)time(NULL) * 1000; +#endif +} +int64_t bux_time_us(void) { return bux_time_ms() * 1000; } +void bux_sleep_ms(int64_t ms) { + if (ms <= 0) return; +#if BUX_IS_WIN + Sleep((DWORD)ms); +#else + struct timespec ts; + ts.tv_sec = (time_t)(ms / 1000); + ts.tv_nsec = (long)((ms % 1000) * 1000000); + nanosleep(&ts, NULL); +#endif +} + +/* ── Process ──────────────────────────────────────────────────────────── */ +int bux_system(const char* cmd) { return cmd ? system(cmd) : -1; } +int bux_run_nim(const char* nim_file, const char* out_bin) { + char cmd[4096]; + snprintf(cmd, sizeof(cmd), "nim c -o:%s -d:release --gc:orc %s 2>&1", + out_bin ? out_bin : "a.out", nim_file ? nim_file : ""); + return system(cmd); +} +int bux_process_run(const char* cmd) { return bux_system(cmd); } +char* bux_process_output(const char* cmd) { + (void)cmd; + return NULL; /* popen portability varies; stub on minimal runtime */ +} + +/* ── Tasks / channels / mutex / async — stubs ─────────────────────────── */ +void bux_task_init(int num_workers) { (void)num_workers; } +void bux_task_shutdown(void) {} +void* bux_task_spawn(void* (*func)(void*), void* arg) { + (void)func; (void)arg; + return NULL; +} +void bux_task_join(void* handle) { (void)handle; } +void bux_task_sleep(int64_t ms) { bux_sleep_ms(ms); } +void bux_task_yield(void) {} +int bux_task_current_id(void) { return 0; } + +void* bux_channel_new(int64_t capacity, int64_t elem_size) { + (void)capacity; (void)elem_size; + return NULL; +} +void bux_channel_send(void* handle, void* elem) { (void)handle; (void)elem; } +int bux_channel_recv(void* handle, void* out) { (void)handle; (void)out; return 0; } +void bux_channel_close(void* handle) { (void)handle; } +void bux_channel_free(void* handle) { (void)handle; } + +void* bux_mutex_new(void) { return bux_alloc(1); } +void bux_mutex_lock(void* handle) { (void)handle; } +void bux_mutex_unlock(void* handle) { (void)handle; } +void bux_mutex_free(void* handle) { bux_free(handle); } +void* bux_rwlock_new(void) { return bux_alloc(1); } +void bux_rwlock_rdlock(void* handle) { (void)handle; } +void bux_rwlock_wrlock(void* handle) { (void)handle; } +void bux_rwlock_unlock(void* handle) { (void)handle; } +void bux_rwlock_free(void* handle) { bux_free(handle); } + +void* bux_async_spawn(void (*func)(void)) { (void)func; return NULL; } +void bux_async_yield(void) {} +void bux_async_run(void) {} +void* bux_async_await(void* handle) { (void)handle; return NULL; } +void bux_async_sleep(int64_t ms) { bux_sleep_ms(ms); } +void bux_async_return(void* value, size_t size) { (void)value; (void)size; } +void* bux_async_result(void* handle) { (void)handle; return NULL; } + +/* ── Sockets — stubs ──────────────────────────────────────────────────── */ +int bux_socket_create(void) { return -1; } +int bux_socket_reuse(int fd) { (void)fd; return -1; } +int bux_socket_bind(int fd, const char* addr, int port) { + (void)fd; (void)addr; (void)port; return -1; +} +int bux_socket_listen(int fd, int backlog) { (void)fd; (void)backlog; return -1; } +int bux_socket_accept(int fd) { (void)fd; return -1; } +int bux_socket_connect(int fd, const char* addr, int port) { + (void)fd; (void)addr; (void)port; return -1; +} +int bux_socket_send(int fd, const char* data, int len) { + (void)fd; (void)data; (void)len; return -1; +} +/* BuxString used by full runtime; provide a simple struct-compatible layout */ +typedef struct { char* data; int len; } BuxString; +BuxString bux_socket_recv(int fd, int max_len) { + (void)fd; (void)max_len; + BuxString s; s.data = NULL; s.len = 0; return s; +} +int bux_socket_close(int fd) { (void)fd; return -1; } +const char* bux_socket_error(void) { return "sockets not available on this platform"; } + +/* TLS stubs (thin runtime has no OpenSSL) */ +void* bux_tls_server_ctx_ex(const char* cert, const char* key, const char* ca) { + (void)cert; (void)key; (void)ca; return NULL; +} +void* bux_tls_server_ctx(const char* cert, const char* key) { + return bux_tls_server_ctx_ex(cert, key, NULL); +} +void bux_tls_ctx_free(void* ctx) { (void)ctx; } +void* bux_tls_accept(void* ctx, int fd) { (void)ctx; (void)fd; return NULL; } +int bux_tls_send(void* ssl, const char* data, int len) { + (void)ssl; (void)data; (void)len; return -1; +} +BuxString bux_tls_recv(void* ssl, int max_len) { + (void)ssl; (void)max_len; + BuxString s; s.data = NULL; s.len = 0; return s; +} +void bux_tls_close(void* ssl) { (void)ssl; } +const char* bux_tls_error(void) { return "tls not available on thin runtime"; } + +/* ── Crypto — stubs (no OpenSSL) ──────────────────────────────────────── */ +static void zero_out(unsigned char* out, int n) { + if (out && n > 0) memset(out, 0, (size_t)n); +} +void bux_sha1(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 20); +} +void bux_sha256(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 32); +} +void bux_sha384(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 48); +} +void bux_sha512(const char* data, int len, unsigned char* out) { + (void)data; (void)len; zero_out(out, 64); +} +void bux_hmac_sha256(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 32); +} +void bux_hmac_sha384(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 48); +} +void bux_hmac_sha512(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + (void)key; (void)keylen; (void)msg; (void)msglen; zero_out(out, 64); +} +int bux_random_bytes(unsigned char* buf, int len) { + if (!buf || len <= 0) return 0; +#if BUX_IS_WIN + /* Best-effort: not cryptographically strong */ + for (int i = 0; i < len; i++) buf[i] = (unsigned char)(rand() & 0xFF); + return 1; +#else + for (int i = 0; i < len; i++) buf[i] = (unsigned char)(rand() & 0xFF); + return 1; +#endif +} +char* bux_base64_encode(const unsigned char* in, int inlen) { + (void)in; (void)inlen; + char* o = (char*)bux_alloc(1); o[0] = 0; return o; +} +char* bux_base64_decode(const char* in, int inlen, int* outlen) { + (void)in; (void)inlen; + if (outlen) *outlen = 0; + return (char*)bux_alloc(1); +} +char* bux_base64url_encode(const unsigned char* in, int inlen) { + return bux_base64_encode(in, inlen); +} +char* bux_base64url_decode(const char* in, int inlen, int* outlen) { + return bux_base64_decode(in, inlen, outlen); +} +char* bux_bytes_to_hex(const unsigned char* data, int len) { + if (!data || len <= 0) { char* e = (char*)bux_alloc(1); e[0]=0; return e; } + char* out = (char*)bux_alloc((size_t)len * 2 + 1); + static const char* hex = "0123456789abcdef"; + for (int i = 0; i < len; i++) { + out[i*2] = hex[(data[i] >> 4) & 0xF]; + out[i*2+1] = hex[data[i] & 0xF]; + } + out[len*2] = 0; + return out; +} +int bux_aes_256_cbc_encrypt(const unsigned char* key, const unsigned char* iv, + const char* in, int inlen, unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)in; (void)inlen; (void)out; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_cbc_decrypt(const unsigned char* key, const unsigned char* iv, + const char* in, int inlen, unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)in; (void)inlen; (void)out; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_gcm_encrypt(const unsigned char* key, const unsigned char* iv, int ivlen, + const char* in, int inlen, unsigned char* out, int* outlen, + unsigned char* tag) { + (void)key; (void)iv; (void)ivlen; (void)in; (void)inlen; (void)out; (void)tag; + if (outlen) *outlen = 0; + return 0; +} +int bux_aes_256_gcm_decrypt(const unsigned char* key, const unsigned char* iv, int ivlen, + const char* in, int inlen, const unsigned char* tag, + unsigned char* out, int* outlen) { + (void)key; (void)iv; (void)ivlen; (void)in; (void)inlen; (void)tag; (void)out; + if (outlen) *outlen = 0; + return 0; +} +char* bux_rsa_sign_sha256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_rsa_sign_sha384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_rsa_sign_sha512(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +int bux_rsa_verify_sha256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_rsa_verify_sha384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_rsa_verify_sha512(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +char* bux_ecdsa_sign_p256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +char* bux_ecdsa_sign_p384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; + if (siglen) *siglen = 0; return NULL; +} +int bux_ecdsa_verify_p256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_ecdsa_verify_p384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + (void)pem; (void)keylen; (void)data; (void)datalen; (void)sig; (void)siglen; + return 0; +} +int bux_ed25519_keypair(unsigned char* pub, unsigned char* priv) { + zero_out(pub, 32); zero_out(priv, 32); return 0; +} +int bux_ed25519_sign(const char* priv, const char* data, int datalen, unsigned char* sig) { + (void)priv; (void)data; (void)datalen; zero_out(sig, 64); return 0; +} +int bux_ed25519_verify(const char* pub, const char* sig, const char* data, int datalen) { + (void)pub; (void)sig; (void)data; (void)datalen; return 0; +} + +/* Legacy string helpers used by some mono paths */ +typedef struct { char* data; int len; } BuxStringLegacy; +BuxStringLegacy bux_string_from_cstr(const char* s) { + BuxStringLegacy r; + r.data = (char*)(s ? s : ""); + r.len = s ? (int)strlen(s) : 0; + return r; +} +BuxStringLegacy bux_string_concat(BuxStringLegacy a, BuxStringLegacy b) { + (void)a; (void)b; + BuxStringLegacy r; r.data = ""; r.len = 0; return r; +} diff --git a/rt/runtime_win.c b/rt/runtime_win.c index 1de7e07..d1087d6 100644 --- a/rt/runtime_win.c +++ b/rt/runtime_win.c @@ -471,6 +471,10 @@ int bux_chdir(const char* path) { } /* ── Time ─────────────────────────────────────────────────────────────── */ +void bux_install_stop_handlers(void) {} +int bux_should_stop(void) { return 0; } +void bux_set_stop_listen_fd(int fd) { (void)fd; } + int64_t bux_time_ms(void) { #if BUX_IS_WIN FILETIME ft; @@ -576,6 +580,24 @@ BuxString bux_socket_recv(int fd, int max_len) { int bux_socket_close(int fd) { (void)fd; return -1; } const char* bux_socket_error(void) { return "sockets not available on this platform"; } +void* bux_tls_server_ctx_ex(const char* cert, const char* key, const char* ca) { + (void)cert; (void)key; (void)ca; return NULL; +} +void* bux_tls_server_ctx(const char* cert, const char* key) { + return bux_tls_server_ctx_ex(cert, key, NULL); +} +void bux_tls_ctx_free(void* ctx) { (void)ctx; } +void* bux_tls_accept(void* ctx, int fd) { (void)ctx; (void)fd; return NULL; } +int bux_tls_send(void* ssl, const char* data, int len) { + (void)ssl; (void)data; (void)len; return -1; +} +BuxString bux_tls_recv(void* ssl, int max_len) { + (void)ssl; (void)max_len; + BuxString s; s.data = NULL; s.len = 0; return s; +} +void bux_tls_close(void* ssl) { (void)ssl; } +const char* bux_tls_error(void) { return "tls not available on this platform"; } + /* ── Crypto — stubs (no OpenSSL) ──────────────────────────────────────── */ static void zero_out(unsigned char* out, int n) { if (out && n > 0) memset(out, 0, (size_t)n); diff --git a/src/c_backend.bux b/src/c_backend.bux index 0988327..9bff009 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -514,8 +514,174 @@ module CBackend { CBE_MarkMovedFromNodeHint(cbe, node, ""); } + /// Find a same-module function by name (for cross-fn ownership, session 77). + func CBE_FindFuncByName(cbe: *CEmitter, name: String) -> *HirFunc { + if cbe == null as *CEmitter || cbe.mod == null as *HirModule { return null as *HirFunc; } + if name == null as String || String_Eq(name, "") { return null as *HirFunc; } + var i: int = 0; + while i < cbe.mod.funcCount { + if String_Eq(cbe.mod.funcs[i].name, name) { + return &cbe.mod.funcs[i]; + } + i = i + 1; + } + return null as *HirFunc; + } + + /// If `node` is `&local` (or load of that), return owner local (alias-resolved). + func CBE_ArgAmpOwner(cbe: *CEmitter, node: *HirNode) -> String { + if node == null as *HirNode { return ""; } + var n: *HirNode = node; + if n.kind == hLoad { n = n.child1; } + if n == null as *HirNode { return ""; } + if n.kind == hUnary && n.intValue == tkAmp { + let op: *HirNode = n.child1; + if op != null as *HirNode && op.kind == hVar { + return CBE_ResolvePtrAlias(cbe, op.strValue); + } + } + // bare pointer local that aliases an owner + if n.kind == hVar { + let owner: String = CBE_ResolvePtrAlias(cbe, n.strValue); + if !String_Eq(owner, n.strValue) { + return owner; + } + } + return ""; + } + + /// True if C type name looks like a pointer (`Bag*` / `*Bag`). + func CBE_TypeNameIsPointer(tn: String) -> bool { + if tn == null as String || String_Eq(tn, "") { return false; } + if String_Contains(tn, "*") { return true; } + return false; + } + + /// If HIR expr is a field path rooted at `param`, mark `owner` partial-moved. + func CBE_ApplyIfParamFieldMove(cbe: *CEmitter, expr: *HirNode, param: String, owner: String) { + if expr == null as *HirNode { return; } + if String_Eq(param, "") || String_Eq(owner, "") { return; } + // Whole pointee: *param + if expr.kind == hUnary && expr.intValue == tkStar { + let op: *HirNode = expr.child1; + if op != null as *HirNode && op.kind == hVar && String_Eq(op.strValue, param) { + CBE_AddMoved(cbe, owner); + return; + } + } + if expr.kind == hFieldAccess || expr.kind == hFieldPtr || expr.kind == hArrowField { + let base: String = CBE_BaseVarNameRaw(expr); + if String_Eq(base, param) { + let path: String = CBE_FieldPathFromNode(expr); + if !String_Eq(path, "") { + CBE_AddPartialMoved(cbe, owner, path); + CBE_AddMoved(cbe, owner); + } + } + return; + } + if expr.kind == hLoad { + CBE_ApplyIfParamFieldMove(cbe, expr.child1, param, owner); + } + } + + /// Walk callee HIR body; when param fields are moved, mark call-site `owner`. + func CBE_ScanBodyParamMoves(cbe: *CEmitter, node: *HirNode, param: String, owner: String) { + if node == null as *HirNode { return; } + let kind: int = node.kind; + if kind == hReturn { + CBE_ApplyIfParamFieldMove(cbe, node.child1, param, owner); + return; + } + if kind == hStore { + CBE_ApplyIfParamFieldMove(cbe, node.child2, param, owner); + // still walk both sides for nested + CBE_ScanBodyParamMoves(cbe, node.child1, param, owner); + CBE_ScanBodyParamMoves(cbe, node.child2, param, owner); + return; + } + if kind == hBlock { + // Stmts linked via child3 starting at child1 (selfhost HIR convention). + var s: *HirNode = node.child1; + while s != null as *HirNode { + CBE_ScanBodyParamMoves(cbe, s, param, owner); + s = s.child3; + } + return; + } + if kind == hIf { + // cond, then, else + CBE_ScanBodyParamMoves(cbe, node.child1, param, owner); + CBE_ScanBodyParamMoves(cbe, node.child2, param, owner); + CBE_ScanBodyParamMoves(cbe, node.child3, param, owner); + return; + } + if kind == hWhile || kind == hLoop { + CBE_ScanBodyParamMoves(cbe, node.child1, param, owner); + CBE_ScanBodyParamMoves(cbe, node.child2, param, owner); + return; + } + // Do not walk child3 generically — it is often the *next sibling* in a + // statement list (handled by hBlock). Only walk expression children. + CBE_ScanBodyParamMoves(cbe, node.child1, param, owner); + CBE_ScanBodyParamMoves(cbe, node.child2, param, owner); + } + + /// Session 77: `TakeItems(&bag)` — scan callee body for moves of `p.field`. + func CBE_MarkCrossFuncFromCall(cbe: *CEmitter, call: *HirNode) { + if call == null as *HirNode || call.kind != hCall { return; } + let fname: String = call.strValue; + if String_Eq(fname, "") { return; } + let f: *HirFunc = CBE_FindFuncByName(cbe, fname); + if f == null as *HirFunc || f.body == null as *HirNode { return; } + // Collect args: child1, child2, then extraData list + var argIdx: int = 0; + var arg: *HirNode = call.child1; + while argIdx < f.paramCount { + if arg == null as *HirNode { + // try child2 for arg1 + if argIdx == 1 { arg = call.child2; } + } + if arg == null as *HirNode && argIdx >= 2 { + break; + } + // For args beyond 2, walk extraData + if argIdx >= 2 { + var ai: int = 0; + var cur: *HirArgList = call.extraData as *HirArgList; + while ai < call.extraCount && cur != null as *HirArgList { + if ai == argIdx - 2 { + arg = cur.node; + break; + } + cur = cur.next; + ai = ai + 1; + } + } + if arg == null as *HirNode { break; } + let hp: *HirParam = CBE_FuncParam(f, argIdx); + if hp != null as *HirParam && CBE_TypeNameIsPointer(hp.typeName) { + let owner: String = CBE_ArgAmpOwner(cbe, arg); + if !String_Eq(owner, "") { + CBE_ScanBodyParamMoves(cbe, f.body, hp.name, owner); + } + } + // advance arg for next + if argIdx == 0 { + arg = call.child2; + } else { + arg = null as *HirNode; + } + argIdx = argIdx + 1; + } + } + func CBE_MarkMovedFromNodeHint(cbe: *CEmitter, node: *HirNode, valueTypeHint: String) { if node == null as *HirNode { return; } + if node.kind == hCall { + CBE_MarkCrossFuncFromCall(cbe, node); + return; + } if node.kind == hVar { CBE_AddMoved(cbe, node.strValue); return; diff --git a/src/cli.bux b/src/cli.bux index 55811d2..cdee1ae 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -15,6 +15,7 @@ module Cli { extern func bux_run_nim(nim_file: String, out_bin: String) -> int; extern func bux_list_dir(dir: String, ext: String, out_count: *int) -> *String; extern func bux_system(cmd: String) -> int; + extern func bux_process_output(cmd: String) -> String; extern func bux_getenv(name: String) -> String; extern func bux_setenv(name: String, value: String) -> int; extern func bux_cc_ld_stable() -> String; @@ -37,6 +38,175 @@ module Cli { return bux_dir_exists(path) != 0; } + // --------------------------------------------------------------------------- + // Link / runtime helpers (session 76 — selfhost parity with bootstrap 75) + // --------------------------------------------------------------------------- + + func Cli_EnvTruthy(name: String) -> bool { + let v: String = bux_getenv(name); + if v == null as String { return false; } + if String_Eq(v, "1") { return true; } + if String_Eq(v, "true") { return true; } + if String_Eq(v, "yes") { return true; } + if String_Eq(v, "on") { return true; } + return false; + } + + func Cli_RuntimeRel(isStatic: bool, targetTriple: String) -> String { + let env: String = bux_getenv("BUX_RUNTIME"); + if env != null as String { + if String_Eq(env, "win") || String_Eq(env, "windows") { + return "rt/runtime_win.c"; + } + if String_Eq(env, "minimal") || String_Eq(env, "thin") || + String_Eq(env, "embed") || String_Eq(env, "embedded") || + String_Eq(env, "freestanding") { + return "rt/runtime_minimal.c"; + } + if String_Eq(env, "full") || String_Eq(env, "posix") { + return "rt/runtime.c"; + } + } + if isStatic { + return "rt/runtime_minimal.c"; + } + if targetTriple != null as String && !String_Eq(targetTriple, "") { + return "rt/runtime_minimal.c"; + } + return "rt/runtime.c"; + } + + func Cli_IsThinRuntimePath(rtPath: String) -> bool { + if String_Contains(rtPath, "runtime_minimal.c") { return true; } + if String_Contains(rtPath, "runtime_win.c") { return true; } + return false; + } + + func Cli_FindRtFile(projectDir: String, rel: String) -> String { + // Try projectDir/rel, then parent, then grandparent (selfhost build dirs). + var p: String = bux_path_join(projectDir, rel); + if FileExists(p) { return p; } + p = bux_path_join(projectDir, String_Concat("../", rel)); + if FileExists(p) { return p; } + p = bux_path_join(projectDir, String_Concat("../../", rel)); + if FileExists(p) { return p; } + // Next to stdlib: $BUX_STDLIB/../rt/... + let stdlib: String = bux_getenv("BUX_STDLIB"); + if stdlib != null as String && !String_Eq(stdlib, "") { + p = bux_path_join(bux_path_parent(stdlib), rel); + if FileExists(p) { return p; } + } + // Also try finding lib/ next to project to infer repo root + let libBeside: String = Cli_FindStdlibDir(projectDir); + if !String_Eq(libBeside, "") { + p = bux_path_join(bux_path_parent(libBeside), rel); + if FileExists(p) { return p; } + } + // cwd-relative (single-file builds) + if FileExists(rel) { return rel; } + p = String_Concat("../", rel); + if FileExists(p) { return p; } + p = String_Concat("../../", rel); + if FileExists(p) { return p; } + return ""; + } + + func Cli_PickCc(targetTriple: String) -> String { + let envCc: String = bux_getenv("BUX_CC"); + if envCc != null as String && !String_Eq(envCc, "") { + return envCc; + } + if targetTriple != null as String && !String_Eq(targetTriple, "") { + let tripleGcc: String = String_Concat(targetTriple, "-gcc"); + let probe: String = String_Concat("command -v ", tripleGcc); + probe = String_Concat(probe, " >/dev/null 2>&1"); + if bux_system(probe) == 0 { + return tripleGcc; + } + let probeClang: String = "command -v clang >/dev/null 2>&1"; + if bux_system(probeClang) == 0 { + return "clang"; + } + return tripleGcc; + } + return "cc"; + } + + func Cli_CcNeedsTargetFlag(ccBin: String) -> bool { + // clang needs -target; *-gcc is already a cross binary. + if String_Eq(ccBin, "clang") { return true; } + if String_Contains(ccBin, "clang-") { return true; } + if String_Contains(ccBin, "/clang") { return true; } + return false; + } + + func Cli_LinkProgram(cFile: String, outBin: String, projectDir: String, + targetTriple: String, isRelease: bool, isStatic: bool) -> int { + let relRt: String = Cli_RuntimeRel(isStatic, targetTriple); + let rtPath: String = Cli_FindRtFile(projectDir, relRt); + let ioPath: String = Cli_FindRtFile(projectDir, "rt/io.c"); + if String_Eq(rtPath, "") { + Print("Error: runtime not found: "); + PrintLine(relRt); + return 1; + } + if String_Eq(ioPath, "") { + PrintLine("Error: rt/io.c not found"); + return 1; + } + + let thin: bool = Cli_IsThinRuntimePath(rtPath); + var optFlags: String = "-O0 -g"; + if isRelease { + optFlags = "-O2 -DNDEBUG"; + } + let extraCf: String = bux_getenv("BUX_CFLAGS"); + if extraCf != null as String && !String_Eq(extraCf, "") { + optFlags = String_Concat(optFlags, " "); + optFlags = String_Concat(optFlags, extraCf); + } + if isStatic || Cli_EnvTruthy("BUX_STATIC") { + optFlags = String_Concat(optFlags, " -static"); + } + + let ccBin: String = Cli_PickCc(targetTriple); + var cmdBuf: StringBuilder = StringBuilder_NewCap(768); + StringBuilder_Append(&cmdBuf, ccBin); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, optFlags); + if targetTriple != null as String && !String_Eq(targetTriple, "") { + if Cli_CcNeedsTargetFlag(ccBin) { + StringBuilder_Append(&cmdBuf, " -target "); + StringBuilder_Append(&cmdBuf, targetTriple); + } + } + if thin { + StringBuilder_Append(&cmdBuf, " -ffunction-sections -fdata-sections"); + } else { + StringBuilder_Append(&cmdBuf, " -pthread"); + StringBuilder_Append(&cmdBuf, bux_cc_ld_stable()); + } + StringBuilder_Append(&cmdBuf, " -o "); + StringBuilder_Append(&cmdBuf, outBin); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, cFile); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, rtPath); + StringBuilder_Append(&cmdBuf, " "); + StringBuilder_Append(&cmdBuf, ioPath); + if thin { + StringBuilder_Append(&cmdBuf, " -Wl,--gc-sections -lm"); + } else { + StringBuilder_Append(&cmdBuf, " -lm -lssl -lcrypto"); + } + let ccRc: int = bux_system(StringBuilder_Build(&cmdBuf)); + if ccRc != 0 { + PrintLine("Error: C compilation failed"); + return 1; + } + return 0; + } + // --------------------------------------------------------------------------- // Diagnostic formatting (Rust-style errors with snippets) // --------------------------------------------------------------------------- @@ -289,11 +459,11 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St // Build command // --------------------------------------------------------------------------- -func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease: bool) -> int { +func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease: bool, isStatic: bool) -> int { // If building the standard project entry point, use full project build // which merges stdlib and supports multi-file projects if String_Eq(srcPath, "src/Main.bux") || String_Eq(srcPath, "src/main.bux") { - let rc: int = Cli_BuildProject(".", targetTriple, isRelease); + let rc: int = Cli_BuildProject(".", targetTriple, isRelease, isStatic); if rc == 0 && !String_Eq(outPath, "build/main") { // Rename output if custom path was requested bux_system(String_Concat(String_Concat("mv build/main ", outPath), " 2>/dev/null || true")); @@ -328,69 +498,10 @@ func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease Print(" → C written to "); PrintLine(cFile); - // Find runtime.c and io.c for linking (rt/ directory) - var rtPath: String = "rt/runtime.c"; - var ioPath: String = "rt/io.c"; - if !FileExists(rtPath) { - rtPath = "../rt/runtime.c"; - } - if !FileExists(ioPath) { - ioPath = "../rt/io.c"; - } - if !FileExists(rtPath) { - rtPath = "../../rt/runtime.c"; - } - if !FileExists(ioPath) { - ioPath = "../../rt/io.c"; - } - - // Compile with cc or clang for cross-compilation - // Default: -O0 -g (debug, matches bootstrap). --release: -O2 -DNDEBUG. PrintLine("Compiling C..."); - var cmdBuf: StringBuilder = StringBuilder_NewCap(512); - var optFlags: String = "-O0 -g"; - if isRelease { - optFlags = "-O2 -DNDEBUG"; - } - let extraCf: String = bux_getenv("BUX_CFLAGS"); - if extraCf != null as String && !String_Eq(extraCf, "") { - optFlags = String_Concat(optFlags, " "); - optFlags = String_Concat(optFlags, extraCf); - } - if !String_Eq(targetTriple, "") { - StringBuilder_Append(&cmdBuf, "clang "); - StringBuilder_Append(&cmdBuf, optFlags); - StringBuilder_Append(&cmdBuf, " -pthread"); - StringBuilder_Append(&cmdBuf, bux_cc_ld_stable()); - StringBuilder_Append(&cmdBuf, " -target "); - StringBuilder_Append(&cmdBuf, targetTriple); - StringBuilder_Append(&cmdBuf, " "); - } else { - StringBuilder_Append(&cmdBuf, "cc "); - StringBuilder_Append(&cmdBuf, optFlags); - StringBuilder_Append(&cmdBuf, " -pthread"); - StringBuilder_Append(&cmdBuf, bux_cc_ld_stable()); - StringBuilder_Append(&cmdBuf, " "); - } - StringBuilder_Append(&cmdBuf, "-o "); - StringBuilder_Append(&cmdBuf, outPath); - StringBuilder_Append(&cmdBuf, " "); - StringBuilder_Append(&cmdBuf, cFile); - StringBuilder_Append(&cmdBuf, " "); - if FileExists(rtPath) { - StringBuilder_Append(&cmdBuf, rtPath); - StringBuilder_Append(&cmdBuf, " "); - } - if FileExists(ioPath) { - StringBuilder_Append(&cmdBuf, ioPath); - StringBuilder_Append(&cmdBuf, " "); - } - StringBuilder_Append(&cmdBuf, "-lm"); - StringBuilder_Append(&cmdBuf, " -lcrypto"); - let ccRc: int = bux_system(StringBuilder_Build(&cmdBuf)); - if ccRc != 0 { - PrintLine("Error: C compilation failed"); - return 1; + let linkRc: int = Cli_LinkProgram(cFile, outPath, ".", targetTriple, isRelease, isStatic); + if linkRc != 0 { + return linkRc; } Print(" → Binary: "); PrintLine(outPath); @@ -922,6 +1033,40 @@ func Cli_Init() -> int { // Test command — build and run tests // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Registry — search / add by name (session 81) +// --------------------------------------------------------------------------- + +func Cli_Search(query: String) -> int { + let reg: Registry = Reg_FindIndex(); + if String_Eq(reg.path, "") { + if !String_Eq(reg.sourceUrl, "") { + Print("Error: failed to fetch registry from "); + PrintLine(reg.sourceUrl); + PrintLine("hint: curl/wget + network, or BUX_REGISTRY=local file"); + } else { + PrintLine("Error: no package registry (set BUX_REGISTRY or config/registry.toml)"); + } + return 1; + } + if !String_Eq(reg.sourceUrl, "") { + Print("Registry: "); + PrintLine(reg.sourceUrl); + Print(" (cached: "); + Print(reg.path); + PrintLine(")"); + } else { + Print("Registry: "); + PrintLine(reg.path); + } + let hits: int = Reg_Search(reg, query); + if hits == 0 { + PrintLine("No packages matched."); + return 1; + } + return 0; +} + // --------------------------------------------------------------------------- // Add — add a dependency to bux.toml // --------------------------------------------------------------------------- @@ -952,6 +1097,48 @@ func Cli_Add(pkgName: String, url: String) -> int { return 0; } +/// Resolve package from registry index and add as path/git dep. +func Cli_AddFromRegistry(pkgName: String, versionReq: String) -> int { + let reg: Registry = Reg_FindIndex(); + if String_Eq(reg.path, "") { + if !String_Eq(reg.sourceUrl, "") { + Print("Error: failed to fetch registry from "); + PrintLine(reg.sourceUrl); + } else { + PrintLine("Error: no package registry (set BUX_REGISTRY)"); + } + return 1; + } + let pkg: RegistryPackage = Reg_Lookup(reg, pkgName, versionReq); + if String_Eq(pkg.name, "") { + Print("Error: package '"); + Print(pkgName); + PrintLine("' not found in registry"); + PrintLine("hint: buxc search | buxc add name "); + return 1; + } + var url: String = pkg.source; + if !String_Eq(pkg.resolvedPath, "") { + url = pkg.resolvedPath; + } + if !String_Eq(reg.sourceUrl, "") { + Print("Resolved '"); + Print(pkgName); + Print("' "); + Print(pkg.version); + Print(" from registry "); + PrintLine(reg.sourceUrl); + } else { + Print("Resolved '"); + Print(pkgName); + Print("' "); + Print(pkg.version); + Print(" from registry "); + PrintLine(reg.path); + } + return Cli_Add(pkgName, url); +} + // --------------------------------------------------------------------------- // Remove — remove a dependency from bux.toml // --------------------------------------------------------------------------- @@ -982,6 +1169,242 @@ func Cli_Remove(pkgName: String) -> int { return 0; } +// --------------------------------------------------------------------------- +// Package checksum (session 80 — selfhost install --locked parity) +// --------------------------------------------------------------------------- + +func Cli_PackageChecksum(dir: String) -> String { + // Same idea as bootstrap: sha1 of sorted *.bux paths+contents via shell. + if String_Eq(dir, "") || !DirExists(dir) { + return ""; + } + let cmd: String = String_Concat( + "find \"", + String_Concat(dir, "\" -name '*.bux' -type f 2>/dev/null | LC_ALL=C sort | xargs cat 2>/dev/null | sha1sum | awk '{print $1}'") + ); + let out: String = bux_process_output(cmd); + if out == null as String { return ""; } + // trim trailing newline + var s: String = String_Trim(out); + return s; +} + +func Cli_ShellQuote(s: String) -> String { + return String_Concat("\"", String_Concat(s, "\"")); +} + +func Cli_IsAbsPath(p: String) -> bool { + if String_Eq(p, "") { return false; } + if p[0] == 47 as char8 { return true; } // / + return false; +} + +func Cli_DepResolvedPath(projectDir: String, depName: String, depUrl: String) -> String { + // Path dep: absolute or relative; git dep: deps/ + if Cli_IsAbsPath(depUrl) { + return depUrl; + } + if String_StartsWith(depUrl, "http://") || String_StartsWith(depUrl, "https://") || + String_EndsWith(depUrl, ".git") { + return bux_path_join(bux_path_join(projectDir, "deps"), depName); + } + // relative path + return bux_path_join(projectDir, depUrl); +} + +func Cli_InstallLocked(projectDir: String) -> int { + let lockPath: String = bux_path_join(projectDir, "bux.lock"); + if !FileExists(lockPath) { + PrintLine("Error: install --locked: bux.lock missing (run install first)"); + return 1; + } + let content: String = ReadFile(lockPath); + if content == null as String || String_Eq(content, "") { + PrintLine("Error: install --locked: empty lock"); + return 1; + } + // Parse [[Package]] blocks: Name, Version, Source, Checksum + var name: String = ""; + var version: String = ""; + var source: String = ""; + var checksum: String = ""; + var verified: int = 0; + let nlines: uint = String_SplitCount(content, "\n"); + var li: uint = 0; + while li <= nlines { + var line: String = ""; + if li < nlines { + line = String_Trim(String_SplitPart(content, "\n", li)); + } + let isEnd: bool = li == nlines; + let isNew: bool = String_StartsWith(line, "[[Package]]") || String_StartsWith(line, "[[package]]"); + if (isNew || isEnd) && !String_Eq(name, "") { + // verify entry + var path: String = source; + if !Cli_IsAbsPath(path) && !String_StartsWith(path, "http") { + path = bux_path_join(projectDir, source); + } + if String_StartsWith(source, "http://") || String_StartsWith(source, "https://") || + String_EndsWith(source, ".git") { + path = bux_path_join(bux_path_join(projectDir, "deps"), name); + } + if !DirExists(path) { + Print("Error: install --locked: missing "); + Print(name); + Print(" at "); + PrintLine(path); + return 1; + } + if !String_Eq(checksum, "") { + let got: String = Cli_PackageChecksum(path); + if !String_Eq(got, checksum) { + Print("Error: install --locked: checksum mismatch for "); + PrintLine(name); + Print(" lock: "); PrintLine(checksum); + Print(" got: "); PrintLine(got); + return 1; + } + } + Print("locked ok: "); + Print(name); + Print(" "); + PrintLine(version); + verified = verified + 1; + name = ""; + version = ""; + source = ""; + checksum = ""; + } + if isEnd { break; } + if isNew { + li = li + 1; + continue; + } + if String_StartsWith(line, "Name") || String_StartsWith(line, "name") { + // Name = "x" + let parts: uint = String_SplitCount(line, "\""); + if parts >= 2 { + name = String_SplitPart(line, "\"", 1); + } + } else if String_StartsWith(line, "Version") || String_StartsWith(line, "version") { + let parts: uint = String_SplitCount(line, "\""); + if parts >= 2 { + version = String_SplitPart(line, "\"", 1); + } + } else if String_StartsWith(line, "Source") || String_StartsWith(line, "source") { + let parts: uint = String_SplitCount(line, "\""); + if parts >= 2 { + source = String_SplitPart(line, "\"", 1); + } + } else if String_StartsWith(line, "Checksum") || String_StartsWith(line, "checksum") { + let parts: uint = String_SplitCount(line, "\""); + if parts >= 2 { + checksum = String_SplitPart(line, "\"", 1); + } + } + li = li + 1; + } + Print("install --locked: "); + PrintInt(verified as int64); + PrintLine(" package(s) verified"); + return 0; +} + +func Cli_Install(projectDir: String, lockedOnly: bool) -> int { + if lockedOnly { + return Cli_InstallLocked(projectDir); + } + let tomlPath: String = bux_path_join(projectDir, "bux.toml"); + if !FileExists(tomlPath) { + PrintLine("Error: no bux.toml found"); + return 1; + } + let man: Manifest = Manifest_Load(tomlPath); + if man.depCount == 0 { + // empty lock + discard WriteFile(bux_path_join(projectDir, "bux.lock"), ""); + PrintLine("install: no dependencies (empty lock)"); + return 0; + } + var sb: StringBuilder = StringBuilder_NewCap(1024); + var i: int = 0; + while i < man.depCount { + var depName: String = ""; + var depUrl: String = ""; + if i == 0 { depName = man.depName0; depUrl = man.depUrl0; } + else if i == 1 { depName = man.depName1; depUrl = man.depUrl1; } + else if i == 2 { depName = man.depName2; depUrl = man.depUrl2; } + else if i == 3 { depName = man.depName3; depUrl = man.depUrl3; } + else if i == 4 { depName = man.depName4; depUrl = man.depUrl4; } + else if i == 5 { depName = man.depName5; depUrl = man.depUrl5; } + else if i == 6 { depName = man.depName6; depUrl = man.depUrl6; } + else if i == 7 { depName = man.depName7; depUrl = man.depUrl7; } + + // Fetch git deps into deps/ when missing + if String_StartsWith(depUrl, "http://") || String_StartsWith(depUrl, "https://") || + String_EndsWith(depUrl, ".git") { + let depsDir: String = bux_path_join(projectDir, "deps"); + discard bux_mkdir_if_needed(depsDir); + let depPath: String = bux_path_join(depsDir, depName); + if !DirExists(depPath) { + Print("Fetching "); Print(depName); Print(" from "); PrintLine(depUrl); + let cmd: String = String_Concat("git clone --quiet \"", String_Concat(depUrl, String_Concat("\" \"", String_Concat(depPath, "\"")))); + if bux_system(cmd) != 0 { + Print("Error: failed to fetch "); PrintLine(depName); + return 1; + } + } + } + + let path: String = Cli_DepResolvedPath(projectDir, depName, depUrl); + if !DirExists(path) { + Print("Error: dependency path missing: "); + PrintLine(path); + return 1; + } + var ver: String = "0.0.0"; + let depToml: String = bux_path_join(path, "bux.toml"); + if FileExists(depToml) { + let dm: Manifest = Manifest_Load(depToml); + if !String_Eq(dm.version, "") { + ver = dm.version; + } + } + let csum: String = Cli_PackageChecksum(path); + StringBuilder_Append(&sb, "[[Package]]\n"); + StringBuilder_Append(&sb, "Name = \""); + StringBuilder_Append(&sb, depName); + StringBuilder_Append(&sb, "\"\n"); + StringBuilder_Append(&sb, "Version = \""); + StringBuilder_Append(&sb, ver); + StringBuilder_Append(&sb, "\"\n"); + StringBuilder_Append(&sb, "Source = \""); + StringBuilder_Append(&sb, path); + StringBuilder_Append(&sb, "\"\n"); + if !String_Eq(csum, "") { + StringBuilder_Append(&sb, "Checksum = \""); + StringBuilder_Append(&sb, csum); + StringBuilder_Append(&sb, "\"\n"); + } + StringBuilder_Append(&sb, "\n"); + Print("Resolved "); + Print(depName); + Print(" "); + Print(ver); + Print(" → "); + PrintLine(path); + i = i + 1; + } + let lockPath: String = bux_path_join(projectDir, "bux.lock"); + if !WriteFile(lockPath, StringBuilder_Build(&sb)) { + PrintLine("Error: cannot write bux.lock"); + return 1; + } + Print("Generated "); + PrintLine(lockPath); + return 0; +} + // --------------------------------------------------------------------------- // Fetch — download dependencies into deps/ // --------------------------------------------------------------------------- @@ -1302,7 +1725,7 @@ func Cli_Test(projectDir: String, filter: String) -> int { // Without --filter, build and run the project's own Main first. // With --filter, only run matching tests/*.bux files. if String_Eq(filter, "") { - let mainRc: int = Cli_BuildProject(projectDir, "", false); + let mainRc: int = Cli_BuildProject(projectDir, "", false, false); if mainRc != 0 { PrintLine("Main test build failed"); return mainRc; @@ -1419,7 +1842,7 @@ func Cli_Test(projectDir: String, filter: String) -> int { } } - let buildRc: int = Cli_BuildProject(tmpDir, "", false); + let buildRc: int = Cli_BuildProject(tmpDir, "", false, false); if buildRc != 0 { PrintLine("FAIL │"); failed = failed + 1; @@ -1467,7 +1890,7 @@ func Cli_Test(projectDir: String, filter: String) -> int { return 0; } -func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) -> int { +func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool, isStatic: bool) -> int { let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml")); var outName: String = man.name; if String_Eq(outName, "") { @@ -1629,27 +2052,37 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) PrintLine(""); } - // Merge dependency declarations (deps shadow stdlib, user shadows deps) + // Merge dependency declarations (path or deps/; shadow stdlib) if man.depCount > 0 { - let depsDir: String = bux_path_join(projectDir, "deps"); var di: int = 0; while di < man.depCount { var depName: String = ""; - if di == 0 { depName = man.depName0; } - else if di == 1 { depName = man.depName1; } - else if di == 2 { depName = man.depName2; } - else if di == 3 { depName = man.depName3; } - else if di == 4 { depName = man.depName4; } - else if di == 5 { depName = man.depName5; } - else if di == 6 { depName = man.depName6; } - else if di == 7 { depName = man.depName7; } - let depSrcDir: String = bux_path_join(bux_path_join(depsDir, depName), "src"); + var depUrl: String = ""; + if di == 0 { depName = man.depName0; depUrl = man.depUrl0; } + else if di == 1 { depName = man.depName1; depUrl = man.depUrl1; } + else if di == 2 { depName = man.depName2; depUrl = man.depUrl2; } + else if di == 3 { depName = man.depName3; depUrl = man.depUrl3; } + else if di == 4 { depName = man.depName4; depUrl = man.depUrl4; } + else if di == 5 { depName = man.depName5; depUrl = man.depUrl5; } + else if di == 6 { depName = man.depName6; depUrl = man.depUrl6; } + else if di == 7 { depName = man.depName7; depUrl = man.depUrl7; } + // Prefer resolved path (absolute / relative / deps/) + let depRoot: String = Cli_DepResolvedPath(projectDir, depName, depUrl); + var depSrcDir: String = bux_path_join(depRoot, "src"); + if !DirExists(depSrcDir) { + // package root may be the src tree itself + if DirExists(depRoot) { + depSrcDir = depRoot; + } + } if DirExists(depSrcDir) { var depFileCount: int = 0; let depFiles: *String = bux_list_dir(depSrcDir, ".bux", &depFileCount); if depFileCount > 0 { Print("Merging dependency: "); PrintLine(depName); + Print(" from "); + PrintLine(depSrcDir); var dfi: int = 0; while dfi < depFileCount { Print(" Merging "); @@ -1661,6 +2094,9 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) dfi = dfi + 1; } } + } else { + Print("WARN: dependency sources not found for "); + PrintLine(depName); } di = di + 1; } @@ -1742,69 +2178,11 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) Print("C code written to "); PrintLine(cFile); - // Find runtime.c and io.c (look in rt/ directory) - var rtPath: String = bux_path_join(projectDir, "rt/runtime.c"); - var ioPath: String = bux_path_join(projectDir, "rt/io.c"); - if !FileExists(rtPath) { - rtPath = bux_path_join(projectDir, "../rt/runtime.c"); - } - if !FileExists(ioPath) { - ioPath = bux_path_join(projectDir, "../rt/io.c"); - } - if !FileExists(rtPath) { - rtPath = bux_path_join(projectDir, "../../rt/runtime.c"); - } - if !FileExists(ioPath) { - ioPath = bux_path_join(projectDir, "../../rt/io.c"); - } - - // Compile with cc — default debug (-O0 -g); --release → -O2 -DNDEBUG PrintLine("Compiling C..."); let outBin: String = bux_path_join(buildDir, outName); - var ccBuf: StringBuilder = StringBuilder_NewCap(512); - var optFlags2: String = "-O0 -g"; - if isRelease { - optFlags2 = "-O2 -DNDEBUG"; - } - let extraCf2: String = bux_getenv("BUX_CFLAGS"); - if extraCf2 != null as String && !String_Eq(extraCf2, "") { - optFlags2 = String_Concat(optFlags2, " "); - optFlags2 = String_Concat(optFlags2, extraCf2); - } - if !String_Eq(targetTriple, "") { - StringBuilder_Append(&ccBuf, "clang "); - StringBuilder_Append(&ccBuf, optFlags2); - StringBuilder_Append(&ccBuf, " -pthread"); - StringBuilder_Append(&ccBuf, bux_cc_ld_stable()); - StringBuilder_Append(&ccBuf, " -target "); - StringBuilder_Append(&ccBuf, targetTriple); - StringBuilder_Append(&ccBuf, " "); - } else { - StringBuilder_Append(&ccBuf, "cc "); - StringBuilder_Append(&ccBuf, optFlags2); - StringBuilder_Append(&ccBuf, " -pthread"); - StringBuilder_Append(&ccBuf, bux_cc_ld_stable()); - StringBuilder_Append(&ccBuf, " "); - } - StringBuilder_Append(&ccBuf, "-o "); - StringBuilder_Append(&ccBuf, outBin); - StringBuilder_Append(&ccBuf, " "); - StringBuilder_Append(&ccBuf, cFile); - StringBuilder_Append(&ccBuf, " "); - if FileExists(rtPath) { - StringBuilder_Append(&ccBuf, rtPath); - StringBuilder_Append(&ccBuf, " "); - } - if FileExists(ioPath) { - StringBuilder_Append(&ccBuf, ioPath); - StringBuilder_Append(&ccBuf, " "); - } - StringBuilder_Append(&ccBuf, "-lm"); - StringBuilder_Append(&ccBuf, " -lcrypto"); - let ccRc: int = bux_system(StringBuilder_Build(&ccBuf)); - if ccRc != 0 { - PrintLine("Error: C compilation failed"); - return 1; + let linkRc2: int = Cli_LinkProgram(cFile, outBin, projectDir, targetTriple, isRelease, isStatic); + if linkRc2 != 0 { + return linkRc2; } Print("Build successful: "); @@ -1816,8 +2194,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool) // Run command — build project and execute // --------------------------------------------------------------------------- -func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool) -> int { - let rc: int = Cli_BuildProject(projectDir, targetTriple, isRelease); +func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool, isStatic: bool) -> int { + let rc: int = Cli_BuildProject(projectDir, targetTriple, isRelease, isStatic); if rc != 0 { return rc; } @@ -1837,9 +2215,10 @@ func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool) - // --------------------------------------------------------------------------- func Cli_Run(args: *String, argCount: int) -> int { - /* Scan for --target and --release flags before processing command */ + /* Scan for --target / --release / --static before processing command */ var targetTriple: String = ""; var isRelease: bool = false; + var isStatic: bool = Cli_EnvTruthy("BUX_STATIC"); var ai: int = 1; while ai < argCount { if String_Eq(args[ai], "--target") && ai + 1 < argCount { @@ -1854,7 +2233,15 @@ func Cli_Run(args: *String, argCount: int) -> int { ai = ai - 1; } else if String_Eq(args[ai], "--release") { isRelease = true; - /* Remove --release from args by shifting */ + var j: int = ai; + while j + 1 < argCount { + args[j] = args[j + 1]; + j = j + 1; + } + argCount = argCount - 1; + ai = ai - 1; + } else if String_Eq(args[ai], "--static") { + isStatic = true; var j: int = ai; while j + 1 < argCount { args[j] = args[j + 1]; @@ -1869,12 +2256,17 @@ func Cli_Run(args: *String, argCount: int) -> int { if argCount < 2 { PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Usage: buxc [args]"); - PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, doc, test, run, project, help, version"); + PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version"); PrintLine(" test --filter Only run tests/*.bux whose name contains "); + PrintLine(" search [query] Search package registry"); + PrintLine(" install [--locked] Write/verify bux.lock (checksums)"); + PrintLine(" add [ver|url] Registry resolve or explicit source"); PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)"); PrintLine(" doc --out file.md [path] API docs from /// comments"); PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g"); - PrintLine(" BUX_CFLAGS Extra flags appended to cc"); + PrintLine(" --static Fully-static link (minimal runtime)"); + PrintLine(" --target Cross-compile (e.g. aarch64-linux-gnu)"); + PrintLine(" BUX_CFLAGS / BUX_CC / BUX_RUNTIME / BUX_STATIC / BUX_REGISTRY"); return 0; } @@ -1887,12 +2279,17 @@ func Cli_Run(args: *String, argCount: int) -> int { if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") { PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Usage: buxc [args]"); - PrintLine("Commands: build, check, new, init, add, remove, fetch, fmt, doc, test, run, project, help, version"); + PrintLine("Commands: build, check, new, init, add, remove, fetch, install, search, fmt, doc, test, run, project, help, version"); PrintLine(" test --filter Only run tests/*.bux whose name contains "); + PrintLine(" search [query] Search package registry"); + PrintLine(" install [--locked] Write/verify bux.lock (checksums)"); + PrintLine(" add [ver|url] Registry resolve or explicit source"); PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)"); PrintLine(" doc --out file.md [path] API docs from /// comments"); PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g"); - PrintLine(" BUX_CFLAGS Extra flags appended to cc"); + PrintLine(" --static Fully-static link (minimal runtime)"); + PrintLine(" --target Cross-compile (e.g. aarch64-linux-gnu)"); + PrintLine(" BUX_CFLAGS / BUX_CC / BUX_RUNTIME / BUX_STATIC / BUX_REGISTRY"); PrintLine("Pipeline modules:"); PrintLine(" Lexer ✅"); PrintLine(" Parser ✅"); @@ -1914,12 +2311,31 @@ func Cli_Run(args: *String, argCount: int) -> int { return Cli_Init(); } + if String_Eq(cmd, "search") { + var query: String = ""; + if argCount >= 3 { + query = args[2]; + } + return Cli_Search(query); + } + if String_Eq(cmd, "add") { - if argCount < 4 { - PrintLine("Usage: buxc add "); + if argCount < 3 { + PrintLine("Usage: buxc add [version|url]"); + PrintLine(" buxc add greet # resolve from registry"); + PrintLine(" buxc add greet 0.1.1 # registry version"); + PrintLine(" buxc add greet /path/or/git-url"); return 1; } - return Cli_Add(args[2], args[3]); + if argCount >= 4 { + let a3: String = args[3]; + if !String_Contains(a3, "/") && !String_StartsWith(a3, "http") && + !String_EndsWith(a3, ".git") { + return Cli_AddFromRegistry(args[2], a3); + } + return Cli_Add(args[2], a3); + } + return Cli_AddFromRegistry(args[2], "*"); } if String_Eq(cmd, "remove") { @@ -1934,6 +2350,21 @@ func Cli_Run(args: *String, argCount: int) -> int { return Cli_Fetch(); } + if String_Eq(cmd, "install") { + var lockedOnly: bool = false; + var dir: String = "."; + var ii: int = 2; + while ii < argCount { + if String_Eq(args[ii], "--locked") { + lockedOnly = true; + } else if !String_StartsWith(args[ii], "-") { + dir = args[ii]; + } + ii = ii + 1; + } + return Cli_Install(dir, lockedOnly); + } + if String_Eq(cmd, "fmt") { var dir: String = "."; var checkOnly: bool = false; @@ -1980,13 +2411,13 @@ func Cli_Run(args: *String, argCount: int) -> int { let out: String = "build/main"; if argCount >= 3 { src = args[2]; } if argCount >= 4 { out = args[3]; } - return Cli_Build(src, out, targetTriple, isRelease); + return Cli_Build(src, out, targetTriple, isRelease, isStatic); } if String_Eq(cmd, "project") { let dir: String = "."; if argCount >= 3 { dir = args[2]; } - return Cli_BuildProject(dir, targetTriple, isRelease); + return Cli_BuildProject(dir, targetTriple, isRelease, isStatic); } if String_Eq(cmd, "test") { @@ -2011,7 +2442,7 @@ func Cli_Run(args: *String, argCount: int) -> int { if String_Eq(cmd, "run") { let dir: String = "."; if argCount >= 3 { dir = args[2]; } - return Cli_RunProject(dir, targetTriple, isRelease); + return Cli_RunProject(dir, targetTriple, isRelease, isStatic); } Print("Unknown command: "); diff --git a/src/macroexpand.bux b/src/macroexpand.bux index 9bdde5d..c2f0522 100644 --- a/src/macroexpand.bux +++ b/src/macroexpand.bux @@ -449,12 +449,21 @@ module MacroExpand { e.macroPat = pat; return e; } - // expr / tt + // expr rejects stmt/pat wrappers + if String_Eq(kindStr, "expr") { + if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; } + return aexp; + } + // tt — any single call-site AST fragment (session 76; raw token trees later) + if String_Eq(kindStr, "tt") { + return aexp; + } + // default: treat as expr if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; } return aexp; } - // Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | expr|tt + // Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | "expr" | "tt" func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool { return Macro_CoerceArg(kindStr, aexp) != null as *Expr; } diff --git a/src/manifest.bux b/src/manifest.bux index 7724258..0a693a1 100644 --- a/src/manifest.bux +++ b/src/manifest.bux @@ -61,7 +61,7 @@ module Manifest { currentSection = "Package"; } else if String_StartsWith(line, "[Build]") { currentSection = "Build"; - } else if String_StartsWith(line, "[dependencies]") { + } else if String_StartsWith(line, "[dependencies]") || String_StartsWith(line, "[Dependencies]") { currentSection = "dependencies"; } else { currentSection = ""; @@ -84,6 +84,37 @@ module Manifest { } } + // Inline table: { Path = "/abs/..." } — extract quoted Path value + if String_StartsWith(val, "{") { + var foundPath: String = ""; + let pl: uint = bux_strlen(line); + var pi: uint = 0; + while pi + 4 < pl { + // match ASCII 'P','a','t','h' + if line[pi] == 80 as char8 && line[pi + 1] == 97 as char8 && + line[pi + 2] == 116 as char8 && line[pi + 3] == 104 as char8 { + var q1: int = -1; + var qj: uint = pi; + while qj < pl { + if line[qj] == 34 as char8 { + if q1 < 0 { + q1 = qj as int; + } else { + foundPath = String_Slice(line, (q1 + 1) as uint, (qj as int - q1 - 1) as uint); + break; + } + } + qj = qj + 1; + } + break; + } + pi = pi + 1; + } + if !String_Eq(foundPath, "") { + val = foundPath; + } + } + if String_Eq(currentSection, "Package") { if String_Eq(key, "Name") { m.name = val; } if String_Eq(key, "Version") { m.version = val; } diff --git a/src/registry.bux b/src/registry.bux new file mode 100644 index 0000000..e278f79 --- /dev/null +++ b/src/registry.bux @@ -0,0 +1,380 @@ +// registry.bux — package registry index for selfhost (session 81) +// Parity with bootstrap registry.nim: local file + HTTP(S) cache. +module Registry { + + extern func Print(s: String); + extern func PrintLine(s: String); + extern func bux_strlen(s: String) -> uint; + extern func bux_read_file(path: String) -> String; + extern func bux_write_file(path: String, content: String) -> bool; + extern func bux_file_exists(path: String) -> int; + extern func bux_dir_exists(path: String) -> int; + extern func bux_mkdir_if_needed(path: String) -> int; + extern func bux_path_join(a: String, b: String) -> String; + extern func bux_path_parent(path: String) -> String; + extern func bux_getenv(name: String) -> String; + extern func bux_getcwd() -> String; + extern func bux_system(cmd: String) -> int; + extern func bux_process_output(cmd: String) -> String; + + const REG_MAX: int = 64; + + struct RegistryPackage { + name: String; + version: String; + source: String; + description: String; + resolvedPath: String; + } + + struct Registry { + path: String; // local index path (or cache path) + sourceUrl: String; // non-empty if from HTTP(S) + count: int; + // fixed slots (no dynamic arrays in selfhost compiler easily) + p0: RegistryPackage; + p1: RegistryPackage; + p2: RegistryPackage; + p3: RegistryPackage; + p4: RegistryPackage; + p5: RegistryPackage; + p6: RegistryPackage; + p7: RegistryPackage; + p8: RegistryPackage; + p9: RegistryPackage; + p10: RegistryPackage; + p11: RegistryPackage; + p12: RegistryPackage; + p13: RegistryPackage; + p14: RegistryPackage; + p15: RegistryPackage; + // up to 16 packages is enough for smoke/demo; expand if needed + } + + func Reg_FileExists(path: String) -> bool { + return bux_file_exists(path) != 0; + } + + func Reg_DirExists(path: String) -> bool { + return bux_dir_exists(path) != 0; + } + + func Reg_IsHttpUrl(s: String) -> bool { + if String_StartsWith(s, "http://") { return true; } + if String_StartsWith(s, "https://") { return true; } + return false; + } + + func Reg_EnvTruthy(name: String) -> bool { + let v: String = bux_getenv(name); + if v == null as String { return false; } + if String_Eq(v, "") { return false; } + return true; + } + + func Reg_HomeCacheDir() -> String { + let home: String = bux_getenv("HOME"); + if home == null as String || String_Eq(home, "") { + return ".bux/cache"; + } + return bux_path_join(bux_path_join(home, ".bux"), "cache"); + } + + func Reg_FetchHttp(url: String) -> String { + // Download to ~/.bux/cache/registry_http.toml; return local path or "". + let cacheDir: String = Reg_HomeCacheDir(); + discard bux_mkdir_if_needed(bux_path_join(bux_getenv("HOME"), ".bux")); + discard bux_mkdir_if_needed(cacheDir); + let cachePath: String = bux_path_join(cacheDir, "registry_http.toml"); + let metaPath: String = bux_path_join(cacheDir, "registry_http.url"); + let force: bool = Reg_EnvTruthy("BUX_REGISTRY_REFRESH"); + if !force && Reg_FileExists(cachePath) && Reg_FileExists(metaPath) { + let cachedUrl: String = String_Trim(bux_read_file(metaPath)); + if String_Eq(cachedUrl, url) { + return cachePath; + } + } + var kflag: String = ""; + if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") { + kflag = " -k"; + } + // prefer curl + let curlCmd: String = String_Concat( + "curl -fsSL", + String_Concat(kflag, String_Concat(" --max-time 30 -o \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\""))))) + ); + var ok: bool = false; + if bux_system("command -v curl >/dev/null 2>&1") == 0 { + ok = bux_system(curlCmd) == 0 && Reg_FileExists(cachePath); + } else if bux_system("command -v wget >/dev/null 2>&1") == 0 { + var nflag: String = ""; + if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") { + nflag = " --no-check-certificate"; + } + let wgetCmd: String = String_Concat( + "wget -q", + String_Concat(nflag, String_Concat(" -T 30 -O \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\""))))) + ); + ok = bux_system(wgetCmd) == 0 && Reg_FileExists(cachePath); + } + if !ok { + return ""; + } + discard bux_write_file(metaPath, String_Concat(url, "\n")); + return cachePath; + } + + func Reg_StripQuotes(val: String) -> String { + var v: String = String_Trim(val); + if String_StartsWith(v, "\"") && String_EndsWith(v, "\"") { + let n: uint = bux_strlen(v); + if n >= 2 { + return String_Slice(v, 1, n - 2); + } + } + return v; + } + + func Reg_ResolveSource(src: String, indexDir: String) -> String { + // file: or path: → absolute path; else "" + var p: String = src; + if String_StartsWith(src, "file:") { + p = String_Slice(src, 5, bux_strlen(src) - 5); + if String_StartsWith(p, "//") { + p = String_Slice(p, 2, bux_strlen(p) - 2); + } + } else if String_StartsWith(src, "path:") { + p = String_Slice(src, 5, bux_strlen(src) - 5); + } else { + return ""; + } + if String_StartsWith(p, "/") { + return p; + } + return bux_path_join(indexDir, p); + } + + func Reg_SetPkg(reg: *Registry, idx: int, pkg: RegistryPackage) { + if idx == 0 { reg.p0 = pkg; } + else if idx == 1 { reg.p1 = pkg; } + else if idx == 2 { reg.p2 = pkg; } + else if idx == 3 { reg.p3 = pkg; } + else if idx == 4 { reg.p4 = pkg; } + else if idx == 5 { reg.p5 = pkg; } + else if idx == 6 { reg.p6 = pkg; } + else if idx == 7 { reg.p7 = pkg; } + else if idx == 8 { reg.p8 = pkg; } + else if idx == 9 { reg.p9 = pkg; } + else if idx == 10 { reg.p10 = pkg; } + else if idx == 11 { reg.p11 = pkg; } + else if idx == 12 { reg.p12 = pkg; } + else if idx == 13 { reg.p13 = pkg; } + else if idx == 14 { reg.p14 = pkg; } + else if idx == 15 { reg.p15 = pkg; } + } + + func Reg_GetPkg(reg: Registry, idx: int) -> RegistryPackage { + if idx == 0 { return reg.p0; } + if idx == 1 { return reg.p1; } + if idx == 2 { return reg.p2; } + if idx == 3 { return reg.p3; } + if idx == 4 { return reg.p4; } + if idx == 5 { return reg.p5; } + if idx == 6 { return reg.p6; } + if idx == 7 { return reg.p7; } + if idx == 8 { return reg.p8; } + if idx == 9 { return reg.p9; } + if idx == 10 { return reg.p10; } + if idx == 11 { return reg.p11; } + if idx == 12 { return reg.p12; } + if idx == 13 { return reg.p13; } + if idx == 14 { return reg.p14; } + return reg.p15; + } + + func Reg_ParseContent(content: String, indexPath: String) -> Registry { + var reg: Registry; + reg.path = indexPath; + reg.sourceUrl = ""; + reg.count = 0; + let indexDir: String = bux_path_parent(indexPath); + var cur: RegistryPackage; + var inPkg: bool = false; + let nlines: uint = String_SplitCount(content, "\n"); + var i: uint = 0; + while i <= nlines { + var line: String = ""; + if i < nlines { + line = String_Trim(String_SplitPart(content, "\n", i)); + } + let flush: bool = (i == nlines) || String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]"); + if flush && inPkg && !String_Eq(cur.name, "") { + cur.resolvedPath = Reg_ResolveSource(cur.source, indexDir); + if reg.count < 16 { + Reg_SetPkg(®, reg.count, cur); + reg.count = reg.count + 1; + } + cur.name = ""; + cur.version = ""; + cur.source = ""; + cur.description = ""; + cur.resolvedPath = ""; + } + if i == nlines { break; } + if String_Eq(line, "") || String_StartsWith(line, "#") { + i = i + 1; + continue; + } + if String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]") { + inPkg = true; + i = i + 1; + continue; + } + if !inPkg { + i = i + 1; + continue; + } + let eqc: uint = String_SplitCount(line, "="); + if eqc >= 2 { + let key: String = String_Trim(String_SplitPart(line, "=", 0)); + let val: String = Reg_StripQuotes(String_SplitPart(line, "=", 1)); + // lowercase-ish compare for common keys + if String_Eq(key, "name") || String_Eq(key, "Name") { + cur.name = val; + } else if String_Eq(key, "version") || String_Eq(key, "Version") { + cur.version = val; + } else if String_Eq(key, "source") || String_Eq(key, "Source") { + cur.source = val; + } else if String_Eq(key, "description") || String_Eq(key, "Description") { + cur.description = val; + } + } + i = i + 1; + } + return reg; + } + + func Reg_FindIndex() -> Registry { + var reg: Registry; + reg.path = ""; + reg.sourceUrl = ""; + reg.count = 0; + let env: String = bux_getenv("BUX_REGISTRY"); + if env != null as String && !String_Eq(env, "") { + if Reg_IsHttpUrl(env) { + let local: String = Reg_FetchHttp(env); + if String_Eq(local, "") { + reg.sourceUrl = env; + return reg; + } + let content: String = bux_read_file(local); + reg = Reg_ParseContent(content, local); + reg.sourceUrl = env; + reg.path = local; + return reg; + } + if Reg_FileExists(env) { + let content: String = bux_read_file(env); + reg = Reg_ParseContent(content, env); + reg.path = env; + return reg; + } + } + let home: String = bux_getenv("HOME"); + if home != null as String && !String_Eq(home, "") { + let homeIdx: String = bux_path_join(bux_path_join(home, ".bux"), "registry.toml"); + if Reg_FileExists(homeIdx) { + let content: String = bux_read_file(homeIdx); + reg = Reg_ParseContent(content, homeIdx); + reg.path = homeIdx; + return reg; + } + } + // cwd-relative candidates + let cwd: String = bux_getcwd(); + var c0: String = bux_path_join(cwd, "config/registry.toml"); + if Reg_FileExists(c0) { + let content: String = bux_read_file(c0); + reg = Reg_ParseContent(content, c0); + reg.path = c0; + return reg; + } + c0 = bux_path_join(cwd, "../config/registry.toml"); + if Reg_FileExists(c0) { + let content: String = bux_read_file(c0); + reg = Reg_ParseContent(content, c0); + reg.path = c0; + return reg; + } + c0 = bux_path_join(cwd, "../../config/registry.toml"); + if Reg_FileExists(c0) { + let content: String = bux_read_file(c0); + reg = Reg_ParseContent(content, c0); + reg.path = c0; + return reg; + } + return reg; + } + + func Reg_VersionOk(have: String, req: String) -> bool { + if String_Eq(req, "") || String_Eq(req, "*") { return true; } + return String_Eq(have, req); + } + + // Lookup by name; versionReq "*" = last matching entry (highest listed last). + func Reg_Lookup(reg: Registry, name: String, versionReq: String) -> RegistryPackage { + var found: RegistryPackage; + found.name = ""; + var i: int = 0; + while i < reg.count { + let p: RegistryPackage = Reg_GetPkg(reg, i); + if String_Eq(p.name, name) && Reg_VersionOk(p.version, versionReq) { + found = p; + } + i = i + 1; + } + return found; + } + + func Reg_Search(reg: Registry, query: String) -> int { + // print hits; return count + var hits: int = 0; + // Dedupe by name: keep last version + // Simple O(n^2): for each pkg, if last occurrence of name, print + var i: int = 0; + while i < reg.count { + let p: RegistryPackage = Reg_GetPkg(reg, i); + var isLast: bool = true; + var j: int = i + 1; + while j < reg.count { + let q: RegistryPackage = Reg_GetPkg(reg, j); + if String_Eq(q.name, p.name) { + isLast = false; + break; + } + j = j + 1; + } + if isLast { + var ok: bool = true; + if !String_Eq(query, "") { + ok = String_Contains(p.name, query) || String_Contains(p.description, query); + } + if ok { + Print(" "); + Print(p.name); + Print(" "); + Print(p.version); + Print(" — "); + if !String_Eq(p.description, "") { + PrintLine(p.description); + } else { + PrintLine(p.source); + } + hits = hits + 1; + } + } + i = i + 1; + } + return hits; + } +} diff --git a/tools/build_health_bin.sh b/tools/build_health_bin.sh new file mode 100755 index 0000000..9c722fc --- /dev/null +++ b/tools/build_health_bin.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Build examples/http_health.bux → build/http_health for container packaging. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +OUT_DIR="${1:-$ROOT/build}" +mkdir -p "$OUT_DIR" + +if [[ ! -x "$ROOT/buxc" ]]; then + (cd "$ROOT" && make build) +fi + +PKG=$(mktemp -d) +trap 'rm -rf "$PKG"' EXIT +mkdir -p "$PKG/src" +cp -a "$ROOT/rt" "$PKG/" +cat > "$PKG/bux.toml" <<'EOF' +[Package] +Name = "http_health" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/http_health.bux" "$PKG/src/Main.bux" + +"$ROOT/buxc" --quiet --release build "$PKG" +cp "$PKG/build/http_health" "$OUT_DIR/http_health" +file "$OUT_DIR/http_health" +echo "wrote $OUT_DIR/http_health" +echo "docker: docker build -f examples/docker/Dockerfile.health -t bux-health $ROOT" diff --git a/tools/build_static_hello.sh b/tools/build_static_hello.sh new file mode 100755 index 0000000..a7a5763 --- /dev/null +++ b/tools/build_static_hello.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Build a fully-static hello binary for container / distroless demos (session 75). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +OUT_DIR="${1:-$ROOT/build}" +mkdir -p "$OUT_DIR" + +if [[ ! -x "$ROOT/buxc" ]]; then + (cd "$ROOT" && make build) +fi + +PKG=$(mktemp -d) +trap 'rm -rf "$PKG"' EXIT +mkdir -p "$PKG/src" +cp -a "$ROOT/rt" "$PKG/" +cat > "$PKG/bux.toml" <<'EOF' +[Package] +Name = "hello_static" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/hello.bux" "$PKG/src/Main.bux" + +"$ROOT/buxc" --quiet --static --release build "$PKG" +cp "$PKG/build/hello_static" "$OUT_DIR/hello_static" +file "$OUT_DIR/hello_static" +echo "wrote $OUT_DIR/hello_static" +echo "docker: docker build -f examples/docker/Dockerfile.static --build-arg BIN=$OUT_DIR/hello_static -t bux-hello-static $ROOT" diff --git a/tools/smoke_drop_move.sh b/tools/smoke_drop_move.sh index de3bf6c..b8390b7 100755 --- a/tools/smoke_drop_move.sh +++ b/tools/smoke_drop_move.sh @@ -150,6 +150,36 @@ if ! sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" | grep -q ' fi echo " move_field_ptr: PASS (run + no Bag_Drop + Tracked_Drop remaining)" +# --- cross-function pointer ownership TakeItems(&bag) --- +echo "=== smoke: move_cross_fn ===" +mkdir -p "$TMP/mcf/src" +cp -a "$ROOT/rt" "$TMP/mcf/" +cat > "$TMP/mcf/bux.toml" <<'EOF' +[Package] +Name = "move_cross_fn" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cp "$ROOT/examples/move_cross_fn.bux" "$TMP/mcf/src/Main.bux" +out=$(cd "$TMP/mcf" && "$BUXC" run .) +echo "$out" | grep -q 'cross_fn_drops=2' +echo "$out" | grep -q 'PASS' +# CallTakeItems must Tracked_Drop remaining tag, not Bag_Drop (would free moved items) +if sed -n '/^int CallTakeItems/,/^}/p' "$TMP/mcf/build/main.c" | grep -q 'Bag_Drop'; then + echo "error: CallTakeItems still Bag_Drops after TakeItems(&bag)" >&2 + sed -n '/^int CallTakeItems/,/^}/p' "$TMP/mcf/build/main.c" + exit 1 +fi +if ! sed -n '/^int CallTakeItems/,/^}/p' "$TMP/mcf/build/main.c" | grep -q 'Tracked_Drop'; then + echo "error: CallTakeItems missing Tracked_Drop for remaining tag" >&2 + sed -n '/^int CallTakeItems/,/^}/p' "$TMP/mcf/build/main.c" + exit 1 +fi +echo " move_cross_fn: PASS (run + no Bag_Drop + Tracked_Drop remaining)" + # --- early return Drop counts --- echo "=== smoke: drop_early_return ===" mkdir -p "$TMP/de/src" @@ -168,4 +198,4 @@ out=$(cd "$TMP/de" && "$BUXC" run .) echo "$out" | grep -q 'PASS' echo " drop_early_return: PASS" -echo "PASS: smoke_drop_move (field-move + partial + remaining + nested + ptr + early-return)" +echo "PASS: smoke_drop_move (field-move + partial + remaining + nested + ptr + cross-fn + early-return)" diff --git a/tools/smoke_linux_targets.sh b/tools/smoke_linux_targets.sh new file mode 100755 index 0000000..a93a843 --- /dev/null +++ b/tools/smoke_linux_targets.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Session 75 — Linux / cloud / embedded smoke: +# 1) BUX_RUNTIME=minimal (thin runtime, run hello) +# 2) --static --release (fully-static binary, file(1) check) +# 3) --target aarch64-linux-gnu (cross build if toolchain present) +# 4) CTFE CRC example under minimal runtime +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +unset BUX_DEBUG_FILE || true +unset BUX_RUNTIME || true +unset BUX_STATIC || true +unset BUX_CC || true + +if [[ -x "$ROOT/buxc" ]]; then + BUXC="$ROOT/buxc" +else + (cd "$ROOT" && make build) + BUXC="$ROOT/buxc" +fi + +mkpkg() { + local name="$1" src="$2" + local d + d=$(mktemp -d) + mkdir -p "$d/src" + cp -a "$ROOT/rt" "$d/" + cat > "$d/bux.toml" <&1) || { echo "$out" >&2; exit 1; } +echo "$out" | grep -q 'Hello, Bux!' +grep -q 'minimal / embedded / static' "$PKG/build/runtime.c" +# must not pull full POSIX +if grep -q 'openssl/evp\|pthread.h' "$PKG/build/runtime.c"; then + echo "error: minimal runtime still has pthread/openssl includes" >&2 + exit 1 +fi +echo "PASS: minimal runtime" +pass=$((pass+1)) +unset BUX_RUNTIME + +# ── 2) fully-static (thin runtime implied) ────────────────────────────── +note "static link (--static --release)" +PKG2=$(mkpkg hello_static "$ROOT/examples/hello.bux") +out=$("$BUXC" --quiet --static --release build "$PKG2" 2>&1) || { echo "$out" >&2; exit 1; } +BIN="$PKG2/build/hello_static" +[[ -x "$BIN" ]] || BIN="$PKG2/build/hello_static.exe" +file_out=$(file "$BIN") +echo "$file_out" +echo "$file_out" | grep -qi 'statically linked\|static-pie\|static ' +# run only if host arch matches +if echo "$file_out" | grep -qi 'x86-64\|x86_64\|Intel 80386'; then + run_out=$("$BIN" 2>&1) || { echo "$run_out" >&2; exit 1; } + echo "$run_out" | grep -q 'Hello, Bux!' +fi +grep -q 'minimal / embedded / static' "$PKG2/build/runtime.c" +echo "PASS: static link" +pass=$((pass+1)) + +# ── 3) cross aarch64 (optional toolchain) ─────────────────────────────── +note "cross aarch64-linux-gnu" +PKG3=$(mkpkg hello_arm "$ROOT/examples/hello.bux") +if command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then + out=$("$BUXC" --quiet --static --release --target aarch64-linux-gnu build "$PKG3" 2>&1) || { + echo "$out" >&2 + exit 1 + } + BIN3="$PKG3/build/hello_arm" + [[ -x "$BIN3" ]] || BIN3="$PKG3/build/hello_arm.exe" + file_out=$(file "$BIN3") + echo "$file_out" + echo "$file_out" | grep -qi 'ARM aarch64\|aarch64' + echo "$file_out" | grep -qi 'statically linked\|static-pie\|static ' + echo "PASS: cross aarch64" + pass=$((pass+1)) +else + echo "SKIP: aarch64-linux-gnu-gcc not on PATH" +fi + +# ── 4) CTFE CRC under minimal runtime ─────────────────────────────────── +note "ctfe_crc (minimal)" +if [[ -f "$ROOT/examples/ctfe_crc.bux" ]]; then + PKG4=$(mkpkg ctfe_crc "$ROOT/examples/ctfe_crc.bux") + export BUX_RUNTIME=minimal + out=$("$BUXC" --quiet run "$PKG4" 2>&1) || { echo "$out" >&2; exit 1; } + echo "$out" + echo "$out" | grep -q 'PASS ctfe_crc' + echo "PASS: ctfe_crc" + pass=$((pass+1)) + unset BUX_RUNTIME +else + echo "SKIP: examples/ctfe_crc.bux missing" +fi + +echo "" +echo "smoke_linux_targets: $pass checks passed" +echo "PASS: smoke_linux_targets" diff --git a/tools/smoke_musl_static.sh b/tools/smoke_musl_static.sh new file mode 100755 index 0000000..47b3f19 --- /dev/null +++ b/tools/smoke_musl_static.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Session 79 — musl fully-static path (skips if no musl-gcc / zig musl target). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" + +if [[ ! -x "$ROOT/buxc" ]]; then + (cd "$ROOT" && make build) +fi + +pick_cc() { + if command -v musl-gcc >/dev/null 2>&1; then + echo "musl-gcc" + return + fi + if command -v x86_64-linux-musl-gcc >/dev/null 2>&1; then + echo "x86_64-linux-musl-gcc" + return + fi + if command -v zig >/dev/null 2>&1; then + # zig cc -target x86_64-linux-musl acts as a C compiler when BUX_CC is a wrapper + echo "zig-musl" + return + fi + echo "" +} + +CC_KIND=$(pick_cc) +if [[ -z "$CC_KIND" ]]; then + echo "SKIP: no musl-gcc / zig on PATH (install musl-tools or zig for Alpine static)" + echo "PASS: smoke_musl_static (skipped)" + exit 0 +fi + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/src" +cp -a "$ROOT/rt" "$TMP/" +cp "$ROOT/examples/hello.bux" "$TMP/src/Main.bux" +cat > "$TMP/bux.toml" <<'EOF' +[Package] +Name = "hello_musl" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF + +export BUX_RUNTIME=minimal +if [[ "$CC_KIND" == "zig-musl" ]]; then + # Wrapper so buxc invokes zig as cc + cat > "$TMP/zigcc" <<'EOF' +#!/bin/sh +exec zig cc -target x86_64-linux-musl "$@" +EOF + chmod +x "$TMP/zigcc" + export BUX_CC="$TMP/zigcc" +else + export BUX_CC="$CC_KIND" +fi + +echo "=== musl static hello (BUX_CC=$BUX_CC) ===" +"$ROOT/buxc" --quiet --static --release build "$TMP" +BIN="$TMP/build/hello_musl" +file "$BIN" +# musl static often reports "statically linked" +if file "$BIN" | grep -qi 'statically linked\|static-pie\|static '; then + echo "static: ok" +else + # some musl toolchains still produce dynamic musl — accept if ldd mentions musl + if command -v ldd >/dev/null 2>&1 && ldd "$BIN" 2>&1 | grep -qi musl; then + echo "dynamic musl: ok" + else + echo "WARN: could not confirm musl/static; file output above" + fi +fi + +# Run only if host can execute +if "$BIN" 2>/dev/null | grep -q 'Hello, Bux!'; then + echo "run: ok" +else + echo "run: skipped or failed (cross?)" +fi + +echo "PASS: smoke_musl_static" diff --git a/tools/smoke_nexus_mtls.sh b/tools/smoke_nexus_mtls.sh new file mode 100755 index 0000000..15ebae9 --- /dev/null +++ b/tools/smoke_nexus_mtls.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Session 80 — Nexus mTLS: reject no-client-cert; accept with client cert. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +PORT="${NEXUS_PORT:-18444}" +BIND="${NEXUS_BIND:-127.0.0.1}" + +if [[ ! -x "$ROOT/apps/nexus/build/nexus" ]]; then + (cd "$ROOT/apps/nexus" && "$ROOT/buxc" --release build) +fi +NEXUS="$ROOT/apps/nexus/build/nexus" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"; kill $NPID 2>/dev/null || true' EXIT + +# CA + server + client certs +openssl req -x509 -newkey rsa:2048 -nodes -keyout "$TMP/ca.key" -out "$TMP/ca.pem" \ + -days 1 -subj "/CN=TestCA" 2>/dev/null +openssl req -newkey rsa:2048 -nodes -keyout "$TMP/server.key" -out "$TMP/server.csr" \ + -subj "/CN=localhost" 2>/dev/null +openssl x509 -req -in "$TMP/server.csr" -CA "$TMP/ca.pem" -CAkey "$TMP/ca.key" \ + -CAcreateserial -out "$TMP/server.pem" -days 1 2>/dev/null +openssl req -newkey rsa:2048 -nodes -keyout "$TMP/client.key" -out "$TMP/client.csr" \ + -subj "/CN=client" 2>/dev/null +openssl x509 -req -in "$TMP/client.csr" -CA "$TMP/ca.pem" -CAkey "$TMP/ca.key" \ + -CAcreateserial -out "$TMP/client.pem" -days 1 2>/dev/null + +NEXUS_PORT="$PORT" NEXUS_BIND="$BIND" NEXUS_WORKERS=2 NEXUS_ACCESS_LOG=0 \ +NEXUS_TLS=1 NEXUS_TLS_CERT="$TMP/server.pem" NEXUS_TLS_KEY="$TMP/server.key" \ +NEXUS_TLS_CLIENT_CA="$TMP/ca.pem" \ + "$NEXUS" >"$TMP/nexus.log" 2>&1 & +NPID=$! +sleep 0.7 + +if ! kill -0 "$NPID" 2>/dev/null; then + echo "error: nexus failed" >&2 + cat "$TMP/nexus.log" >&2 + exit 1 +fi + +# Without client cert → fail +if curl -sk --max-time 3 "https://${BIND}:${PORT}/api/health" -o /dev/null 2>/dev/null; then + # some curl versions might still get empty; check exit code + : +fi +set +e +curl -sk --max-time 3 "https://${BIND}:${PORT}/api/health" >/dev/null 2>&1 +noclient=$? +set -e +if [[ $noclient -eq 0 ]]; then + # Try again more strictly — handshake should fail + if curl -sk --max-time 3 "https://${BIND}:${PORT}/api/health" 2>&1 | grep -q status; then + echo "error: mTLS allowed request without client cert" >&2 + cat "$TMP/nexus.log" >&2 + exit 1 + fi +fi +echo "no-client: rejected (curl exit $noclient)" + +# With client cert → ok +body=$(curl -sk --max-time 5 \ + --cert "$TMP/client.pem" --key "$TMP/client.key" \ + --cacert "$TMP/ca.pem" \ + "https://${BIND}:${PORT}/api/health") +echo "$body" +echo "$body" | grep -q '"status":"ok"' +echo "$body" | grep -q '0.6.0' + +kill -TERM "$NPID" 2>/dev/null || true +sleep 0.6 +kill -0 "$NPID" 2>/dev/null && kill -9 "$NPID" 2>/dev/null || true + +grep -q 'mTLS' "$TMP/nexus.log" || grep -q 'client certificates' "$TMP/nexus.log" + +echo "PASS: smoke_nexus_mtls" diff --git a/tools/smoke_nexus_tls.sh b/tools/smoke_nexus_tls.sh new file mode 100755 index 0000000..a292d07 --- /dev/null +++ b/tools/smoke_nexus_tls.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Session 78 — Nexus HTTPS smoke (self-signed cert + curl -k). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +PORT="${NEXUS_PORT:-18443}" +BIND="${NEXUS_BIND:-127.0.0.1}" + +if [[ ! -x "$ROOT/apps/nexus/build/nexus" ]]; then + (cd "$ROOT/apps/nexus" && "$ROOT/buxc" --release build) +fi +NEXUS="$ROOT/apps/nexus/build/nexus" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"; kill $NPID 2>/dev/null || true' EXIT + +# Self-signed cert (10y, CN=localhost) +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$TMP/key.pem" -out "$TMP/cert.pem" \ + -days 3650 -subj "/CN=localhost" 2>/dev/null + +NEXUS_PORT="$PORT" NEXUS_BIND="$BIND" NEXUS_WORKERS=2 NEXUS_ACCESS_LOG=1 \ +NEXUS_TLS=1 NEXUS_TLS_CERT="$TMP/cert.pem" NEXUS_TLS_KEY="$TMP/key.pem" \ + "$NEXUS" >"$TMP/nexus.log" 2>&1 & +NPID=$! +sleep 0.6 + +if ! kill -0 "$NPID" 2>/dev/null; then + echo "error: nexus failed to start" >&2 + cat "$TMP/nexus.log" >&2 + exit 1 +fi + +body=$(curl -sk --max-time 5 "https://${BIND}:${PORT}/api/health") +echo "$body" +echo "$body" | grep -q '"status":"ok"' +echo "$body" | grep -q '0.6.0' + +info=$(curl -sk --max-time 5 "https://${BIND}:${PORT}/api/info") +echo "$info" | grep -q 'TLS' + +kill -TERM "$NPID" 2>/dev/null || true +sleep 0.8 +if kill -0 "$NPID" 2>/dev/null; then + kill -9 "$NPID" 2>/dev/null || true + echo "WARN: forced kill after SIGTERM" +else + echo "PASS: SIGTERM exit" +fi + +grep -q 'Listening on https://' "$TMP/nexus.log" || { + echo "error: expected https banner" >&2 + cat "$TMP/nexus.log" >&2 + exit 1 +} +grep -q 'GET /api/health' "$TMP/nexus.log" || true + +echo "PASS: smoke_nexus_tls" diff --git a/tools/smoke_registry.sh b/tools/smoke_registry.sh index 6757abe..d2b2180 100755 --- a/tools/smoke_registry.sh +++ b/tools/smoke_registry.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -# Smoke: registry search + add + install + build with greet package (E.1) -# Also verifies HTTP-fetchable registry index (E.1b). +# Smoke: registry search + add + install + lock reproducibility + HTTPS (E.1 / session 79) set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" BUXC="$ROOT/buxc" @@ -12,11 +11,16 @@ fi TMP=$(mktemp -d) HTTP_PID="" +HTTPS_PID="" cleanup() { if [[ -n "$HTTP_PID" ]]; then kill "$HTTP_PID" 2>/dev/null || true wait "$HTTP_PID" 2>/dev/null || true fi + if [[ -n "$HTTPS_PID" ]]; then + kill "$HTTPS_PID" 2>/dev/null || true + wait "$HTTPS_PID" 2>/dev/null || true + fi rm -rf "$TMP" } trap cleanup EXIT @@ -60,20 +64,67 @@ echo "=== bux add greet ===" grep -q greet bux.toml cat bux.toml -echo "=== bux install ===" +echo "=== bux install (with checksum) ===" "$BUXC" install test -f bux.lock grep -q greet bux.lock +grep -q Checksum bux.lock cat bux.lock +cp bux.lock "$TMP/lock1" + +echo "=== lock reproducibility (second install) ===" +"$BUXC" install +# Source path + version + checksum must match +diff -u "$TMP/lock1" bux.lock + +echo "=== bux install --locked ===" +"$BUXC" install --locked + +echo "=== install --locked fails without lock ===" +rm -f bux.lock +if "$BUXC" install --locked 2>"$TMP/locked_err"; then + echo "error: expected --locked to fail without lock" >&2 + exit 1 +fi +grep -qi 'missing\|locked' "$TMP/locked_err" +"$BUXC" install +test -f bux.lock + +echo "=== checksum mismatch detected ===" +# Corrupt checksum +python3 - <<'PY' +from pathlib import Path +p = Path("bux.lock") +t = p.read_text() +# flip last hex nibble of Checksum line if present +lines = [] +for line in t.splitlines(): + if line.startswith("Checksum"): + # Checksum = "abcdef..." + import re + m = re.search(r'"([0-9a-fA-F]+)"', line) + if m: + h = m.group(1) + h2 = h[:-1] + ("0" if h[-1] != "0" else "1") + line = f'Checksum = "{h2}"' + lines.append(line) +p.write_text("\n".join(lines) + "\n") +PY +if "$BUXC" install --locked 2>"$TMP/csum_err"; then + echo "error: expected checksum mismatch failure" >&2 + exit 1 +fi +grep -qi 'checksum' "$TMP/csum_err" +# restore good lock +"$BUXC" install >/dev/null echo "=== bux run ===" "$BUXC" run . | tee "$TMP/run.out" grep -q "Hello, Bux!" "$TMP/run.out" # --- HTTP registry index --- -echo "=== HTTP registry index (E.1b) ===" +echo "=== HTTP registry index ===" mkdir -p "$TMP/http" -# Absolute file: path so resolution works after download to ~/.bux/cache cat > "$TMP/http/registry.toml" </dev/null 2>&1 ) & HTTP_PID=$! -# Wait until server responds for _ in 1 2 3 4 5 6 7 8 9 10; do if curl -fsS "http://127.0.0.1:${PORT}/registry.toml" >/dev/null 2>&1; then break @@ -101,9 +150,43 @@ export BUX_REGISTRY="http://127.0.0.1:${PORT}/registry.toml" export BUX_REGISTRY_REFRESH=1 "$BUXC" search greet | tee "$TMP/http_search.out" grep -q greet "$TMP/http_search.out" -grep -q "http://127.0.0.1" "$TMP/http_search.out" || grep -q "cached" "$TMP/http_search.out" unset BUX_REGISTRY_REFRESH -# Second search should hit cache without refresh "$BUXC" search greet | grep -q greet -echo "PASS: registry smoke (local + HTTP search + add + install + build)" +# --- HTTPS registry (self-signed) --- +echo "=== HTTPS registry index (self-signed + BUX_REGISTRY_INSECURE) ===" +mkdir -p "$TMP/https" +cp "$TMP/http/registry.toml" "$TMP/https/registry.toml" +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$TMP/https/key.pem" -out "$TMP/https/cert.pem" \ + -days 1 -subj "/CN=localhost" 2>/dev/null + +SPORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') +python3 - </dev/null 2>&1; then + break + fi + sleep 0.15 +done + +export BUX_REGISTRY="https://127.0.0.1:${SPORT}/registry.toml" +export BUX_REGISTRY_REFRESH=1 +export BUX_REGISTRY_INSECURE=1 +"$BUXC" search greet | tee "$TMP/https_search.out" +grep -q greet "$TMP/https_search.out" +grep -q "https://" "$TMP/https_search.out" || grep -q "cached" "$TMP/https_search.out" +unset BUX_REGISTRY_INSECURE +unset BUX_REGISTRY_REFRESH +unset BUX_REGISTRY + +echo "PASS: registry smoke (local + lock/checksum + locked + HTTP + HTTPS)" diff --git a/tools/smoke_selfhost_install.sh b/tools/smoke_selfhost_install.sh new file mode 100755 index 0000000..2f0df49 --- /dev/null +++ b/tools/smoke_selfhost_install.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Session 80 — selfhost buxc2 install + --locked checksum parity +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" + +if [[ ! -x "$ROOT/build/selfhost/build/buxc2" ]]; then + (cd "$ROOT" && make selfhost) +fi +BUXC2="$ROOT/build/selfhost/build/buxc2" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +mkdir -p "$TMP/app/src" "$TMP/libpkg/src" +# mini lib package +cat > "$TMP/libpkg/bux.toml" <<'EOF' +[Package] +Name = "mini" +Version = "1.2.3" +Type = "lib" +EOF +cat > "$TMP/libpkg/src/Lib.bux" <<'EOF' +func Mini_Version() -> String { return "1.2.3"; } +EOF + +cat > "$TMP/app/bux.toml" < "$TMP/app/src/Main.bux" <<'EOF' +func Main() -> int { return 0; } +EOF + +cd "$TMP/app" +echo "=== buxc2 install ===" +"$BUXC2" install . +test -f bux.lock +grep -q mini bux.lock +grep -q Checksum bux.lock +cat bux.lock +cp bux.lock "$TMP/lock1" + +echo "=== install reproducible ===" +"$BUXC2" install . +diff -u "$TMP/lock1" bux.lock + +echo "=== install --locked ===" +"$BUXC2" install --locked . + +echo "=== --locked fails without lock ===" +rm bux.lock +if "$BUXC2" install --locked . >"$TMP/err" 2>&1; then + echo "error: expected failure" >&2 + cat "$TMP/err" >&2 + exit 1 +fi +grep -qi 'missing\|locked' "$TMP/err" +"$BUXC2" install . >/dev/null + +echo "=== checksum mismatch ===" +python3 - <<'PY' +from pathlib import Path +import re +p = Path("bux.lock") +t = p.read_text() +lines = [] +for line in t.splitlines(): + if line.startswith("Checksum"): + m = re.search(r'"([0-9a-fA-F]+)"', line) + if m: + h = m.group(1) + h2 = h[:-1] + ("0" if h[-1] != "0" else "1") + line = f'Checksum = "{h2}"' + lines.append(line) +p.write_text("\n".join(lines) + "\n") +PY +if "$BUXC2" install --locked . >"$TMP/err2" 2>&1; then + echo "error: expected checksum fail" >&2 + cat "$TMP/err2" >&2 + exit 1 +fi +grep -qi checksum "$TMP/err2" + +echo "PASS: smoke_selfhost_install" diff --git a/tools/smoke_selfhost_registry.sh b/tools/smoke_selfhost_registry.sh new file mode 100755 index 0000000..69c53da --- /dev/null +++ b/tools/smoke_selfhost_registry.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Session 81 — selfhost registry: search / add by name / install / HTTP index +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}" +export BUX_REGISTRY="$ROOT/config/registry.toml" + +if [[ ! -x "$ROOT/build/selfhost/build/buxc2" ]]; then + (cd "$ROOT" && make selfhost) +fi +BUXC2="$ROOT/build/selfhost/build/buxc2" + +TMP=$(mktemp -d) +HTTP_PID="" +cleanup() { + [[ -n "$HTTP_PID" ]] && kill "$HTTP_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap cleanup EXIT + +echo "=== buxc2 search greet (local) ===" +"$BUXC2" search greet | tee "$TMP/s.out" +grep -q greet "$TMP/s.out" + +echo "=== consumer + add greet (registry) ===" +mkdir -p "$TMP/app/src" +cat > "$TMP/app/bux.toml" <<'EOF' +[Package] +Name = "reg_consumer" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" +EOF +cat > "$TMP/app/src/Main.bux" <<'EOF' +import Std::Io::{PrintLine}; +import Std::String::{String_Eq}; +import Std::Test::{Test_AssertTrue, Test_Pass}; + +func Main() -> int { + let msg: String = Greet_Hello("Bux"); + Test_AssertTrue(String_Eq(msg, "Hello, Bux!")); + PrintLine(msg); + Test_Pass("reg_consumer"); + return 0; +} +EOF + +cd "$TMP/app" +"$BUXC2" add greet | tee "$TMP/add.out" +grep -q greet bux.toml +"$BUXC2" install . +test -f bux.lock +grep -q Checksum bux.lock +"$BUXC2" install --locked . + +echo "=== buxc2 run with registry dep ===" +"$BUXC2" run . | tee "$TMP/run.out" +grep -q "Hello, Bux!" "$TMP/run.out" + +echo "=== HTTP registry search ===" +mkdir -p "$TMP/http" +cat > "$TMP/http/registry.toml" </dev/null 2>&1 & +HTTP_PID=$! +for _ in 1 2 3 4 5 6 7 8 9 10; do + curl -fsS "http://127.0.0.1:${PORT}/registry.toml" >/dev/null 2>&1 && break + sleep 0.1 +done +export BUX_REGISTRY="http://127.0.0.1:${PORT}/registry.toml" +export BUX_REGISTRY_REFRESH=1 +"$BUXC2" search greet | tee "$TMP/http.out" +grep -q greet "$TMP/http.out" +grep -q "http://" "$TMP/http.out" || grep -q cached "$TMP/http.out" + +echo "PASS: smoke_selfhost_registry"