Files
ObsidianDragon/build.sh
T
DanSandClaude Opus 5.5 3ac46abe1a fix(security): compile the UI sweep only into dev builds
The screenshot sweep and the full UI sweep are dev tools. The full sweep
injects demo data into the live wallet state and the real data and
config dirs, and the tab sweep screenshots real balances and addresses.
Both were compiled into every build: the Settings buttons were
reachable in full-node releases, and the DRAGONX_FULL_SWEEP env hook
worked in every build, Lite releases included.

A default-OFF option alone would fail open: dev and release builds
share build/linux (and build/mac), so a release configured without
--clean would inherit a cached ON from the last dev configure.

- CMake: option(DRAGONX_ENABLE_UI_SWEEP ... OFF), passed to
  ObsidianDragon and ObsidianDragonTests as a compile definition.
- build.sh passes it explicitly: ON in the dev build, OFF in every
  release configure (Linux, Windows, macOS osxcross and native).
- Packaging guard: while the cache says ON, dragonx_release depends on
  dragonx_release_sweep_guard, which fails, so a hand-configured ON
  tree cannot package a release.
- startSweepImpl is a no-op unless the option is ON (an undefined macro
  counts as 0, so it fails closed). The enabled branch logs the
  sentinel "[Sweep] DRAGONX_UI_SWEEP_COMPILED_IN", so a release can be
  checked with `strings <bin> | grep -c DRAGONX_UI_SWEEP_COMPILED_IN`
  (must be 0).
- main.cpp: the DRAGONX_FULL_SWEEP hook is compiled out (autoSweep is
  false). It must be: with the sweep a no-op, the hook would quit the
  app once its warm-up frames ran out.
- Settings > Node > Debug options: both sweep buttons, open-dir, the
  seed-demo-chat button and the current-theme checkbox are compiled only
  into dev builds. The daemon debug categories are unchanged.

No i18n change: the existing strings stay in the tables.

Verified with an isolated release (DRAGONX_BUILD_ROOT/DRAGONX_RELEASE_ROOT
in a scratch dir) whose tree was first configured by hand with ON:
build.sh --linux-release without --clean leaves the cache OFF, and the
sentinel count is 0 in the built binary, the zip and the AppImage.
With the cache set back to ON by hand, --target dragonx_release fails
at the guard before compiling anything. Dev trees configured ON keep
the sweep (sentinel present; a one-surface DRAGONX_FULL_SWEEP run
against the fake daemon captured and exited cleanly).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00

1507 lines
75 KiB
Bash
Executable File

