fix macOS signing - #1820
Merged
Merged
Conversation
christianparpart
force-pushed
the
fix/macos-signing
branch
from
December 13, 2025 21:36
025f675 to
fafe58a
Compare
christianparpart
force-pushed
the
fix/macos-signing
branch
from
December 13, 2025 22:45
fafe58a to
399d7b4
Compare
christianparpart
force-pushed
the
fix/macos-signing
branch
2 times, most recently
from
December 14, 2025 07:55
16c506e to
68c89a2
Compare
christianparpart
force-pushed
the
fix/macos-signing
branch
3 times, most recently
from
December 18, 2025 13:34
d931301 to
b5ea841
Compare
Member
|
I guess that you want to close this one: #1541 |
Member
Author
i don't know man. macOS is giving me PTSD! We may need to decide to not provide macOS .dmg binary for the imminent release, to not hold it back any longer, and take some more time to get macOS packaging right. (who said Linux packaging is bad, they've never dealt with macOS before :D) |
christianparpart
marked this pull request as draft
February 18, 2026 16:59
christianparpart
force-pushed
the
fix/macos-signing
branch
from
July 30, 2026 08:37
b5ea841 to
747f48a
Compare
christianparpart
marked this pull request as ready for review
August 4, 2026 14:33
christianparpart
force-pushed
the
fix/macos-signing
branch
from
August 4, 2026 15:38
2676255 to
7971738
Compare
…cpkg The .dmg used to require whatever macOS the build host ran. Homebrew ships one prebuilt bottle per macOS release and installs the one matching the machine, so every library Contour linked carried that host's version as its LC_BUILD_VERSION minimum -- an invisible compatibility cliff that moved whenever the CI image was rotated. Build those libraries from source instead, through a vcpkg triplet that pins VCPKG_OSX_DEPLOYMENT_TARGET, and set CMAKE_OSX_DEPLOYMENT_TARGET before project(), which is where CMake consumes it. 13.3 is not arbitrary: it is the first macOS whose libc++ exports the floating-point std::to_chars that <format> needs, and we use std::format throughout. Homebrew Qt is no longer installed by default either -- it is split across ~40 per-module prefixes that macdeployqt cannot follow. Release builds use the official Qt; CONTOUR_INSTALL_BREW_QT=ON restores the old behaviour for dev builds that are never packaged. The autotools go in because some vcpkg ports on the way to fontconfig need them on the build host. The harfbuzz include_directories() hack is now guarded: it is global, and under vcpkg it would put Homebrew's headers ahead of the libraries we actually link. Signed-off-by: Christian Parpart <christian@parpart.family>
qt_add_executable() runs Qt's finalization: it imports the QML plugins the app actually uses and installs Qt's Info.plist template. That last part matters more than it looks -- CMake only falls back to searching CMAKE_MODULE_PATH for its own MacOSXBundleInfo.plist.in when MACOSX_BUNDLE_INFO_PLIST is unset, and Qt sets it. Our copy of that template was therefore never read by anything, so it goes, along with the two properties Qt's template does not substitute (CFBundleGetInfoString and CFBundleLongVersionString, both deprecated by Apple regardless). Qt's template also supplies LSMinimumSystemVersion from CMAKE_OSX_DEPLOYMENT_TARGET, which is how the declared floor stays tied to the one the build actually targets, and omits NSHighResolutionCapable, whose absence is the enabled state. MACOSX_BUNDLE_ICON_FILE is set at last: the .icns was already installed to Contents/Resources, but with CFBundleIconFile empty both the app and the disk image showed the generic executable icon. Signed-off-by: Christian Parpart <christian@parpart.family>
Gatekeeper rejected every .dmg this project produced, for five independent reasons: the bundle was missing frameworks, it was signed with --deep, it had no hardened runtime, it was never notarized, and the disk image itself was neither signed nor stapled. Any one of those is fatal to distribution. scripts/macos-bundle.py carries the four steps that fix it. `verify` walks every Mach-O in the bundle, resolves each @rpath/@loader_path/@executable_path reference against the referring image's LC_RPATH *and* the main executable's, and fails on anything unresolvable or pointing outside the bundle -- which is what turns macdeployqt's silent breakage into a build failure, since it reports "Cannot resolve rpath" on stderr and still exits 0. `prune` drops the plugin categories that can never load, from an allowlist, so an unexpected one fails loudly instead of vanishing. `sign` strips extended attributes and signs inside-out with --options=runtime and a secure timestamp, entitlements on the outermost signature only. `notarize` submits, waits, and staples. verify also reports the highest minimum-OS across the bundled binaries, and CONTOUR_MACOS_MIN_SUPPORTED turns that report into an assertion: an artifact users could not launch now fails the build instead of reaching them. CPack runs after install(), so the image needs its own pass -- the pre-build hook notarizes and staples the staged app before hdiutil wraps it (the ticket has to be inside the image for the dragged-out copy to launch offline), and the post-build hook signs, notarizes and staples the .dmg. Every execute_process() in the install path gains COMMAND_ERROR_IS_FATAL ANY. A failed macdeployqt or codesign used to produce a green build and a broken .dmg. Signed-off-by: Christian Parpart <christian@parpart.family>
There were two ways to produce a .dmg and they disagreed. Whatever CI passed on the command line was the configuration users downloaded, and it was exercised nowhere else; the local package preset was a different build that nobody shipped. `macos-package` is now the only package preset: official Qt, the vcpkg triplet, notarization and stapling on, CONTOUR_MACOS_MIN_SUPPORTED=13.3, and CONTOUR_INSTALL_TOOLS=OFF so bench-headless stays out of a user-facing bundle. appleclang-release keeps its Homebrew Qt for day-to-day work and loses its package preset, which was the second source of truth. The Developer ID moves into a hidden `macos-signing` preset that all three inherit, so the identity is written once. Signed-off-by: Christian Parpart <christian@parpart.family>
The job configured its own build by hand, so the .dmg users downloaded came from a configuration nothing else ran. It now uses the macos-package preset and overrides only what a preset cannot know: which identity this run's keychain holds, and whether notarization secrets exist. Signing is gated on the secrets being reachable rather than on the branch name. GitHub withholds secrets from fork pull requests, which is the real precondition; the branch test approximated it badly in both directions -- fork PRs asked codesign for a Developer ID that was in no keychain, breaking every PR run, while the signing path on a release branch went unexercised until it was already on master. It has to be proven before the merge. The keychain import gains -T /usr/bin/codesign and set-key-partition-list, without which codesign blocks on a GUI prompt that never arrives and the job hangs until timeout. Notarization uses an App Store Connect API key: no personal account, scopable to the Developer role, revocable on its own. The runner is pinned to macos-15 so an image rotation cannot silently change what ships, and Qt comes from install-qt-action as it already did on Linux and Windows. Finally, an assertion that the artifact is something a user can open: mount the image, drag the app out as a user would, mark the copy quarantined, and require stapler validate and spctl --type exec to accept it. A ticket stapled only to the .dmg does not travel with that copy. A green package step used to mean only that hdiutil had succeeded. The DMG is also named from VERSION_STRING, matching the .deb -- `version` omits the -prerelease suffix, so prereleases published a .dmg whose name claimed otherwise. Signed-off-by: Christian Parpart <christian@parpart.family>
The existing note recommended `codesign --force --deep`, which is the practice this branch removed. Replaced with the actual procedure: how to obtain and store the Developer ID and the App Store Connect key, why release builds use vcpkg and official Qt rather than Homebrew, the two macdeployqt failure modes worth knowing, how the minimum-macOS floor is set and enforced, and a troubleshooting table for the errors that are hard to search for -- including why the entitlements plist carries no comments (AMFI rejects XML comments outright). Added to the mkdocs nav, where the old page was never listed. Signed-off-by: Christian Parpart <christian@parpart.family>
christianparpart
force-pushed
the
fix/macos-signing
branch
from
August 4, 2026 15:52
7971738 to
1dda01a
Compare
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.
again and again 🗡️