Mac packaging: Igneum Devnet.app and DMG for Apple silicon miners
packaging/mac: build-dmg.sh assembles Igneum Devnet.app (shell launcher that opens igneum-devnet.sh in Terminal, stripped ad-hoc-signed copies of igneumd, igneum-miner and the Metal worker), README.txt, Stop Igneum.command and an Applications link into dist/igneum-devnet-mac.dmg (17 MB, lzfse). The script starts the node peered to SEED_PEERS, waits for sync, runs one miner identity mac-<hostname> on the Metal worker, prints a status line every 30 s, uploads logs every 60 s, keeps the Mac awake and stops everything in order on Ctrl+C, window close, --stop or the Stop command. Tested on this Mac from the mounted DMG against a test node on 27310/27311 peered to the live node: sync in 22 s, 24 accepted blocks, listed on igneum.network/live, clean stop with no stray process on SIGINT, SIGTERM and Stop Igneum.command. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
75a08060f3
commit
543833c50d
8 changed files with 715 additions and 0 deletions
3
packaging/mac/.gitignore
vendored
Normal file
3
packaging/mac/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# built outputs: the app with its binaries, and the DMG
|
||||
build/
|
||||
dist/
|
||||
96
packaging/mac/README.md
Normal file
96
packaging/mac/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Igneum Devnet for Mac (packaging/mac)
|
||||
|
||||
A DMG a friend can open on an Apple silicon Mac to run a devnet node and the Metal miner, and show up on
|
||||
igneum.network/live. Built 3 October 2026 from the binaries already compiled in this repo; nothing is compiled here.
|
||||
|
||||
## Build
|
||||
|
||||
packaging/mac/build-dmg.sh
|
||||
|
||||
Output: `packaging/mac/dist/igneum-devnet-mac.dmg` (about 17 MB, lzfse). The script takes
|
||||
`vendor/igneum-node/target-rename/release/igneumd` (falls back to `target/release/kaspad` renamed, and says so),
|
||||
`vendor/igneum-node/target/release/igneum-miner` and `proto-metal/igneum-bench` (the plain build, the one whose
|
||||
`--serve` protocol matches that miner), copies them into the bundle, strips the copies and signs them ad hoc again
|
||||
(strip invalidates the linker signature and arm64 macOS refuses an unsigned binary), and checks that each copy still
|
||||
runs. The originals are untouched. The icon comes from `site/icon-512.png`. `build/` and `dist/` are ignored by git.
|
||||
|
||||
## What is on the DMG
|
||||
|
||||
| Item | What it is |
|
||||
|---|---|
|
||||
| `Igneum Devnet.app` | the launcher bundle (below) |
|
||||
| `README.txt` | 9 lines for the friend (`dmg/README.txt`) |
|
||||
| `Stop Igneum.command` | stops a copy whose window was closed without Ctrl+C (`app/Stop Igneum.command`) |
|
||||
| `Applications` | symlink, for the drag |
|
||||
|
||||
## The app bundle
|
||||
|
||||
Igneum Devnet.app/Contents/MacOS/Igneum Devnet app/launcher.sh: removes quarantine from Resources, opens the script in Terminal
|
||||
Igneum Devnet.app/Contents/Resources/igneum-devnet.sh the run script (app/igneum-devnet.sh)
|
||||
Igneum Devnet.app/Contents/Resources/bin/igneumd, igneum-miner, igneum-bench
|
||||
Igneum Devnet.app/Contents/Resources/AppIcon.icns
|
||||
Igneum Devnet.app/Contents/Info.plist app/Info.plist (CFBundleVersion = build stamp)
|
||||
|
||||
Why a shell launcher and not an AppleScript applet: `open -a Terminal <script>` needs no Automation consent;
|
||||
`tell application "Terminal" to do script` would prompt "Igneum Devnet wants to control Terminal" on first run.
|
||||
The launcher strips `com.apple.quarantine` from its own Resources first, because after Gatekeeper lets the app
|
||||
through, every binary inside would still be checked on its first exec and refused as unidentified. That only works on
|
||||
a writable volume, so the app refuses (with a dialog) to run straight from the DMG.
|
||||
|
||||
## The run script (settings at the top)
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `SEED_PEERS` | `192.168.68.64:26611` | the Mac node on the project lead's LAN; a public seed node replaces it; comma list allowed |
|
||||
| `MINERS` | `1` | miner identities; 0 = node only. One is right: one worker owns the whole GPU |
|
||||
| `STATUS_SECS`, `UPLOAD_SECS` | 30, 60 | status line and log upload periods |
|
||||
| `IGNEUM_RPC_PORT`, `IGNEUM_P2P_PORT` | 26610, 26611 | environment overrides (tests) |
|
||||
| `IGNEUM_DATA`, `IGNEUM_LOGS` | `~/Library/Application Support/Igneum/devnet`, `~/Library/Logs/Igneum` | the only places written outside the bundle |
|
||||
|
||||
Order: checks (arm64, binaries, ports, no second copy via the pid file) > `caffeinate -dims -w <launcher>` >
|
||||
`igneumd --devnet --appdir ... --rpclisten 127.0.0.1:26610 --listen 0.0.0.0:26611 --addpeer <each seed> --nodnsseed
|
||||
--disable-upnp --nologfiles` > every 5 s `igneum-miner watch 1` until synced (peers > 0, blocks >= headers > 1 and
|
||||
the count moving between readings, or stable for 60 s; this build's watch line has no synced= field) > `igneum-miner
|
||||
mine grpc://127.0.0.1:26610 1 100000000 mac-<hostname> --worker igneum-bench --status-secs 30 --exit-on-seed-change
|
||||
--payout-label mac-<hostname>` > loop: status every 30 s, uploads every 60 s (labels `mac-<host>`, `nodelog-mac-<host>`,
|
||||
`miner-mac-<host>`, run id `mac-<host>-<stamp>`), node crash restarted after 5 to 60 s with the miners stopped until it
|
||||
is synced again, miner exit 42 (hourly program change) restarted at once, other miner exits after 5 to 60 s.
|
||||
|
||||
Stopping: Ctrl+C (SIGINT), the window closing (SIGHUP) or SIGTERM set a flag; the loop then stops the miners and their
|
||||
workers (TERM, 8 s, KILL), the node (TERM, 30 s, KILL), caffeinate, uploads the logs once more and prints a summary.
|
||||
Children are started with SIGHUP ignored (bash already makes background children of a script ignore SIGINT), so the
|
||||
launcher controls the order. `igneum-devnet.sh --stop` and `Stop Igneum.command` signal the launcher from the pid file
|
||||
and fall back to `pkill -f 'Igneum Devnet.app/Contents/Resources/bin/'`.
|
||||
|
||||
The miner identity shows on igneum.network/live as the first 8 hex of its vote key hash; the window prints it.
|
||||
|
||||
## Gatekeeper (expected on macOS 15 and 26)
|
||||
|
||||
The app is unsigned and not notarized. Double-click gives "Apple could not verify ... is free of malware" with no
|
||||
Open button. Right-click > Open offers Open on older systems; on macOS 15 and later the route is System Settings >
|
||||
Privacy & Security > "Open Anyway" (the button appears after the first refusal), then open the app again. Both are in
|
||||
README.txt. Removing the flag by hand also works: `xattr -dr com.apple.quarantine "/Applications/Igneum Devnet.app"`.
|
||||
The first time igneumd listens on 0.0.0.0:26611 the macOS firewall (if on) asks to allow incoming connections; Allow.
|
||||
|
||||
## Test (3 October 2026, this Mac, macOS 26.6.2, M5 Max)
|
||||
|
||||
Mounted the DMG and ran the script from the mounted bundle with `SEED_PEERS=127.0.0.1:26611 MINERS=1
|
||||
IGNEUM_RPC_PORT=27310 IGNEUM_P2P_PORT=27311 IGNEUM_DATA=/tmp/igneum-mac-test/data IGNEUM_LOGS=/tmp/igneum-mac-test/logs`
|
||||
(the live node as the seed, own node on 27300+, the Metal miner against that node).
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| fresh database to synced | 22 s (0 blocks, 5,017 headers at 5 s; 9,988 blocks at 22 s); existing database 12 to 13 s |
|
||||
| miner | started at sync, id 0aa660fe printed, first block 6 to 11 s later, 24 accepted blocks in 3 min 17 s, 0 rejected, 0 mismatched |
|
||||
| rate | 16 to 18 MH/s wall, 40 MH/s inside jobs (the Mac was running two cargo builds and a census) |
|
||||
| status line | every 30 s: `status: accepted 22 blocks, 17.76 MH/s, template 0.89 s old \| node 10205 blocks, 1 peers, synced \| up 00:03:02` |
|
||||
| uploads | 12 uploads in the intake under run id mac-MacBook-Pro-20261003-224420 (`node tools/logs.mjs`) |
|
||||
| live page | 0aa660fe listed in igneum.network/api/live miners within a minute |
|
||||
| Ctrl+C (SIGINT) | miners, worker, node gone in 5 s, summary printed, no stray process, pid and run files removed, ports free |
|
||||
| SIGTERM | same, 4 s |
|
||||
| Stop Igneum.command | node-only run stopped in 2 s; a second run says "Igneum is not running" |
|
||||
|
||||
Two things the test caught and fixed: an unquoted `$NODE_EXE` in the version line (the bundle path has a space), and
|
||||
`uname -m` as the Apple silicon check (an x86_64 process, Rosetta, reports x86_64; now `sysctl hw.optional.arm64`).
|
||||
Note for harness tests: a script started as a background job of a non-interactive shell has SIGINT ignored at entry
|
||||
and cannot trap it; run it in the foreground or through `perl -e '$SIG{INT}="DEFAULT"; exec ...'`.
|
||||
32
packaging/mac/app/Info.plist
Normal file
32
packaging/mac/app/Info.plist
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?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>Igneum Devnet</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Igneum Devnet</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>network.igneum.devnet</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STAMP</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Igneum Devnet</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.3</string>
|
||||
<key>LSArchitecturePriority</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>LSRequiresNativeExecution</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Igneum devnet. Nothing is bought or sold.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
20
packaging/mac/app/Stop Igneum.command
Executable file
20
packaging/mac/app/Stop Igneum.command
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
# Stops a running Igneum Devnet (miners, Metal worker, then the node) when its window was closed without Ctrl+C.
|
||||
# Finds the running copy through the run file the launcher writes, and falls back to the process list.
|
||||
DATA="${IGNEUM_DATA:-$HOME/Library/Application Support/Igneum/devnet}"
|
||||
RUNFILE="$DATA/igneum-devnet.run"
|
||||
script="$(sed -n 's/^script=//p' "$RUNFILE" 2>/dev/null)"
|
||||
if [ -n "$script" ] && [ -x "$script" ]; then
|
||||
exec "$script" --stop
|
||||
fi
|
||||
left="$(pgrep -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null)"
|
||||
if [ -n "$left" ]; then
|
||||
echo "stopping Igneum processes: $(echo $left | tr '\n' ' ')"
|
||||
pkill -TERM -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null
|
||||
sleep 5
|
||||
pkill -KILL -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null
|
||||
rm -f "$DATA/igneum-devnet.pid" "$RUNFILE"
|
||||
echo "Igneum is stopped."
|
||||
else
|
||||
echo "Igneum is not running."
|
||||
fi
|
||||
454
packaging/mac/app/igneum-devnet.sh
Executable file
454
packaging/mac/app/igneum-devnet.sh
Executable file
|
|
@ -0,0 +1,454 @@
|
|||
#!/bin/bash
|
||||
# Igneum devnet for Apple silicon Macs: the node and the Metal miner in one Terminal window.
|
||||
# Lives in "Igneum Devnet.app/Contents/Resources" next to bin/ (igneumd, igneum-miner, igneum-bench).
|
||||
# The app's launcher opens this script in Terminal. 3 October 2026.
|
||||
#
|
||||
# What it does, in order:
|
||||
# 1. Checks the Mac (Apple silicon), the binaries, the ports, and that no other copy is running.
|
||||
# 2. Starts igneumd --devnet with the database under ~/Library/Application Support/Igneum/devnet,
|
||||
# RPC on 127.0.0.1:26610, p2p on 0.0.0.0:26611, peered to SEED_PEERS (--addpeer, never --connect,
|
||||
# so the seed node can dial back in). The node's output goes to ~/Library/Logs/Igneum/node-<stamp>.log.
|
||||
# 3. Waits until the node is synced ("syncing, N blocks" every 5 s meanwhile). The reading is
|
||||
# `igneum-miner watch`: peers > 0, blocks caught up with headers, and the block count moving.
|
||||
# 4. Starts MINERS identities of igneum-miner with the Metal worker (igneum-bench --serve), identity and
|
||||
# payout label mac-<hostname> (mac-<hostname>-2, -3 ... for more). One identity is plenty: one worker
|
||||
# owns the whole GPU.
|
||||
# 5. Prints a plain status line every 30 s, uploads the logs to the intake every 60 s, keeps the Mac
|
||||
# awake with caffeinate, restarts the node (5 to 60 s later) or a miner after a crash, restarts a
|
||||
# miner at once when it exits with code 42 (the hourly program change).
|
||||
# 6. Ctrl+C, closing the window (SIGHUP) or `igneum-devnet.sh --stop` stops the miners, their workers,
|
||||
# then the node, then uploads the logs one last time.
|
||||
#
|
||||
# Nothing is written outside the app except the data directory and the log directory below.
|
||||
|
||||
# ---- settings ---------------------------------------------------------------------------------------------
|
||||
# The seed node(s) to peer with. For now the project lead's Mac on the house LAN; a public seed node address
|
||||
# (host:port) replaces it when one exists. A comma list is allowed: "1.2.3.4:26611,5.6.7.8:26611".
|
||||
SEED_PEERS="${SEED_PEERS:-192.168.68.64:26611}"
|
||||
# Miner identities on this Mac. 1 is right for one GPU; 0 runs the node alone.
|
||||
MINERS="${MINERS:-1}"
|
||||
STATUS_SECS="${STATUS_SECS:-30}"
|
||||
UPLOAD_SECS="${UPLOAD_SECS:-60}"
|
||||
RPC_PORT="${IGNEUM_RPC_PORT:-26610}"
|
||||
P2P_PORT="${IGNEUM_P2P_PORT:-26611}"
|
||||
DATA="${IGNEUM_DATA:-$HOME/Library/Application Support/Igneum/devnet}"
|
||||
LOGS="${IGNEUM_LOGS:-$HOME/Library/Logs/Igneum}"
|
||||
# The log intake (site/api/log.mjs). The key only authorises log uploads and is meant to ship here.
|
||||
LOG_URL="https://igneum-six.vercel.app/api/log"
|
||||
LOG_KEY="***INTAKE-KEY-REMOVED***"
|
||||
LIVE_PAGE="igneum.network/live"
|
||||
|
||||
# ---- paths and names --------------------------------------------------------------------------------------
|
||||
RES="$(cd "$(dirname "$0")" && pwd)"
|
||||
BIN="$RES/bin"
|
||||
NODE_EXE="$BIN/igneumd"
|
||||
MINER_EXE="$BIN/igneum-miner"
|
||||
WORKER_EXE="$BIN/igneum-bench"
|
||||
RPC_URL="grpc://127.0.0.1:$RPC_PORT"
|
||||
PIDFILE="$DATA/igneum-devnet.pid"
|
||||
RUNFILE="$DATA/igneum-devnet.run"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
HOST="$(scutil --get LocalHostName 2>/dev/null || hostname -s)"
|
||||
HOST="$(printf '%s' "$HOST" | tr -c 'A-Za-z0-9-' '-' | sed -e 's/^-*//' -e 's/-*$//')"
|
||||
[ -n "$HOST" ] || HOST="mac"
|
||||
LABEL_BASE="mac-$HOST"
|
||||
RUN_ID="$LABEL_BASE-$STAMP"
|
||||
LAUNCHER_LOG="$LOGS/igneum-$STAMP.log"
|
||||
NODE_LOG=""
|
||||
START_AT=$SECONDS
|
||||
|
||||
log() {
|
||||
local line
|
||||
line="$(date '+%Y-%m-%d %H:%M:%S') $*"
|
||||
printf '%s\n' "$line" 2>/dev/null || true
|
||||
[ -d "$LOGS" ] && printf '%s\n' "$line" >> "$LAUNCHER_LOG" 2>/dev/null
|
||||
}
|
||||
|
||||
alive() { [ -n "$1" ] && kill -0 "$1" 2>/dev/null; }
|
||||
|
||||
uptime_txt() {
|
||||
local s=$(( SECONDS - START_AT ))
|
||||
printf '%02d:%02d:%02d' $(( s / 3600 )) $(( s % 3600 / 60 )) $(( s % 60 ))
|
||||
}
|
||||
|
||||
# ---- --stop: stop a running instance (also used by "Stop Igneum.command") ----------------------------------
|
||||
stop_mode() {
|
||||
local pid="" waited=0 left
|
||||
[ -f "$PIDFILE" ] && pid="$(cat "$PIDFILE" 2>/dev/null)"
|
||||
if alive "$pid"; then
|
||||
echo "stopping Igneum (launcher pid $pid): miners first, then the node"
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
while alive "$pid" && [ $waited -lt 60 ]; do sleep 1; waited=$(( waited + 1 )); done
|
||||
if alive "$pid"; then echo "the launcher did not stop in 60 s; forcing"; kill -KILL "$pid" 2>/dev/null; fi
|
||||
else
|
||||
echo "no running launcher found (no live pid in $PIDFILE)"
|
||||
fi
|
||||
# Anything left from this app (a launcher killed with SIGKILL leaves its children behind)
|
||||
left="$(pgrep -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null)"
|
||||
if [ -n "$left" ]; then
|
||||
echo "stopping leftover processes: $(echo $left | tr '\n' ' ')"
|
||||
pkill -TERM -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null
|
||||
sleep 5
|
||||
pkill -KILL -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null
|
||||
fi
|
||||
rm -f "$PIDFILE" "$RUNFILE"
|
||||
echo "Igneum is stopped."
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
--stop) stop_mode ;;
|
||||
--help|-h) sed -n '2,20p' "$0"; echo; echo "usage: igneum-devnet.sh [--stop] (settings: SEED_PEERS, MINERS, STATUS_SECS, UPLOAD_SECS at the top)"; exit 0 ;;
|
||||
esac
|
||||
|
||||
# ---- 1. checks --------------------------------------------------------------------------------------------
|
||||
# sysctl rather than uname -m: a Terminal running under Rosetta reports x86_64 from uname on an Apple silicon Mac.
|
||||
if [ "$(sysctl -n hw.optional.arm64 2>/dev/null)" != "1" ]; then
|
||||
echo "Igneum devnet needs an Apple silicon Mac (M1 or later). This Mac reports $(uname -m) and no arm64 support. Nothing was started."
|
||||
exit 1
|
||||
fi
|
||||
for f in "$NODE_EXE" "$MINER_EXE" "$WORKER_EXE"; do
|
||||
if [ ! -x "$f" ]; then echo "missing or not executable: $f. The app is incomplete; copy it again from the DMG."; exit 1; fi
|
||||
done
|
||||
mkdir -p "$DATA" "$LOGS" || { echo "cannot create $DATA or $LOGS"; exit 1; }
|
||||
if [ -f "$PIDFILE" ] && alive "$(cat "$PIDFILE" 2>/dev/null)"; then
|
||||
echo "Igneum is already running in another window (launcher pid $(cat "$PIDFILE")). Press Ctrl+C there, or run Stop Igneum."
|
||||
exit 1
|
||||
fi
|
||||
for port in "$RPC_PORT" "$P2P_PORT"; do
|
||||
busy="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1)"
|
||||
if [ -n "$busy" ]; then
|
||||
echo "port $port is already in use by pid $busy ($(ps -p "$busy" -o comm= 2>/dev/null)). Is a node already running? Run Stop Igneum first."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
case "$MINERS" in ''|*[!0-9]*) echo "MINERS must be a number (got '$MINERS')"; exit 1 ;; esac
|
||||
|
||||
echo "$$" > "$PIDFILE"
|
||||
printf 'pid=%s\nscript=%s\nrpc=%s\nrun=%s\n' "$$" "$RES/igneum-devnet.sh" "$RPC_URL" "$RUN_ID" > "$RUNFILE"
|
||||
|
||||
log "Igneum devnet on $HOST (run $RUN_ID)"
|
||||
log "node: RPC 127.0.0.1:$RPC_PORT, p2p 0.0.0.0:$P2P_PORT, data $DATA"
|
||||
log "seed peers: $SEED_PEERS; miners: $MINERS; logs: $LOGS"
|
||||
log "node $("$NODE_EXE" --version 2>&1 | head -1); worker $(basename "$WORKER_EXE") (Metal)"
|
||||
|
||||
# ---- keep awake -------------------------------------------------------------------------------------------
|
||||
caffeinate -dims -w $$ >/dev/null 2>&1 &
|
||||
CAFFEINATE_PID=$!
|
||||
log "keeping the Mac awake while this window runs (caffeinate)"
|
||||
|
||||
# ---- the node ---------------------------------------------------------------------------------------------
|
||||
NODE_PID=""; NODE_STARTS=0; NODE_RESTARTS=0; NODE_RESTART_AT=""; NODE_STARTED_AT=0
|
||||
NODE_SYNCED=0; NODE_SYNCED_AT=""; SYNC_PREV=""; SYNC_STABLE_SINCE=""; NOPEER_WARNED=0
|
||||
NODE_BLOCKS=0; NODE_HEADERS=0; NODE_PEERS=0; NODE_DAA=0; NODE_READ_OK=0
|
||||
|
||||
start_node() {
|
||||
local args p seg
|
||||
NODE_STARTS=$(( NODE_STARTS + 1 ))
|
||||
seg=""; [ $NODE_STARTS -gt 1 ] && seg="-r$NODE_STARTS"
|
||||
NODE_LOG="$LOGS/node-$STAMP$seg.log"
|
||||
args=(--devnet "--appdir=$DATA" "--rpclisten=127.0.0.1:$RPC_PORT" "--listen=0.0.0.0:$P2P_PORT" --nodnsseed --disable-upnp --nologfiles)
|
||||
for p in $(printf '%s' "$SEED_PEERS" | tr ',' ' '); do args+=("--addpeer=$p"); done
|
||||
# Children ignore SIGHUP (the window closing) so the launcher can stop them in order; bash already makes
|
||||
# background children ignore SIGINT (Ctrl+C) in a script.
|
||||
( trap '' HUP; exec "$NODE_EXE" "${args[@]}" ) > "$NODE_LOG" 2>&1 &
|
||||
NODE_PID=$!
|
||||
NODE_STARTED_AT=$SECONDS
|
||||
NODE_RESTART_AT=""
|
||||
NODE_SYNCED=0; SYNC_PREV=""; SYNC_STABLE_SINCE=""; NODE_READ_OK=0
|
||||
log "igneumd started (pid $NODE_PID): ${args[*]}"
|
||||
log "node output: $NODE_LOG"
|
||||
}
|
||||
|
||||
stop_node() {
|
||||
local waited=0
|
||||
alive "$NODE_PID" || return 0
|
||||
kill -TERM "$NODE_PID" 2>/dev/null
|
||||
while alive "$NODE_PID" && [ $waited -lt 30 ]; do sleep 1; waited=$(( waited + 1 )); done
|
||||
if alive "$NODE_PID"; then log "the node did not stop in 30 s; forcing"; kill -KILL "$NODE_PID" 2>/dev/null; fi
|
||||
wait "$NODE_PID" 2>/dev/null
|
||||
}
|
||||
|
||||
# One reading from `igneum-miner watch 1 <rpc>`: the first "node1 blocks=..." line. The watch command sleeps
|
||||
# 10 s after its sample, so it runs in the background and is stopped once the line is there.
|
||||
read_node() {
|
||||
local tmp="$LOGS/.watch-$$.tmp" pid n=0 line
|
||||
NODE_READ_OK=0
|
||||
alive "$NODE_PID" || return 1
|
||||
"$MINER_EXE" watch 1 "$RPC_URL" > "$tmp" 2>/dev/null &
|
||||
pid=$!
|
||||
while [ $n -lt 32 ]; do
|
||||
if grep -q ' node1 blocks=' "$tmp" 2>/dev/null; then break; fi
|
||||
alive "$pid" || break
|
||||
sleep 0.25; n=$(( n + 1 ))
|
||||
done
|
||||
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
|
||||
line="$(grep ' node1 blocks=' "$tmp" 2>/dev/null | head -1)"
|
||||
rm -f "$tmp"
|
||||
[ -n "$line" ] || return 1
|
||||
set -- $(printf '%s' "$line" | sed -E 's/.* blocks=([0-9]+) headers=([0-9]+) daa=([0-9]+) tips=([0-9]+) peers=([0-9]+).*/\1 \2 \3 \4 \5/')
|
||||
[ "$#" -eq 5 ] || return 1
|
||||
NODE_BLOCKS=$1; NODE_HEADERS=$2; NODE_DAA=$3; NODE_PEERS=$5; NODE_READ_OK=1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Synced: a peer, blocks caught up with headers, more than genesis, and the count moving between two readings
|
||||
# (or stable for 60 s with a peer, for a devnet that is not producing blocks right now).
|
||||
check_sync() {
|
||||
read_node || return 1
|
||||
if [ "$NODE_PEERS" -gt 0 ] && [ "$NODE_BLOCKS" -ge "$NODE_HEADERS" ] && [ "$NODE_BLOCKS" -gt 1 ]; then
|
||||
[ -n "$SYNC_STABLE_SINCE" ] || SYNC_STABLE_SINCE=$SECONDS
|
||||
if { [ -n "$SYNC_PREV" ] && [ "$NODE_BLOCKS" -gt "$SYNC_PREV" ]; } || [ $(( SECONDS - SYNC_STABLE_SINCE )) -ge 60 ]; then
|
||||
SYNC_PREV=$NODE_BLOCKS
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
SYNC_STABLE_SINCE=""
|
||||
fi
|
||||
SYNC_PREV=$NODE_BLOCKS
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- the miners -------------------------------------------------------------------------------------------
|
||||
# Per identity i (1-based): label, pid, worker pid, log, restart bookkeeping, accepted blocks of earlier runs.
|
||||
MINER_LABEL=(); MINER_PID=(); MINER_WORKER=(); MINER_LOG=(); MINER_STARTS=(); MINER_RESTARTS=(); MINER_RESTART_AT=(); MINER_ACC_BASE=(); MINER_ID=()
|
||||
i=1
|
||||
while [ $i -le "$MINERS" ]; do
|
||||
if [ $i -eq 1 ]; then MINER_LABEL[$i]="$LABEL_BASE"; else MINER_LABEL[$i]="$LABEL_BASE-$i"; fi
|
||||
MINER_PID[$i]=""; MINER_WORKER[$i]=""; MINER_LOG[$i]=""; MINER_STARTS[$i]=0; MINER_RESTARTS[$i]=0; MINER_RESTART_AT[$i]=""; MINER_ACC_BASE[$i]=0; MINER_ID[$i]=""
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
MINERS_UP=0
|
||||
|
||||
start_miner() {
|
||||
local i=$1 label seg
|
||||
label="${MINER_LABEL[$i]}"
|
||||
MINER_STARTS[$i]=$(( ${MINER_STARTS[$i]} + 1 ))
|
||||
seg=""; [ "${MINER_STARTS[$i]}" -gt 1 ] && seg="-r${MINER_STARTS[$i]}"
|
||||
MINER_LOG[$i]="$LOGS/miner-$label-$STAMP$seg.log"
|
||||
MINER_RESTART_AT[$i]=""
|
||||
( trap '' HUP; exec "$MINER_EXE" mine "$RPC_URL" 1 100000000 "$label" --worker "$WORKER_EXE" --status-secs "$STATUS_SECS" --exit-on-seed-change --payout-label "$label" ) > "${MINER_LOG[$i]}" 2>&1 &
|
||||
MINER_PID[$i]=$!
|
||||
MINER_WORKER[$i]=""
|
||||
log "miner $label started (pid ${MINER_PID[$i]}): igneum-miner mine $RPC_URL 1 100000000 $label --worker igneum-bench --status-secs $STATUS_SECS --exit-on-seed-change --payout-label $label"
|
||||
log "miner output: ${MINER_LOG[$i]}"
|
||||
}
|
||||
|
||||
# The worker is the miner's child; remembered so it can be stopped if the miner dies without closing it.
|
||||
note_worker() {
|
||||
local i=$1
|
||||
[ -z "${MINER_WORKER[$i]}" ] && alive "${MINER_PID[$i]}" && MINER_WORKER[$i]="$(pgrep -P "${MINER_PID[$i]}" 2>/dev/null | head -1)"
|
||||
}
|
||||
|
||||
accepted_in() { [ -f "$1" ] && grep -c ' ACCEPTED block' "$1" 2>/dev/null || echo 0; }
|
||||
|
||||
stop_miner() {
|
||||
local i=$1 waited=0
|
||||
note_worker "$i"
|
||||
alive "${MINER_PID[$i]}" && kill -TERM "${MINER_PID[$i]}" 2>/dev/null
|
||||
alive "${MINER_WORKER[$i]}" && kill -TERM "${MINER_WORKER[$i]}" 2>/dev/null
|
||||
while { alive "${MINER_PID[$i]}" || alive "${MINER_WORKER[$i]}"; } && [ $waited -lt 8 ]; do sleep 1; waited=$(( waited + 1 )); done
|
||||
alive "${MINER_PID[$i]}" && kill -KILL "${MINER_PID[$i]}" 2>/dev/null
|
||||
alive "${MINER_WORKER[$i]}" && kill -KILL "${MINER_WORKER[$i]}" 2>/dev/null
|
||||
wait "${MINER_PID[$i]}" 2>/dev/null
|
||||
MINER_ACC_BASE[$i]=$(( ${MINER_ACC_BASE[$i]} + $(accepted_in "${MINER_LOG[$i]}") ))
|
||||
MINER_PID[$i]=""; MINER_WORKER[$i]=""
|
||||
}
|
||||
|
||||
start_all_miners() {
|
||||
local i=1
|
||||
while [ $i -le "$MINERS" ]; do start_miner $i; i=$(( i + 1 )); done
|
||||
MINERS_UP=1
|
||||
}
|
||||
|
||||
stop_all_miners() {
|
||||
local i=1
|
||||
while [ $i -le "$MINERS" ]; do stop_miner $i; i=$(( i + 1 )); done
|
||||
MINERS_UP=0
|
||||
}
|
||||
|
||||
# Miner bookkeeping every second: the live id once it is known, exits and restarts.
|
||||
tend_miners() {
|
||||
local i=1 rc delay
|
||||
while [ $i -le "$MINERS" ]; do
|
||||
if alive "${MINER_PID[$i]}"; then
|
||||
note_worker "$i"
|
||||
if [ -z "${MINER_ID[$i]}" ]; then
|
||||
MINER_ID[$i]="$(sed -n 's/.*vote_key_hash=\([0-9a-f]\{8\}\).*/\1/p' "${MINER_LOG[$i]}" 2>/dev/null | head -1)"
|
||||
[ -n "${MINER_ID[$i]}" ] && log "miner ${MINER_LABEL[$i]} shows on $LIVE_PAGE as ${MINER_ID[$i]} once it finds a block"
|
||||
fi
|
||||
elif [ -n "${MINER_PID[$i]}" ]; then
|
||||
wait "${MINER_PID[$i]}" 2>/dev/null; rc=$?
|
||||
alive "${MINER_WORKER[$i]}" && kill -TERM "${MINER_WORKER[$i]}" 2>/dev/null
|
||||
MINER_ACC_BASE[$i]=$(( ${MINER_ACC_BASE[$i]} + $(accepted_in "${MINER_LOG[$i]}") ))
|
||||
MINER_PID[$i]=""; MINER_WORKER[$i]=""
|
||||
if [ "$rc" -eq 42 ]; then
|
||||
log "miner ${MINER_LABEL[$i]}: the hourly program changed (exit 42); restarting it now"
|
||||
MINER_RESTART_AT[$i]=$SECONDS
|
||||
else
|
||||
delay=$(( RANDOM % 56 + 5 ))
|
||||
MINER_RESTARTS[$i]=$(( ${MINER_RESTARTS[$i]} + 1 ))
|
||||
MINER_RESTART_AT[$i]=$(( SECONDS + delay ))
|
||||
log "miner ${MINER_LABEL[$i]} exited with code $rc; restarting in $delay s (restart ${MINER_RESTARTS[$i]}). Last lines:"
|
||||
tail -3 "${MINER_LOG[$i]}" 2>/dev/null | while IFS= read -r l; do log " miner: $l"; done
|
||||
fi
|
||||
elif [ -n "${MINER_RESTART_AT[$i]}" ] && [ "$SECONDS" -ge "${MINER_RESTART_AT[$i]}" ] && [ "$NODE_SYNCED" -eq 1 ]; then
|
||||
start_miner $i
|
||||
fi
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
# ---- status -----------------------------------------------------------------------------------------------
|
||||
STATUS_COUNT=0
|
||||
print_status() {
|
||||
local i=1 acc=0 a st mh age node_txt miner_txt sync_txt
|
||||
STATUS_COUNT=$(( STATUS_COUNT + 1 ))
|
||||
if alive "$NODE_PID"; then
|
||||
if read_node; then
|
||||
if [ "$NODE_SYNCED" -eq 1 ]; then sync_txt="synced"; else sync_txt="syncing"; fi
|
||||
node_txt="node $NODE_BLOCKS blocks, $NODE_PEERS peers, $sync_txt"
|
||||
else
|
||||
node_txt="node running (pid $NODE_PID), no reading from the RPC yet"
|
||||
fi
|
||||
elif [ -n "$NODE_RESTART_AT" ]; then
|
||||
node_txt="node restarting in $(( NODE_RESTART_AT - SECONDS )) s"
|
||||
else
|
||||
node_txt="node not running"
|
||||
fi
|
||||
if [ "$MINERS" -eq 0 ]; then
|
||||
miner_txt="no miner (MINERS=0)"
|
||||
else
|
||||
mh="n/a"; age="n/a"
|
||||
while [ $i -le "$MINERS" ]; do
|
||||
acc=$(( acc + ${MINER_ACC_BASE[$i]} + $(accepted_in "${MINER_LOG[$i]}") ))
|
||||
if alive "${MINER_PID[$i]}"; then
|
||||
st="$(grep ' STATUS ' "${MINER_LOG[$i]}" 2>/dev/null | tail -1)"
|
||||
if [ -n "$st" ]; then
|
||||
a="$(printf '%s' "$st" | sed -E 's/.* hash=([0-9.]+) MH\/s wall.* template_age=([0-9.]+)s.*/\1 \2/')"
|
||||
set -- $a; mh="${1:-n/a}"; age="${2:-n/a}"
|
||||
fi
|
||||
fi
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
if [ "$MINERS_UP" -eq 1 ]; then
|
||||
miner_txt="accepted $acc blocks, $mh MH/s, template $age s old"
|
||||
else
|
||||
miner_txt="accepted $acc blocks, miner waiting for the node"
|
||||
fi
|
||||
fi
|
||||
log "status: $miner_txt | $node_txt | up $(uptime_txt)"
|
||||
}
|
||||
|
||||
# ---- uploads ----------------------------------------------------------------------------------------------
|
||||
# JSON {label, machine, run_id, lines} with the last 256 KB of the file, as site/api/log.mjs expects.
|
||||
upload_one() {
|
||||
local file=$1 label=$2 json="$LOGS/.upload-$$-$RANDOM.json" code
|
||||
[ -s "$file" ] || return 0
|
||||
{
|
||||
printf '{"label":"%s","machine":"%s","run_id":"%s","lines":"' "$label" "$HOST" "$RUN_ID"
|
||||
tail -c 262144 "$file" | tr -d '\000-\010\013\014\016-\037' | awk 'BEGIN{ORS=""} { gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); gsub(/\t/,"\\t"); printf "%s\\n", $0 }'
|
||||
printf '"}'
|
||||
} > "$json"
|
||||
code="$(curl -sS --max-time 60 -X POST "$LOG_URL" -H "Content-Type: application/json" -H "x-igneum-key: $LOG_KEY" --data-binary "@$json" -o /dev/null -w '%{http_code}' 2>&1)"
|
||||
rm -f "$json"
|
||||
printf '%s' "$code"
|
||||
}
|
||||
|
||||
upload_logs() {
|
||||
local i=1 r
|
||||
r="$(upload_one "$LAUNCHER_LOG" "$LABEL_BASE")"; [ "$r" = "200" ] || log "upload of the launcher log failed: $r"
|
||||
r="$(upload_one "$NODE_LOG" "nodelog-$LABEL_BASE")"; [ "$r" = "200" ] || [ -z "$r" ] || log "upload of the node log failed: $r"
|
||||
while [ $i -le "$MINERS" ]; do
|
||||
r="$(upload_one "${MINER_LOG[$i]}" "miner-${MINER_LABEL[$i]}")"; [ "$r" = "200" ] || [ -z "$r" ] || log "upload of the miner log failed: $r"
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
UPLOAD_PID=""
|
||||
upload_logs_bg() {
|
||||
alive "$UPLOAD_PID" && return 0 # the previous upload is still running
|
||||
upload_logs &
|
||||
UPLOAD_PID=$!
|
||||
}
|
||||
|
||||
# ---- stop -------------------------------------------------------------------------------------------------
|
||||
STOPPING=0; STOP_REASON=""
|
||||
on_signal() { STOPPING=1; STOP_REASON="$1"; }
|
||||
trap 'on_signal "Ctrl+C"' INT
|
||||
trap 'on_signal "stop requested"' TERM
|
||||
trap 'on_signal "window closed"' HUP
|
||||
|
||||
cleanup() {
|
||||
trap '' INT TERM HUP
|
||||
local acc=0 i=1
|
||||
log "$STOP_REASON: stopping the miners, then the node"
|
||||
[ "$MINERS" -gt 0 ] && stop_all_miners
|
||||
stop_node
|
||||
alive "$CAFFEINATE_PID" && kill "$CAFFEINATE_PID" 2>/dev/null
|
||||
alive "$UPLOAD_PID" && wait "$UPLOAD_PID" 2>/dev/null
|
||||
while [ $i -le "$MINERS" ]; do acc=$(( acc + ${MINER_ACC_BASE[$i]} )); i=$(( i + 1 )); done
|
||||
log "SUMMARY after $(uptime_txt): node started $NODE_STARTS time(s), restarts $NODE_RESTARTS, $STATUS_COUNT status lines, $acc accepted blocks; data stays in $DATA"
|
||||
upload_logs
|
||||
rm -f "$PIDFILE" "$RUNFILE"
|
||||
log "stopped. This window can be closed."
|
||||
}
|
||||
|
||||
# ---- run --------------------------------------------------------------------------------------------------
|
||||
start_node
|
||||
sleep 2
|
||||
if ! alive "$NODE_PID"; then
|
||||
log "igneumd exited at once. Last lines of $NODE_LOG:"
|
||||
tail -8 "$NODE_LOG" 2>/dev/null | while IFS= read -r l; do log " node: $l"; done
|
||||
log "Nothing to restart. If it mentions the database, delete \"$DATA\" and start again (the chain resyncs from the seed in seconds)."
|
||||
alive "$CAFFEINATE_PID" && kill "$CAFFEINATE_PID" 2>/dev/null
|
||||
rm -f "$PIDFILE" "$RUNFILE"
|
||||
exit 1
|
||||
fi
|
||||
log "running. Press Ctrl+C to stop. Status every $STATUS_SECS s, logs uploaded every $UPLOAD_SECS s."
|
||||
log "node: syncing, waiting for the first peer ($SEED_PEERS)"
|
||||
|
||||
LAST_SYNC_CHECK=$SECONDS
|
||||
LAST_STATUS=$SECONDS
|
||||
LAST_UPLOAD=$SECONDS
|
||||
while [ "$STOPPING" -eq 0 ]; do
|
||||
sleep 1
|
||||
[ "$STOPPING" -eq 0 ] || break
|
||||
|
||||
# the node: crash, restart
|
||||
if [ -n "$NODE_PID" ] && ! alive "$NODE_PID" && [ -z "$NODE_RESTART_AT" ]; then
|
||||
wait "$NODE_PID" 2>/dev/null; rc=$?
|
||||
NODE_RESTARTS=$(( NODE_RESTARTS + 1 ))
|
||||
delay=$(( RANDOM % 56 + 5 ))
|
||||
NODE_RESTART_AT=$(( SECONDS + delay ))
|
||||
NODE_SYNCED=0
|
||||
log "igneumd exited with code $rc after $(( SECONDS - NODE_STARTED_AT )) s; restarting in $delay s (restart $NODE_RESTARTS). Last lines:"
|
||||
tail -5 "$NODE_LOG" 2>/dev/null | while IFS= read -r l; do log " node: $l"; done
|
||||
if [ "$MINERS_UP" -eq 1 ]; then log "stopping the miners until the node is back and synced (their connection died with it)"; stop_all_miners; fi
|
||||
fi
|
||||
if [ -n "$NODE_RESTART_AT" ] && [ "$SECONDS" -ge "$NODE_RESTART_AT" ]; then start_node; fi
|
||||
|
||||
# sync, then the miners
|
||||
if alive "$NODE_PID" && [ "$NODE_SYNCED" -eq 0 ] && [ $(( SECONDS - LAST_SYNC_CHECK )) -ge 5 ]; then
|
||||
LAST_SYNC_CHECK=$SECONDS
|
||||
if check_sync; then
|
||||
NODE_SYNCED=1; NODE_SYNCED_AT=$SECONDS
|
||||
log "node synced: $NODE_BLOCKS blocks, $NODE_PEERS peer(s), $(( SECONDS - NODE_STARTED_AT )) s after the start"
|
||||
if [ "$MINERS" -gt 0 ] && [ "$MINERS_UP" -eq 0 ]; then start_all_miners; fi
|
||||
elif [ "$NODE_READ_OK" -eq 1 ]; then
|
||||
log "node: syncing, $NODE_BLOCKS blocks ($NODE_HEADERS headers, $NODE_PEERS peers)"
|
||||
if [ "$NOPEER_WARNED" -eq 0 ] && [ "$NODE_PEERS" -eq 0 ] && [ $(( SECONDS - NODE_STARTED_AT )) -ge 90 ]; then
|
||||
NOPEER_WARNED=1
|
||||
log "no peer after 90 s: is the seed node reachable at $SEED_PEERS? The node keeps retrying (backoff up to 8 minutes)."
|
||||
fi
|
||||
elif [ $(( SECONDS - NODE_STARTED_AT )) -ge 60 ]; then
|
||||
log "node: no answer from the RPC at $RPC_URL yet (still opening its database?); see $NODE_LOG"
|
||||
fi
|
||||
fi
|
||||
[ "$MINERS" -gt 0 ] && tend_miners
|
||||
|
||||
if [ $(( SECONDS - LAST_STATUS )) -ge "$STATUS_SECS" ]; then LAST_STATUS=$SECONDS; print_status; fi
|
||||
if [ $(( SECONDS - LAST_UPLOAD )) -ge "$UPLOAD_SECS" ]; then LAST_UPLOAD=$SECONDS; upload_logs_bg; fi
|
||||
done
|
||||
cleanup
|
||||
exit 0
|
||||
19
packaging/mac/app/launcher.sh
Executable file
19
packaging/mac/app/launcher.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
# Bundle executable of "Igneum Devnet.app" (Contents/MacOS/Igneum Devnet). Finder runs this with no terminal, so
|
||||
# it opens Resources/igneum-devnet.sh in Terminal and exits. A shell script, not an AppleScript applet: Terminal
|
||||
# opening a file it was handed by `open -a Terminal` needs no Automation consent, an AppleScript `tell Terminal
|
||||
# to do script` would prompt "Igneum Devnet wants to control Terminal" on the first run.
|
||||
# Before that it removes the quarantine flag from the bundle's own files: once the user has let the app through
|
||||
# Gatekeeper, the binaries inside would still be checked one by one on their first exec and refused as
|
||||
# unidentified. The flag can only be removed on a writable volume, so the app must be run from Applications
|
||||
# (or any folder), not straight from the DMG.
|
||||
CONTENTS="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
RES="$CONTENTS/Resources"
|
||||
if ! xattr -dr com.apple.quarantine "$RES" 2>/dev/null; then
|
||||
if [ -n "$(xattr -p com.apple.quarantine "$RES/igneum-devnet.sh" 2>/dev/null)" ]; then
|
||||
osascript -e 'display dialog "Drag Igneum Devnet into Applications first, then open it from there. It cannot start from the disk image." buttons {"OK"} default button 1 with title "Igneum Devnet"' >/dev/null 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chmod +x "$RES/igneum-devnet.sh" "$RES/bin/"* 2>/dev/null
|
||||
exec /usr/bin/open -a Terminal "$RES/igneum-devnet.sh"
|
||||
82
packaging/mac/build-dmg.sh
Executable file
82
packaging/mac/build-dmg.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
# Builds packaging/mac/dist/igneum-devnet-mac.dmg: "Igneum Devnet.app" + README.txt + "Stop Igneum.command" + a link
|
||||
# to /Applications. Reuses the binaries already built in the repo (nothing is compiled here); copies of the
|
||||
# binaries are stripped and re-signed ad hoc (strip invalidates the linker signature, and arm64 macOS refuses to
|
||||
# run an unsigned binary). The originals are not touched. 3 October 2026.
|
||||
#
|
||||
# packaging/mac/build-dmg.sh build
|
||||
# NODE=<path> packaging/mac/build-dmg.sh use another node binary
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
NODE="${NODE:-$ROOT/vendor/igneum-node/target-rename/release/igneumd}"
|
||||
MINER="${MINER:-$ROOT/vendor/igneum-node/target/release/igneum-miner}"
|
||||
WORKER="${WORKER:-$ROOT/proto-metal/igneum-bench}"
|
||||
ICON_PNG="$ROOT/site/icon-512.png"
|
||||
BUILD="$HERE/build"
|
||||
DIST="$HERE/dist"
|
||||
DMG="$DIST/igneum-devnet-mac.dmg"
|
||||
APP="$BUILD/dmg/Igneum Devnet.app"
|
||||
STAMP="$(date -u +%Y%m%d%H%M)"
|
||||
|
||||
if [ ! -x "$NODE" ]; then
|
||||
if [ -x "$ROOT/vendor/igneum-node/target/release/kaspad" ]; then
|
||||
echo "note: $NODE is missing; using target/release/kaspad renamed to igneumd"
|
||||
NODE="$ROOT/vendor/igneum-node/target/release/kaspad"
|
||||
else
|
||||
echo "no node binary at $NODE"; exit 1
|
||||
fi
|
||||
fi
|
||||
for f in "$MINER" "$WORKER"; do [ -x "$f" ] || { echo "missing: $f"; exit 1; }; done
|
||||
for f in "$NODE" "$MINER" "$WORKER"; do
|
||||
file "$f" | grep -q 'arm64' || { echo "$f is not an arm64 binary"; exit 1; }
|
||||
done
|
||||
|
||||
rm -rf "$BUILD"
|
||||
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources/bin" "$DIST"
|
||||
|
||||
# the bundle
|
||||
sed "s/VERSION_STAMP/$STAMP/" "$HERE/app/Info.plist" > "$APP/Contents/Info.plist"
|
||||
printf 'APPL????' > "$APP/Contents/PkgInfo"
|
||||
cp "$HERE/app/launcher.sh" "$APP/Contents/MacOS/Igneum Devnet"
|
||||
cp "$HERE/app/igneum-devnet.sh" "$APP/Contents/Resources/igneum-devnet.sh"
|
||||
cp "$NODE" "$APP/Contents/Resources/bin/igneumd"
|
||||
cp "$MINER" "$APP/Contents/Resources/bin/igneum-miner"
|
||||
cp "$WORKER" "$APP/Contents/Resources/bin/igneum-bench"
|
||||
chmod 755 "$APP/Contents/MacOS/Igneum Devnet" "$APP/Contents/Resources/igneum-devnet.sh" "$APP/Contents/Resources/bin/"*
|
||||
|
||||
# strip the copies, then sign them ad hoc again
|
||||
for b in "$APP/Contents/Resources/bin/"*; do
|
||||
before=$(stat -f %z "$b")
|
||||
strip "$b" 2>/dev/null || strip -x "$b"
|
||||
codesign -s - -f "$b" 2>/dev/null
|
||||
codesign -v "$b"
|
||||
echo "$(basename "$b"): $before -> $(stat -f %z "$b") bytes, signed ad hoc"
|
||||
done
|
||||
# each copy must still run
|
||||
v="$("$APP/Contents/Resources/bin/igneumd" --version 2>&1 || true)"; echo "node: ${v%%$'\n'*}"
|
||||
v="$("$APP/Contents/Resources/bin/igneum-miner" 2>&1 || true)"; case "$v" in usage*) ;; *) echo "igneum-miner copy does not run: $v"; exit 1 ;; esac
|
||||
v="$(echo quit | "$APP/Contents/Resources/bin/igneum-bench" --serve 2>&1)"; case "$v" in "ready metal"*) echo "worker: $v" ;; *) echo "igneum-bench copy does not serve: $v"; exit 1 ;; esac
|
||||
|
||||
# icon from the site's 512 px icon
|
||||
if [ -f "$ICON_PNG" ]; then
|
||||
ICONSET="$BUILD/AppIcon.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
for s in 16 32 128 256 512; do
|
||||
sips -z $s $s "$ICON_PNG" --out "$ICONSET/icon_${s}x${s}.png" >/dev/null
|
||||
d=$(( s * 2 ))
|
||||
if [ $d -le 1024 ]; then sips -z $d $d "$ICON_PNG" --out "$ICONSET/icon_${s}x${s}@2x.png" >/dev/null; fi
|
||||
done
|
||||
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"
|
||||
fi
|
||||
|
||||
# the rest of the image
|
||||
cp "$HERE/dmg/README.txt" "$BUILD/dmg/README.txt"
|
||||
cp "$HERE/app/Stop Igneum.command" "$BUILD/dmg/Stop Igneum.command"
|
||||
chmod 755 "$BUILD/dmg/Stop Igneum.command"
|
||||
ln -s /Applications "$BUILD/dmg/Applications"
|
||||
|
||||
rm -f "$DMG"
|
||||
hdiutil create -volname "Igneum Devnet" -srcfolder "$BUILD/dmg" -ov -format ULFO -fs HFS+ "$DMG" >/dev/null
|
||||
hdiutil verify "$DMG" >/dev/null
|
||||
echo "built $DMG ($(du -h "$DMG" | cut -f1), $(stat -f %z "$DMG") bytes, version $STAMP)"
|
||||
9
packaging/mac/dmg/README.txt
Normal file
9
packaging/mac/dmg/README.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Igneum Devnet for Mac (Apple silicon only: M1 or later). Nothing is bought or sold; this is a test network.
|
||||
1. Drag "Igneum Devnet" into Applications. It will not start from this disk image.
|
||||
2. First time: in Applications, right-click "Igneum Devnet" and choose Open, then Open again. The app is unsigned.
|
||||
If macOS still refuses, open System Settings > Privacy & Security, scroll down, click "Open Anyway", open the app again.
|
||||
3. A Terminal window opens. It starts the node, shows "syncing, N blocks" until it has the chain (usually under a minute),
|
||||
then starts the Metal miner. Every 30 s one status line: accepted blocks, MH/s, template age, node blocks and peers.
|
||||
4. Your miner appears on igneum.network/live within about a minute of its first block, under the id the window prints.
|
||||
5. To stop: press Ctrl+C in that window (miners stop first, then the node). If the window was closed, open "Stop Igneum".
|
||||
The Mac stays awake while the window is open. Data: ~/Library/Application Support/Igneum, logs: ~/Library/Logs/Igneum.
|
||||
Loading…
Reference in a new issue