Packaging: Igneum Miner for Mac and Windows, one-tap installs with the coin on every file and window

Mac: bundle renamed Igneum Miner.app (network.igneum.miner, 0.1.0, LSMinimumSystemVersion 11.0, igneum.icns),
Terminal title Igneum Miner, seed line in the first-run text, branded DMG (volume icon, background, icon slots via
dmgbuild), dist/Igneum-Miner-0.1.0.dmg at 16.2 MB, tested synced against the live node on 27400/27401.
Windows: packaging/windows with windres .rc files and embed-resources.sh (relink commands for the next cross-build),
Igneum-Miner.iss (Program Files, Start Menu group, firewall rule, uninstall stops the processes, licence, seed line),
BUILD-INSTALLER.bat and build-installer.ps1 for the project lead's PC (winget Inno Setup 6, rcedit fallback for the exe icon).
Icons: brand/icons/make-icons.py makes igneum.icns, igneum.ico, the Inno wizard art and the DMG background.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-03 22:26:57 +00:00
parent d4cd91f55f
commit 3baf399615
29 changed files with 815 additions and 95 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
brand/icons/igneum-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
brand/icons/igneum.icns Normal file

Binary file not shown.

BIN
brand/icons/igneum.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

179
brand/icons/make-icons.py Normal file
View file

