From d46c25d2a82ae7c9f35001c5c7e4c5d10b03bd8a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 21:09:40 +0000 Subject: [PATCH 1/2] Auto-install pinned Zig toolchain in setup + devcontainer The Zig FFI bridge is half of the ABI-FFI standard, but nothing installed Zig: .tool-versions only lists it (commented), setup.sh stops at `just`, and the devcontainer's `postCreateCommand: just deps` referenced a `deps` recipe that did not exist. Unlike the other toolchain pieces, Zig is not distributed via GitHub releases, so it must come from ziglang.org. Add scripts/install-zig.sh: an idempotent, fail-soft installer for the pinned Zig 0.14.0 (arch/OS-aware, uses the system CA store the agent proxy populates, never --insecure). If ziglang.org is not on the session's egress allowlist the download 403s and the script exits 0 with an actionable message, so it never blocks setup or a session. Wire it in via the two paths the project already uses: a "Step 1b" in setup.sh (where the template exposes that step), and a new `deps` Justfile recipe backing the devcontainer postCreateCommand. Once ziglang.org is allowlisted, future setups and dev containers install Zig automatically. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH --- Justfile | 6 +++++ scripts/install-zig.sh | 59 ++++++++++++++++++++++++++++++++++++++++++ setup.sh | 9 +++++++ 3 files changed, 74 insertions(+) create mode 100755 scripts/install-zig.sh diff --git a/Justfile b/Justfile index 055c13d..22f0484 100644 --- a/Justfile +++ b/Justfile @@ -116,3 +116,9 @@ crg-badge: D) color="orange" ;; E) color="red" ;; F) color="critical" ;; \ *) color="lightgrey" ;; esac; \ echo "[![CRG $$grade](https://img.shields.io/badge/CRG-$$grade-$$color?style=flat-square)](https://github.com/hyperpolymath/standards/tree/main/component-readiness-grades)" + +# Install dev dependencies (invoked by the devcontainer postCreateCommand). +# Installs the pinned Zig FFI toolchain, then warms the Cargo cache. +deps: + ./scripts/install-zig.sh + cargo fetch diff --git a/scripts/install-zig.sh b/scripts/install-zig.sh new file mode 100755 index 0000000..63edd64 --- /dev/null +++ b/scripts/install-zig.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# install-zig.sh — install the pinned Zig toolchain (the Zig FFI bridge half of +# the ABI-FFI standard). Idempotent and fail-soft: it never aborts the caller. +# +# Egress note: Zig is NOT distributed via GitHub releases, so it is fetched from +# ziglang.org. Inside a Claude Code session, outbound HTTPS goes through the +# policy-enforcing agent proxy; github.com is allowlisted by default but +# ziglang.org must be added explicitly, or this download returns 403. We use the +# system CA store the proxy already populated — never pass --insecure. +set -eu + +ZIG_VERSION="${ZIG_VERSION:-0.14.0}" +PREFIX="${ZIG_PREFIX:-/usr/local}" + +# Already at the pinned version? Done. +if command -v zig >/dev/null 2>&1 && [ "$(zig version 2>/dev/null)" = "$ZIG_VERSION" ]; then + echo "install-zig: zig $ZIG_VERSION already installed" + exit 0 +fi + +# Map host arch/OS to Zig's release naming. +case "$(uname -m)" in + x86_64|amd64) zarch="x86_64" ;; + aarch64|arm64) zarch="aarch64" ;; + *) echo "install-zig: unsupported arch $(uname -m); install Zig $ZIG_VERSION manually" >&2; exit 0 ;; +esac +case "$(uname -s)" in + Linux) zos="linux" ;; + Darwin) zos="macos" ;; + *) echo "install-zig: unsupported OS $(uname -s); install Zig $ZIG_VERSION manually" >&2; exit 0 ;; +esac + +tarball="zig-${zos}-${zarch}-${ZIG_VERSION}.tar.xz" +url="https://ziglang.org/download/${ZIG_VERSION}/${tarball}" +dest="${PREFIX}/lib/zig-${ZIG_VERSION}" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +echo "install-zig: fetching $url" +if ! curl -fsSL --retry 2 -o "$tmp/$tarball" "$url"; then + echo "install-zig: download failed (HTTP error or blocked host)." >&2 + echo "install-zig: if this is a Claude Code session, add 'ziglang.org' to the" >&2 + echo " egress allowlist — github.com is allowed but ziglang.org is not." >&2 + exit 0 # fail-soft: a missing Zig must not block setup or session start +fi + +mkdir -p "$dest" "${PREFIX}/bin" +tar -xJf "$tmp/$tarball" -C "$dest" --strip-components=1 +ln -sf "$dest/zig" "${PREFIX}/bin/zig" + +if command -v zig >/dev/null 2>&1 && [ "$(zig version 2>/dev/null)" = "$ZIG_VERSION" ]; then + echo "install-zig: installed zig $(zig version)" +else + echo "install-zig: installed to ${PREFIX}/bin/zig — ensure ${PREFIX}/bin is on PATH" >&2 +fi diff --git a/setup.sh b/setup.sh index 6382977..5707414 100755 --- a/setup.sh +++ b/setup.sh @@ -194,6 +194,15 @@ main() { printf "%sStep 1: Install task runner%s\n" "$BOLD" "$RESET" install_just || { fail "Cannot proceed without just"; exit 1; } printf "\n" + # Step 1b: Install the pinned Zig toolchain (the Zig FFI bridge half of the + # ABI-FFI standard). Best-effort — see scripts/install-zig.sh. + printf "%sStep 1b: Install Zig toolchain%s\n" "$BOLD" "$RESET" + if [ -x ./scripts/install-zig.sh ]; then + ./scripts/install-zig.sh || warn "Zig install skipped (see scripts/install-zig.sh)" + else + warn "scripts/install-zig.sh not found — skipping Zig install" + fi + printf "\n" # Step 2: Check if we're in the repo directory if [ ! -f "Justfile" ] && [ ! -f "justfile" ]; then From 467f2c86b29b4e7fdfa02ac26c7205e437670a8e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 22:04:26 +0000 Subject: [PATCH 2/2] P1: fix Zig FFI to build and match the Idris2 ABI `zig test src/main.zig -lc` crashed with "free(): double free detected" on the second test (create supervisor and worker). Root cause: dual ownership of tree nodes. The handle's flat `nodes` ArrayList owns every node created by otpiser_create_supervisor / otpiser_create_worker, while otpiser_add_child also links a child into its parent supervisor's `children` list. TreeNode.deinit then recursively freed each child (child.deinit() + allocator.destroy(child)), so any node that was added as a child was freed twice on otpiser_free: once via the parent's recursive deinit and once via the handle's flat `nodes` sweep. Fix: make `children` a non-owning reference list. TreeNode.deinit now releases only its own `children` container and no longer frees the child nodes. The handle's `nodes` list remains the single owner and frees every node exactly once. This matches the alloyiser reference idiom (single, flat ownership; handles are plain structs behind ?*T). No exported C symbol names or Result-enum integer values were changed. All 17 C:otpiser_* symbols in Foreign.idr keep their matching export fn, and the Result enum still mirrors resultToInt (Ok=0 .. MalformedTree=6). Verification: - src/interface/ffi: `zig test src/main.zig -lc` -> all 7 tests pass, 0 errors/warnings - src/interface/abi: `idris2 --build otpiser-abi.ipkg` -> exit 0 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH --- src/interface/ffi/src/main.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index 0eda31b..b9d5f6a 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -73,15 +73,16 @@ const TreeNode = struct { module_name: []const u8, restart_type: ChildRestartType, shutdown_ms: u32, - // Tree structure + // Tree structure. + // `children` holds non-owning references to other nodes; every node is + // owned exactly once by the handle's flat `nodes` list (see OtpiserHandle). children: std.ArrayList(*TreeNode), allocator: std.mem.Allocator, + /// Release only this node's own resources. Children are NOT freed here: + /// they are owned by the handle's `nodes` list and freed there exactly + /// once, avoiding the double-free that arises from dual ownership. fn deinit(self: *TreeNode) void { - for (self.children.items) |child| { - child.deinit(); - self.allocator.destroy(child); - } self.children.deinit(); } };