From dfdd9fee2acb4a26ebce0807234dd113f6ea410a Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 26 Jul 2026 18:46:17 +0800 Subject: [PATCH 1/6] ci: pin XcodeGen release for project verification --- .github/workflows/build-and-test.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9e6ae591c..3459ecd0e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,19 +26,29 @@ jobs: # ASFW.xcodeproj is generated from project.yml (XcodeGen) and committed. # Fail if they drifted apart — someone edited project.yml or added source # files without regenerating, or hand-edited the pbxproj. If this fails - # after an xcodegen version bump changed the output format, regenerate - # locally with the new version and commit. + # after the pinned xcodegen version changes, regenerate locally with the + # same version and commit. - name: Verify ASFW.xcodeproj matches project.yml (XcodeGen) + env: + XCODEGEN_VERSION: 2.45.4 + XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef run: | - brew install xcodegen - xcodegen --version - xcodegen generate --quiet + XCODEGEN_ARCHIVE="$RUNNER_TEMP/xcodegen.zip" + XCODEGEN_ROOT="$RUNNER_TEMP/xcodegen-release" + curl --fail --location --retry 3 \ + --output "$XCODEGEN_ARCHIVE" \ + "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" + echo "${XCODEGEN_SHA256} ${XCODEGEN_ARCHIVE}" | shasum -a 256 -c - + ditto -x -k "$XCODEGEN_ARCHIVE" "$XCODEGEN_ROOT" + XCODEGEN_BIN="$XCODEGEN_ROOT/xcodegen/bin/xcodegen" + "$XCODEGEN_BIN" --version + "$XCODEGEN_BIN" generate --quiet # Diff only the pbxproj: scheme files are cosmetically rewritten by # any open Xcode (version attr, BuildableName flavor, empty blocks), # so they flip-flop between xcodegen and Xcode styles; their semantic # content comes from project.yml either way. if ! git diff --exit-code --stat -- ASFW.xcodeproj/project.pbxproj project.yml; then - echo "::error::ASFW.xcodeproj is out of sync with project.yml. Run 'xcodegen generate' locally and commit the regenerated project (see README → Building)." + echo "::error::ASFW.xcodeproj is out of sync with project.yml. Regenerate it with XcodeGen ${XCODEGEN_VERSION} and commit the result (see README → Building)." exit 1 fi From da9e0d39968bbb713f1f4cd6dc641eae79f503d3 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 26 Jul 2026 19:13:18 +0800 Subject: [PATCH 2/6] ci: defer GoogleTest discovery until test execution --- tests/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0eb481a0..ba76bfb64 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,6 +50,8 @@ if (NOT GTest_FOUND) endif() include(GoogleTest) +# Avoid parallel post-build discovery racing on empty GoogleTest JSON output. +set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) # Define common test settings interface library add_library(asfw_test_common_interface INTERFACE) From 3e19cb1498fcae18ca16cabd121e4b4b4ed7bcc7 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 26 Jul 2026 21:38:43 +0800 Subject: [PATCH 3/6] fix(build): preserve Swift test failures in quiet mode --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 08ec15ab5..48c7a1440 100755 --- a/build.sh +++ b/build.sh @@ -231,7 +231,7 @@ run_swift_tests() { if $VERBOSE; then xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 else - xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | grep -E '(Test Case|passed|failed|error:)' || true + xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | grep -E '(Test Case|passed|failed|error:)' fi local test_status=${PIPESTATUS[0]} set -e From 94b9fd31d8c569e247f614f3f863ec082f72b3c4 Mon Sep 17 00:00:00 2001 From: gly11 Date: Mon, 27 Jul 2026 02:06:53 +0800 Subject: [PATCH 4/6] ci: enforce the XcodeGen pin in build.sh, not just CI The pin added earlier in this branch only bound CI. build.sh still ran whatever xcodegen was on PATH, and it regenerates ASFW.xcodeproj on every non-test-only build. Homebrew currently ships 2.46.0 while the pin was 2.45.4, and their output is not byte-identical -- 2.46.0 reorders the pbxproj `targets` array. So a single ./build.sh on a brew-current machine produces a project the drift check rejects, and that check's error message tells you to regenerate, which reproduces the same diff. Move the version and its SHA-256 into .xcodegen-version and have both the workflow and build.sh read it, so the two can no longer disagree. build.sh now stops with an actionable error when the installed version differs, rather than warning and skipping regeneration: a build silently missing a newly added source file is much harder to diagnose than a version mismatch. --no-xcodegen builds the committed project as-is for machines that can't install the pin. Bump the pin to 2.46.0 to match what Homebrew ships today, and regenerate ASFW.xcodeproj with it. The only change is the `targets` array ordering, which has no semantic effect. Document how to install the pinned release in README -- the same download, pin, and checksum CI uses -- since "install the pinned version" isn't actionable without it. --- .github/workflows/build-and-test.yml | 6 ++-- .xcodegen-version | 18 ++++++++++++ ASFW.xcodeproj/project.pbxproj | 2 +- README.md | 42 +++++++++++++++++++++++----- build.sh | 38 ++++++++++++++++++++++--- 5 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 .xcodegen-version diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3459ecd0e..6c67bf1d7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -29,10 +29,10 @@ jobs: # after the pinned xcodegen version changes, regenerate locally with the # same version and commit. - name: Verify ASFW.xcodeproj matches project.yml (XcodeGen) - env: - XCODEGEN_VERSION: 2.45.4 - XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef run: | + # Single source of truth, shared with build.sh, so a contributor's + # locally installed xcodegen can never disagree with this check. + . ./.xcodegen-version XCODEGEN_ARCHIVE="$RUNNER_TEMP/xcodegen.zip" XCODEGEN_ROOT="$RUNNER_TEMP/xcodegen-release" curl --fail --location --retry 3 \ diff --git a/.xcodegen-version b/.xcodegen-version new file mode 100644 index 000000000..dbafa5344 --- /dev/null +++ b/.xcodegen-version @@ -0,0 +1,18 @@ +# XcodeGen release pinned for generating ASFW.xcodeproj from project.yml. +# +# Both CI (.github/workflows/build-and-test.yml) and build.sh read this file, so +# the version that regenerates the committed project is the same everywhere. +# Homebrew's xcodegen floats and its output is NOT byte-identical across +# releases (2.45.4 -> 2.46.0 reorders the pbxproj `targets` list), which is why +# the pin exists and why build.sh refuses to regenerate with a different one. +# +# Upgrading — do all three steps together, or CI's drift check will fail: +# 1. bump XCODEGEN_VERSION below +# 2. curl the release zip, put its `shasum -a 256` in XCODEGEN_SHA256 +# curl -fL -o /tmp/xcodegen.zip \ +# https://github.com/yonaskolb/XcodeGen/releases/download//xcodegen.zip +# shasum -a 256 /tmp/xcodegen.zip +# 3. regenerate ASFW.xcodeproj with that exact version and commit it alongside +# +XCODEGEN_VERSION=2.46.0 +XCODEGEN_SHA256=4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806 diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index cbbb809ad..44ec75d46 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -3000,8 +3000,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - EE5A446B3669D0171F9606EE /* ASFW */, 0EB9A8DA75D08971084A440A /* ASFWDriver */, + EE5A446B3669D0171F9606EE /* ASFW */, 348672D607701677ABCB567C /* ASFWTests */, ); }; diff --git a/README.md b/README.md index 3503420c4..f168981c4 100644 --- a/README.md +++ b/README.md @@ -427,20 +427,48 @@ Build scripts or CMakeLists are for quick testing and creating compile_commands. ### Xcode project is generated (XcodeGen) `ASFW.xcodeproj` is generated from the root [`project.yml`](project.yml) with -[XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`). -The generated project is committed, so plain checkouts (and CI) build without -XcodeGen installed — but **never edit the pbxproj or project settings in the -Xcode UI**; change `project.yml` instead. +[XcodeGen](https://github.com/yonaskolb/XcodeGen). The generated project is +committed, so plain checkouts (and CI) build without XcodeGen installed — but +**never edit the pbxproj or project settings in the Xcode UI**; change +`project.yml` instead. + +**The XcodeGen version is pinned** in [`.xcodegen-version`](.xcodegen-version), +because its output is *not* byte-identical across releases (2.45.4 → 2.46.0 +reorders the pbxproj `targets` list) and CI diffs the regenerated project +against the committed one. Homebrew's `xcodegen` floats, so install the pinned +release rather than `brew install xcodegen` — this is the same download, pin, +and checksum CI uses: + +```bash +. ./.xcodegen-version +curl --fail --location --retry 3 --output /tmp/xcodegen.zip \ + "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" +echo "${XCODEGEN_SHA256} /tmp/xcodegen.zip" | shasum -a 256 -c - +ditto -x -k /tmp/xcodegen.zip /tmp/xcodegen-release +sudo /tmp/xcodegen-release/xcodegen/install.sh # -> /usr/local/bin/xcodegen +# no-sudo variant: install.sh takes a prefix, e.g. +# /tmp/xcodegen-release/xcodegen/install.sh "$HOME/.local" +``` + +If `xcodegen` isn't on your `PATH` at all, nothing above applies — `build.sh` +skips regeneration entirely and builds the committed project. The pin only +matters once you have *some* `xcodegen` installed, because `build.sh` +regenerates on every build; if that version doesn't match the pin it stops with +an error rather than writing a pbxproj CI would reject. Use `--no-xcodegen` to +build anyway. After **adding, removing, or renaming source files**, regenerate the project and commit it together with your change: ```bash -xcodegen generate # ./build.sh does this automatically when xcodegen is installed +xcodegen generate # ./build.sh does this automatically, and refuses to run + # if your xcodegen doesn't match the pin ``` -Output is deterministic — regenerating with no changes produces an identical -pbxproj. +Output is deterministic *for a given XcodeGen version* — regenerating with no +changes produces an identical pbxproj. If your machine has a different version +and you only need to build, pass `./build.sh --no-xcodegen` to use the committed +project as-is (added or removed sources will not be picked up). NOTE: You need an Apple Developer account (paid) and appropriate entitlements — or a free account plus SIP disabled — to build/load the driver on your machine. See Apple's documentation for details: https://developer.apple.com/documentation/driverkit/debugging-and-testing-system-extensions diff --git a/build.sh b/build.sh index 48c7a1440..cef264804 100755 --- a/build.sh +++ b/build.sh @@ -60,6 +60,10 @@ SWIFT_COVERAGE_LCOV="${BUILD_DIR}/swift_coverage.lcov" # See README "SCSI HBA — opt-in". ENABLE_SCSI=false +# Skip the XcodeGen regeneration in preflight() and build the committed project +# as-is. Escape hatch for a machine whose xcodegen differs from the pin. +NO_XCODEGEN=false + usage() { cat </dev/null 2>&1; then + # source globs pick up added/removed files; output is deterministic *for a + # given xcodegen version*, so this is a no-op when nothing changed. Falls + # through to the committed project when xcodegen isn't installed (e.g. CI + # runners, which use the pinned release directly). + if [[ -f "project.yml" ]] && ! $NO_XCODEGEN && command -v xcodegen >/dev/null 2>&1; then + # XcodeGen output is not byte-identical across releases, and CI diffs the + # regenerated pbxproj against the committed one. Regenerating with an + # unpinned version silently produces a project that CI will reject, so + # stop here instead — a build missing a newly added source file is much + # harder to diagnose than this message. + if [[ -f ".xcodegen-version" ]]; then + . ./.xcodegen-version + xcodegen_actual="$(xcodegen --version 2>/dev/null | awk '{print $NF}')" + if [[ -n "${XCODEGEN_VERSION:-}" && "$xcodegen_actual" != "$XCODEGEN_VERSION" ]]; then + err "xcodegen ${xcodegen_actual:-} is installed, but this repo pins ${XCODEGEN_VERSION}." + err "Regenerating would produce a pbxproj that CI's drift check rejects." + err "Either:" + err " - install the pinned release (copy-paste block in README -> Building ->" + err " 'Xcode project is generated (XcodeGen)'), or" + err " - re-run with --no-xcodegen to build the committed project as-is" + err " (added/removed source files will NOT be picked up)." + exit 1 + fi + fi log "Regenerating ${PROJECT_NAME}.xcodeproj from project.yml..." xcodegen generate --quiet || { err "xcodegen generate failed"; exit 1; } fi From 7538dba7ac60ccf892e83a22194e1d402d889e15 Mon Sep 17 00:00:00 2001 From: gly11 Date: Mon, 27 Jul 2026 02:23:39 +0800 Subject: [PATCH 5/6] ci: harden XcodeGen pin enforcement --- CLAUDE.md | 10 ++++++---- README.md | 21 +++++++++++++++------ build.sh | 35 ++++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3b0cbb464..054d891c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,10 +14,12 @@ Two components: **`ASFW.xcodeproj` is GENERATED from the root `project.yml` (XcodeGen).** Never edit the pbxproj or hand-tune settings in Xcode — change `project.yml` and run -`xcodegen generate` (`./build.sh` does it automatically when xcodegen is -installed). After adding/removing/renaming source files, regenerate and commit -the updated `ASFW.xcodeproj` together with `project.yml`. Output is -deterministic; the generated project stays committed so CI builds without +`xcodegen generate` with the version pinned in `.xcodegen-version` +(`./build.sh` checks an installed xcodegen automatically; use +`--no-xcodegen` only to build the committed project as-is). After +adding/removing/renaming source files, regenerate and commit the updated +`ASFW.xcodeproj` together with `project.yml`. Output is deterministic for the +pinned version; the generated project stays committed so CI builds without xcodegen. (`ADKVirtualAudioLab/` has its own separate `project.yml`.) **Primary build (Xcode — required for signing and producing `.dext`):** diff --git a/README.md b/README.md index f168981c4..50c9e5578 100644 --- a/README.md +++ b/README.md @@ -441,15 +441,24 @@ and checksum CI uses: ```bash . ./.xcodegen-version -curl --fail --location --retry 3 --output /tmp/xcodegen.zip \ +XCODEGEN_TMP="$(mktemp -d)" +XCODEGEN_PREFIX="$HOME/.local" +curl --fail --location --retry 3 \ + --output "$XCODEGEN_TMP/xcodegen.zip" \ "https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" -echo "${XCODEGEN_SHA256} /tmp/xcodegen.zip" | shasum -a 256 -c - -ditto -x -k /tmp/xcodegen.zip /tmp/xcodegen-release -sudo /tmp/xcodegen-release/xcodegen/install.sh # -> /usr/local/bin/xcodegen -# no-sudo variant: install.sh takes a prefix, e.g. -# /tmp/xcodegen-release/xcodegen/install.sh "$HOME/.local" +echo "${XCODEGEN_SHA256} ${XCODEGEN_TMP}/xcodegen.zip" | shasum -a 256 -c - +ditto -x -k "$XCODEGEN_TMP/xcodegen.zip" "$XCODEGEN_TMP/release" +mkdir -p "$XCODEGEN_PREFIX" +"$XCODEGEN_TMP/release/xcodegen/install.sh" "$XCODEGEN_PREFIX" +export PATH="$XCODEGEN_PREFIX/bin:$PATH" +hash -r +xcodegen --version ``` +The `PATH` update deliberately places the pinned binary before a Homebrew +installation. Add the same `export` command to your shell startup file if you +want future terminal sessions to use the pinned version. + If `xcodegen` isn't on your `PATH` at all, nothing above applies — `build.sh` skips regeneration entirely and builds the committed project. The pin only matters once you have *some* `xcodegen` installed, because `build.sh` diff --git a/build.sh b/build.sh index cef264804..fb98c1f91 100755 --- a/build.sh +++ b/build.sh @@ -136,19 +136,28 @@ preflight() { # unpinned version silently produces a project that CI will reject, so # stop here instead — a build missing a newly added source file is much # harder to diagnose than this message. - if [[ -f ".xcodegen-version" ]]; then - . ./.xcodegen-version - xcodegen_actual="$(xcodegen --version 2>/dev/null | awk '{print $NF}')" - if [[ -n "${XCODEGEN_VERSION:-}" && "$xcodegen_actual" != "$XCODEGEN_VERSION" ]]; then - err "xcodegen ${xcodegen_actual:-} is installed, but this repo pins ${XCODEGEN_VERSION}." - err "Regenerating would produce a pbxproj that CI's drift check rejects." - err "Either:" - err " - install the pinned release (copy-paste block in README -> Building ->" - err " 'Xcode project is generated (XcodeGen)'), or" - err " - re-run with --no-xcodegen to build the committed project as-is" - err " (added/removed source files will NOT be picked up)." - exit 1 - fi + if [[ ! -r ".xcodegen-version" ]]; then + err "xcodegen is installed, but the required .xcodegen-version pin is missing or unreadable." + err "Refusing to regenerate ${PROJECT_NAME}.xcodeproj with an unpinned version." + exit 1 + fi + unset XCODEGEN_VERSION + . ./.xcodegen-version + if [[ -z "${XCODEGEN_VERSION:-}" ]]; then + err ".xcodegen-version does not define XCODEGEN_VERSION." + err "Refusing to regenerate ${PROJECT_NAME}.xcodeproj with an unpinned version." + exit 1 + fi + xcodegen_actual="$(xcodegen --version 2>/dev/null | awk '{print $NF}')" + if [[ "$xcodegen_actual" != "$XCODEGEN_VERSION" ]]; then + err "xcodegen ${xcodegen_actual:-} is installed, but this repo pins ${XCODEGEN_VERSION}." + err "Regenerating would produce a pbxproj that CI's drift check rejects." + err "Either:" + err " - install the pinned release (copy-paste block in README -> Building ->" + err " 'Xcode project is generated (XcodeGen)'), or" + err " - re-run with --no-xcodegen to build the committed project as-is" + err " (added/removed source files will NOT be picked up)." + exit 1 fi log "Regenerating ${PROJECT_NAME}.xcodeproj from project.yml..." xcodegen generate --quiet || { err "xcodegen generate failed"; exit 1; } From 21ea8e27102c01448ef532f5aa4b8085536bc572 Mon Sep 17 00:00:00 2001 From: gly11 Date: Tue, 28 Jul 2026 18:28:00 +0800 Subject: [PATCH 6/6] ci: regenerate dev scheme with pinned XcodeGen --- .../xcschemes/ASFWDriver.xcscheme | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme index 09c8b9bed..0d79cf893 100644 --- a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme +++ b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme @@ -1,10 +1,11 @@ + version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> @@ -26,18 +27,21 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + +