From 4d5656c56ae79ebb8cb2b9bd1e15e9cb3e8a1248 Mon Sep 17 00:00:00 2001 From: Alexander Goodkind Date: Thu, 23 Jul 2026 08:20:53 -0700 Subject: [PATCH] Build, sign, and verify every platform in one CI Verify job SWIFT_VERIFY_BUILD_CMD builds every platform through CellTunnelDev `build all`, so one Verify job compiles the Mac agent and tunnel, the Catalyst app, the iPhone simulator app, the iPhone device app, and the daemon. These targets carry App Groups and Network Extension entitlements, so each needs a provisioning profile. CI runners are not registered devices, so development provisioning cannot work there; only App Store distribution profiles, which carry no device list, sign on an unregistered machine. The ci-provision setup step runs fastlane before the build to create or renew one App Store profile per target through the App Store Connect API key (fastlane/Fastfile), so profiles do not expire out from under CI. Project.swift pins each profile by name and signs manually with the Apple Distribution certificate when TUIST_DEVELOPER_ID_SIGNING is set; local builds keep automatic development signing. ci.yml imports the distribution certificate and runs ci-provision, and no longer installs static profiles. SWIFT_MK_VERIFY_SIGNING_ROOTS verifies every runnable product is signed with the team and is not ad-hoc, skipping the ad-hoc simulator product. The separate build-catalyst, build-iphone-sim, and build-daemon extra-target jobs are removed because the one build produces them all. Rename ProcessSupport EOF semaphore locals. Co-authored-by: Codex Co-authored-by: Claude --- .github/workflows/ci.yml | 36 ++-- .gitignore | 9 + AGENTS.md | 2 +- Gemfile | 7 + Gemfile.lock | 245 +++++++++++++++++++++++ Makefile | 26 ++- Project.swift | 72 +++++-- README.md | 12 +- Tools/CellTunnelDev/ProcessSupport.swift | 12 +- fastlane/Fastfile | 68 +++++++ fastlane/README.md | 32 +++ 11 files changed, 474 insertions(+), 47 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Fastfile create mode 100644 fastlane/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef72065..5dd48de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,26 +33,24 @@ jobs: # adds /opt/homebrew/bin to PATH, which is where brew installs go. The shared # required jobs and the signed macOS build all link WireGuardKitGo. brew-packages: go - # Import the Apple Distribution certificate into the runner keychain and hand - # its SHA-1 to swift-mk as CODE_SIGN_IDENTITY. swift-mk builds the signing - # xcconfig (the identity + Manual style + team) and exports XCODE_XCCONFIG_FILE - # so it wins over every target. The macOS agent and tunnel provider use the - # app-extension NetworkExtension entitlement (packet-tunnel-provider), which - # Developer ID cannot authorize on macOS; App Store distribution profiles do, - # and they enumerate no devices, so the build signs on an unregistered runner. + # Sign every product in one Verify job. SWIFT_VERIFY_BUILD_CMD (`build all`) + # builds the Mac agent and tunnel, the Catalyst app, the iPhone simulator app, + # the iPhone device app, and the daemon. These targets carry App Groups and + # Network Extension entitlements, so each needs a provisioning profile. The CI + # runner is not a registered device, so development provisioning cannot work; + # only App Store distribution profiles, which carry no device list, sign there. + # + # import-signing-cert imports the Apple Distribution certificate that manual + # signing uses. The ci-provision setup step runs fastlane before the build to + # create or renew one App Store profile per target through the App Store Connect + # API key (APPLE_NOTARY_*), so profiles never expire out from under CI; + # Project.swift pins each profile by name. TUIST_DEVELOPER_ID_SIGNING selects the + # project's distribution signing mode. install-provisioning-profile is not set, + # because fastlane provisions instead. SWIFT_MK_VERIFY_SIGNING_ROOTS verifies each + # runnable product is signed with the team and is not ad-hoc. import-signing-cert: true signing-identity-name: "Apple Distribution: Alex Goodkind (H3BMXM4W7H)" apple-team-id: H3BMXM4W7H - # The agent and tunnel-provider targets carry App Groups + Network Extensions - # entitlements, so each needs its own provisioning profile. Two Mac App Store - # provisioning profiles are installed from the newline-separated - # APPLE_DEVELOPER_ID_PROFILE_BASE64 secret; Project.swift maps each NE target to - # its profile by name when the engine signals provisioning (a non-empty - # PROVISIONING_PROFILE_SPECIFIER), so local builds that install no profile are - # unaffected. - install-provisioning-profile: true - # Signed Build job runs `make build`, which SWIFT_BUILD_CMD wires to - # CellTunnelDev `build all` (Apple Distribution on the Mac products). Do not - # set CODE_SIGN_IDENTITY or CODE_SIGN_STYLE in make-args: that would beat - # swift-mk's signing override and force failing development signing. + setup-target: ci-provision + make-args: TUIST_DEVELOPER_ID_SIGNING=1 secrets: inherit diff --git a/.gitignore b/.gitignore index e1076b7..3e67aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,12 @@ Sources/CellTunnelCore/Generated/ *.xcworkspace/ xcuserdata/ .config/mise/conf.d/swift-mk.toml + +# Ruby / fastlane +/vendor/ +/.bundle/ +/fastlane/report.xml +# fastlane downloads a copy of each provisioning profile into the working dir; the +# authoritative copy is installed into Xcode's profiles folder. +/AppStore_*.mobileprovision +/AppStore_*.provisionprofile diff --git a/AGENTS.md b/AGENTS.md index 47f8fde..409b503 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ Targets reference the xcconfig values rather than literal identifiers. A make ta ## Build and install -`README.md` is the user-facing build, install, and run quickstart, and `make help` lists every target. Prefer a named platform target (`make build-mac`, `make build-iphone`, and the rest) when you need one product. `make build` and `make build-all` compile every platform for the engine and CI path. The `install` targets deploy a prebuilt bundle, so build the target first. `make install-mac` copies the agent app to `/Applications/CellTunnel/CellTunnelAgent.app`, then always restarts the launchd service so the freshly built binary is the one running. +`README.md` is the user-facing build, install, and run quickstart, and `make help` lists every target. Use the named build targets (`make build-mac`, `make build-iphone`, and the rest) for normal platform builds. `make build` and `make build-all` compile every platform. CI builds every platform in one Verify job, signs each product, and verifies each runnable product's signature. The `install` targets deploy a prebuilt bundle, so build the target first. `make install-mac` copies the agent app to `/Applications/CellTunnel/CellTunnelAgent.app`, then always restarts the launchd service so the freshly built binary is the one running. `Products/celltunnelctl` is built alongside the daemon target and is the agent control client; run `celltunnelctl --help` for the current command set. The agent owns one config library, exposed both there (`celltunnelctl configs`) and in the Mac Catalyst app. The library model (UUID identity, profile stamping, and the non-mutating boot assertion) lives in `docs/architecture.md` under "Configuration and routes". diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7b49451 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +# fastlane provisions the App Store distribution profiles CI signs with. The CI +# runner and unregistered machines cannot use development provisioning (it requires +# a registered device), so `fastlane sigh` creates and renews device-less App Store +# profiles for each target through the App Store Connect API key. See fastlane/Fastfile. +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..9d9ec06 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,245 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1273.0) + aws-sdk-core (3.254.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.228.1) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (1.6.0) + logger + faraday (1.10.6) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.237.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.105.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.65.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.21.1) + jwt (3.2.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.8.0) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-25 + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.7.2 diff --git a/Makefile b/Makefile index 5aceb29..4348b6f 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ XCCONFIG_EXPORTED_VARS := \ # exports that command into child environments. SWIFT_NAMED_RUN_HINT := use make run-catalyst|run-iphone|run-iphone-sim SWIFT_BUILD_CMD ?= $(CELL_TUNNEL_DEV) build all $(CONFIG) +SWIFT_VERIFY_BUILD_CMD ?= $(CELL_TUNNEL_DEV) build all $(CONFIG) SWIFT_TEST_CMD ?= $(CELL_TUNNEL_DEV) test SWIFT_RUN_CMD ?= printf 'run: use a named target (%s)\n' '$(SWIFT_NAMED_RUN_HINT)'; exit 1 # The dev tool's `generate` installs Tuist dependencies and renders the project; it is @@ -70,6 +71,19 @@ ifneq ($(strip $(PROVISIONING_PROFILE_SPECIFIER)),) export TUIST_DEVELOPER_ID_SIGNING := 1 endif +# The Verify gate builds every platform through SWIFT_VERIFY_BUILD_CMD and verifies +# the actual signed output. SWIFT_MK_VERIFY_SIGNING_ROOTS makes the engine discover +# every runnable .app the build dropped under Products and check each one's signature, +# so the Mac agent, the Catalyst app, and the iPhone device app are all confirmed +# signed with the team and not ad-hoc, without listing paths. The engine skips the +# iPhone simulator app, which is ad-hoc by design. +# +# The pre-build settings check (SWIFT_MK_VERIFY_WORKSPACE/SCHEME) is intentionally +# unset: under automatic App Store Connect API-key signing the identity resolves at +# build time, so static xcodebuild -showBuildSettings reports CODE_SIGN_IDENTITY = - +# for targets that Xcode signs at build time. The signed identity is knowable only +# after the build, which the products check inspects directly. +SWIFT_MK_VERIFY_SIGNING_ROOTS := Products SWIFT_SOURCE_ROOTS := Apps Sources Tests Tools/CellTunnelCtl Tools/CellTunnelDev SWIFT_OWNED_SWIFT_FILES := $(shell find $(SWIFT_SOURCE_ROOTS) -path '*/.build/*' -prune -o -name '*.swift' -print) SWIFT_PACKAGE_MANIFESTS := Package.swift Project.swift Tuist.swift Tuist/Package.swift Tools/Package.swift Tools/cell-tunnel-dev.swift @@ -87,12 +101,22 @@ include bootstrap.mk .DEFAULT_GOAL := check -.PHONY: format iphone-install install-mac smoke logs \ +.PHONY: format iphone-install install-mac smoke logs ci-provision \ build-all build-mac build-catalyst build-iphone build-iphone-sim build-daemon \ run-catalyst run-iphone run-iphone-sim \ relay-up relay-reload relay-status relay-down \ mac-logs iphone-logs +# CI signing provisioning. CI and unregistered machines cannot use development +# provisioning (it requires a registered device), so this creates or renews one App +# Store distribution profile per target with the App Store Connect API key and installs +# it, for a manual-signed build. The engine runs this as the Verify job's setup step, +# before the build, with APPLE_NOTARY_* in the environment. See fastlane/Fastfile. +ci-provision: + @command -v bundle >/dev/null 2>&1 || gem install bundler --no-document + @bundle install --quiet + @bundle exec fastlane ios ci_provision + help:: @printf '\n%s\n' 'Cell Tunnel:' @printf ' %-40s %s\n' 'build-all' 'build every platform (same as make build)' diff --git a/Project.swift b/Project.swift index 9c9d118..09237a9 100644 --- a/Project.swift +++ b/Project.swift @@ -46,10 +46,9 @@ let moduleVerifierSettings: SettingsDictionary = [ "MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS": "gnu11 gnu++14", ] -// Tuist writes CODE_SIGN_IDENTITY = "-" (ad-hoc) at target level by default. The -// signing identity, team, and style are no longer forwarded here; swift-mk owns -// them through an XCODE_XCCONFIG_FILE override that wins over the target-level -// ad-hoc default for every target at once. +// Shared macOS runtime settings. Signing identity and style are added per target +// through developmentSigning below, which replaces Tuist's ad-hoc "-" target default +// for the development build; the team comes from the DEVELOPMENT_TEAM the build sets. let macHardenedRuntimeSettings: SettingsDictionary = [ "ENABLE_HARDENED_RUNTIME": "YES", "REGISTER_APP_GROUPS": "YES", @@ -63,14 +62,50 @@ let macHardenedRuntimeSettings: SettingsDictionary = [ // and for local builds, so the per-target specifiers below apply only then. let isDeveloperIdProvisioning = Environment.developerIdSigning.getBoolean(default: false) +// How each signable target signs, in two modes. The CI build (isDeveloperIdProvisioning, +// set by ci.yml) signs manually with an Apple Distribution certificate and an App Store +// provisioning profile pinned per target, because CI runs on machines that are not +// registered devices and only App Store profiles, which carry no device list, work +// there; fastlane creates and renews those profiles with the App Store Connect API key +// (see fastlane/Fastfile). Local builds sign with an Apple Development certificate and +// automatic provisioning against the developer's registered machine. The Catalyst slice +// needs the macOS-SDK identity set explicitly, because Tuist injects +// CODE_SIGN_IDENTITY[sdk=macosx*] = "-" for it, which is more specific than the generic +// key and would otherwise sign it ad-hoc. +let developmentSigning: SettingsDictionary = [ + "CODE_SIGN_IDENTITY": "Apple Development", + "CODE_SIGN_IDENTITY[sdk=macosx*]": "Apple Development", + "CODE_SIGN_STYLE": "Automatic", +] +let distributionSigning: SettingsDictionary = [ + "CODE_SIGN_IDENTITY": "Apple Distribution", + "CODE_SIGN_IDENTITY[sdk=macosx*]": "Apple Distribution", + "CODE_SIGN_STYLE": "Manual", +] +let baseSigning: SettingsDictionary = + isDeveloperIdProvisioning ? distributionSigning : developmentSigning + +// The App Store profile names fastlane creates for the iPhone app and the Mac Catalyst +// slice (which shares the iPhone bundle identifier), and for the iPhone tunnel. Applied +// only in the CI distribution build; local builds provision automatically. +let phoneProvisioning: SettingsDictionary = + isDeveloperIdProvisioning + ? [ + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]": "Managed AppStore CellTunnelPhone iOS", + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]": "Managed AppStore CellTunnelPhone Catalyst", + ] + : [:] +let phoneTunnelProvisioning: SettingsDictionary = + isDeveloperIdProvisioning + ? ["PROVISIONING_PROFILE_SPECIFIER": "Managed AppStore CellTunnelPhoneTunnel iOS"] + : [:] + // The two macOS NetworkExtension targets carry App Groups + Network Extensions // entitlements (the app-extension packet-tunnel-provider), so each needs its own -// provisioning profile; a single global override cannot express two specifiers, and -// Developer ID cannot authorize app-extension NetworkExtension on macOS. Pin each -// macOS-only target to its installed Mac App Store profile by name. Applied only in -// CI provisioning mode; locally the target keeps its automatic signing. +// provisioning profile. In the CI build, pin each macOS-only target to its App Store +// profile by name. Locally the target provisions automatically. func macNetworkExtensionSettings(profileName: String) -> SettingsDictionary { - var settings = macHardenedRuntimeSettings + var settings = macHardenedRuntimeSettings.merging(baseSigning) { _, new in new } if isDeveloperIdProvisioning { settings["PROVISIONING_PROFILE_SPECIFIER"] = SettingValue(stringLiteral: profileName) } @@ -87,14 +122,13 @@ let cellTunnelPhoneBaseSettings: SettingsDictionary = [ // entitlement. Adding .macCatalyst to the destinations turns on // SUPPORTS_MACCATALYST. "DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER": "NO", - // The Catalyst slice signs from a macOS-only entitlements file. The signing - // identity/team/style are not set per SDK slice here; swift-mk's - // XCODE_XCCONFIG_FILE override supplies them and wins over Tuist's per-target - // ad-hoc default, so the Catalyst slice signs with the development certificate - // its entitlements require. + // The Catalyst slice signs from a macOS-only entitlements file. Both the iPhone and + // Catalyst slices take their signing mode from baseSigning and, in the CI build, + // their App Store profile from phoneProvisioning. "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]": "$(SRCROOT)/Apps/iOS/Entitlements/CellTunnelPhone-Catalyst.entitlements", -] +].merging(baseSigning) { _, new in new } + .merging(phoneProvisioning) { _, new in new } let appDependencies: [TargetDependency] = [ .target(name: "CellTunnelCore"), @@ -198,7 +232,7 @@ let project = Project( .external(name: "WireGuardKit"), ], settings: .settings( - base: macNetworkExtensionSettings(profileName: "CellTunnel Agent App Store")) + base: macNetworkExtensionSettings(profileName: "Managed AppStore CellTunnelAgent")) ), .target( name: "CellTunnelTunnelProvider", @@ -213,7 +247,7 @@ let project = Project( entitlements: .file(path: "Apps/macOS/Entitlements/TunnelProvider.entitlements"), dependencies: tunnelProviderDependencies, settings: .settings( - base: macNetworkExtensionSettings(profileName: "CellTunnel TunnelProvider App Store")) + base: macNetworkExtensionSettings(profileName: "Managed AppStore CellTunnelTunnelProvider")) ), .target( name: "CellTunnelPhoneTunnel", @@ -229,7 +263,9 @@ let project = Project( path: "Apps/iOS/Entitlements/CellTunnelPhoneTunnel.entitlements" ), dependencies: appDependencies + [.target(name: "CellTunnelRelay")], - settings: .settings(base: ["REGISTER_APP_GROUPS": "YES"]) + settings: .settings( + base: ["REGISTER_APP_GROUPS": "YES"].merging(baseSigning) { _, new in new } + .merging(phoneTunnelProvisioning) { _, new in new }) ), ], schemes: [ diff --git a/README.md b/README.md index 012d794..3303967 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Cell Tunnel routes a Mac's internet traffic through an iPhone's native cellular `make help` lists engine targets, then the named Cell Tunnel targets. `make build` and `make build-all` compile every platform through CellTunnelDev. + Named targets build one platform: ```sh -make build-all make build-mac make build-catalyst make build-iphone @@ -18,7 +18,8 @@ make build-iphone-sim make build-daemon ``` -Continuous integration runs `make build` for the signed full build. +Continuous integration builds every platform in one Verify job, signs each +product, and verifies each runnable product's signature. `CONFIG` defaults to `Debug`. Pass `CONFIG=Release` when needed. @@ -35,6 +36,13 @@ to the interactive Xcode account. The macOS targets sign from `Config/local.xcconfig` (`DEVELOPMENT_TEAM`, `CODE_SIGN_IDENTITY`, `CODE_SIGN_STYLE`). +Continuous integration signs differently, because its runners are not registered +devices and development provisioning requires one. `make ci-provision` runs fastlane +to create or renew one App Store distribution profile per target through the App Store +Connect API key, and the build signs manually against those profiles. `Project.swift` +pins each profile by name when `TUIST_DEVELOPER_ID_SIGNING` is set. See +[fastlane/Fastfile](fastlane/Fastfile). + ## Install and run Install the Mac side, then install and launch the iPhone app: diff --git a/Tools/CellTunnelDev/ProcessSupport.swift b/Tools/CellTunnelDev/ProcessSupport.swift index b0d68d1..a8e65f0 100644 --- a/Tools/CellTunnelDev/ProcessSupport.swift +++ b/Tools/CellTunnelDev/ProcessSupport.swift @@ -82,8 +82,8 @@ final class PrefixedProcess: @unchecked Sendable { let lineQueue = DispatchQueue(label: "celltunnel.prefixed-process.\(executable)") let outBuffer = PrefixedLineBuffer(prefix: linePrefix) let errBuffer = PrefixedLineBuffer(prefix: linePrefix) - let stdoutEOF = DispatchSemaphore(value: 0) - let stderrEOF = DispatchSemaphore(value: 0) + let stdoutEndOfFile = DispatchSemaphore(value: 0) + let stderrEndOfFile = DispatchSemaphore(value: 0) // Appends and EOF signals share lineQueue so waitUntilExitAndDrain can flush only // after every in-flight append has finished (no post-flush tail loss). @@ -92,7 +92,7 @@ final class PrefixedProcess: @unchecked Sendable { handle: handle, buffer: outBuffer, queue: lineQueue, - eof: stdoutEOF + eof: stdoutEndOfFile ) } errPipe.fileHandleForReading.readabilityHandler = { handle in @@ -100,7 +100,7 @@ final class PrefixedProcess: @unchecked Sendable { handle: handle, buffer: errBuffer, queue: lineQueue, - eof: stderrEOF + eof: stderrEndOfFile ) } @@ -111,8 +111,8 @@ final class PrefixedProcess: @unchecked Sendable { queue: lineQueue, stdoutState: outBuffer, stderrState: errBuffer, - stdoutEOF: stdoutEOF, - stderrEOF: stderrEOF + stdoutEOF: stdoutEndOfFile, + stderrEOF: stderrEndOfFile ) ) } diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..e4c7610 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,68 @@ +# Fastfile: provisioning for CI code signing. +# +# CI and any unregistered machine cannot use development provisioning profiles, +# because those enumerate registered devices. App Store distribution profiles carry +# no device list, so they sign on any machine. This lane creates or renews one App +# Store profile per signable target through the App Store Connect API key and installs +# it, so a manual-signed `xcodebuild build` finds each profile by name. Project.swift +# pins these exact names for the CI (distribution) build. + +# Distinct "Managed AppStore ..." names so fastlane owns these profiles outright and +# never collides with any manually created profile of the same app identifier. +# Project.swift pins these exact names for the CI distribution build. +APP_STORE_PROFILES = [ + { app_identifier: "io.goodkind.CellTunnel.Agent", platform: "macos", name: "Managed AppStore CellTunnelAgent" }, + { app_identifier: "io.goodkind.CellTunnel.Agent.TunnelProvider", platform: "macos", name: "Managed AppStore CellTunnelTunnelProvider" }, + { app_identifier: "io.goodkind.CellTunnelPhone", platform: "ios", name: "Managed AppStore CellTunnelPhone iOS" }, + { app_identifier: "io.goodkind.CellTunnelPhone", platform: "macos", name: "Managed AppStore CellTunnelPhone Catalyst" }, + { app_identifier: "io.goodkind.CellTunnelPhone.Tunnel", platform: "ios", name: "Managed AppStore CellTunnelPhoneTunnel iOS" }, +].freeze + +require "base64" + +platform :ios do + desc "Create or renew the App Store provisioning profiles CI signs with, using the App Store Connect API key." + lane :ci_provision do + connect_token = app_store_connect_api_key( + key_id: ENV.fetch("APPLE_NOTARY_KEY_ID"), + issuer_id: ENV.fetch("APPLE_NOTARY_ISSUER_ID"), + key_content: ENV.fetch("APPLE_NOTARY_KEY_BASE64"), + is_key_content_base64: true, + in_house: false, + ) + + # The same key, as a spaceship token, so the lane can delete stale profiles. The + # account can hold more than one Apple Distribution certificate (a local one and + # the CI one), and a profile is bound to a specific certificate. A profile of the + # right name left over from a different certificate makes the build fail with + # "profile does not include signing certificate", so delete every profile of each + # name first, then recreate it bound to the certificate this build uses. + Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create( + key_id: ENV.fetch("APPLE_NOTARY_KEY_ID"), + issuer_id: ENV.fetch("APPLE_NOTARY_ISSUER_ID"), + key: Base64.decode64(ENV.fetch("APPLE_NOTARY_KEY_BASE64")), + ) + + APP_STORE_PROFILES.each do |profile| + target_name = profile[:name] + Spaceship::ConnectAPI::Profile.all.each do |existing| + next unless existing.name == target_name || existing.name.start_with?("#{target_name} ") + + UI.message("Deleting stale profile '#{existing.name}'") + existing.delete! + end + + # Default type is an App Store distribution profile: no device list, so it + # signs on an unregistered runner. force recreates it bound to the current + # certificate. + get_provisioning_profile( + api_key: connect_token, + app_identifier: profile[:app_identifier], + platform: profile[:platform], + provisioning_name: target_name, + force: true, + output_path: "build/fastlane-profiles", + ) + end + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..dbc5ca6 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,32 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios ci_provision + +```sh +[bundle exec] fastlane ios ci_provision +``` + +Create or renew the App Store provisioning profiles CI signs with, using the App Store Connect API key. + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).