Files
hash-wallet/.github/workflows/build-windows.yml
T
jwinterm 94bf0e8c41 ci(windows): enable Developer Mode for plugin symlink support
flutter build windows materialises plugins under
windows/flutter/ephemeral/.plugin_symlinks/ as NTFS symlinks. Flutter gates
this on the AppModelUnlock registry key rather than the actual privilege, so
the build failed with "Building with plugins requires symlink support".

Sets AllowDevelopmentWithoutDevLicense=1 in HKLM (what the Settings toggle
does); LocalSystem can write it, so no interactive login. Idempotent.
2026-08-07 12:38:36 -04:00

444 lines
22 KiB
YAML

name: Hash Bags Windows build
# Phase 1 of Windows CI: produce an unsigned Windows release bundle (.zip of
# the Flutter Release output + MSVC runtime DLLs). Drop the zip on any Win
# 10/11 box, extract, run hash_wallet.exe.
#
# Phase 2 (separate iteration): wire up Inno Setup installer once the
# scripts/windows/build_exe_installer.iss file is rebranded from Cake Wallet
# → Hash Bags.
on:
# Manual-only. Trigger via Actions → "Hash Bags Windows build" → Run workflow.
# workflow_dispatch runs as the triggering user, so secrets are always
# available (PR triggers from forks would strip them — not relevant here
# since the runner is self-hosted and listening on a self-hosted Gitea).
workflow_dispatch:
# Cancel in-flight runs when a newer commit lands on the same branch.
concurrency:
group: windows-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
# Explicit Git Bash path (not `shell: bash`) because Windows resolves
# bare `bash` to C:\Windows\System32\bash.exe (WSL), which refuses to
# run when the act_runner service is hosted under LocalSystem:
# "Running WSL as local system is not supported."
# Git Bash works fine under LocalSystem and matches how GitHub's
# hosted windows-latest runner invokes bash.
shell: '"C:\Program Files\Git\bin\bash.exe" --noprofile --norc -eo pipefail {0}'
jobs:
build:
# Matches the label our act_runner.exe advertised: "windows:host" → executor=host.
runs-on: windows
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# Belt-and-suspenders fallback that bypasses Node TLS verification for
# actions/* against the self-hosted runner's internal git host. Same
# fallback as build-ios-sim.yml.
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# ---- Git config for LocalSystem ---------------------------------------
# The runner service runs as LocalSystem, which has no global git
# identity. scripts/monero_c/apply_patches.sh (and a few other
# submodule operations) do `git commit`-equivalents that need
# user.email + user.name set. Also pre-empt git's "safe.directory"
# check, which fires when the checkout dir owner != runner identity.
- name: Configure git identity + safe.directory
run: |
git config --global --add safe.directory '*'
git config --global user.email "ci@suchsoftware.com"
git config --global user.name "Hash Bags CI"
git config --global --list
# ---- Rust toolchain ---------------------------------------------------
# The dnssec_proof plugin builds its native lib through cargokit
# (windows/CMakeLists.txt -> apply_cargokit), so `flutter build windows`
# genuinely needs cargo/rustc — the prebuilt monero_c bundle doesn't
# cover it.
#
# setup-windows-runner.ps1 installs Rust to C:\Rust and puts
# C:\Rust\cargo\bin on the *Machine* PATH, but the act_runner service
# only picks up Machine PATH changes when it restarts, and a rebuilt or
# re-imaged box loses it entirely. Either way the job died at exit 127
# ("rustc: command not found"). Locate it ourselves, and install it if
# it really isn't there, so CI doesn't depend on the box's env being
# right. No-op when rustc is already on PATH.
- name: Ensure Rust toolchain is on PATH
run: |
set -e
if command -v rustc >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1; then
echo "Rust already on PATH: $(command -v rustc)"
exit 0
fi
# Known install locations, in preference order. First is where
# setup-windows-runner.ps1 puts it; the rest cover a rustup-init
# run under a normal user account.
CANDIDATES=(
"/c/Rust/cargo/bin"
"$USERPROFILE/.cargo/bin"
"/c/Users/Administrator/.cargo/bin"
"/c/Windows/System32/config/systemprofile/.cargo/bin"
)
for d in "${CANDIDATES[@]}"; do
if [[ -x "$d/rustc.exe" ]]; then
echo "Found Rust at $d — adding to PATH for later steps"
export PATH="$d:$PATH"
# $GITHUB_PATH wants a Windows-style path; MSYS converts the
# resulting semicolon PATH back to POSIX form on shell start.
cygpath -w "$d" >> "$GITHUB_PATH"
# rustup's shims resolve toolchains via RUSTUP_HOME; without it
# they look in %USERPROFILE%\.rustup, which is empty for the
# LocalSystem service account.
if [[ -d "$(dirname "$(dirname "$d")")/rustup" ]]; then
RUSTUP_DIR="$(dirname "$(dirname "$d")")/rustup"
echo "RUSTUP_HOME=$(cygpath -w "$RUSTUP_DIR")" >> "$GITHUB_ENV"
echo "CARGO_HOME=$(cygpath -w "$(dirname "$d")")" >> "$GITHUB_ENV"
fi
rustc --version
exit 0
fi
done
echo "Rust not found in any known location — installing to C:\\Rust"
CARGO_HOME="C:\\Rust\\cargo"
RUSTUP_HOME="C:\\Rust\\rustup"
export CARGO_HOME RUSTUP_HOME
mkdir -p /c/Rust/cargo /c/Rust/rustup
# act_runner sets RUNNER_TEMP, but don't hard-depend on it.
DL_DIR="${RUNNER_TEMP:-/c/Rust}"
curl -fsSL -o "$DL_DIR/rustup-init.exe" https://win.rustup.rs/x86_64
"$DL_DIR/rustup-init.exe" -y \
--default-toolchain stable \
--profile minimal \
--target x86_64-pc-windows-msvc \
--no-modify-path
echo "C:\\Rust\\cargo\\bin" >> "$GITHUB_PATH"
echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV"
echo "RUSTUP_HOME=$RUSTUP_HOME" >> "$GITHUB_ENV"
/c/Rust/cargo/bin/rustc --version
# ---- Windows Developer Mode (symlink support) ------------------------
# `flutter build windows` materialises each plugin under
# windows/flutter/ephemeral/.plugin_symlinks/ as a real NTFS symlink, and
# creating one needs SeCreateSymbolicLinkPrivilege. Flutter doesn't test
# the privilege directly — it reads the AppModelUnlock registry key and
# bails with:
# "Building with plugins requires symlink support.
# Please enable Developer Mode in your system settings."
# Setting the key is exactly what the Settings toggle does; HKLM is
# writable by the LocalSystem service account, so no interactive login
# is involved. Idempotent — a no-op once the value is 1.
- name: Enable Developer Mode (symlink support for plugin builds)
run: |
set -e
powershell -NoProfile -Command '
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null }
$cur = (Get-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense
if ($cur -eq 1) {
Write-Host "Developer Mode already enabled"
} else {
Set-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense -Value 1 -Type DWord
Write-Host "Developer Mode enabled (was: $cur)"
}
'
# ---- Toolchain sanity ------------------------------------------------
# Our self-hosted runner has Flutter / Rust / VS Build Tools / Inno
# Setup pre-installed (see scripts/windows/setup-windows-runner.ps1).
# This step just prints versions so failures are easy to diagnose.
- name: Show installed toolchain versions
run: |
set -x
# PATH may need refreshing after install — refresh from the registry.
# Git Bash inherits Windows PATH at shell start, so just printing
# `which` is enough.
which flutter dart cargo rustc git 2>/dev/null || true
flutter --version
dart --version
rustc --version
cargo --version
# ---- External path-dep plugins ----------------------------------------
# Several plugins are path-deps in pubspec.yaml. The path directories
# must exist BEFORE `flutter pub get` runs against the regenerated
# pubspec, or pub fails with "could not find package X at <path>".
#
# The Android + iOS + Linux workflows use upstream-published .tar.gz
# tarballs. We don't, because those tarballs contain example/*
# symlinks pointing to absolute /home/runner/.pub-cache/ paths
# (artifacts from the upstream build host) — Linux tar tolerates
# dangling symlinks, Windows tar bails the whole extraction. Clone
# the source repos at the matching tagged versions instead. Same
# pattern as bitbox_flutter further down.
- name: Clone torch_dart at v1.0.17
run: |
set -x -e
pushd scripts
rm -rf torch_dart
git clone --depth 1 --branch v1.0.17 https://github.com/MrCyjaneK/torch_dart
popd
- name: Clone reown_flutter at v0.0.4
run: |
set -x -e
pushd scripts
rm -rf reown_flutter
git clone --depth 1 --branch v0.0.4 https://github.com/cake-tech/reown_flutter
popd
- name: Clone BitBox Flutter
run: |
# Pubspec has bitbox_flutter as a path dep — the directory must
# exist for pub get even if BitBox Windows support isn't
# functional. Same approach as the iOS workflow: clone the repo
# at a pinned commit, don't run any Android-binding build step.
set -x -e
pushd scripts
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
popd
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
# The same release-bundle.zip that Android + iOS use. Contains
# pre-cross-compiled native libs for many target triples. We'll need
# the x86_64-w64-mingw32 (Windows MinGW) or x86_64-pc-windows-msvc
# entries — inspection step below logs what's actually shipped.
- name: Fetch prebuilt monero_c bundle
run: |
set -x -e
./scripts/prepare_moneroc.sh
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
echo "monero_c TAG: $MONERO_C_TAG"
mkdir -p "scripts/monero_c/release/$MONERO_C_TAG"
pushd "scripts/monero_c/release/$MONERO_C_TAG"
curl -fsSL -O https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
unzip -q release-bundle.zip
rm release-bundle.zip
echo "=== bundle contents (top level) ==="
ls
popd
- name: Inspect Windows targets in monero_c bundle
run: |
set -x
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG"
echo "=== full mingw32 dir contents ==="
ls -la "$BUNDLE_DIR/x86_64-w64-mingw32" 2>/dev/null || true
# ---- Stage monero_c DLLs into the layout windows/CMakeLists.txt expects
# Upstream Cake's Windows build cross-compiles monero_c on Linux via
# scripts/windows/build_all.sh, which lays files out as:
# scripts/monero_c/release/<coin>/<triple>_<basename>.dll
# The CMakeLists.txt at windows/CMakeLists.txt:84-94 hardcodes those
# paths in install(FILES ...) rules — it copies + RENAMEs them to the
# final names (monero_libwallet2_api_c.dll etc.) in the Release dir.
#
# We're using the prebuilt bundle from MrCyjaneK's release, which has
# a different layout:
# scripts/monero_c/release/<TAG>/<triple>/lib<coin>_wallet2_api_c.dll
# So we restage the bundle's files into the upstream-build-all-style
# layout before flutter build windows runs.
- name: Stage monero_c bundle into upstream cross-compile layout
run: |
set -x -e
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
SRC="scripts/monero_c/release/$MONERO_C_TAG/x86_64-w64-mingw32"
MONERO_DST="scripts/monero_c/release/monero"
WOWNERO_DST="scripts/monero_c/release/wownero"
mkdir -p "$MONERO_DST" "$WOWNERO_DST"
# The wallet C-API DLLs, one per coin
cp -v "$SRC/libmonero_wallet2_api_c.dll" "$MONERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll"
cp -v "$SRC/libwownero_wallet2_api_c.dll" "$WOWNERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll"
# MinGW runtime DLLs (libssp-0, libwinpthread-1) — the wallet libs
# are dynamically linked against these. MrCyjaneK's prebuilt
# release-bundle.zip does NOT ship them (only the wallet *.dll +
# *.dll.a import libs), so we source them from Git Bash's bundled
# MinGW-w64 distribution, which ships the same x86_64-w64-mingw32
# ABI used to build the wallet libs.
# ORDERING MATTERS: monero_c's prebuilt wallet DLLs are built against
# recent MinGW-w64 (post-2022 winpthreads ABI with pthread_cond_timedwait64).
# Flutter's bundled mingit ships a 2017-vintage libwinpthread that's
# missing newer symbols and crashes wallet creation at runtime. Prefer
# Git for Windows' mingw64/bin which is updated frequently.
RUNTIME_PATHS=(
"/c/Program Files/Git/mingw64/bin" # Git for Windows — modern MinGW runtime (preferred)
"$SRC" # monero_c bundle (in case it grows DLLs)
"/c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin" # choco install -y mingw
"/c/flutter/bin/mingit/mingw64/bin" # Flutter's bundled mingit — old (2017), use only as last resort
"/mingw64/bin" # MSYS2 fallback
)
for runtime in libssp-0.dll libwinpthread-1.dll; do
src=""
for d in "${RUNTIME_PATHS[@]}"; do
if [[ -f "$d/$runtime" ]]; then
src="$d/$runtime"
break
fi
done
if [[ -z "$src" ]]; then
echo "FATAL: $runtime not found in any of:"
printf ' %s\n' "${RUNTIME_PATHS[@]}"
for d in "${RUNTIME_PATHS[@]}"; do
echo "--- contents of $d (if exists) ---"
ls "$d" 2>/dev/null | head -20 || echo "(missing)"
done
exit 1
fi
cp -v "$src" "$MONERO_DST/x86_64-w64-mingw32_${runtime}"
done
echo "=== final staged layout ==="
ls -la "$MONERO_DST/" "$WOWNERO_DST/"
# ---- Configure: pubspec.yaml + per-coin enablement -------------------
# Mirror of hashwallet.bat — drives tool/configure.dart with the same
# set of coin flags that upstream Cake's Windows build uses.
- name: Configure pubspec + Wallet types
run: |
set -x -e
cp -f pubspec_description.yaml pubspec.yaml
flutter pub get
dart run tool/generate_pubspec.dart
flutter pub get
dart run tool/configure.dart \
--monero --bitcoin --ethereum --polygon --nano --bitcoinCash \
--wownero --dogecoin --base --arbitrum --bsc
# ---- Secrets ---------------------------------------------------------
- name: Generate per-module secrets.g.dart files (empty defaults)
run: dart run tool/generate_new_secrets.dart
- name: Inject Trocador affiliate secrets into lib/.secrets.g.dart
env:
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }}
TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }}
run: |
# Length-only debug visibility — never prints actual secrets.
echo "TROCADOR_API_KEY length: ${#TROCADOR_API_KEY}"
echo "TROCADOR_MONERO_API_KEY length: ${#TROCADOR_MONERO_API_KEY}"
echo "TROCADOR_EXCHANGE_MARKUP length: ${#TROCADOR_EXCHANGE_MARKUP}"
if [[ -z "$TROCADOR_API_KEY" ]]; then
echo "WARN: TROCADOR_API_KEY not reaching runner — build will proceed"
echo " but Trocador exchange features won't work in this binary."
fi
sed -i \
-e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \
-e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \
-e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \
lib/.secrets.g.dart
grep '^const trocador' lib/.secrets.g.dart
# ---- Codegen ---------------------------------------------------------
- name: Build generated code (mobx + hive adapters)
run: bash model_generator.sh
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# ---- Build the Windows .exe ------------------------------------------
- name: Build Windows release
run: |
set -x -e
flutter config --enable-windows-desktop
flutter build windows --dart-define-from-file=env.json --release --verbose
# ---- Reconcile monero/wownero DLL naming for Dart FFI ----------------
# windows/CMakeLists.txt's install(FILES ... RENAME ...) rules produce
# <coin>_libwallet2_api_c.dll, but package:monero's Dart FFI loader
# (mrcyjanek's package) opens lib<coin>_wallet2_api_c.dll — different
# filename layout, runtime error "Failed to load dynamic library".
# Create the lib-prefixed copies so both name conventions resolve.
- name: Mirror monero/wownero DLLs under lib-prefixed names
run: |
set -x -e
DST="build/windows/x64/runner/Release"
for coin in monero wownero; do
src="$DST/${coin}_libwallet2_api_c.dll"
dst="$DST/lib${coin}_wallet2_api_c.dll"
if [[ -f "$src" ]]; then
cp -v "$src" "$dst"
else
echo "WARN: $src not in Release dir — CMake install may have skipped it"
ls "$DST" | head -20
fi
done
# ---- Bundle MSVC runtime DLLs with the .exe --------------------------
# Standalone Windows builds need msvcp140.dll + vcruntime140.dll +
# vcruntime140_1.dll next to the .exe (or installed via vc_redist on
# the user's machine). Bundling avoids the "VCRUNTIME140.dll not found"
# error on machines without VC++ redistributable installed.
- name: Copy MSVC runtime DLLs next to the .exe
run: |
set -x -e
# Find the redist dir for the currently-installed MSVC version (the
# version number rolls forward with VS updates, so do NOT hardcode).
REDIST_BASE="/c/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Redist/MSVC"
REDIST_DIR=$(find "$REDIST_BASE" -type d -name 'Microsoft.VC*.CRT' -path '*/x64/*' 2>/dev/null | sort | tail -1)
if [[ -z "$REDIST_DIR" ]]; then
echo "FATAL: could not find VC redist dir under $REDIST_BASE"
ls "$REDIST_BASE" 2>/dev/null || echo "(redist base dir missing entirely)"
exit 1
fi
echo "Using redist dir: $REDIST_DIR"
cp -v "$REDIST_DIR/msvcp140.dll" "$REDIST_DIR/vcruntime140.dll" "$REDIST_DIR/vcruntime140_1.dll" \
build/windows/x64/runner/Release/
# ---- Package + upload ------------------------------------------------
- name: List built artifacts (for debug visibility)
run: |
ls -la build/windows/x64/runner/Release/
# ---- Package + upload as zip -----------------------------------------
# The Flutter Windows build produces a directory tree (~500 files —
# HashWallet.exe + plugin DLLs + data/ + locales/ + bundled monero_c
# + MinGW runtime DLLs). Ship as a zip; users extract and run.
#
# Tried wrapping with 7-Zip SFX for a single-file "portable" .exe
# earlier but path_provider_windows computes the wallet storage dir
# from the .exe location, and SFX extracts to a different %TEMP%
# subdir on every launch — wallet keys couldn't be found across
# runs. Multi-file zip is what Firefox Portable / VS Code Portable
# / every other Flutter Windows desktop app does too.
- name: Create Hash Bags Windows release zip
run: |
set -x -e
cd build/windows/x64/runner/Release
# PowerShell's Compress-Archive is in PATH; no extra deps needed.
powershell -NoProfile -Command "Compress-Archive -Path .\* -DestinationPath ..\..\..\..\..\HashBags-windows-${{ github.sha }}.zip -Force"
cd "$GITHUB_WORKSPACE"
ls -la HashBags-windows-*.zip
echo "Size: $(du -h HashBags-windows-${{ github.sha }}.zip | cut -f1)"
- name: Upload Hash Bags Windows zip
uses: actions/upload-artifact@v3
with:
name: hash-bags-windows-${{ github.sha }}
path: HashBags-windows-${{ github.sha }}.zip
retention-days: 14