diff --git a/.envrc b/.envrc index ef739ef..2702355 100644 --- a/.envrc +++ b/.envrc @@ -12,15 +12,10 @@ if has guix && [ -f guix.scm ]; then use guix fi -# Load Nix flake if flake.nix exists -if has nix && [ -f flake.nix ]; then -fi - # Project environment variables -export PROJECT_NAME="{{PROJECT_NAME}}" +export PROJECT_NAME="halideiser" export RSR_TIER="infrastructure" -# export DATABASE_URL="..." -# export API_KEY="..." +# Set service credentials in the gitignored .env file when needed. # Source .env if it exists (gitignored) dotenv_if_exists diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index 44b79e8..dc44042 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -44,7 +44,7 @@ release schedules, contributor access, and community standards. - ADR statuses: `proposed`, `accepted`, `deprecated`, `superseded`, `rejected`. - ADRs provide a historical record of why decisions were made and what alternatives were considered. -- See `.machine_readable/META.a2ml` for the machine-readable ADR index. +- See `.machine_readable/descriptiles/META.a2ml` for the machine-readable ADR index. --- diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a8accd..ada1e9e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,9 +26,9 @@ ### As Applicable -- [ ] `.machine_readable/STATE.a2ml` updated (if project state changed) -- [ ] `.machine_readable/ECOSYSTEM.a2ml` updated (if integrations changed) -- [ ] `.machine_readable/META.a2ml` updated (if architectural decisions changed) +- [ ] `.machine_readable/descriptiles/STATE.a2ml` updated (if project state changed) +- [ ] `.machine_readable/descriptiles/ECOSYSTEM.a2ml` updated (if integrations changed) +- [ ] `.machine_readable/descriptiles/META.a2ml` updated (if architectural decisions changed) - [ ] Documentation updated for user-facing changes - [ ] `TOPOLOGY.md` updated (if architecture changed) - [ ] `CHANGELOG` or release notes updated diff --git a/.github/workflows/abi-ffi-gate.yml b/.github/workflows/abi-ffi-gate.yml index 6bd8ad0..e9bdea0 100644 --- a/.github/workflows/abi-ffi-gate.yml +++ b/.github/workflows/abi-ffi-gate.yml @@ -22,8 +22,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7.0.1 + - name: Install Julia 1.11.5 + run: | + curl --proto "=https" --proto-redir "=https" --tlsv1.2 -fsSL https://julialang-s3.julialang.org/bin/linux/x64/1.11/julia-1.11.5-linux-x86_64.tar.gz -o /tmp/julia.tar.gz + # Official julialang-s3.julialang.org/bin/checksums/julia-1.11.5.sha256 + echo '723e878c642220cc0251a0e13758c059a389cadc7f01376feaf1ea7388fe8f9c /tmp/julia.tar.gz' | sha256sum --check --strict + tar -xf /tmp/julia.tar.gz -C /tmp + echo "/tmp/julia-1.11.5/bin" >> "$GITHUB_PATH" - name: Run ABI-FFI gate - run: python3 scripts/abi-ffi-gate.py + run: | + julia --version # confirms the pinned 1.11.5 is on PATH, not the runner default + julia scripts/abi-ffi-gate.jl + bash tests/abi-gate.sh zig-build: name: Zig FFI builds + tests (Zig 0.14.0) @@ -32,7 +42,9 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Install Zig 0.14.0 run: | - curl -fsSL https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz -o /tmp/zig.tar.xz + curl --proto "=https" --proto-redir "=https" --tlsv1.2 -fsSL https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz -o /tmp/zig.tar.xz + # Official ziglang.org/download/index.json: 0.14.0 / x86_64-linux + echo '473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 /tmp/zig.tar.xz' | sha256sum --check --strict tar -xf /tmp/zig.tar.xz -C /tmp echo "/tmp/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" - name: zig test FFI diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 62bb9f7..9b0678f 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -11,12 +11,12 @@ workflows: - 'actions/cache@v6.1.0' - 'actions/checkout@v7.0.1' - 'actions/configure-pages@v6.0.0' - - 'actions/deploy-pages@v5.0.0' + - 'actions/deploy-pages@v5.0.1' - 'actions/upload-pages-artifact@v5.0.0' - 'haskell-actions/setup@v2.12.0' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - - 'github/codeql-action@v4.37.7' + - 'github/codeql-action@v4.37.9' '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': [] @@ -31,7 +31,7 @@ workflows: '.github/workflows/release.yml': - 'actions/checkout@v7.0.1' - 'actions/upload-artifact@v7.0.1' - - 'softprops/action-gh-release@v3.0.2' + - 'softprops/action-gh-release@v3.0.3' '.github/workflows/rhodibot.yml': - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': [] @@ -58,9 +58,9 @@ dependencies: commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' owner_id: 44036562 repo_id: 513659658 - 'actions/deploy-pages@v5.0.0': - ref: 'v5.0.0' - commit: 'sha1-cd2ce8fcbc39b97be8ca5fce6e763baed58fa128' + 'actions/deploy-pages@v5.0.1': + ref: 'v5.0.1' + commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 'actions/download-artifact@v8.0.1': @@ -90,9 +90,9 @@ dependencies: commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@v4.37.7': - ref: 'v4.37.7' - commit: 'sha1-ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd' + 'github/codeql-action@v4.37.9': + ref: 'v4.37.9' + commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' owner_id: 9919 repo_id: 259445878 'haskell-actions/setup@v2.12.0': @@ -110,8 +110,8 @@ dependencies: commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' owner_id: 18365890 repo_id: 220359305 - 'softprops/action-gh-release@v3.0.2': - ref: 'v3.0.2' - commit: 'sha1-3d0d9888cb7fd7b750713d6e236d1fcb99157228' + 'softprops/action-gh-release@v3.0.3': + ref: 'v3.0.3' + commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' owner_id: 2242 repo_id: 204253808 diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index be5d5f2..cfd459c 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# This workflow is managed by gh actions-lock. name: Governance on: @@ -16,4 +15,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@469605210e767ee94d1c7a9c13cb6a1d0a78cad1 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 23785f3..c139558 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# This workflow is managed by gh actions-lock. name: Hypatia Security Scan on: @@ -19,4 +18,6 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@469605210e767ee94d1c7a9c13cb6a1d0a78cad1 + with: + block-on-high: true diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index 907170c..0d7a543 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# This workflow is managed by gh actions-lock. # Instant Forge Sync - Triggers propagation to all forges on push/release name: Instant Sync @@ -16,8 +15,13 @@ permissions: jobs: dispatch: runs-on: ubuntu-latest + timeout-minutes: 5 + env: + FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }} steps: - name: Trigger Propagation + id: propagate + if: env.FARM_DISPATCH_TOKEN != '' uses: peter-evans/repository-dispatch@v4.0.1 with: token: ${{ secrets.FARM_DISPATCH_TOKEN }} @@ -32,4 +36,5 @@ jobs: } - name: Confirm + if: steps.propagate.outcome == 'success' run: echo "::notice::Propagation triggered for ${{ github.event.repository.name }}" diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 3c5f3f3..ea52151 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -18,3 +18,5 @@ permissions: jobs: rust-ci: uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + with: + enable_audit: true diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 25600df..0aab19c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# This workflow is managed by gh actions-lock. name: OSSF Scorecard on: @@ -14,4 +13,9 @@ permissions: jobs: scorecard: + permissions: + actions: read + contents: read + security-events: write + id-token: write uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a diff --git a/.machine_readable/ai/AI.a2ml b/.machine_readable/ai/AI.a2ml index ba07cd4..bb5fdb2 100644 --- a/.machine_readable/ai/AI.a2ml +++ b/.machine_readable/ai/AI.a2ml @@ -24,8 +24,8 @@ Key domain concepts: ## Workflow -1. Inspect `.machine_readable/6a2/STATE.a2ml` for blockers and next actions. -2. Respect constraints in `.machine_readable/6a2/AGENTIC.a2ml`. +1. Inspect `.machine_readable/descriptiles/STATE.a2ml` for blockers and next actions. +2. Respect constraints in `.machine_readable/descriptiles/AGENTIC.a2ml`. 3. After finishing edits, update STATE.a2ml with outcomes and commit. ## Key Rules diff --git a/.machine_readable/ai/README.adoc b/.machine_readable/ai/README.adoc index 121bbc8..4e4f653 100644 --- a/.machine_readable/ai/README.adoc +++ b/.machine_readable/ai/README.adoc @@ -18,5 +18,5 @@ Recommended machine read order: * `.machine_readable/policies/MAINTENANCE-AXES.a2ml` * `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` * `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` -* `.machine_readable/STATE.a2ml` -* `.machine_readable/META.a2ml` +* `.machine_readable/descriptiles/STATE.a2ml` +* `.machine_readable/descriptiles/META.a2ml` diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml similarity index 100% rename from .machine_readable/6a2/AGENTIC.a2ml rename to .machine_readable/descriptiles/AGENTIC.a2ml diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/descriptiles/ECOSYSTEM.a2ml similarity index 100% rename from .machine_readable/6a2/ECOSYSTEM.a2ml rename to .machine_readable/descriptiles/ECOSYSTEM.a2ml diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/descriptiles/META.a2ml similarity index 100% rename from .machine_readable/6a2/META.a2ml rename to .machine_readable/descriptiles/META.a2ml diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/descriptiles/NEUROSYM.a2ml similarity index 100% rename from .machine_readable/6a2/NEUROSYM.a2ml rename to .machine_readable/descriptiles/NEUROSYM.a2ml diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml similarity index 93% rename from .machine_readable/6a2/PLAYBOOK.a2ml rename to .machine_readable/descriptiles/PLAYBOOK.a2ml index 261b166..5c68390 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -13,7 +13,7 @@ last-updated = "2026-03-21" # target = "container" # container | binary | library | wasm [incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status +# 1. Check .machine_readable/descriptiles/STATE.a2ml for current status # 2. Review recent commits and CI results # 3. Run `just validate` to check compliance # 4. Run `just security` to audit for vulnerabilities diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml similarity index 80% rename from .machine_readable/6a2/STATE.a2ml rename to .machine_readable/descriptiles/STATE.a2ml index c75386c..ea972ba 100644 --- a/.machine_readable/6a2/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -38,3 +38,8 @@ actions = [ [maintenance-status] last-run-utc = "2026-03-21T00:00:00Z" last-result = "unknown" # unknown | pass | warn | fail + +[language-portfolio-audit-20260907] +scope = "Evidence audit and scoped repairs; no blanket readiness upgrade" +report = "https://github.com/hyperpolymath/nextgen-languages/blob/main/docs/audits/2026-09-07-language-portfolio.md" +metadata-path = ".machine_readable/descriptiles/" diff --git a/.machine_readable/policies/MAINTENANCE-AXES.a2ml b/.machine_readable/policies/MAINTENANCE-AXES.a2ml index 8cc906f..0b35f74 100644 --- a/.machine_readable/policies/MAINTENANCE-AXES.a2ml +++ b/.machine_readable/policies/MAINTENANCE-AXES.a2ml @@ -18,7 +18,7 @@ machine-entrypoints = [ ".machine_readable/policies/MAINTENANCE-AXES.a2ml", ".machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml", ".machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml", - ".machine_readable/META.a2ml", + ".machine_readable/descriptiles/META.a2ml", ".machine_readable/ai/README.adoc", ".machine_readable/bot_directives/README.scm", ] diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index 9075eb6..b4243dd 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -18,7 +18,7 @@ Halideiser makes this separation automatic. ### Machine-Readable Metadata: `.machine_readable/` ONLY -These 6 a2ml files MUST exist in `.machine_readable/6a2/` directory ONLY: +These 6 a2ml files MUST exist in `.machine_readable/descriptiles/` directory ONLY: 1. **STATE.a2ml** - Project state, progress, blockers 2. **META.a2ml** - Architecture decisions, governance 3. **ECOSYSTEM.a2ml** - Position in -iser ecosystem, relationships @@ -88,7 +88,7 @@ halideiser/ ├── container/ # Stapeln container ecosystem ├── verification/ # Formal verification artifacts └── .machine_readable/ # ALL machine-readable metadata - ├── 6a2/ # STATE, META, ECOSYSTEM, AGENTIC, NEUROSYM, PLAYBOOK + ├── descriptiles/ # STATE, META, ECOSYSTEM, AGENTIC, NEUROSYM, PLAYBOOK ├── anchors/ # ANCHOR.a2ml ├── policies/ # Maintenance policies ├── bot_directives/ # Bot instructions @@ -109,7 +109,7 @@ halideiser/ Read THIS file (0-AI-MANIFEST.a2ml) first. Understand canonical location: `.machine_readable/`. -Read `.machine_readable/6a2/STATE.a2ml` for current status and next actions. +Read `.machine_readable/descriptiles/STATE.a2ml` for current status and next actions. ## ATTESTATION PROOF diff --git a/README.adoc b/README.adoc index cdcaee8..99d6f62 100644 --- a/README.adoc +++ b/README.adoc @@ -151,7 +151,7 @@ halideiser generate halideiser build --release # Run the pipeline -halideiser run -- input.png output.png +halideiser run --release -- input.png output.png ---- == License diff --git a/container/0.1-AI-MANIFEST.a2ml b/container/0.1-AI-MANIFEST.a2ml index ccb5bc5..85f2261 100644 --- a/container/0.1-AI-MANIFEST.a2ml +++ b/container/0.1-AI-MANIFEST.a2ml @@ -30,7 +30,7 @@ canonical_locations: build_pipeline: "container/ct-build.sh" entrypoint: "container/entrypoint.sh" monitoring: "container/vordr.toml" - deployment: "container/deploy.k9.ncl" + deployment: "container/deploy.k9.ncl.in" example: "container/compose.example.toml" --- diff --git a/container/README.adoc b/container/README.adoc index 7a2ef6c..1c99c81 100644 --- a/container/README.adoc +++ b/container/README.adoc @@ -59,8 +59,8 @@ All files use `{{PLACEHOLDER}}` tokens that are replaced by `just container-init | **vordr** runtime monitoring configuration. Defines health endpoints, crash detection, resource thresholds, and log output. -| `deploy.k9.ncl` -| **k9-svc** deployment component at Hunt trust level. Full pedigree +| `deploy.k9.ncl.in` +| Uninstantiated **k9-svc** deployment template at Hunt trust level. Proposed pedigree (L1--L5), environment configs (dev/staging/prod), container configuration, and rolling deployment strategy. @@ -154,11 +154,10 @@ For k9-svc managed deployments: [source,bash] ---- -# Validate the deployment component -nickel typecheck container/deploy.k9.ncl - -# Deploy (requires Hunt-level authorisation) -k9-svc deploy container/deploy.k9.ncl --env production +# The .in file is a template, not a deployable K9 contract. +# Render all placeholders into deploy.k9.ncl, provide a valid K9 header and +# pedigree, and verify its signature before validation or Hunt authorisation. +k9-svc validate container/deploy.k9.ncl ---- == Base Images diff --git a/container/deploy.k9.ncl b/container/deploy.k9.ncl.in similarity index 97% rename from container/deploy.k9.ncl rename to container/deploy.k9.ncl.in index 0ad0d04..ac23436 100644 --- a/container/deploy.k9.ncl +++ b/container/deploy.k9.ncl.in @@ -7,7 +7,9 @@ # WARNING: This component can execute shell commands! # It requires explicit authorisation via the Leash system. # -# Usage: +# TEMPLATE ONLY: render placeholders and validate a signed K9 contract before use. +# This file is not deployable. +# Example commands for the rendered contract: # nickel typecheck container/deploy.k9.ncl # k9-svc validate container/deploy.k9.ncl # k9-svc deploy container/deploy.k9.ncl --env production diff --git a/docs/RSR_OUTLINE.adoc b/docs/RSR_OUTLINE.adoc index 8faf3bb..9817d6f 100644 --- a/docs/RSR_OUTLINE.adoc +++ b/docs/RSR_OUTLINE.adoc @@ -215,7 +215,7 @@ project/ * `Justfile` * `README.adoc` * `LICENSE` (MPL-2.0) -* `.machine_readable/STATE.a2ml` +* `.machine_readable/descriptiles/STATE.a2ml` * `.well-known/security.txt` * `.well-known/ai.txt` * `.well-known/humans.txt` diff --git a/docs/governance/MAINTENANCE-CHECKLIST.a2ml b/docs/governance/MAINTENANCE-CHECKLIST.a2ml index eaee720..e50f6c8 100644 --- a/docs/governance/MAINTENANCE-CHECKLIST.a2ml +++ b/docs/governance/MAINTENANCE-CHECKLIST.a2ml @@ -2,6 +2,7 @@ # Cross-repo maintenance baseline (machine-readable canonical) [metadata] +name = "Cross-repository maintenance checklist" version = "1.1.0" last-updated = "2026-02-24" scope = "cross-repo" diff --git a/docs/practice/AI-CONVENTIONS.adoc b/docs/practice/AI-CONVENTIONS.adoc index 58e132b..80a9e40 100644 --- a/docs/practice/AI-CONVENTIONS.adoc +++ b/docs/practice/AI-CONVENTIONS.adoc @@ -10,12 +10,12 @@ Per-tool config files (.cursorrules, .clinerules, etc.) reference this document. ## Session Startup 1. Read `0-AI-MANIFEST.a2ml` FIRST (mandatory gatekeeper). -2. Read `.machine_readable/STATE.a2ml` for current status and blockers. +2. Read `.machine_readable/descriptiles/STATE.a2ml` for current status and blockers. 3. Read `.machine_readable/anchors/ANCHOR.a2ml` for canonical authority boundaries. 4. Read `.machine_readable/policies/MAINTENANCE-AXES.a2ml` for maintenance/audit sequencing. 5. Read `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` for baseline controls. 6. Read `.machine_readable/policies/SOFTWARE-DEVELOPMENT-APPROACH.a2ml` for execution order. -7. Read `.machine_readable/AGENTIC.a2ml` for agent constraints. +7. Read `.machine_readable/descriptiles/AGENTIC.a2ml` for agent constraints. ## License @@ -77,8 +77,8 @@ Use `just` (justfile) for all build, test, lint, and format tasks. ## References - `0-AI-MANIFEST.a2ml` -- universal AI entry point -- `.machine_readable/AGENTIC.a2ml` -- agent permissions and constraints -- `.machine_readable/STATE.a2ml` -- current project state +- `.machine_readable/descriptiles/AGENTIC.a2ml` -- agent permissions and constraints +- `.machine_readable/descriptiles/STATE.a2ml` -- current project state - `.machine_readable/anchors/ANCHOR.a2ml` -- canonical authority and policy boundary - `.machine_readable/policies/MAINTENANCE-AXES.a2ml` -- canonical axis sequencing and audit requirements - `.machine_readable/policies/MAINTENANCE-CHECKLIST.a2ml` -- baseline maintenance checklist policy diff --git a/scripts/abi-ffi-gate.jl b/scripts/abi-ffi-gate.jl new file mode 100644 index 0000000..9c0eeb8 --- /dev/null +++ b/scripts/abi-ffi-gate.jl @@ -0,0 +1,118 @@ +#!/usr/bin/env julia +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# abi-ffi-gate.jl — fail (exit 1) if the Zig FFI does not conform to the Idris2 +# ABI. The Idris2 ABI is the source of truth. Checks, with no compile toolchain +# needed (pure base-Julia text analysis): +# +# 1. the Zig FFI carries no unrendered `{{...}}` template tokens; +# 2. every `%foreign "C:"` symbol declared anywhere in the ABI .idr +# sources is exported by the Zig FFI (`export fn `); +# 3. the Zig `Result = enum(c_int)` and the Idris `resultToInt` agree on BOTH +# names and integer values (the `Error`/`err` spelling is treated as one). +# +# Usage: julia scripts/abi-ffi-gate.jl [repo_root] (defaults to cwd) +# +# Julia port of the former scripts/abi-ffi-gate.py (Python is banned estate-wide, +# RSR-H4); required inputs now fail closed. + +"camelCase / PascalCase → snake_case (insert `_` before each non-initial capital)." +camel_to_snake(s) = lowercase(replace(s, r"(? "_")) + +"Canonical result-code key: lowercased, with `err`/`error` unified to `error`." +function canon_rc(name) + n = lowercase(name) + (n == "err" || n == "error") ? "error" : n +end + +"Return {variant => value} for the C-ABI `Result` enum (the `enum(c_int)` block whose `ok = 0`), or empty." +function find_result_enum(zig::AbstractString) + best = Dict{String,Int}() + for m in eachmatch(r"enum\s*\(\s*c_int\s*\)\s*\{(.*?)\}"s, zig) + body = m.captures[1] + variants = Dict{String,Int}() + for vm in eachmatch(r"@?\"?([A-Za-z_][A-Za-z0-9_]*)\"?\s*=\s*(\d+)", body) + variants[canon_rc(vm.captures[1])] = parse(Int, vm.captures[2]) + end + # The Result enum is the one starting at ok = 0. + if get(variants, "ok", nothing) == 0 && length(variants) > length(best) + best = variants + end + end + return best +end + +"Collect every `*.idr` under `abi_dir`, skipping any `build/` output directory." +function idr_sources(abi_dir::AbstractString) + files = String[] + isdir(abi_dir) || return files + for (root, _dirs, fs) in walkdir(abi_dir) + occursin("/build/", root * "/") && continue + for f in fs + endswith(f, ".idr") && push!(files, joinpath(root, f)) + end + end + return files +end + +function main(root::AbstractString)::Int + name = basename(rstrip(abspath(root), '/')) + abi_dir = joinpath(root, "src/interface/abi") + zig_path = joinpath(root, "src/interface/ffi/src/main.zig") + errs = String[] + + idr_files = idr_sources(abi_dir) + if isempty(idr_files) + println("ABI-FFI GATE: FAIL ($name) — no Idris2 ABI .idr files under $abi_dir") + return 1 + end + if !isfile(zig_path) + println("ABI-FFI GATE: FAIL ($name) — no Zig FFI at $zig_path") + return 1 + end + + idr = join((read(p, String) for p in idr_files), "\n") + zig = read(zig_path, String) + + # 1. unrendered template tokens + toks = sort(unique(String(m.match) for m in eachmatch(r"\{\{[A-Za-z0-9_]+\}\}", zig))) + isempty(toks) || push!(errs, "Zig FFI has unrendered template tokens: $(toks)") + + # 2. foreign C symbols must be exported + csyms = sort(unique(String(m.captures[1]) for m in eachmatch(r"C:([A-Za-z0-9_]+)", idr))) + exports = Set(String(m.captures[1]) for m in eachmatch(r"export fn ([A-Za-z0-9_]+)", zig)) + missing_syms = [s for s in csyms if !(s in exports)] + isempty(missing_syms) || + push!(errs, "$(length(missing_syms)) ABI function(s) not exported by the Zig FFI: $(missing_syms)") + + # 3. result-code map (names + values) must agree + idr_rc = Dict{String,Int}() + for m in eachmatch(r"resultToInt\s+([A-Za-z0-9]+)\s*=\s*(\d+)", idr) + idr_rc[canon_rc(camel_to_snake(m.captures[1]))] = parse(Int, m.captures[2]) + end + zig_rc = find_result_enum(zig) + if isempty(idr_rc) + push!(errs, "no Idris resultToInt mapping found to compare result codes") + elseif isempty(zig_rc) + push!(errs, "no Zig `enum(c_int)` Result block (with `ok = 0`) found to compare result codes") + elseif !isempty(idr_rc) && !isempty(zig_rc) && idr_rc != zig_rc + push!(errs, "Result-code map differs (name or value):\n" * + " Idris resultToInt: $(sort(collect(idr_rc)))\n" * + " Zig Result enum: $(sort(collect(zig_rc)))") + end + + if !isempty(errs) + println("ABI-FFI GATE: FAIL ($name)") + for e in errs + println(" - " * e) + end + return 1 + end + println("ABI-FFI GATE: OK ($name) — $(length(csyms)) ABI functions exported, " * + "$(length(idr_rc)) result codes match") + return 0 +end + +root = length(ARGS) >= 1 ? ARGS[1] : "." +exit(main(root)) diff --git a/scripts/abi-ffi-gate.py b/scripts/abi-ffi-gate.py deleted file mode 100755 index 9ee96db..0000000 --- a/scripts/abi-ffi-gate.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# abi-ffi-gate.py — fail (exit 1) if the Zig FFI does not conform to the Idris2 -# ABI. The Idris2 ABI is the source of truth. Checks, with no toolchain needed: -# -# 1. the Zig FFI carries no unrendered `{{...}}` template tokens; -# 2. every `%foreign "C:"` symbol declared anywhere in the ABI .idr -# sources is exported by the Zig FFI (`export fn `); -# 3. the Zig `Result = enum(c_int)` and the Idris `resultToInt` agree on BOTH -# names and integer values (the `Error`/`err` spelling is treated as one). -# -# Usage: python3 scripts/abi-ffi-gate.py [repo_root] (defaults to cwd) - -import os -import re -import sys -import glob - - -def camel_to_snake(s): - return re.sub(r"(? len(best): - best = variants - return best - - -def main(): - root = sys.argv[1] if len(sys.argv) > 1 else "." - name = os.path.basename(os.path.abspath(root)) - abi_dir = os.path.join(root, "src/interface/abi") - zig_path = os.path.join(root, "src/interface/ffi/src/main.zig") - errs = [] - - idr_files = [ - p for p in glob.glob(os.path.join(abi_dir, "**", "*.idr"), recursive=True) - if os.sep + "build" + os.sep not in p - ] - if not idr_files: - print(f"ABI-FFI GATE: SKIP ({name}) — no Idris2 ABI .idr files under {abi_dir}") - return 0 - if not os.path.exists(zig_path): - print(f"ABI-FFI GATE: FAIL ({name}) — no Zig FFI at {zig_path}") - return 1 - - idr = "\n".join(open(p, encoding="utf-8").read() for p in idr_files) - zig = open(zig_path, encoding="utf-8").read() - - # 1. unrendered template tokens - toks = sorted(set(re.findall(r"\{\{[A-Za-z0-9_]+\}\}", zig))) - if toks: - errs.append(f"Zig FFI has unrendered template tokens: {toks}") - - # 2. foreign C symbols must be exported - csyms = sorted(set(re.findall(r"C:([A-Za-z0-9_]+)", idr))) - exports = set(re.findall(r"export fn ([A-Za-z0-9_]+)", zig)) - missing = [s for s in csyms if s not in exports] - if missing: - errs.append(f"{len(missing)} ABI function(s) not exported by the Zig FFI: {missing}") - - # 3. result-code map (names + values) must agree - idr_rc = {} - for m in re.finditer(r"resultToInt\s+([A-Za-z0-9]+)\s*=\s*(\d+)", idr): - idr_rc[canon_rc(camel_to_snake(m.group(1)))] = int(m.group(2)) - zig_rc = find_result_enum(zig) - if idr_rc and not zig_rc: - errs.append("no Zig `enum(c_int)` Result block (with `ok = 0`) found to compare result codes") - elif idr_rc and zig_rc and idr_rc != zig_rc: - errs.append( - "Result-code map differs (name or value):\n" - f" Idris resultToInt: {dict(sorted(idr_rc.items()))}\n" - f" Zig Result enum: {dict(sorted(zig_rc.items()))}" - ) - - if errs: - print(f"ABI-FFI GATE: FAIL ({name})") - for e in errs: - print(" - " + e) - return 1 - print(f"ABI-FFI GATE: OK ({name}) — {len(csyms)} ABI functions exported, " - f"{len(idr_rc)} result codes match") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/setup.sh b/setup.sh old mode 100755 new mode 100644 index 9da0aa5..ba2abc5 --- a/setup.sh +++ b/setup.sh @@ -6,8 +6,7 @@ # Then hands off to `just setup` for project-specific configuration. # # Usage: -# curl -fsSL https://raw.githubusercontent.com/hyperpolymath/halideiser/main/setup.sh | sh -# # or after cloning: +# Review this script in a verified checkout before running: # ./setup.sh # # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) @@ -77,7 +76,7 @@ detect_shell() { printf "%s" "$CURRENT_SHELL" } -# ── Platform Detection ── +# detect_platform identifies the operating system, distribution, architecture, and available package manager, storing the results in global variables. detect_platform() { OS="unknown" DISTRO="unknown" @@ -128,7 +127,7 @@ detect_platform() { esac } -# ── Install just ── +# install_just installs the `just` task runner using the detected package manager and reports whether installation succeeded. install_just() { if command -v just >/dev/null 2>&1; then ok "just already installed: $(just --version 2>/dev/null | head -1)" @@ -139,10 +138,7 @@ install_just() { case "$PKG_MGR" in dnf) sudo dnf install -y just ;; - apt) sudo apt-get install -y just 2>/dev/null || { - # just not in older apt repos — use installer - curl -fsSL https://just.systems/install.sh | bash -s -- --to /usr/local/bin - } ;; + apt) sudo apt-get install -y just ;; pacman) sudo pacman -S --noconfirm just ;; apk) sudo apk add just ;; brew) brew install just ;; @@ -152,8 +148,8 @@ install_just() { guix) guix install just ;; nix) nix-env -iA nixpkgs.just ;; *) - info "Using just installer script..." - curl -fsSL https://just.systems/install.sh | bash -s -- --to /usr/local/bin + fail "No supported package manager; install a verified just package before continuing." + return 1 ;; esac diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 0e3cba1..42b55eb 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -18,13 +18,28 @@ use std::path::Path; use crate::manifest::Manifest; -/// Generate all artifacts: Halide C++ source, schedule, and CMakeLists.txt. +/// Generates the Halide generator source, runner source, and CMake configuration files. /// -/// Output directory structure: -/// / -/// _generator.cpp — Halide generator (algorithm + schedule) -/// _runner.cpp — Runner that loads input, runs pipeline, writes output -/// CMakeLists.txt — Build configuration for Halide +/// The files are written to `output_dir` using the project name from the manifest: +/// `_generator.cpp`, `_runner.cpp`, and `CMakeLists.txt`. +/// +/// # Arguments +/// +/// * `manifest` - Project manifest containing the pipeline and generation settings. +/// * `output_dir` - Directory in which to write the generated files. +/// +/// # Errors +/// +/// Returns an error if the pipeline is invalid, the output directory cannot be created, +/// or any generated file cannot be written. +/// +/// # Examples +/// +/// ```no_run +/// # let manifest: Manifest = todo!(); +/// generate_all(&manifest, "generated")?; +/// # Ok::<(), anyhow::Error>(()) +/// ``` pub fn generate_all(manifest: &Manifest, output_dir: &str) -> Result<()> { let out = Path::new(output_dir); fs::create_dir_all(out).context("Failed to create output directory")?; @@ -57,22 +72,112 @@ pub fn generate_all(manifest: &Manifest, output_dir: &str) -> Result<()> { Ok(()) } -/// Build generated artifacts by invoking CMake + make. +/// Builds the generated project using CMake. +/// +/// The build uses `Release` mode when `release` is `true` and `Debug` mode +/// otherwise. +/// +/// # Examples +/// +/// ```ignore +/// build(&manifest, true)?; +/// # Ok::<(), anyhow::Error>(()) +/// ``` +/// +/// # Errors +/// +/// Returns an error if the manifest is invalid, CMake cannot be started, or +/// configuration or compilation fails. +/// +/// # Arguments +/// +/// * `release` - Selects the `Release` build configuration when `true`; +/// otherwise selects `Debug`. pub fn build(manifest: &Manifest, release: bool) -> Result<()> { + crate::manifest::validate(manifest)?; let build_type = if release { "Release" } else { "Debug" }; println!( "Building {} ({} mode) — target: {}", manifest.project.name, build_type, manifest.target.arch ); - println!( - " Run: cd generated/halideiser && cmake -B build -DCMAKE_BUILD_TYPE={} && cmake --build build", - build_type + let source_dir = Path::new("generated/halideiser"); + let build_dir = source_dir.join("build"); + // Give single- and multi-configuration generators the same runtime layout. + // The per-configuration variable prevents CMake appending another Release/ + // or Debug/ directory when using Ninja Multi-Config or Visual Studio. + let runtime_dir = std::env::current_dir()? + .join(&build_dir) + .join("bin") + .join(build_type); + let configure = std::process::Command::new("cmake") + .arg("-S") + .arg(source_dir) + .arg("-B") + .arg(&build_dir) + .arg(format!("-DCMAKE_BUILD_TYPE={build_type}")) + .arg(format!( + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={}", + runtime_dir.display() + )) + .arg(format!( + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{}={}", + build_type.to_uppercase(), + runtime_dir.display() + )) + .status() + .context("Failed to start CMake configuration")?; + anyhow::ensure!( + configure.success(), + "CMake configuration failed: {configure}" ); + let compile = std::process::Command::new("cmake") + .arg("--build") + .arg(&build_dir) + .arg("--config") + .arg(build_type) + .status() + .context("Failed to start CMake build")?; + anyhow::ensure!(compile.success(), "CMake build failed: {compile}"); Ok(()) } -/// Run the generated pipeline binary. +/// Runs the generated pipeline using the debug configuration. +/// +/// # Arguments +/// +/// * `args` - Arguments passed to the generated runner. +/// +/// # Returns +/// +/// `Ok(())` if the pipeline completes successfully; otherwise, an error. +/// +/// # Examples +/// +/// ```no_run +/// let manifest = Manifest::default(); +/// run(&manifest, &[])?; +/// # Ok::<(), _>(()) +/// ``` pub fn run(manifest: &Manifest, args: &[String]) -> Result<()> { + run_configuration(manifest, false, args) +} + +/// Executes the generated pipeline runner using the selected build configuration. +/// +/// Returns an error if the manifest is invalid, the runner cannot be started, or +/// the pipeline exits unsuccessfully. +/// +/// # Examples +/// +/// ```no_run +/// # use crate::codegen::run_configuration; +/// # use crate::manifest::Manifest; +/// # let manifest: Manifest = todo!(); +/// run_configuration(&manifest, false, &[])?; +/// # Ok::<(), anyhow::Error>(()) +/// ``` +pub fn run_configuration(manifest: &Manifest, release: bool, args: &[String]) -> Result<()> { + crate::manifest::validate(manifest)?; println!( "Running {} pipeline ({} stages)", manifest.project.name, @@ -81,9 +186,23 @@ pub fn run(manifest: &Manifest, args: &[String]) -> Result<()> { if !args.is_empty() { println!(" Extra args: {}", args.join(" ")); } - println!( - " Run: ./generated/halideiser/build/{}_runner ", - manifest.project.name - ); + let build_type = if release { "Release" } else { "Debug" }; + let binary = Path::new("generated/halideiser/build/bin") + .join(build_type) + .join(format!( + "{}_runner{}", + manifest.project.name, + std::env::consts::EXE_SUFFIX + )); + let status = std::process::Command::new(&binary) + .args(args) + .status() + .with_context(|| { + format!( + "Failed to execute {}; build the pipeline first", + binary.display() + ) + })?; + anyhow::ensure!(status.success(), "Pipeline execution failed: {status}"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index c3d4a0d..4f98426 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,9 @@ enum Commands { /// Path to the manifest file. #[arg(short, long, default_value = "halideiser.toml")] manifest: String, + /// Run the release configuration (matches build --release). + #[arg(long)] + release: bool, /// Additional arguments passed to the pipeline binary. #[arg(trailing_var_arg = true)] args: Vec, @@ -85,6 +88,18 @@ enum Commands { }, } +/// Runs the `halideiser` command-line interface and dispatches the selected subcommand. +/// +/// # Examples +/// +/// ``` +/// let command = "halideiser info --manifest halideiser.toml"; +/// assert!(command.starts_with("halideiser ")); +/// ``` +/// +/// # Errors +/// +/// Returns an error if the selected subcommand cannot complete successfully. fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { @@ -111,9 +126,17 @@ fn main() -> Result<()> { let m = manifest::load_manifest(&manifest)?; codegen::build(&m, release)?; } - Commands::Run { manifest, args } => { + Commands::Run { + manifest, + release, + args, + } => { let m = manifest::load_manifest(&manifest)?; - codegen::run(&m, &args)?; + if release { + codegen::run_configuration(&m, true, &args)?; + } else { + codegen::run(&m, &args)?; + } } Commands::Info { manifest } => { let m = manifest::load_manifest(&manifest)?; diff --git a/tests/abi-gate.sh b/tests/abi-gate.sh new file mode 100644 index 0000000..d315ff2 --- /dev/null +++ b/tests/abi-gate.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Regression controls for the actual structural ABI gate, not compiled proofs. +set -euo pipefail +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +fixture="$(mktemp -d)" +trap 'rm -rf -- "$fixture"' EXIT +mkdir -p "$fixture/src/interface/abi" "$fixture/src/interface/ffi/src" +abi="$fixture/src/interface/abi/Types.idr" +ffi="$fixture/src/interface/ffi/src/main.zig" +printf '%%foreign "C:sample"\nresultToInt Ok = 0\nresultToInt Error = 1\n' > "$abi" +printf 'export fn sample() void {}\nconst Result = enum(c_int) { ok = 0, err = 1 };\n' > "$ffi" +julia "$repo_root/scripts/abi-ffi-gate.jl" "$fixture" +cp "$abi" "$fixture/valid.idr" +cp "$ffi" "$fixture/valid.zig" +# reject verifies that the ABI/FFI gate rejects the fixture with exit status 1 for the named invalid condition. +reject() { + local name="$1" status=0 + julia "$repo_root/scripts/abi-ffi-gate.jl" "$fixture" > "$fixture/result.log" 2>&1 || status=$? + if [ "$status" -ne 1 ]; then + cat "$fixture/result.log" + echo "Expected structural rejection for $name; got $status" >&2 + exit 1 + fi + echo "Rejected: $name" +} +mv "$abi" "$fixture/temporarily-absent.idr" +reject 'missing Idris ABI sources' +mv "$fixture/temporarily-absent.idr" "$abi" +mv "$ffi" "$fixture/temporarily-absent.zig" +reject 'missing Zig FFI source' +mv "$fixture/temporarily-absent.zig" "$ffi" +printf '%%foreign "C:sample"\n' > "$abi" +reject 'missing Idris result mapping' +cp "$fixture/valid.idr" "$abi" +printf 'export fn sample() void {}\n' > "$ffi" +reject 'missing Zig result mapping' +printf 'export fn sample() void {}\nconst Result = enum(c_int) { ok = 0, err = 2 };\n' > "$ffi" +reject 'mismatched result code' +cp "$fixture/valid.zig" "$ffi" +julia "$repo_root/scripts/abi-ffi-gate.jl" "$fixture" diff --git a/tests/build_process.rs b/tests/build_process.rs new file mode 100644 index 0000000..84bd6a2 --- /dev/null +++ b/tests/build_process.rs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell +//! Execute real CMake and a host C fixture to test process orchestration. +//! This does not certify generated Halide algorithms or their schedules. +use std::{fs, process::Command}; + +/// Creates a temporary project fixture with a minimal configuration and generated-output directory. +/// +/// # Examples +/// +/// ``` +/// let dir = fixture(); +/// assert!(dir.path().join("halideiser.toml").is_file()); +/// assert!(dir.path().join("generated/halideiser").is_dir()); +/// ``` +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("halideiser.toml"), + "[project]\nname = 'probe'\n[[stages]]\nname = 'blur'\noperation = 'blur'\n[target]\narch = 'x86'\n[pipeline]\ninput-format = 'png'\noutput-format = 'png'\n" + ).unwrap(); + fs::create_dir_all(dir.path().join("generated/halideiser")).unwrap(); + dir +} + +/// Runs the `halideiser` binary in the specified directory with the given arguments. +/// +/// # Examples +/// +/// ```no_run +/// let output = invoke(std::path::Path::new("."), &["build"]); +/// assert!(output.status.success()); +/// ``` +/// +/// # Panics +/// +/// Panics if the process cannot be started. +fn invoke(dir: &std::path::Path, args: &[&str]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_halideiser")) + .current_dir(dir) + .args(args) + .output() + .unwrap() +} + +#[test] +fn build_and_run_execute_and_propagate_exit_status() { + let dir = fixture(); + let source = dir.path().join("generated/halideiser"); + fs::write(source.join("CMakeLists.txt"), + "cmake_minimum_required(VERSION 3.22)\nproject(probe C)\nadd_executable(probe_runner probe.c)\n").unwrap(); + fs::write(source.join("probe.c"), + "#include \nint main(int argc, char **argv) { return argc == 2 && strcmp(argv[1], \"two words\") == 0 ? 0 : 7; }\n").unwrap(); + let output = invoke(dir.path(), &["build", "--release"]); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + invoke(dir.path(), &["run", "--release", "--", "two words"]) + .status + .success() + ); + assert!(!invoke(dir.path(), &["run", "--release"]).status.success()); + // A release build must not silently satisfy a request for debug mode. + assert!( + !invoke(dir.path(), &["run", "--", "two words"]) + .status + .success() + ); +} + +#[test] +fn multi_configuration_generator_runs_the_selected_mode() { + let ninja = Command::new("ninja") + .arg("--version") + .output() + .is_ok_and(|output| output.status.success()); + let generator = Command::new("cmake") + .arg("--help") + .output() + .is_ok_and(|output| { + output.status.success() + && String::from_utf8_lossy(&output.stdout).contains("Ninja Multi-Config") + }); + if !ninja || !generator { + // Local developers may have only a single-configuration generator. + // CI must exercise this regression, never silently report a skipped pass. + assert!( + std::env::var_os("CI").is_none(), + "CI requires Ninja and CMake with Ninja Multi-Config support" + ); + eprintln!("SKIP: install Ninja and a CMake supporting Ninja Multi-Config to run this test"); + return; + } + let dir = fixture(); + let source = dir.path().join("generated/halideiser"); + fs::write(source.join("CMakeLists.txt"), + "cmake_minimum_required(VERSION 3.22)\nproject(probe C)\nadd_executable(probe_runner probe.c)\n").unwrap(); + fs::write(source.join("probe.c"), + "#include \nint main(void) {\n#ifdef NDEBUG\nputs(\"release\");\n#else\nputs(\"debug\");\n#endif\nreturn 0; }\n").unwrap(); + for (mode, expected) in [("Release", "release"), ("Debug", "debug")] { + let mut command = Command::new(env!("CARGO_BIN_EXE_halideiser")); + command + .current_dir(dir.path()) + .env("CMAKE_GENERATOR", "Ninja Multi-Config") + .arg("build"); + if mode == "Release" { + command.arg("--release"); + } + let output = command.output().unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let args = if mode == "Release" { + vec!["run", "--release"] + } else { + vec!["run"] + }; + let output = invoke(dir.path(), &args); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line == expected) + ); + } + // The second build must preserve the independently selected first artifact. + let output = invoke(dir.path(), &["run", "--release"]); + assert!(output.status.success()); + assert!( + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line == "release") + ); +} + +#[test] +fn missing_build_inputs_and_missing_runner_fail() { + let dir = fixture(); + assert!(!invoke(dir.path(), &["build"]).status.success()); + assert!(!invoke(dir.path(), &["run"]).status.success()); +} + +#[test] +fn compilation_failure_after_successful_configuration_fails() { + let dir = fixture(); + let source = dir.path().join("generated/halideiser"); + fs::write(source.join("CMakeLists.txt"), + "cmake_minimum_required(VERSION 3.22)\nproject(probe C)\nadd_executable(probe_runner broken.c)\n").unwrap(); + fs::write( + source.join("broken.c"), + "#error planted compilation failure\n", + ) + .unwrap(); + assert!(!invoke(dir.path(), &["build"]).status.success()); +}