mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 07:11:29 +02:00
The CMake target keeps its upstream name so the twenty-odd target_* calls stay put, but OUTPUT_NAME now produces build/bin/wowlet, and on macOS a wowlet.app whose CFBundleExecutable matches it. Linux and Windows already renamed the artifact as they copied it, so their cp lines just move to the new path. macOS copied the whole bundle instead, so it shipped Contents/MacOS/feather with a Feather name in the Info.plist, which is what anyone installing it actually saw. The Linux install rules had the same problem one level down: they installed feather.desktop, which sets Exec=feather and Name=Feather Wallet, while the correct wowlet.desktop sat unused beside it. CFBundleIdentifier moves to org.wownero.wowlet. Nothing in signing or notarization reads it, but macOS keys per-app state by bundle id, so an existing install starts with fresh window state.
269 lines
12 KiB
YAML
269 lines
12 KiB
YAML
name: windows build + sign (gitea self-hosted)
|
|
|
|
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
|
|
# repository. Gitea is master; GitHub is a mirror and a release surface.
|
|
#
|
|
# Two jobs on two different machines, and that split is the whole point:
|
|
#
|
|
# build -> `deb`, which advertises windows-latest:host because it carries the
|
|
# MinGW cross-compiler. Produces an UNSIGNED wowlet.exe.
|
|
# sign -> `desktop-iim2fs2` (windows:host), the only machine with the EV
|
|
# code-signing token plugged into it.
|
|
#
|
|
# The signing host does not sign in this job. The runner there is
|
|
# NT SERVICE\act_runner, which cannot use the token: SafeNet holds the token
|
|
# login per user, and the login is established interactively by an operator.
|
|
# So the job queues the artifact for the signing broker that Fleet installs
|
|
# (roles/windows_code_signing) and waits for it. CI never sees the key and never
|
|
# sees the token password, so it can never spend one of the token's three
|
|
# password attempts.
|
|
#
|
|
# Dispatch-only. Both runners are capacity 1 and shared with other lanes.
|
|
|
|
on: [workflow_dispatch]
|
|
|
|
env:
|
|
HOST: x86_64-w64-mingw32
|
|
|
|
jobs:
|
|
build:
|
|
name: "Windows x86_64 (mingw cross, depends)"
|
|
runs-on: windows-latest # gitea label windows-latest:host -> deb, host mode
|
|
steps:
|
|
- name: verify the cross toolchain is present
|
|
# Host mode on a shared fleet runner: this job installs nothing and
|
|
# needs no sudo. Fleet owns the toolchain; if something is missing the
|
|
# job says exactly what rather than half-building.
|
|
run: |
|
|
missing=0
|
|
for t in curl bzip2 unzip zip git cmake make ccache python3 perl pkg-config \
|
|
x86_64-w64-mingw32-gcc-posix x86_64-w64-mingw32-g++-posix; do
|
|
command -v "$t" >/dev/null || { echo "MISSING: $t"; missing=1; }
|
|
done
|
|
# mingw32.mk needs the posix threading variant (std::thread/mutex/cv).
|
|
x86_64-w64-mingw32-g++ -v 2>&1 | grep -q 'Thread model: posix' || {
|
|
echo "MISSING: x86_64-w64-mingw32-g++ is not the posix threading variant"; missing=1; }
|
|
[ "$missing" = 0 ] || { echo "Fix the build host in such-fleet, not here."; exit 1; }
|
|
echo "toolchain ok"; nproc; free -g | head -2
|
|
|
|
- name: configure git
|
|
run: git config --global --add safe.directory '*'
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
# depends is the slow part (static Qt6/boost/openssl/tor from source).
|
|
# Cache the source tarballs and the compiled packages so only a cold run
|
|
# pays the ~1h cost.
|
|
- name: restore depends sources cache
|
|
id: cache-sources
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: contrib/depends/sources
|
|
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
|
restore-keys: |
|
|
depends-sources-
|
|
|
|
- name: restore depends built cache (mingw)
|
|
id: cache-built
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: contrib/depends/built
|
|
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
|
restore-keys: |
|
|
depends-built-mingw32-
|
|
|
|
- name: build depends (static Qt6 + tor + boost + openssl for mingw)
|
|
# -j4, not the -j2 the GitHub lane needs: that limit existed because a
|
|
# parallel static Qt6 compile OOMs a 16G hosted runner. deb has 30G.
|
|
run: make -C contrib/depends HOST="$HOST" -j4
|
|
|
|
# Save with if:always() so an expensive toolchain banks even when the
|
|
# later wowlet.exe build fails; the automatic post-save is skipped on
|
|
# job failure, which otherwise re-pays hours every time.
|
|
- name: save depends sources cache
|
|
if: always() && steps.cache-sources.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: contrib/depends/sources
|
|
key: depends-sources-${{ hashFiles('contrib/depends/packages/*') }}
|
|
|
|
- name: save depends built cache (mingw)
|
|
if: always() && steps.cache-built.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: contrib/depends/built
|
|
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
|
|
|
|
- name: fetch wownerod.exe to embed
|
|
run: |
|
|
rm -rf /tmp/wownerod-embed && mkdir -p /tmp/wownerod-embed
|
|
curl -fL -o /tmp/wownero-win.zip \
|
|
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-w64-mingw32-v0.11.4.0.zip
|
|
# -j flattens the versioned subdir so wownerod.exe is a DIRECT child
|
|
# (WownerodQrcGenerator globs ${WOWNEROD_DIR}/* non-recursively).
|
|
unzip -o -j /tmp/wownero-win.zip 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d /tmp/wownerod-embed
|
|
ls -la /tmp/wownerod-embed
|
|
|
|
- name: build wowlet.exe
|
|
run: |
|
|
rm -rf build && mkdir build && cd build
|
|
# ARCH=x86-64: portable baseline. wownero's CMake defaults ARCH=native,
|
|
# which under a cross compiler bakes the BUILD HOST's instructions into
|
|
# the Windows binary -> SIGILL on older CPUs.
|
|
cmake --toolchain "$GITHUB_WORKSPACE/contrib/depends/$HOST/share/toolchain.cmake" \
|
|
-DARCH=x86-64 \
|
|
-DWITH_SCANNER=OFF \
|
|
-DCHECK_UPDATES=OFF \
|
|
-DTOR_INSTALLED=On \
|
|
-DFEATHER_TARGET_TRIPLET="$HOST" \
|
|
-DWOWNEROD_DIR=/tmp/wownerod-embed \
|
|
..
|
|
cmake --build . -j6
|
|
ls -la bin/
|
|
|
|
- name: stage the unsigned exe for signing
|
|
# The signing job needs the bare exe, not the zip: the broker signs one
|
|
# PE file at a time. The zip is assembled after signing so the published
|
|
# archive contains the signed binary.
|
|
run: |
|
|
mkdir -p stage/tor
|
|
cp build/bin/wowlet.exe stage/wowlet.exe
|
|
cp "contrib/depends/$HOST/Tor/tor.exe" stage/tor/tor.exe
|
|
(cp LICENSE stage/LICENSE.txt 2>/dev/null || cp COPYING stage/LICENSE.txt 2>/dev/null) || true
|
|
ls -la stage stage/tor
|
|
|
|
- name: upload unsigned staging tree
|
|
# v3, not v4: Gitea Actions implements the older artifact protocol and
|
|
# rejects @actions/artifact v2 clients with
|
|
# "GHESNotSupportedError ... not currently supported on GHES".
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows-unsigned
|
|
path: stage/
|
|
if-no-files-found: error
|
|
|
|
sign:
|
|
name: "Authenticode sign (hardware token broker)"
|
|
needs: build
|
|
runs-on: windows # gitea label windows:host -> desktop-iim2fs2
|
|
steps:
|
|
- name: download unsigned staging tree
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows-unsigned
|
|
path: stage
|
|
|
|
- name: keep the unsigned tree for the reproducible archive
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
Copy-Item 'stage' 'unsigned' -Recurse -Force
|
|
|
|
- name: queue wowlet.exe for the signing broker and wait
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$queued = "wowlet-${env:GITHUB_RUN_ID}.exe"
|
|
$inPath = "C:\signing\in\$queued"
|
|
$outPath = "C:\signing\out\$queued"
|
|
|
|
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
|
Get-Content 'C:\signing\SIGNING_LOCKED'
|
|
throw "The signing broker's circuit breaker is tripped. An operator must clear it."
|
|
}
|
|
|
|
Copy-Item 'stage\wowlet.exe' $inPath -Force
|
|
Set-Content -LiteralPath "$inPath.ready" -Value $env:GITHUB_RUN_ID
|
|
Write-Host "queued $queued"
|
|
|
|
$deadline = (Get-Date).AddMinutes(10)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (Test-Path "$outPath.done") { break }
|
|
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
|
|
Get-Content 'C:\signing\SIGNING_LOCKED'
|
|
throw "The broker tripped its circuit breaker while signing this artifact."
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
if (-not (Test-Path "$outPath.done")) { throw "Timed out waiting for the signing broker." }
|
|
|
|
Move-Item $outPath 'stage\wowlet.exe' -Force
|
|
Remove-Item "$outPath.done" -Force -ErrorAction SilentlyContinue
|
|
|
|
- name: verify the published binary is signed and timestamped
|
|
# The broker verifies before releasing; this verifies again on the
|
|
# artifact that is actually about to ship. A signature or timestamp
|
|
# failure blocks distribution.
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$sig = Get-AuthenticodeSignature 'stage\wowlet.exe'
|
|
$sig | Format-List Status, StatusMessage,
|
|
@{n='Signer';e={$_.SignerCertificate.Subject}},
|
|
@{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}}
|
|
if ($sig.Status -ne 'Valid') { throw "Authenticode status is $($sig.Status)" }
|
|
if (-not $sig.TimeStamperCertificate) { throw "Signature carries no RFC 3161 timestamp" }
|
|
if ($sig.SignerCertificate.Subject -notmatch 'Such Software LLC') {
|
|
throw "Unexpected signer: $($sig.SignerCertificate.Subject)"
|
|
}
|
|
Write-Host "SHA256 $((Get-FileHash 'stage\wowlet.exe' -Algorithm SHA256).Hash)"
|
|
|
|
- name: package both zips
|
|
# Two archives ship, deliberately. Authenticode mutates the exe, so a
|
|
# signed binary can never byte-match what someone reproduces from
|
|
# source. The unsigned archive stays reproducible and is what verifiers
|
|
# check; the signed one is what a normal user should download. Both
|
|
# hashes go in the PGP-signed hashes.txt.
|
|
shell: powershell
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
|
|
# Not Compress-Archive: Windows PowerShell writes the OS separator
|
|
# into entry names, producing "tor\tor.exe". The ZIP format mandates
|
|
# forward slashes, and extractors that take the backslash literally
|
|
# leave a file called "tor\tor.exe" in the root instead of a tor
|
|
# directory -- at which point the bundled Tor is not where wowlet
|
|
# looks for it, and this wallet gives its daemon an exclusive
|
|
# --tx-proxy, so no Tor means no transaction relay at all.
|
|
# v5.1.0 shipped tor/tor.exe from `zip -X`; keep that shape.
|
|
function New-Archive([string] $Source, [string] $Destination) {
|
|
if (Test-Path $Destination) { Remove-Item $Destination -Force }
|
|
$root = (Resolve-Path $Source).Path.TrimEnd('\')
|
|
$zip = [System.IO.Compression.ZipFile]::Open($Destination, 'Create')
|
|
try {
|
|
foreach ($file in Get-ChildItem $root -Recurse -File | Sort-Object FullName) {
|
|
$entry = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
|
[void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
|
$zip, $file.FullName, $entry, 'Optimal')
|
|
}
|
|
} finally { $zip.Dispose() }
|
|
}
|
|
|
|
New-Archive 'unsigned' 'wowlet-x86_64-windows-unsigned.zip'
|
|
New-Archive 'stage' 'wowlet-x86_64-windows.zip'
|
|
|
|
# Prove the shape rather than trusting it: a backslash in any entry
|
|
# name fails the build.
|
|
foreach ($archive in @('wowlet-x86_64-windows.zip', 'wowlet-x86_64-windows-unsigned.zip')) {
|
|
$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $archive))
|
|
try {
|
|
$names = $zip.Entries | ForEach-Object { $_.FullName }
|
|
if ($names -match '\\') { throw "$archive contains a backslash entry name: $($names -join ', ')" }
|
|
if ($names -notcontains 'tor/tor.exe') { throw "$archive is missing tor/tor.exe (got: $($names -join ', '))" }
|
|
} finally { $zip.Dispose() }
|
|
"{0} {1} {2}" -f $archive, (Get-Item $archive).Length,
|
|
(Get-FileHash $archive -Algorithm SHA256).Hash
|
|
}
|
|
|
|
- name: upload Windows zips
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wowlet-x86_64-windows
|
|
path: |
|
|
wowlet-x86_64-windows.zip
|
|
wowlet-x86_64-windows-unsigned.zip
|
|
if-no-files-found: error
|