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
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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
╔══════════════════════════════════════════════╗
|
╔══════════════════════════════════════════════╗
|
||||||
|
|||||||
@@ -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: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
+176
-32
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
Print("Listening on http://");
|
Os_SetStopListenFd(serverFd);
|
||||||
|
|
||||||
|
if config.tlsEnabled {
|
||||||
|
Print("Listening on https://");
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+222
-27
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 80–81):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
buxc2 search [query]
|
||||||
|
buxc2 add greet # registry resolve
|
||||||
|
buxc2 add greet 0.1.1
|
||||||
|
buxc2 install / install --locked
|
||||||
|
make test-selfhost-install
|
||||||
|
make test-selfhost-registry # search + add + HTTP index + run
|
||||||
|
```
|
||||||
|
|
||||||
|
`$BUX_REGISTRY` / HTTP(S) cache / `BUX_REGISTRY_INSECURE` match bootstrap.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+199
-7
@@ -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 75–79 |
|
||||||
|
| **Embedded** | Cross (`--target`), CTFE tables, **thin runtime**, no-GC story | ✅ minimal + aarch64 + ctfe_crc (75); 🔧 riscv / bare-metal spike |
|
||||||
|
| **Windows** | Не е product target | ⛔ no further investment (existing MinGW hello = historical) |
|
||||||
|
|
||||||
|
**Правило:** нов runtime / stdlib / CI effort отива към Linux + cloud + embedded. Windows-only work не влиза в следващи сесии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 75 (Linux / cloud / embedded foundation)
|
||||||
|
|
||||||
|
1. **`rt/runtime_minimal.c`** — thin runtime (no pthread / ucontext / sockets / OpenSSL);
|
||||||
|
same feature surface as historical `runtime_win.c`, documented for Linux static/embed.
|
||||||
|
2. **Bootstrap CLI** (`bootstrap/cli.nim`):
|
||||||
|
- `BUX_RUNTIME=full|minimal|thin|embed|win`
|
||||||
|
- `--static` / `BUX_STATIC=1` → fully-static link; defaults to minimal runtime
|
||||||
|
- `--target <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 75–81 platform stack
|
||||||
|
|
||||||
|
**Verified:** local smokes previously green; CI wiring ready for GHA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. Windows: more examples (strings/ownership) on MinGW; optional Win OpenSSL
|
### 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 75–76
|
||||||
|
7. ~~**Multi-arch Linux smoke**~~ ✅ session 75
|
||||||
|
8. ~~**Nexus production polish**~~ ✅ session 77
|
||||||
|
9. ~~**Nexus TLS**~~ ✅ session 78
|
||||||
|
10. ~~**Container story**~~ ✅ session 78
|
||||||
|
11. ~~**Registry + deploy**~~ ✅ session 79 (HTTPS + lock checksum + `--locked`)
|
||||||
|
12. ~~**musl path**~~ ✅ session 79 (smoke + docs; SKIP without toolchain)
|
||||||
|
13. ~~**mTLS / client certs**~~ ✅ session 80 (`NEXUS_TLS_CLIENT_CA`)
|
||||||
|
14. ~~**Selfhost install --locked**~~ ✅ session 80
|
||||||
|
15. ~~**Selfhost full registry**~~ ✅ session 81 (search / add / HTTP / path-dep build)
|
||||||
|
16. **Language P0 leftovers** — raw macro `tt` delimiter balancing (optional)
|
||||||
|
|
||||||
|
### P2 — Embedded / cross
|
||||||
|
|
||||||
|
12. ~~**Cross / thin / CTFE**~~ ✅ session 75
|
||||||
|
13. **riscv64 cross smoke** (when toolchain available)
|
||||||
|
14. **no-libc / bare-metal research** (spike only) — Cortex-M / qemu-system; not v1.0 blocker
|
||||||
|
|
||||||
|
### Изрично **не** правим
|
||||||
|
|
||||||
|
- Повече Windows examples / Win OpenSSL / Win sockets
|
||||||
|
- Windows като required CI gate за product features (остава optional historical smoke ако CI вече го има)
|
||||||
|
- Desktop GUI / Win32 APIs
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
* ============================================================================ */
|
* ============================================================================ */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
// registry.bux — package registry index for selfhost (session 81)
|
||||||
|
// Parity with bootstrap registry.nim: local file + HTTP(S) cache.
|
||||||
|
module Registry {
|
||||||
|
|
||||||
|
extern func Print(s: String);
|
||||||
|
extern func PrintLine(s: String);
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_read_file(path: String) -> String;
|
||||||
|
extern func bux_write_file(path: String, content: String) -> bool;
|
||||||
|
extern func bux_file_exists(path: String) -> int;
|
||||||
|
extern func bux_dir_exists(path: String) -> int;
|
||||||
|
extern func bux_mkdir_if_needed(path: String) -> int;
|
||||||
|
extern func bux_path_join(a: String, b: String) -> String;
|
||||||
|
extern func bux_path_parent(path: String) -> String;
|
||||||
|
extern func bux_getenv(name: String) -> String;
|
||||||
|
extern func bux_getcwd() -> String;
|
||||||
|
extern func bux_system(cmd: String) -> int;
|
||||||
|
extern func bux_process_output(cmd: String) -> String;
|
||||||
|
|
||||||
|
const REG_MAX: int = 64;
|
||||||
|
|
||||||
|
struct RegistryPackage {
|
||||||
|
name: String;
|
||||||
|
version: String;
|
||||||
|
source: String;
|
||||||
|
description: String;
|
||||||
|
resolvedPath: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Registry {
|
||||||
|
path: String; // local index path (or cache path)
|
||||||
|
sourceUrl: String; // non-empty if from HTTP(S)
|
||||||
|
count: int;
|
||||||
|
// fixed slots (no dynamic arrays in selfhost compiler easily)
|
||||||
|
p0: RegistryPackage;
|
||||||
|
p1: RegistryPackage;
|
||||||
|
p2: RegistryPackage;
|
||||||
|
p3: RegistryPackage;
|
||||||
|
p4: RegistryPackage;
|
||||||
|
p5: RegistryPackage;
|
||||||
|
p6: RegistryPackage;
|
||||||
|
p7: RegistryPackage;
|
||||||
|
p8: RegistryPackage;
|
||||||
|
p9: RegistryPackage;
|
||||||
|
p10: RegistryPackage;
|
||||||
|
p11: RegistryPackage;
|
||||||
|
p12: RegistryPackage;
|
||||||
|
p13: RegistryPackage;
|
||||||
|
p14: RegistryPackage;
|
||||||
|
p15: RegistryPackage;
|
||||||
|
// up to 16 packages is enough for smoke/demo; expand if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_FileExists(path: String) -> bool {
|
||||||
|
return bux_file_exists(path) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_DirExists(path: String) -> bool {
|
||||||
|
return bux_dir_exists(path) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_IsHttpUrl(s: String) -> bool {
|
||||||
|
if String_StartsWith(s, "http://") { return true; }
|
||||||
|
if String_StartsWith(s, "https://") { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_EnvTruthy(name: String) -> bool {
|
||||||
|
let v: String = bux_getenv(name);
|
||||||
|
if v == null as String { return false; }
|
||||||
|
if String_Eq(v, "") { return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_HomeCacheDir() -> String {
|
||||||
|
let home: String = bux_getenv("HOME");
|
||||||
|
if home == null as String || String_Eq(home, "") {
|
||||||
|
return ".bux/cache";
|
||||||
|
}
|
||||||
|
return bux_path_join(bux_path_join(home, ".bux"), "cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_FetchHttp(url: String) -> String {
|
||||||
|
// Download to ~/.bux/cache/registry_http.toml; return local path or "".
|
||||||
|
let cacheDir: String = Reg_HomeCacheDir();
|
||||||
|
discard bux_mkdir_if_needed(bux_path_join(bux_getenv("HOME"), ".bux"));
|
||||||
|
discard bux_mkdir_if_needed(cacheDir);
|
||||||
|
let cachePath: String = bux_path_join(cacheDir, "registry_http.toml");
|
||||||
|
let metaPath: String = bux_path_join(cacheDir, "registry_http.url");
|
||||||
|
let force: bool = Reg_EnvTruthy("BUX_REGISTRY_REFRESH");
|
||||||
|
if !force && Reg_FileExists(cachePath) && Reg_FileExists(metaPath) {
|
||||||
|
let cachedUrl: String = String_Trim(bux_read_file(metaPath));
|
||||||
|
if String_Eq(cachedUrl, url) {
|
||||||
|
return cachePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var kflag: String = "";
|
||||||
|
if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") {
|
||||||
|
kflag = " -k";
|
||||||
|
}
|
||||||
|
// prefer curl
|
||||||
|
let curlCmd: String = String_Concat(
|
||||||
|
"curl -fsSL",
|
||||||
|
String_Concat(kflag, String_Concat(" --max-time 30 -o \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\"")))))
|
||||||
|
);
|
||||||
|
var ok: bool = false;
|
||||||
|
if bux_system("command -v curl >/dev/null 2>&1") == 0 {
|
||||||
|
ok = bux_system(curlCmd) == 0 && Reg_FileExists(cachePath);
|
||||||
|
} else if bux_system("command -v wget >/dev/null 2>&1") == 0 {
|
||||||
|
var nflag: String = "";
|
||||||
|
if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") {
|
||||||
|
nflag = " --no-check-certificate";
|
||||||
|
}
|
||||||
|
let wgetCmd: String = String_Concat(
|
||||||
|
"wget -q",
|
||||||
|
String_Concat(nflag, String_Concat(" -T 30 -O \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\"")))))
|
||||||
|
);
|
||||||
|
ok = bux_system(wgetCmd) == 0 && Reg_FileExists(cachePath);
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
discard bux_write_file(metaPath, String_Concat(url, "\n"));
|
||||||
|
return cachePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_StripQuotes(val: String) -> String {
|
||||||
|
var v: String = String_Trim(val);
|
||||||
|
if String_StartsWith(v, "\"") && String_EndsWith(v, "\"") {
|
||||||
|
let n: uint = bux_strlen(v);
|
||||||
|
if n >= 2 {
|
||||||
|
return String_Slice(v, 1, n - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_ResolveSource(src: String, indexDir: String) -> String {
|
||||||
|
// file: or path: → absolute path; else ""
|
||||||
|
var p: String = src;
|
||||||
|
if String_StartsWith(src, "file:") {
|
||||||
|
p = String_Slice(src, 5, bux_strlen(src) - 5);
|
||||||
|
if String_StartsWith(p, "//") {
|
||||||
|
p = String_Slice(p, 2, bux_strlen(p) - 2);
|
||||||
|
}
|
||||||
|
} else if String_StartsWith(src, "path:") {
|
||||||
|
p = String_Slice(src, 5, bux_strlen(src) - 5);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if String_StartsWith(p, "/") {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return bux_path_join(indexDir, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_SetPkg(reg: *Registry, idx: int, pkg: RegistryPackage) {
|
||||||
|
if idx == 0 { reg.p0 = pkg; }
|
||||||
|
else if idx == 1 { reg.p1 = pkg; }
|
||||||
|
else if idx == 2 { reg.p2 = pkg; }
|
||||||
|
else if idx == 3 { reg.p3 = pkg; }
|
||||||
|
else if idx == 4 { reg.p4 = pkg; }
|
||||||
|
else if idx == 5 { reg.p5 = pkg; }
|
||||||
|
else if idx == 6 { reg.p6 = pkg; }
|
||||||
|
else if idx == 7 { reg.p7 = pkg; }
|
||||||
|
else if idx == 8 { reg.p8 = pkg; }
|
||||||
|
else if idx == 9 { reg.p9 = pkg; }
|
||||||
|
else if idx == 10 { reg.p10 = pkg; }
|
||||||
|
else if idx == 11 { reg.p11 = pkg; }
|
||||||
|
else if idx == 12 { reg.p12 = pkg; }
|
||||||
|
else if idx == 13 { reg.p13 = pkg; }
|
||||||
|
else if idx == 14 { reg.p14 = pkg; }
|
||||||
|
else if idx == 15 { reg.p15 = pkg; }
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_GetPkg(reg: Registry, idx: int) -> RegistryPackage {
|
||||||
|
if idx == 0 { return reg.p0; }
|
||||||
|
if idx == 1 { return reg.p1; }
|
||||||
|
if idx == 2 { return reg.p2; }
|
||||||
|
if idx == 3 { return reg.p3; }
|
||||||
|
if idx == 4 { return reg.p4; }
|
||||||
|
if idx == 5 { return reg.p5; }
|
||||||
|
if idx == 6 { return reg.p6; }
|
||||||
|
if idx == 7 { return reg.p7; }
|
||||||
|
if idx == 8 { return reg.p8; }
|
||||||
|
if idx == 9 { return reg.p9; }
|
||||||
|
if idx == 10 { return reg.p10; }
|
||||||
|
if idx == 11 { return reg.p11; }
|
||||||
|
if idx == 12 { return reg.p12; }
|
||||||
|
if idx == 13 { return reg.p13; }
|
||||||
|
if idx == 14 { return reg.p14; }
|
||||||
|
return reg.p15;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_ParseContent(content: String, indexPath: String) -> Registry {
|
||||||
|
var reg: Registry;
|
||||||
|
reg.path = indexPath;
|
||||||
|
reg.sourceUrl = "";
|
||||||
|
reg.count = 0;
|
||||||
|
let indexDir: String = bux_path_parent(indexPath);
|
||||||
|
var cur: RegistryPackage;
|
||||||
|
var inPkg: bool = false;
|
||||||
|
let nlines: uint = String_SplitCount(content, "\n");
|
||||||
|
var i: uint = 0;
|
||||||
|
while i <= nlines {
|
||||||
|
var line: String = "";
|
||||||
|
if i < nlines {
|
||||||
|
line = String_Trim(String_SplitPart(content, "\n", i));
|
||||||
|
}
|
||||||
|
let flush: bool = (i == nlines) || String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]");
|
||||||
|
if flush && inPkg && !String_Eq(cur.name, "") {
|
||||||
|
cur.resolvedPath = Reg_ResolveSource(cur.source, indexDir);
|
||||||
|
if reg.count < 16 {
|
||||||
|
Reg_SetPkg(®, reg.count, cur);
|
||||||
|
reg.count = reg.count + 1;
|
||||||
|
}
|
||||||
|
cur.name = "";
|
||||||
|
cur.version = "";
|
||||||
|
cur.source = "";
|
||||||
|
cur.description = "";
|
||||||
|
cur.resolvedPath = "";
|
||||||
|
}
|
||||||
|
if i == nlines { break; }
|
||||||
|
if String_Eq(line, "") || String_StartsWith(line, "#") {
|
||||||
|
i = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]") {
|
||||||
|
inPkg = true;
|
||||||
|
i = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !inPkg {
|
||||||
|
i = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let eqc: uint = String_SplitCount(line, "=");
|
||||||
|
if eqc >= 2 {
|
||||||
|
let key: String = String_Trim(String_SplitPart(line, "=", 0));
|
||||||
|
let val: String = Reg_StripQuotes(String_SplitPart(line, "=", 1));
|
||||||
|
// lowercase-ish compare for common keys
|
||||||
|
if String_Eq(key, "name") || String_Eq(key, "Name") {
|
||||||
|
cur.name = val;
|
||||||
|
} else if String_Eq(key, "version") || String_Eq(key, "Version") {
|
||||||
|
cur.version = val;
|
||||||
|
} else if String_Eq(key, "source") || String_Eq(key, "Source") {
|
||||||
|
cur.source = val;
|
||||||
|
} else if String_Eq(key, "description") || String_Eq(key, "Description") {
|
||||||
|
cur.description = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_FindIndex() -> Registry {
|
||||||
|
var reg: Registry;
|
||||||
|
reg.path = "";
|
||||||
|
reg.sourceUrl = "";
|
||||||
|
reg.count = 0;
|
||||||
|
let env: String = bux_getenv("BUX_REGISTRY");
|
||||||
|
if env != null as String && !String_Eq(env, "") {
|
||||||
|
if Reg_IsHttpUrl(env) {
|
||||||
|
let local: String = Reg_FetchHttp(env);
|
||||||
|
if String_Eq(local, "") {
|
||||||
|
reg.sourceUrl = env;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
let content: String = bux_read_file(local);
|
||||||
|
reg = Reg_ParseContent(content, local);
|
||||||
|
reg.sourceUrl = env;
|
||||||
|
reg.path = local;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
if Reg_FileExists(env) {
|
||||||
|
let content: String = bux_read_file(env);
|
||||||
|
reg = Reg_ParseContent(content, env);
|
||||||
|
reg.path = env;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let home: String = bux_getenv("HOME");
|
||||||
|
if home != null as String && !String_Eq(home, "") {
|
||||||
|
let homeIdx: String = bux_path_join(bux_path_join(home, ".bux"), "registry.toml");
|
||||||
|
if Reg_FileExists(homeIdx) {
|
||||||
|
let content: String = bux_read_file(homeIdx);
|
||||||
|
reg = Reg_ParseContent(content, homeIdx);
|
||||||
|
reg.path = homeIdx;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cwd-relative candidates
|
||||||
|
let cwd: String = bux_getcwd();
|
||||||
|
var c0: String = bux_path_join(cwd, "config/registry.toml");
|
||||||
|
if Reg_FileExists(c0) {
|
||||||
|
let content: String = bux_read_file(c0);
|
||||||
|
reg = Reg_ParseContent(content, c0);
|
||||||
|
reg.path = c0;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
c0 = bux_path_join(cwd, "../config/registry.toml");
|
||||||
|
if Reg_FileExists(c0) {
|
||||||
|
let content: String = bux_read_file(c0);
|
||||||
|
reg = Reg_ParseContent(content, c0);
|
||||||
|
reg.path = c0;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
c0 = bux_path_join(cwd, "../../config/registry.toml");
|
||||||
|
if Reg_FileExists(c0) {
|
||||||
|
let content: String = bux_read_file(c0);
|
||||||
|
reg = Reg_ParseContent(content, c0);
|
||||||
|
reg.path = c0;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_VersionOk(have: String, req: String) -> bool {
|
||||||
|
if String_Eq(req, "") || String_Eq(req, "*") { return true; }
|
||||||
|
return String_Eq(have, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup by name; versionReq "*" = last matching entry (highest listed last).
|
||||||
|
func Reg_Lookup(reg: Registry, name: String, versionReq: String) -> RegistryPackage {
|
||||||
|
var found: RegistryPackage;
|
||||||
|
found.name = "";
|
||||||
|
var i: int = 0;
|
||||||
|
while i < reg.count {
|
||||||
|
let p: RegistryPackage = Reg_GetPkg(reg, i);
|
||||||
|
if String_Eq(p.name, name) && Reg_VersionOk(p.version, versionReq) {
|
||||||
|
found = p;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reg_Search(reg: Registry, query: String) -> int {
|
||||||
|
// print hits; return count
|
||||||
|
var hits: int = 0;
|
||||||
|
// Dedupe by name: keep last version
|
||||||
|
// Simple O(n^2): for each pkg, if last occurrence of name, print
|
||||||
|
var i: int = 0;
|
||||||
|
while i < reg.count {
|
||||||
|
let p: RegistryPackage = Reg_GetPkg(reg, i);
|
||||||
|
var isLast: bool = true;
|
||||||
|
var j: int = i + 1;
|
||||||
|
while j < reg.count {
|
||||||
|
let q: RegistryPackage = Reg_GetPkg(reg, j);
|
||||||
|
if String_Eq(q.name, p.name) {
|
||||||
|
isLast = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
if isLast {
|
||||||
|
var ok: bool = true;
|
||||||
|
if !String_Eq(query, "") {
|
||||||
|
ok = String_Contains(p.name, query) || String_Contains(p.description, query);
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
Print(" ");
|
||||||
|
Print(p.name);
|
||||||
|
Print(" ");
|
||||||
|
Print(p.version);
|
||||||
|
Print(" — ");
|
||||||
|
if !String_Eq(p.description, "") {
|
||||||
|
PrintLine(p.description);
|
||||||
|
} else {
|
||||||
|
PrintLine(p.source);
|
||||||
|
}
|
||||||
|
hits = hits + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+32
@@ -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"
|
||||||
Executable
+32
@@ -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"
|
||||||
@@ -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)"
|
||||||
|
|||||||
Executable
+117
@@ -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"
|
||||||
Executable
+86
@@ -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"
|
||||||
Executable
+76
@@ -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"
|
||||||
Executable
+58
@@ -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
@@ -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)"
|
||||||
|
|||||||
Executable
+93
@@ -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"
|
||||||
Executable
+84
@@ -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"
|
||||||
Reference in New Issue
Block a user