# gitea-macos-runner # # Virtualization.framework refuses to start a VM unless the calling process # carries the `com.apple.security.virtualization` entitlement, and entitlements # only survive on a signed bundle. So the shipping artifact is not a bare # executable but a minimal `.app` bundle that we sign -- with a Developer ID # certificate when one is in the keychain, ad-hoc otherwise. See docs/DESIGN.md # ("Verified Facts", item 10). SHELL := /bin/bash APP_NAME := GiteaMacosRunner BIN_NAME := gitea-macos-runner BUILD_DIR := .build APP_DIR := $(BUILD_DIR)/$(APP_NAME).app CONTENTS := $(APP_DIR)/Contents MACOS_DIR := $(CONTENTS)/MacOS RES_DIR := $(CONTENTS)/Resources INFO_PLIST := Resources/Info.plist # The entitlements plist grants exactly one entitlement, # `com.apple.security.virtualization`. Virtualization.framework refuses to # create a VM without it. It is not a restricted entitlement: ad-hoc signing # (`codesign --sign -`) grants it, and a Developer ID certificate grants it # without a provisioning profile. # # Deliberately absent: com.apple.vm.networking, which would be needed for a # bridged network attachment. That one IS restricted and requires an approved # provisioning profile. We use NAT instead, which needs nothing extra and has # the side benefit of putting each guest into /var/db/dhcpd_leases, which is # how the daemon discovers guest IPs. # # Keep that plist free of XML comments. `plutil` accepts them, but codesign # hands the file to AMFI's stricter parser, which rejects a `` block # with "AMFIUnserializeXML: syntax error" -- and the bundle then signs with no # entitlements at all, so every VM start fails at runtime. ENTITLEMENTS:= Resources/gitea-macos-runner.entitlements # Data files the tool reads at runtime. `GuestProvisioner`, `LaunchdService`, # and `config init` each look in `Contents/Resources` first and only then fall # back to repo-relative paths, so an installed .app that lacks these is a # working binary with a broken `image build` / `service install` / `config init`. APP_RESOURCES := Resources/provision.sh \ Resources/launchd.plist.template \ Resources/config.example.json INSTALL_DIR := $(HOME)/Applications LINK_PATH := /usr/local/bin/$(BIN_NAME) # Code signing identity. # # A real Developer ID certificate is what makes the bundle's code identity # *stable across rebuilds*. Its designated requirement is anchored to the team # ("... and certificate leaf[subject.OU] = L7UDTQ6F5W"), so macOS recognises # every subsequent build as the same program. An ad-hoc signature has no such # anchor, so the system falls back to the main executable's Mach-O UUID -- which # the linker regenerates on essentially every link. Each `make install` then # presents a program macOS has never seen, and per TN3179 that silently # withdraws the app's Local Network grant. See docs/troubleshooting.md. # # TEAM_ID picks the certificate out of the keychain. When no matching # "Developer ID Application" identity is present the build still succeeds -- # ad-hoc, with a warning -- because CI runs `make all` inside a throwaway guest # that has neither a keychain nor a certificate, and that path must keep # working. Override with `make sign TEAM_ID=...`, or `TEAM_ID=` to force ad-hoc. TEAM_ID ?= L7UDTQ6F5W # Hardened runtime plus a trusted timestamp: the pair notarization requires. # Neither costs anything at runtime here, and having them means the bundle can # be notarized later without re-signing. `--timestamp` contacts Apple's # timestamp authority, so signing needs network access. Both are rejected by an # ad-hoc signature, hence they are only passed on the Developer ID path. SIGN_OPTS ?= --options runtime --timestamp # Release by default; `make dev` overrides to debug. CONFIG ?= release BIN_PATH = $(BUILD_DIR)/$(CONFIG)/$(BIN_NAME) .PHONY: all build bundle sign dev test install uninstall clean help # These targets are a pipeline, not independent work: `bundle` needs the binary # `build` produced, and `sign` signs the tree `bundle` assembled — a signature # that overtook the resource copy would not cover Contents/Resources, and the # bundle would fail to launch. Expressing that as prerequisites is not an option # because `dev` reuses `bundle` against a debug build it made itself, so serial # execution is imposed instead. .NOTPARALLEL: all: build bundle sign ## build: compile the release binary for arm64 build: swift build -c release --arch arm64 ## bundle: assemble the minimal .app around the compiled binary bundle: @test -x "$(BIN_PATH)" || { echo "error: $(BIN_PATH) not built; run 'make build' (or 'make dev')"; exit 1; } mkdir -p "$(MACOS_DIR)" "$(RES_DIR)" cp "$(BIN_PATH)" "$(MACOS_DIR)/$(BIN_NAME)" cp "$(INFO_PLIST)" "$(CONTENTS)/Info.plist" cp $(APP_RESOURCES) "$(RES_DIR)/" chmod +x "$(RES_DIR)/provision.sh" ## sign: sign the bundle (Developer ID when available, else ad-hoc) with the virtualization entitlement sign: @identity=$$(security find-identity -v -p codesigning 2>/dev/null \ | grep "Developer ID Application" | grep -F "($(TEAM_ID))" \ | head -1 | awk '{print $$2}'); \ if [ -n "$$identity" ]; then \ echo "signing with Developer ID $$identity (team $(TEAM_ID))"; \ codesign --sign "$$identity" $(SIGN_OPTS) \ --entitlements "$(ENTITLEMENTS)" --force "$(APP_DIR)"; \ else \ echo "warning: no 'Developer ID Application' identity for team '$(TEAM_ID)' in the keychain."; \ echo " Falling back to an ad-hoc signature. The bundle runs and the entitlement"; \ echo " works, but its code identity changes on every rebuild, so a macOS Local"; \ echo " Network grant will not survive the next 'make install'."; \ echo " See docs/troubleshooting.md."; \ codesign --sign - --entitlements "$(ENTITLEMENTS)" --force "$(APP_DIR)"; \ fi @echo "--- entitlements ---" @codesign -d --entitlements - "$(APP_DIR)" 2>/dev/null || true @echo "--- identity ---" @# -dvv, not -dv: the Authority chain is only printed at the second -v. @# The CodeDirectory line is where `flags=0x10000(runtime)` shows up, which @# is the only proof the hardened runtime actually landed. @codesign -dvv "$(APP_DIR)" 2>&1 \ | grep -E "^(Identifier|TeamIdentifier|Authority|Timestamp|CodeDirectory)" || true ## dev: debug build + bundle + sign (fast iteration loop) dev: swift build --arch arm64 $(MAKE) CONFIG=debug bundle $(MAKE) sign ## test: run the unit test suite test: swift test ## install: copy the signed app to ~/Applications and link the CLI install: all mkdir -p "$(INSTALL_DIR)" rm -rf "$(INSTALL_DIR)/$(APP_NAME).app" cp -R "$(APP_DIR)" "$(INSTALL_DIR)/$(APP_NAME).app" @if [ -w "$$(dirname $(LINK_PATH))" ]; then \ ln -sf "$(INSTALL_DIR)/$(APP_NAME).app/Contents/MacOS/$(BIN_NAME)" "$(LINK_PATH)"; \ echo "linked $(LINK_PATH)"; \ else \ echo "note: $$(dirname $(LINK_PATH)) not writable; skipping symlink."; \ echo " run: sudo ln -sf $(INSTALL_DIR)/$(APP_NAME).app/Contents/MacOS/$(BIN_NAME) $(LINK_PATH)"; \ fi ## uninstall: remove the installed app and symlink uninstall: rm -rf "$(INSTALL_DIR)/$(APP_NAME).app" @if [ -L "$(LINK_PATH)" ]; then rm -f "$(LINK_PATH)"; fi ## clean: remove all build products clean: rm -rf "$(BUILD_DIR)" ## help: list targets help: @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'