From 0720f55790a019feae5072d463c3110d077b527d Mon Sep 17 00:00:00 2001 From: Mathias Hellevang Date: Thu, 9 Jul 2026 20:08:14 +0200 Subject: [PATCH 1/4] ci: prebuilt ad-hoc-signed tester release + install docs build.sh builds unsigned (CODE_SIGNING_ALLOWED=NO), so a dext with restricted entitlements will not load. Add sign.sh to ad-hoc re-sign the dext then the app with their entitlement files (embedding them so AMFI honors them with SIP off + developer mode, no amfi boot-arg). Ad-hoc has no team/profile, so one build installs on any Mac. Add .github/workflows/release.yml: on v* tags builds Release, signs, and attaches the zip to a GitHub Release; workflow_dispatch uploads it as an artifact for pipeline testing on a fork. Add a README 'Installing a prebuilt build (testers)' section: quarantine strip, SIP off, developer mode, install, verify. --- .github/workflows/release.yml | 80 +++++++++++++++++++++++++++++++++++ README.md | 49 +++++++++++++++++++++ sign.sh | 72 +++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100755 sign.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..acc8eb06 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Release (prebuilt tester build) + +# Produces an ad-hoc-signed, ready-to-install ASFW.app for eager testers. +# - Push a tag matching v* -> builds, signs, and attaches the zip to a GitHub Release. +# - Run manually (workflow_dispatch) -> builds, signs, and uploads the zip as a workflow artifact +# (useful for testing this pipeline on a fork before tagging). +# +# The build is UNSIGNED by design (build.sh forces CODE_SIGNING_ALLOWED=NO); sign.sh then +# ad-hoc re-signs the dext + app with their entitlements so the extension can load with +# SIP off + `systemextensionsctl developer on`. Ad-hoc has no team/profile, so one zip +# installs on any Mac. This is NOT notarized and requires SIP disabled — testers only. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write # needed to attach assets to a Release on tag pushes + +jobs: + release: + name: Build & ad-hoc sign + runs-on: macos-26 + env: + CONFIGURATION: Release + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Show toolchain + run: | + xcode-select -p + xcodebuild -version + + - name: Build (Release, unsigned by design) + run: | + chmod +x build.sh bump.sh sign.sh + ./build.sh --no-bump --config Release + + - name: Ad-hoc sign (embed entitlements) + run: ./sign.sh + + - name: Package + id: package + run: | + APP="build/DerivedData/Build/Products/Release/ASFW.app" + [ -d "$APP" ] || { echo "::error::App not found at $APP"; exit 1; } + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + VERSION="${GITHUB_REF_NAME}" + else + VERSION="dev-${GITHUB_SHA:0:8}" + fi + ZIP="ASFW-${VERSION}.zip" + ditto -c -k --keepParent "$APP" "$ZIP" + echo "zip=$ZIP" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + shasum -a 256 "$ZIP" + + - name: Upload workflow artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.package.outputs.zip }} + path: ${{ steps.package.outputs.zip }} + if-no-files-found: error + + - name: Attach to Release (tag pushes only) + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" "${{ steps.package.outputs.zip }}" \ + --title "${GITHUB_REF_NAME}" \ + --notes "Experimental prebuilt tester build (ad-hoc signed, NOT notarized). Requires SIP off + \`systemextensionsctl developer on\`. See the README install section." \ + --prerelease \ + || gh release upload "${GITHUB_REF_NAME}" "${{ steps.package.outputs.zip }}" --clobber diff --git a/README.md b/README.md index 00364898..d477e5d5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [General pitfalls and gotchas](#general-pitfalls-and-gotchas) - [Project structure](#project-structure) - [Building](#building) +- [Installing a prebuilt build (testers)](#installing-a-prebuilt-build-testers) - [Contributing](#contributing) - [Contacts](#contacts) - [References](#references) @@ -347,6 +348,54 @@ NOTE: You need an Apple Developer account (paid) and appropriate entitlements Enabling `systemextensionsctl developer on` is recommended — it allows installing system extensions from the build +## Installing a prebuilt build (testers) + +If you want to test ASFireWire without building it yourself, tagged releases attach a +prebuilt `ASFW.app`. This build is **ad-hoc signed and NOT notarized**, so it can only +be loaded on a machine with System Integrity Protection (SIP) disabled. It is intended +for experimental testing only — not general use. + +> **Warning:** These steps disable SIP, which lowers your Mac's security system-wide. +> Only do this on a machine you are comfortable using for testing, and re-enable SIP +> (`csrutil enable`) when you are done. The build is unsigned/un-notarized and provided +> as-is; run it only if you understand and accept that. + +**Requirements:** an Apple Silicon Mac running macOS 26 (Tahoe), and FireWire hardware +(a PCIe FireWire/OHCI card, or an Apple Thunderbolt-to-FireWire adapter). + +1. **Download** the `ASFW-.zip` from the [Releases](../../releases) page and unzip it. +2. **Remove the quarantine flag** (Gatekeeper quarantines downloaded apps): + ```bash + xattr -dr com.apple.quarantine ASFW.app + ``` +3. **Disable SIP.** Shut down, then boot into Recovery (hold the power button until + "Loading startup options" appears → **Options** → open **Terminal**), and run: + ```bash + csrutil disable + ``` + Reboot back into macOS. Confirm with `csrutil status` (should report disabled). +4. **Enable system-extension developer mode** (runtime toggle, no reboot; requires SIP + already off): + ```bash + systemextensionsctl developer on + ``` +5. **Install the driver.** Move `ASFW.app` to `/Applications`, open it, and use its + **Install** button. Approve the extension in **System Settings → General → Login Items + & Extensions** if prompted (the prompt may not appear in developer mode). +6. **Verify** the driver is active: + ```bash + systemextensionsctl list + ``` + You should see `net.mrmidi.ASFW.ASFWDriver` marked `[activated enabled]`. + +**Uninstall / revert:** remove the extension via the app (or +`systemextensionsctl uninstall`), then re-enable SIP from Recovery with `csrutil enable`. + +**Troubleshooting:** if installation fails with `Missing entitlement +com.apple.developer.system-extension.install`, the build was not signed with its +entitlements embedded — that is a packaging bug in the release, not something to fix on +your machine; please open an issue rather than changing boot-args. + ## Contributing Nice place to start with — [DeepWiki page for ASFW](https://deepwiki.com/mrmidi/ASFireWire). diff --git a/sign.sh b/sign.sh new file mode 100755 index 00000000..ec98a24a --- /dev/null +++ b/sign.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# +# ASFireWire – ad-hoc signing for local install & prebuilt distribution. +# +# build.sh intentionally builds UNSIGNED (CODE_SIGNING_ALLOWED=NO), which yields +# an app with an empty entitlement dump and a fully unsigned .dext. A DriverKit +# extension carrying restricted entitlements will NOT load unless those +# entitlements are actually embedded in its signature — so this script re-signs +# both, ad-hoc, with the correct per-target entitlement files. +# +# Verified on Apple Silicon / macOS 26 (Tahoe): an app+dext signed this way loads +# with `csrutil disable` + `systemextensionsctl developer on` alone — no +# amfi_get_out_of_my_way boot-arg required, because the entitlements are present +# in the signature (AMFI only *validates* entitlements, it does not *inject* them). +# +# Ad-hoc (`--sign -`) has no provisioning profile and no team, so it is not tied +# to any machine: the same signed .app installs on any Mac (with SIP off). +# +# Usage: +# ./sign.sh [path/to/ASFW.app] +# If no path is given, the app is resolved from the build output: +# ${DERIVED:-./build/DerivedData}/Build/Products/${CONFIGURATION:-Release}/ASFW.app + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +CONFIGURATION="${CONFIGURATION:-Release}" +DERIVED="${DERIVED:-./build/DerivedData}" + +APP_ENTITLEMENTS="ASFW/App.entitlements" +DEXT_ENTITLEMENTS="ASFWDriver/ASFWDriver.entitlements" + +APP_PATH="${1:-${DERIVED}/Build/Products/${CONFIGURATION}/ASFW.app}" + +err() { echo "[ERROR] $*" >&2; } +log() { echo "[INFO] $*"; } +ok() { echo "[OK] $*"; } + +[[ -d "${APP_PATH}" ]] || { err "App bundle not found: ${APP_PATH} (build it first with ./build.sh --config ${CONFIGURATION})"; exit 1; } +[[ -f "${APP_ENTITLEMENTS}" ]] || { err "Missing ${APP_ENTITLEMENTS}"; exit 1; } +[[ -f "${DEXT_ENTITLEMENTS}" ]] || { err "Missing ${DEXT_ENTITLEMENTS}"; exit 1; } + +# The dext is bundled inside the app under Contents/Library/SystemExtensions/. +DEXT_PATH="$(find "${APP_PATH}/Contents/Library/SystemExtensions" -maxdepth 1 -name '*.dext' -print -quit 2>/dev/null || true)" +[[ -n "${DEXT_PATH}" ]] || { err "No .dext found under ${APP_PATH}/Contents/Library/SystemExtensions"; exit 1; } + +# Sign inner-first: the dext, then the enclosing app. +log "Signing dext: ${DEXT_PATH}" +codesign --force --sign - --timestamp=none \ + --entitlements "${DEXT_ENTITLEMENTS}" \ + "${DEXT_PATH}" + +log "Signing app: ${APP_PATH}" +codesign --force --sign - --timestamp=none \ + --entitlements "${APP_ENTITLEMENTS}" \ + "${APP_PATH}" + +# Fail loudly if entitlements did not embed — that is the exact failure mode +# (empty entitlement dump -> "Missing entitlement ..." at install time). +log "Verifying embedded entitlements…" +if ! codesign -d --entitlements - --xml "${APP_PATH}" 2>/dev/null | grep -q 'system-extension.install'; then + err "app is missing com.apple.developer.system-extension.install after signing" + exit 1 +fi +if ! codesign -d --entitlements - --xml "${DEXT_PATH}" 2>/dev/null | grep -q 'driverkit'; then + err "dext is missing DriverKit entitlements after signing" + exit 1 +fi + +ok "Signed ${APP_PATH} (ad-hoc, entitlements embedded). Install with SIP off + 'systemextensionsctl developer on'." From dbe496191db5407aa3ede716c726e1f9ffee46f5 Mon Sep 17 00:00:00 2001 From: Mathias Hellevang Date: Thu, 9 Jul 2026 20:19:29 +0200 Subject: [PATCH 2/4] ci: fix artifact double-zip name, tag/version warning, full uninstall command - upload-artifact name without .zip suffix (artifact downloads are zip containers themselves) - warn when tag disagrees with CFBundleShortVersionString (--no-bump build) - drop unneeded fetch-depth/submodules from checkout (no submodules) - README: spell out systemextensionsctl uninstall with '-' team ID --- .github/workflows/release.yml | 13 +++++++++---- README.md | 8 ++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index acc8eb06..f7921eed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,9 +28,6 @@ jobs: DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - name: Show toolchain run: | @@ -55,6 +52,12 @@ jobs: else VERSION="dev-${GITHUB_SHA:0:8}" fi + # Warn (non-fatal) if the tag disagrees with the app's own version — + # build.sh runs with --no-bump, so the app reports whatever is committed. + APP_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist" 2>/dev/null || true)" + if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "v${APP_VERSION}" != "${VERSION}" ]; then + echo "::warning::Tag ${VERSION} != app CFBundleShortVersionString ${APP_VERSION} (built with --no-bump)" + fi ZIP="ASFW-${VERSION}.zip" ditto -c -k --keepParent "$APP" "$ZIP" echo "zip=$ZIP" >> "$GITHUB_OUTPUT" @@ -64,7 +67,9 @@ jobs: - name: Upload workflow artifact uses: actions/upload-artifact@v4 with: - name: ${{ steps.package.outputs.zip }} + # No .zip suffix: artifact downloads are zip containers themselves, + # so a .zip name would yield ASFW-x.zip.zip. + name: ASFW-${{ steps.package.outputs.version }} path: ${{ steps.package.outputs.zip }} if-no-files-found: error diff --git a/README.md b/README.md index d477e5d5..e2d3a337 100644 --- a/README.md +++ b/README.md @@ -388,8 +388,12 @@ for experimental testing only — not general use. ``` You should see `net.mrmidi.ASFW.ASFWDriver` marked `[activated enabled]`. -**Uninstall / revert:** remove the extension via the app (or -`systemextensionsctl uninstall`), then re-enable SIP from Recovery with `csrutil enable`. +**Uninstall / revert:** remove the extension via the app, or manually (ad-hoc builds +have no team ID, hence the `-`): +```bash +systemextensionsctl uninstall - net.mrmidi.ASFW.ASFWDriver +``` +Then re-enable SIP from Recovery with `csrutil enable`. **Troubleshooting:** if installation fails with `Missing entitlement com.apple.developer.system-extension.install`, the build was not signed with its From 609d8fbb706b1b2bb28062a2bfd07ffb7302b07d Mon Sep 17 00:00:00 2001 From: Mathias Hellevang Date: Thu, 9 Jul 2026 20:24:51 +0200 Subject: [PATCH 3/4] ci: name ad-hoc asset ASFW--adhoc.zip Keeps ASFW-.zip free for a future properly signed/notarized asset on the same release. --- .github/workflows/release.yml | 6 ++++-- README.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7921eed..09427d73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,9 @@ jobs: if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "v${APP_VERSION}" != "${VERSION}" ]; then echo "::warning::Tag ${VERSION} != app CFBundleShortVersionString ${APP_VERSION} (built with --no-bump)" fi - ZIP="ASFW-${VERSION}.zip" + # "-adhoc" suffix keeps ASFW-.zip free for a future + # properly signed/notarized asset on the same release. + ZIP="ASFW-${VERSION}-adhoc.zip" ditto -c -k --keepParent "$APP" "$ZIP" echo "zip=$ZIP" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" @@ -69,7 +71,7 @@ jobs: with: # No .zip suffix: artifact downloads are zip containers themselves, # so a .zip name would yield ASFW-x.zip.zip. - name: ASFW-${{ steps.package.outputs.version }} + name: ASFW-${{ steps.package.outputs.version }}-adhoc path: ${{ steps.package.outputs.zip }} if-no-files-found: error diff --git a/README.md b/README.md index e2d3a337..b8991de4 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ for experimental testing only — not general use. **Requirements:** an Apple Silicon Mac running macOS 26 (Tahoe), and FireWire hardware (a PCIe FireWire/OHCI card, or an Apple Thunderbolt-to-FireWire adapter). -1. **Download** the `ASFW-.zip` from the [Releases](../../releases) page and unzip it. +1. **Download** the `ASFW--adhoc.zip` from the [Releases](../../releases) page and unzip it. 2. **Remove the quarantine flag** (Gatekeeper quarantines downloaded apps): ```bash xattr -dr com.apple.quarantine ASFW.app From c640f0a9ddb5e05bda4e8cf31315eb8237b5321d Mon Sep 17 00:00:00 2001 From: Mathias Hellevang Date: Thu, 9 Jul 2026 23:15:58 +0200 Subject: [PATCH 4/4] docs: link Apple's SIP and Recovery guides from install steps --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b8991de4..dbe5bd81 100644 --- a/README.md +++ b/README.md @@ -368,7 +368,10 @@ for experimental testing only — not general use. ```bash xattr -dr com.apple.quarantine ASFW.app ``` -3. **Disable SIP.** Shut down, then boot into Recovery (hold the power button until +3. **Disable SIP** (see Apple's guide on + [disabling and enabling System Integrity Protection](https://developer.apple.com/documentation/security/disabling-and-enabling-system-integrity-protection)). + Shut down, then boot into + [Recovery](https://support.apple.com/en-us/102518) (hold the power button until "Loading startup options" appears → **Options** → open **Terminal**), and run: ```bash csrutil disable