diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9e6ae591c..6c67bf1d7 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) run: | - brew install xcodegen - xcodegen --version - xcodegen generate --quiet + # 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 \ + --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 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/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"> + + + + + + 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 3503420c4..50c9e5578 100644 --- a/README.md +++ b/README.md @@ -427,20 +427,57 @@ 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 +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} ${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` +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 08ec15ab5..fb98c1f91 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 [[ ! -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; } fi @@ -231,7 +270,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 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)