From da6c96585085929d9b92002b3b335c86d6a1e4a2 Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:00:53 +0200 Subject: [PATCH 1/9] Build a disk image for each architecture The disk image was built on macos-latest, which is Apple silicon, and a Mach-O holds one architecture - as do the Homebrew libraries dylibbundler puts beside it. An Intel Mac got "Bad CPU type in executable", because Rosetta translates x86_64 to arm64 and not the other way about. Build on a runner of each kind instead and name each image for what it holds. The runners are pinned rather than macos-latest for a second reason: every Homebrew bottle is built for the OS of the machine that installed it, so the runner decides the lowest macOS a release can run on, and macos-latest moves that floor with every new macOS. That floor was not being told either - Info.plist claimed 10.13 while the binary asked for 26.0, so an older Mac got dyld failing over a library rather than LaunchServices naming the macOS it needs. Read it from the binary's LC_BUILD_VERSION and write what is true. --- .github/workflows/build.yml | 62 ++++++++++++++++++++++++++++++++----- Makefile | 22 +++++++++++-- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8d13ab..33b84d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,11 +219,12 @@ jobs: files: avanor-[0-9]*.exe fail_on_unmatched_files: true + # Compiles and tests every configuration, on whatever macOS is newest, + # so a break there is seen when it lands. The release images are built + # by macos-dmg below, on runners pinned for the reasons given there. macos: name: macos ${{ matrix.configuration || 'release' }} runs-on: macos-latest - permissions: - contents: write strategy: fail-fast: false matrix: @@ -231,7 +232,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Install dependencies - run: brew install argparse cereal coreutils create-dmg dylibbundler fmt luajit make notcurses pkgconf sol2 wget zstd + run: brew install argparse cereal coreutils fmt luajit make notcurses pkgconf sol2 wget zstd # gmake, not make: macOS still ships GNU Make 3.81 from 2006, which # reads a "#" inside $(shell ...) as the start of a comment and cuts # the line in half. Homebrew's is the same 4.x every other platform @@ -246,14 +247,59 @@ jobs: CONFIGURATION: ${{ matrix.configuration }} run: ./.github/run-tests.sh - # As on Windows: only on a tag, and only from the release build of - # the default backend, since all four would write the same file. + # The disk images, one per architecture, on a tag only. + # + # A Mach-O built here holds one architecture and the Homebrew libraries + # bundled beside it hold the same one, so an image made on an Apple + # silicon runner does not run on an Intel Mac at all - Rosetta goes from + # x86_64 to arm64, not back. Hence a runner of each kind rather than the + # `macos-latest` the job above uses. + # + # The OS is pinned for the same reason it is named: every Homebrew + # bottle is built for the OS of the machine that installed it, so the + # runner image decides the lowest macOS the release can run on, and + # `macos-latest` quietly moves that floor up with every new release of + # macOS. macos-15-intel is served until August 2027, after which the + # x86_64 half of this matrix goes away with it. + macos-dmg: + name: macos disk image (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + if: github.ref_type == 'tag' + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + arch: arm64 + - runner: macos-15-intel + arch: x86_64 + steps: + - uses: actions/checkout@v7 + - name: Install dependencies + run: brew install argparse cereal create-dmg dylibbundler fmt luajit make pkgconf sol2 wget zstd + - name: Build + run: gmake -j$(sysctl -n hw.ncpu) + - name: Check the binary starts + run: ./avanor --version + + # The runner labels decide which architecture each half of the + # matrix builds, and a label that quietly changes what it points at + # would otherwise put the same slice in both images under two + # different names. Say out loud which one was expected. + - name: Check it is the architecture this job is for + run: | + built=$(lipo -archs ./avanor) + if [ "$built" != "${{ matrix.arch }}" ]; then + echo "::error::${{ matrix.runner }} built $built, expected ${{ matrix.arch }}" + exit 1 + fi + - name: Build the disk image - if: github.ref_type == 'tag' && matrix.configuration == '' run: gmake dmg - name: Add it to the release - if: github.ref_type == 'tag' && matrix.configuration == '' uses: softprops/action-gh-release@v3 with: - files: avanor-[0-9]*.dmg + files: avanor-[0-9]*-${{ matrix.arch }}.dmg fail_on_unmatched_files: true diff --git a/Makefile b/Makefile index ed385e8..dbeffef 100644 --- a/Makefile +++ b/Makefile @@ -329,7 +329,14 @@ installer: $(NAME) mainfiles.nsh datafiles.nsh avanor.nsi # dylibbundler copies the Homebrew libraries the binary needs into the # bundle and rewrites its load paths to point inside it, so the .dmg runs # on a Mac that has never seen Homebrew. -DMG := avanor-$(VERSION).dmg +# +# The image is named for the architecture it holds, because it only ever +# holds the one: the binary and the Homebrew libraries beside it are all +# single-slice, and an arm64 build does not start on an Intel Mac - +# Rosetta translates x86_64 to arm64 and not the other way about. CI +# builds one image on a runner of each kind and puts both on the release. +ARCH := $(shell uname -m) +DMG := avanor-$(VERSION)-$(ARCH).dmg DMGROOT := dmgroot APPDIR := $(DMGROOT)/Avanor.app @@ -359,10 +366,21 @@ dmg: $(NAME) resources/Avanor.icns "Add :CFBundleShortVersionString string $(VERSION_NUMBER)" \ $(APPDIR)/Contents/Info.plist + # What the bundle claims to need has to be what it was built against. + # Every Homebrew bottle is compiled for the OS of the machine it was + # installed on, so the floor is whatever the build machine ran, and + # LC_BUILD_VERSION is where that is written down. Promising less than + # the truth does not widen the audience, it only swaps LaunchServices + # naming the macOS this needs for dyld failing over a library. + /usr/libexec/PlistBuddy -c \ + "Set :LSMinimumSystemVersion $$(otool -l $(NAME) \ + | awk '/LC_BUILD_VERSION/ { f = 1 } f && /minos/ { print $$2; exit }')" \ + $(APPDIR)/Contents/Info.plist + cp -p COPYING CHANGELOG.md README.md $(DMGROOT)/ create-dmg \ - --volname "Avanor $(VERSION)" \ + --volname "Avanor $(VERSION) $(ARCH)" \ --volicon resources/Avanor.icns \ --window-pos 400 100 \ --window-size 480 360 \ From 9c2b8538d7b48b93994970e7df623b5b2c00a9ec Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:11:18 +0200 Subject: [PATCH 2/9] Say in the disk image window what macOS will do The window showed the application, a folder to drag it into and three documents, and said nothing. What actually happens on a double click is that Gatekeeper refuses the application, because nothing here is signed, and macOS 15 withdrew the Control-click shortcut that used to get past that - System Settings is now the whole of the way through. Somebody meeting that has had no warning and no instructions. Draw a backdrop that gives them both: what to do with the icons above, and underneath, what the refusal will look like and where to go. Two of them, so that a signed image - when there is one - does not carry a warning that no longer applies. The picture is generated rather than drawn, from the game's own colours, the way resources/make-icon.sh generates the icons, and committed beside them. The icons moved right to make this work. Finder will not place an icon within 114 pixels of the left edge, and it shifts every icon in the window rather than clamp the one - so the README at 70 was quietly sliding the whole layout 44 pixels out from under the backdrop. --- Makefile | 25 ++++-- resources/dmg-background-unsigned.png | Bin 0 -> 32421 bytes resources/dmg-background.png | Bin 0 -> 14123 bytes resources/make-background.sh | 117 ++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 resources/dmg-background-unsigned.png create mode 100644 resources/dmg-background.png create mode 100755 resources/make-background.sh diff --git a/Makefile b/Makefile index dbeffef..2315463 100644 --- a/Makefile +++ b/Makefile @@ -335,8 +335,12 @@ installer: $(NAME) mainfiles.nsh datafiles.nsh avanor.nsi # single-slice, and an arm64 build does not start on an Intel Mac - # Rosetta translates x86_64 to arm64 and not the other way about. CI # builds one image on a runner of each kind and puts both on the release. +# +# The backdrop tells the user what to do, and - until the image is signed +# - what macOS is about to say when they do it. See make-background.sh. ARCH := $(shell uname -m) DMG := avanor-$(VERSION)-$(ARCH).dmg +DMG_BACKGROUND := resources/dmg-background-unsigned.png DMGROOT := dmgroot APPDIR := $(DMGROOT)/Avanor.app @@ -379,18 +383,29 @@ dmg: $(NAME) resources/Avanor.icns cp -p COPYING CHANGELOG.md README.md $(DMGROOT)/ + # The window and the backdrop behind it are one layout, drawn by + # resources/make-background.sh from these same coordinates - move an + # icon here and the arrow there points at nothing. The taller window + # is what makes room for what the backdrop has to say underneath. + # + # No icon is nearer the left edge than 114: Finder will not put one + # there, and it shifts the whole window's worth of icons rather than + # clamp the one, which is how a README at 70 drags everything else + # out from under the backdrop. See the note in make-background.sh. create-dmg \ --volname "Avanor $(VERSION) $(ARCH)" \ --volicon resources/Avanor.icns \ + --background $(DMG_BACKGROUND) \ --window-pos 400 100 \ - --window-size 480 360 \ + --window-size 480 460 \ --icon-size 64 \ - --icon "Avanor.app" 110 130 \ + --icon "Avanor.app" 130 130 \ --hide-extension "Avanor.app" \ --app-drop-link 350 130 \ - --icon "README.md" 70 260 \ - --icon "CHANGELOG.md" 190 260 \ - --icon "COPYING" 310 260 \ + --icon "README.md" 120 260 \ + --icon "CHANGELOG.md" 240 260 \ + --icon "COPYING" 360 260 \ + --no-internet-enable \ $(DMG) $(DMGROOT)/ $(RM) -r $(DMGROOT) diff --git a/resources/dmg-background-unsigned.png b/resources/dmg-background-unsigned.png new file mode 100644 index 0000000000000000000000000000000000000000..48a43c011fc659f551049890b5472ba665fbcb2a GIT binary patch literal 32421 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 + echo " fontconfig offered: $file" >&2 + echo " Install the DejaVu family and run this again -" >&2 + echo " on macOS: brew install --cask font-dejavu" >&2 + exit 1 + ;; + esac + + echo "$file" +} + +sans=$(font_for "DejaVu Sans") +sans_bold=$(font_for "DejaVu Sans:bold") + +# The arrow runs between the two icons of the top row, stopping clear of +# both: 64-pixel icons centred on icon_row reach 32 to either side, and a +# little more is left so it points at the folder rather than into it. +arrow_from=$((app_x + 46)) +arrow_to=$((applications_x - 58)) +head=$((arrow_to + 16)) + +# ImageMagick stamps the hour it ran into the PNG, which would make +# every regeneration a diff with no visible change in it. -strip and +# excluding the date and time chunks leave the file a function of this +# script alone, so `git diff` after a run means the picture really moved. +plain='-strip -define png:exclude-chunk=date,time' + +draw_common() { + magick -size ${width}x${height} xc:"$paper" \ + -font "$sans_bold" -fill "$ink" -pointsize 17 \ + -gravity North -annotate +0+24 'Drag Avanor into Applications' \ + -stroke "$ink" -strokewidth 3 \ + -draw "line $arrow_from,$icon_row $arrow_to,$icon_row" \ + -stroke none -fill "$ink" \ + -draw "polygon $arrow_to,$((icon_row - 9)) $arrow_to,$((icon_row + 9)) $head,$icon_row" \ + -font "$sans" -fill "$dim" -pointsize 10 \ + -gravity South -annotate +0+18 \ + 'Avanor is free software under the GPL, version 2 or later.' \ + $plain "$1" +} + +# The signed image: nothing to warn about, so nothing is said. +draw_common "$out" + +# The unsigned one. What it describes is the only way through on macOS 15 +# and later, which withdrew the Control-click shortcut that used to open +# an unsigned application - System Settings is now the whole of it. +magick "$out" \ + -stroke "$dim" -strokewidth 1 -draw "line 60,330 420,330" \ + -stroke none \ + -font "$sans_bold" -fill "$ink" -pointsize 13 \ + -gravity North -annotate +0+350 'First launch' \ + -font "$sans" -fill "$dim" -pointsize 11 \ + -gravity North \ + -annotate +0+376 'macOS will refuse to open Avanor: it is not signed by a' \ + -annotate +0+392 'registered Apple developer. Open System Settings > Privacy' \ + -annotate +0+408 '& Security, scroll to the bottom, and click "Open Anyway".' \ + $plain "$out_unsigned" + +echo "wrote $out and $out_unsigned" From 39803a95113e0eab70c7a4e817df15ca2c8ffd11 Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:16:10 +0200 Subject: [PATCH 3/9] Let the game open its own Terminal The bundle's executable was a shell script beside the game, which opened Terminal on it. That left the executable macOS approves and the executable that actually runs as two different files, and the second one is a plain Mach-O: nothing that can hold a notarisation ticket. So it was assessed on its own, and a lone Mach-O can only be assessed by asking Apple over the network - approve Avanor, go offline, open it again, and it would not start. Move that into the game, behind __CFBundleIdentifier, which LaunchServices sets to the bundle it is starting and a shell never sets at all. Finder now runs the same file it approved, and the script is gone. --- Main.cpp | 65 +++++++++++++++++++++++++++++++++++++++ Makefile | 7 ++--- resources/Info.plist | 12 +++++--- resources/avanor-launcher | 18 ----------- 4 files changed, 76 insertions(+), 26 deletions(-) delete mode 100755 resources/avanor-launcher diff --git a/Main.cpp b/Main.cpp index 33ce71e..a7f132a 100644 --- a/Main.cpp +++ b/Main.cpp @@ -27,6 +27,12 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #include #include +#ifdef __APPLE__ + #include + #include + #include +#endif + #include #include #include @@ -587,8 +593,67 @@ void ShowLogo() XGame Game; +#ifdef __APPLE__ +// Avanor draws on a terminal, and an application opened from Finder has +// none. What used to bridge that was a shell script beside the game in +// the bundle, with the script as the bundle's executable; the trouble is +// that this left the game a plain executable standing next to the thing +// macOS had approved rather than being it, and a plain executable can +// carry no notarisation ticket of its own - so it was assessed a second +// time, on its own, and that assessment can only be answered by asking +// Apple over the network. Approving Avanor and then opening it again +// with no network would fail. +// +// So the bundle's executable is the game, and the game is what hands +// itself to Terminal. +// +// LaunchServices sets __CFBundleIdentifier to the bundle it is starting, +// which is how this tells being opened from Finder from being run in a +// shell. Terminal sets the variable too - to its own identifier - so the +// copy it goes on to run does not bounce back here, and neither does +// `./avanor --version` in CI, which has no bundle and no terminal. +static void vRelaunchInTerminal() +{ + // Must match CFBundleIdentifier in resources/Info.plist. + static const char* const BundleId = "com.avanor.avanor"; + + const char* const launched = getenv("__CFBundleIdentifier"); + + if (launched == nullptr || strcmp(launched, BundleId) != 0) { + return; + } + + if (isatty(STDOUT_FILENO)) { + return; + } + + char path[PATH_MAX]; + uint32_t size = sizeof(path); + + if (_NSGetExecutablePath(path, &size) != 0) { + return; + } + + execl("/usr/bin/open", "open", "-a", "Terminal", path, + static_cast(nullptr)); + + // Only reached if open could not be started at all. Carrying on would + // draw the game where nobody can see it, so say why instead. + std::cerr << "avanor: cannot open Terminal to play in" << std::endl; + + exit(1); +} +#endif + + int main(int argc, char* argv[]) { +#ifdef __APPLE__ + // Before the arguments are read: opened from Finder there are none + // worth reading, and this does not come back. + vRelaunchInTerminal(); +#endif + argparse::ArgumentParser program("avanor", GAME_VERSION_FULL); program.add_description("Avanor, the Land of Mystery - a roguelike."); diff --git a/Makefile b/Makefile index 2315463..fd7e34c 100644 --- a/Makefile +++ b/Makefile @@ -323,8 +323,9 @@ installer: $(NAME) mainfiles.nsh datafiles.nsh avanor.nsi # # The bundle keeps the game in Contents/MacOS and its world and manual in # Contents/Resources, which is the layout vEnterDataDir() knows to look -# for. What Finder runs is resources/avanor-launcher rather than the game -# itself - see the comment in that file. +# for. What Finder runs is the game itself, which opens a Terminal to +# draw in when it finds it was started without one - see +# vRelaunchInTerminal() in Main.cpp. # # dylibbundler copies the Homebrew libraries the binary needs into the # bundle and rewrites its load paths to point inside it, so the .dmg runs @@ -352,8 +353,6 @@ dmg: $(NAME) resources/Avanor.icns mkdir -p $(APPDIR)/Contents/MacOS $(APPDIR)/Contents/Resources cp -p $(NAME) $(APPDIR)/Contents/MacOS/ - cp -p resources/avanor-launcher $(APPDIR)/Contents/MacOS/ - chmod +x $(APPDIR)/Contents/MacOS/avanor-launcher dylibbundler -cd -b -of \ -x $(APPDIR)/Contents/MacOS/$(NAME) \ diff --git a/resources/Info.plist b/resources/Info.plist index f21cf80..c9f3a40 100644 --- a/resources/Info.plist +++ b/resources/Info.plist @@ -6,11 +6,15 @@ English CFBundleDisplayName Avanor - + CFBundleExecutable - avanor-launcher + avanor CFBundleIconFile Avanor.icns CFBundleIdentifier diff --git a/resources/avanor-launcher b/resources/avanor-launcher deleted file mode 100755 index 0aa30ee..0000000 --- a/resources/avanor-launcher +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# -# What Finder runs when somebody opens Avanor.app. -# -# Avanor draws on a terminal, and an application launched from Finder does -# not have one - so rather than the game itself, the bundle's executable is -# this, which opens Terminal and starts the game there. The game finds its -# world and manual by looking beside itself and then in ../Resources, so it -# does not matter what directory Terminal happens to start in. -# -# The game binary keeps its own name beside this one, so what Terminal -# shows in its title bar is "avanor" rather than a launcher. - -set -eu - -here=$(cd "$(dirname "$0")" && pwd) - -exec /usr/bin/open -a Terminal "$here/avanor" From fa670fa08d7f67dfabf5d93d20f8296ae285e850 Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:41:42 +0200 Subject: [PATCH 4/9] Sign and notarise the disk image, when there is a key Nothing in the image is signed, so Gatekeeper refuses it. Add the signing and notarisation, switched on by CODESIGN_ID and NOTARY_PROFILE: with neither set - a fork, a pull request, anybody's own machine - this builds precisely the unsigned image it built before, because the signing collapses to ":" rather than into a second path that has to be kept working. A key can be added later without touching any of this again. Two entitlements, because notarisation means the hardened runtime and under it LuaJIT may not run what it compiles. allow-jit covers MAP_JIT, which the LuaJIT Homebrew ships does not use; allow-unsigned-executable- memory covers what it does. Both, so it keeps working either way. The entitlements go on the bundle rather than on the game inside it: signing a bundle signs its executable too, and doing the executable first left a signature that verified perfectly and had no entitlements left on it at all. Signed and notarised twice, application and image. The image's ticket stays behind when the application is dragged to /Applications, and only the one stapled into the application is there when it is opened offline. --- Makefile | 65 +++++++++++++++++++++++++++++++++-- resources/avanor.entitlements | 31 +++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 resources/avanor.entitlements diff --git a/Makefile b/Makefile index fd7e34c..32f098b 100644 --- a/Makefile +++ b/Makefile @@ -337,11 +337,35 @@ installer: $(NAME) mainfiles.nsh datafiles.nsh avanor.nsi # Rosetta translates x86_64 to arm64 and not the other way about. CI # builds one image on a runner of each kind and puts both on the release. # -# The backdrop tells the user what to do, and - until the image is signed -# - what macOS is about to say when they do it. See make-background.sh. +# Signing is off unless CODESIGN_ID names a Developer ID Application +# identity, and notarisation off unless NOTARY_PROFILE also names a +# notarytool keychain profile. Neither is set in a fork, in a pull +# request, or on anybody's own machine, and without them this builds +# precisely the unsigned image it always has: there is no second path +# kept alive for the unsigned case, only this one with the signing +# collapsed to `:`, the shell command that does nothing with whatever it +# is handed. See SIGNING.md for where the two values come from. +CODESIGN_ID ?= +NOTARY_PROFILE ?= + +sign = $(if $(CODESIGN_ID),codesign --force --timestamp --options runtime \ + --sign "$(CODESIGN_ID)",:) +notarise = $(if $(NOTARY_PROFILE),xcrun notarytool submit \ + --keychain-profile "$(NOTARY_PROFILE)" --wait,:) +staple = $(if $(NOTARY_PROFILE),xcrun stapler staple,:) + ARCH := $(shell uname -m) DMG := avanor-$(VERSION)-$(ARCH).dmg -DMG_BACKGROUND := resources/dmg-background-unsigned.png + +# The backdrop tells the user what to do, and - when the image is not +# signed - what macOS is about to say when they do it. A signed image +# gets the one without that warning, since it does not apply to it. See +# make-background.sh. +DMG_BACKGROUND := resources/dmg-background$(if $(CODESIGN_ID),,-unsigned).png + +# Notarisation is handed a zip rather than the bundle. ditto, not zip: +# zip(1) drops the extended attributes and symlinks a bundle is made of. +NOTARY_ZIP := avanor-$(VERSION)-$(ARCH)-app.zip DMGROOT := dmgroot APPDIR := $(DMGROOT)/Avanor.app @@ -380,6 +404,39 @@ dmg: $(NAME) resources/Avanor.icns | awk '/LC_BUILD_VERSION/ { f = 1 } f && /minos/ { print $$2; exit }')" \ $(APPDIR)/Contents/Info.plist + # Signing runs from the inside out, and has to: Contents/MacOS is a + # place macOS looks for nested code, so the bundle's own signature + # records those by the hash of *their* signatures - which therefore + # have to exist first. Nothing may be touched afterwards either, which + # is why dylibbundler rewrote the load paths further up and not here. + # + # No --deep. It has been deprecated for signing since macOS 13, and it + # would apply these entitlements to the three libraries as well, which + # is not what they are for. + @for lib in $(APPDIR)/Contents/MacOS/libs/*.dylib; do \ + echo $(sign) "$$lib"; $(sign) "$$lib" || exit 1; \ + done + # The entitlements go on the bundle, not on the game beneath it. + # Signing a bundle signs its executable too, and would throw away a + # signature already put there - entitlements and all - leaving + # something that verifies perfectly and has none of them. Since the + # game is now the bundle's executable rather than a file beside one, + # this signs both at once and is the only place they can be given. + $(sign) --entitlements resources/avanor.entitlements $(APPDIR) + $(if $(CODESIGN_ID),codesign --verify --deep --strict --verbose=2 $(APPDIR),:) + + # Notarised twice, and stapled twice, because the two tickets are not + # the same ticket. The one on the disk image is left behind the moment + # the application is dragged out of it; only the one stapled into the + # application travels to /Applications, and that is the copy that has + # to open on a machine that is offline. + $(if $(NOTARY_PROFILE),ditto -c -k --keepParent $(APPDIR) $(NOTARY_ZIP)) + $(notarise) $(NOTARY_ZIP) + $(if $(NOTARY_PROFILE),$(RM) $(NOTARY_ZIP)) + # stapler is the gate rather than notarytool: it is what fails if the + # submission came back anything other than accepted. + $(staple) $(APPDIR) + cp -p COPYING CHANGELOG.md README.md $(DMGROOT)/ # The window and the backdrop behind it are one layout, drawn by @@ -405,6 +462,8 @@ dmg: $(NAME) resources/Avanor.icns --icon "CHANGELOG.md" 240 260 \ --icon "COPYING" 360 260 \ --no-internet-enable \ + $(if $(CODESIGN_ID),--codesign "$(CODESIGN_ID)") \ + $(if $(NOTARY_PROFILE),--notarize "$(NOTARY_PROFILE)") \ $(DMG) $(DMGROOT)/ $(RM) -r $(DMGROOT) diff --git a/resources/avanor.entitlements b/resources/avanor.entitlements new file mode 100644 index 0000000..fd4b651 --- /dev/null +++ b/resources/avanor.entitlements @@ -0,0 +1,31 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + From 1bcb07a38df12f31c1fc860a7d74945f3c821191 Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:42:22 +0200 Subject: [PATCH 5/9] Give CI the key to sign the disk images with Imports the certificate into a keychain of the run's own and tells the Makefile what to sign with. The step is skipped when the certificate secret is empty, which is every fork and every pull request - secrets do not reach those - and make then builds the unsigned image as before. The certificate is tested through an environment variable rather than in the `if:` directly, because a secret cannot be read from one. --- .github/workflows/build.yml | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33b84d2..3caf958 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,6 +275,11 @@ jobs: arch: arm64 - runner: macos-15-intel arch: x86_64 + # A secret cannot be read in an `if:`, only an environment variable + # can, so the one that decides whether there is anything to sign with + # is lifted up here for the step below to test. + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} steps: - uses: actions/checkout@v7 - name: Install dependencies @@ -296,6 +301,63 @@ jobs: exit 1 fi + # Everything the signing needs, put where the Makefile will find it. + # Skipped when the certificate is not there - a fork has no secrets + # - and the build below then produces the unsigned image instead of + # failing. See SIGNING.md for where these five secrets come from. + - name: Set up the signing key + if: env.MACOS_CERTIFICATE != '' + env: + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + NOTARY_KEY: ${{ secrets.NOTARY_KEY }} + NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }} + NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }} + run: | + set -eu + + keychain="$RUNNER_TEMP/signing.keychain-db" + password=$(uuidgen) + + printf '%s' "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12" + printf '%s' "$NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8" + + security create-keychain -p "$password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$password" "$keychain" + security import "$RUNNER_TEMP/cert.p12" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + + # Without this codesign stops to ask permission to use the key, + # of a runner with nobody at it to answer. + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$password" "$keychain" > /dev/null + + # The default keychain, not merely a searched one: notarytool + # reads the default when it is given a profile name. + security list-keychains -d user -s "$keychain" + security default-keychain -d user -s "$keychain" + + xcrun notarytool store-credentials AVANOR_NOTARY \ + --key "$RUNNER_TEMP/notary.p8" \ + --key-id "$NOTARY_KEY_ID" \ + --issuer "$NOTARY_ISSUER" \ + --keychain "$keychain" + + rm -f "$RUNNER_TEMP/cert.p12" "$RUNNER_TEMP/notary.p8" + + identity=$(security find-identity -v -p codesigning "$keychain" \ + | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -1) + + if [ -z "$identity" ]; then + echo "::error::no Developer ID Application identity in the certificate" + exit 1 + fi + + echo "CODESIGN_ID=$identity" >> "$GITHUB_ENV" + echo "NOTARY_PROFILE=AVANOR_NOTARY" >> "$GITHUB_ENV" + + # CODESIGN_ID and NOTARY_PROFILE reach make through the environment + # when the step above ran, and are simply unset when it did not. - name: Build the disk image run: gmake dmg - name: Add it to the release From 5b4aed0fb123ef1626734494064c93c836b58d8a Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:43:11 +0200 Subject: [PATCH 6/9] Sign the Windows installer, when there is a key The installer is unsigned, so SmartScreen calls it an unknown publisher. SignPath signs open source projects for nothing, which Avanor is, and it signs from an artifact of the run rather than from a key on the runner - their hardware holds the certificate and it never comes here - so the installer is uploaded first and the signed copy replaces it. Switched on by the same kind of check as the disk images: no token, no signing, and the unsigned installer goes on the release as before. Worth saying plainly: this will not make the SmartScreen prompt go away at once. Since 2024 a certificate no longer buys an exemption, only a reputation that accrues as people download. What it does buy immediately is a name in the prompt instead of "unknown publisher". --- .github/workflows/build.yml | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3caf958..4f5eb4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,6 +173,10 @@ jobs: runs-on: windows-latest permissions: contents: write + # As in macos-dmg: a secret cannot be read in an `if:`, an + # environment variable can. + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} strategy: fail-fast: false matrix: @@ -211,6 +215,42 @@ jobs: - name: Build the installer if: github.ref_type == 'tag' && matrix.configuration == '' run: make installer + + # SignPath signs from an artifact of this run rather than from a + # key on the runner - the certificate lives in their hardware and + # never comes here - so the installer is uploaded first and the + # signed copy comes back to replace it. + - name: Keep the unsigned installer + id: installer + if: github.ref_type == 'tag' && matrix.configuration == '' + uses: actions/upload-artifact@v4 + with: + name: avanor-windows-installer-unsigned + # [0-9] so this cannot pick up avanor-d.exe by accident + path: avanor-[0-9]*.exe + if-no-files-found: error + + # Skipped when there is no token - a fork, a pull request, or an + # upstream that has not applied to SignPath yet - and the unsigned + # installer goes on the release instead, as it did before. See + # SIGNING.md. + - name: Sign the installer + if: github.ref_type == 'tag' && matrix.configuration == '' + && env.SIGNPATH_API_TOKEN != '' + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_POLICY_SLUG }} + github-artifact-id: ${{ steps.installer.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed + - name: Take the signed installer over the unsigned one + if: github.ref_type == 'tag' && matrix.configuration == '' + && env.SIGNPATH_API_TOKEN != '' + run: mv -f signed/avanor-[0-9]*.exe . + - name: Add it to the release if: github.ref_type == 'tag' && matrix.configuration == '' uses: softprops/action-gh-release@v3 From 712d26641ebd6371bdec48d2f903d15281da06c5 Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:45:01 +0200 Subject: [PATCH 7/9] Say how to install a release, and how to sign one The README explained how to build from source and nothing else, so somebody who had downloaded a package had nowhere to look - not for which of the two disk images is theirs, and not for what to do about the refusal the unsigned one produces. SIGNING.md is for whoever publishes the releases rather than for whoever builds them: what the accounts cost, how to get the certificate and the notarisation key, and which secret each one becomes. The machinery it describes is already in the Makefile and in CI and does nothing until those secrets exist, so this is the missing half of it. --- README.md | 62 +++++++++++++++++ SIGNING.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 SIGNING.md diff --git a/README.md b/README.md index cbb1ef5..7d83802 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - [Project History](#project-history) - [Current Status](#current-status) - [Features](#features) +- [Installing a release](#installing-a-release) - [Building and Running](#building-and-running) - [Required C++ modules](#required-c-modules) - [Gameplay](#gameplay) @@ -178,6 +179,67 @@ The project is currently in **active modernization** with the following status: --- +## Installing a release + +Every tag builds a package for each platform and puts it on the +[release](https://github.com/jaydg/avanor/releases). Building from source +is the next section; this one is for the packages. + +### macOS + +There are two disk images and they are not interchangeable, because each +holds one architecture: + +| | For | +| --- | --- | +| `avanor--arm64.dmg` | Apple silicon - M1 and later | +| `avanor--x86_64.dmg` | Intel | + +If you are not sure which you have, the Apple menu, About This Mac, says +so. Taking the wrong one gets you `Bad CPU type in executable`, which is +the only thing it can do - Rosetta translates Intel code for Apple +silicon and not the other way about. + +Open the image and drag Avanor into Applications. + +The first time you open it, macOS may refuse: + +> **"Avanor" Not Opened** - Apple could not verify "Avanor" is free of +> malware that may harm your Mac or compromise your privacy. + +That is Gatekeeper saying the application is not signed by a registered +Apple developer, which costs money the project has not spent. Nothing is +wrong with the download. To open it anyway, go to **System Settings**, +**Privacy & Security**, scroll to the bottom, and click **Open Anyway** +beside the message about Avanor. You only have to do this once. + +Control-clicking the application and choosing Open used to be a quicker +way through this. macOS 15 removed it; System Settings is now the only +route. + +(If you are the person publishing these, [SIGNING.md](SIGNING.md) +describes what it would take to make that dialogue stop appearing.) + +### Windows + +Run `avanor-.exe`. SmartScreen will probably say "Windows +protected your PC" and give the publisher as unknown, for the same reason +as on macOS. Click **More info**, then **Run anyway**. + +### Debian, Ubuntu and Fedora + +Install the `.deb` or the `.rpm` from the release: + +```bash +sudo apt install ./avanor_*.deb # Debian, Ubuntu +sudo dnf install ./avanor-*.rpm # Fedora +``` + +Both put the game on the path as `avanor`, so there is nothing to drag +anywhere and nothing to approve. + +--- + ## Building and Running Avanor builds with **GNU Make**. diff --git a/SIGNING.md b/SIGNING.md new file mode 100644 index 0000000..29cd469 --- /dev/null +++ b/SIGNING.md @@ -0,0 +1,192 @@ +# Signing the releases + +This is for whoever publishes Avanor. Everything it describes is already +built and already in CI; none of it does anything until the accounts +below exist and their secrets are in the repository settings. Until then +every release is produced exactly as it is today, unsigned, and the +disk image tells the user what macOS will say about that. + +Nothing here needs to be done in a hurry, and nothing here needs to be +done at all if the answer is that it is not worth ninety-nine dollars a +year. Read the first section and decide. + +## What this buys, and what it does not + +**macOS.** Today, opening Avanor for the first time fails: *"Avanor" Not +Opened*. The only way through is System Settings, Privacy & Security, +scroll to the bottom, Open Anyway - macOS 15 withdrew the old +Control-click shortcut, so there is no quicker route to point people at. +Signing and notarising removes that entirely. The application opens on a +double click, with no dialogue at all. This is a complete fix, and it is +the reason to spend the money. + +**Windows.** Today SmartScreen says "Windows protected your PC" and names +the publisher as unknown. Signing replaces *unknown publisher* with a +real name, which is most of the benefit. It does **not** make the prompt +go away immediately: since a policy change in 2024 a certificate no +longer buys an exemption, only a reputation, and that accrues as people +download the installer. Expect the prompt to persist for a while and then +stop. This part is free, so the only cost is the application effort. + +## What it costs + +| | Cost | Notes | +| --- | --- | --- | +| Apple Developer Program | **$99/year** | The whole of the macOS cost | +| Developer ID certificate | included | Comes with the membership | +| App Store Connect API key | free | What CI notarises with | +| Notarisation | free | No limit worth worrying about | +| SignPath Foundation | **free** | Windows, for open source | + +So: **$99 a year, and nothing else.** Both alternatives to SignPath are +worse for this project - Azure Trusted Signing is about $10 a month and +is limited to organisations in the US and Canada, and a commercial +certificate is $200-600 a year and, since the key-storage rules changed +in 2023, has to live on a hardware token, which is its own problem for a +build that runs on someone else's machine. + +## Apple, part one: the membership + +Enrol at . + +**Enrol as an Individual, not as an Organization.** They cost the same. +An Organization has to supply a D-U-N-S number registered to a legal +entity, which Avanor does not have and would have to invent a company to +get. An Individual needs none. The only visible difference is that the +certificate, and so the name macOS shows, is a personal name rather than +a project name. + +Identity verification takes days rather than minutes, so start it before +you need it. The membership renews annually; see the last section for +what lapsing does. + +## Apple, part two: the certificate + +1. In the Apple Developer account, under Certificates, create a + certificate of type **Developer ID Application**. Not "Mac + Development", not "Developer ID Installer" - only the Application one + signs an application and a disk image. +2. Download it and open it, which puts it in the login keychain. +3. In Keychain Access, find it, and export **the certificate together + with its private key** as a `.p12`. It will ask for a password; make + one up and keep it, it becomes a secret below. +4. Turn it into text, because that is what a GitHub secret holds: + + ```sh + base64 -i Certificates.p12 | pbcopy + ``` + + That is `MACOS_CERTIFICATE`. The password from step 3 is + `MACOS_CERTIFICATE_PASSWORD`. + +## Apple, part three: the notarisation key + +Notarisation is a separate conversation with Apple, and CI does it with +an App Store Connect API key rather than with an Apple ID and password. +The key is not tied to a person, survives two-factor prompts, and can be +revoked on its own. + +1. In App Store Connect, under Users and Access, Integrations, Keys, + generate a **team key**. Developer access is enough to notarise. +2. Download the `.p8`. **It downloads once.** If it is lost, revoke it + and make another. +3. Note the **Key ID** beside it, and the **Issuer ID** shown above the + list - the issuer is one value for the whole team, not per key. +4. As before: + + ```sh + base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy + ``` + + That is `NOTARY_KEY`; the other two are `NOTARY_KEY_ID` and + `NOTARY_ISSUER`. + +## Windows: SignPath Foundation + +SignPath Foundation signs open source projects at no cost. Avanor +qualifies: the repository is public and the licence is GPLv2+. The +certificate is issued to the Foundation and its private key stays in +their hardware, so nothing sensitive is ever in this repository - CI +uploads the built installer and gets a signed one back. + +Apply through . When the project is approved they +provide an organisation id, a project slug and a signing policy slug. +The token is a secret; the other three are not, and go in repository +**variables** rather than secrets. + +## The secrets, in one place + +Repository Settings, Secrets and variables, Actions. + +**Secrets:** + +| Name | What it holds | From | +| --- | --- | --- | +| `MACOS_CERTIFICATE` | the `.p12`, base64 | Apple, part two | +| `MACOS_CERTIFICATE_PASSWORD` | the password you chose for it | Apple, part two | +| `NOTARY_KEY` | the `.p8`, base64 | Apple, part three | +| `NOTARY_KEY_ID` | the key's id | Apple, part three | +| `NOTARY_ISSUER` | the team's issuer id | Apple, part three | +| `SIGNPATH_API_TOKEN` | the SignPath token | SignPath | + +**Variables:** + +| Name | What it holds | +| --- | --- | +| `SIGNPATH_ORGANIZATION_ID` | from SignPath | +| `SIGNPATH_PROJECT_SLUG` | from SignPath | +| `SIGNPATH_POLICY_SLUG` | from SignPath | + +They are independent. The macOS six and the Windows four can be added +months apart, and each platform starts signing as soon as its own are +there. Adding none of them leaves everything working as it does now. + +## Checking that it worked + +Push a tag, then download the disk image from the release the way a +stranger would - through a browser, not with `curl`, so that it carries +the quarantine flag that makes macOS care. + +```sh +spctl -a -vvv -t exec /Applications/Avanor.app # wanted: accepted +xcrun stapler validate /Applications/Avanor.app # the app, not just the image +codesign -dvv /Applications/Avanor.app # wanted: a Developer ID, a TeamIdentifier +``` + +`spctl` saying `accepted` and a `TeamIdentifier` that is not `not set` +are the two that matter. The honest test is the one that needs no +commands: double-click it and see nothing happen except the game +starting. + +Two things worth knowing before you debug something that is not broken: + +- **Do not test this with ad-hoc signing** (`codesign --sign -`). The + hardened runtime turns on library validation, which requires the + bundled libraries and the game to carry the *same* team, and an ad-hoc + signature has no team at all. It fails with *"mapping process and + mapped file (non-platform) have different Team IDs"*, which looks + alarming and means only that you signed with a dash. A real Developer + ID signs all four with one team and the problem does not exist. +- **Check the Apple silicon image in particular.** The two entitlements + in `resources/avanor.entitlements` exist because the hardened runtime + otherwise forbids LuaJIT from running what it compiles. On Intel this + was tested and the game runs either way; arm64 enforces it far more + strictly, and if it were ever going to bite it would bite there. The + symptom to watch for is not a crash but sluggishness, because the + refusal stops the compiler and not the game. + +## Renewal, expiry and loss + +The membership renews every year. **Let it lapse and new releases stop +being signed** - the build does not fail, it quietly produces the +unsigned image again, which is the right behaviour but is easy not to +notice. Releases already published are unaffected: a notarised, stapled +copy keeps working forever, because the ticket travels with it and does +not get re-checked against a live certificate. + +The Developer ID certificate itself is good for five years. Renewing it +means repeating "Apple, part two" and replacing two secrets. + +If the `.p12` or the `.p8` ever leaks, revoke it in the Apple developer +account first and replace the secret afterwards. Revoking does not +invalidate what was already notarised. From c048d05ace596f4ce40ea0c398fe7cb7c4ced5cf Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 12:52:15 +0200 Subject: [PATCH 8/9] Ignore the zip the notarisation is submitted from --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 14e92e0..287cce9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ datafiles.nsh avanor-*.exe avanor-*.dmg +# The zip the application is notarised from. Made and removed inside +# the dmg target, so it is only ever left behind by a failed run. +avanor-*-app.zip + # Generated by dpkg-buildpackage debian/avanor/ debian/files From 154e8bdf406b859af33d0148e2a0ac54cea8d10e Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Sat, 19 Sep 2026 14:52:15 +0200 Subject: [PATCH 9/9] Seal the unsigned bundle too, so macOS calls it unsigned Making the game the bundle's executable had a consequence on the unsigned path that only showed up on a download. The linker leaves an ad-hoc signature on the executable whatever we do, and that executable is now the bundle's own - so a bundle with nothing sealing it is a bundle whose signature claims resources it does not have: code has no resources but signature indicates they must be present macOS can put that to the user as "Avanor is damaged and can't be opened", which is alarming, untrue, and offers no way forward - where the refusal we want says the developer is unidentified and can be got past in System Settings, which is what the README describes. So seal it either way, and vary what with: a Developer ID with the hardened runtime and the entitlements when there is one, an ad-hoc seal when there is not. Gatekeeper still refuses the ad-hoc one, which is right, but it refuses it as what it is. No hardened runtime on the ad-hoc path: it turns on library validation, which wants the libraries and the executable to share a team, and an ad-hoc signature has none - the three bundled libraries would not load. --- Makefile | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 32f098b..e3b6d65 100644 --- a/Makefile +++ b/Makefile @@ -348,8 +348,30 @@ installer: $(NAME) mainfiles.nsh datafiles.nsh avanor.nsi CODESIGN_ID ?= NOTARY_PROFILE ?= -sign = $(if $(CODESIGN_ID),codesign --force --timestamp --options runtime \ - --sign "$(CODESIGN_ID)",:) +# Signed either way, and the difference is what with. With an identity: +# the hardened runtime and the entitlements, which is what notarisation +# requires. Without one: an ad-hoc seal, which Gatekeeper still refuses - +# as it should, nobody has vouched for this - but refuses cleanly, as an +# application from an unidentified developer, which is the refusal the +# README tells people how to get past. +# +# The alternative is not "no signature": the linker leaves an ad-hoc one +# on the executable whatever we do, and since the executable is now the +# bundle's own, a bundle with no seal around it is a bundle whose +# signature says it has resources and has none. codesign calls that "code +# has no resources but signature indicates they must be present", and +# macOS can put it to the user as "Avanor is damaged and can't be +# opened" - alarming, and untrue. +# +# No hardened runtime on the ad-hoc path. It turns on library validation, +# which requires the libraries and the executable to share a team, and an +# ad-hoc signature has no team at all - so the three bundled libraries +# would refuse to load and the game would not start. +sign_lib = $(if $(CODESIGN_ID),codesign --force --timestamp --options runtime \ + --sign "$(CODESIGN_ID)",codesign --force --sign -) +sign_app = $(if $(CODESIGN_ID),codesign --force --timestamp --options runtime \ + --entitlements resources/avanor.entitlements --sign "$(CODESIGN_ID)",\ + codesign --force --sign -) notarise = $(if $(NOTARY_PROFILE),xcrun notarytool submit \ --keychain-profile "$(NOTARY_PROFILE)" --wait,:) staple = $(if $(NOTARY_PROFILE),xcrun stapler staple,:) @@ -414,7 +436,7 @@ dmg: $(NAME) resources/Avanor.icns # would apply these entitlements to the three libraries as well, which # is not what they are for. @for lib in $(APPDIR)/Contents/MacOS/libs/*.dylib; do \ - echo $(sign) "$$lib"; $(sign) "$$lib" || exit 1; \ + echo $(sign_lib) "$$lib"; $(sign_lib) "$$lib" || exit 1; \ done # The entitlements go on the bundle, not on the game beneath it. # Signing a bundle signs its executable too, and would throw away a @@ -422,8 +444,8 @@ dmg: $(NAME) resources/Avanor.icns # something that verifies perfectly and has none of them. Since the # game is now the bundle's executable rather than a file beside one, # this signs both at once and is the only place they can be given. - $(sign) --entitlements resources/avanor.entitlements $(APPDIR) - $(if $(CODESIGN_ID),codesign --verify --deep --strict --verbose=2 $(APPDIR),:) + $(sign_app) $(APPDIR) + codesign --verify --deep --strict --verbose=2 $(APPDIR) # Notarised twice, and stapled twice, because the two tickets are not # the same ticket. The one on the disk image is left behind the moment