Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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

- 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
# 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
# "-adhoc" suffix keeps ASFW-<version>.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"
shasum -a 256 "$ZIP"

- name: Upload workflow artifact
uses: actions/upload-artifact@v4
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 }}-adhoc
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
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -347,6 +348,61 @@ 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-<version>-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
```
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
```
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 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
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).
Expand Down
72 changes: 72 additions & 0 deletions sign.sh
Original file line number Diff line number Diff line change
@@ -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'."
Loading