#!/bin/bash
# DragonX ImGui Wallet - Unified Build Script
# Copyright 2024-2026 The Hush Developers
# Released under the GPLv3
#
# Usage:
# ./build.sh # Dev build (Linux, debug-friendly)
# ./build.sh --linux-release # Linux release (zip + AppImage)
# ./build.sh --win-release # Windows cross-compile (mingw-w64)
# ./build.sh --mac-release # macOS .app bundle + DMG
# ./build.sh --linux-release --win-release # Multiple targets
# ./build.sh --clean --win-release # Clean first, then build
#
# Prerequisites:
# Linux: cmake, g++, libsdl3-dev (or fetched), libsodium-dev
# Windows: mingw-w64 (posix threads), cmake
# macOS (native): Xcode CLT, cmake, create-dmg (brew install create-dmg)
# macOS (cross from Linux): osxcross + macOS SDK, genisoimage, icnsutils
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
# parsing — see the APP_BASENAME block below. Placeholder until then.
VERSION=""
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; }
header(){ echo -e "\n${CYAN}══════════════════════════════════════════════════════════${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}══════════════════════════════════════════════════════════${NC}"; }
JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
# ── Defaults ─────────────────────────────────────────────────────────────────
DO_DEV=false
DO_LINUX=false
DO_WIN=false
DO_MAC=false
DO_LITE=false
DO_LITE_BACKEND=false
CLEAN=false
BUILD_TYPE="Release"
usage() {
cat <<EOF
DragonX Wallet — Unified Build Script
Usage: $0 [options]
Targets (at least one required, or none for dev build):
--linux-release Linux release (zip + AppImage) -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
(auto-discovers build/lite-backend/<platform>/; build it with
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
Build trees are stored under build/{linux,windows,mac}/
Options:
-c, --clean Remove build artifacts before building
-d, --debug Use Debug build type instead of Release
-j N Parallel jobs (default: $JOBS)
-h, --help Show this help
Cross-compiling from Linux:
Windows: sudo apt install mingw-w64
macOS: Requires osxcross (https://github.com/tpoechtrager/osxcross)
export OSXCROSS=/path/to/osxcross
sudo apt install genisoimage icnsutils # DMG + icon tools
Examples:
$0 # Quick dev build (Linux)
$0 --linux-release # Linux release (zip + AppImage)
$0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
$0 --clean --linux-release --win-release # Clean + both
EOF
exit 0
}
# ── Parse args ───────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case $1 in
--linux-release) DO_LINUX=true; shift ;;
--win-release) DO_WIN=true; shift ;;
--mac-release) DO_MAC=true; shift ;;
--lite) DO_LITE=true; shift ;;
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
-c|--clean) CLEAN=true; shift ;;
-d|--debug) BUILD_TYPE="Debug"; shift ;;
-j) JOBS="$2"; shift 2 ;;
-h|--help) usage ;;
*) err "Unknown option: $1"; usage ;;
esac
done
# No release target → dev build (native, no packaging)
if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
DO_DEV=true
fi
APP_BASENAME="ObsidianDragon"
CMAKE_LITE_ARGS=()
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
if $DO_LITE; then
APP_BASENAME="ObsidianDragonLite"
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
info "Lite mode enabled: building ${APP_BASENAME}"
else
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
fi
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
if $DO_LITE; then
VERSION="${_lite_ver}${_lite_suffix}"
else
VERSION="${_full_ver}${_full_suffix}"
fi
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
info "Release version: ${VERSION} (${APP_BASENAME})"
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
# override the directory with DRAGONX_LITE_BACKEND_DIR.
if $DO_LITE_BACKEND; then
# Artifact platform follows the cross target when exactly one non-host release is requested,
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
case "$(uname -s)" in
Linux) lb_platform="linux" ;;
Darwin) lb_platform="macos" ;;
*) lb_platform="linux" ;;
esac
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
lb_lib=""
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
done
lb_symbols="$lb_dir/lite-backend-symbols.txt"
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
err "Lite backend artifact not found under: $lb_dir"
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
exit 1
fi
CMAKE_LITE_ARGS+=(
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
)
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
if [[ "$lb_platform" == "windows" ]]; then
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
fi
info "Lite backend enabled ($lb_platform): $lb_lib"
else
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
# (which would then fail the BUILD_LITE=OFF guard in CMake).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
fi
should_bundle_full_node_assets() {
! $DO_LITE
}
# The offline wallet-rebuild helper is the ONLY thing that repairs a genuinely BDB-inconsistent
# wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade. A full-node release must
# NEVER ship without it (the in-app "Repair automatically" option silently disappears otherwise), so
# treat a missing helper as a HARD build failure instead of degrading recovery to Restore-only.
# $1 = built helper path (e.g. bin/dragonx-wallet-rebuild[.exe]); $2 = the BDB depends dir for the hint.
require_wallet_rebuild_helper() {
local helper="$1" depends="$2"
should_bundle_full_node_assets || return 0 # lite builds have no BDB wallet.dat to rebuild
if [[ ! -f "$helper" ]]; then
err "wallet-rebuild helper was NOT built: $helper"
err " → the recovery 'Repair automatically' option would be MISSING from this release."
err " Cause: the vendored Berkeley DB depends are absent, so CMake skipped the dragonx-wallet-rebuild target."
err " Fix: provide ${depends}/{lib/libdb-6.2.a,include/db.h} (same static libdb the daemon links), then rebuild."
exit 1
fi
info " wallet-rebuild helper present: $helper"
}
# Vendored Berkeley DB depends directory for the macOS wallet-rebuild helper, by TARGET arch. The daemon
# links a static libdb-6.2; the helper must match the build arch, so derive the triple from it rather than
# hardcoding one. Single-arch: x86_64 -> x86_64-apple-darwin, arm64 -> aarch64-apple-darwin. "universal"
# (or anything unexpected) falls back to the host arch — a universal .app needs a universal libdb, so if
# only a single-arch libdb is vendored, build single-arch via DRAGONX_MAC_ARCHS.
mac_bdb_dir() {
local arch="$1" triple
case "$arch" in
x86_64) triple="x86_64-apple-darwin" ;;
arm64) triple="aarch64-apple-darwin" ;;
*) [[ "$(uname -m)" == "arm64" ]] && triple="aarch64-apple-darwin" || triple="x86_64-apple-darwin" ;;
esac
printf '%s/external/dragonx/depends/%s\n' "$SCRIPT_DIR" "$triple"
}
# ── Helper: find resource files ──────────────────────────────────────────────
find_sapling_params() {
local dirs=(
"$HOME/.zcash-params"
"$HOME/.hush-params"
"$SCRIPT_DIR/../SilentDragonX"
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux"
"$SCRIPT_DIR"
)
for d in "${dirs[@]}"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_SPEND="$d/sapling-spend.params"
SAPLING_OUTPUT="$d/sapling-output.params"
info "Found Sapling params in $d"
return 0
fi
done
warn "Sapling params not found — embedded resources will be unavailable"
return 1
}
find_asmap() {
local paths=(
"$SCRIPT_DIR/external/dragonx/asmap.dat"
"$SCRIPT_DIR/external/dragonx/contrib/asmap/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/asmap.dat"
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-win/asmap.dat"
)
for p in "${paths[@]}"; do
if [[ -f "$p" ]]; then
ASMAP_DAT="$p"
info "Found asmap.dat at $p"
return 0
fi
done
return 1
}
# ── Helper: bundle daemon binaries into a target dir ─────────────────────────
bundle_linux_daemon() {
local dest="$1"
local found=0
local daemon_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/dragonxd"; chmod +x "$dest/dragonxd"
info " Bundled dragonxd"; found=1; break
fi
done
local cli_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonx-cli"
"$SCRIPT_DIR/../dragonx-cli"
"$SCRIPT_DIR/external/dragonx/src/dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/dragonx-cli"; chmod +x "$dest/dragonx-cli"
info " Bundled dragonx-cli"; break
fi
done
local dragonxd_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${dragonxd_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/dragonxd"; chmod +x "$dest/dragonxd"
info " Bundled dragonxd"; break
fi
done
# asmap.dat
find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat"
# (The dragonx-wallet-rebuild recovery helper is built into bin/ by CMake and packaged explicitly
# by each release path — required via require_wallet_rebuild_helper — so it is not copied here.)
return $found
}
# ═══════════════════════════════════════════════════════════════════════════════
# DEV BUILD (native, no packaging)
# ═══════════════════════════════════════════════════════════════════════════════
build_dev() {
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
# Use platform-appropriate build directory
if [[ "$(uname -s)" == "Darwin" ]]; then
local bd="$SCRIPT_DIR/build/mac"
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
local bd="$SCRIPT_DIR/build/linux"
fi
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
fi
mkdir -p "$bd" && cd "$bd"
info "Configuring ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
-DDRAGONX_ENABLE_UI_SWEEP=ON \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
}
# ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage
# ═══════════════════════════════════════════════════════════════════════════════
build_release_linux() {
header "Release: Linux x86_64"
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/linux"
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/linux"
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
fi
mkdir -p "$bd" && cd "$bd"
# ── Compile ──────────────────────────────────────────────────────────────
# Point the wallet-rebuild helper at the vendored static Berkeley DB (same libdb the daemon links)
# so its output is a v6.2 btree the bundled dragonxd reads. Pass the paths EXPLICITLY (bypasses
# find_library + its cache); the helper target is simply not built if the depends tree is absent.
local lin_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-unknown-linux-gnu"
local BDB_ARGS=()
if [[ -f "$lin_bdb/lib/libdb-6.2.a" && -f "$lin_bdb/include/db.h" ]]; then
BDB_ARGS=( -DBDB_INCLUDE_DIR="$lin_bdb/include" -DBDB_LIBRARY="$lin_bdb/lib/libdb-6.2.a" )
elif should_bundle_full_node_assets; then
err "Vendored Berkeley DB depends missing at $lin_bdb — the wallet-rebuild recovery helper cannot be built."
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
exit 1
fi
info "Configuring ..."
# UI sweep OFF explicitly: build/linux is shared with dev builds, which cache it ON.
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
"${BDB_ARGS[@]}" \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS" --target dragonx_release
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$lin_bdb"
info "Stripping ..."
strip "bin/${APP_BASENAME}"
[[ -f "bin/dragonx-wallet-rebuild" ]] && strip "bin/dragonx-wallet-rebuild"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
if should_bundle_full_node_assets; then
# ── Bundle daemon ────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
else
info "Lite mode: skipping daemon and Sapling/asmap bundling"
fi
# ── Package: release/linux/ ──────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}" "$dist_dir/"
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
cp bin/dragonx-wallet-rebuild "$dist_dir/" && chmod +x "$dist_dir/dragonx-wallet-rebuild"
info " Bundled dragonx-wallet-rebuild"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
# ── AppImage (single-file) ───────────────────────────────────────────────
info "Creating AppImage ..."
local APPDIR="$bd/AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" \
"$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
"$APPDIR/usr/share/ObsidianDragon/res"
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
cp bin/dragonx-wallet-rebuild "$APPDIR/usr/bin/" && chmod +x "$APPDIR/usr/bin/dragonx-wallet-rebuild"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
[Desktop Entry]
Type=Application
Name=DragonX Wallet
Comment=DragonX Cryptocurrency Wallet
Exec=${APP_BASENAME}
Icon=ObsidianDragon
Categories=Finance;Network;
Terminal=false
StartupNotify=true
DESK
cp "$APPDIR/usr/share/applications/ObsidianDragon.desktop" "$APPDIR/"
# Icon
if [[ -f "$SCRIPT_DIR/res/icons/dragonx-256.png" ]]; then
cp "$SCRIPT_DIR/res/icons/dragonx-256.png" \
"$APPDIR/usr/share/icons/hicolor/256x256/apps/ObsidianDragon.png"
else
cat > "$APPDIR/ObsidianDragon.svg" <<'SVG'
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" rx="32" fill="#1a1a1a"/>
<circle cx="128" cy="128" r="80" fill="none" stroke="#2e7d32" stroke-width="8"/>
<text x="128" y="145" font-family="Arial,sans-serif" font-size="72" font-weight="bold"
fill="#4caf50" text-anchor="middle">DX</text>
</svg>
SVG
if command -v rsvg-convert &>/dev/null; then
rsvg-convert -w 256 -h 256 "$APPDIR/ObsidianDragon.svg" > \
"$APPDIR/usr/share/icons/hicolor/256x256/apps/ObsidianDragon.png"
fi
fi
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/ObsidianDragon.png" "$APPDIR/" 2>/dev/null || \
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
# AppRun
cat > "$APPDIR/AppRun" <<APPRUN
#!/bin/bash
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
cd "\${HERE}/usr/share/ObsidianDragon"
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
APPRUN
chmod +x "$APPDIR/AppRun"
# Bundle SDL3
for lib in libSDL3.so; do
local lp
lp=$(ldconfig -p 2>/dev/null | grep "$lib" | head -1 | awk '{print $NF}')
[[ -n "$lp" && -f "$lp" ]] && cp "$lp" "$APPDIR/usr/lib/" 2>/dev/null || true
done
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
local APPIMAGETOOL=""
if command -v appimagetool &>/dev/null; then
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
else
local at="$bd/appimagetool-x86_64.AppImage"
# Re-verify any cached copy too; a stale unverified download must not be trusted.
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
info "Downloading appimagetool 1.9.0 (pinned) ..."
wget -q -O "$at" "$APPIMAGETOOL_URL"
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
err "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$at"
return 1
fi
chmod +x "$at"
fi
APPIMAGETOOL="$at"
fi
local ARCH
ARCH=$(uname -m)
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/"
info "Linux release artifacts: $out/"
ls -lh "$out/"
}
# ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: WINDOWS — mingw-w64 cross-compile + INCBIN embedding
# ═══════════════════════════════════════════════════════════════════════════════
build_release_win() {
header "Release: Windows x86_64 (cross-compile)"
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/windows"
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/windows"
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
fi
mkdir -p "$bd" && cd "$bd"
# ── Find MinGW ───────────────────────────────────────────────────────────
local MINGW_GCC="" MINGW_GXX=""
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
MINGW_GCC="x86_64-w64-mingw32-gcc-posix"
MINGW_GXX="x86_64-w64-mingw32-g++-posix"
info "Using POSIX thread model"
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
MINGW_GCC="x86_64-w64-mingw32-gcc"
MINGW_GXX="x86_64-w64-mingw32-g++"
if x86_64-w64-mingw32-gcc -v 2>&1 | grep -q "posix"; then
info "Using POSIX thread model"
else
warn "Using win32 thread model — may have threading issues"
warn "Switch: sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix"
fi
else
err "mingw-w64 not found! Install: sudo apt install mingw-w64"
exit 1
fi
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
# collides with ours during .rsrc merge, causing Task Manager to show
# the wrong process description.
local PATCHED_LIB_DIR="$bd/patched-lib"
mkdir -p "$PATCHED_LIB_DIR"
for plib in libwinpthread.a libpthread.a; do
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
if [[ -f "$SYS_LIB" ]]; then
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
info "Patched $plib (removed version.o VERSIONINFO resource)"
fi
done
# ── Toolchain file ───────────────────────────────────────────────────────
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER $MINGW_GCC)
set(CMAKE_CXX_COMPILER $MINGW_GXX)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(BUILD_SHARED_LIBS OFF)
TOOLCHAIN
# ── INCBIN embedded resources ────────────────────────────────────────────
info "Generating embedded resources (INCBIN) ..."
local GEN="$bd/generated"
local RES="$GEN/res"
mkdir -p "$RES"
SAPLING_SPEND="" SAPLING_OUTPUT="" ASMAP_DAT=""
local EMBED_NODE_DATA=false
should_bundle_full_node_assets && EMBED_NODE_DATA=true
if $EMBED_NODE_DATA; then
find_sapling_params || true
find_asmap || true
fi
# Lite needs no Sapling params/asmap (its backend embeds its own), so it always gets a header for
# images/themes/xmrig; the full node still requires the params to be found.
if ! $EMBED_NODE_DATA || [[ -n "$SAPLING_SPEND" && -n "$SAPLING_OUTPUT" ]]; then
cat > "$GEN/embedded_data.h" <<HDR
// Auto-generated — DO NOT EDIT (build.sh --win-release)
#pragma once
#include <cstdint>
#include <cstddef>
#include "incbin.h"
HDR
if $EMBED_NODE_DATA; then
cp -f "$SAPLING_SPEND" "$RES/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "$RES/sapling-output.params"
cat >> "$GEN/embedded_data.h" <<HDR
INCBIN(sapling_spend_params, "$RES/sapling-spend.params");
INCBIN(sapling_output_params, "$RES/sapling-output.params");
HDR
if [[ -n "$ASMAP_DAT" ]]; then
cp -f "$ASMAP_DAT" "$RES/asmap.dat"
echo "INCBIN(asmap_dat, \"$RES/asmap.dat\");" >> "$GEN/embedded_data.h"
else
echo 'extern "C" { static const uint8_t* g_asmap_dat_data = nullptr; }' >> "$GEN/embedded_data.h"
echo 'static const unsigned int g_asmap_dat_size = 0;' >> "$GEN/embedded_data.h"
fi
else
info "Lite mode: not embedding Sapling params/asmap (the SDXL backend embeds its own)"
for sym in sapling_spend_params sapling_output_params asmap_dat; do
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
done
fi
# ── Daemon binaries ──────────────────────────────────────────────
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
else
info "Lite mode: skipping embedded daemon binaries"
fi
# ── Wallet-rebuild recovery helper ───────────────────────────────
# Built in-tree (not a prebuilt like the daemon), so compile it standalone HERE — before the
# main app compiles embedded_resources.cpp — and INCBIN it, so a bare, self-extracting
# ObsidianDragon.exe carries the recovery tool exactly like it does the daemon.
if should_bundle_full_node_assets; then
local WBDB="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
if [[ -f "$WBDB/lib/libdb-6.2.a" && -f "$WBDB/include/db.h" ]]; then
info "Compiling + embedding wallet-rebuild helper ..."
x86_64-w64-mingw32-g++ -std=c++17 -O2 -static -static-libgcc -static-libstdc++ \
-I"$SCRIPT_DIR/src" -I"$WBDB/include" \
"$SCRIPT_DIR/tools/wallet_rebuild/main.cpp" \
"$WBDB/lib/libdb-6.2.a" -lws2_32 \
-o "$RES/dragonx-wallet-rebuild.exe" \
|| { err "wallet-rebuild helper failed to compile for embedding"; exit 1; }
x86_64-w64-mingw32-strip "$RES/dragonx-wallet-rebuild.exe" 2>/dev/null || true
echo -e "\n#define HAS_EMBEDDED_WALLET_REBUILD 1" >> "$GEN/embedded_data.h"
echo "INCBIN(dragonx_wallet_rebuild_exe, \"$RES/dragonx-wallet-rebuild.exe\");" >> "$GEN/embedded_data.h"
info " Embedded dragonx-wallet-rebuild.exe ($(du -h "$RES/dragonx-wallet-rebuild.exe" | cut -f1))"
else
err "Vendored mingw Berkeley DB missing at $WBDB — cannot embed the wallet-rebuild recovery helper."
exit 1
fi
fi
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
# no miner ("xmrig binary not found" at runtime).
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
local _xtmp; _xtmp=$(mktemp -d)
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
# non-zero if a pattern matches nothing).
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
if [[ -f "$_xtmp/xmrig.exe" ]]; then
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
info " Extracted xmrig.exe from $(basename "$_xz")"
fi
rm -rf "$_xtmp"
fi
fi
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
echo -e "\n#define HAS_EMBEDDED_XMRIG 1" >> "$GEN/embedded_data.h"
echo "INCBIN(xmrig_exe, \"$RES/xmrig.exe\");" >> "$GEN/embedded_data.h"
else
echo 'extern "C" { static const uint8_t* g_xmrig_exe_data = nullptr; }' >> "$GEN/embedded_data.h"
echo 'static const unsigned int g_xmrig_exe_size = 0;' >> "$GEN/embedded_data.h"
fi
# ── Theme images ─────────────────────────────────────────────────
echo -e "\n// ---- Embedded theme images ----" >> "$GEN/embedded_data.h"
local IMAGE_TABLE="" IMAGE_COUNT=0
for dir in "$SCRIPT_DIR/res/img/backgrounds/texture" "$SCRIPT_DIR/res/img/backgrounds/gradient" "$SCRIPT_DIR/res/img/logos"; do
[[ -d "$dir" ]] || continue
for img in "$dir"/*.png; do
[[ -f "$img" ]] || continue
local bn=$(basename "$img")
local sym=$(echo "$bn" | sed 's/[^a-zA-Z0-9]/_/g')
cp -f "$img" "$RES/$bn"
echo "INCBIN(img_${sym}, \"$RES/$bn\");" >> "$GEN/embedded_data.h"
IMAGE_TABLE+=" { g_img_${sym}_data, g_img_${sym}_size, \"${bn}\" },\n"
IMAGE_COUNT=$((IMAGE_COUNT + 1))
done
done
echo "" >> "$GEN/embedded_data.h"
echo "#define HAS_EMBEDDED_IMAGES 1" >> "$GEN/embedded_data.h"
echo "#define EMBEDDED_IMAGE_COUNT $IMAGE_COUNT" >> "$GEN/embedded_data.h"
echo "#define HAS_EMBEDDED_GRADIENT 1" >> "$GEN/embedded_data.h"
echo "#define HAS_EMBEDDED_LOGO 1" >> "$GEN/embedded_data.h"
# Backward compat aliases
echo "static const uint8_t* g_dark_gradient_png_data = g_img_dark_gradient_png_data;" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_dark_gradient_png_size = g_img_dark_gradient_png_size;" >> "$GEN/embedded_data.h"
echo "static const uint8_t* g_logo_ObsidianDragon_dark_png_data = g_img_logo_ObsidianDragon_dark_png_data;" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_logo_ObsidianDragon_dark_png_size = g_img_logo_ObsidianDragon_dark_png_size;" >> "$GEN/embedded_data.h"
echo "" >> "$GEN/embedded_data.h"
echo "struct EmbeddedImageEntry { const uint8_t* data; unsigned int size; const char* filename; };" >> "$GEN/embedded_data.h"
echo "static const EmbeddedImageEntry s_embedded_images[] = {" >> "$GEN/embedded_data.h"
echo -e "$IMAGE_TABLE" >> "$GEN/embedded_data.h"
echo " { nullptr, 0, nullptr }" >> "$GEN/embedded_data.h"
echo "};" >> "$GEN/embedded_data.h"
# ── Overlay themes ───────────────────────────────────────────────
# Expand skin files with layout sections from ui.toml before embedding
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
local THEME_STAGE_DIR="$bd/_expanded_themes"
mkdir -p "$THEME_STAGE_DIR"
python3 "$SCRIPT_DIR/scripts/expand_themes.py" "$SCRIPT_DIR/res/themes" "$THEME_STAGE_DIR"
local THEME_TABLE="" THEME_COUNT=0
for tf in "$THEME_STAGE_DIR"/*.toml; do
local tbn=$(basename "$tf")
[[ "$tbn" == "ui.toml" ]] && continue
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
cp -f "$tf" "$RES/$tbn"
echo "INCBIN(theme_${tsym}, \"$RES/$tbn\");" >> "$GEN/embedded_data.h"
THEME_TABLE+=" { g_theme_${tsym}_data, g_theme_${tsym}_size, \"${tbn}\" },\n"
THEME_COUNT=$((THEME_COUNT + 1))
done
echo "" >> "$GEN/embedded_data.h"
echo "#define EMBEDDED_THEME_COUNT $THEME_COUNT" >> "$GEN/embedded_data.h"
echo "struct EmbeddedThemeEntry { const uint8_t* data; unsigned int size; const char* filename; };" >> "$GEN/embedded_data.h"
echo "static const EmbeddedThemeEntry s_embedded_themes[] = {" >> "$GEN/embedded_data.h"
echo -e "$THEME_TABLE" >> "$GEN/embedded_data.h"
echo " { nullptr, 0, nullptr }" >> "$GEN/embedded_data.h"
echo "};" >> "$GEN/embedded_data.h"
info "Embedded resources header generated"
else
warn "Building WITHOUT embedded resources (Sapling params not found)"
rm -f "$GEN/embedded_data.h" # never compile a stale header left by the other variant in this shared build dir
fi
# ── Fetch libsodium for Windows if needed ──────────────────────────────
if [[ ! -f "$SCRIPT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
info "Fetching libsodium for Windows ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
fi
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
info "Cross-building FreeType for Windows (color emoji) ..."
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
fi
local FT_CMAKE_ARG=()
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
fi
# ── CMake + build ────────────────────────────────────────────────────────
# The wallet-rebuild helper links the vendored mingw static Berkeley DB (the mingw toolchain's
# find_library is sysroot-only, so pass the depends paths EXPLICITLY to bypass the search). Only
# enabled if the depends tree is present; guarded with -DBDB_* left empty otherwise.
local win_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
local BDB_ARGS=()
if [[ -f "$win_bdb/lib/libdb-6.2.a" && -f "$win_bdb/include/db.h" ]]; then
BDB_ARGS=( -DBDB_INCLUDE_DIR="$win_bdb/include" -DBDB_LIBRARY="$win_bdb/lib/libdb-6.2.a" )
elif should_bundle_full_node_assets; then
err "Vendored Berkeley DB depends missing at $win_bdb — the wallet-rebuild recovery helper cannot be built."
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
exit 1
fi
info "Configuring (cross-compile) ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
"${BDB_ARGS[@]}" \
"${FT_CMAKE_ARG[@]}" \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS" --target dragonx_release
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
# Strip the app exe — the Linux and macOS release paths already strip theirs, and even the Windows
# helper (dragonx-wallet-rebuild.exe) is stripped, but the main app exe was shipping unstripped
# (~5MB of symbols on the full node, ~19MB on the params-heavy lite build). Best-effort.
x86_64-w64-mingw32-strip --strip-all "bin/${APP_BASENAME}.exe" 2>/dev/null \
|| warn " strip unavailable — shipping unstripped ${APP_BASENAME}.exe"
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild.exe" "$win_bdb"
# ── Package: release/windows/ ────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) — required.
cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/" && info " Bundled dragonx-wallet-rebuild.exe"
[[ -f "$dist_dir/dragonx-wallet-rebuild.exe" ]] || { err "Failed to bundle dragonx-wallet-rebuild.exe"; exit 1; }
# Bundle Sapling params + asmap for the zip distribution
# (The single-file exe has these embedded via INCBIN, but the zip
# needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
else
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
fi
# The exe embeds xmrig (HAS_EMBEDDED_XMRIG) and getXmrigPath() always prefers / re-extracts the copy in
# %APPDATA%\<app>\dragonx, so a loose exe-dir xmrig.exe is never used. Ship it loose only when not embedded.
if grep -q '^#define HAS_EMBEDDED_XMRIG 1' "$GEN/embedded_data.h" 2>/dev/null; then
info "xmrig.exe is embedded in the exe — not copied loose into the zip"
else
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
fi
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ────────────────────────────
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
info "Windows release artifacts: $out/"
ls -lh "$out/"
}
# ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: macOS — .app bundle + DMG
#
# Cross-compile from Linux:
# 1. Install osxcross: https://github.com/tpoechtrager/osxcross
# export OSXCROSS="$HOME/osxcross" (or wherever you installed it)
# 2. For DMG: sudo apt install genisoimage
# 3. For .icns icons: sudo apt install icnsutils
# 4. Place macOS daemon binaries in prebuilt-binaries/dragonxd-mac/ (optional)
#
# Native on macOS:
# Works out of the box. brew install create-dmg for prettier DMGs.
# ═══════════════════════════════════════════════════════════════════════════════
build_release_mac() {
local IS_CROSS=false
local MAC_ARCH="x86_64"
# Detect cross-compilation from Linux
if [[ "$(uname -s)" == "Linux" ]]; then
IS_CROSS=true
info "Cross-compiling for macOS from Linux"
# Find osxcross
if [[ -z "${OSXCROSS:-}" ]]; then
# Try common locations
for try_path in "$SCRIPT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross" "/usr/local/osxcross"; do
if [[ -d "$try_path/target" ]]; then
OSXCROSS="$try_path"
break
fi
done
fi
if [[ -z "${OSXCROSS:-}" || ! -d "${OSXCROSS}/target" ]]; then
err "osxcross not found! Set OSXCROSS=/path/to/osxcross or install it:"
echo ""
echo " git clone https://github.com/tpoechtrager/osxcross"
echo " cd osxcross"
echo " # Place MacOSX SDK (e.g. MacOSX13.0.sdk.tar.xz) in tarballs/"
echo " UNATTENDED=1 ./build.sh"
echo " export OSXCROSS=\$PWD"
echo ""
exit 1
fi
info "Using osxcross at: $OSXCROSS"
export PATH="$OSXCROSS/target/bin:$PATH"
# Find the right compiler triple
local OSXCROSS_CC="" OSXCROSS_CXX="" OSXCROSS_TRIPLE=""
# Look for full-triple compilers first (e.g. x86_64-apple-darwin22.4-clang++)
local found_triple
found_triple=$(ls "$OSXCROSS/target/bin/"x86_64-apple-darwin*-clang++ 2>/dev/null | head -1)
if [[ -n "$found_triple" ]]; then
OSXCROSS_TRIPLE=$(basename "$found_triple" | sed 's/-clang++$//')
OSXCROSS_CC="$OSXCROSS/target/bin/${OSXCROSS_TRIPLE}-clang"
OSXCROSS_CXX="$OSXCROSS/target/bin/${OSXCROSS_TRIPLE}-clang++"
MAC_ARCH="x86_64"
fi
# Prefer arm64 if available (Apple Silicon)
found_triple=$(ls "$OSXCROSS/target/bin/"aarch64-apple-darwin*-clang++ 2>/dev/null | head -1)
if [[ -n "$found_triple" ]]; then
OSXCROSS_TRIPLE=$(basename "$found_triple" | sed 's/-clang++$//')
OSXCROSS_CC="$OSXCROSS/target/bin/${OSXCROSS_TRIPLE}-clang"
OSXCROSS_CXX="$OSXCROSS/target/bin/${OSXCROSS_TRIPLE}-clang++"
MAC_ARCH="arm64"
fi
# Also try o64/oa64 wrapper — but resolve the underlying triple
if [[ -z "$OSXCROSS_CXX" ]]; then
if command -v o64-clang++ &>/dev/null; then
# Resolve the x86_64 triple from binutils
OSXCROSS_TRIPLE=$(ls "$OSXCROSS/target/bin/"x86_64-apple-darwin*-ar 2>/dev/null | head -1 | xargs basename | sed 's/-ar$//')
OSXCROSS_CC="o64-clang"
OSXCROSS_CXX="o64-clang++"
MAC_ARCH="x86_64"
elif command -v oa64-clang++ &>/dev/null; then
OSXCROSS_TRIPLE=$(ls "$OSXCROSS/target/bin/"aarch64-apple-darwin*-ar 2>/dev/null | head -1 | xargs basename | sed 's/-ar$//')
OSXCROSS_CC="oa64-clang"
OSXCROSS_CXX="oa64-clang++"
MAC_ARCH="arm64"
fi
fi
if [[ -z "$OSXCROSS_CXX" ]]; then
err "Could not find osxcross compilers in PATH"
echo " Ensure \$OSXCROSS/target/bin contains *-apple-darwin*-clang++"
exit 1
fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else
# Native macOS: build universal (arm64 + x86_64) by default. Override with
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
# When linking the real lite backend, the app can only include architectures
# the backend static library actually provides. Its pinned ring 0.16.11 has no
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
# otherwise the arm64 slice fails to link.
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
MAC_ARCHS="$_backend_archs"
fi
fi
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
MAC_ARCH="universal"
else
MAC_ARCH="$MAC_ARCHS"
fi
export MACOSX_DEPLOYMENT_TARGET="11.0"
fi
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/mac"
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/mac"
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
fi
mkdir -p "$bd" && cd "$bd"
# ── Compile ──────────────────────────────────────────────────────────────
if $IS_CROSS; then
# OSXCROSS_TRIPLE already resolved during compiler detection above
local OSXCROSS_SDK_PATH
OSXCROSS_SDK_PATH=$(ls -d "$OSXCROSS"/target/SDK/MacOSX*.sdk 2>/dev/null | head -1)
# Generate osxcross toolchain file
cat > "$bd/osxcross-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR ${MAC_ARCH})
# Compilers
set(CMAKE_C_COMPILER ${OSXCROSS_CC})
set(CMAKE_CXX_COMPILER ${OSXCROSS_CXX})
set(CMAKE_OBJC_COMPILER ${OSXCROSS_CC})
set(CMAKE_OBJCXX_COMPILER ${OSXCROSS_CXX})
# SDK sysroot
set(CMAKE_OSX_SYSROOT "${OSXCROSS_SDK_PATH}")
# Cross-compilation binutils
set(CMAKE_AR "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-ar" CACHE FILEPATH "ar")
set(CMAKE_RANLIB "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-ranlib" CACHE FILEPATH "ranlib")
set(CMAKE_STRIP "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip" CACHE FILEPATH "strip")
set(CMAKE_INSTALL_NAME_TOOL "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool" CACHE FILEPATH "install_name_tool")
set(CMAKE_OTOOL "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-otool" CACHE FILEPATH "otool")
set(CMAKE_LIBTOOL "${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-libtool" CACHE FILEPATH "libtool")
# Search paths
set(CMAKE_FIND_ROOT_PATH
"${OSXCROSS_SDK_PATH}"
"${OSXCROSS}/target/macports/pkgs"
"${OSXCROSS}/target/macports/pkgs/opt/local"
)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0)
TOOLCHAIN
# Find compiler-rt builtins for macOS
local COMPILER_RT=""
local CLANG_RESOURCE_DIR
CLANG_RESOURCE_DIR=$("$OSXCROSS_CC" -print-resource-dir 2>/dev/null || echo "")
if [[ -n "$CLANG_RESOURCE_DIR" && -f "$CLANG_RESOURCE_DIR/lib/darwin/libclang_rt.osx.a" ]]; then
COMPILER_RT="$CLANG_RESOURCE_DIR/lib/darwin/libclang_rt.osx.a"
elif [[ -f "$OSXCROSS/build/compiler-rt/compiler-rt/build/lib/darwin/libclang_rt.osx.a" ]]; then
COMPILER_RT="$OSXCROSS/build/compiler-rt/compiler-rt/build/lib/darwin/libclang_rt.osx.a"
fi
# Fetch libsodium for macOS if needed
if [[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
info "Fetching libsodium for macOS ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --mac
fi
info "Configuring (cross-compile via osxcross) ..."
# Vendored static libdb for the recovery helper (full-node only), by target arch; see mac_bdb_dir.
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
local BDB_ARGS=()
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
fi
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/osxcross-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${BDB_ARGS[@]}" \
"${CMAKE_LITE_ARGS[@]}"
else
# Build libsodium as universal if needed
local need_sodium=false
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true
fi
fi
if $need_sodium; then
info "Building libsodium (universal) ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
# Point CMake at the vendored static libdb for the recovery helper (full-node only); see mac_bdb_dir.
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
local BDB_ARGS=()
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
fi
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
"${BDB_ARGS[@]}" \
"${CMAKE_LITE_ARGS[@]}"
fi
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS" --target dragonx_release
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
# A full-node release MUST include the recovery helper. macOS needs a static libdb-6.2 (Homebrew
# berkeley-db for a native build, or a vendored external/dragonx/depends/<triple>) — otherwise CMake
# skips the target and this fails loudly rather than shipping a mac release with no recovery option.
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$(mac_bdb_dir "$MAC_ARCH")"
# Strip — use osxcross strip for cross-builds
if $IS_CROSS; then
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
if [[ -x "$STRIP_CMD" ]]; then
info "Stripping (osxcross) ..."
"$STRIP_CMD" "bin/${APP_BASENAME}"
else
warn "osxcross strip not found at $STRIP_CMD — skipping"
fi
else
info "Stripping ..."
strip "bin/${APP_BASENAME}"
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info "bin/${APP_BASENAME}"
fi
fi
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
# ── Create .app bundle ───────────────────────────────────────────────────
mkdir -p "$out"
# Clean only THIS variant's prior artifacts so full-node and lite releases can
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
# and the ".app" names are exact.
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
local APP="$out/${APP_BASENAME}.app"
local CONTENTS="$APP/Contents"
local MACOS="$CONTENTS/MacOS"
local RESOURCES="$CONTENTS/Resources"
local FRAMEWORKS="$CONTENTS/Frameworks"
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
# Main binary
cp "bin/${APP_BASENAME}" "$MACOS/"
chmod +x "$MACOS/${APP_BASENAME}"
# Resources
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in dragonxd dragonx-cli dragonx-tx; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
for f in sapling-spend.params sapling-output.params; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local daemon_paths=(
"$SCRIPT_DIR/../dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
done
local cli_paths=(
"$SCRIPT_DIR/../dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
done
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
# Offline wallet-rebuild recovery helper — required (asserted after build); next to the daemon.
cp "bin/dragonx-wallet-rebuild" "$MACOS/" && chmod +x "$MACOS/dragonx-wallet-rebuild" && info " Bundled dragonx-wallet-rebuild"
else
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
fi
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
if [[ -f "$XMRIG_MAC" ]]; then
cp "$XMRIG_MAC" "$MACOS/xmrig"
chmod +x "$MACOS/xmrig"
info " Bundled xmrig"
else
warn "xmrig not found — mining unavailable in .app"
fi
if should_bundle_full_node_assets; then
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
fi
# Bundle SDL3 dylib
local sdl_dylib=""
for candidate in \
"$bd/_deps/sdl3-build/libSDL3.dylib" \
"$bd/_deps/sdl3-build/libSDL3.0.dylib" \
"/usr/local/lib/libSDL3.dylib" \
"/opt/homebrew/lib/libSDL3.dylib"; do
if [[ -f "$candidate" ]]; then
sdl_dylib="$candidate"
break
fi
done
if [[ -n "$sdl_dylib" ]]; then
cp "$sdl_dylib" "$FRAMEWORKS/"
local sdl_name=$(basename "$sdl_dylib")
# Fix the rpath so the binary finds SDL3 in Frameworks/
if $IS_CROSS; then
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
else
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
fi
info " Bundled $sdl_name"
fi
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
#!/bin/bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
cd "\$DIR/../Resources"
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
LAUNCH
chmod +x "$MACOS/${APP_BASENAME}"
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
local APP_DISPLAY_NAME="DragonX Wallet"
local APP_BUNDLE_ID="is.hush.dragonx"
if $DO_LITE; then
APP_DISPLAY_NAME="DragonX Wallet Lite"
APP_BUNDLE_ID="is.hush.dragonx.lite"
fi
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_DISPLAY_NAME}</string>
<key>CFBundleDisplayName</key>
<string>${APP_DISPLAY_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${APP_BUNDLE_ID}</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_BASENAME}</string>
<key>CFBundleIconFile</key>
<string>ObsidianDragon</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>DRGX</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
</dict>
</plist>
PLIST
# ── Icon (.icns) ─────────────────────────────────────────────────────────
if [[ -f "$SCRIPT_DIR/res/img/ObsidianDragon.icns" ]]; then
cp "$SCRIPT_DIR/res/img/ObsidianDragon.icns" "$RESOURCES/ObsidianDragon.icns"
elif [[ -f "$SCRIPT_DIR/res/icons/dragonx-256.png" ]]; then
if command -v iconutil &>/dev/null && command -v sips &>/dev/null; then
# Native macOS: sips + iconutil
local iconset="$bd/ObsidianDragon.iconset"
mkdir -p "$iconset"
local src="$SCRIPT_DIR/res/icons/dragonx-256.png"
for sz in 16 32 64 128 256; do
sips -z $sz $sz "$src" --out "$iconset/icon_${sz}x${sz}.png" &>/dev/null || true
done
for sz in 16 32 128; do
local r=$((sz * 2))
sips -z $r $r "$src" --out "$iconset/icon_${sz}x${sz}@2x.png" &>/dev/null || true
done
iconutil -c icns "$iconset" -o "$RESOURCES/ObsidianDragon.icns" 2>/dev/null || true
rm -rf "$iconset"
elif command -v png2icns &>/dev/null; then
# Linux: png2icns from icnsutils (sudo apt install icnsutils)
info "Creating .icns with png2icns ..."
local src="$SCRIPT_DIR/res/icons/dragonx-256.png"
# png2icns needs specific sizes; resize with ImageMagick if available
if command -v convert &>/dev/null; then
local icondir="$bd/icon-sizes"
mkdir -p "$icondir"
for sz in 16 32 48 128 256; do
convert "$src" -resize ${sz}x${sz} "$icondir/icon_${sz}.png" 2>/dev/null || true
done
png2icns "$RESOURCES/ObsidianDragon.icns" "$icondir"/icon_*.png 2>/dev/null || true
rm -rf "$icondir"
else
# Just use the 256px as-is
png2icns "$RESOURCES/ObsidianDragon.icns" "$src" 2>/dev/null || true
fi
else
warn "No .icns tool found (install: sudo apt install icnsutils)"
fi
fi
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
# (APP_DISPLAY_NAME above).
local DMG_BASENAME="${APP_BASENAME}"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then
# create-dmg (works on macOS; also available on Linux via npm)
info "Creating DMG with create-dmg ..."
create-dmg \
--volname "${APP_DISPLAY_NAME}" \
--volicon "$RESOURCES/ObsidianDragon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "${APP_BASENAME}.app" 150 190 \
--app-drop-link 450 190 \
--no-internet-enable \
"$out/$DMG_NAME" \
"$APP" 2>/dev/null && {
info "DMG: $out/$DMG_NAME ($(du -h "$out/$DMG_NAME" | cut -f1))"
} || warn "create-dmg failed — .app bundle still available"
elif command -v hdiutil &>/dev/null; then
# Native macOS
info "Creating DMG with hdiutil ..."
local staging="$bd/dmg-staging"
rm -rf "$staging"
mkdir -p "$staging"
cp -a "$APP" "$staging/"
ln -s /Applications "$staging/Applications"
hdiutil create -volname "${APP_DISPLAY_NAME}" \
-srcfolder "$staging" \
-ov -format UDZO \
"$out/$DMG_NAME" 2>/dev/null && {
info "DMG: $out/$DMG_NAME ($(du -h "$out/$DMG_NAME" | cut -f1))"
} || warn "hdiutil failed — .app bundle still available"
rm -rf "$staging"
elif command -v genisoimage &>/dev/null; then
# Linux fallback: genisoimage produces a hybrid ISO/DMG that macOS can open
info "Creating DMG with genisoimage (Linux) ..."
local staging="$bd/dmg-staging"
rm -rf "$staging"
mkdir -p "$staging"
cp -a "$APP" "$staging/"
# Can't create a real symlink to /Applications in an ISO, but the .app
# is the important part — users drag it to Applications manually.
genisoimage -V "${APP_DISPLAY_NAME}" \
-D -R -apple -no-pad \
-o "$out/$DMG_NAME" \
"$staging" 2>/dev/null && {
info "DMG: $out/$DMG_NAME ($(du -h "$out/$DMG_NAME" | cut -f1))"
info " (ISO/HFS hybrid — mountable on macOS)"
} || warn "genisoimage failed — .app bundle still available"
rm -rf "$staging"
else
warn "No DMG tool found — .app bundle still available"
echo ""
echo " To create DMGs on Linux, install one of:"
echo " sudo apt install genisoimage # Basic DMG (recommended)"
echo " sudo apt install icnsutils # For .icns icon creation"
echo ""
fi
info "macOS release artifacts: $out/"
ls -lhR "$out/" 2>/dev/null | head -30
}
# ═══════════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════════
echo -e "${GREEN}DragonX Wallet v${VERSION} — Unified Build${NC}"
echo "─────────────────────────────────────────"
$DO_DEV && build_dev
$DO_LINUX && build_release_linux
$DO_WIN && build_release_win
$DO_MAC && build_release_mac
header "Done"
if $DO_LINUX || $DO_WIN || $DO_MAC; then
echo -e " Release artifacts in: ${GREEN}$SCRIPT_DIR/release/${NC}"
[[ -d "$SCRIPT_DIR/release/linux" ]] && echo -e " ${CYAN}linux/${NC} — AppImage + binary"
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
fi
# Reaching here means the build completed (real failures exit 1 at their point of failure).
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
# become the script's exit code and make a successful build report failure (e.g. to CI).
exit 0