ci: add PyInstaller binary build workflow for foryc #12
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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. | |
| # ───────────────────────────────────────────────────────────────────────────── |