feat: Linux/cloud platform stack (TLS, registry, static/cross, selfhost PM)
ci / build (ubuntu) (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled

Ship the QUALITY_PLAN platform focus: thin/minimal runtime, --static/--target,
Nexus HTTPS/mTLS with graceful stop, lock checksums + install --locked,
selfhost registry (search/add/HTTP), containers, and CI smokes for cloud path.
This commit is contained in:
2026-07-23 23:00:55 +03:00
parent a939f74b1b
commit a785747c37
44 changed files with 4318 additions and 279 deletions
+18 -5
View File
@@ -171,14 +171,15 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ 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 - name: Download buxc
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: buxc-linux name: buxc-linux
- name: Prepare buxc - name: Prepare buxc
run: chmod +x buxc && ./buxc --version run: chmod +x buxc && ./buxc --version
- name: errors + stdlib + registry + dwarf - name: errors + stdlib + registry + dwarf + linux-targets
env: env:
BUX_SKIP_BUILD: "1" BUX_SKIP_BUILD: "1"
run: | run: |
@@ -188,6 +189,7 @@ jobs:
make test-registry BUX_SKIP_BUILD=1 make test-registry BUX_SKIP_BUILD=1
make test-dwarf BUX_SKIP_BUILD=1 make test-dwarf BUX_SKIP_BUILD=1
make test-drop-move BUX_SKIP_BUILD=1 make test-drop-move BUX_SKIP_BUILD=1
make test-linux-targets BUX_SKIP_BUILD=1
apps: apps:
name: apps name: apps
@@ -199,19 +201,22 @@ jobs:
- name: Install build deps - name: Install build deps
run: | run: |
sudo apt-get update 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 - name: Download buxc
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: buxc-linux name: buxc-linux
- name: Prepare buxc - name: Prepare buxc
run: chmod +x buxc && ./buxc --version run: chmod +x buxc && ./buxc --version
- name: test-apps - name: test-apps + nexus TLS/mTLS
env: env:
BUX_SKIP_BUILD: "1" BUX_SKIP_BUILD: "1"
run: | run: |
unset BUX_DEBUG_FILE || true unset BUX_DEBUG_FILE || true
make test-apps BUX_SKIP_BUILD=1 make test-apps BUX_SKIP_BUILD=1
make test-nexus-tls BUX_SKIP_BUILD=1
make test-nexus-mtls BUX_SKIP_BUILD=1
selfhost: selfhost:
name: selfhost smoke name: selfhost smoke
@@ -223,7 +228,8 @@ jobs:
- name: Install build deps - name: Install build deps
run: | run: |
sudo apt-get update 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 - name: Download buxc
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -237,6 +243,13 @@ jobs:
unset BUX_DEBUG_FILE || true unset BUX_DEBUG_FILE || true
unset BUX_SELFHOST_FIXED_POINT || true unset BUX_SELFHOST_FIXED_POINT || true
make test-selfhost-smoke BUX_SKIP_BUILD=1 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 - name: Upload selfhost artifacts on failure
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+43 -4
View File
@@ -5,12 +5,12 @@ BUILD_DIR := build
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim). # Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
NIMFLAGS ?= --nimcache:nimcache 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. # 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 all: build
@@ -37,7 +37,7 @@ debug: dev
@echo "Debug binary: buxc_debug" @echo "Debug binary: buxc_debug"
# Full local / sequential suite (same coverage as split CI jobs combined). # 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). # Nim unit tests + tiny CLI smoke (needs Nim + buxc).
test-unit: ensure-buxc test-unit: ensure-buxc
@@ -265,6 +265,45 @@ test-dwarf: ensure-buxc
@chmod +x tools/smoke_dwarf.sh @chmod +x tools/smoke_dwarf.sh
@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) # Drop / field-move goldens (whole + partial field move; early-return counts)
.PHONY: test-drop-move .PHONY: test-drop-move
test-drop-move: ensure-buxc test-drop-move: ensure-buxc
+29 -3
View File
@@ -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 | | **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 | | **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 | | **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 ## Quick Start
@@ -34,10 +38,32 @@ cd apps/nexus
./nexus ./nexus
# Optional env (also used by `make bench-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
```
``` ```
╔══════════════════════════════════════════════╗ ╔══════════════════════════════════════════════╗
+16
View File
@@ -6,6 +6,16 @@ module Config {
workerCount: int; workerCount: int;
publicDir: String; publicDir: String;
backlog: int; 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 { pub const func DefaultConfig() -> ServerConfig {
@@ -15,6 +25,12 @@ module Config {
workerCount: 4, workerCount: 4,
publicDir: "public", publicDir: "public",
backlog: 128, backlog: 128,
maxBodyBytes: 1048576,
accessLog: true,
tlsEnabled: false,
tlsCertPath: "",
tlsKeyPath: "",
tlsClientCaPath: "",
}; };
} }
+2 -2
View File
@@ -63,12 +63,12 @@ module Handlers {
pub func HandleApiHealth() -> HttpResponse { pub func HandleApiHealth() -> HttpResponse {
return Http_NewResponse(200, "application/json; charset=utf-8", 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 { pub func HandleApiInfo() -> HttpResponse {
return Http_NewResponse(200, "application/json; charset=utf-8", 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 { pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
+42 -2
View File
@@ -6,7 +6,7 @@ module Main {
import Server::{RunServer}; import Server::{RunServer};
import Std::Array::{Array, Array_New, Array_Push}; import Std::Array::{Array, Array_New, Array_Push};
import Std::Os::{Os_GetEnv}; import Std::Os::{Os_GetEnv};
import Std::String::{String_Len, String_ToInt}; import Std::String::{String_Len, String_ToInt, String_Eq};
func BuildRouter() -> Router { func BuildRouter() -> Router {
var routes: Array<Route> = Array_New<Route>(8); var routes: Array<Route> = Array_New<Route>(8);
@@ -42,7 +42,10 @@ module Main {
} }
/// Apply optional env overrides for benches / ops: /// 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) { func ApplyEnvConfig(config: *ServerConfig) {
let portEnv: String = Os_GetEnv("NEXUS_PORT"); let portEnv: String = Os_GetEnv("NEXUS_PORT");
if String_Len(portEnv) > 0 { if String_Len(portEnv) > 0 {
@@ -66,6 +69,43 @@ module Main {
if String_Len(pubEnv) > 0 { if String_Len(pubEnv) > 0 {
config.publicDir = pubEnv; 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 { func Main() -> int {
+175 -31
View File
@@ -1,12 +1,17 @@
module Server { module Server {
import Std::Io::{Print, PrintLine, PrintInt}; 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::Net::{
import Std::String::{String_Len, String_StartsWith}; 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::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
import Std::Array::{Array_Drop}; import Std::Array::{Array_Drop};
import Std::Os::{Os_InstallStopHandlers, Os_ShouldStop, Os_SetStopListenFd};
import Std::Time::{Time_NowMs};
import Config::{ServerConfig}; 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 Errors::{ParseResult};
import Parser::{ParseRequest}; import Parser::{ParseRequest};
import Router::{Router, Router_Dispatch}; import Router::{Router, Router_Dispatch};
@@ -21,8 +26,11 @@ module Server {
/// Cap requests per TCP connection (safety + fair scheduling). /// Cap requests per TCP connection (safety + fair scheduling).
const MAX_KEEPALIVE_REQUESTS: int = 1000; 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 { pub struct ConnectionTask {
fd: int; fd: int;
tls: *void;
} }
pub func BuildResponse(resp: HttpResponse, keepAlive: bool) -> String { 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, Http_StatusText(resp.statusCode));
bux_sb_append(sb, "\r\n"); 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 { if bux_strlen(resp.extraHeaders) > 0 {
bux_sb_append(sb, resp.extraHeaders); bux_sb_append(sb, resp.extraHeaders);
@@ -68,42 +76,99 @@ module Server {
return result; return result;
} }
/// Serve one TCP client: zero or more HTTP requests (HTTP/1.1 keep-alive). func ConnRecv(fd: int, tls: *void, maxLen: int) -> String {
pub func HandleConnection(fd: int, router: Router) { 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; var reqCount: int = 0;
let maxRecv: int = config.maxBodyBytes;
while reqCount < MAX_KEEPALIVE_REQUESTS { 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 { if String_Len(raw) == 0 {
return; return;
} }
// HTTP/2 preface detection — one-shot response, then close if String_Len(raw) as int >= maxRecv {
if String_StartsWith(raw, "PRI * HTTP/2.0") { let resp: HttpResponse = Http_NewResponse(413, "text/plain; charset=utf-8", "Payload Too Large");
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", ConnSend(fd, tls, BuildResponse(resp, false));
"HTTP/2 detected — full support planned for future release.\r\n");
Net_Send(fd, BuildResponse(resp, false));
return; 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); let parsed: ParseResult = ParseRequest(raw);
var keepAlive: bool = false; var keepAlive: bool = false;
var statusOut: int = 400;
var methodName: String = "?";
var pathOut: String = "-";
if parsed.tag == ParseResult_Ok { if parsed.tag == ParseResult_Ok {
var req: HttpRequest = parsed.data.Ok_0; 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<HeaderEntry>(&req.headers);
return;
}
keepAlive = Request_WantsKeepAlive(&req); keepAlive = Request_WantsKeepAlive(&req);
// Last request on the connection quota must close
if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS { if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS {
keepAlive = false; keepAlive = false;
} }
let resp: HttpResponse = Router_Dispatch(router, req); let resp: HttpResponse = Router_Dispatch(router, req);
Net_Send(fd, BuildResponse(resp, keepAlive)); statusOut = resp.statusCode;
// Free header buffer (moved into req at parse; no Drop on nested fields) ConnSend(fd, tls, BuildResponse(resp, keepAlive));
Array_Drop<HeaderEntry>(&req.headers); Array_Drop<HeaderEntry>(&req.headers);
} else { } else {
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request"); 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; return;
} }
if config.accessLog {
let dt: int64 = Time_NowMs() - t0;
AccessLog(methodName, pathOut, statusOut, dt);
}
reqCount = reqCount + 1; reqCount = reqCount + 1;
if !keepAlive { if !keepAlive {
return; return;
@@ -114,12 +179,19 @@ module Server {
pub struct WorkerCtx { pub struct WorkerCtx {
taskQueue: *Channel<ConnectionTask>; taskQueue: *Channel<ConnectionTask>;
router: Router; router: Router;
config: ServerConfig;
} }
pub func Worker(ctx: *WorkerCtx) { pub func Worker(ctx: *WorkerCtx) {
while true { while true {
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue); let task: ConnectionTask = Channel_Recv<ConnectionTask>(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); Net_Close(task.fd);
} }
} }
@@ -127,29 +199,78 @@ module Server {
pub struct AcceptorCtx { pub struct AcceptorCtx {
serverFd: int; serverFd: int;
taskQueue: *Channel<ConnectionTask>; taskQueue: *Channel<ConnectionTask>;
workerCount: int;
tlsCtx: *void;
} }
pub func Acceptor(ctx: *AcceptorCtx) { pub func Acceptor(ctx: *AcceptorCtx) {
while true { while !Os_ShouldStop() {
let fd: int = Net_Accept(ctx.serverFd); let fd: int = Net_Accept(ctx.serverFd);
if fd >= 0 { 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<ConnectionTask>(ctx.taskQueue, task); Channel_Send<ConnectionTask>(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<ConnectionTask>(ctx.taskQueue, poison);
i = i + 1;
}
PrintLine("acceptor: stop — workers poisoned");
}
pub func RunServer(config: ServerConfig, router: Router) -> int { pub func RunServer(config: ServerConfig, router: Router) -> int {
PrintLine("================================================"); PrintLine("================================================");
PrintLine(" Nexus HTTP Server v0.3.0"); PrintLine(" Nexus HTTP Server v0.6.0");
PrintLine(" HTTP/1.1 keep-alive + thread-pool"); PrintLine(" HTTP/1.1 + TLS/mTLS + keep-alive + graceful stop");
PrintLine(" Built with Bux"); PrintLine(" Built with Bux");
PrintLine("================================================"); PrintLine("================================================");
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(); let serverFd: int = Net_Create();
if serverFd < 0 { if serverFd < 0 {
PrintLine("FATAL: socket() failed"); PrintLine("FATAL: socket() failed");
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
return 1; return 1;
} }
@@ -161,43 +282,66 @@ module Server {
Print("FATAL: bind failed: "); Print("FATAL: bind failed: ");
PrintLine(Net_LastError()); PrintLine(Net_LastError());
Net_Close(serverFd); Net_Close(serverFd);
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
return 1; return 1;
} }
if !Net_Listen(serverFd, config.backlog) { if !Net_Listen(serverFd, config.backlog) {
PrintLine("FATAL: listen() failed"); PrintLine("FATAL: listen() failed");
Net_Close(serverFd); Net_Close(serverFd);
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
return 1; return 1;
} }
Os_SetStopListenFd(serverFd);
if config.tlsEnabled {
Print("Listening on https://");
} else {
Print("Listening on http://"); Print("Listening on http://");
}
Print(config.bindAddr); Print(config.bindAddr);
Print(":"); Print(":");
PrintInt(config.port); PrintInt(config.port);
PrintLine(""); PrintLine("");
PrintInt(config.workerCount); 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("Endpoints: / /api/health /api/info /ws");
PrintLine("Press Ctrl+C to stop."); PrintLine("Stop: Ctrl+C / SIGTERM (graceful).");
PrintLine(""); PrintLine("");
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64); let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
let workerCtx: WorkerCtx = WorkerCtx { taskQueue: &taskQueue, router: router }; let workerCtx: WorkerCtx = WorkerCtx {
let acceptorCtx: AcceptorCtx = AcceptorCtx { serverFd: serverFd, taskQueue: &taskQueue }; 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; var i: int = 0;
while i < config.workerCount - 1 { while i < config.workerCount {
spawn Worker(&workerCtx); spawn Worker(&workerCtx);
i = i + 1; i = i + 1;
} }
// Spawn acceptor Acceptor(&acceptorCtx);
spawn Acceptor(&acceptorCtx);
// Main thread works too Os_SetStopListenFd(-1);
Worker(&workerCtx); if serverFd >= 0 {
Net_Close(serverFd);
}
if tlsCtx != null as *void {
Tls_CtxFree(tlsCtx);
}
PrintLine("nexus: acceptor stopped — exit");
return 0; return 0;
} }
+221 -26
View File
@@ -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 lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
import source_location import source_location
import fmt import fmt
@@ -12,11 +12,19 @@ type
cmOn cmOn
cmOff 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 GlobalOptions* = object
color*: ColorMode color*: ColorMode
quiet*: bool quiet*: bool
verbose*: 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 <triple>: cross-compile (e.g. aarch64-linux-gnu)
proc printUsage*() = proc printUsage*() =
echo """Bux Programming Language (bootstrap compiler) echo """Bux Programming Language (bootstrap compiler)
@@ -44,22 +52,32 @@ Command options:
fmt --check Exit 1 if any file would be reformatted (CI) fmt --check Exit 1 if any file would be reformatted (CI)
doc --out <file> Write docs to file (default: stdout) doc --out <file> Write docs to file (default: stdout)
add --path / --git Explicit source; else resolve via registry 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 --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 Local path or http(s):// URL to registry.toml
BUX_REGISTRY_REFRESH=1 Force re-download of HTTP index cache 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_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: Global options:
--color <auto|on|off> Control colored output (default: auto) --color <auto|on|off> Control colored output (default: auto)
-q, --quiet Suppress non-error output -q, --quiet Suppress non-error output
-v, --verbose Verbose output -v, --verbose Verbose output
--release Optimize (-O2), omit -g and #line maps --release Optimize (-O2), omit -g and #line maps
--static Fully-static link + thin runtime (containers / distroless)
--target <triple> Cross-compile (e.g. aarch64-linux-gnu)
""" """
proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] = 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.rest = @[]
result.ok = true result.ok = true
var i = 0 var i = 0
@@ -85,6 +103,17 @@ proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq
result.opts.verbose = true result.opts.verbose = true
elif arg == "--release": elif arg == "--release":
result.opts.release = true 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: else:
result.rest.add(arg) result.rest.add(arg)
inc i inc i
@@ -95,6 +124,80 @@ proc shouldUseColor(opts: GlobalOptions): bool =
of cmOff: false of cmOff: false
of cmAuto: terminal.isatty(stdout) 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 <triple>-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) = proc printError(msg: string, useColor: bool) =
if useColor: if useColor:
stdout.setForegroundColor(fgRed) stdout.setForegroundColor(fgRed)
@@ -487,9 +590,87 @@ proc cmdSearch*(args: seq[string], opts: GlobalOptions): int =
echo &" {p.name} {p.version} — {desc}" echo &" {p.name} {p.version} — {desc}"
return 0 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 = proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts) 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() let root = getCurrentDir()
if lockedOnly:
return verifyLockedInstall(root, useColor, opts)
let manifestPath = root / "bux.toml" let manifestPath = root / "bux.toml"
if not fileExists(manifestPath): if not fileExists(manifestPath):
printError("no bux.toml found", useColor) printError("no bux.toml found", useColor)
@@ -510,11 +691,12 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
return 1 return 1
# Read dependency manifest # Read dependency manifest
let depManifestPath = absPath / "bux.toml" let depManifestPath = absPath / "bux.toml"
let csum = packageChecksum(absPath)
if fileExists(depManifestPath): if fileExists(depManifestPath):
let depMan = loadManifest(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: 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: if not opts.quiet:
printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor) printInfo(&"Resolved path dependency '{dep.name}' from {absPath}", useColor)
of dkGit: of dkGit:
@@ -530,7 +712,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
if not opts.quiet: if not opts.quiet:
printInfo(&"Using cached '{dep.name}' from {depDir}", useColor) printInfo(&"Using cached '{dep.name}' from {depDir}", useColor)
# Lock stores git URL; build loads from cache by name # 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: of dkVersion:
# Registry lookup (E.1) # Registry lookup (E.1)
if reg.path.len == 0: 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) printError(&"package '{dep.name}' not found in registry", useColor)
return 1 return 1
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath): 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: if not opts.quiet:
printInfo(&"Resolved '{dep.name}' {pkg.version} → {pkg.resolvedPath}", useColor) printInfo(&"Resolved '{dep.name}' {pkg.version} → {pkg.resolvedPath}", useColor)
elif isGitSource(pkg.source): elif isGitSource(pkg.source):
@@ -553,7 +737,8 @@ proc cmdInstall*(args: seq[string], opts: GlobalOptions): int =
if code != 0: if code != 0:
printError(&"failed to clone {pkg.source}: {outp}", useColor) printError(&"failed to clone {pkg.source}: {outp}", useColor)
return 1 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: if not opts.quiet:
printInfo(&"Resolved '{dep.name}' {pkg.version} → git {pkg.source}", useColor) printInfo(&"Resolved '{dep.name}' {pkg.version} → git {pkg.source}", useColor)
else: else:
@@ -750,14 +935,25 @@ proc mergeDecls(stdlibDecls: seq[Decl], userDecls: seq[Decl]): seq[Decl] =
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
var opts = opts var opts = opts
var pathArgs: seq[string] = @[] var pathArgs: seq[string] = @[]
for a in args: var i = 0
while i < args.len:
let a = args[i]
if a == "--release": if a == "--release":
opts.release = true 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("-"): elif a.startsWith("-"):
# ignore unknown flags for forward-compat; keep path-like later # ignore unknown flags for forward-compat
discard discard
else: else:
pathArgs.add(a) pathArgs.add(a)
inc i
let useColor = shouldUseColor(opts) let useColor = shouldUseColor(opts)
let root = if pathArgs.len > 0: absolutePath(pathArgs[0]) else: getCurrentDir() let root = if pathArgs.len > 0: absolutePath(pathArgs[0]) else: getCurrentDir()
let (pctx, status) = prepareProject(root, useColor, opts) let (pctx, status) = prepareProject(root, useColor, opts)
@@ -805,11 +1001,11 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
return 1 return 1
let baseDir = stdlibDir.parentDir() let baseDir = stdlibDir.parentDir()
# Windows / BUX_RUNTIME=win → minimal runtime (no pthread/OpenSSL). # Runtime pick: full POSIX | minimal (Linux static/embed) | win (historical).
# Full POSIX runtime is rt/runtime.c. # See resolveRuntimeFlavor — BUX_RUNTIME, --static, --target.
let forceWinRt = getEnv("BUX_RUNTIME") == "win" or getEnv("BUX_RUNTIME") == "windows" let flavor = resolveRuntimeFlavor(opts)
let useWinRt = forceWinRt or (when defined(windows): true else: false) let thinRt = isThinRuntime(flavor)
let runtimeName = if useWinRt: "runtime_win.c" else: "runtime.c" let runtimeName = runtimeFileName(flavor)
let runtimeSrc = baseDir / "rt" / runtimeName let runtimeSrc = baseDir / "rt" / runtimeName
if fileExists(runtimeSrc): if fileExists(runtimeSrc):
copyFile(runtimeSrc, runtimeDst) copyFile(runtimeSrc, runtimeDst)
@@ -830,26 +1026,25 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let outputFile = buildDir / (outputName & exeSuffix) let outputFile = buildDir / (outputName & exeSuffix)
let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g" let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g"
let extraCflags = getEnv("BUX_CFLAGS") let extraCflags = getEnv("BUX_CFLAGS")
let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags var cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags
# Host C toolchain + link flags let doStatic = wantStaticLink(opts)
let envCc = getEnv("BUX_CC") if doStatic:
let ccBin = cflags = cflags & " -static"
if envCc.len > 0: envCc # Host / cross C toolchain + link flags
else: let ccBin = resolveCCompiler(opts)
when defined(windows): "gcc" cflags = cflags & cTargetFlags(opts, ccBin)
else: "cc"
let ldStable = let ldStable =
when defined(linux): when defined(linux):
if useWinRt: "" else: " -Wl,--build-id=none" if thinRt: "" else: " -Wl,--build-id=none"
else: else:
"" ""
# Note: -l libs must come *after* .c/.o inputs (GNU ld left-to-right). # Note: -l libs must come *after* .c/.o inputs (GNU ld left-to-right).
let (hostCflags, hostLibs) = let (hostCflags, hostLibs) =
if useWinRt: if thinRt:
# gc-sections drops mono stdlib that is never called (crypto/tasks, …) # gc-sections drops mono stdlib that is never called (crypto/tasks, …)
(" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm") (" -ffunction-sections -fdata-sections", " -Wl,--gc-sections -lm")
else: else:
(" -pthread" & ldStable, " -lm -lcrypto") (" -pthread" & ldStable, " -lm -lssl -lcrypto")
let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1" let ccCmd = &"{ccBin} {cflags}{hostCflags} -o {outputFile} {cFile} {runtimeDst} {ioDst}{hostLibs} 2>&1"
if opts.verbose: if opts.verbose:
printInfo(&"running: {ccCmd}", useColor) printInfo(&"running: {ccCmd}", useColor)
+170
View File
@@ -98,6 +98,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]): 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 = proc resolvePtrAlias(ctx: LowerCtx, name: string): string =
## Follow `p → bag` aliases (depth-limited). ## Follow `p → bag` aliases (depth-limited).
@@ -173,9 +174,174 @@ proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
of ekTuple: of ekTuple:
for e in expr.exprTupleElements: for e in expr.exprTupleElements:
ctx.markMovedOutFromAst(e) 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: else:
discard 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 = proc shouldSkipDrop(ctx: LowerCtx, dropNode: HirNode, skipName: string): bool =
## Skip Drop for explicit skipName or any moved-out local. ## Skip Drop for explicit skipName or any moved-out local.
let target = dropTargetName(dropNode) let target = dropTargetName(dropNode)
@@ -1461,6 +1627,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
return hirBinary(expr.exprBinaryOp, left, right, typ, loc) return hirBinary(expr.exprBinaryOp, left, right, typ, loc)
of ekCall: of ekCall:
# Cross-function pointer ownership (before any lowering side effects)
ctx.markCrossFuncPtrMoves(expr)
# Method call desugaring: obj.method(args) → Type_method(obj, args) # Method call desugaring: obj.method(args) → Type_method(obj, args)
if expr.exprCallCallee.kind == ekField: if expr.exprCallCallee.kind == ekField:
let methodName = expr.exprCallCallee.exprFieldName let methodName = expr.exprCallCallee.exprFieldName
@@ -2006,6 +2174,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
case stmt.kind case stmt.kind
of skExpr: of skExpr:
if stmt.stmtExpr != nil:
ctx.markMovedOutFromAst(stmt.stmtExpr)
return ctx.flushPending(ctx.lowerExpr(stmt.stmtExpr)) return ctx.flushPending(ctx.lowerExpr(stmt.stmtExpr))
of skLet: of skLet:
+8 -1
View File
@@ -1006,9 +1006,16 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
let pat = exprToPattern(arg) let pat = exprToPattern(arg)
if pat == nil: return nil if pat == nil: return nil
return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat) return Expr(kind: ekMacroPat, loc: arg.loc, exprMacroPat: pat)
of mfkExpr, mfkTt: of mfkExpr:
if arg.kind in {ekMacroStmt, ekMacroPat}: return nil if arg.kind in {ekMacroStmt, ekMacroPat}: return nil
return arg 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 = proc fragMatches(k: MacroFragKind, arg: Expr): bool =
## Kind constraint at match time (after arg expand). ## Kind constraint at match time (after arg expand).
+10 -5
View File
@@ -44,12 +44,13 @@ proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
p = p[2 .. ^1] p = p[2 .. ^1]
if not p.isAbsolute: if not p.isAbsolute:
p = indexDir / p p = indexDir / p
pkg.resolvedPath = p.absolutePath # Collapse ../ segments for cleaner lockfiles (session 82)
pkg.resolvedPath = expandFilename(p)
elif pkg.source.startsWith("path:"): elif pkg.source.startsWith("path:"):
var p = pkg.source["path:".len .. ^1] var p = pkg.source["path:".len .. ^1]
if not p.isAbsolute: if not p.isAbsolute:
p = indexDir / p p = indexDir / p
pkg.resolvedPath = p.absolutePath pkg.resolvedPath = expandFilename(p)
pkg.source = "file:" & pkg.resolvedPath pkg.source = "file:" & pkg.resolvedPath
proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] = 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: if not force and fileExists(cachePath) and cachedUrl == url:
return cachePath.absolutePath 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 var ok = false
if findExe("curl").len > 0: 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) let (_, code) = execCmdEx(cmd)
ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0
elif findExe("wget").len > 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) let (_, code) = execCmdEx(cmd)
ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0 ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0
else: else:
+20 -1
View File
@@ -745,7 +745,20 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue =
of ekLiteral: of ekLiteral:
case expr.exprLit.kind case expr.exprLit.kind
of tkIntLiteral: 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: of tkBoolLiteral:
return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true") return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true")
of tkStringLiteral: of tkStringLiteral:
@@ -786,6 +799,12 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue =
of tkPercent: of tkPercent:
if right.intVal != 0: if right.intVal != 0:
return CtValue(kind: ctkInt, intVal: left.intVal mod right.intVal) 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 tkEq: return CtValue(kind: ctkBool, boolVal: left.intVal == right.intVal)
of tkNe: 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) of tkLt: return CtValue(kind: ctkBool, boolVal: left.intVal < right.intVal)
+44 -14
View File
@@ -20,7 +20,7 @@ On macOS:
brew install nim gcc make openssl 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. Build output goes to `build/` by default.
### Cross-Compilation ### Cross-Compilation, static, and thin runtime (Linux-first)
Use `--target <triple>` 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 ```bash
# Cross-compile for ARM Linux # Thin runtime (no pthread / OpenSSL / sockets) — good for CLI & embed
./buxc build --target aarch64-linux-gnu BUX_RUNTIME=minimal ./buxc build
# Cross-compile for x86_64 Linux (explicit) # Fully-static binary (implies minimal runtime; container / distroless friendly)
./buxc build --target x86_64-linux-gnu ./buxc --static --release build
# same: BUX_STATIC=1 ./buxc --release build
# Cross-compile and run project build # Cross-compile for ARM64 Linux (prefers aarch64-linux-gnu-gcc, else clang -target)
./buxc project --target x86_64-linux-gnu ./buxc --static --release --target aarch64-linux-gnu build
./buxc run --target aarch64-linux-gnu
# 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 <triple>` | 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-registry # package registry (local + HTTP index)
make test-apps # showcase apps build + simpledb/jwt CLI smoke (in `make test`) 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-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-registry # package registry local + HTTP (in `make test`)
make test-selfhost-smoke # buxc2: move_field + multi-file #line (in `make test`) make test-selfhost-smoke # buxc2: move_field + multi-file #line (in `make test`)
make test-lsp # hover + references/rename + call hierarchy 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 | | `build` | ubuntu | `make build` → upload `buxc` artifact |
| `unit` | ubuntu | `fmt-check` + `test-unit` (reuse artifact) | | `unit` | ubuntu | `fmt-check` + `test-unit` (reuse artifact) |
| `examples` | ubuntu | `test-examples` (full list) | | `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` | | `apps` | ubuntu | `test-apps` |
| `selfhost` | ubuntu | `test-selfhost-smoke` | | `selfhost` | ubuntu | `test-selfhost-smoke` |
| `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) | | `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) |
@@ -331,8 +360,9 @@ bux/
│ ├── Task.bux │ ├── Task.bux
│ └── Channel.bux │ └── Channel.bux
├── rt/ # C runtime ├── rt/ # C runtime
│ ├── runtime.c # full POSIX + OpenSSL (Unix) │ ├── runtime.c # full POSIX + OpenSSL (Unix default)
│ ├── runtime_win.c # MinGW minimal (Windows / BUX_RUNTIME=win) │ ├── runtime_minimal.c # thin: no pthread/net/crypto (static/embed)
│ ├── runtime_win.c # MinGW historical (BUX_RUNTIME=win)
│ └── io.c │ └── io.c
├── examples/ # Example programs ├── examples/ # Example programs
├── tests/ # Unit tests (Nim) ├── tests/ # Unit tests (Nim)
+28 -6
View File
@@ -877,9 +877,30 @@ return p.items; // same as (*p).items
Nested paths work the same: `p.inner.items` resolves `p → outer` then path Nested paths work the same: `p.inner.items` resolves `p → outer` then path
`inner.items`. `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<int> {
return p.items;
}
func Caller() {
let bag: Bag = …;
let items: Array<int> = 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` / Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux` /
`examples/move_field_remaining.bux` / `examples/move_field_nested.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 #### Manual Drop and non-Drop types
@@ -891,10 +912,11 @@ Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux` /
#### Limits (honest) #### Limits (honest)
- Partial field moves skip the **parent** `Type_Drop` and drop **remaining** - Partial field moves skip the **parent** `Type_Drop` and drop **remaining**
droppable fields individually, including nested paths `a.b.c` and pointer droppable fields individually, including nested paths `a.b.c`, local pointer
aliases `p = &owner` (sessions 70/73/74). aliases `p = &owner`, and **cross-function** `&owner` args when the callee
- Pointer aliases are tracked for **local** `p = &local` only (not parameters body is visible (sessions 70/73/74/76).
that point at caller-owned data across function boundaries). - 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 - Interface Drop uses a static `TypeName_Drop` symbol (zero cost), not dynamic
dispatch through a vtable. dispatch through a vtable.
- Double-free bugs in **unchecked** code that manually free *and* auto-drop are - Double-free bugs in **unchecked** code that manually free *and* auto-drop are
@@ -1262,7 +1284,7 @@ macro! with_acc {
|------|---------| |------|---------|
| `expr` | any expression | | `expr` | any expression |
| `ident` | bare identifier (`ekIdent`) | | `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 | | `literal` / `lit` | int/float/string/char/bool literal only |
| `block` | block expression `{ … }` | | `block` | block expression `{ … }` |
| `stmt` | one statement (`let`/`if`/… or expression-stmt) | | `stmt` | one statement (`let`/`if`/… or expression-stmt) |
+39 -3
View File
@@ -50,12 +50,15 @@ Default locations (first hit wins):
2. `~/.bux/registry.toml` 2. `~/.bux/registry.toml`
3. `config/registry.toml` next to the Bux repo / compiler 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 `~/.bux/cache/registry_http.toml`. Set `BUX_REGISTRY_REFRESH=1` to force
re-download. Relative `file:` / `path:` entries in a remote index resolve re-download. Relative `file:` / `path:` entries in a remote index resolve
against the cache directory — prefer **absolute paths** or **git URLs** for against the cache directory — prefer **absolute paths** or **git URLs** for
HTTP-served registries. 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: Format:
```toml ```toml
@@ -100,12 +103,45 @@ bux add greet 0.1.1
bux add utils --path "../utils" bux add utils --path "../utils"
bux add network --git "https://github.com/bux-lang/network" 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 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 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 8081):
```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.
--- ---
+198 -6
View File
@@ -1,8 +1,10 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-21 > **Дата:** 2026-07-23
> **Текущо:** v0.5.x — macros, field-move via pointers, Windows hello > **Текущо:** v0.5.x — CI cloud smokes + registry path cleanup (session 82)
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден 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. 3. **Selfhost като dogfood** — компилаторът и apps (`nexus`, `boko`) са proof.
4. **Инструменти** — fmt, test, LSP, package install без ръчна магия. 4. **Инструменти** — fmt, test, LSP, package install без ръчна магия.
5. **Стабилна спецификация** — LanguageRef = реалното поведение. 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 7579 |
| **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 <triple>` → prefers `<triple>-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 <name>` / `add <name> <version|url>`
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 7581 platform stack
**Verified:** local smokes previously green; CI wiring ready for GHA.
---
## Следващи стъпки ## Следващи стъпки
1. Windows: more examples (strings/ownership) on MinGW; optional Win OpenSSL ### P0 — Compiler / language
2. Macro: true token-tree `tt` / nested pattern rewrite depth
3. Cross-function pointer ownership transfer (callee `*Bag` param) 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 7576
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
+82
View File
@@ -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;
}
+21
View File
@@ -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"]
+18
View File
@@ -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"]
+26
View File
@@ -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"]
+20
View File
@@ -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"]
+78
View File
@@ -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;
}
+44
View File
@@ -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;
}
+84
View File
@@ -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<int>,
tag: Tracked
}
func Bag_Drop(self: *Bag) {
Array_Drop<int>(&self.items);
Tracked_Drop(&self.tag);
}
// Callee moves p.items out of the pointee — caller passed &bag.
func TakeItems(p: *Bag) -> Array<int> {
return p.items;
}
func NestedTake(p: *Bag) -> Array<int> {
let moved: Array<int> = p.items;
return moved;
}
// Caller holds bag and transfers via &bag into TakeItems.
func CallTakeItems(counter: *int) -> int {
var items: Array<int> = Array_New<int>(2);
Array_Push<int>(&items, 1);
Array_Push<int>(&items, 2);
let bag: Bag = Bag {
items: items,
tag: Tracked { id: 1, counter: counter }
};
let taken: Array<int> = TakeItems(&bag);
Test_AssertTrue(Array_Len<int>(&taken) == 2);
Test_AssertTrue(Array_Get<int>(&taken, 0) == 1);
return Array_Len<int>(&taken) as int;
}
func CallNestedTake(counter: *int) -> int {
var items2: Array<int> = Array_New<int>(1);
Array_Push<int>(&items2, 9);
let bag2: Bag = Bag {
items: items2,
tag: Tracked { id: 2, counter: counter }
};
let taken2: Array<int> = NestedTake(&bag2);
Test_AssertTrue(Array_Get<int>(&taken2, 0) == 9);
return Array_Get<int>(&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;
}
+48
View File
@@ -10,6 +10,16 @@ module Std::Net {
extern func bux_socket_close(fd: int) -> int; extern func bux_socket_close(fd: int) -> int;
extern func bux_socket_error() -> String; 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. */ /* Create a TCP socket. Returns -1 on error. */
func Net_Create() -> int { func Net_Create() -> int {
return bux_socket_create(); return bux_socket_create();
@@ -59,4 +69,42 @@ module Std::Net {
func Net_LastError() -> String { func Net_LastError() -> String {
return bux_socket_error(); 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();
}
} }
+19
View File
@@ -7,6 +7,9 @@ module Std::Os {
extern func bux_getcwd() -> String; extern func bux_getcwd() -> String;
extern func bux_chdir(path: String) -> int; extern func bux_chdir(path: String) -> int;
extern func bux_exit(code: 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 { func Os_ArgsCount() -> int {
return bux_argc(); return bux_argc();
@@ -37,4 +40,20 @@ module Std::Os {
bux_exit(code); 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);
}
} }
+155
View File
@@ -1517,6 +1517,41 @@ int64_t bux_time_ms(void) {
return 0; 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) { int64_t bux_time_us(void) {
struct timespec ts; struct timespec ts;
if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { if (clock_gettime(CLOCK_REALTIME, &ts) == 0) {
@@ -1652,6 +1687,126 @@ const char* bux_socket_error(void) {
return strerror(errno); return strerror(errno);
} }
/* ============================================================================
* TLS server primitives (OpenSSL) session 78
* opaque SSL_CTX* / SSL* as void*
* ============================================================================ */
#include <openssl/ssl.h>
#include <openssl/err.h>
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 * Test / Assert primitives
* ============================================================================ */ * ============================================================================ */
+760
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <errno.h>
#if defined(_WIN32) || defined(_WIN64)
# include <windows.h>
# include <direct.h>
# include <io.h>
# define BUX_IS_WIN 1
# define bux_mkdir_one(p) _mkdir(p)
#else
# include <unistd.h>
# include <sys/stat.h>
# 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;
}
+22
View File
@@ -471,6 +471,10 @@ int bux_chdir(const char* path) {
} }
/* ── Time ─────────────────────────────────────────────────────────────── */ /* ── 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) { int64_t bux_time_ms(void) {
#if BUX_IS_WIN #if BUX_IS_WIN
FILETIME ft; 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; } int bux_socket_close(int fd) { (void)fd; return -1; }
const char* bux_socket_error(void) { return "sockets not available on this platform"; } 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) ──────────────────────────────────────── */ /* ── Crypto — stubs (no OpenSSL) ──────────────────────────────────────── */
static void zero_out(unsigned char* out, int n) { static void zero_out(unsigned char* out, int n) {
if (out && n > 0) memset(out, 0, (size_t)n); if (out && n > 0) memset(out, 0, (size_t)n);
+166
View File
@@ -514,8 +514,174 @@ module CBackend {
CBE_MarkMovedFromNodeHint(cbe, node, ""); 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) { func CBE_MarkMovedFromNodeHint(cbe: *CEmitter, node: *HirNode, valueTypeHint: String) {
if node == null as *HirNode { return; } if node == null as *HirNode { return; }
if node.kind == hCall {
CBE_MarkCrossFuncFromCall(cbe, node);
return;
}
if node.kind == hVar { if node.kind == hVar {
CBE_AddMoved(cbe, node.strValue); CBE_AddMoved(cbe, node.strValue);
return; return;
+584 -153
View File
@@ -15,6 +15,7 @@ module Cli {
extern func bux_run_nim(nim_file: String, out_bin: String) -> int; 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_list_dir(dir: String, ext: String, out_count: *int) -> *String;
extern func bux_system(cmd: String) -> int; 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_getenv(name: String) -> String;
extern func bux_setenv(name: String, value: String) -> int; extern func bux_setenv(name: String, value: String) -> int;
extern func bux_cc_ld_stable() -> String; extern func bux_cc_ld_stable() -> String;
@@ -37,6 +38,175 @@ module Cli {
return bux_dir_exists(path) != 0; 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) // Diagnostic formatting (Rust-style errors with snippets)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -289,11 +459,11 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St
// Build command // 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 // If building the standard project entry point, use full project build
// which merges stdlib and supports multi-file projects // which merges stdlib and supports multi-file projects
if String_Eq(srcPath, "src/Main.bux") || String_Eq(srcPath, "src/main.bux") { 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") { if rc == 0 && !String_Eq(outPath, "build/main") {
// Rename output if custom path was requested // Rename output if custom path was requested
bux_system(String_Concat(String_Concat("mv build/main ", outPath), " 2>/dev/null || true")); 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 "); Print(" → C written to ");
PrintLine(cFile); 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..."); PrintLine("Compiling C...");
var cmdBuf: StringBuilder = StringBuilder_NewCap(512); let linkRc: int = Cli_LinkProgram(cFile, outPath, ".", targetTriple, isRelease, isStatic);
var optFlags: String = "-O0 -g"; if linkRc != 0 {
if isRelease { return linkRc;
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;
} }
Print(" → Binary: "); Print(" → Binary: ");
PrintLine(outPath); PrintLine(outPath);
@@ -922,6 +1033,40 @@ func Cli_Init() -> int {
// Test command — build and run tests // 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 // Add — add a dependency to bux.toml
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -952,6 +1097,48 @@ func Cli_Add(pkgName: String, url: String) -> int {
return 0; 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 <url>");
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 // Remove — remove a dependency from bux.toml
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -982,6 +1169,242 @@ func Cli_Remove(pkgName: String) -> int {
return 0; 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/<name>
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/<name> 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/ // 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. // Without --filter, build and run the project's own Main first.
// With --filter, only run matching tests/*.bux files. // With --filter, only run matching tests/*.bux files.
if String_Eq(filter, "") { if String_Eq(filter, "") {
let mainRc: int = Cli_BuildProject(projectDir, "", false); let mainRc: int = Cli_BuildProject(projectDir, "", false, false);
if mainRc != 0 { if mainRc != 0 {
PrintLine("Main test build failed"); PrintLine("Main test build failed");
return mainRc; 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 { if buildRc != 0 {
PrintLine("FAIL │"); PrintLine("FAIL │");
failed = failed + 1; failed = failed + 1;
@@ -1467,7 +1890,7 @@ func Cli_Test(projectDir: String, filter: String) -> int {
return 0; 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")); let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml"));
var outName: String = man.name; var outName: String = man.name;
if String_Eq(outName, "") { if String_Eq(outName, "") {
@@ -1629,27 +2052,37 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
PrintLine(""); PrintLine("");
} }
// Merge dependency declarations (deps shadow stdlib, user shadows deps) // Merge dependency declarations (path or deps/<name>; shadow stdlib)
if man.depCount > 0 { if man.depCount > 0 {
let depsDir: String = bux_path_join(projectDir, "deps");
var di: int = 0; var di: int = 0;
while di < man.depCount { while di < man.depCount {
var depName: String = ""; var depName: String = "";
if di == 0 { depName = man.depName0; } var depUrl: String = "";
else if di == 1 { depName = man.depName1; } if di == 0 { depName = man.depName0; depUrl = man.depUrl0; }
else if di == 2 { depName = man.depName2; } else if di == 1 { depName = man.depName1; depUrl = man.depUrl1; }
else if di == 3 { depName = man.depName3; } else if di == 2 { depName = man.depName2; depUrl = man.depUrl2; }
else if di == 4 { depName = man.depName4; } else if di == 3 { depName = man.depName3; depUrl = man.depUrl3; }
else if di == 5 { depName = man.depName5; } else if di == 4 { depName = man.depName4; depUrl = man.depUrl4; }
else if di == 6 { depName = man.depName6; } else if di == 5 { depName = man.depName5; depUrl = man.depUrl5; }
else if di == 7 { depName = man.depName7; } else if di == 6 { depName = man.depName6; depUrl = man.depUrl6; }
let depSrcDir: String = bux_path_join(bux_path_join(depsDir, depName), "src"); else if di == 7 { depName = man.depName7; depUrl = man.depUrl7; }
// Prefer resolved path (absolute / relative / deps/<name>)
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) { if DirExists(depSrcDir) {
var depFileCount: int = 0; var depFileCount: int = 0;
let depFiles: *String = bux_list_dir(depSrcDir, ".bux", &depFileCount); let depFiles: *String = bux_list_dir(depSrcDir, ".bux", &depFileCount);
if depFileCount > 0 { if depFileCount > 0 {
Print("Merging dependency: "); Print("Merging dependency: ");
PrintLine(depName); PrintLine(depName);
Print(" from ");
PrintLine(depSrcDir);
var dfi: int = 0; var dfi: int = 0;
while dfi < depFileCount { while dfi < depFileCount {
Print(" Merging "); Print(" Merging ");
@@ -1661,6 +2094,9 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
dfi = dfi + 1; dfi = dfi + 1;
} }
} }
} else {
Print("WARN: dependency sources not found for ");
PrintLine(depName);
} }
di = di + 1; di = di + 1;
} }
@@ -1742,69 +2178,11 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
Print("C code written to "); Print("C code written to ");
PrintLine(cFile); 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..."); PrintLine("Compiling C...");
let outBin: String = bux_path_join(buildDir, outName); let outBin: String = bux_path_join(buildDir, outName);
var ccBuf: StringBuilder = StringBuilder_NewCap(512); let linkRc2: int = Cli_LinkProgram(cFile, outBin, projectDir, targetTriple, isRelease, isStatic);
var optFlags2: String = "-O0 -g"; if linkRc2 != 0 {
if isRelease { return linkRc2;
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;
} }
Print("Build successful: "); Print("Build successful: ");
@@ -1816,8 +2194,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
// Run command — build project and execute // Run command — build project and execute
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool) -> int { func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool, isStatic: bool) -> int {
let rc: int = Cli_BuildProject(projectDir, targetTriple, isRelease); let rc: int = Cli_BuildProject(projectDir, targetTriple, isRelease, isStatic);
if rc != 0 { if rc != 0 {
return rc; return rc;
} }
@@ -1837,9 +2215,10 @@ func Cli_RunProject(projectDir: String, targetTriple: String, isRelease: bool) -
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func Cli_Run(args: *String, argCount: int) -> int { 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 targetTriple: String = "";
var isRelease: bool = false; var isRelease: bool = false;
var isStatic: bool = Cli_EnvTruthy("BUX_STATIC");
var ai: int = 1; var ai: int = 1;
while ai < argCount { while ai < argCount {
if String_Eq(args[ai], "--target") && ai + 1 < 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; ai = ai - 1;
} else if String_Eq(args[ai], "--release") { } else if String_Eq(args[ai], "--release") {
isRelease = true; 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; var j: int = ai;
while j + 1 < argCount { while j + 1 < argCount {
args[j] = args[j + 1]; args[j] = args[j + 1];
@@ -1869,12 +2256,17 @@ func Cli_Run(args: *String, argCount: int) -> int {
if argCount < 2 { if argCount < 2 {
PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Bux Self-Hosting Compiler v0.2.0");
PrintLine("Usage: buxc <command> [args]"); PrintLine("Usage: buxc <command> [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 <name> Only run tests/*.bux whose name contains <name>"); PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
PrintLine(" search [query] Search package registry");
PrintLine(" install [--locked] Write/verify bux.lock (checksums)");
PrintLine(" add <name> [ver|url] Registry resolve or explicit source");
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)"); PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
PrintLine(" doc --out file.md [path] API docs from /// comments"); PrintLine(" doc --out file.md [path] API docs from /// comments");
PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g"); 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 <triple> Cross-compile (e.g. aarch64-linux-gnu)");
PrintLine(" BUX_CFLAGS / BUX_CC / BUX_RUNTIME / BUX_STATIC / BUX_REGISTRY");
return 0; 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") { if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") {
PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Bux Self-Hosting Compiler v0.2.0");
PrintLine("Usage: buxc <command> [args]"); PrintLine("Usage: buxc <command> [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 <name> Only run tests/*.bux whose name contains <name>"); PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
PrintLine(" search [query] Search package registry");
PrintLine(" install [--locked] Write/verify bux.lock (checksums)");
PrintLine(" add <name> [ver|url] Registry resolve or explicit source");
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)"); PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
PrintLine(" doc --out file.md [path] API docs from /// comments"); PrintLine(" doc --out file.md [path] API docs from /// comments");
PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g"); 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 <triple> Cross-compile (e.g. aarch64-linux-gnu)");
PrintLine(" BUX_CFLAGS / BUX_CC / BUX_RUNTIME / BUX_STATIC / BUX_REGISTRY");
PrintLine("Pipeline modules:"); PrintLine("Pipeline modules:");
PrintLine(" Lexer ✅"); PrintLine(" Lexer ✅");
PrintLine(" Parser ✅"); PrintLine(" Parser ✅");
@@ -1914,12 +2311,31 @@ func Cli_Run(args: *String, argCount: int) -> int {
return Cli_Init(); 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 String_Eq(cmd, "add") {
if argCount < 4 { if argCount < 3 {
PrintLine("Usage: buxc add <name> <url>"); PrintLine("Usage: buxc add <name> [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 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") { if String_Eq(cmd, "remove") {
@@ -1934,6 +2350,21 @@ func Cli_Run(args: *String, argCount: int) -> int {
return Cli_Fetch(); 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") { if String_Eq(cmd, "fmt") {
var dir: String = "."; var dir: String = ".";
var checkOnly: bool = false; var checkOnly: bool = false;
@@ -1980,13 +2411,13 @@ func Cli_Run(args: *String, argCount: int) -> int {
let out: String = "build/main"; let out: String = "build/main";
if argCount >= 3 { src = args[2]; } if argCount >= 3 { src = args[2]; }
if argCount >= 4 { out = args[3]; } 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") { if String_Eq(cmd, "project") {
let dir: String = "."; let dir: String = ".";
if argCount >= 3 { dir = args[2]; } if argCount >= 3 { dir = args[2]; }
return Cli_BuildProject(dir, targetTriple, isRelease); return Cli_BuildProject(dir, targetTriple, isRelease, isStatic);
} }
if String_Eq(cmd, "test") { if String_Eq(cmd, "test") {
@@ -2011,7 +2442,7 @@ func Cli_Run(args: *String, argCount: int) -> int {
if String_Eq(cmd, "run") { if String_Eq(cmd, "run") {
let dir: String = "."; let dir: String = ".";
if argCount >= 3 { dir = args[2]; } if argCount >= 3 { dir = args[2]; }
return Cli_RunProject(dir, targetTriple, isRelease); return Cli_RunProject(dir, targetTriple, isRelease, isStatic);
} }
Print("Unknown command: "); Print("Unknown command: ");
+11 -2
View File
@@ -449,12 +449,21 @@ module MacroExpand {
e.macroPat = pat; e.macroPat = pat;
return e; 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; } if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; }
return aexp; 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 { func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool {
return Macro_CoerceArg(kindStr, aexp) != null as *Expr; return Macro_CoerceArg(kindStr, aexp) != null as *Expr;
} }
+32 -1
View File
@@ -61,7 +61,7 @@ module Manifest {
currentSection = "Package"; currentSection = "Package";
} else if String_StartsWith(line, "[Build]") { } else if String_StartsWith(line, "[Build]") {
currentSection = "Build"; currentSection = "Build";
} else if String_StartsWith(line, "[dependencies]") { } else if String_StartsWith(line, "[dependencies]") || String_StartsWith(line, "[Dependencies]") {
currentSection = "dependencies"; currentSection = "dependencies";
} else { } else {
currentSection = ""; 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(currentSection, "Package") {
if String_Eq(key, "Name") { m.name = val; } if String_Eq(key, "Name") { m.name = val; }
if String_Eq(key, "Version") { m.version = val; } if String_Eq(key, "Version") { m.version = val; }
+380
View File
@@ -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, 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;
}
}
+32
View File
@@ -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"
+32
View File
@@ -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"
+31 -1
View File
@@ -150,6 +150,36 @@ if ! sed -n '/^Array_int TakeViaPtr/,/^}/p' "$TMP/mptr/build/main.c" | grep -q '
fi fi
echo " move_field_ptr: PASS (run + no Bag_Drop + Tracked_Drop remaining)" 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 --- # --- early return Drop counts ---
echo "=== smoke: drop_early_return ===" echo "=== smoke: drop_early_return ==="
mkdir -p "$TMP/de/src" mkdir -p "$TMP/de/src"
@@ -168,4 +198,4 @@ out=$(cd "$TMP/de" && "$BUXC" run .)
echo "$out" | grep -q 'PASS' echo "$out" | grep -q 'PASS'
echo " drop_early_return: 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)"
+117
View File
@@ -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" <<EOF
[Package]
Name = "$name"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
EOF
cp "$src" "$d/src/Main.bux"
echo "$d"
}
pass=0
fail=0
note() { echo "=== $* ==="; }
# ── 1) minimal runtime ──────────────────────────────────────────────────
note "minimal runtime (BUX_RUNTIME=minimal)"
PKG=$(mkpkg hello_min "$ROOT/examples/hello.bux")
trap 'rm -rf "$PKG" "${PKG2:-}" "${PKG3:-}" "${PKG4:-}"' EXIT
export BUX_RUNTIME=minimal
out=$("$BUXC" --quiet run "$PKG" 2>&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"
+86
View File
@@ -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"
+76
View File
@@ -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"
+58
View File
@@ -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"
+93 -10
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Smoke: registry search + add + install + build with greet package (E.1) # Smoke: registry search + add + install + lock reproducibility + HTTPS (E.1 / session 79)
# Also verifies HTTP-fetchable registry index (E.1b).
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUXC="$ROOT/buxc" BUXC="$ROOT/buxc"
@@ -12,11 +11,16 @@ fi
TMP=$(mktemp -d) TMP=$(mktemp -d)
HTTP_PID="" HTTP_PID=""
HTTPS_PID=""
cleanup() { cleanup() {
if [[ -n "$HTTP_PID" ]]; then if [[ -n "$HTTP_PID" ]]; then
kill "$HTTP_PID" 2>/dev/null || true kill "$HTTP_PID" 2>/dev/null || true
wait "$HTTP_PID" 2>/dev/null || true wait "$HTTP_PID" 2>/dev/null || true
fi fi
if [[ -n "$HTTPS_PID" ]]; then
kill "$HTTPS_PID" 2>/dev/null || true
wait "$HTTPS_PID" 2>/dev/null || true
fi
rm -rf "$TMP" rm -rf "$TMP"
} }
trap cleanup EXIT trap cleanup EXIT
@@ -60,20 +64,67 @@ echo "=== bux add greet ==="
grep -q greet bux.toml grep -q greet bux.toml
cat bux.toml cat bux.toml
echo "=== bux install ===" echo "=== bux install (with checksum) ==="
"$BUXC" install "$BUXC" install
test -f bux.lock test -f bux.lock
grep -q greet bux.lock grep -q greet bux.lock
grep -q Checksum bux.lock
cat 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 ===" echo "=== bux run ==="
"$BUXC" run . | tee "$TMP/run.out" "$BUXC" run . | tee "$TMP/run.out"
grep -q "Hello, Bux!" "$TMP/run.out" grep -q "Hello, Bux!" "$TMP/run.out"
# --- HTTP registry index --- # --- HTTP registry index ---
echo "=== HTTP registry index (E.1b) ===" echo "=== HTTP registry index ==="
mkdir -p "$TMP/http" mkdir -p "$TMP/http"
# Absolute file: path so resolution works after download to ~/.bux/cache
cat > "$TMP/http/registry.toml" <<EOF cat > "$TMP/http/registry.toml" <<EOF
[[package]] [[package]]
name = "greet" name = "greet"
@@ -82,14 +133,12 @@ source = "file:$ROOT/registry/packages/greet"
description = "HTTP-served greet package" description = "HTTP-served greet package"
EOF EOF
# Free port via python
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
( (
cd "$TMP/http" cd "$TMP/http"
python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1
) & ) &
HTTP_PID=$! HTTP_PID=$!
# Wait until server responds
for _ in 1 2 3 4 5 6 7 8 9 10; do 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 if curl -fsS "http://127.0.0.1:${PORT}/registry.toml" >/dev/null 2>&1; then
break break
@@ -101,9 +150,43 @@ export BUX_REGISTRY="http://127.0.0.1:${PORT}/registry.toml"
export BUX_REGISTRY_REFRESH=1 export BUX_REGISTRY_REFRESH=1
"$BUXC" search greet | tee "$TMP/http_search.out" "$BUXC" search greet | tee "$TMP/http_search.out"
grep -q greet "$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 unset BUX_REGISTRY_REFRESH
# Second search should hit cache without refresh
"$BUXC" search greet | grep -q greet "$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 - <<PY &
import http.server, ssl, os
os.chdir("$TMP/https")
httpd = http.server.HTTPServer(("127.0.0.1", $SPORT), http.server.SimpleHTTPRequestHandler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("$TMP/https/cert.pem", "$TMP/https/key.pem")
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()
PY
HTTPS_PID=$!
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if curl -kfsS "https://127.0.0.1:${SPORT}/registry.toml" >/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)"
+93
View File
@@ -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" <<EOF
[Package]
Name = "app"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
[Dependencies]
mini = { Path = "$TMP/libpkg" }
EOF
cat > "$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"
+84
View File
@@ -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" <<EOF
[[package]]
name = "greet"
version = "0.1.1"
source = "file:$ROOT/registry/packages/greet"
description = "HTTP-served greet (selfhost)"
EOF
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
python3 -m http.server "$PORT" --bind 127.0.0.1 -d "$TMP/http" >/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"