Add a double-clickable macOS app bundle - #5
Open
JYPochez wants to merge 6 commits into
Open
Conversation
SwiftPM has no .app product type, so until now the app could only be launched
from a terminal via run.sh. Add two equivalent build paths that each produce a
real, self-contained "AirPort Utility.app":
./make-app.sh [--sign] [--notarize] [--zip] shell script, no Xcode needed
open AirPortUtility.xcodeproj scheme "AirPort Utility App"
Both build a universal binary (arm64 + x86_64) and embed the Python backend so
the bundle runs without a source checkout beside it.
Several bundling constraints drove the layout:
* The backend lives in Contents/Resources, not Contents/MacOS. codesign treats
everything in Contents/MacOS as code and fails verification on non-Mach-O
files there ("code object is not signed at all"). This is the only change
that touches shared source: defaultRepoPath() now also resolves the backend
through Bundle.main.resourceURL.
* Info.plist declares NSLocalNetworkUsageDescription and NSBonjourServices. On
macOS 15+ a bundled app is its own TCC principal, and without these Bonjour
discovery silently returns nothing and ACP connections to port 5009 fail.
Running from a terminal masks this, because there the permission belongs to
the terminal.
* LSEnvironment sets PATH, because a Finder-launched app inherits a minimal one
and "#!/usr/bin/env python3" would resolve to /usr/bin/python3, the Command
Line Tools stub (3.9.6). The backend's suite does pass there, but a real
interpreter is preferred when installed.
* LSEnvironment also sets PYTHONDONTWRITEBYTECODE, since Python would otherwise
write __pycache__ beside the bundled backend and invalidate the signature.
* The entitlements file is intentionally empty: the app is not sandboxed, and
Hardened Runtime comes from `codesign --options runtime`. Note that AMFI
rejects XML comments inside an entitlements plist, so the rationale lives in
Packaging/README.md.
Package.swift gains a library product for AirPortUtilityCore so the Xcode
target can link it as a local package product instead of duplicating the
source list.
Tagging v* runs .github/workflows/release.yml, which builds, signs, notarizes,
staples, verifies and attaches the zip to a GitHub release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
Package.swift declares swift-tools-version 6.0, but the project did not build
on Swift 6.0 -- only on the newer toolchain most contributors happen to have.
CI surfaced it on an Xcode 16 runner:
error: call can throw, but it is not marked with 'try'
accounts += selectedTopologyDeviceIdentifiers.compactMap(Self.passwordStoreAccount)
passwordStoreAccount is a plain non-throwing "-> String?". The problem is that
passing it as a *function value* to the nonisolated rethrows compactMap crosses
an actor boundary, since it inherits @mainactor from AirportAppModel. Swift 6.0
reports that as the argument being able to throw; 6.2 accepts it.
Calling it from inside a closure keeps the call in the isolated context, so no
function value crosses the boundary. Behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
This reverts commit d4fb703.
The release workflow pinned Xcode 16, whose Swift 6.0 rejects concurrency patterns this project uses -- passing actor-isolated methods as function values to nonisolated generics, in AirPortCommandRunner, AirPortServices, TopologyView and AirportAppModelPasswords. None of that is specific to this branch; the project simply needs a newer toolchain than the pin allowed. Selects the highest-numbered Xcode present instead, and logs what was available so a future mismatch is diagnosable from the run alone. This supersedes the previous commit, which patched one of those call sites. That was treating a symptom: the other three remained, so the project still did not build on Swift 6.0, and the change was unrelated churn in shared code. Reverted.
The file holds a fork's own bundle identifier and signing team and is meant to stay untracked. The ignore rule lived only on a later branch, so on this one it was not ignored at all and got committed. Adds the rule here, where Packaging/ is introduced, so it applies to every branch built on this one.
Owner
|
Still making my way through this PR. I appreciate the effort and thought you put into making the icon and it gave me a chuckle, but would it be alright if we instead reuse the AirPort Utility icon instead? If Apple complains about this, then we can move to another icon design at that point. |
Author
|
Hi, I made effort NOT to use any apple logo or part of logo intentionnaly, because I'm sure the minute you publish it you get an army of lawyers sending you cease&desists in your mailbox ... |
Tagging a release in a repository without the signing secrets failed at the certificate import, leaving a red run on every tag. That is the state any fork starts in, including this one, so the workflow reported a problem where there was none. The credential-dependent steps are now skipped when MACOS_CERTIFICATE is unset: the run builds, tests and stops, and emits a notice saying signing and publishing were skipped and why. With the secrets configured it behaves exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146wivD92uwBMRnBFjF6cDh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SwiftPM has no
.appproduct type, so the app could previously only be launchedfrom a terminal through
run.sh. This adds two equivalent build paths that eachproduce a real, self-contained
AirPort Utility.app:Both build a universal binary (
arm64+x86_64) and embed the Python backend,so the bundle runs without a source checkout beside it. They share one
Packaging/Info.plist: Xcode substitutes the$(VAR)placeholders natively andthe script substitutes them with
sed, so the two cannot drift apart.Why the layout is what it is
Four constraints shaped this, each found by building and running rather than by
inspection:
The backend lives in
Contents/Resources, notContents/MacOS.codesigntreats everything in
Contents/MacOSas code and fails verification onnon-Mach-O files there (
code object is not signed at all). This is the onlychange that touches shared source:
defaultRepoPath()now also resolves thebackend through
Bundle.main.resourceURL.Info.plistdeclaresNSLocalNetworkUsageDescriptionandNSBonjourServices. On macOS 15+ a bundled app is its own TCC principal.Without these, Bonjour discovery silently returns nothing and ACP connections to
port 5009 fail. Running from a terminal hides the problem, because there the
permission belongs to the terminal.
LSEnvironmentsetsPATH. A Finder-launched app inherits a minimal one, so#!/usr/bin/env python3resolves to/usr/bin/python3— the Command Line Toolsstub, Python 3.9.6. The backend's full test suite does pass on 3.9.6, but this
prefers a real interpreter when one is installed.
LSEnvironmentalso setsPYTHONDONTWRITEBYTECODE. Python would otherwisewrite
__pycache__beside the bundled backend on import, mutating a sealedresource and invalidating the code signature.
Notes for review
Hardened Runtime comes from
codesign --options runtimerather than anentitlement. AMFI rejects XML comments inside an entitlements plist, so the
rationale lives in
Packaging/README.md.Package.swiftgains a library product forAirPortUtilityCoreso the Xcodetarget can link it as a local package product instead of duplicating the
source list.
Packaging/Base.xcconfigwith an optional#include? "Local.xcconfig", so a fork can set its own identifier withoutmodifying a tracked file. A clone without that file builds with the default.
.github/workflows/release.ymlbuilds, signs, notarizes, staples, verifies andattaches a zip on
v*tags. It is inert without the six repository secretslisted at the top of the file.
Testing
swift testand the Python backend suite pass unchanged.resolve both
Bundle.moduleresources and the embedded backend, and keep itssignature intact after running.
archivepath produces the same bundle, signed with Developer ID andHardened Runtime, passing
codesign --verify --strict.