From 914fa1ed32b0fff5642ecaaab2020b1839287133 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 07:04:39 -0400 Subject: [PATCH 01/18] ci: native macOS lane for the Gitea self-hosted runner Dispatch-only workflow in .gitea/workflows (GitHub ignores the directory): brew dependencies, native cmake build with the embedded wownerod fetched per-arch, unsigned app bundle artifact. Runs on the mac runner whose work dir lives on DataSSD and is outside every backup path. Signing/notarization follows once a Developer ID Application certificate is provisioned as repo secrets. --- .gitea/workflows/build-macos.yml | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .gitea/workflows/build-macos.yml diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml new file mode 100644 index 00000000..2c136d45 --- /dev/null +++ b/.gitea/workflows/build-macos.yml @@ -0,0 +1,77 @@ +name: macos build (gitea self-hosted) + +# Runs ONLY on the Gitea mac runner (GitHub ignores .gitea/workflows). +# Dispatch-only: the mac runner has capacity 1 and also serves the Hash Bags +# iOS lane, so macOS wowlet builds are an explicit button, not a push hook. +# All build state lands under the runner's work dir, which lives on +# /Volumes/DataSSD (never backed up: the fleet backup pulls an allowlist of +# gitea data only). +# +# Produces an UNSIGNED app bundle artifact. Signing/notarization is a +# follow-up step once a Developer ID Application certificate exists: +# codesign --deep --options runtime --sign "Developer ID Application: ..." +# xcrun notarytool submit --wait; xcrun stapler staple +# with the P12 + App Store Connect API key delivered as repo secrets and +# imported into an ephemeral keychain per run (same pattern as the Hash Bags +# iOS lane). + +on: [workflow_dispatch] + +jobs: + build-macos: + name: "macOS (brew, native)" + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: install dependencies (brew) + run: | + export PATH="/opt/homebrew/bin:$PATH" + brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp + + - name: fetch wownerod to embed + run: | + set -e + ARCH=$(uname -m) + case "$ARCH" in + arm64) WOWTAR=wownero-aarch64-apple-darwin11-v0.11.4.0.tar.bz2 ;; + *) WOWTAR=wownero-x86_64-apple-darwin11-v0.11.4.0.tar.bz2 ;; + esac + mkdir -p /tmp/wownerod-embed + curl -L -o /tmp/wownero.tar.bz2 \ + "https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/$WOWTAR" + tar xjf /tmp/wownero.tar.bz2 -C /tmp + find /tmp -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \; + chmod +x /tmp/wownerod-embed/wownerod + ls -la /tmp/wownerod-embed + + - name: build + run: | + export PATH="/opt/homebrew/bin:$PATH" + mkdir -p build && cd build + cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed .. + cmake --build . -j "$(sysctl -n hw.ncpu)" + + - name: package app bundle (unsigned) + run: | + set -e + cd build + ls -la bin/ || true + APP=$(find . -maxdepth 3 -name "*.app" -type d | head -1) + if [ -z "$APP" ]; then + echo "no .app bundle produced; uploading raw binary" + mkdir -p dist && cp bin/wowlet dist/ 2>/dev/null || cp wowlet dist/ + else + mkdir -p dist && cp -R "$APP" dist/ + fi + cd dist && zip -ry ../wowlet-macos-unsigned.zip . && cd .. + ls -la wowlet-macos-unsigned.zip + + - name: upload artifact + uses: actions/upload-artifact@v3 + with: + name: wowlet-macos-unsigned + path: build/wowlet-macos-unsigned.zip + retention-days: 14 From 1217ce174c5abb2b08adae2f8a123cd943b3b3ed Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 12:17:21 -0400 Subject: [PATCH 02/18] ci(macos): sign + notarize with Developer ID cert via desktop-lane secrets --- .gitea/workflows/build-macos.yml | 93 +++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 2c136d45..cb25febf 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -7,13 +7,12 @@ name: macos build (gitea self-hosted) # /Volumes/DataSSD (never backed up: the fleet backup pulls an allowlist of # gitea data only). # -# Produces an UNSIGNED app bundle artifact. Signing/notarization is a -# follow-up step once a Developer ID Application certificate exists: -# codesign --deep --options runtime --sign "Developer ID Application: ..." -# xcrun notarytool submit --wait; xcrun stapler staple -# with the P12 + App Store Connect API key delivered as repo secrets and -# imported into an ephemeral keychain per run (same pattern as the Hash Bags -# iOS lane). +# Signing/notarization uses the SAME seven secrets as the smirk-monorepo +# desktop lane, set by ~/keys/suchsoftware/set-gitea-desktop-secrets.sh: +# APPLE_CERTIFICATE (b64 Developer ID Application .p12), +# APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID, +# APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_P8_BASE64 (notarytool). +# If APPLE_CERTIFICATE is unset the build ships unsigned instead of failing. on: [workflow_dispatch] @@ -21,6 +20,13 @@ jobs: build-macos: name: "macOS (brew, native)" runs-on: macos-latest + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} steps: - uses: actions/checkout@v4 with: @@ -54,24 +60,75 @@ jobs: cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed .. cmake --build . -j "$(sysctl -n hw.ncpu)" - - name: package app bundle (unsigned) + - name: package app bundle run: | set -e cd build - ls -la bin/ || true APP=$(find . -maxdepth 3 -name "*.app" -type d | head -1) - if [ -z "$APP" ]; then - echo "no .app bundle produced; uploading raw binary" - mkdir -p dist && cp bin/wowlet dist/ 2>/dev/null || cp wowlet dist/ - else - mkdir -p dist && cp -R "$APP" dist/ + [ -n "$APP" ] || { echo "no .app bundle produced"; ls -la bin/; exit 1; } + mkdir -p dist + cp -R "$APP" dist/Wowlet.app + # bundle Qt frameworks/plugins so the app runs off-machine + /opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app + ls -la dist/ + + - name: sign and notarize (skipped when secrets absent) + run: | + set -e + cd build + if [ -z "$APPLE_CERTIFICATE" ]; then + echo "APPLE_CERTIFICATE not set — shipping UNSIGNED build" + exit 0 fi - cd dist && zip -ry ../wowlet-macos-unsigned.zip . && cd .. - ls -la wowlet-macos-unsigned.zip + KC=/tmp/wowlet-ci.keychain-db + KCPW=$(/usr/bin/uuidgen) + echo "$KCPW" > /tmp/wowlet-ci-kcpw # for the cleanup step only + security create-keychain -p "$KCPW" "$KC" + security set-keychain-settings -lut 3600 "$KC" + security unlock-keychain -p "$KCPW" "$KC" + printf '%s' "$APPLE_CERTIFICATE" | base64 -d > /tmp/dev_id.p12 + security import /tmp/dev_id.p12 -k "$KC" -P "$APPLE_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$KCPW" "$KC" >/dev/null + security list-keychains -d user -s "$KC" login.keychain-db + + # sign nested Mach-O executables first (embedded wownerod etc.), + # then frameworks, then the bundle — all hardened-runtime + timestamp + SIGN() { codesign --force --options runtime --timestamp --keychain "$KC" \ + --sign "$APPLE_SIGNING_IDENTITY" "$@"; } + find dist/Wowlet.app -type f -perm +111 ! -path "*/Frameworks/*" \ + | while read -r f; do file "$f" | grep -q Mach-O && SIGN "$f" || true; done + find dist/Wowlet.app/Contents/Frameworks -maxdepth 1 \( -name "*.framework" -o -name "*.dylib" \) 2>/dev/null \ + | while read -r fw; do SIGN "$fw"; done + SIGN dist/Wowlet.app + codesign --verify --deep --strict dist/Wowlet.app + + printf '%s' "$APPLE_API_KEY_P8_BASE64" | base64 -d > /tmp/notary.p8 + ditto -c -k --keepParent dist/Wowlet.app /tmp/wowlet-notarize.zip + xcrun notarytool submit /tmp/wowlet-notarize.zip \ + --key /tmp/notary.p8 --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" \ + --wait --timeout 30m + xcrun stapler staple dist/Wowlet.app + echo "signed + notarized + stapled" + + - name: zip artifact + run: | + set -e + cd build/dist + ditto -c -k --keepParent Wowlet.app ../wowlet-macos-$(uname -m).zip + ls -la ../wowlet-macos-*.zip + + - name: cleanup signing material + if: always() + run: | + if [ -f /tmp/wowlet-ci-kcpw ]; then + security delete-keychain /tmp/wowlet-ci.keychain-db || true + fi + rm -f /tmp/dev_id.p12 /tmp/notary.p8 /tmp/wowlet-ci-kcpw /tmp/wowlet-notarize.zip - name: upload artifact uses: actions/upload-artifact@v3 with: - name: wowlet-macos-unsigned - path: build/wowlet-macos-unsigned.zip + name: wowlet-macos + path: build/wowlet-macos-*.zip retention-days: 14 From f4c5ec3c252f79d3aca5619c1e673afd77b9d96d Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:10:03 -0400 Subject: [PATCH 03/18] ci(macos): non-interactive brew --- .gitea/workflows/build-macos.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index cb25febf..6fbc4824 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -35,6 +35,7 @@ jobs: - name: install dependencies (brew) run: | export PATH="/opt/homebrew/bin:$PATH" + export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp - name: fetch wownerod to embed From a526061d97456309a97384a5bca238b964cdc206 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:15:24 -0400 Subject: [PATCH 04/18] ci(macos): place appicon.icns into the bundle --- .gitea/workflows/build-macos.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 6fbc4824..c08ed420 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -69,6 +69,10 @@ jobs: [ -n "$APP" ] || { echo "no .app bundle produced"; ls -la bin/; exit 1; } mkdir -p dist cp -R "$APP" dist/Wowlet.app + # cmake never places the icon in the bundle (upstream only copies it + # for the dormant `installed/` layout) + mkdir -p dist/Wowlet.app/Contents/Resources + cp ../src/assets/images/appicons/appicon.icns dist/Wowlet.app/Contents/Resources/ # bundle Qt frameworks/plugins so the app runs off-machine /opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app ls -la dist/ From 36d878e7bf57c081be052414f961e721e4803a3f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:16:13 -0400 Subject: [PATCH 05/18] ci(macos): also skip dependent upgrades in brew install --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index c08ed420..270f7e8d 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -35,7 +35,7 @@ jobs: - name: install dependencies (brew) run: | export PATH="/opt/homebrew/bin:$PATH" - export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 + export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_UPGRADE=1 brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp - name: fetch wownerod to embed From dc5f9408012367f113d0b7a5e0e17bcb427c5d5b Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:27:05 -0400 Subject: [PATCH 06/18] =?UTF-8?q?ci(macos):=20pipe=20yes=20into=20brew=20?= =?UTF-8?q?=E2=80=94=20Homebrew=206=20prompts=20for=20dependency=20install?= =?UTF-8?q?s=20regardless=20of=20NONINTERACTIVE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 270f7e8d..23a79af3 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -36,7 +36,7 @@ jobs: run: | export PATH="/opt/homebrew/bin:$PATH" export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_UPGRADE=1 - brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp + yes | brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp - name: fetch wownerod to embed run: | From 8b846a8dc0a65f4acb5e1f4aafdfc16d9f0ba135 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:37:41 -0400 Subject: [PATCH 07/18] ci(macos): pin CMAKE_PREFIX_PATH to brew qt6 (qt@5 keg coexists unlinked on the runner) --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 23a79af3..b5834816 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -58,7 +58,7 @@ jobs: run: | export PATH="/opt/homebrew/bin:$PATH" mkdir -p build && cd build - cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed .. + cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. cmake --build . -j "$(sysctl -n hw.ncpu)" - name: package app bundle From a2b34f5ebcb61d51c9a9a9f76d8e70473fddd760 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:44:28 -0400 Subject: [PATCH 08/18] =?UTF-8?q?ci(macos):=20embed=20tor=20(expert=20bund?= =?UTF-8?q?le=2015.0)=20=E2=80=94=20arms=20daemon=20--tx-proxy=20via=20HAS?= =?UTF-8?q?=5FTOR=5FBIN,=20parity=20with=20linux=20lane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-macos.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index b5834816..af736318 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -54,11 +54,29 @@ jobs: chmod +x /tmp/wownerod-embed/wownerod ls -la /tmp/wownerod-embed + - name: fetch tor to embed + run: | + set -e + ARCH=$(uname -m) + case "$ARCH" in + arm64) TORTAR=tor-expert-bundle-macos-aarch64-15.0.tar.gz ;; + *) TORTAR=tor-expert-bundle-macos-x86_64-15.0.tar.gz ;; + esac + mkdir -p /tmp/teb /tmp/tor-embed + curl -L -o /tmp/teb.tgz "https://archive.torproject.org/tor-package-archive/torbrowser/15.0/$TORTAR" + tar xzf /tmp/teb.tgz -C /tmp/teb + cp /tmp/teb/tor/tor /tmp/tor-embed/ + chmod +x /tmp/tor-embed/tor + # expert-bundle RELEASE number (15.0) is NOT the tor version — derive it + TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1) + echo "TOR_VERSION=$TORVER" >> "$GITHUB_ENV" + echo "embedded tor version: $TORVER" + - name: build run: | export PATH="/opt/homebrew/bin:$PATH" mkdir -p build && cd build - cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. + cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. cmake --build . -j "$(sysctl -n hw.ncpu)" - name: package app bundle From 1853ebf558b10e3769b45a55a6737ba812747074 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:47:37 -0400 Subject: [PATCH 09/18] ci(macos): disable pipefail around yes|brew (SIGPIPE 141 when brew asks nothing) --- .gitea/workflows/build-macos.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index af736318..aff2ea69 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -35,6 +35,7 @@ jobs: - name: install dependencies (brew) run: | export PATH="/opt/homebrew/bin:$PATH" + set +o pipefail # yes(1) takes SIGPIPE when brew has nothing to ask export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_UPGRADE=1 yes | brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp From 410df0c8fff284b4cdff11bb28ca860a7310b75b Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 13:55:16 -0400 Subject: [PATCH 10/18] =?UTF-8?q?ci(macos):=20extract=20wownerod=20into=20?= =?UTF-8?q?an=20explicit=20dir=20=E2=80=94=20find(1)=20does=20not=20descen?= =?UTF-8?q?d=20the=20/tmp=20symlink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-macos.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index aff2ea69..205ee455 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -47,11 +47,12 @@ jobs: arm64) WOWTAR=wownero-aarch64-apple-darwin11-v0.11.4.0.tar.bz2 ;; *) WOWTAR=wownero-x86_64-apple-darwin11-v0.11.4.0.tar.bz2 ;; esac - mkdir -p /tmp/wownerod-embed + mkdir -p /tmp/wownerod-embed /tmp/wow-extract curl -L -o /tmp/wownero.tar.bz2 \ "https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/$WOWTAR" - tar xjf /tmp/wownero.tar.bz2 -C /tmp - find /tmp -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \; + # /tmp is a symlink on macOS — find(1) won't descend it as a bare argument + tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-extract + find /tmp/wow-extract -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \; chmod +x /tmp/wownerod-embed/wownerod ls -la /tmp/wownerod-embed From 1fedbf0faabb7c057cc0226727d666e7b6930225 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 14:06:15 -0400 Subject: [PATCH 11/18] ci(macos): ship libevent dylib next to tor (@executable_path linkage) --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 205ee455..1ce4d173 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -67,7 +67,7 @@ jobs: mkdir -p /tmp/teb /tmp/tor-embed curl -L -o /tmp/teb.tgz "https://archive.torproject.org/tor-package-archive/torbrowser/15.0/$TORTAR" tar xzf /tmp/teb.tgz -C /tmp/teb - cp /tmp/teb/tor/tor /tmp/tor-embed/ + cp /tmp/teb/tor/tor /tmp/teb/tor/libevent-*.dylib /tmp/tor-embed/ chmod +x /tmp/tor-embed/tor # expert-bundle RELEASE number (15.0) is NOT the tor version — derive it TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1) From 0f6892c123fc9b3e46d456be86a462ec610041e6 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 14:17:26 -0400 Subject: [PATCH 12/18] =?UTF-8?q?ci(macos):=20ad-hoc=20sign=20tor=20before?= =?UTF-8?q?=20embedding=20=E2=80=94=20arm64=20macOS=20SIGKILLs=20unsigned?= =?UTF-8?q?=20binaries,=20including=20the=20runtime-unpacked=20copy=20on?= =?UTF-8?q?=20user=20machines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-macos.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 1ce4d173..71a93e9a 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -68,6 +68,10 @@ jobs: curl -L -o /tmp/teb.tgz "https://archive.torproject.org/tor-package-archive/torbrowser/15.0/$TORTAR" tar xzf /tmp/teb.tgz -C /tmp/teb cp /tmp/teb/tor/tor /tmp/teb/tor/libevent-*.dylib /tmp/tor-embed/ + # tor ships UNSIGNED; arm64 macOS SIGKILLs unsigned binaries. Ad-hoc sign + # BEFORE embedding — the signature travels inside the file, so the copy + # the app unpacks at runtime on user machines is runnable too. + codesign -s - -f /tmp/tor-embed/tor /tmp/tor-embed/libevent-*.dylib chmod +x /tmp/tor-embed/tor # expert-bundle RELEASE number (15.0) is NOT the tor version — derive it TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1) From 3cf58f031369cd7abddff66447f75152d4a566bf Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 14:28:09 -0400 Subject: [PATCH 13/18] cmake: mkdir src/assets/tor before the TOR_DIR copy (untracked dir; BSD cp usage error evades the ret==1 check) --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a3bb703..101fe0ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,10 @@ if(TOR_DIR) message(STATUS "Embedded Tor version: ${TOR_VERSION}") + # wowlet: the target dir is not tracked in git — create it (BSD cp's usage + # error exits 64, which the ret check below does not catch) + execute_process(COMMAND bash -c "mkdir -p ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") + # Always copy Tor when doing a reproducible build to prevent old versions from getting included if (REPRODUCIBLE) set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") From 49b3264b0e8dc23a145fcc0fee8036fdc43eb639 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 14:38:24 -0400 Subject: [PATCH 14/18] =?UTF-8?q?cmake:=20portable=20tor/wownerod=20copies?= =?UTF-8?q?=20=E2=80=94=20BSD=20cp=20lacks=20-u=20(exit=2064=20evaded=20th?= =?UTF-8?q?e=20ret=3D=3D1=20check,=20silently=20shipping=20without=20embed?= =?UTF-8?q?ded=20node/tor);=20fail=20on=20ANY=20nonzero=20cp=20exit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 101fe0ec..a110f935 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,11 +156,11 @@ if(TOR_DIR) if (REPRODUCIBLE) set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") else() - set(TOR_COPY_CMD "cp -au ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") + set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") # wowlet: BSD cp has no -u (macOS: illegal option, exit 64, silent no-op) endif() execute_process(COMMAND bash -c "${TOR_COPY_CMD}" RESULT_VARIABLE ret) - if(ret EQUAL "1") + if(NOT ret EQUAL "0") message(FATAL_ERROR "Tor copy failure: ${TOR_COPY_CMD}") endif() @@ -176,11 +176,11 @@ if(WOWNEROD_DIR) if (REPRODUCIBLE) set(WOWNEROD_COPY_CMD "cp -a ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod") else() - set(WOWNEROD_COPY_CMD "cp -au ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod") + set(WOWNEROD_COPY_CMD "cp -a ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod") # wowlet: BSD cp has no -u endif() execute_process(COMMAND bash -c "${WOWNEROD_COPY_CMD}" RESULT_VARIABLE ret) - if(ret EQUAL "1") + if(NOT ret EQUAL "0") message(FATAL_ERROR "wownerod copy failure: ${WOWNEROD_COPY_CMD}") endif() From 9e6d85c246318c96181e8e4f03d2a98cc0143e50 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 30 Aug 2026 04:42:14 -0400 Subject: [PATCH 15/18] ci(macos): disable Trezor (USE_DEVICE_TREZOR=OFF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit brew's protobuf pulls Abseil headers that require C++17, breaking the feather compile (DEVICE_TREZOR_READY gates the protobuf link). Trezor hardware does not support Wownero, so this removes a useless dependency rather than working around it — the Linux lane only tolerates it because its depends system pins an old protobuf. --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 71a93e9a..5f539d92 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -82,7 +82,7 @@ jobs: run: | export PATH="/opt/homebrew/bin:$PATH" mkdir -p build && cd build - cmake -DWITH_SCANNER=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. + cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. cmake --build . -j "$(sysctl -n hw.ncpu)" - name: package app bundle From 87d5977243880b1a934ad2dbca820167a41e9674 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 30 Aug 2026 04:55:20 -0400 Subject: [PATCH 16/18] ci(macos): CPATH/LIBRARY_PATH to brew prefix so feather finds boost headers monero's static libs built fine, but the feather target includes monero headers pulling boost/variant.hpp, and clang doesn't search /opt/homebrew/include by default (modern BoostConfig leaves Boost_INCLUDE_DIRS empty). --- .gitea/workflows/build-macos.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 5f539d92..aee76321 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -81,6 +81,12 @@ jobs: - name: build run: | export PATH="/opt/homebrew/bin:$PATH" + # clang does not search /opt/homebrew/include by default; the feather + # target pulls monero headers that include boost, and modern Boost's + # CMake config leaves Boost_INCLUDE_DIRS empty. CPATH/LIBRARY_PATH make + # every linked brew header/lib findable without per-dep flag surgery. + export CPATH="$(brew --prefix)/include" + export LIBRARY_PATH="$(brew --prefix)/lib" mkdir -p build && cd build cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. cmake --build . -j "$(sysctl -n hw.ncpu)" From f2c3ce45a0a2617c702827220733fb9146932037 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 30 Aug 2026 05:08:01 -0400 Subject: [PATCH 17/18] ci(macos): link boost statically (Boost_USE_STATIC_LIBS=ON) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 100%-linked app dynamically referenced brew's libboost_*.dylib, which macdeployqt failed to bundle — the app would fail to launch on any Mac without Homebrew (notarization doesn't check runtime dylib resolution). Static boost matches how guix/depends release builds work and removes the runtime dependency entirely. --- .gitea/workflows/build-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index aee76321..37223111 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -88,7 +88,7 @@ jobs: export CPATH="$(brew --prefix)/include" export LIBRARY_PATH="$(brew --prefix)/lib" mkdir -p build && cd build - cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. + cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DBoost_USE_STATIC_LIBS=ON -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" .. cmake --build . -j "$(sysctl -n hw.ncpu)" - name: package app bundle From 3d38fdd1352e6e9538d38fd757d4d603d8bbe607 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 30 Aug 2026 05:34:04 -0400 Subject: [PATCH 18/18] ci(macos): bundle Qt image-plugin dylib deps (webp/sharpyuv/brotli) for clean-Mac runtime macdeployqt bundles Qt frameworks but not the plain @rpath/*.dylib brew deps of its image plugins, so a no-Homebrew Mac couldn't load webp. Add a Qt-safe pass that resolves @rpath/*.dylib (never @rpath/*.framework) from brew, copies them in, rewrites load paths, and fails the build if any remain. Sign step now re-signs by Mach-O type (not exec bit) so the install_name_tool-modified plugins get valid signatures. --- .gitea/workflows/build-macos.yml | 57 +++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 37223111..0e4f32d2 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -105,6 +105,52 @@ jobs: cp ../src/assets/images/appicons/appicon.icns dist/Wowlet.app/Contents/Resources/ # bundle Qt frameworks/plugins so the app runs off-machine /opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app + + # macdeployqt bundles Qt frameworks but not the plain-dylib brew deps + # of Qt's image plugins (libwebp/libsharpyuv/libbrotli...), which are + # referenced as @rpath/lib*.dylib. Without them a clean (no-Homebrew) + # Mac can't load those image formats. Qt frameworks use a different + # ref form (@rpath/Qt*.framework/...), so keying strictly on + # `@rpath/*.dylib` bundles the stragglers WITHOUT touching Qt. + # Several passes converge (a bundled lib pulls in further deps). + APP=dist/Wowlet.app + FW="$APP/Contents/Frameworks" + mkdir -p "$FW" + for pass in 1 2 3 4; do + while IFS= read -r macho; do + file "$macho" | grep -q Mach-O || continue + chmod u+w "$macho" + otool -L "$macho" | awk 'NR>1{print $1}' | while IFS= read -r dep; do + case "$dep" in + @rpath/*.dylib) + name=$(basename "$dep") + if [ -f "$FW/$name" ]; then + install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true + continue + fi + real=$(find /opt/homebrew/lib /opt/homebrew/opt/*/lib -maxdepth 1 -name "$name" 2>/dev/null | head -1) + if [ -n "$real" ]; then + cp -n "$real" "$FW/$name" + chmod u+w "$FW/$name" + install_name_tool -id "@executable_path/../Frameworks/$name" "$FW/$name" 2>/dev/null || true + install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true + fi + ;; + esac + done + done < <(find "$APP" -type f) + done + + # Fail if any @rpath/*.dylib (non-Qt) remains unresolved in the bundle + leftover=$(find "$APP" -type f | while IFS= read -r m; do + file "$m" | grep -q Mach-O || continue + otool -L "$m" | awk 'NR>1{print $1}' | grep -E '^@rpath/[^/]+\.dylib$' || true + done | sort -u) + if [ -n "$leftover" ]; then + echo "UNBUNDLED @rpath dylibs remain:"; echo "$leftover"; exit 1 + fi + echo "all plain-dylib deps bundled" + ls -la "$FW" | grep -iE "webp|sharpyuv|brotli" || true ls -la dist/ - name: sign and notarize (skipped when secrets absent) @@ -127,13 +173,16 @@ jobs: security set-key-partition-list -S apple-tool:,apple: -s -k "$KCPW" "$KC" >/dev/null security list-keychains -d user -s "$KC" login.keychain-db - # sign nested Mach-O executables first (embedded wownerod etc.), - # then frameworks, then the bundle — all hardened-runtime + timestamp + # Sign inside-out: every Mach-O NOT inside a .framework (embedded + # wownerod/tor, the bundled webp/sharpyuv/brotli dylibs, and the + # image-format plugins whose signatures install_name_tool just + # invalidated — keyed on `file`, not exec bit, since plugins/dylibs + # aren't +x), then each Qt framework as a unit, then the bundle. SIGN() { codesign --force --options runtime --timestamp --keychain "$KC" \ --sign "$APPLE_SIGNING_IDENTITY" "$@"; } - find dist/Wowlet.app -type f -perm +111 ! -path "*/Frameworks/*" \ + find dist/Wowlet.app -type f ! -path "*/*.framework/*" \ | while read -r f; do file "$f" | grep -q Mach-O && SIGN "$f" || true; done - find dist/Wowlet.app/Contents/Frameworks -maxdepth 1 \( -name "*.framework" -o -name "*.dylib" \) 2>/dev/null \ + find dist/Wowlet.app/Contents/Frameworks -maxdepth 1 -name "*.framework" 2>/dev/null \ | while read -r fw; do SIGN "$fw"; done SIGN dist/Wowlet.app codesign --verify --deep --strict dist/Wowlet.app