163 lines
7.3 KiB
Makefile
163 lines
7.3 KiB
Makefile
# 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/^## / /'
|