[CI] (.github/workflows, docs/ci, scripts/ci): decide split build reuse in one guard job

- a new build-guard job configures both Linux lines on one runner (configure only, disaggregation branches) and outputs identical, different or unknown with its runner image and compiler; it is continue-on-error at job level, so it never fails the run, and it takes MOBILEGL_CI_DISAGGREGATED from build-linux's branch expression, so a drifted branch list gives different
- build_fingerprint.py guard: one shared FetchContent directory (normalised as @DEPS@) so only the first configure fetches, a failed configure, or one killed with its process group after 10 minutes, retried once, and unknown for a configure that failed twice, any error or too little evidence; different is a warning annotation and unknown a notice, each with its first reasons; the toolchain is recorded, not compared; self-test cases for both sides thin, no libMobileGL.so link, a hung configure, the retry after a failure and after a timeout, the annotations, the shared directory and every verdict
- build-linux-split builds nothing only on identical, and the consumers take build-linux's artifacts only then; different and unknown keep both builds, the full split matrix, A/B and the split unit run, and no consumer can red on a disagreement
- build-linux drops its probe and runs the link ratchet on every disaggregation build as link-ratchet-primary; integration-split reds on it when it takes that build and prints both jobs' image and compiler
- build-linux exports its build type, and the split consumers refuse to reuse a build-linux that is not Release (split_reuse_build_type.sh, self-test in pipe-gates): a debug-logging re-run of failed jobs keeps the guard's Release verdict
- configure_build.sh passes extra arguments through to cmake
This commit is contained in:
2026-10-17 17:13:49 -04:00
parent 91b889888e
commit 8aa92c357b
5 changed files with 614 additions and 278 deletions
+258 -146
View File
@@ -40,8 +40,10 @@ jobs:
actions: write
contents: read
outputs:
split-identical: ${{ steps.split-guard.outputs.identical }}
link-ratchet: ${{ steps.ratchet.outcome }}
image: ${{ steps.toolchain.outputs.image }}
compiler: ${{ steps.toolchain.outputs.compiler }}
build-type: ${{ steps.configure.outputs.build-type }}
env:
BUILD_DIR: build-linux
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
@@ -110,38 +112,28 @@ jobs:
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
# The image and compiler are job outputs: integration-split prints them beside build-guard's.
- name: Show installed toolchain
id: toolchain
run: |
ccache --version
"${MOBILEGL_CI_CC}" --version
"${MOBILEGL_CI_CXX}" --version
"${MOBILEGL_CI_LLD}" --version
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
echo "image=${ImageOS:-} ${ImageVersion:-}" >> "${GITHUB_OUTPUT}"
echo "compiler=$("${MOBILEGL_CI_CXX}" --version | head -n 1)" >> "${GITHUB_OUTPUT}"
# The configure line lives in scripts/ci/configure_build.sh, shared with build-linux-split.
# The configure line lives in scripts/ci/configure_build.sh, shared with build-linux-split
# and build-guard. The build type is a job output: the split consumers reuse this build only
# when it is Release, the only type build-guard can find identical to the split line.
- name: Configure CMake
id: configure
env:
CI_BUILD_TYPE: ${{ secrets.ACTIONS_STEP_DEBUG == 'true' && 'Debug' || 'Release' }}
run: bash scripts/ci/configure_build.sh primary "${BUILD_DIR}"
# On the disaggregation branches build-linux-split configures the same sources with only the
# benchmarks left out. This configures its line here as a probe, on this runner, and compares
# the two trees (scripts/ci/build_fingerprint.py). Equal means the split consumers take this
# job's artifacts; a difference, or a guard that cannot decide, means false, and
# build-linux-split builds its own as before. It never reds this job.
- name: Split configuration guard
id: split-guard
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
run: |
if python3 scripts/ci/build_fingerprint.py --self-test > "${RUNNER_TEMP}/fingerprint-self-test.txt"; then
python3 scripts/ci/build_fingerprint.py guard --built "${BUILD_DIR}" --probe split \
--probe-dir build-guard-split --github-output identical || echo "identical=false" >> "${GITHUB_OUTPUT}"
else
cat "${RUNNER_TEMP}/fingerprint-self-test.txt"
echo "::warning::build_fingerprint.py's self-test failed, so the split build is not reused"
echo "identical=false" >> "${GITHUB_OUTPUT}"
fi
rm -rf build-guard-split
echo "build-type=${CI_BUILD_TYPE}" >> "${GITHUB_OUTPUT}"
bash scripts/ci/configure_build.sh primary "${BUILD_DIR}"
- name: Build
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
@@ -154,12 +146,14 @@ jobs:
python3 scripts/ci/runtime_mode_proof.py build --build-dir "${BUILD_DIR}" \
--mode "${mode}" --output "${BUILD_DIR}/runtime-mode.json"
# build-linux-split's server link-closure ratchet, run on this build when that job reuses it.
# continue-on-error keeps a ratchet red from skipping this job's consumers; integration-split
# turns it into a red of its own instead (Split runtime agreement).
# build-linux-split's server link-closure ratchet, on this build, which the split consumers
# take when build-guard proves the two configurations identical. This job does not wait for
# that verdict, so it runs on every disaggregation build. continue-on-error keeps a ratchet
# red from skipping this job's consumers; integration-split reds on it when it takes this
# build. The artifact has its own name, so it never replaces build-linux-split's.
- name: Server link-closure ratchet
id: ratchet
if: steps.split-guard.outputs.identical == 'true'
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
continue-on-error: true
run: |
mkdir -p ci-artifacts
@@ -169,16 +163,15 @@ jobs:
--baseline scripts/data/link_ratchet_baseline.txt \
--assert-monotone \
--bucket \
--json ci-artifacts/link-ratchet.json
--json ci-artifacts/link-ratchet-primary.json
- name: Upload link ratchet
if: ${{ always() && steps.split-guard.outputs.identical == 'true' }}
if: ${{ always() && env.MOBILEGL_CI_DISAGGREGATED == 'ON' }}
uses: actions/upload-artifact@v7
with:
name: link-ratchet
path: ci-artifacts/link-ratchet.json
name: link-ratchet-primary
path: ci-artifacts/link-ratchet-primary.json
if-no-files-found: warn
overwrite: true
- name: Show ccache stats
if: ${{ always() }}
@@ -1192,55 +1185,54 @@ jobs:
path: /tmp/core.*
if-no-files-found: ignore
# The split build: the same sources with the transport and its in-process arm always on, which the
# split integration and retrace lanes consume. On the disaggregation branches build-linux has the
# same configuration apart from the benchmarks; the guard below proves that on this runner, and
# when it holds this job skips its compile and the consumers take build-linux's artifacts
# (build-linux runs the same guard, and the consumers require both). On dev it builds as before.
#
# The `nm` step is the point of a build of its own. In a build without the option, MG_Remote is
# not compiled and MOBILEGL_TRANSPORT's parser does not exist, so the variable is accepted and
# ignored and every downstream lane would run monolith under a name that says split. CMake does
# not complain about a typo'd -D, so the build-level assertion is the guard; in reuse mode
# build-linux's runtime_mode_proof (--mode disaggregated) is the same assertion and more.
build-linux-split:
# Whether build-linux-split may skip its compile. On the disaggregation branches its configure
# line and build-linux's differ only in the benchmarks; this job configures both on one runner
# (configure only) and compares the trees (scripts/ci/build_fingerprint.py). Its verdict is
# identical, different or unknown, and its failure never fails the run: only 'identical' makes
# build-linux-split skip its compile and the split consumers take build-linux's artifacts,
# while 'different' and 'unknown' keep both builds. The builds run on the same runner image as
# this job, which records the image and compiler it saw; integration-split prints both sides.
# MOBILEGL_CI_DISAGGREGATED is build-linux's own expression, so a branch list that drifts
# between the two jobs configures an OFF primary here and gives 'different'.
build-guard:
name: build guard (split reuse)
runs-on: ubuntu-26.04
timeout-minutes: 120
permissions:
actions: write
contents: read
timeout-minutes: 150
if: ${{ github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup' }}
# Consumers read only its outputs, and any verdict but 'identical' (none at all included)
# keeps both builds.
continue-on-error: true
outputs:
reuse-primary: ${{ steps.guard.outputs.identical == 'true' && 'true' || 'false' }}
verdict: ${{ steps.guard.outputs.verdict || 'unknown' }}
image: ${{ steps.guard.outputs.image }}
compiler: ${{ steps.guard.outputs.compiler }}
env:
BUILD_DIR: build-split
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
# Every step may fail without failing the job: a setup step that failed leaves a configure
# that fails, which the guard reports as 'unknown'.
steps:
- name: Checkout repo
continue-on-error: true
timeout-minutes: 15
uses: actions/checkout@v6
with:
# recursive, and load-bearing here beyond the usual: MOBILEGL_BUILD_DISAGGREGATED
# shadows itself back to OFF when 3rdparty/flatbuffers/include is missing (a normal
# variable rather than a cache force, deliberately). A shallow checkout would therefore
# configure cleanly, build a monolith library, and be caught only by the nm step below.
submodules: recursive
- name: Configure runner environment
continue-on-error: true
timeout-minutes: 15
uses: ./.github/actions/setup-test-runner
- name: Get CMake
continue-on-error: true
timeout-minutes: 15
uses: lukka/get-cmake@v4.3.3
with:
cmakeVersion: 3.31.10
- name: Prepare Vulkan SDK
id: netsetup1274
id: vulkan
continue-on-error: true
timeout-minutes: 15
if: env.MOBILEGL_CI_NATIVE_GPU != '1'
@@ -1251,8 +1243,9 @@ jobs:
vulkan-use-cache: true
- name: Prepare Vulkan SDK (retry)
continue-on-error: true
timeout-minutes: 15
if: ${{ (env.MOBILEGL_CI_NATIVE_GPU != '1') && steps.netsetup1274.outcome == 'failure' }}
if: ${{ (env.MOBILEGL_CI_NATIVE_GPU != '1') && steps.vulkan.outcome == 'failure' }}
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
@@ -1260,17 +1253,143 @@ jobs:
vulkan-use-cache: true
- name: Update glslang external sources
continue-on-error: true
timeout-minutes: 15
working-directory: 3rdparty/glslang
run: python3 update_glslang_sources.py
run: python3 update_glslang_sources.py || python3 update_glslang_sources.py
- name: Install build dependencies
continue-on-error: true
timeout-minutes: 15
if: env.MOBILEGL_CI_NATIVE_GPU != '1'
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
# The self-test first: a guard whose canned cases fail decides nothing. The two configures
# share one FetchContent directory, so only the first fetches; each attempt is killed after
# 10 minutes and retried once, which this step's timeout leaves room for.
- name: Configure both lines and compare
id: guard
continue-on-error: true
timeout-minutes: 45
env:
CI_BUILD_TYPE: ${{ secrets.ACTIONS_STEP_DEBUG == 'true' && 'Debug' || 'Release' }}
run: |
if python3 scripts/ci/build_fingerprint.py --self-test > "${RUNNER_TEMP}/fingerprint-self-test.txt"; then
python3 scripts/ci/build_fingerprint.py guard --primary-dir build-guard-primary \
--split-dir build-guard-split --deps-dir build-guard-deps \
--github-output verdict --report "${RUNNER_TEMP}/build-guard.json"
else
cat "${RUNNER_TEMP}/fingerprint-self-test.txt"
echo "::warning::build_fingerprint.py's self-test failed, so the guard decides nothing"
echo "verdict=unknown" >> "${GITHUB_OUTPUT}"
fi
- name: Upload guard report
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: build-guard
path: ${{ runner.temp }}/build-guard.json
if-no-files-found: warn
# The split build: the same sources with the transport and its in-process arm always on, which the
# split integration and retrace lanes consume. When build-guard proves its configuration
# identical to build-linux's (the disaggregation branches, benchmarks apart), this job builds
# nothing and the consumers take build-linux's artifacts; on any other verdict, and where
# build-guard does not run, it builds.
#
# The `nm` step is the point of a build of its own. In a build without the option, MG_Remote is
# not compiled and MOBILEGL_TRANSPORT's parser does not exist, so the variable is accepted and
# ignored and every downstream lane would run monolith under a name that says split. CMake does
# not complain about a typo'd -D, so the build-level assertion is the guard; in reuse mode
# build-linux's runtime_mode_proof (--mode disaggregated) is the same assertion and more.
build-linux-split:
runs-on: ubuntu-26.04
timeout-minutes: 120
needs: [build-guard]
# A build-guard that was skipped, failed or undecided means this job builds.
if: ${{ !cancelled() }}
permissions:
actions: write
contents: read
env:
BUILD_DIR: build-split
SPLIT_REUSE: ${{ needs.build-guard.outputs.verdict == 'identical' && 'true' || 'false' }}
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
steps:
- name: Reuse decision
env:
GUARD_VERDICT: ${{ needs.build-guard.outputs.verdict }}
run: |
if [ "${SPLIT_REUSE}" = true ]; then
echo "build-guard found the two configurations identical: this job builds nothing and the split consumers take build-linux's artifacts"
else
echo "build-guard verdict '${GUARD_VERDICT:-none}': this job builds the split library"
fi
- name: Checkout repo
if: env.SPLIT_REUSE != 'true'
uses: actions/checkout@v6
with:
# recursive, and load-bearing here beyond the usual: MOBILEGL_BUILD_DISAGGREGATED
# shadows itself back to OFF when 3rdparty/flatbuffers/include is missing (a normal
# variable rather than a cache force, deliberately). A shallow checkout would therefore
# configure cleanly, build a monolith library, and be caught only by the nm step below.
submodules: recursive
- name: Configure runner environment
if: env.SPLIT_REUSE != 'true'
uses: ./.github/actions/setup-test-runner
- name: Get CMake
if: env.SPLIT_REUSE != 'true'
timeout-minutes: 15
uses: lukka/get-cmake@v4.3.3
with:
cmakeVersion: 3.31.10
- name: Prepare Vulkan SDK
id: netsetup1274
continue-on-error: true
timeout-minutes: 15
if: ${{ env.SPLIT_REUSE != 'true' && (env.MOBILEGL_CI_NATIVE_GPU != '1') }}
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Prepare Vulkan SDK (retry)
timeout-minutes: 15
if: ${{ env.SPLIT_REUSE != 'true' && ((env.MOBILEGL_CI_NATIVE_GPU != '1') && steps.netsetup1274.outcome == 'failure') }}
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources
if: env.SPLIT_REUSE != 'true'
working-directory: 3rdparty/glslang
run: python3 update_glslang_sources.py
- name: Install build dependencies
timeout-minutes: 15
if: ${{ env.SPLIT_REUSE != 'true' && (env.MOBILEGL_CI_NATIVE_GPU != '1') }}
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
- name: Show installed toolchain
if: env.SPLIT_REUSE != 'true'
run: |
ccache --version
"${MOBILEGL_CI_CC}" --version
@@ -1280,35 +1399,17 @@ jobs:
# Release/INFO like the shipped build; the line and the reasons are in
# scripts/ci/configure_build.sh, shared with build-linux.
- name: Configure CMake
if: env.SPLIT_REUSE != 'true'
run: bash scripts/ci/configure_build.sh split "${BUILD_DIR}"
# build-linux's configuration, probed on this runner and compared with this tree. Equal
# means this job builds nothing and ends green; the consumers then take build-linux's
# artifacts, and red if build-linux's own guard disagrees. It never reds this job.
- name: Primary configuration guard
id: guard
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
env:
CI_BUILD_TYPE: ${{ secrets.ACTIONS_STEP_DEBUG == 'true' && 'Debug' || 'Release' }}
run: |
if python3 scripts/ci/build_fingerprint.py --self-test > "${RUNNER_TEMP}/fingerprint-self-test.txt"; then
python3 scripts/ci/build_fingerprint.py guard --built "${BUILD_DIR}" --probe primary \
--probe-dir build-guard-primary --github-output identical || echo "identical=false" >> "${GITHUB_OUTPUT}"
else
cat "${RUNNER_TEMP}/fingerprint-self-test.txt"
echo "::warning::build_fingerprint.py's self-test failed, so this job builds its own library"
echo "identical=false" >> "${GITHUB_OUTPUT}"
fi
rm -rf build-guard-primary
- name: Set swap space
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Restore ccache
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
uses: actions/cache/restore@v5
with:
path: .ccache
@@ -1317,7 +1418,7 @@ jobs:
${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-
- name: Build
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
# `nm` and NOT `nm -D`, for the reason build-linux-verify spells out: everything under
@@ -1326,7 +1427,7 @@ jobs:
# stripped library would make the grep fail for a third, silent reason.
#
- name: MG_Remote symbols in split library
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: |
test -f "${BUILD_DIR}/libMobileGL.so"
defined=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | wc -l)
@@ -1354,7 +1455,7 @@ jobs:
# script re-checks CMakeCache.txt) and before anything deletes the *.o files it reads: here,
# or in build-linux when this job reuses that build.
- name: Server link-closure ratchet
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: |
mkdir -p ci-artifacts
python3 scripts/link_ratchet.py --self-test
@@ -1368,28 +1469,27 @@ jobs:
# Uploaded even when the gate fired: a red ratchet is exactly the moment someone needs the
# per-symbol definer/referrer attribution the JSON carries.
- name: Upload link ratchet
if: ${{ always() && steps.guard.outputs.identical != 'true' }}
if: ${{ always() && env.SPLIT_REUSE != 'true' }}
uses: actions/upload-artifact@v7
with:
name: link-ratchet
path: ci-artifacts/link-ratchet.json
if-no-files-found: warn
overwrite: true
- name: Show ccache stats
if: ${{ always() && steps.guard.outputs.identical != 'true' }}
if: ${{ always() && env.SPLIT_REUSE != 'true' }}
run: ccache --show-stats
# Only after a build: with nothing built, deleting would drop this branch's entry for good.
- name: Release superseded ccache entry
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" --ref "${GITHUB_REF}" || true
- name: Save ccache
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
@@ -1397,11 +1497,11 @@ jobs:
key: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1
- name: Prepare packaged test metadata
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: python3 scripts/ci/prepare_test_runtime.py "${BUILD_DIR}"
- name: Package Linux split runtime
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: |
mkdir -p ci-artifacts
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
@@ -1419,7 +1519,7 @@ jobs:
"${SHARED_LIBS[@]}"
- name: Upload Linux split runtime
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
uses: actions/upload-artifact@v7
with:
name: mobilegl-linux-runtime-split
@@ -1429,14 +1529,14 @@ jobs:
# The retrace legs load only the library, and the server image on the spawn arm, so they
# take this instead of the whole test runtime.
- name: Package retrace library
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
run: |
files=("${BUILD_DIR}/.ci-source-root" "${BUILD_DIR}/libMobileGL.so")
if [ -f "${BUILD_DIR}/libMobileGLServer.so" ]; then files+=("${BUILD_DIR}/libMobileGLServer.so"); fi
tar -czf ci-artifacts/mobilegl-linux-lib-split.tgz "${files[@]}"
- name: Upload retrace library
if: steps.guard.outputs.identical != 'true'
if: env.SPLIT_REUSE != 'true'
uses: actions/upload-artifact@v7
with:
name: mobilegl-linux-lib-split
@@ -1448,38 +1548,41 @@ jobs:
integration-split:
runs-on: ubuntu-26.04
timeout-minutes: 120
needs: [build-linux-split, build-linux]
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }}
needs: [build-guard, build-linux-split, build-linux]
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-guard.outputs.verdict != 'identical' || needs.build-linux.result == 'success') }}
# Short-lived test processes skip the glslang prewarm, as in the test job.
env:
MOBILEGL_DISABLE_SHADER_PREWARM: "1"
SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }}
SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }}
SPLIT_RUNTIME: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }}
SPLIT_REUSE: ${{ needs.build-guard.outputs.verdict == 'identical' && 'true' || 'false' }}
SPLIT_BUILD_DIR: ${{ needs.build-guard.outputs.verdict == 'identical' && 'build-linux' || 'build-split' }}
SPLIT_RUNTIME: ${{ needs.build-guard.outputs.verdict == 'identical' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }}
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
# The two build guards must agree. build-linux-split skips its compile on its own guard's
# word; the artifacts these lanes then take are build-linux's, so build-linux's guard must
# have proved the same equality on the runner that built them.
- name: Split runtime agreement
# Which build these lanes take: build-linux's when build-guard proved the two configurations
# identical, build-linux-split's otherwise. build-guard compared them on its own runner, so
# the image and compiler it saw are printed beside build-linux's. It reds only when it would
# reuse a build-linux that is not Release (see scripts/ci/split_reuse_build_type.sh).
- name: Split runtime
env:
PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }}
PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }}
GUARD_VERDICT: ${{ needs.build-guard.outputs.verdict }}
GUARD_IMAGE: ${{ needs.build-guard.outputs.image }}
GUARD_COMPILER: ${{ needs.build-guard.outputs.compiler }}
PRIMARY_IMAGE: ${{ needs.build-linux.outputs.image }}
PRIMARY_COMPILER: ${{ needs.build-linux.outputs.compiler }}
PRIMARY_BUILD_TYPE: ${{ needs.build-linux.outputs.build-type }}
run: |
if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then
echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust"
exit 1
echo "split runtime: ${SPLIT_BUILD_DIR} (build-guard verdict '${GUARD_VERDICT:-none}')"
bash scripts/ci/split_reuse_build_type.sh
echo "build-guard ran on '${GUARD_IMAGE}' with '${GUARD_COMPILER}'"
echo "build-linux ran on '${PRIMARY_IMAGE}' with '${PRIMARY_COMPILER}'"
if [ "${SPLIT_REUSE}" = true ] && { [ "${GUARD_IMAGE}" != "${PRIMARY_IMAGE}" ] || [ "${GUARD_COMPILER}" != "${PRIMARY_COMPILER}" ]; }; then
echo "::notice::build-guard and build-linux saw different runner images or compilers; the reused build is the one build-linux configured"
fi
if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_RATCHET}" != "success" ]; then
echo "::error::the server link-closure ratchet ran in build-linux for this reused build and did not pass (outcome '${PRIMARY_RATCHET}'); its link-ratchet artifact names the symbols"
exit 1
fi
echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})"
- name: Configure runner environment
uses: ./.github/actions/setup-test-runner
@@ -1684,6 +1787,19 @@ jobs:
SPLIT_BASELINE_JUNIT: ${{ runner.temp }}/split-baseline.xml
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
# build-linux ran the server link-closure ratchet on the build these lanes took, with
# continue-on-error so that a red there does not skip build-linux's own consumers.
- name: Server link-closure ratchet of the reused build
if: ${{ !cancelled() && env.SPLIT_REUSE == 'true' }}
env:
PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }}
run: |
if [ "${PRIMARY_RATCHET}" != success ]; then
echo "::error::the server link-closure ratchet of the build-linux build these lanes took did not pass (outcome '${PRIMARY_RATCHET}'); build-linux's link-ratchet-primary artifact names the symbols"
exit 1
fi
echo "the server link-closure ratchet passed on the build-linux build these lanes took"
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
@@ -1727,34 +1843,31 @@ jobs:
integration-split-strict:
runs-on: ubuntu-26.04
timeout-minutes: 60
needs: [build-linux-split, build-linux]
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }}
needs: [build-guard, build-linux-split, build-linux]
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-guard.outputs.verdict != 'identical' || needs.build-linux.result == 'success') }}
# Short-lived test processes skip the glslang prewarm, as in the test job.
env:
MOBILEGL_DISABLE_SHADER_PREWARM: "1"
SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }}
SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }}
SPLIT_RUNTIME: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }}
SPLIT_REUSE: ${{ needs.build-guard.outputs.verdict == 'identical' && 'true' || 'false' }}
SPLIT_BUILD_DIR: ${{ needs.build-guard.outputs.verdict == 'identical' && 'build-linux' || 'build-split' }}
SPLIT_RUNTIME: ${{ needs.build-guard.outputs.verdict == 'identical' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }}
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
# The two build guards must agree. build-linux-split skips its compile on its own guard's
# word; the artifacts these lanes then take are build-linux's, so build-linux's guard must
# have proved the same equality on the runner that built them.
- name: Split runtime agreement
# build-linux's build when build-guard proved the two configurations identical,
# build-linux-split's otherwise; integration-split prints the guard's evidence. A build-linux
# that is not Release is never reused (scripts/ci/split_reuse_build_type.sh).
- name: Split runtime
env:
PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }}
PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }}
GUARD_VERDICT: ${{ needs.build-guard.outputs.verdict }}
PRIMARY_BUILD_TYPE: ${{ needs.build-linux.outputs.build-type }}
run: |
if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then
echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust"
exit 1
fi
echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})"
echo "split runtime: ${SPLIT_BUILD_DIR} (build-guard verdict '${GUARD_VERDICT:-none}')"
bash scripts/ci/split_reuse_build_type.sh
- name: Configure runner environment
uses: ./.github/actions/setup-test-runner
@@ -3204,24 +3317,25 @@ jobs:
timeout-minutes: 240
# The setup action selects lavapipe on GitHub-hosted runners.
needs:
- build-guard
- build-linux-split
- build-linux
- build-retrace
- trace-cases
- trace-fixtures
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }}
if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' && (needs.build-guard.outputs.verdict != 'identical' || needs.build-linux.result == 'success') }}
env:
SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }}
SPLIT_LIB: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-lib' || 'mobilegl-linux-lib-split' }}
SPLIT_LIB_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }}
SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }}
SPLIT_REUSE: ${{ needs.build-guard.outputs.verdict == 'identical' && 'true' || 'false' }}
SPLIT_LIB: ${{ needs.build-guard.outputs.verdict == 'identical' && 'mobilegl-linux-lib' || 'mobilegl-linux-lib-split' }}
SPLIT_LIB_DIR: ${{ needs.build-guard.outputs.verdict == 'identical' && 'build-linux' || 'build-split' }}
SPLIT_BUILD_DIR: ${{ needs.build-guard.outputs.verdict == 'identical' && 'build-linux' || 'build-split' }}
strategy:
fail-fast: false
max-parallel: 8
# With build-linux reused, the inproc rows (except OpenRA's) are replayed by the retrace job's
# inproc-acceptance rows on the same library, strict and with the dual block and run-ahead
# on; the spawn rows below prove per leg that those are also the defaults they run with.
matrix: ${{ fromJSON(needs.build-linux-split.outputs.reuse-primary == 'true' && needs.trace-cases.outputs.split-spawn-matrix || needs.trace-cases.outputs.split-matrix) }}
matrix: ${{ fromJSON(needs.build-guard.outputs.verdict == 'identical' && needs.trace-cases.outputs.split-spawn-matrix || needs.trace-cases.outputs.split-matrix) }}
steps:
- name: Set swap space
@@ -3233,19 +3347,16 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v6
# The two build guards must agree. build-linux-split skips its compile on its own guard's
# word; the artifacts these lanes then take are build-linux's, so build-linux's guard must
# have proved the same equality on the runner that built them.
- name: Split runtime agreement
# build-linux's build when build-guard proved the two configurations identical,
# build-linux-split's otherwise; integration-split prints the guard's evidence. A build-linux
# that is not Release is never reused (scripts/ci/split_reuse_build_type.sh).
- name: Split runtime
env:
PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }}
PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }}
GUARD_VERDICT: ${{ needs.build-guard.outputs.verdict }}
PRIMARY_BUILD_TYPE: ${{ needs.build-linux.outputs.build-type }}
run: |
if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then
echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust"
exit 1
fi
echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})"
echo "split runtime: ${SPLIT_BUILD_DIR} (build-guard verdict '${GUARD_VERDICT:-none}')"
bash scripts/ci/split_reuse_build_type.sh
- name: Configure runner environment
uses: ./.github/actions/setup-test-runner
@@ -3766,6 +3877,7 @@ jobs:
python3 tools/trace_replay/test_compare_actuals.py
python3 tools/trace_replay/test_run_trace_case.py
python3 scripts/ci/build_fingerprint.py --self-test
bash scripts/ci/split_reuse_build_type.sh --self-test
python3 MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py
python3 scripts/ci/transport_pinned.py --self-test
+1 -1
View File
@@ -46,7 +46,7 @@ them wrong.
| 2026-10-17 | `Negative control A (server-side read)` and `Negative control B (omitted stamp)` | Test / `integration-verify-split` | the first re-ran the two VerifySplitReadCorrupted entries the lane had just run and grepped what they already assert in-test (at least two where=read reports in the server half); the second re-ran with the pair the in-lane VerifySplitPoisonOmitted entries already pin, and accepted any red | `Verify-split control entries`: `--require-entry-passed` on VerifySplitArming, VerifySplitCorrupted, VerifySplitReadCorrupted and VerifySplitPoisonOmitted of both backends |
| 2026-10-17 | the pre-flip arm of `Strict split lane` (red-by-design while `kMGPipeP5eRunAheadReady` was false) and the "GREEN with rows outstanding" rule of `Dual-block census lane` | Test / `integration-split-strict` | dead: the constant is true, and `dualblock-expected-fatals.txt` has no rows, so neither branch could run | the hard-green arm and its two-sided marker census, and the empty-census rule (rows == 0 requires ctest success), both kept |
| 2026-10-17 | `scripts/ci/tcp_log_forwarding_control.py` | none | no caller outside the archived planning notes | none needed |
| 2026-10-17 | the compile of `build-linux-split` on the disaggregation branches (when its guard proves the configurations equal) | Test / `build-linux-split` | it built the library `build-linux` builds there, from a configuration that differs only by the benchmarks (211 paired runs never disagreed); it delayed every split consumer by up to 9 minutes | `build_fingerprint.py` guards in both build jobs (cache, compile commands, link statements, CTest files, toolchain; benchmarks excluded); the consumers take `build-linux`'s artifacts and red if `build-linux`'s own guard disagrees; the link ratchet runs in `build-linux`; on any difference `build-linux-split` builds as before, and dev is unchanged |
| 2026-10-17 | the compile of `build-linux-split` on the disaggregation branches (when `build-guard` proves the configurations identical) | Test / `build-linux-split` | it built the library `build-linux` builds there, from a configuration that differs only by the benchmarks (211 paired runs never disagreed); it delayed every split consumer by up to 9 minutes | the `build-guard` job configures both lines on one runner and compares them with `build_fingerprint.py` (cache, compile commands, link statements, CTest files; benchmarks excluded; one shared FetchContent directory, a failed configure or one killed after 10 minutes retried once) and records its image and compiler; only the verdict `identical` lets the consumers take `build-linux`'s artifacts, and only a Release one, while `different` (annotated as a warning) and `unknown` (a configure that failed twice, any error, too little evidence; a notice) keep both builds and the full split lanes; the guard job's own failure never fails the run; `build-linux` runs the link ratchet on every disaggregation build (artifact `link-ratchet-primary`) and integration-split reds on it when it takes that build; dev runs no guard and always builds |
| 2026-10-17 | `Transport A/B (monolith vs inproc)` (skip-census lanes `arm-monolith`, `arm-inproc` now optional) | Test / `integration-split` | with `build-linux` reused it repeats the integration job's two legs on the same library (79 paired runs, no one-sided red) | the integration legs; `Default-knob resolution check` (disaggregation branches) reads 'Split lanes' own run and proves, from every passed entry's client log, strict=0, role-split-state=1 and run-ahead=1 unless pinned (`split_log_paths.py ipc-config`); A/B still runs when a default differs or the build is not reused |
| 2026-10-17 | `Unit tests on the split runtime` (skip-census lane `unit-split` now optional) | Test / `integration-split` | with `build-linux` reused the test job runs the same unit lane on the same build (182 paired runs; the one recurring one-sided red was a fixed test race) | the test job, which now carries `MOBILEGL_ITEST_REQUIRE_GPU=1` and `libegl-mesa0` on the three branches; the strict unit step is unchanged |
| 2026-10-17 | the integration-gpu entries that pin their own `MOBILEGL_TRANSPORT` (804 inproc, 1446 spawn, 131 monolith) and the 12 `TcpServer` fixture entries only they need, in the inproc-acceptance leg (both passes) | Test / `integration` (inproc-acceptance) | their pin overrides the only variable the two legs differ in, so they ran identically in both (173 paired runs; the one one-sided red was a port bind fixed by cde6512b) | the monolith-control leg, which runs them on every branch; `transport_pinned.py` proves per run that the trimmed selection lost exactly that population (else the leg runs everything) and that the monolith-control leg ran every one; skip census: the monolith-control lanes list their own names and the inproc-acceptance lanes drop the pinned ones |
+304 -128
View File
@@ -7,51 +7,63 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Prove that two CMake build trees were configured identically, apart from the benchmarks.
"""Decide whether the two Linux library configurations are identical apart from the benchmarks.
build-linux and build-linux-split configure the same sources twice. On the disaggregation
branches the only intended difference is MOBILEGL_BUILD_BENCHMARK, which adds MG_Benchmark and the
google benchmark dependency and touches nothing else. When the two configurations are equal, the
split consumers can take build-linux's artifacts and build-linux-split need not compile again.
google benchmark dependency and touches nothing else. When the two configurations are identical,
the split consumers can take build-linux's artifacts and build-linux-split need not compile again.
The fingerprint is read from what configure writes, so it can be taken before the build:
cache MOBILEGL_* entries (except MOBILEGL_BUILD_BENCHMARK), the build type, compilers,
launchers, flags and the link tools from CMakeCache.txt;
compile every compile_commands.json entry: file, output and arguments;
link every link statement of build.ninja: outputs, rule, inputs and variables;
tests the text of every CTestTestfile.cmake and gtest *_include.cmake;
toolchain compiler, linker, cmake, ninja and package versions on this runner;
image the runner image, recorded and never compared.
The build directory's own path is replaced by @BUILD@, and everything under MobileGL/MG_Benchmark
or the benchmark dependency is left out. A fingerprint with fewer than 200 compile entries, 3 link
statements (libMobileGL.so among them) or 2 CTest files proves nothing and never counts as equal.
tests the text of every CTestTestfile.cmake and gtest *_include.cmake.
The build directory's path becomes @BUILD@ and a shared FetchContent directory @DEPS@; everything
under MobileGL/MG_Benchmark or the benchmark dependency is left out. Both fingerprints come from
one runner, so its toolchain and image are recorded beside the verdict and never compared.
build_fingerprint.py fingerprint --build-dir D [--out F]
build_fingerprint.py fingerprint --build-dir D [--deps-dir F] [--out F]
build_fingerprint.py compare A.json B.json
build_fingerprint.py guard --built D --probe primary|split --probe-dir P [--github-output KEY]
build_fingerprint.py guard --primary-dir P --split-dir S --deps-dir F [--github-output KEY] [--report R]
build_fingerprint.py --self-test
guard configures the probe with scripts/ci/configure_build.sh (configure only), compares it with
the built tree and writes KEY=true|false to $GITHUB_OUTPUT. Any error makes it false, and it always
exits 0: a difference sends the workflow down the path that builds both, it is not a red.
guard configures both lines with scripts/ci/configure_build.sh (configure only) into P and S, with
F as their one FetchContent directory, so only the first configure fetches anything; a configure
that fails, or runs past 10 minutes and is killed, is retried once. It writes
KEY=identical|different|unknown, image= and compiler= to $GITHUB_OUTPUT and always exits 0.
'different' is a proven difference. A configure that failed twice, any error, or a tree with fewer
than 200 compile entries, 3 link statements (libMobileGL.so among them) or 2 CTest files is
'unknown': only 'identical' lets the workflow reuse a build. Any other verdict is annotated with
its first reasons, a warning for 'different' and a notice for 'unknown'.
"""
import argparse
import contextlib
import io
import json
import os
from pathlib import Path
import re
import shlex
import shutil
import signal
import subprocess
import sys
import tempfile
import time
ROOT = Path(__file__).resolve().parents[2]
CONFIGURE = ROOT / 'scripts' / 'ci' / 'configure_build.sh'
# Each configure attempt is killed after CONFIGURE_TIMEOUT seconds. GUARD_STEP_MINUTES must equal the
# guard step's timeout-minutes in test.yml, and leave room for four killed attempts.
CONFIGURE_TIMEOUT = 600
GUARD_STEP_MINUTES = 45
MIN_COMPILE = 200
MIN_LINK = 3
MIN_TESTS = 2
VERDICTS = ('identical', 'different', 'unknown')
CACHE_KEY = re.compile(
r'^(MOBILEGL_\w+'
@@ -64,18 +76,22 @@ CACHE_KEY = re.compile(
CACHE_SKIP = {'MOBILEGL_BUILD_BENCHMARK'}
CACHE_LINE = re.compile(r'^([A-Za-z0-9_.+-]+):([A-Z]+)=(.*)$')
LINK_RULE = re.compile(r'^(C|CXX)_(SHARED_LIBRARY|EXECUTABLE|MODULE|STATIC_LIBRARY)_LINKER__')
BENCHMARK_PATH = re.compile(r'(^|/)(MobileGL/MG_Benchmark|_deps/benchmark-[a-z]+)(/|$)')
BENCHMARK_SUBDIRS = re.compile(r'^\s*subdirs\("[^"]*(MobileGL/MG_Benchmark|_deps/benchmark-build)"\)\s*$')
BENCHMARK_PATH = re.compile(r'(^|/)(MobileGL/MG_Benchmark|(_deps|@DEPS@)/benchmark-[a-z]+)(/|$)')
BENCHMARK_SUBDIRS = re.compile(
r'^\s*subdirs\("[^"]*(MobileGL/MG_Benchmark|(_deps|@DEPS@)/benchmark-build)"\)\s*$')
PACKAGES = ('clang-20', 'lld-20', 'libc++-20-dev', 'libc++abi-20-dev', 'libvulkan-dev',
'libegl1-mesa-dev', 'libgles2-mesa-dev')
def normaliser(build_dir):
roots = sorted({str(Path(build_dir)), str(Path(build_dir).resolve())}, key=len, reverse=True)
def normaliser(build_dir, deps_dir=None):
roots = [(str(Path(build_dir)), '@BUILD@'), (str(Path(build_dir).resolve()), '@BUILD@')]
if deps_dir:
roots += [(str(Path(deps_dir)), '@DEPS@'), (str(Path(deps_dir).resolve()), '@DEPS@')]
roots = sorted(set(roots), key=lambda pair: len(pair[0]), reverse=True)
def norm(text):
for root in roots:
text = text.replace(root, '@BUILD@')
for root, token in roots:
text = text.replace(root, token)
return text
return norm
@@ -197,9 +213,8 @@ def read_tests(build, norm):
continue
if is_benchmark(rel) or '/CMakeFiles/' in '/' + rel:
continue
text = path.read_text(errors='replace')
kept = [norm(line) for line in text.splitlines()
if not BENCHMARK_SUBDIRS.match(line) and not line.startswith('# ')]
lines = [norm(line) for line in path.read_text(errors='replace').splitlines()]
kept = [line for line in lines if not BENCHMARK_SUBDIRS.match(line) and not line.startswith('# ')]
tests[rel] = '\n'.join(kept)
return tests
@@ -223,22 +238,21 @@ def read_toolchain():
return tools
def fingerprint(build_dir, toolchain=None):
def read_image():
return {key: os.environ.get(key, '') for key in ('ImageOS', 'ImageVersion', 'RUNNER_NAME')}
def fingerprint(build_dir, deps_dir=None):
build = Path(build_dir)
norm = normaliser(build)
print_counts = {}
norm = normaliser(build, deps_dir)
result = {
'cache': read_cache(build, norm),
'compile': read_compile(build, norm),
'link': read_links(build, norm),
'tests': read_tests(build, norm),
'toolchain': read_toolchain() if toolchain is None else toolchain,
'image': {key: os.environ.get(key, '') for key in ('ImageOS', 'ImageVersion', 'RUNNER_NAME')},
}
print_counts['compile'] = len(result['compile'])
print_counts['link'] = len(result['link'])
print_counts['tests'] = len(result['tests'])
result['counts'] = print_counts
result['counts'] = {'compile': len(result['compile']), 'link': len(result['link']),
'tests': len(result['tests'])}
return result
@@ -293,62 +307,119 @@ def compare(a, b, limit=8):
if a['tests'].get(name) != b['tests'].get(name)]
if tests:
diffs['tests'] = tests
toolchain = [k for k in sorted(set(a['toolchain']) | set(b['toolchain']))
if a['toolchain'].get(k) != b['toolchain'].get(k)]
if toolchain:
diffs['toolchain'] = [f'{k} differs' for k in toolchain]
return {field: rows[:limit] + ([f'... {len(rows) - limit} more'] if len(rows) > limit else [])
for field, rows in diffs.items()}
def verdict(a, b):
"""(identical, reasons). Unproven evidence on either side is never 'identical'."""
reasons = [f'first: {p}' for p in unproven(a)] + [f'second: {p}' for p in unproven(b)]
diffs = compare(a, b)
for field, rows in diffs.items():
reasons += [f'{field}: {row}' for row in rows]
return not reasons, reasons
"""(verdict, reasons). Too little evidence on either side is 'unknown', never 'identical'."""
thin = [f'first: {p}' for p in unproven(a)] + [f'second: {p}' for p in unproven(b)]
if thin:
return 'unknown', thin
reasons = [f'{field}: {row}' for field, rows in compare(a, b).items() for row in rows]
return ('different' if reasons else 'identical'), reasons
def write_output(key, identical):
def run_attempt(argv, label, timeout=CONFIGURE_TIMEOUT):
"""One configure attempt; True on success. One that runs past the timeout (a stalled fetch) is
killed with its whole process group, cmake and its git clones included, and counts as failed."""
proc = subprocess.Popen(argv, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
start_new_session=True)
try:
output, _ = proc.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
try:
output, _ = proc.communicate(timeout=60)
except subprocess.TimeoutExpired:
output = ''
print(output[-6000:])
print(f'build_fingerprint: {label} ran past {timeout} s and was killed')
return False
if proc.returncode != 0:
print(output[-6000:])
print(f'build_fingerprint: {label} failed with exit {proc.returncode}')
return proc.returncode == 0
def run_configure(variant, build_dir, deps_dir, timeout=None):
"""configure_build.sh, configure only, with the shared FetchContent directory; True on success."""
return run_attempt(['bash', str(CONFIGURE), variant, str(build_dir), f'-DFETCHCONTENT_BASE_DIR={deps_dir}'],
f'configure_build.sh {variant}', CONFIGURE_TIMEOUT if timeout is None else timeout)
def configure_twice(configure, variant, build_dir, deps_dir):
"""One retry: a transient fetch failure must not decide anything. Returns the attempts used."""
for attempt in (1, 2):
if build_dir.exists():
shutil.rmtree(build_dir)
if configure(variant, build_dir, deps_dir):
return attempt
return 0
def decide(primary_dir, split_dir, deps_dir, configure=run_configure):
"""(verdict, reasons, counts). The primary line runs first: it alone fetches the benchmark."""
primary_dir, split_dir, deps_dir = Path(primary_dir), Path(split_dir), Path(deps_dir)
counts = {}
for variant, build_dir in (('primary', primary_dir), ('split', split_dir)):
attempts = configure_twice(configure, variant, build_dir, deps_dir)
if not attempts:
return 'unknown', [f'the {variant} configure failed twice'], counts
counts[f'{variant}_attempts'] = attempts
primary = fingerprint(primary_dir, deps_dir)
split = fingerprint(split_dir, deps_dir)
counts.update(primary=primary['counts'], split=split['counts'])
result, reasons = verdict(primary, split)
return result, reasons, counts
def one_line(text, limit=200):
return ' '.join(str(text).split())[:limit]
def annotation(result, reasons):
"""The workflow annotation for a verdict other than 'identical': a 'different' that persists is
configuration drift that brings the second build back on every run, so it is a warning."""
if result == 'identical':
return None
level = 'warning' if result == 'different' else 'notice'
text = one_line('; '.join(reasons[:3]) or 'no reason recorded', 600).replace('%', '%25')
return f'::{level}::build-guard verdict {result}, so build-linux-split builds: {text}'
def write_outputs(key, result, image, compiler):
lines = [f'{key}={result}', f'image={one_line(image)}', f'compiler={one_line(compiler)}']
for line in lines:
print(line)
path = os.environ.get('GITHUB_OUTPUT')
line = f"{key}={'true' if identical else 'false'}"
print(line)
if path:
with open(path, 'a') as out:
out.write(line + '\n')
out.write(''.join(line + '\n' for line in lines))
def guard(args):
identical = False
def guard(args, configure=run_configure, toolchain_reader=read_toolchain):
result, reasons, counts = 'unknown', [], {}
image = read_image()
toolchain = {}
try:
probe = Path(args.probe_dir)
if probe.exists():
shutil.rmtree(probe)
configured = subprocess.run(['bash', str(CONFIGURE), args.probe, str(probe)], cwd=ROOT,
capture_output=True, text=True)
if configured.returncode != 0:
print(configured.stdout[-4000:] + configured.stderr[-4000:])
raise RuntimeError(f'configure_build.sh {args.probe} failed with exit {configured.returncode}')
toolchain = read_toolchain()
built = fingerprint(args.built, toolchain)
probed = fingerprint(probe, toolchain)
print(f"build_fingerprint: built {args.built}: {built['counts']}; probe {args.probe} at {probe}: "
f"{probed['counts']}")
print(f"build_fingerprint: compiler {toolchain.get('MOBILEGL_CI_CXX', '').splitlines()[:1]}, "
f"image {built['image']}")
identical, reasons = verdict(built, probed)
for reason in reasons:
print(f'build_fingerprint: {reason}')
if args.report:
Path(args.report).write_text(json.dumps({'identical': identical, 'reasons': reasons,
'built': built['counts'], 'probe': probed['counts'],
'toolchain': toolchain, 'image': built['image']},
indent=1) + '\n')
except Exception as error: # a guard that cannot decide must not reuse anything
print(f'build_fingerprint: guard could not compare the configurations: {error}')
identical = False
write_output(args.github_output, identical)
toolchain = toolchain_reader()
result, reasons, counts = decide(args.primary_dir, args.split_dir, args.deps_dir, configure)
except Exception as error: # a guard that cannot decide says so, and nothing is reused
result, reasons = 'unknown', [f'the guard could not compare the configurations: {error!r}']
compiler = (toolchain.get('MOBILEGL_CI_CXX') or 'unset').splitlines()[0] if toolchain else 'unread'
image_text = f"{image['ImageOS']} {image['ImageVersion']}".strip() or 'unknown image'
print(f'build_fingerprint: {counts}')
print(f'build_fingerprint: this runner: {image_text}, {compiler}')
for reason in reasons:
print(f'build_fingerprint: {reason}')
note = annotation(result, reasons)
if note:
print(note)
if args.report:
Path(args.report).write_text(json.dumps({'verdict': result, 'reasons': reasons, 'counts': counts,
'toolchain': toolchain, 'image': image}, indent=1) + '\n')
write_outputs(args.github_output, result, image_text, compiler)
return 0
@@ -356,12 +427,13 @@ def guard(args):
def canned_tree(root, *, name='build', mobilegl_flag='ON', build_type='Release', define='-DMG=1',
link_flag='-Wl,--as-needed', test_env='VK_ICD_FILENAMES=/icd.json', benchmark=False,
empty=False):
empty=False, compile_count=MIN_COMPILE + 5, deps=None, library='libMobileGL.so'):
build = root / name
build.mkdir(parents=True)
if empty:
return build
abs_build = str(build)
deps = str(deps) if deps else abs_build + '/_deps'
(build / 'CMakeCache.txt').write_text(
f'CMAKE_BUILD_TYPE:STRING={build_type}\n'
'CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++-20\n'
@@ -369,24 +441,33 @@ def canned_tree(root, *, name='build', mobilegl_flag='ON', build_type='Release',
'CMAKE_CXX_FLAGS:STRING=\n'
f'MOBILEGL_BUILD_DISAGGREGATED:BOOL={mobilegl_flag}\n'
f"MOBILEGL_BUILD_BENCHMARK:BOOL={'ON' if benchmark else 'OFF'}\n"
f'FETCHCONTENT_BASE_DIR:PATH={deps}\n'
f'CMAKE_CACHEFILE_DIR:INTERNAL={abs_build}\n')
entries = [{'directory': abs_build, 'file': f'/src/MobileGL/f{i}.cpp',
'output': f'CMakeFiles/MobileGL.dir/f{i}.cpp.o',
'command': f'ccache clang++ {define if i == 7 else "-DMG=1"} -I{abs_build}/gen '
f'-I{deps}/googletest-src/googletest/include '
f'-o CMakeFiles/MobileGL.dir/f{i}.cpp.o -c /src/MobileGL/f{i}.cpp'}
for i in range(MIN_COMPILE + 5)]
for i in range(compile_count)]
entries.append({'directory': deps + '/googletest-build/googletest',
'file': deps + '/googletest-src/googletest/src/gtest-all.cc',
'output': 'CMakeFiles/gtest.dir/src/gtest-all.cc.o', 'command': 'clang++ -c gtest-all.cc'})
if benchmark:
entries.append({'directory': abs_build + '/MobileGL/MG_Benchmark', 'file': '/src/MobileGL/MG_Benchmark/b.cpp',
'output': 'CMakeFiles/SanityBench.dir/b.cpp.o', 'command': 'clang++ -c b.cpp'})
entries.append({'directory': deps + '/benchmark-build/src', 'file': deps + '/benchmark-src/src/benchmark.cc',
'output': 'CMakeFiles/benchmark.dir/benchmark.cc.o', 'command': 'clang++ -c benchmark.cc'})
(build / 'compile_commands.json').write_text(json.dumps(entries))
links = [
('libMobileGL.so', 'CXX_SHARED_LIBRARY_LINKER__MobileGL_Release', 'CMakeFiles/MobileGL.dir/f1.cpp.o'),
(library, 'CXX_SHARED_LIBRARY_LINKER__MobileGL_Release', 'CMakeFiles/MobileGL.dir/f1.cpp.o'),
('libMobileGL_s.a', 'CXX_STATIC_LIBRARY_LINKER__MobileGL_s_Release', 'CMakeFiles/MobileGL.dir/f2.cpp.o'),
('MobileGL/MG_Test/SanityTest', 'CXX_EXECUTABLE_LINKER__SanityTest_Release', 'libMobileGL_s.a'),
]
if benchmark:
links.append(('MobileGL/MG_Benchmark/SanityBench', 'CXX_EXECUTABLE_LINKER__SanityBench_Release',
'libMobileGL_s.a'))
links.append((deps + '/benchmark-build/src/libbenchmark.a', 'CXX_STATIC_LIBRARY_LINKER__benchmark_Release',
deps + '/benchmark-build/src/CMakeFiles/benchmark.dir/benchmark.cc.o'))
ninja = ['ninja_required_version = 1.5', '']
for out, rule, inp in links:
ninja += [f'build {out}: {rule} {inp} | /usr/lib/libc.so || cmake_object_order_depends_target_x',
@@ -394,7 +475,8 @@ def canned_tree(root, *, name='build', mobilegl_flag='ON', build_type='Release',
f' POST_BUILD = cd {abs_build}/MobileGL && :', '']
ninja.append('build CMakeFiles/x.o: CXX_COMPILER__MobileGL_Release /src/x.cpp')
(build / 'build.ninja').write_text('\n'.join(ninja) + '\n')
subdirs = 'subdirs("MobileGL/MG_Test")\n' + ('subdirs("MobileGL/MG_Benchmark")\n' if benchmark else '')
subdirs = (f'subdirs("MobileGL/MG_Test")\nsubdirs("{deps}/googletest-build")\n'
+ (f'subdirs("MobileGL/MG_Benchmark")\nsubdirs("{deps}/benchmark-build")\n' if benchmark else ''))
(build / 'CTestTestfile.cmake').write_text(f'# CMake generated Testfile for {abs_build}\n{subdirs}')
(build / 'MobileGL' / 'MG_Test').mkdir(parents=True)
(build / 'MobileGL' / 'MG_Test' / 'CTestTestfile.cmake').write_text(
@@ -408,51 +490,144 @@ def canned_tree(root, *, name='build', mobilegl_flag='ON', build_type='Release',
def self_test():
toolchain = {'MOBILEGL_CI_CXX': 'clang version 20.1.8', 'cmake': 'cmake version 3.31.10'}
other_compiler = dict(toolchain, MOBILEGL_CI_CXX='clang version 20.1.2')
cases = [
('none', {}, toolchain, True),
('a MOBILEGL_* cache line', {'mobilegl_flag': 'OFF'}, toolchain, False),
("one TU's -D", {'define': '-DMG=2'}, toolchain, False),
('the build type', {'build_type': 'Debug'}, toolchain, False),
("the compiler's --version text", {}, other_compiler, False),
('a link flag', {'link_flag': '-Wl,--no-as-needed'}, toolchain, False),
('a test ENVIRONMENT', {'test_env': 'VK_ICD_FILENAMES=/other.json'}, toolchain, False),
('only the benchmark TUs, link lines and subdirs line', {'benchmark': True}, toolchain, True),
('only the build directory name', {'name': 'build-other'}, toolchain, True),
('an empty directory', {'empty': True}, toolchain, False),
('none', {}, 'identical'),
('a MOBILEGL_* cache line', {'mobilegl_flag': 'OFF'}, 'different'),
("one TU's -D", {'define': '-DMG=2'}, 'different'),
('the build type', {'build_type': 'Debug'}, 'different'),
('a link flag', {'link_flag': '-Wl,--no-as-needed'}, 'different'),
('a test ENVIRONMENT', {'test_env': 'VK_ICD_FILENAMES=/other.json'}, 'different'),
('only the benchmark TUs, link lines, subdirs lines and dependency', {'benchmark': True}, 'identical'),
('only the build directory name', {'name': 'build-other'}, 'identical'),
('only where the shared FetchContent directory is', {'deps_name': 'deps-elsewhere'}, 'identical'),
('an empty directory', {'empty': True}, 'unknown'),
]
failures = 0
with tempfile.TemporaryDirectory() as tmp:
base = fingerprint(canned_tree(Path(tmp) / 'base'), toolchain)
assert not unproven(base), unproven(base)
for label, change, chain, expected in cases:
other = fingerprint(canned_tree(Path(tmp) / label.replace(' ', '_').replace("'", ''), **change),
chain)
identical, reasons = verdict(base, other)
ok = identical == expected
failures += not ok
print(f"{'ok' if ok else 'NOT OK':7} {label}: identical={identical}"
+ ('' if ok or identical else f' ({reasons[:2]})'))
# The guard itself: a probe that cannot be configured is never 'identical'.
output = Path(tmp) / 'github_output'
old = os.environ.get('GITHUB_OUTPUT')
os.environ['GITHUB_OUTPUT'] = str(output)
try:
args = argparse.Namespace(built=str(Path(tmp) / 'base' / 'build'), probe='no-such-variant',
probe_dir=str(Path(tmp) / 'probe'), github_output='identical', report=None)
import contextlib
import io
with contextlib.redirect_stdout(io.StringIO()):
guard(args)
finally:
if old is None:
os.environ.pop('GITHUB_OUTPUT', None)
else:
os.environ['GITHUB_OUTPUT'] = old
ok = output.read_text().strip() == 'identical=false'
def report(ok, label, got):
nonlocal failures
failures += not ok
print(f"{'ok' if ok else 'NOT OK':7} a probe that fails to configure: {output.read_text().strip()}")
print(f"{'ok' if ok else 'NOT OK':7} {label}: {got}")
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
base_deps = tmp / 'deps'
base = fingerprint(canned_tree(tmp / 'base', deps=base_deps), base_deps)
assert not unproven(base), unproven(base)
for label, change, expected in cases:
change = dict(change)
deps = tmp / change.pop('deps_name', 'deps')
root = tmp / re.sub(r'\W+', '_', label)
other = fingerprint(canned_tree(root, deps=deps, **change), deps)
got, reasons = verdict(base, other)
report(got == expected, label, got + ('' if got == expected or not reasons else f' ({reasons[:2]})'))
# Two trees that agree on everything but are too thin to prove anything are never identical.
thin_a = fingerprint(canned_tree(tmp / 'thin-a', compile_count=10, deps=base_deps), base_deps)
thin_b = fingerprint(canned_tree(tmp / 'thin-b', compile_count=10, deps=base_deps), base_deps)
report(verdict(thin_a, thin_b)[0] == 'unknown', 'both sides thin', verdict(thin_a, thin_b)[0])
empty = verdict(fingerprint(canned_tree(tmp / 'empty-a', empty=True)),
fingerprint(canned_tree(tmp / 'empty-b', empty=True)))[0]
report(empty == 'unknown', 'both sides empty', empty)
# Full-sized trees that agree but link no libMobileGL.so prove nothing about the library.
no_lib = [fingerprint(canned_tree(tmp / f'no-lib-{side}', deps=base_deps, library='libMobileGLServer.so'),
base_deps) for side in 'ab']
report(verdict(*no_lib)[0] == 'unknown', 'both sides without a libMobileGL.so link', verdict(*no_lib)[0])
# A configure attempt that hangs is killed with its children, so it cannot hold the step.
started = time.monotonic()
with contextlib.redirect_stdout(io.StringIO()):
hung = run_attempt(['bash', '-c', 'sleep 60 & wait'], 'a hung configure', timeout=1)
elapsed = time.monotonic() - started
report(not hung and elapsed < 30, 'a hung configure is killed and fails', f'{hung} after {elapsed:.1f} s')
# The production path carries the timeout too, and four killed attempts fit the step.
stub = tmp / 'hanging-configure.sh'
stub.write_text('sleep 60 & wait\n')
real_configure = CONFIGURE
globals()['CONFIGURE'] = stub
try:
started = time.monotonic()
with contextlib.redirect_stdout(io.StringIO()):
hung = run_configure('primary', tmp / 'hung-build', tmp / 'hung-deps', timeout=1)
elapsed = time.monotonic() - started
finally:
globals()['CONFIGURE'] = real_configure
report(not hung and elapsed < 30, 'run_configure passes its timeout on', f'{hung} after {elapsed:.1f} s')
report(isinstance(CONFIGURE_TIMEOUT, int) and CONFIGURE_TIMEOUT > 0
and 4 * (CONFIGURE_TIMEOUT + 60) <= GUARD_STEP_MINUTES * 60,
'four killed configure attempts fit the guard step', CONFIGURE_TIMEOUT)
escaped = annotation('different', ['100% of the flags'])
report(escaped is not None and escaped.endswith('100%25 of the flags'), "an annotation escapes '%'", escaped)
# The guard itself, with a stand-in for configure_build.sh.
def run_guard(configure, label):
output = tmp / f'github_output-{label}'
old = os.environ.get('GITHUB_OUTPUT')
os.environ['GITHUB_OUTPUT'] = str(output)
try:
args = argparse.Namespace(primary_dir=str(tmp / f'{label}-primary'),
split_dir=str(tmp / f'{label}-split'),
deps_dir=str(tmp / f'{label}-deps'), github_output='verdict', report=None)
printed = io.StringIO()
with contextlib.redirect_stdout(printed):
guard(args, configure, lambda: {'MOBILEGL_CI_CXX': 'clang version 20.1.8'})
finally:
if old is None:
os.environ.pop('GITHUB_OUTPUT', None)
else:
os.environ['GITHUB_OUTPUT'] = old
outputs = dict(line.split('=', 1) for line in output.read_text().splitlines())
outputs['annotations'] = [line for line in printed.getvalue().splitlines() if line.startswith('::')]
return outputs
calls = []
def flaky(variant, build_dir, deps_dir):
calls.append(variant)
if variant == 'primary' and calls.count('primary') == 1:
return False
canned_tree(build_dir.parent, name=build_dir.name, benchmark=variant == 'primary', deps=deps_dir)
return True
out = run_guard(flaky, 'flaky')
report(out.get('verdict') == 'identical' and calls == ['primary', 'primary', 'split'],
'a configure that fails once is retried', f"{out.get('verdict')} after {calls}")
report(set(out) == {'verdict', 'image', 'compiler', 'annotations'},
'the guard writes verdict, image and compiler', sorted(out))
report(out['annotations'] == [], "'identical' is not annotated", out['annotations'])
hangs = []
def hangs_once(variant, build_dir, deps_dir):
hangs.append(variant)
if len(hangs) == 1:
return run_attempt(['bash', '-c', 'sleep 60 & wait'], 'a hung configure', timeout=1)
canned_tree(build_dir.parent, name=build_dir.name, benchmark=variant == 'primary', deps=deps_dir)
return True
out = run_guard(hangs_once, 'hangs')
report(out.get('verdict') == 'identical' and hangs == ['primary', 'primary', 'split'],
'a configure killed on its timeout is retried', f"{out.get('verdict')} after {hangs}")
def broken(variant, build_dir, deps_dir):
return variant != 'split'
out = run_guard(broken, 'broken')
report(out.get('verdict') == 'unknown', 'a configure that fails twice', out.get('verdict'))
report(len(out['annotations']) == 1 and out['annotations'][0].startswith('::notice::')
and 'failed twice' in out['annotations'][0], "'unknown' is a notice with its reason",
out['annotations'])
def debug_split(variant, build_dir, deps_dir):
canned_tree(build_dir.parent, name=build_dir.name, deps=deps_dir,
build_type='Debug' if variant == 'split' else 'Release')
return True
out = run_guard(debug_split, 'differ')
report(out.get('verdict') == 'different', 'a proven difference', out.get('verdict'))
report(len(out['annotations']) == 1 and out['annotations'][0].startswith('::warning::')
and 'CMAKE_BUILD_TYPE' in out['annotations'][0], "'different' is a warning with its reason",
out['annotations'])
def raising(variant, build_dir, deps_dir):
raise OSError('no bash')
out = run_guard(raising, 'raising')
report(out.get('verdict') == 'unknown', 'an error inside the guard', out.get('verdict'))
print(f'build_fingerprint self-test: {"FAILED" if failures else "ok"}')
return 1 if failures else 0
@@ -466,19 +641,20 @@ def main(argv=None):
sub = parser.add_subparsers(dest='command', required=True)
p = sub.add_parser('fingerprint')
p.add_argument('--build-dir', required=True)
p.add_argument('--deps-dir')
p.add_argument('--out')
p = sub.add_parser('compare')
p.add_argument('first')
p.add_argument('second')
p = sub.add_parser('guard')
p.add_argument('--built', required=True)
p.add_argument('--probe', required=True, choices=('primary', 'split'))
p.add_argument('--probe-dir', required=True)
p.add_argument('--github-output', default='identical')
p.add_argument('--primary-dir', required=True)
p.add_argument('--split-dir', required=True)
p.add_argument('--deps-dir', required=True)
p.add_argument('--github-output', default='verdict')
p.add_argument('--report')
args = parser.parse_args(argv)
if args.command == 'fingerprint':
fp = fingerprint(args.build_dir)
fp = fingerprint(args.build_dir, args.deps_dir)
text = json.dumps(fp, indent=1, sort_keys=True) + '\n'
if args.out:
Path(args.out).write_text(text)
@@ -487,11 +663,11 @@ def main(argv=None):
if args.command == 'compare':
a = json.loads(Path(args.first).read_text())
b = json.loads(Path(args.second).read_text())
identical, reasons = verdict(a, b)
result, reasons = verdict(a, b)
for reason in reasons:
print(reason)
print(f"identical={'true' if identical else 'false'}")
return 0 if identical else 1
print(f'verdict={result}')
return 0 if result == 'identical' else 1
return guard(args)
+5 -3
View File
@@ -16,12 +16,14 @@
# library logs the resolved transport at INFO, and run_trace_case.cmake and the split lanes read
# that line back as proof the transport resolved in the process under test.
#
# usage: configure_build.sh <primary|split> <build dir>
# with MOBILEGL_CI_CC, MOBILEGL_CI_CXX and VK_ICD_FILENAMES from setup-test-runner.
# usage: configure_build.sh <primary|split> <build dir> [extra cmake arguments...]
# with MOBILEGL_CI_CC, MOBILEGL_CI_CXX and VK_ICD_FILENAMES from setup-test-runner. The builds pass
# no extra arguments; build_fingerprint.py's guard passes its shared FetchContent directory.
set -euo pipefail
variant="${1:?usage: configure_build.sh <primary|split> <build dir>}"
build_dir="${2:?usage: configure_build.sh <primary|split> <build dir>}"
shift 2
common=(
-S . -B "${build_dir}" -G Ninja
@@ -72,4 +74,4 @@ case "${variant}" in
;;
esac
cmake "${common[@]}" "${flags[@]}"
cmake "${common[@]}" "${flags[@]}" "$@"
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# MobileGL - scripts/ci/split_reuse_build_type.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# The split consumers' build-type check. A consumer that reuses build-linux's build (SPLIT_REUSE is
# true: build-guard found the two configurations identical) takes it only when build-linux built
# Release, the only type build-guard can find identical to the split line. A re-run of failed jobs
# with debug logging rebuilds build-linux as Debug but keeps build-guard's verdict, while
# build-linux-split built nothing, so the consumer stops here and says so.
#
# usage: split_reuse_build_type.sh (SPLIT_REUSE and PRIMARY_BUILD_TYPE from the job)
# split_reuse_build_type.sh --self-test
set -euo pipefail
if [ "${1:-}" = --self-test ]; then
failures=0
expect() {
local want="$1" reuse="$2" type="$3" got=0
SPLIT_REUSE="${reuse}" PRIMARY_BUILD_TYPE="${type}" bash "$0" > /dev/null || got=$?
if [ "${got}" -ne "${want}" ]; then
echo "NOT OK reuse=${reuse} build type '${type}': exit ${got}, expected ${want}"
failures=$((failures + 1))
fi
}
expect 0 true Release
expect 1 true Debug
expect 1 true ''
expect 0 false Debug
expect 0 '' ''
if [ "${failures}" -ne 0 ]; then
echo "split_reuse_build_type self-test: FAILED"
exit 1
fi
echo "split_reuse_build_type self-test: ok"
exit 0
fi
if [ "${SPLIT_REUSE:-false}" = true ] && [ "${PRIMARY_BUILD_TYPE:-}" != Release ]; then
echo "::error::build-guard found the two configurations identical as Release builds, but build-linux built '${PRIMARY_BUILD_TYPE:-an unknown type}' (a re-run of failed jobs with debug logging keeps the guard's verdict) and build-linux-split built nothing; re-run all jobs"
exit 1
fi