@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Builds the Igneum Miner icon and installer art from brand/igneum-coin-1024.png (3 October 2026).
Outputs, all in brand/icons/:
igneum.icns Mac app and DMG volume icon (iconutil from the full 10-file iconset)
igneum.ico Windows icon, 16 24 32 48 64 128 256 px, uncompressed BMP entries
igneum-256.png the coin cut out on transparency (the source of both)
inno-wizard-164x314.bmp Inno Setup WizardImageFile (left side of the welcome and finish pages)
inno-wizard-small-55x58.bmp Inno Setup WizardSmallImageFile (top right of the other pages)
inno-banner-150x57.bmp header banner (NSIS size; kept for the Windows README and any header-style page)
dmg-background.png 660x400 DMG window background
dmg-background@2x.png the same at 1320x800 for Retina; dmg-background.tiff holds both when tiffutil is present
Needs Pillow and, on a Mac, iconutil and tiffutil (both ship with macOS).
python3 brand/icons/make-icons.py
"""
import os, subprocess, sys, shutil, tempfile
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
BRAND = os.path.dirname(HERE)
COIN = os.path.join(BRAND, 'igneum-coin-1024.png')
FONT = os.path.join(BRAND, 'Unbounded.ttf')
OBSIDIAN = (12, 12, 14)
EMBER = (242, 84, 27)
MOLTEN = (255, 179, 92)
BONE = (244, 241, 236)
RADIUS = 480.0 # the ember ring's outer edge, measured on the 1024 px coin
CENTRE = (511.5, 511.5)
def font(size, weight='SemiBold'):
f = ImageFont.truetype(FONT, size)
try:
f.set_variation_by_name(weight)
except Exception:
pass
return f
def coin_rgba():
"""The coin on transparency: a 4x supersampled circular mask at the ring's outer edge."""
im = Image.open(COIN).convert('RGBA')
big = Image.new('L', (4096, 4096), 0)
d = ImageDraw.Draw(big)
cx, cy, r = CENTRE[0] * 4 + 2, CENTRE[1] * 4 + 2, (RADIUS - 0.5) * 4
d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)
mask = big.resize((1024, 1024), Image.LANCZOS)
im.putalpha(mask)
return im
def fit(im, size):
return im.resize((size, size), Image.LANCZOS)
def make_icns(coin):
tmp = tempfile.mkdtemp()
iconset = os.path.join(tmp, 'igneum.iconset')
os.mkdir(iconset)
for s in (16, 32, 128, 256, 512):
fit(coin, s).save(os.path.join(iconset, f'icon_{s}x{s}.png'))
fit(coin, s * 2).save(os.path.join(iconset, f'icon_{s}x{s}@2x.png'))
out = os.path.join(HERE, 'igneum.icns')
subprocess.run(['iconutil', '-c', 'icns', iconset, '-o', out], check=True)
shutil.rmtree(tmp)
return out
def make_ico(coin):
out = os.path.join(HERE, 'igneum.ico')
sizes = [(s, s) for s in (16, 24, 32, 48, 64, 128, 256)]
base = fit(coin, 256)
try:
base.save(out, format='ICO', sizes=sizes, bitmap_format='bmp')
except TypeError:
base.save(out, format='ICO', sizes=sizes)
return out
def wordmark(text, size, colour=BONE, weight='SemiBold'):
f = font(size, weight)
l, t, r, b = f.getbbox(text)
im = Image.new('RGBA', (r - l + 4, b - t + 4), (0, 0, 0, 0))
ImageDraw.Draw(im).text((2 - l, 2 - t), text, font=f, fill=colour)
return im
def paste_centre(dst, src, cx, cy):
dst.alpha_composite(src, (int(cx - src.width / 2), int(cy - src.height / 2)))
def bmp(im, path):
im.convert('RGB').save(path, format='BMP')
def make_inno(coin):
# 164x314: the coin above a two-line wordmark
big = Image.new('RGBA', (164, 314), OBSIDIAN + (255,))
paste_centre(big, fit(coin, 112), 82, 110)
paste_centre(big, wordmark('IGNEUM', 19, BONE, 'Bold'), 82, 205)
paste_centre(big, wordmark('MINER', 13, MOLTEN, 'Medium'), 82, 232)
bmp(big, os.path.join(HERE, 'inno-wizard-164x314.bmp'))
# 55x58: the coin alone
small = Image.new('RGBA', (55, 58), OBSIDIAN + (255,))
paste_centre(small, fit(coin, 50), 27.5, 29)
bmp(small, os.path.join(HERE, 'inno-wizard-small-55x58.bmp'))
# 150x57: coin left, wordmark right
banner = Image.new('RGBA', (150, 57), OBSIDIAN + (255,))
paste_centre(banner, fit(coin, 44), 28, 28.5)
w1 = wordmark('IGNEUM', 15, BONE, 'Bold')
w2 = wordmark('MINER', 10, MOLTEN, 'Medium')
banner.alpha_composite(w1, (56, 14))
banner.alpha_composite(w2, (57, 34))
bmp(banner, os.path.join(HERE, 'inno-banner-150x57.bmp'))
def arrow(draw, x0, x1, y, colour, width):
draw.line((x0, y, x1 - width * 2, y), fill=colour, width=width)
h = width * 3
draw.polygon([(x1, y), (x1 - h * 1.6, y - h), (x1 - h * 1.6, y + h)], fill=colour)
# Finder icon slots on the DMG (1x pixels, icon size 96): the app at APP_SLOT, the Applications link at APPS_SLOT,
# README.txt and the Stop command in the row below. build-dmg.sh uses the same numbers.
ICON = 96
APP_SLOT = (165, 130)
APPS_SLOT = (495, 130)
ROW2_Y = 330
def dashed_box(im, cx, cy, r, colour, width=3, dash=16, gap=10, radius=18):
"""A dashed rounded square: the outline drawn once, then masked by diagonal bars."""
W, H = im.size
layer = Image.new('RGBA', (W, H), (0, 0, 0, 0))
ImageDraw.Draw(layer).rounded_rectangle((cx - r, cy - r, cx + r, cy + r), radius=radius, outline=colour, width=width)
bars = Image.new('L', (W, H), 0)
bd = ImageDraw.Draw(bars)
k = -H
while k < W + H:
bd.line((k, 0, k + H, H), fill=255, width=dash)
k += dash + gap
alpha = layer.split()[3]
alpha = Image.composite(alpha, Image.new('L', (W, H), 0), bars)
layer.putalpha(alpha)
im.alpha_composite(layer)
def make_dmg_background(coin):
"""660x400 at 1x; drawn at 2x and downsampled. The coin is painted under the app slot, so the slot reads as the
product even before Finder draws the app icon on top of it."""
S = 2
W, H = 660 * S, 400 * S
im = Image.new('RGBA', (W, H), OBSIDIAN + (255,))
d = ImageDraw.Draw(im)
ax, ay = APP_SLOT[0] * S, APP_SLOT[1] * S
bx, by = APPS_SLOT[0] * S, APPS_SLOT[1] * S
glow = Image.new('RGBA', (W, H), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
for i, a in ((76 * S, 10), (66 * S, 12), (58 * S, 14)):
gd.ellipse((ax - i, ay - i, ax + i, ay + i), fill=EMBER + (a,))
im.alpha_composite(glow)
paste_centre(im, fit(coin, ICON * S), ax, ay)
dashed_box(im, bx, by, (ICON // 2 + 14) * S, MOLTEN + (190,), width=2 * S, dash=9 * S, gap=6 * S, radius=9 * S)
arrow(d, (APP_SLOT[0] + 78) * S, (APPS_SLOT[0] - 78) * S, ay, EMBER + (255,), 4 * S)
paste_centre(im, wordmark('Drag Igneum Miner to Applications', 16 * S, BONE, 'Medium'), 330 * S, 236 * S)
paste_centre(im, wordmark('Then open it from Applications. Nobody from Igneum will ever ask for your seed.', 8 * S, (150, 148, 145), 'Regular'), 330 * S, 262 * S)
im2x = im.convert('RGB')
im2x.save(os.path.join(HERE, 'dmg-background@2x.png'))
im1x = im2x.resize((660, 400), Image.LANCZOS)
im1x.save(os.path.join(HERE, 'dmg-background.png'))
tiff = os.path.join(HERE, 'dmg-background.tiff')
if shutil.which('tiffutil'):
subprocess.run(['tiffutil', '-cathidpicheck', os.path.join(HERE, 'dmg-background.png'),
os.path.join(HERE, 'dmg-background@2x.png'), '-out', tiff], check=True)
def main():
coin = coin_rgba()
fit(coin, 256).save(os.path.join(HERE, 'igneum-256.png'))
if sys.platform == 'darwin' and shutil.which('iconutil'):
print('icns:', make_icns(coin))
else:
print('icns skipped: iconutil is a macOS tool')
print('ico:', make_ico(coin))
make_inno(coin)
make_dmg_background(coin)
for f in sorted(os.listdir(HERE)):
p = os.path.join(HERE, f)
if os.path.isfile(p) and not f.endswith('.py'):
print(f'{os.path.getsize(p):>8} {f}')
if __name__ == '__main__':
main()

View file

@ -1,38 +1,44 @@
# Igneum Devnet for Mac (packaging/mac)
# Igneum Miner 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.
A DMG a miner can open on an Apple silicon Mac to run a 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. Renamed from "Igneum
Devnet" to "Igneum Miner" the same evening (bundle id network.igneum.miner, version 0.1.0); the devnet is still the only
network and its name appears in the status lines, never in a window title.
## Build
packaging/mac/build-dmg.sh
Output: `packaging/mac/dist/igneum-devnet-mac.dmg` (about 17 MB, lzfse). The script takes
Output: `packaging/mac/dist/Igneum-Miner-0.1.0.dmg` (about 16 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.
runs. The originals are untouched. The icon is `brand/icons/igneum.icns` and the DMG art `brand/icons/dmg-background.tiff`
(both from `python3 brand/icons/make-icons.py`). The image is laid out by `dmgbuild` (`pip3 install dmgbuild`, settings in
`dmg/settings.py`: volume "Igneum Miner", hidden `.VolumeIcon.icns` with the custom-icon flag, background, icon slots app
(165,130), Applications (495,130), README and Stop command on the row below, no Finder scripting needed). Without dmgbuild
the script builds a plain hdiutil image and says so. `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`) |
| `Igneum Miner.app` | the launcher bundle (below) |
| `README.txt` | 10 lines for the miner (`dmg/README.txt`), with the seed line |
| `Stop Igneum Miner.command` | stops a copy whose window was closed without Ctrl+C (`app/Stop Igneum Miner.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)
Igneum Miner.app/Contents/MacOS/Igneum Miner app/launcher.sh: removes quarantine from Resources, opens the script in Terminal
Igneum Miner.app/Contents/Resources/igneum-miner.sh the run script (app/igneum-miner.sh); sets the Terminal title "Igneum Miner"
Igneum Miner.app/Contents/Resources/bin/igneumd, igneum-miner, igneum-bench
Igneum Miner.app/Contents/Resources/igneum.icns
Igneum Miner.app/Contents/Info.plist app/Info.plist (CFBundleVersion = build stamp, LSMinimumSystemVersion 11.0)
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.
`tell application "Terminal" to do script` would prompt "Igneum Miner 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.
@ -59,17 +65,17 @@ is synced again, miner exit 42 (hourly program change) restarted at once, other
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/'`.
launcher controls the order. `igneum-miner.sh --stop` and `Stop Igneum Miner.command` signal the launcher from the pid file
and fall back to `pkill -f 'Igneum Miner.app/Contents/Resources/bin/'` (the bundle path only, never a bare name).
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 >
Open button (unsigned and not notarized until the foundation has an Apple developer account). 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"`.
README.txt. Removing the flag by hand also works: `xattr -dr com.apple.quarantine "/Applications/Igneum Miner.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)
@ -94,3 +100,15 @@ Two things the test caught and fixed: an unquoted `$NODE_EXE` in the version lin
`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 ...'`.
## Test of the renamed bundle (3 October 2026, 23:20, this Mac)
Mounted `Igneum-Miner-0.1.0.dmg` (16,994,318 bytes): volume "Igneum Miner", `.VolumeIcon.icns` present and the custom-icon
flag set on the root (`xattr -px com.apple.FinderInfo`), `.DS_Store` with the four icon slots, `.background.tiff`. Copied the
app out and ran its script in a real Terminal window (`open -a Terminal`, as the launcher does) with `MINERS=0
SEED_PEERS=127.0.0.1:26611 IGNEUM_RPC_PORT=27400 IGNEUM_P2P_PORT=27401` and scratch data and log folders: the window title
read "joshm, Igneum Miner, sleep, igneum-miner.sh, 80x24" (Terminal joins those with its own dashes), the first two log lines are the version line and the seed
line, synced in 77 s (11,995 blocks, the Mac was under load 25+), `igneum-miner.sh --stop` stopped it in 4 s through the
pid file, no leftover process, ports free. The live node on 26610/26611 was only a peer. Note: `proto-metal/igneum-bench`
carries `minos 16.0` in its load command (built without a `-target`), so on a Mac older than macOS 26 the worker may refuse
to load even though the bundle says 11.0; a rebuild with `-target arm64-apple-macos11` fixes that.

View file

@ -3,23 +3,27 @@
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Igneum Devnet</string>
<string>Igneum Miner</string>
<key>CFBundleDisplayName</key>
<string>Igneum Devnet</string>
<string>Igneum Miner</string>
<key>CFBundleIdentifier</key>
<string>network.igneum.devnet</string>
<string>network.igneum.miner</string>
<key>CFBundleVersion</key>
<string>VERSION_STAMP</string>
<key>CFBundleShortVersionString</key>
<string>0.1</string>
<string>0.1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>Igneum Devnet</string>
<string>Igneum Miner</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<string>igneum</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSMinimumSystemVersion</key>
<string>13.3</string>
<string>11.0</string>
<key>LSArchitecturePriority</key>
<array>
<string>arm64</string>
@ -27,6 +31,6 @@
<key>LSRequiresNativeExecution</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Igneum devnet. Nothing is bought or sold.</string>
<string>Igneum. Nothing is bought or sold.</string>
</dict>
</plist>

View file

@ -1,20 +1,21 @@
#!/bin/bash
# Stops a running Igneum Devnet (miners, Metal worker, then the node) when its window was closed without Ctrl+C.
[ -t 1 ] && printf '\033]0;Stop Igneum Miner\007'
# Stops a running Igneum Miner (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"
RUNFILE="$DATA/igneum-miner.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)"
left="$(pgrep -f 'Igneum Miner.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
pkill -TERM -f 'Igneum Miner.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."
pkill -KILL -f 'Igneum Miner.app/Contents/Resources/bin/' 2>/dev/null
rm -f "$DATA/igneum-miner.pid" "$RUNFILE"
echo "Igneum Miner is stopped."
else
echo "Igneum is not running."
echo "Igneum Miner is not running."
fi

View file

@ -1,7 +1,7 @@
#!/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.
# Igneum Miner for Apple silicon Macs: the node and the Metal miner in one Terminal window. The devnet is the only
# network for now. Lives in "Igneum Miner.app/Contents/Resources" next to bin/ (igneumd, igneum-miner, igneum-bench).
# The app's launcher opens this script in Terminal. 3 October 2026 (was the devnet launcher, renamed the same day).
#
# What it does, in order:
# 1. Checks the Mac (Apple silicon), the binaries, the ports, and that no other copy is running.
@ -16,12 +16,14 @@
# 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,
# 6. Ctrl+C, closing the window (SIGHUP) or `igneum-miner.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 ---------------------------------------------------------------------------------------------
VERSION="0.1.0"
NETWORK="devnet"
# 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}"
@ -45,8 +47,8 @@ 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"
PIDFILE="$DATA/igneum-miner.pid"
RUNFILE="$DATA/igneum-miner.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/-*$//')"
@ -71,12 +73,15 @@ uptime_txt() {
printf '%02d:%02d:%02d' $(( s / 3600 )) $(( s % 3600 / 60 )) $(( s % 60 ))
}
# ---- --stop: stop a running instance (also used by "Stop Igneum.command") ----------------------------------
# The Terminal window and tab title: the product name, never the script path (OSC 0 sets both in Terminal.app).
[ -t 1 ] && printf '\033]0;Igneum Miner\007'
# ---- --stop: stop a running instance (also used by "Stop Igneum Miner.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"
echo "stopping Igneum Miner (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
@ -84,27 +89,27 @@ stop_mode() {
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)"
left="$(pgrep -f 'Igneum Miner.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
pkill -TERM -f 'Igneum Miner.app/Contents/Resources/bin/' 2>/dev/null
sleep 5
pkill -KILL -f 'Igneum Devnet.app/Contents/Resources/bin/' 2>/dev/null
pkill -KILL -f 'Igneum Miner.app/Contents/Resources/bin/' 2>/dev/null
fi
rm -f "$PIDFILE" "$RUNFILE"
echo "Igneum is stopped."
echo "Igneum Miner 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 ;;
--help|-h) sed -n '2,20p' "$0"; echo; echo "usage: igneum-miner.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."
echo "Igneum Miner 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
@ -112,22 +117,23 @@ for f in "$NODE_EXE" "$MINER_EXE" "$WORKER_EXE"; do
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."
echo "Igneum Miner is already running in another window (launcher pid $(cat "$PIDFILE")). Press Ctrl+C there, or open Stop Igneum Miner."
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."
echo "port $port is already in use by pid $busy ($(ps -p "$busy" -o comm= 2>/dev/null)). Is a node already running? Open Stop Igneum Miner 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"
printf 'pid=%s\nscript=%s\nrpc=%s\nrun=%s\n' "$$" "$RES/igneum-miner.sh" "$RPC_URL" "$RUN_ID" > "$RUNFILE"
log "Igneum devnet on $HOST (run $RUN_ID)"
log "Igneum Miner $VERSION on $HOST, network $NETWORK (run $RUN_ID)"
log "Nobody from Igneum will ever ask for your seed. Nothing is bought or sold on this network."
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)"
@ -147,7 +153,7 @@ start_node() {
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)
args=("--$NETWORK" "--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.

View file

@ -1,8 +1,8 @@
#!/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
# Bundle executable of "Igneum Miner.app" (Contents/MacOS/Igneum Miner). Finder runs this with no terminal, so
# it opens Resources/igneum-miner.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.
# to do script` would prompt "Igneum Miner 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
@ -10,10 +10,10 @@
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
if [ -n "$(xattr -p com.apple.quarantine "$RES/igneum-miner.sh" 2>/dev/null)" ]; then
osascript -e 'display dialog "Drag Igneum Miner into Applications first, then open it from there. It cannot start from the disk image." buttons {"OK"} default button 1 with title "Igneum Miner"' >/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"
chmod +x "$RES/igneum-miner.sh" "$RES/bin/"* 2>/dev/null
exec /usr/bin/open -a Terminal "$RES/igneum-miner.sh"

View file

@ -1,22 +1,27 @@
#!/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.
# Builds packaging/mac/dist/Igneum-Miner-<version>.dmg: "Igneum Miner.app" + README.txt + "Stop Igneum Miner.command" +
# a link to /Applications, on a branded image (volume "Igneum Miner", coin volume icon, background with the drag arrow,
# icons placed). 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
# packaging/mac/build-dmg.sh build
# NODE=<path> packaging/mac/build-dmg.sh use another node binary
# Needs: brand/icons/igneum.icns and dmg-background.tiff (python3 brand/icons/make-icons.py) and dmgbuild
# (pip3 install dmgbuild) for the icon layout. Without dmgbuild a plain hdiutil image is built and said so.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
VERSION="0.1.0"
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"
ICONS="$ROOT/brand/icons"
BUILD="$HERE/build"
DIST="$HERE/dist"
DMG="$DIST/igneum-devnet-mac.dmg"
APP="$BUILD/dmg/Igneum Devnet.app"
DMG="$DIST/Igneum-Miner-$VERSION.dmg"
STAGE="$BUILD/dmg"
APP="$STAGE/Igneum Miner.app"
STAMP="$(date -u +%Y%m%d%H%M)"
if [ ! -x "$NODE" ]; then
@ -31,19 +36,22 @@ for f in "$MINER" "$WORKER"; do [ -x "$f" ] || { echo "missing: $f"; exit 1; };
for f in "$NODE" "$MINER" "$WORKER"; do
file "$f" | grep -q 'arm64' || { echo "$f is not an arm64 binary"; exit 1; }
done
[ -f "$ICONS/igneum.icns" ] || { echo "no $ICONS/igneum.icns; run: python3 brand/icons/make-icons.py"; exit 1; }
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"
plutil -lint "$APP/Contents/Info.plist" >/dev/null
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 "$HERE/app/launcher.sh" "$APP/Contents/MacOS/Igneum Miner"
cp "$HERE/app/igneum-miner.sh" "$APP/Contents/Resources/igneum-miner.sh"
cp "$ICONS/igneum.icns" "$APP/Contents/Resources/igneum.icns"
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/"*
chmod 755 "$APP/Contents/MacOS/Igneum Miner" "$APP/Contents/Resources/igneum-miner.sh" "$APP/Contents/Resources/bin/"*
# strip the copies, then sign them ad hoc again
for b in "$APP/Contents/Resources/bin/"*; do
@ -58,25 +66,19 @@ v="$("$APP/Contents/Resources/bin/igneumd" --version 2>&1 || true)"; echo "node:
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"
cp "$HERE/dmg/README.txt" "$STAGE/README.txt"
cp "$HERE/app/Stop Igneum Miner.command" "$STAGE/Stop Igneum Miner.command"
chmod 755 "$STAGE/Stop Igneum Miner.command"
rm -f "$DMG"
hdiutil create -volname "Igneum Devnet" -srcfolder "$BUILD/dmg" -ov -format ULFO -fs HFS+ "$DMG" >/dev/null
if command -v dmgbuild >/dev/null 2>&1; then
dmgbuild -s "$HERE/dmg/settings.py" -D "app=$APP" -D "stage=$STAGE" -D "icons=$ICONS" "Igneum Miner" "$DMG"
else
echo "note: dmgbuild is not installed (pip3 install dmgbuild); building a plain image without icon positions or background"
ln -s /Applications "$STAGE/Applications"
cp "$ICONS/igneum.icns" "$STAGE/.VolumeIcon.icns"
hdiutil create -volname "Igneum Miner" -srcfolder "$STAGE" -ov -format ULFO -fs HFS+ "$DMG" >/dev/null
fi
hdiutil verify "$DMG" >/dev/null
echo "built $DMG ($(du -h "$DMG" | cut -f1), $(stat -f %z "$DMG") bytes, version $STAMP)"
echo "built $DMG ($(du -h "$DMG" | cut -f1), $(stat -f %z "$DMG") bytes, version $VERSION build $STAMP)"

View file

@ -1,9 +1,10 @@
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.
Igneum Miner 0.1.0 for Mac (Apple silicon only: M1 or later). Test network; nothing is bought or sold.
Nobody from Igneum will ever ask for your seed.
1. Drag "Igneum Miner" into Applications. It will not start from this disk image.
2. First time: in Applications, right-click "Igneum Miner" 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.
3. A Terminal window called "Igneum Miner" 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: blocks, MH/s, 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".
5. To stop: press Ctrl+C in that window (miners stop first, then the node). If the window was closed, open "Stop Igneum Miner".
The Mac stays awake while the window is open. Data: ~/Library/Application Support/Igneum, logs: ~/Library/Logs/Igneum.

View file

@ -0,0 +1,40 @@
# dmgbuild settings for Igneum-Miner-<version>.dmg (used by build-dmg.sh; pip3 install dmgbuild).
# Defines passed in: app (the built .app), stage (the folder with README.txt and the Stop command), icons (brand/icons).
# Icon slots match brand/icons/make-icons.py (APP_SLOT, APPS_SLOT, ROW2_Y, ICON) and the arrow painted on the background.
import os
app = defines['app']
stage = defines['stage']
icons = defines['icons']
appname = os.path.basename(app)
format = 'ULFO'
filesystem = 'HFS+'
size = None
files = [app, os.path.join(stage, 'README.txt'), os.path.join(stage, 'Stop Igneum Miner.command')]
symlinks = {'Applications': '/Applications'}
icon = os.path.join(icons, 'igneum.icns') # the volume icon (.VolumeIcon.icns, hidden, custom-icon flag set)
background = os.path.join(icons, 'dmg-background.tiff') if os.path.exists(os.path.join(icons, 'dmg-background.tiff')) else os.path.join(icons, 'dmg-background.png')
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
sidebar_width = 180
window_rect = ((200, 120), (660, 400))
default_view = 'icon-view'
show_icon_preview = False
include_icon_view_settings = 'auto'
include_list_view_settings = 'auto'
arrange_by = None
grid_offset = (0, 0)
grid_spacing = 100
scroll_position = (0, 0)
label_pos = 'bottom'
text_size = 12
icon_size = 96
icon_locations = {
appname: (165, 130),
'Applications': (495, 130),
'README.txt': (165, 330),
'Stop Igneum Miner.command': (495, 330),
}

3
packaging/windows/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# built outputs: the windres objects, the assembled payload, the downloaded tools, the installer
build/
dist/

View file

@ -0,0 +1,17 @@
@echo off
rem Builds Igneum-Miner-Setup-0.1.0.exe on a Windows PC. Double-click this file. The first run needs the internet:
rem Inno Setup 6 comes through winget if it is missing, rcedit stamps the icon into the exes if they do not carry it yet.
rem The payload is the extracted igneum-windows folder next to this file, or the zip at the URL in build-installer.ps1.
cd /d "%~dp0"
where powershell >nul 2>nul
if errorlevel 1 (
echo PowerShell was not found. Windows 10 and 11 ship it.
pause
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-installer.ps1" %*
if errorlevel 1 (
echo.
echo The build did not finish. The reason is in the lines above.
)
pause

View file

@ -0,0 +1,112 @@
; Igneum Miner installer for Windows, Inno Setup 6.3 or newer. Compiled by build-installer.ps1, which passes
; /DPayload=<assembled payload folder> and /DArtDir=<brand\icons>. The defaults below are for a run from the repo.
#ifndef Payload
#define Payload "build\payload"
#endif
#ifndef ArtDir
#define ArtDir "..\..\brand\icons"
#endif
#ifndef AppVersion
#define AppVersion "0.1.0"
#endif
#define AppName "Igneum Miner"
#define Publisher "Igneum"
#define Url "https://igneum.network"
#define FirewallRule "Igneum Miner node"
[Setup]
AppId={{A4C1F0E2-6B8D-4E7A-9F31-2C5D8E7B9A01}
AppName={#AppName}
AppVersion={#AppVersion}
AppVerName={#AppName} {#AppVersion}
AppPublisher={#Publisher}
AppPublisherURL={#Url}
AppSupportURL={#Url}
AppUpdatesURL={#Url}
AppCopyright=Igneum. Nothing is bought or sold.
VersionInfoVersion={#AppVersion}.0
VersionInfoCompany={#Publisher}
VersionInfoProductName={#AppName}
VersionInfoDescription={#AppName} Setup
DefaultDirName={autopf}\{#AppName}
DefaultGroupName={#AppName}
DisableProgramGroupPage=yes
LicenseFile=LICENSE.txt
OutputDir=dist
OutputBaseFilename=Igneum-Miner-Setup-{#AppVersion}
SetupIconFile={#ArtDir}\igneum.ico
UninstallDisplayIcon={app}\igneum.ico
UninstallDisplayName={#AppName}
WizardStyle=modern
WizardImageFile={#ArtDir}\inno-wizard-164x314.bmp
WizardSmallImageFile={#ArtDir}\inno-wizard-small-55x58.bmp
Compression=lzma2/max
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=admin
MinVersion=10.0
CloseApplications=no
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Messages]
english.WelcomeLabel2=This will install [name/ver] on your computer.%n%nOne window runs your node and the GPU miners. The install is about 60 MB. Nothing is bought or sold on this network.
english.FinishedHeadingLabel=Igneum Miner is installed
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "firewall"; Description: "Let other Igneum nodes connect to this PC (Windows Firewall rule for igneumd.exe on private networks)"; GroupDescription: "Network:"
[Dirs]
; The launcher writes its logs, the hourly program packs and the built GPU workers next to its scripts.
Name: "{app}"; Permissions: users-modify
[Files]
Source: "{#Payload}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion; Excludes: "*.log,*.seeds,*.DS_Store,packs\*"
Source: "{#ArtDir}\igneum.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "wrappers\Igneum Miner.cmd"; DestDir: "{app}"; Flags: ignoreversion
Source: "wrappers\Stop Igneum Miner.cmd"; DestDir: "{app}"; Flags: ignoreversion
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\Igneum Miner"; Filename: "{app}\Igneum Miner.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Start the node and mine"
Name: "{group}\Stop Igneum Miner"; Filename: "{app}\Stop Igneum Miner.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Stop the miners and the node"
Name: "{group}\Igneum Miner logs"; Filename: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "The folder the log files land in"
Name: "{group}\Uninstall Igneum Miner"; Filename: "{uninstallexe}"; IconFilename: "{app}\igneum.ico"
Name: "{autodesktop}\Igneum Miner"; Filename: "{app}\Igneum Miner.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Tasks: desktopicon
[Run]
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#FirewallRule}"""; Flags: runhidden; Tasks: firewall
Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#FirewallRule}"" dir=in action=allow enable=yes profile=private,domain protocol=TCP program=""{app}\igneumd.exe"""; Flags: runhidden; Tasks: firewall; StatusMsg: "Adding the firewall rule for the node"
; Started through the shortcut so the console window carries the coin icon, and as the signed-in user, not as administrator.
Filename: "{group}\Igneum Miner.lnk"; Description: "Start Igneum Miner now"; Flags: shellexec postinstall nowait skipifsilent runasoriginaluser unchecked
[UninstallRun]
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\stop-igneum.ps1"""; Flags: runhidden waituntilterminated; RunOnceId: "StopIgneum"
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#FirewallRule}"""; Flags: runhidden; RunOnceId: "FirewallRule"
[UninstallDelete]
; Logs, packs and the GPU workers built on this PC are not in the install log; remove them with the folder.
Type: filesandordirs; Name: "{app}"
[Code]
// Stops a running copy before the files are replaced (an upgrade over a running miner).
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
StopScript: String;
ResultCode: Integer;
begin
Result := '';
StopScript := ExpandConstant('{app}\stop-igneum.ps1');
if FileExists(StopScript) then
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
procedure CurPageChanged(CurPageID: Integer);
begin
if CurPageID = wpFinished then
WizardForm.FinishedLabel.Caption := WizardForm.FinishedLabel.Caption + #13#10#13#10 +
'Nobody from Igneum will ever ask for your seed.';
end;

View file

@ -0,0 +1,20 @@
Igneum Miner 0.1.0
Licence: MIT (placeholder; the final licence is pending the foundation's decision).
Copyright (c) 2026 Igneum
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This is a test network. Nothing is bought or sold. Nobody from Igneum will ever ask for your seed.

View file

@ -0,0 +1,11 @@
# Igneum Miner for Windows (packaging/windows)
Inno Setup installer around the windows-app payload (proto-cuda/windows-app/make-package.sh makes igneum-windows.zip).
On the PC: put the extracted `igneum-windows` folder next to this folder (or let the script fetch `IGNEUM_PAYLOAD_URL`,
default dl.igneum.network/igneum-windows.zip), double-click `BUILD-INSTALLER.bat`, read `dist\Igneum-Miner-Setup-0.1.0.exe`.
It installs Inno Setup 6 through winget if missing and stamps the coin icon into the exes with rcedit unless they already
carry it. On the Mac, `embed-resources.sh` compiles `resources/*.rc` with windres and (with `--relink`) relinks both exes
after a cross-build, so the next make-package ships them branded. The installer: Program Files\Igneum Miner, Start Menu
group (Igneum Miner, Stop Igneum Miner, Igneum Miner logs, Uninstall), optional desktop icon and firewall rule, uninstall
stops the processes first, licence page (MIT placeholder, pending), finish page carries the seed line.
Not possible without accounts: Authenticode signing (SmartScreen warns until a code-signing certificate exists).

View file

@ -0,0 +1,163 @@
# Builds dist\Igneum-Miner-Setup-<version>.exe on a Windows PC (started by BUILD-INSTALLER.bat, or run directly).
# 1. Finds ISCC.exe (Inno Setup 6.3 or newer); installs Inno Setup 6 through winget when it is missing.
# 2. Finds the art (brand\icons in the repo layout, or an art\ folder next to this script).
# 3. Finds the payload: the extracted igneum-windows folder (START-IGNEUM.bat, igneumd.exe, igneum-miner.exe) next to
# this script, in the parent folder, or given with -Payload; otherwise the zip at -PayloadUrl is downloaded.
# 4. Copies the payload into build\payload without logs, packs and per-PC worker builds.
# 5. Stamps the coin icon and the version block into igneumd.exe and igneum-miner.exe with rcedit when the exes do not
# carry them yet (the Mac relink with packaging\windows\embed-resources.sh --relink makes this a no-op).
# 6. Compiles Igneum-Miner.iss and prints where the installer is.
#
# build-installer.ps1 [-Payload <folder>] [-PayloadUrl <zip url>] [-Version 0.1.0] [-NoRcedit] [-NoWinget]
param(
[string]$Payload = '',
[string]$PayloadUrl = $(if ($env:IGNEUM_PAYLOAD_URL) { $env:IGNEUM_PAYLOAD_URL } else { 'https://dl.igneum.network/igneum-windows.zip' }),
[string]$Version = '0.1.0',
[switch]$NoRcedit,
[switch]$NoWinget
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$build = Join-Path $here 'build'
$dist = Join-Path $here 'dist'
$rceditUrl = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
New-Item -ItemType Directory -Force -Path $build, $dist | Out-Null
function Say([string]$text) { Write-Host ('[build] ' + $text) }
function Find-Iscc {
$candidates = @()
foreach ($base in @(${env:ProgramFiles(x86)}, $env:ProgramFiles, (Join-Path $env:LOCALAPPDATA 'Programs'))) {
if ($base) { $candidates += (Join-Path $base 'Inno Setup 6\ISCC.exe') }
}
foreach ($key in @('HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1')) {
try {
$loc = (Get-ItemProperty -Path $key -ErrorAction Stop).InstallLocation
if ($loc) { $candidates += (Join-Path $loc 'ISCC.exe') }
} catch { }
}
foreach ($c in $candidates) { if ($c -and (Test-Path $c)) { return $c } }
return $null
}
# ---- 1. Inno Setup ----------------------------------------------------------------------------------------------------
$iscc = Find-Iscc
if (-not $iscc) {
if ($NoWinget) { throw 'Inno Setup 6 is not installed (and -NoWinget was given). Install it from https://jrsoftware.org/isdl.php and run again.' }
if (-not (Get-Command winget.exe -ErrorAction SilentlyContinue)) { throw 'Inno Setup 6 is not installed and winget is not available. Install Inno Setup 6 from https://jrsoftware.org/isdl.php and run again.' }
Say 'Inno Setup 6 is not installed; installing it through winget (a UAC prompt may appear)'
& winget.exe install --id JRSoftware.InnoSetup --exact --silent --accept-package-agreements --accept-source-agreements
$iscc = Find-Iscc
if (-not $iscc) { throw 'winget finished but ISCC.exe was not found. Install Inno Setup 6 from https://jrsoftware.org/isdl.php and run again.' }
}
$isccVersion = (Get-Item $iscc).VersionInfo.FileVersion
Say "Inno Setup $isccVersion at $iscc"
$v = [Version]($isccVersion -replace '[^0-9.].*$', '')
if ($v -lt [Version]'6.3.0') { throw "Inno Setup $isccVersion is too old; 6.3 or newer is needed (winget upgrade --id JRSoftware.InnoSetup)." }
# ---- 2. the art ------------------------------------------------------------------------------------------------------
$art = $null
foreach ($dir in @((Join-Path $here '..\..\brand\icons'), (Join-Path $here 'art'))) {
if (Test-Path (Join-Path $dir 'igneum.ico')) { $art = (Resolve-Path $dir).Path; break }
}
if (-not $art) { throw "No art folder: expected brand\icons two levels up (the repo layout) or an art\ folder next to this script with igneum.ico, inno-wizard-164x314.bmp and inno-wizard-small-55x58.bmp (made by brand\icons\make-icons.py)." }
foreach ($f in @('igneum.ico', 'inno-wizard-164x314.bmp', 'inno-wizard-small-55x58.bmp')) {
if (-not (Test-Path (Join-Path $art $f))) { throw "missing $f in $art" }
}
Say "art: $art"
# ---- 3. the payload --------------------------------------------------------------------------------------------------
function Test-PayloadDir([string]$dir) {
if (-not $dir -or -not (Test-Path $dir)) { return $false }
foreach ($f in @('START-IGNEUM.bat', 'start-igneum.ps1', 'igneum-common.ps1', 'stop-igneum.ps1', 'igneumd.exe', 'igneum-miner.exe')) {
if (-not (Test-Path (Join-Path $dir $f))) { return $false }
}
return $true
}
$source = $null
if ($Payload) {
if (-not (Test-PayloadDir $Payload)) { throw "-Payload $Payload does not hold START-IGNEUM.bat, the .ps1 files and both exes." }
$source = (Resolve-Path $Payload).Path
} else {
foreach ($dir in @((Join-Path $here 'igneum-windows'), (Join-Path $here '..\igneum-windows'), (Join-Path $here 'payload'), (Join-Path $here '..\..\igneum-windows'))) {
if (Test-PayloadDir $dir) { $source = (Resolve-Path $dir).Path; break }
}
}
if (-not $source) {
$zip = Join-Path $build 'igneum-windows.zip'
$unpacked = Join-Path $build 'payload-src'
Say "no igneum-windows folder next to this script; downloading $PayloadUrl"
Invoke-WebRequest -Uri $PayloadUrl -OutFile $zip -UseBasicParsing
if (Test-Path $unpacked) { Remove-Item -Recurse -Force $unpacked }
Expand-Archive -Path $zip -DestinationPath $unpacked -Force
$hit = Get-ChildItem -Path $unpacked -Recurse -Filter 'START-IGNEUM.bat' | Select-Object -First 1
if (-not $hit -or -not (Test-PayloadDir $hit.DirectoryName)) { throw "the zip from $PayloadUrl holds no complete payload (START-IGNEUM.bat with the .ps1 files and both exes)." }
$source = $hit.DirectoryName
}
Say "payload source: $source"
# ---- 4. assemble build\payload ---------------------------------------------------------------------------------------
$payloadDir = Join-Path $build 'payload'
if (Test-Path $payloadDir) { Remove-Item -Recurse -Force $payloadDir }
New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null
$skipNames = @('.DS_Store')
Get-ChildItem -Path $source -Recurse -Force | ForEach-Object {
$rel = $_.FullName.Substring($source.Length).TrimStart('\')
if ($rel -match '(^|\\)packs(\\|$)') { return }
if ($rel -match '\.(log|seeds)$') { return }
if ($rel -match '^proto-(cuda|opencl)\\[^\\]+\.exe$') { return } # GPU workers are built per PC on the first run
if ($skipNames -contains $_.Name) { return }
$dest = Join-Path $payloadDir $rel
if ($_.PSIsContainer) { New-Item -ItemType Directory -Force -Path $dest | Out-Null }
else {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null
Copy-Item -Path $_.FullName -Destination $dest -Force
}
}
$count = (Get-ChildItem -Path $payloadDir -Recurse -File | Measure-Object).Count
$bytes = (Get-ChildItem -Path $payloadDir -Recurse -File | Measure-Object -Property Length -Sum).Sum
Say ("payload: {0} files, {1:N1} MB" -f $count, ($bytes / 1MB))
# ---- 5. icon and version block in the exes ---------------------------------------------------------------------------
$rcedit = $null
foreach ($exeName in @('igneumd.exe', 'igneum-miner.exe')) {
$exe = Join-Path $payloadDir $exeName
$info = (Get-Item $exe).VersionInfo
if ($info.ProductName -eq 'Igneum Miner') { Say "$exeName already carries the Igneum Miner version block (relinked on the Mac)"; continue }
if ($NoRcedit) { Say "$exeName has no version block; -NoRcedit given, so Explorer shows it without the coin icon"; continue }
if (-not $rcedit) {
if ($env:RCEDIT -and (Test-Path $env:RCEDIT)) { $rcedit = $env:RCEDIT }
else {
$rcedit = Join-Path $build 'rcedit-x64.exe'
if (-not (Test-Path $rcedit)) { Say "downloading rcedit from $rceditUrl"; Invoke-WebRequest -Uri $rceditUrl -OutFile $rcedit -UseBasicParsing }
}
}
$desc = if ($exeName -eq 'igneumd.exe') { 'Igneum Miner node' } else { 'Igneum Miner' }
$rcArgs = @($exe, '--set-icon', (Join-Path $art 'igneum.ico'),
'--set-version-string', 'CompanyName', 'Igneum',
'--set-version-string', 'ProductName', 'Igneum Miner',
'--set-version-string', 'FileDescription', $desc,
'--set-version-string', 'OriginalFilename', $exeName,
'--set-version-string', 'LegalCopyright', 'Igneum. Nothing is bought or sold.',
'--set-file-version', $Version,
'--set-product-version', $Version)
& $rcedit @rcArgs
if ($LASTEXITCODE -ne 0) { throw "rcedit failed on $exeName (exit $LASTEXITCODE)" }
Say "$exeName stamped: coin icon, Igneum Miner $Version"
}
# ---- 6. compile ------------------------------------------------------------------------------------------------------
$iss = Join-Path $here 'Igneum-Miner.iss'
$isccArgs = @('/Qp', "/DPayload=$payloadDir", "/DArtDir=$art", "/DAppVersion=$Version", "/O$dist", $iss)
Say "compiling $iss"
& $iscc @isccArgs
if ($LASTEXITCODE -ne 0) { throw "ISCC failed (exit $LASTEXITCODE)" }
$out = Join-Path $dist "Igneum-Miner-Setup-$Version.exe"
if (-not (Test-Path $out)) { throw "ISCC finished but $out is missing" }
$size = (Get-Item $out).Length
Say ("done: {0} ({1:N1} MB)" -f $out, ($size / 1MB))
Say 'The installer is unsigned until Igneum has a code-signing certificate: SmartScreen shows "Windows protected your PC" (More info > Run anyway).'

View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Gives igneumd.exe and igneum-miner.exe the coin icon and a version block (CompanyName Igneum, ProductName
# Igneum Miner, 0.1.0). Two steps, both on the Mac:
# 1. windres compiles resources/igneumd.rc and resources/igneum-miner.rc into COFF objects (cheap, always done).
# 2. --relink runs `cargo rustc` for each binary with `-C link-arg=<object>`: only the final crate is recompiled and
# relinked, every dependency comes from the cache, so it costs a few minutes after a normal cross-build. The
# objects are linked in as extra inputs, nothing in vendor/ changes. Without --relink the exact commands are
# printed, and build-installer.ps1 stamps the resources with rcedit on the PC instead.
# The resulting exes land where proto-cuda/windows-app/make-package.sh already picks them up
# (vendor/igneum-node-win/target/x86_64-pc-windows-gnu/release/).
#
# packaging/windows/embed-resources.sh compile the objects, print the relink commands
# packaging/windows/embed-resources.sh --relink also relink both exes (needs a completed cross-build in the cache)
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
ICONS="$ROOT/brand/icons"
OUT="$HERE/build/res"
NODE="${NODE_WORKTREE:-$ROOT/vendor/igneum-node-win}"
MINGW="/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32"
command -v x86_64-w64-mingw32-windres >/dev/null || { echo "x86_64-w64-mingw32-windres not found (brew install mingw-w64)" >&2; exit 1; }
[ -f "$ICONS/igneum.ico" ] || { echo "no $ICONS/igneum.ico; run python3 brand/icons/make-icons.py first" >&2; exit 1; }
mkdir -p "$OUT"
for name in igneumd igneum-miner; do
x86_64-w64-mingw32-windres -I "$ICONS" -i "$HERE/resources/$name.rc" -O coff -o "$OUT/$name.res.o"
echo "compiled $OUT/$name.res.o ($(stat -f %z "$OUT/$name.res.o") bytes)"
done
# The same environment as proto-cuda/windows-node/cross-build.sh, so the cached dependencies are reused.
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:$PATH"
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-target}"
export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc
export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++
export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar
export LIBCLANG_PATH=/opt/homebrew/opt/llvm/lib
export BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw32 --sysroot=$MINGW -I$MINGW/include"
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc"
relink_cmd() { # $1 package, $2 bin, $3 object
printf 'nice -n 19 cargo rustc --release -j 2 -p %s --bin %s --features igneum-pow --target x86_64-pc-windows-gnu -- -C link-arg=%q\n' "$1" "$2" "$3"
}
if [ "${1:-}" = "--relink" ]; then
cd "$NODE"
for spec in "kaspad igneumd" "igneum-miner igneum-miner"; do
set -- $spec
echo "relinking $2.exe"
eval "$(relink_cmd "$1" "$2" "$OUT/$2.res.o")"
exe="$CARGO_TARGET_DIR/x86_64-pc-windows-gnu/release/$2.exe"
x86_64-w64-mingw32-objdump -h "$exe" | grep -q '\.rsrc' && echo "$exe: .rsrc section present ($(stat -f %z "$exe") bytes)" || { echo "$exe has no .rsrc section" >&2; exit 1; }
done
else
cat <<TXT
Not relinked (no --relink). After the next cross-build (proto-cuda/windows-node/cross-build.sh), from $NODE
with the same environment variables as that script, run:
$(relink_cmd kaspad igneumd "$OUT/igneumd.res.o")
$(relink_cmd igneum-miner igneum-miner "$OUT/igneum-miner.res.o")
Then proto-cuda/windows-app/make-package.sh ships the branded exes. Check with:
x86_64-w64-mingw32-objdump -h vendor/igneum-node-win/target/x86_64-pc-windows-gnu/release/igneum-miner.exe | grep rsrc
TXT
fi

View file

@ -0,0 +1,34 @@
// Windows resources for igneum-miner.exe: the coin icon and the version block Explorer shows under Properties > Details.
// Compiled by embed-resources.sh with x86_64-w64-mingw32-windres (icon path relative to brand/icons, passed with -I).
#include <winver.h>
1 ICON "igneum.ico"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "Igneum"
VALUE "FileDescription", "Igneum Miner"
VALUE "FileVersion", "0.1.0"
VALUE "InternalName", "igneum-miner"
VALUE "LegalCopyright", "Igneum. Nothing is bought or sold."
VALUE "OriginalFilename", "igneum-miner.exe"
VALUE "ProductName", "Igneum Miner"
VALUE "ProductVersion", "0.1.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View file

@ -0,0 +1,34 @@
// Windows resources for igneumd.exe: the coin icon and the version block Explorer shows under Properties > Details.
// Compiled by embed-resources.sh with x86_64-w64-mingw32-windres (icon path relative to brand/icons, passed with -I).
#include <winver.h>
1 ICON "igneum.ico"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "Igneum"
VALUE "FileDescription", "Igneum Miner node"
VALUE "FileVersion", "0.1.0"
VALUE "InternalName", "igneumd"
VALUE "LegalCopyright", "Igneum. Nothing is bought or sold."
VALUE "OriginalFilename", "igneumd.exe"
VALUE "ProductName", "Igneum Miner"
VALUE "ProductVersion", "0.1.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View file

@ -0,0 +1,9 @@
@echo off
rem Igneum Miner: what the Start Menu and Desktop shortcuts run. Names the window, prints the first lines, then hands
rem over to START-IGNEUM.bat in the same folder (settings are at the top of that file).
title Igneum Miner
cd /d "%~dp0"
echo Igneum Miner 0.1.0
echo Nobody from Igneum will ever ask for your seed.
echo.
call "%~dp0START-IGNEUM.bat"

View file

@ -0,0 +1,5 @@
@echo off
rem Stop Igneum Miner: the Start Menu entry for a copy whose window was closed without Ctrl+C.
title Stop Igneum Miner
cd /d "%~dp0"
call "%~dp0STOP-IGNEUM.bat"