validate #406
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
| name: validate | |
| on: | |
| pull_request: | |
| paths: ["pkgs/**/*.lua", "tests/**", "README.md", ".github/workflows/validate.yml"] | |
| push: | |
| branches: [main] | |
| schedule: | |
| # nightly full regression — exercises every workspace member regardless of diff | |
| - cron: "0 6 * * *" | |
| workflow_dispatch: | |
| env: | |
| # 0.0.102: windows command-line ceiling (mcpp#261 — the clang scan rule got | |
| # its P1689 JSON through shell redirection, which forced a `cmd /c` wrapper | |
| # and with it cmd.exe's 8191-char limit; clang-scan-deps -o removes both, and | |
| # $local_includes-carrying rules now fall back to response files). The pin | |
| # matters here because a package consumed FROM the registry sits under a | |
| # ~124-char xpkgs path instead of its own ~23-char checkout, which is what | |
| # pushed the vendored-opencv scan command over the line. Also: purview-include | |
| # depfile tracking extended to Clang (#257 — stale BMI reuse), OS-conditional | |
| # `[build].flags` (#258), and per-OS splices keyed on the resolved target | |
| # rather than the host (#254). | |
| # 0.0.101: per-feature per-glob flags + per-OS features (mcpp#253) — what | |
| # lets opencv select its dnn gemm backend per platform. | |
| # 0.0.99: feature dep/feat forwarding (mcpp#243 — a feature can open a | |
| # feature OF a dependency, e.g. opencv `dnn` forwarding compat.opencv/dnn); | |
| # vendored xlings 0.4.67 for the >=2 index_repo install fix (mcpp#238 / | |
| # openxlings/xlings#374); build.mcpp compiled program named `.exe` on | |
| # Windows (mcpp#230 secondary surface, after the 0.0.96 scanner crash fix). | |
| # 0.0.98: closes the obj-path disambiguation follow-ups that gated the | |
| # source-build compat.opencv unification — #240 (link inputs now follow | |
| # the disambiguated object names, so a dependency + consumer sharing a | |
| # source basename like `src/main.cpp` no longer 'obj/main.o missing') and | |
| # #239 (absolute/`..` dep-generated source paths sanitized component-wise | |
| # so objects stay under obj/). Also: `MCPP_DEP_<NAME>_DIR` build.mcpp | |
| # contract (#241), consumer-side `default-features = false` (#242), and a | |
| # loud unknown-mcpp-key warning with did-you-mean (#237, replaces the | |
| # silent-ignore at build time). Carried from 0.0.97: default-namespace | |
| # index redirect (`[indices] default = { path }`), which turned the public | |
| # module packages (imgui/ffmpeg/opencv/tinyhttps) into ordinary workspace | |
| # members and retired the per-package reseeding smoke shells + their | |
| # dedicated jobs; synchronous nasm bootstrap (mcpp#232 — the `mcpp index | |
| # update` pre-step is gone), obj-path disambiguation (#233), spacey-defines | |
| # quoting (#234), purview-include depfile tracking (#235). Older floors of | |
| # note: 0.0.96 fixed the windows scanner symlink-escape crash (mcpp#230); | |
| # 0.0.94 fixed feature-gated `sources` under `mcpp test` (mcpp#218); 0.0.91 | |
| # added standard = "c++fly" to the resolver grammar, so c++fly descriptors | |
| # get the lint WARN below, not a hard grammar-parse rejection. | |
| MCPP_VERSION: "0.0.103" | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install lua | |
| run: sudo apt-get install -y --no-install-recommends lua5.4 | |
| - name: Lint package descriptors | |
| run: | | |
| fail=0 | |
| for f in pkgs/*/*.lua; do | |
| # 1. Lua syntax check — load (= compile) without executing. | |
| # `loadfile(name, 't')` rejects bytecode and parses text only. | |
| if ! lua5.4 -e "assert(loadfile('$f', 't'))" >/dev/null 2>&1; then | |
| echo "::error file=$f::lua syntax error" | |
| fail=1 | |
| fi | |
| # 2. xpkg V1 baseline: the file has to populate `package = { ... }` | |
| # with at least `spec`, `name`, and an `xpm` table. Form A vs | |
| # Form B (mcpp = "<path>" / mcpp = { ... }) is descriptor-author | |
| # choice and not enforced here. | |
| for needle in 'spec *=' 'name *=' 'xpm *='; do | |
| if ! grep -q "$needle" "$f"; then | |
| echo "::error file=$f::missing required field ($needle)" | |
| fail=1 | |
| fi | |
| done | |
| # 3. Package version identifiers and dependency versions should be | |
| # bare versions ("1.2.3"), not upstream tag names ("v1.2.3"). | |
| # Download URLs may still contain refs/tags/v* when upstream | |
| # uses that tag spelling. | |
| if grep -nE '\["v[0-9]+|\["[^"]+"\][[:space:]]*=[[:space:]]*"v[0-9]+' "$f"; then | |
| echo "::error file=$f::version identifiers must not use a leading v" | |
| fail=1 | |
| fi | |
| # 4. Mirror table sanity: when a download `url` is written as a | |
| # { GLOBAL=..., CN=... } table, both regions must be present and | |
| # the CN entry must point at the gitcode mcpp-res mirror. | |
| if ! lua5.4 tests/check_mirror_urls.lua "$f"; then | |
| fail=1 | |
| fi | |
| # 5. `name` must be the fully-qualified `<namespace>.<short>`. | |
| # The split form (namespace = "chriskohlhoff", name = "asio") | |
| # parses and passes `mcpp xpkg parse`, but xlings keys the index | |
| # on the literal `name` while mcpp asks for the reconstructed | |
| # FQN — they never meet, so the package is uninstallable on | |
| # every platform (mcpp-community/mcpp#278). This check is cheap | |
| # and catches in seconds what otherwise fails an hour into the | |
| # workspace job. | |
| if ! lua5.4 tests/check_package_name.lua "$f"; then | |
| fail=1 | |
| fi | |
| # 6. c++fly admission policy (mcpp design 2026-07-14 §11-Q2, v1): | |
| # c++fly means "toolchain's latest level + every experimental | |
| # gate" — deliberately toolchain-dependent, so a published | |
| # package built with it is not reproducible for consumers. | |
| # Policy: WARN (never fail) and observe ecosystem usage before | |
| # deciding whether to tighten. Two spellings: `language = ` is | |
| # the descriptor's inline mcpp-segment key; `standard = ` covers | |
| # mcpp.toml content embedded in heredoc/generated_files blocks. | |
| if grep -nE '\b(language|standard)[[:space:]]*=[[:space:]]*"c\+\+fly"' "$f" >/dev/null; then | |
| echo "::warning file=$f::declares C++ standard \"c++fly\" (experimental playground mode) — toolchain-dependent and non-reproducible for consumers; published packages should pin a concrete standard (c++23/c++26)" | |
| fi | |
| done | |
| [ $fail -eq 0 ] && echo "All package files valid." | |
| exit $fail | |
| # ── Single-source-of-truth grammar check ───────────────────────── | |
| # `mcpp xpkg parse` uses EXACTLY the resolver's parser, so what | |
| # passes here is what builds for users of the pinned MCPP_VERSION. | |
| # Strict by default: unknown mcpp-segment keys fail (they would be | |
| # silently ignored at build time). This also mechanically enforces | |
| # the rollout rule "floor first, new grammar after": descriptors | |
| # needing a newer grammar cannot pass a lint pinned to an older mcpp. | |
| - name: Download pinned mcpp | |
| run: | | |
| curl -L -fsS -o mcpp.tar.gz \ | |
| "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-linux-x86_64.tar.gz" | |
| tar -xzf mcpp.tar.gz | |
| echo "MCPP=$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/bin/mcpp" >> "$GITHUB_ENV" | |
| - name: Parse descriptors with the resolver grammar (mcpp xpkg parse) | |
| run: | | |
| fail=0 | |
| for f in pkgs/*/*.lua; do | |
| if ! "$MCPP" xpkg parse "$f" > /dev/null; then | |
| echo "::error file=$f::mcpp xpkg parse failed (resolver grammar)" | |
| fail=1 | |
| fi | |
| done | |
| [ $fail -eq 0 ] && echo "All descriptors parse with mcpp ${MCPP_VERSION}." | |
| exit $fail | |
| mirror-cn-reachable: | |
| # Closed-loop guard for the CN mirror: every CN url referenced by a | |
| # descriptor must be a live, downloadable gitcode release asset. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install lua | |
| run: sudo apt-get install -y --no-install-recommends lua5.4 | |
| - name: Check CN mirror assets are reachable | |
| run: | | |
| fail=0 | |
| # collect unique CN urls across all descriptors | |
| : > /tmp/cn.tsv | |
| for f in pkgs/*/*.lua; do | |
| lua5.4 tests/list_cn_urls.lua "$f" >> /tmp/cn.tsv || true | |
| done | |
| sort -u /tmp/cn.tsv -o /tmp/cn.tsv | |
| total=$(grep -c . /tmp/cn.tsv || true) | |
| echo "checking $total CN mirror url(s)" | |
| while IFS=$'\t' read -r url sha; do | |
| [ -z "$url" ] && continue | |
| # follow redirects; gitcode release assets resolve to object storage | |
| code=$(curl -fsSL -o /dev/null -w '%{http_code}' --retry 2 --max-time 60 "$url" || echo "000") | |
| if [ "$code" != "200" ]; then | |
| echo "::error::CN mirror unreachable ($code): $url" | |
| fail=1 | |
| else | |
| echo "ok: $url" | |
| fi | |
| done < /tmp/cn.tsv | |
| [ $fail -eq 0 ] && echo "All CN mirror urls reachable." | |
| exit $fail | |
| # ── The whole test surface, as a mcpp workspace ─────────────────────── | |
| # mcpp-index is a mcpp [workspace]; every per-library test project under | |
| # tests/examples/ is a member. `mcpp test --workspace` builds + runs each | |
| # member's tests/ (behavioral assertions) on each OS — members self-gate by | |
| # `[target.'cfg(...)']` (e.g. the X11/glfw stack is linux-only, openblas is | |
| # windows-only), so one command covers the matrix with no shell driver. | |
| # The ~/.mcpp/registry cache carries the built compat packages (xpkgs) across | |
| # runs, so repeat builds are fast. | |
| # | |
| # timeout-minutes is sized for the COLD build, not the cached path. The opencv | |
| # module package carries a from-source OpenCV 5 build, and each feature variant | |
| # re-keys the store into a full recompile, so a full run (forced whenever this | |
| # workflow file changes — e.g. a version bump) serially builds three OpenCV | |
| # variants on one runner: the opencv-module base member plus the `unifont` and | |
| # `dnn` feature members. The registry cache (restore-keys prefix below) | |
| # amortizes those across subsequent runs. 150 covers the one-time cold full | |
| # build with headroom; it is a ceiling, not a target. | |
| workspace: | |
| name: workspace (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 150 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Archive names are derived from env.MCPP_VERSION in the Download | |
| # step — bumping the pin is a ONE-line change (hardcoded versions | |
| # here once 404'd a pin bump). | |
| - platform: linux | |
| os: ubuntu-latest | |
| suffix: linux-x86_64 | |
| ext: tar.gz | |
| mcpp: bin/mcpp | |
| xlings: registry/bin/xlings | |
| mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION | |
| - platform: macos | |
| os: macos-15 | |
| suffix: macosx-arm64 | |
| ext: tar.gz | |
| mcpp: bin/mcpp | |
| xlings: registry/bin/xlings | |
| mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION | |
| - platform: windows | |
| os: windows-latest | |
| suffix: windows-x86_64 | |
| ext: zip | |
| mcpp: bin/mcpp.exe | |
| xlings: registry/bin/xlings.exe | |
| mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION | |
| env: | |
| MCPP_EFFECTIVE: ${{ matrix.mcpp_version }} | |
| steps: | |
| # Full history: the member-selection step below diffs against the PR | |
| # base to decide which workspace members to test. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Restore mcpp registry cache | |
| uses: actions/cache@v4 | |
| with: | |
| # Holds toolchains AND the built compat packages (data/xpkgs), so a | |
| # repeat `mcpp test` rebuilds little. | |
| path: ~/.mcpp/registry | |
| key: mcpp-registry-${{ runner.os }}-${{ env.MCPP_EFFECTIVE }}-${{ hashFiles('pkgs/**/*.lua', 'tests/**', '.github/workflows/validate.yml') }} | |
| restore-keys: | | |
| mcpp-registry-${{ runner.os }}-${{ env.MCPP_EFFECTIVE }}- | |
| - name: Download mcpp | |
| shell: bash | |
| env: | |
| MCPP_ARCHIVE: mcpp-${{ env.MCPP_EFFECTIVE }}-${{ matrix.suffix }}.${{ matrix.ext }} | |
| MCPP_ROOT: mcpp-${{ env.MCPP_EFFECTIVE }}-${{ matrix.suffix }} | |
| run: | | |
| curl -L -fsS -o "$MCPP_ARCHIVE" \ | |
| "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_EFFECTIVE}/${MCPP_ARCHIVE}" | |
| case "$MCPP_ARCHIVE" in | |
| *.zip) powershell -NoProfile -Command "Expand-Archive -Force -Path '${MCPP_ARCHIVE}' -DestinationPath '.'" ;; | |
| *) tar -xzf "$MCPP_ARCHIVE" ;; | |
| esac | |
| root="$PWD/$MCPP_ROOT" | |
| mkdir -p "$HOME/.mcpp/registry" | |
| cp -a "$root/registry/." "$HOME/.mcpp/registry/" | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| echo "MCPP=$(cygpath -m "$root/${{ matrix.mcpp }}")" >> "$GITHUB_ENV" | |
| echo "MCPP_VENDORED_XLINGS=$(cygpath -m "$root/${{ matrix.xlings }}")" >> "$GITHUB_ENV" | |
| echo "$(cygpath -m "$root/bin")" >> "$GITHUB_PATH" | |
| else | |
| echo "MCPP=$root/${{ matrix.mcpp }}" >> "$GITHUB_ENV" | |
| echo "MCPP_VENDORED_XLINGS=$root/${{ matrix.xlings }}" >> "$GITHUB_ENV" | |
| echo "$root/bin" >> "$GITHUB_PATH" | |
| fi | |
| # compat.ffmpeg / compat.opencv5 carry NASM .asm sources. No host | |
| # install and no index-refresh pre-step needed: mcpp >= 0.0.97 | |
| # resolves nasm itself through the same synchronous gate as the | |
| # toolchain (index refresh + install + payload check BEFORE the build | |
| # plans, mcpp#232). The sandbox copy lands in ~/.mcpp/registry, so | |
| # the cache carries it across runs. | |
| # ── Selective member testing ────────────────────────────────────── | |
| # `mcpp test --workspace` builds every member (opencv, ffmpeg, …) and | |
| # dominates CI wall-clock, while a PR almost always touches one | |
| # package. Map changed files → affected members and test only those: | |
| # pkgs/<x>/<lib>.lua → members whose mcpp.toml references <lib> | |
| # tests/examples/<m>/** → member <m> | |
| # Run the FULL workspace when the change can affect everything: | |
| # non-PR events (push to main, the nightly cron, dispatch), this | |
| # workflow file (it carries the mcpp version pins, so a version bump | |
| # always re-validates every package), a non-member edit to the | |
| # workspace manifest, or shared test scripts. Docs-only and tools/-only | |
| # changes select nothing. | |
| # Note: bash 3.2 on macOS runners — no associative arrays here. | |
| - name: Select affected workspace members | |
| shell: bash | |
| run: | | |
| full() { echo "MEMBERS=__ALL__" >> "$GITHUB_ENV"; echo "full run: $1"; exit 0; } | |
| [ "${{ github.event_name }}" = "pull_request" ] || full "event=${{ github.event_name }}" | |
| base="origin/${{ github.base_ref }}" | |
| changed=$(git diff --name-only "$base"...HEAD) | |
| printf 'changed files vs %s:\n%s\n' "$base" "$changed" | |
| sel="" | |
| add() { case " $sel " in *" $1 "*) ;; *) sel="$sel $1" ;; esac; } | |
| while IFS= read -r f; do | |
| [ -n "$f" ] || continue | |
| case "$f" in | |
| .github/workflows/validate.yml|tests/*.sh) full "$f" ;; | |
| mcpp.toml) | |
| # Workspace manifest. Every new-package PR appends to the | |
| # members list, so that alone must NOT force a full run: | |
| # select the added members; anything else in this file | |
| # (indices, settings) affects everyone → full. | |
| if ! diff -q <(git show "$base:mcpp.toml" | grep -v 'tests/examples/') \ | |
| <(grep -v 'tests/examples/' mcpp.toml) >/dev/null; then | |
| full "mcpp.toml non-member change" | |
| fi | |
| for p in $(comm -13 <(git show "$base:mcpp.toml" | grep -o 'tests/examples/[A-Za-z0-9._-]*' | sort -u) \ | |
| <(grep -o 'tests/examples/[A-Za-z0-9._-]*' mcpp.toml | sort -u)); do | |
| add "${p#tests/examples/}" | |
| done ;; | |
| tests/examples/*) | |
| m=${f#tests/examples/}; m=${m%%/*} | |
| # A deleted/renamed member dir implies a mcpp.toml edit, | |
| # which already forces a full run above. | |
| [ -d "tests/examples/$m" ] && add "$m" ;; | |
| pkgs/*.lua|pkgs/*/*.lua) | |
| lib=$(basename "$f" .lua); lib=${lib#compat.} | |
| hit=0 | |
| for mt in tests/examples/*/mcpp.toml; do | |
| if grep -q "$lib" "$mt"; then add "$(basename "$(dirname "$mt")")"; hit=1; fi | |
| done | |
| [ "$hit" = 1 ] || echo "note: no workspace member exercises $f" ;; | |
| # tools/ holds OFFLINE descriptor-generation and publishing | |
| # helpers (tools/compat-*/, tools/gtc/, publish_mcpp_index.sh). | |
| # Nothing under it is consumed by a package build: when one of | |
| # them actually changes a package, the generated pkgs/*.lua | |
| # changes with it and the rule above selects the right members. | |
| # So a tools/ edit alone selects nothing rather than forcing a | |
| # full workspace rebuild. | |
| *.md|docs/*|.agents/*|.github/*|tools/*) : ;; | |
| *) full "unclassified change: $f" ;; | |
| esac | |
| done <<EOF | |
| $changed | |
| EOF | |
| sel=${sel# } | |
| echo "MEMBERS=$sel" >> "$GITHUB_ENV" | |
| echo "selected members: ${sel:-<none>}" | |
| - name: mcpp test (workspace or affected members) | |
| shell: bash | |
| env: | |
| MCPP_INDEX_MIRROR: GLOBAL | |
| run: | | |
| "$MCPP" --version | |
| # No `timeout` wrapper: absent on macOS runners; job-level timeout-minutes bounds it. | |
| if [ "$MEMBERS" = "__ALL__" ]; then | |
| "$MCPP" test --workspace | |
| elif [ -z "$MEMBERS" ]; then | |
| echo "No workspace member affected by this change — nothing to test." | |
| else | |
| rc=0 | |
| for m in $MEMBERS; do | |
| echo "::group::mcpp test -p $m" | |
| "$MCPP" test -p "$m" || rc=1 | |
| echo "::endgroup::" | |
| done | |
| exit $rc | |
| fi | |