Skip to content

ci: add PyInstaller binary build workflow for foryc #12

ci: add PyInstaller binary build workflow for foryc

ci: add PyInstaller binary build workflow for foryc #12

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: Build foryc Binaries
on:
workflow_dispatch:
workflow_call:
outputs:
artifacts_ready:
description: "True if all 5 foryc binaries built and validated."
value: ${{ jobs.build-complete.outputs.ready }}
push:
tags: ["foryc-v*"]
pull_request:
paths:
- "compiler/**"
- ".github/workflows/build-foryc-binaries.yml"
# Cancel in-progress runs on the same PR to avoid wasting CI minutes on
# force-pushes. Does NOT cancel tagged or dispatch runs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
PYTHON_VERSION: "3.11"
# ─────────────────────────────────────────────────────────────────────────────
# ACTION SHA PINS — verified live against GitHub commit graph 2026-02-24
#
# actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
# actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
# actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
#
# Node.js 24 runner requirement:
# setup-python v6, upload-artifact v6, and download-artifact v7 all run on
# Node.js 24 (runs.using: node24) and require Actions runner >= v2.327.1.
# GitHub-hosted runners (ubuntu-22.04, macos-15, windows-latest) are
# auto-updated and satisfy this requirement automatically.
# Self-hosted runners MUST be upgraded to >= v2.327.1 before using this
# workflow, otherwise setup-python, upload-artifact, and download-artifact
# will refuse to execute.
#
# Re-verify SHAs at each Fory release:
# https://github.com/actions/checkout/releases
# https://github.com/actions/setup-python/releases
# https://github.com/actions/upload-artifact/releases
# https://github.com/actions/download-artifact/releases
# ─────────────────────────────────────────────────────────────────────────────
# ─────────────────────────────────────────────────────────────────────────────
# SPEC-CHECK JOB (pull_request only — lightweight, no binary build)
#
# Full 5-platform binary builds on every compiler PR cost ~25 CI-minutes.
# On PRs we run a fast spec-check instead:
# 1. Verify all fory_compiler modules are importable via importlib.import_module
# (same pkgutil discovery as foryc.spec — single source of truth)
# 2. AST-parse foryc.spec to catch Python syntax errors without executing it
# (executing the spec requires PyInstaller-injected Analysis/PYZ/EXE/COLLECT
# globals absent in a plain interpreter; ast.parse validates syntax only)
#
# Full builds run on: workflow_dispatch, workflow_call, push tags foryc-v*
# ─────────────────────────────────────────────────────────────────────────────
jobs:
spec-check:
name: spec / import-check (PR)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
cache-dependency-path: |
compiler/requirements-dev.txt
compiler/pyproject.toml
- name: Install fory_compiler (no pyinstaller needed for spec-check)
run: |
python -m pip install --upgrade pip
pip install ./compiler
# importlib.import_module CONTRACT:
# Unlike __import__, importlib.import_module('fory_compiler.ir.ast')
# explicitly loads the named submodule, not just its parent chain.
# This catches broken submodules that a parent __init__.py would mask.
#
# onerror collects ALL failures before raising so every broken package
# is reported in one pass, not just the first one encountered.
- name: Verify all fory_compiler modules are importable
working-directory: compiler
run: |
python -c "
import importlib, pkgutil, fory_compiler
walk_errors = []
def onerror(name):
walk_errors.append(name)
mods = ['fory_compiler'] + [
m.name for m in pkgutil.walk_packages(
path=fory_compiler.__path__,
prefix=fory_compiler.__name__ + '.',
onerror=onerror,
)
]
if walk_errors:
raise ImportError(
'walk_packages failed to recurse into: ' + str(walk_errors) +
'\nFix import errors in these packages before building.'
)
import_errors = []
for m in mods:
try:
importlib.import_module(m)
print(f'OK: {m}')
except Exception as e:
import_errors.append(f'{m}: {e}')
if import_errors:
raise ImportError(
'Failed to import:\n' + '\n'.join(import_errors)
)
print(f'All {len(mods)} module(s) verified.')
"
# ast.parse validates Python syntax without executing the spec.
# Executing foryc.spec directly fails because Analysis/PYZ/EXE/COLLECT
# are PyInstaller-injected globals absent in a plain interpreter.
- name: Syntax-check foryc.spec (AST parse)
run: |
python -c "
import ast
with open('compiler/foryc.spec', encoding='utf-8') as f:
src = f.read()
tree = ast.parse(src, filename='foryc.spec')
print(f'foryc.spec: syntax OK ({len(list(ast.walk(tree)))} AST nodes)')
"
# ─────────────────────────────────────────────────────────────────────────────
# BUILD JOB (tags / workflow_dispatch / workflow_call only — NOT pull_request)
#
# Runner notes:
# linux-x86_64 : ubuntu-22.04
# linux-aarch64 : ubuntu-24.04-arm (native ARM64 runner — no QEMU)
# macos-x86_64 : macos-15-intel
# macos-aarch64 : macos-15
# windows-x86_64: windows-latest (Server 2022)
#
# --onedir mode (not --onefile):
# --onefile extracts DLLs to %TEMP% at runtime. Windows Defender on GitHub's
# hardened runners intercepts LoadLibrary at the memory-mapping level
# (PYI-xxxx ERROR_NOACCESS) even with DisableRealtimeMonitoring $true.
# --onedir pre-extracts at build time; no runtime extraction, no interception.
#
# UPX notes:
# Applied to the exe stub only, not the entire dist/foryc/ directory.
# macOS: skipped — UPX 4.x+ dropped Mach-O support entirely.
# Windows: skipped — onedir bootloader stub PE layout incompatible with
# UPX --best --lzma (exits code 1, produces no output).
#
# Phase 2 distribution note:
# --onedir produces 20-40 MB total (python311.dll + stdlib .pyc files).
# This exceeds the 10 MB crates.io per-crate limit.
# foryc-bin will distribute via zip archive or download-on-first-install.
# foryc_path in foryc-build Config must resolve to a DIRECTORY, not a
# single file — Phase 2 implementors must account for this.
#
# windows-aarch64:
# Not included in this PR. Tracked in issue #3292.
# GitHub Actions windows-11-arm runners are now available but PyInstaller
# aarch64 Windows support requires validation. Targeted for Phase 2.
# ─────────────────────────────────────────────────────────────────────────────
build:
name: build / ${{ matrix.target }}
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: linux-x86_64
os: ubuntu-22.04
artifact_name: foryc-linux-x86_64
binary_path: compiler/dist/foryc/foryc
binary_name: foryc
use_upx: true
codesign: false
- target: linux-aarch64
os: ubuntu-24.04-arm
artifact_name: foryc-linux-aarch64
binary_path: compiler/dist/foryc/foryc
binary_name: foryc
use_upx: true
codesign: false
- target: macos-x86_64
os: macos-15-intel
artifact_name: foryc-macos-x86_64
binary_path: compiler/dist/foryc/foryc
binary_name: foryc
use_upx: false
codesign: false
- target: macos-aarch64
os: macos-15
artifact_name: foryc-macos-aarch64
binary_path: compiler/dist/foryc/foryc
binary_name: foryc
use_upx: false
codesign: true
- target: windows-x86_64
os: windows-latest
artifact_name: foryc-windows-x86_64
binary_path: compiler/dist/foryc/foryc.exe
binary_name: foryc.exe
use_upx: false
codesign: false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
cache-dependency-path: |
compiler/requirements-dev.txt
compiler/pyproject.toml
# ── Install UPX ─────────────────────────────────────────────────────────
- name: Install UPX (Linux)
if: runner.os == 'Linux' && matrix.use_upx
run: |
sudo apt-get update -qq
sudo apt-get install -y upx-ucl
upx --version
- name: Install UPX (Windows)
if: runner.os == 'Windows' && matrix.use_upx
shell: pwsh
run: |
choco install upx --yes --no-progress
upx --version
# ── macOS: pin deployment target ─────────────────────────────────────────
# Without MACOSX_DEPLOYMENT_TARGET, binaries built on macOS 15 embed
# LC_BUILD_VERSION minos=15.0 and silently fail on macOS ≤14 with:
# dyld: Symbol not found / incompatible library version
# 13.0 rationale:
# - Covers all active Apple Silicon + Intel users
# - Python 3.11 minimum is 10.9; 13.0 is safe
# - macOS 13 (Ventura) is the oldest release still receiving security
# patches as of 2026
- name: Set macOS deployment target
if: runner.os == 'macOS'
run: echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> "$GITHUB_ENV"
# ── Install build dependencies ──────────────────────────────────────────
# pyinstaller is pinned in requirements-dev.txt (>=6.0,<7.0).
# fory_compiler installed from source so PyInstaller's import tracer
# walks the actual installed package tree, matching pkgutil.walk_packages
# in foryc.spec.
- name: Install PyInstaller and fory_compiler
run: |
python -m pip install --upgrade pip
pip install -r compiler/requirements-dev.txt
pip install ./compiler
# ── Verify all fory_compiler modules are importable ─────────────────────
# Same pkgutil discovery as foryc.spec — single source of truth.
# importlib.import_module guarantees each named submodule is independently
# loaded, not just its parent chain.
# onerror collects ALL failing packages before raising.
- name: Verify all fory_compiler modules are importable
working-directory: compiler
run: |
python -c "
import importlib, pkgutil, fory_compiler
walk_errors = []
def onerror(name):
walk_errors.append(name)
mods = ['fory_compiler'] + [
m.name for m in pkgutil.walk_packages(
path=fory_compiler.__path__,
prefix=fory_compiler.__name__ + '.',
onerror=onerror,
)
]
if walk_errors:
raise ImportError(
'walk_packages failed to recurse into: ' + str(walk_errors) +
'\nFix import errors in these packages before building.'
)
import_errors = []
for m in mods:
try:
importlib.import_module(m)
print(f'OK: {m}')
except Exception as e:
import_errors.append(f'{m}: {e}')
if import_errors:
raise ImportError(
'Failed to import:\n' + '\n'.join(import_errors)
)
print(f'All {len(mods)} module(s) verified.')
"
# ── Build ───────────────────────────────────────────────────────────────
# Must run from compiler/ so pathex=['.'] in foryc.spec resolves
# fory_compiler/__main__.py correctly.
- name: Build standalone binary with PyInstaller
working-directory: compiler
run: pyinstaller foryc.spec
# ── Verify macOS deployment target ───────────────────────────────────────
# Asserts minos <= 13.x. Fails hard if minos cannot be parsed — a
# WARNING-and-pass would allow a minos=15.0 binary through silently.
- name: Verify macOS deployment target (minos ≤ 13.x)
if: runner.os == 'macOS'
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
echo "=== otool LC_BUILD_VERSION ==="
otool -l "${BINARY_PATH}" | grep -A4 LC_BUILD_VERSION \
|| otool -l "${BINARY_PATH}" | grep -A3 LC_VERSION_MIN_MACOSX \
|| true
MINOS=$(otool -l "${BINARY_PATH}" \
| grep -A4 LC_BUILD_VERSION \
| awk '/minos/{print $2}' \
| head -1)
if [ -z "${MINOS}" ]; then
MINOS=$(otool -l "${BINARY_PATH}" \
| grep -A3 LC_VERSION_MIN_MACOSX \
| awk '/version/{print $2}' \
| head -1)
fi
if [ -z "${MINOS}" ]; then
echo "ERROR: could not parse minos from otool output."
echo " Verify MACOSX_DEPLOYMENT_TARGET=13.0 was set during build."
exit 1
fi
MAJOR=$(echo "${MINOS}" | cut -d. -f1)
if [ "${MAJOR}" -gt 13 ]; then
echo "FAIL: minos=${MINOS} — binary requires macOS ${MAJOR}+ and will"
echo " not launch on macOS ≤13."
exit 1
fi
echo "PASS: minos=${MINOS} (≤ 13.x — compatible with all active macOS releases)"
# ── Pre-compression smoke test ──────────────────────────────────────────
- name: Smoke test (pre-UPX)
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
echo "=== Pre-UPX binary size ==="
python -c "
import os
p = os.environ['BINARY_PATH']
s = os.path.getsize(p)
print(f'Size: {s:,} bytes ({s/1024/1024:.2f} MB)')
"
"${BINARY_PATH}" --help
# ── UPX compression ─────────────────────────────────────────────────────
- name: Compress with UPX
if: matrix.use_upx
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
upx --best --lzma "${BINARY_PATH}"
echo "=== Post-UPX binary size ==="
python -c "
import os
p = os.environ['BINARY_PATH']
s = os.path.getsize(p)
print(f'Size: {s:,} bytes ({s/1024/1024:.2f} MB)')
"
# ── macOS aarch64: ad-hoc codesign ───────────────────────────────────────
# Apple Silicon requires valid signatures on the exe AND every .dylib/.so
# in the --onedir package. codesign --deep does NOT traverse flat
# directories (only .app/.framework bundles).
# BINARY_PATH via env prevents quoting failures on paths with spaces.
# Verify loop covers all dylibs — an unsigned dylib causes DYLD_LIBRARY_PATH
# resolution failure at runtime even if the main exe is correctly signed.
- name: Ad-hoc codesign (macOS aarch64 — Apple Silicon requirement)
if: matrix.codesign
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
DIST_DIR="$(dirname "${BINARY_PATH}")"
echo "=== Signing all .dylib and .so files in ${DIST_DIR} ==="
find "${DIST_DIR}" \( -name "*.dylib" -o -name "*.so" \) \
-exec codesign --force --sign - {} \;
echo "=== Signing main executable ==="
codesign --force --sign - "${BINARY_PATH}"
echo "=== Verifying all .dylib and .so signatures ==="
VERIFY_FAILURES=0
while IFS= read -r -d '' lib; do
if ! codesign --verify --verbose "${lib}" 2>&1; then
echo "FAIL: signature verification failed for ${lib}"
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
fi
done < <(find "${DIST_DIR}" \( -name "*.dylib" -o -name "*.so" \) -print0)
echo "=== Verifying main executable signature ==="
codesign --verify --verbose "${BINARY_PATH}"
if [ "${VERIFY_FAILURES}" -gt 0 ]; then
echo "FAIL: ${VERIFY_FAILURES} library signature verification(s) failed."
exit 1
fi
echo "PASS: all signatures verified."
# ── Final smoke test ─────────────────────────────────────────────────────
# Named "final" not "post-UPX" — UPX is skipped on 3 of 5 targets.
# Runs after all transformations (UPX, codesign) are complete.
- name: Smoke test (final)
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
"${BINARY_PATH}" --help
# ── Onedir package size report ───────────────────────────────────────────
- name: Report onedir package size
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
python -c "
import os, sys, pathlib
binary = pathlib.Path(os.environ['BINARY_PATH'])
dist_dir = binary.parent
total = sum(f.stat().st_size for f in dist_dir.rglob('*') if f.is_file())
exe_size = binary.stat().st_size
print(f'Executable: {exe_size:,} bytes ({exe_size/1024/1024:.2f} MB)')
print(f'Total dir: {total:,} bytes ({total/1024/1024:.2f} MB)')
if total > 50 * 1024 * 1024:
print('FAIL: onedir package exceeds 50 MB sanity limit.')
sys.exit(1)
print('PASS')
"
# ── SHA-256 checksums ────────────────────────────────────────────────────
# Generates SHA256SUMS.txt for Phase 2 foryc-build integrity verification.
# Python used for cross-platform consistency (sha256sum/shasum/certutil
# are all platform-specific; Python's hashlib is not).
# chr(92) == backslash — normalises Windows paths in the output file.
- name: Generate SHA-256 checksums
shell: bash
env:
BINARY_PATH: ${{ matrix.binary_path }}
run: |
python -c "
import hashlib, os, pathlib
binary = pathlib.Path(os.environ['BINARY_PATH'])
dist_dir = binary.parent
lines = []
for f in sorted(dist_dir.rglob('*')):
if f.is_file() and f.name != 'SHA256SUMS.txt':
digest = hashlib.sha256(f.read_bytes()).hexdigest()
rel = str(f.relative_to(dist_dir)).replace(chr(92), '/')
lines.append(f'{digest} {rel}')
print(f'{digest} {rel}')
(dist_dir / 'SHA256SUMS.txt').write_text(
'\n'.join(lines) + '\n', encoding='utf-8'
)
print(f'Wrote {len(lines)} checksums to SHA256SUMS.txt')
"
# ── Upload artifact ──────────────────────────────────────────────────────
# retention-days: tagged releases → 90 days (Phase 2 runway)
# all other builds → 30 days
- name: Upload binary artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ matrix.artifact_name }}
path: compiler/dist/foryc/
retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 90 || 30 }}
if-no-files-found: error
# ─────────────────────────────────────────────────────────────────────────────
# VALIDATE JOB (tags / workflow_dispatch / workflow_call only)
#
# All 5 generator backends validated — a generator can be correctly included
# in hiddenimports at build time but silently fail at runtime (e.g. a
# conditional import inside the generator that PyInstaller missed).
# Python is NOT set up — binary must be fully self-contained.
# ─────────────────────────────────────────────────────────────────────────────
validate:
name: validate / ${{ matrix.target }}
if: github.event_name != 'pull_request'
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: linux-x86_64
os: ubuntu-22.04
artifact_name: foryc-linux-x86_64
binary_name: foryc
- target: linux-aarch64
os: ubuntu-24.04-arm
artifact_name: foryc-linux-aarch64
binary_name: foryc
- target: macos-x86_64
os: macos-15-intel
artifact_name: foryc-macos-x86_64
binary_name: foryc
- target: macos-aarch64
os: macos-15
artifact_name: foryc-macos-aarch64
binary_name: foryc
- target: windows-x86_64
os: windows-latest
artifact_name: foryc-windows-x86_64
binary_name: foryc.exe
steps:
- name: Checkout (sparse — compiler/examples only)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: compiler/examples
sparse-checkout-cone-mode: true
- name: Download artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ${{ matrix.artifact_name }}
path: ./artifact
- name: Make executable (Unix)
if: runner.os != 'Windows'
run: chmod +x ./artifact/${{ matrix.binary_name }}
# Pre-flight: clear error message instead of confusing foryc exit code.
- name: Assert compiler/examples/demo.fdl exists
shell: bash
run: |
if [ ! -f compiler/examples/demo.fdl ]; then
echo "ERROR: compiler/examples/demo.fdl not found."
echo " This file must exist in the repository for E2E validation."
exit 1
fi
echo "PASS: compiler/examples/demo.fdl found."
# ── Test 1: --help ───────────────────────────────────────────────────────
- name: Validate --help output
shell: bash
run: |
OUTPUT=$(./artifact/${{ matrix.binary_name }} --help 2>&1)
echo "$OUTPUT"
echo "$OUTPUT" | grep -qiE "^usage:.*foryc" || {
echo "ERROR: --help output missing expected 'usage: ... foryc' line."
exit 1
}
# ── Test 2: FDL → Rust ───────────────────────────────────────────────────
- name: End-to-end compile demo.fdl → Rust
shell: bash
run: |
OUT_DIR="${RUNNER_TEMP}/foryc-e2e-rust"
mkdir -p "${OUT_DIR}"
./artifact/${{ matrix.binary_name }} --rust_out "${OUT_DIR}" compiler/examples/demo.fdl
ls -la "${OUT_DIR}/"
RS_COUNT=$(find "${OUT_DIR}" -name "*.rs" -size +0c | wc -l)
[ "${RS_COUNT}" -gt 0 ] || { echo "ERROR: no .rs files generated"; exit 1; }
echo "PASS: ${RS_COUNT} .rs file(s) generated"
# ── Test 3: FDL → Java ───────────────────────────────────────────────────
- name: End-to-end compile demo.fdl → Java
shell: bash
run: |
OUT_DIR="${RUNNER_TEMP}/foryc-e2e-java"
mkdir -p "${OUT_DIR}"
./artifact/${{ matrix.binary_name }} --java_out "${OUT_DIR}" compiler/examples/demo.fdl
ls -la "${OUT_DIR}/"
JAVA_COUNT=$(find "${OUT_DIR}" -name "*.java" -size +0c | wc -l)
[ "${JAVA_COUNT}" -gt 0 ] || { echo "ERROR: no .java files generated"; exit 1; }
echo "PASS: ${JAVA_COUNT} .java file(s) generated"
# ── Test 4: FDL → Python ─────────────────────────────────────────────────
- name: End-to-end compile demo.fdl → Python
shell: bash
run: |
OUT_DIR="${RUNNER_TEMP}/foryc-e2e-python"
mkdir -p "${OUT_DIR}"
./artifact/${{ matrix.binary_name }} --python_out "${OUT_DIR}" compiler/examples/demo.fdl
ls -la "${OUT_DIR}/"
PY_COUNT=$(find "${OUT_DIR}" -name "*.py" -size +0c | wc -l)
[ "${PY_COUNT}" -gt 0 ] || { echo "ERROR: no .py files generated"; exit 1; }
echo "PASS: ${PY_COUNT} .py file(s) generated"
# ── Test 5: FDL → C++ ────────────────────────────────────────────────────
- name: End-to-end compile demo.fdl → C++
shell: bash
run: |
OUT_DIR="${RUNNER_TEMP}/foryc-e2e-cpp"
mkdir -p "${OUT_DIR}"
./artifact/${{ matrix.binary_name }} --cpp_out "${OUT_DIR}" compiler/examples/demo.fdl
ls -la "${OUT_DIR}/"
CPP_COUNT=$(find "${OUT_DIR}" \
\( -name "*.h" -o -name "*.cc" -o -name "*.cpp" \) -size +0c | wc -l)
[ "${CPP_COUNT}" -gt 0 ] || { echo "ERROR: no C++ files generated"; exit 1; }
echo "PASS: ${CPP_COUNT} C++ file(s) generated"
# ── Test 6: FDL → Go ─────────────────────────────────────────────────────
# Go output may be nested in subdirs based on go_package schema option.
- name: End-to-end compile demo.fdl → Go
shell: bash
run: |
OUT_DIR="${RUNNER_TEMP}/foryc-e2e-go"
mkdir -p "${OUT_DIR}"
./artifact/${{ matrix.binary_name }} --go_out "${OUT_DIR}" compiler/examples/demo.fdl
find "${OUT_DIR}" -type f | sort
GO_COUNT=$(find "${OUT_DIR}" -name "*.go" -size +0c | wc -l)
[ "${GO_COUNT}" -gt 0 ] || { echo "ERROR: no .go files generated"; exit 1; }
echo "PASS: ${GO_COUNT} .go file(s) generated"
# ─────────────────────────────────────────────────────────────────────────────
# SUMMARY JOB
# Single required status check for branch protection rules.
# Phase 4 release pipeline reads artifacts_ready via workflow_call.
#
# GitHub Actions result values for a conditionally skipped job (if: false):
# needs.<job>.result == "skipped" — documented, always "skipped" not "".
#
# Three valid terminal states:
# 1. Tagged/dispatch: BUILD+VALIDATE success → ready=true, exit 0
# SPEC_CHECK = "skipped" on these events — State 1 does not check it.
# 2. PR: SPEC_CHECK success, BUILD+VALIDATE skipped → ready=false, exit 0
# 3. Any actual failure → ready=false, exit 1
# ─────────────────────────────────────────────────────────────────────────────
build-complete:
name: foryc / all binaries ready
needs: [spec-check, build, validate]
runs-on: ubuntu-latest
if: always()
outputs:
ready: ${{ steps.check.outputs.ready }}
steps:
- name: Evaluate results
id: check
run: |
SPEC_CHECK="${{ needs.spec-check.result }}"
BUILD="${{ needs.build.result }}"
VALIDATE="${{ needs.validate.result }}"
echo "spec-check: ${SPEC_CHECK}"
echo "build: ${BUILD}"
echo "validate: ${VALIDATE}"
# State 1: Full build (tag / workflow_dispatch / workflow_call)
if [[ "${BUILD}" == "success" && "${VALIDATE}" == "success" ]]; then
echo "ready=true" >> "${GITHUB_OUTPUT}"
echo "PASS: full binary build and validation succeeded."
exit 0
fi
# State 2: PR — build+validate skipped by design, spec-check ran
if [[ "${BUILD}" == "skipped" && "${VALIDATE}" == "skipped" \
&& "${SPEC_CHECK}" == "success" ]]; then
echo "ready=false" >> "${GITHUB_OUTPUT}"
echo "PASS: PR spec-check succeeded. Full build skipped by design."
exit 0
fi
# State 3: Failure
echo "ready=false" >> "${GITHUB_OUTPUT}"
echo "FAIL: spec-check=${SPEC_CHECK} build=${BUILD} validate=${VALIDATE}"
exit 1
# ─────────────────────────────────────────────────────────────────────────────
# FALLBACK: If ubuntu-24.04-arm runner is unavailable in your fork,
# replace the linux-aarch64 matrix entry with:
#
# - target: linux-aarch64
# os: ubuntu-22.04
# artifact_name: foryc-linux-aarch64
# binary_path: compiler/dist/foryc/foryc
# binary_name: foryc
# use_upx: true
# codesign: false
#
# Add `if: matrix.target != 'linux-aarch64'` to these three steps:
# "Install PyInstaller and fory_compiler"
# "Verify all fory_compiler modules are importable"
# "Build standalone binary with PyInstaller"
#
# Then add these two steps BEFORE "Install PyInstaller and fory_compiler":
#
# - name: Set up QEMU (linux-aarch64 fallback only)
# if: matrix.target == 'linux-aarch64'
# uses: docker/setup-qemu-action@v3
# # Pin to full SHA: https://github.com/docker/setup-qemu-action/releases
# with:
# platforms: arm64
#
# - name: Build via Docker (linux-aarch64 QEMU fallback)
# if: matrix.target == 'linux-aarch64'
# uses: addnab/docker-run-action@v3
# # Pin to full SHA: https://github.com/addnab/docker-run-action/releases
# with:
# image: python:3.11-slim-bookworm
# options: --platform linux/arm64 -v ${{ github.workspace }}:/ws
# run: |
# apt-get update -qq && apt-get install -y upx-ucl binutils
# pip install -r /ws/compiler/requirements-dev.txt
# pip install /ws/compiler
# cd /ws/compiler && pyinstaller foryc.spec
#
# QEMU builds are 10-20x slower than native. Expect 20-30 min per run.
# ─────────────────────────────────────────────────────────────────────────────