Igneum Miner 0.3.0: GPU selection per card (switch, kind, VRAM, identities), Windows WebView2 host + BUILD-APP.bat, Mac DMG and Windows payload around the app, installer scripts for the engine

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-04 10:11:47 +00:00
parent cbe003e38b
commit a7d7f5da2a
30 changed files with 1469 additions and 527 deletions

View file

@ -4,9 +4,17 @@
//! the log intake). Neither is in the UI source.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// What the user chose for one GPU (keyed by vendor, device index and name).
#[derive(Clone, Serialize, Deserialize, Default)]
pub struct CardPref {
pub enabled: bool,
pub identities: u32,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default)]
pub setup_done: bool,
@ -19,8 +27,11 @@ pub struct Settings {
/// The one-time "save your key" sheet was confirmed.
#[serde(default)]
pub key_saved: bool,
/// Legacy single value (0.3.0 first cut); per-card counts live in `cards`.
#[serde(default = "one")]
pub identities: u32,
#[serde(default)]
pub cards: HashMap<String, CardPref>,
#[serde(default = "yes")]
pub vote: bool,
#[serde(default)]
@ -37,6 +48,12 @@ fn yes() -> bool {
true
}
impl Default for Settings {
fn default() -> Settings {
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), vote: true, paused: false, accepted_total: 0 }
}
}
impl Settings {
pub fn load(path: &Path) -> Settings {
std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default()

View file

@ -56,6 +56,7 @@ pub fn run_timeout(cmd: &mut Command, stdin_text: Option<&str>, limit: Duration)
fn card(index: usize, name: &str, vendor: &str, worker: &str, detail: &str, device: &str) -> CardState {
CardState {
index,
key: format!("{vendor}:{device}:{name}"),
name: name.to_string(),
vendor: vendor.into(),
worker: worker.into(),
@ -67,6 +68,34 @@ fn card(index: usize, name: &str, vendor: &str, worker: &str, detail: &str, devi
}
}
/// Integrated GPUs by name: AMD APUs ("Radeon Graphics", "Vega 8"), Intel iGPUs (Iris, UHD, HD Graphics, Arc A3xx is discrete).
#[allow(dead_code)]
pub fn looks_integrated(name: &str) -> bool {
let n = name.to_ascii_lowercase();
let integrated = ["radeon(tm) graphics", "radeon graphics", "vega 8", "vega 7", "vega 6", "vega 3", "vega 11", "iris", "uhd graphics", "hd graphics", "intel(r) graphics", "intel graphics", "apu", "780m", "760m", "680m", "610m", "890m", "880m"];
integrated.iter().any(|k| n.contains(k)) && !n.contains("arc ")
}
/// The defaults the launchers use: discrete cards on (8 identities on a big card, 2 on a small one), integrated off
/// (1 identity), Apple silicon on with 1.
pub fn apply_defaults(c: &mut CardState) {
match c.kind.as_str() {
"apple" => {
c.enabled = true;
c.identities = 1;
}
"integrated" => {
c.enabled = false;
c.identities = 1;
c.reason = "integrated: about 3 MH/s and it shares your system memory. Switch it on if you want it.".into();
}
_ => {
c.enabled = true;
c.identities = if c.vram_mb >= 8192 { 8 } else { 2 };
}
}
}
#[cfg(target_os = "macos")]
pub fn detect(bins: &Bins, notes: &mut Vec<String>) -> Vec<CardState> {
let mut cards = Vec::new();
@ -104,7 +133,14 @@ pub fn detect(bins: &Bins, notes: &mut Vec<String>) -> Vec<CardState> {
if !prepare {
notes.push("this Metal worker has no prepare support; the miner restarts at the hour boundary".into());
}
cards.push(card(0, &name, "apple", "Metal", &detail, ""));
let mut c = card(0, &name, "apple", "Metal", &detail, "");
c.kind = "apple".into();
c.path = "prebuilt".into();
if let Some(mem) = run_timeout(Command::new("sysctl").args(["-n", "hw.memsize"]), None, Duration::from_secs(3)) {
c.vram_mb = mem.trim().parse::<u64>().map(|b| b / (1024 * 1024)).unwrap_or(0);
}
apply_defaults(&mut c);
cards.push(c);
cards
}
@ -118,13 +154,17 @@ pub fn detect(bins: &Bins, notes: &mut Vec<String>) -> Vec<CardState> {
for line in out.lines() {
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() >= 2 && parts[0].chars().all(|c| c.is_ascii_digit()) && !parts[0].is_empty() {
let detail = parts.get(2).map(|m| m.replace("MiB", "MB")).unwrap_or_default();
let detail = detail.trim().parse::<f64>().map(|_| detail.clone()).unwrap_or(detail);
let mem_mb: u64 = parts.get(2).and_then(|m| m.split_whitespace().next()).and_then(|n| n.parse::<f64>().ok()).map(|v| v as u64).unwrap_or(0);
let detail = if mem_mb > 0 { format!("{} GB", (mem_mb + 512) / 1024) } else { String::new() };
let worker_ok = bins.cuda.is_some();
let mut c = card(cards.len(), parts[1], "nvidia", "CUDA", &detail, parts[0]);
c.kind = if looks_integrated(parts[1]) { "integrated".into() } else { "discrete".into() };
c.vram_mb = mem_mb;
c.path = if worker_ok { "prebuilt".into() } else { "build".into() };
if !worker_ok {
c.message = "CUDA worker not installed; built from source on first run (needs CUDA Toolkit and Visual Studio)".into();
c.message = "no prebuilt CUDA worker in the package; built from source on first run (needs the CUDA Toolkit and Visual Studio)".into();
}
apply_defaults(&mut c);
cards.push(c);
}
}
@ -157,6 +197,9 @@ pub fn detect(bins: &Bins, notes: &mut Vec<String>) -> Vec<CardState> {
let units = info.split(", ").find(|p| p.contains("compute units")).unwrap_or("").to_string();
let mut c = card(cards.len(), name, vendor, "OpenCL", &units, idx);
c.device = idx.to_string();
c.kind = if looks_integrated(name) { "integrated".into() } else { "discrete".into() };
c.path = "prebuilt".into();
apply_defaults(&mut c);
cards.push(c);
}
}
@ -169,8 +212,9 @@ pub fn detect(bins: &Bins, notes: &mut Vec<String>) -> Vec<CardState> {
for n in out.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
let vendor = if n.contains("NVIDIA") { "nvidia" } else if n.contains("AMD") || n.contains("Radeon") { "amd" } else { "other" };
let mut c = card(cards.len(), n, vendor, if vendor == "nvidia" { "CUDA" } else { "OpenCL" }, "", "0");
c.kind = if looks_integrated(n) { "integrated".into() } else { "unknown".into() };
c.enabled = false;
c.message = "seen by Windows, but no worker can drive it yet".into();
c.reason = "seen by Windows, but no worker can drive it (no NVIDIA driver and no OpenCL worker)".into();
cards.push(c);
}
}
@ -200,7 +244,10 @@ pub fn find_bins() -> Result<Bins, String> {
let p = dir.join(format!("{n}{ext}"));
if p.exists() { Some(p) } else { None }
};
return Ok(Bins { node, miner, metal: opt("igneum-bench"), cuda: opt("igneum-worker-cuda"), opencl: opt("igneum-worker-opencl"), dir: dir.clone() });
// the prebuilt CUDA worker needs NVIDIA's nvrtc64_*_0.dll next to it (as igneum-common.ps1 checks)
let nvrtc = std::fs::read_dir(dir).ok().map(|rd| rd.flatten().any(|e| { let n = e.file_name().to_string_lossy().to_ascii_lowercase(); n.starts_with("nvrtc64_") && n.ends_with("_0.dll") })).unwrap_or(false);
let cuda = opt("igneum-worker-cuda").filter(|_| nvrtc || cfg!(not(windows)));
return Ok(Bins { node, miner, metal: opt("igneum-bench"), cuda, opencl: opt("igneum-worker-opencl"), dir: dir.clone() });
}
}
Err(format!("igneumd and igneum-miner were not found next to the app (looked in {})", candidates.iter().map(|c| c.display().to_string()).collect::<Vec<_>>().join(", ")))

View file

@ -18,8 +18,16 @@ use std::time::{Duration, Instant};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
const POW_EPOCH_BLOCKS: u64 = 3_600;
#[derive(Clone)]
pub struct CardChoice {
pub key: String,
pub enabled: bool,
pub identities: u32,
}
pub enum Cmd {
Detect,
ApplyCards(Vec<CardChoice>),
Detected(Vec<CardState>, Vec<String>),
Start,
Pause,
@ -276,6 +284,7 @@ struct MinerSlot {
restarts: u32,
building: bool,
needs_rebuild: bool,
prepared: bool, // the pack is exported (and the worker built) for the next start
last_status: Option<Instant>,
error_at: Option<Instant>,
}
@ -436,19 +445,24 @@ impl Engine {
for n in &notes {
self.shared.log(&format!("detection: {n}"));
}
let prefs = self.shared.settings.lock().unwrap().cards.clone();
let mut st = self.st();
st.detecting = false;
st.detect_message = notes.join(". ");
let identities = st.settings.identities;
st.mining.cards = cards;
for c in st.mining.cards.iter_mut() {
c.identities = identities;
if let Some(p) = prefs.get(&c.key) {
c.enabled = p.enabled && c.kind != "unknown";
c.identities = p.identities.clamp(1, 64);
c.reason = String::new();
}
}
drop(st);
if self.running {
self.plan_miners();
}
}
Cmd::ApplyCards(choices) => self.apply_cards(choices),
Cmd::Start => self.start(),
Cmd::Pause => {
if !self.running {
@ -471,10 +485,6 @@ impl Engine {
}
Cmd::RestartMiners(why) => {
self.shared.event("info", &format!("settings changed ({why}); the miners restart"));
let ids = self.st().settings.identities;
for c in self.st().mining.cards.iter_mut() {
c.identities = ids;
}
self.stop_miners("settings changed");
for m in self.miners.iter_mut() {
m.restart_at = Some(Instant::now());
@ -504,10 +514,13 @@ impl Engine {
m.building = false;
match result {
Ok(p) => {
if p != m.worker {
self.shared.event("build", "GPU worker built from source");
}
m.worker = p;
m.needs_rebuild = false;
m.prepared = true;
m.restart_at = Some(Instant::now());
self.shared.event("build", "GPU worker built from source");
}
Err(e) => {
self.shared.event("error", &format!("worker build failed: {e}"));
@ -672,6 +685,7 @@ impl Engine {
restarts: 0,
building: false,
needs_rebuild,
prepared: false,
last_status: None,
error_at: None,
});
@ -701,9 +715,9 @@ impl Engine {
"--evm-address".into(),
s.address.clone(),
];
if s.identities > 1 {
if card.identities > 1 {
a.push("--identities".into());
a.push(s.identities.to_string());
a.push(card.identities.to_string());
}
if !s.vote {
a.push("--no-vote".into());
@ -715,16 +729,29 @@ impl Engine {
a.push(r.network.clone());
}
if card.worker != "Metal" {
// ahead-of-time workers build the next program from the pack the miner writes here
// The miner runs with the app data folder as its cwd: the pack paths stay relative (the miner splits
// --worker-args on spaces, and %LOCALAPPDATA% may carry a space in the user name). The prebuilt workers
// take the exported pack with --pack and build the next program from --prepare-packs; a worker built
// from source has the program compiled in and exits 42 at the boundary instead.
a.push("--prepare-packs".into());
a.push(r.app_dir.join("packs").join("prepare").display().to_string());
a.push("packs\\prepare".into());
if card.worker == "OpenCL" {
a.push("--job-nonces".into());
a.push("2097152".into());
}
let mut wargs = String::new();
if !card.device.is_empty() {
wargs.push_str(&format!("--device {}", card.device));
}
if slot.worker.file_name().map(|f| f.to_string_lossy().starts_with("igneum-worker-")).unwrap_or(false) {
if !wargs.is_empty() {
wargs.push(' ');
}
wargs.push_str("--pack packs\\devnet");
}
if !wargs.is_empty() {
a.push("--worker-args".into());
a.push(format!("--device {}", card.device));
a.push(wargs);
}
}
a
@ -742,7 +769,8 @@ impl Engine {
self.miners[i].starts += 1;
let seg = if self.miners[i].starts > 1 { format!("-r{}", self.miners[i].starts) } else { String::new() };
let log = self.shared.runtime.log_dir.join(format!("miner-{}-{}{seg}.log", self.miners[i].label, self.stamp));
let cwd = self.miners[i].worker.parent().map(|p| p.to_path_buf());
let cwd = if card.worker == "Metal" { None } else { Some(self.shared.runtime.app_dir.clone()) };
self.miners[i].prepared = false;
match procs::spawn(Source::Miner(card_idx), &self.bins.miner, &args, cwd.as_deref(), &log, &self.lines_tx) {
Ok(p) => {
self.shared.log(&format!("miner {} started (pid {}): {}", self.miners[i].label, p.pid(), p.cmdline));
@ -794,24 +822,75 @@ impl Engine {
st.mining.state = if st.mining.paused { "paused".into() } else if self.running { "waiting".into() } else { "stopped".into() };
}
/// Windows without a prebuilt worker: export the pack from the node and build the CUDA worker from source
/// (today's launcher path). The worker then has no prepare support and is rebuilt at every hour boundary.
fn build_worker(&mut self, i: usize) {
/// Ahead-of-time workers (Windows): export this hour's program pack from the node for the prebuilt worker, or,
/// with no prebuilt worker, build one from source (today's launcher path; it then has no prepare support and is
/// rebuilt at every hour boundary). Runs off the supervisor thread; WorkerBuilt starts the miner.
fn prepare_worker(&mut self, i: usize) {
let card_idx = self.miners[i].card;
self.miners[i].building = true;
let build = self.miners[i].needs_rebuild;
if let Some(c) = self.st().mining.cards.get_mut(card_idx) {
c.state = "starting".into();
c.message = "building the GPU worker from source (about a minute)".into();
c.message = if build { "building the GPU worker from source (about a minute)".into() } else { "exporting this hour's program".into() };
}
let shared = self.shared.clone();
let bins = self.bins.clone();
let vendor = self.st().mining.cards.get(card_idx).map(|c| c.vendor.clone()).unwrap_or_default();
let worker = self.miners[i].worker.clone();
std::thread::spawn(move || {
let r = build_worker_from_source(&shared, &bins, &vendor);
let r = if build { build_worker_from_source(&shared, &bins, &vendor) } else { export_pack(&shared, &bins).map(|_| worker) };
shared.send(Cmd::WorkerBuilt(card_idx, r));
});
}
/// The user changed which cards mine or how many identities each runs: only the affected workers restart.
fn apply_cards(&mut self, choices: Vec<CardChoice>) {
let mut changed = Vec::new();
{
let mut settings = self.shared.settings.lock().unwrap();
let mut st = self.st();
for ch in &choices {
let Some(c) = st.mining.cards.iter_mut().find(|c| c.key == ch.key) else { continue };
let identities = ch.identities.clamp(1, 64);
let enabled = ch.enabled && c.kind != "unknown";
settings.cards.insert(ch.key.clone(), crate::config::CardPref { enabled, identities });
if c.enabled != enabled || c.identities != identities {
c.enabled = enabled;
c.identities = identities;
c.reason = String::new();
if !enabled {
c.state = "off".into();
c.hash_now = 0.0;
c.message = String::new();
}
changed.push((c.index, c.name.clone(), enabled, identities));
}
}
settings.save(&self.shared.settings_path);
}
for (idx, name, enabled, identities) in changed {
if let Some(pos) = self.miners.iter().position(|m| m.card == idx) {
if let Some(mut p) = self.miners[pos].proc.take() {
p.write_stdin("quit\n");
p.stop(8);
}
if enabled {
self.miners[pos].restart_at = Some(Instant::now());
self.miners[pos].prepared = false;
self.shared.event("info", &format!("{name}: {identities} identit{}; its worker restarts", if identities == 1 { "y" } else { "ies" }));
} else {
self.miners.remove(pos);
self.shared.event("info", &format!("{name}: switched off"));
}
} else if enabled {
self.shared.event("ok", &format!("{name}: switched on, {identities} identit{}", if identities == 1 { "y" } else { "ies" }));
}
}
if self.running {
self.plan_miners();
}
}
// ---- the tick ----------------------------------------------------------------------------------------------
fn tick(&mut self) {
@ -985,8 +1064,9 @@ impl Engine {
}
if let Some(at) = self.miners[i].restart_at {
if now >= at {
if self.miners[i].needs_rebuild {
self.build_worker(i);
let aot = self.st().mining.cards.get(card_idx).map(|c| c.worker != "Metal").unwrap_or(false);
if (aot || self.miners[i].needs_rebuild) && !self.miners[i].prepared {
self.prepare_worker(i);
} else {
self.start_miner(i);
}
@ -1067,7 +1147,6 @@ impl Engine {
}
Source::Miner(card) => self.miner_line(card, l.stderr, &l.text),
Source::Node => {}
Source::Engine => {}
}
}
@ -1373,6 +1452,16 @@ fn civil_from_days(z: i64) -> (i64, u32, u32) {
(if m <= 2 { y + 1 } else { y }, m, d)
}
/// Exports this hour's program pack from the node to <app data>\packs\devnet (the prebuilt workers read it with --pack).
#[allow(unused_variables)]
fn export_pack(shared: &Arc<Shared>, bins: &Bins) -> Result<(), String> {
let pack = shared.runtime.app_dir.join("packs").join("devnet");
let _ = std::fs::create_dir_all(&pack);
let out = crate::detect::run_timeout(std::process::Command::new(&bins.miner).args(["export-pack", &shared.runtime.rpc_url(), &pack.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
shared.log(&format!("export-pack: {}", out.lines().last().unwrap_or("no output")));
if pack.join("seeds.txt").exists() { Ok(()) } else { Err("export-pack wrote no seeds.txt (is the node reachable?)".into()) }
}
/// Today's Windows path when no prebuilt worker ships: export the program pack, run proto-cuda\build.bat (or
/// proto-opencl\build.bat) in the MSVC environment, and use the exe it writes. Untested from the Mac.
#[allow(unused_variables)]

View file

@ -3,7 +3,9 @@
//! --wrapper, reads `URL ...` and `STATE {...}` lines from its stdout and writes `quit` on its stdin. Without a host
//! (--open) the dashboard opens in the default browser.
//!
//! igneum-app [--wrapper | --open | --no-open] [--print-url]
//! igneum-app [--wrapper | --open | --no-open | --launch] [--print-url]
//! --launch is what the Start Menu entry runs on Windows: it hands over to "Igneum Miner.exe" (the window host) when
//! that is installed next to the engine, else runs the engine with the dashboard in the default browser.
//!
//! Environment (tests): IGNEUM_APP_NETWORK devnet|simnet, IGNEUM_APP_RPC_PORT, IGNEUM_APP_P2P_PORT, IGNEUM_APP_PEERS
//! (comma list, empty for none), IGNEUM_APP_UNSYNCED=1, IGNEUM_APP_DATA, IGNEUM_APP_LOGS, IGNEUM_APP_BIN, IGNEUM_APP_NODE_DIR.
@ -31,6 +33,19 @@ fn main() {
println!("igneum-app {}", engine::VERSION);
return;
}
if args.iter().any(|a| a == "--launch") {
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
let host = dir.join("Igneum Miner.exe");
if host.exists() {
let mut c = std::process::Command::new(&host);
c.current_dir(&dir);
if c.spawn().is_ok() {
return;
}
}
}
// no window host: the engine runs on its own and the dashboard opens in the default browser
}
platform::clear_quarantine();
let runtime = config::Runtime::from_env();
let _ = std::fs::create_dir_all(&runtime.app_dir);

View file

@ -12,7 +12,6 @@ pub enum Source {
Node,
Watch,
Miner(usize), // card index
Engine,
}
impl Source {
@ -21,7 +20,6 @@ impl Source {
Source::Node => "node".into(),
Source::Watch => "watch".into(),
Source::Miner(i) => format!("miner{}", i + 1),
Source::Engine => "app".into(),
}
}
}

View file

@ -194,6 +194,21 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
shared.send(Cmd::Resume);
Ok(json!({ "ok": true }))
}
"/api/cards" => {
let list = body.get("cards").and_then(|v| v.as_array()).ok_or("cards missing")?;
let choices: Vec<crate::engine::CardChoice> = list
.iter()
.filter_map(|c| {
Some(crate::engine::CardChoice {
key: c.get("key")?.as_str()?.to_string(),
enabled: c.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
identities: c.get("identities").and_then(|v| v.as_u64()).unwrap_or(1).clamp(1, 64) as u32,
})
})
.collect();
shared.send(Cmd::ApplyCards(choices));
Ok(json!({ "ok": true }))
}
"/api/settings" => {
let identities = body.get("identities").and_then(|v| v.as_u64()).map(|v| v.clamp(1, 64) as u32);
let vote = body.get("vote").and_then(|v| v.as_bool());

View file

@ -26,6 +26,11 @@ pub struct NodeState {
#[derive(Clone, Serialize, Default)]
pub struct CardState {
pub index: usize,
pub key: String, // stable id for the saved preference: vendor:device:name
pub kind: String, // apple | discrete | integrated | external | unknown
pub vram_mb: u64, // 0 when unknown
pub reason: String, // why it is off by default, if it is
pub path: String, // prebuilt | build (how the worker comes to be)
pub name: String,
pub vendor: String, // apple | nvidia | amd | other
pub worker: String, // Metal | CUDA | OpenCL

View file

@ -118,6 +118,52 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
@keyframes draw{to{stroke-dashoffset:0}}
.gpu .msg{font-size:12px;color:var(--ember);margin-top:4px}
/* GPU rows (first run and settings) */
.gpu-row{display:flex;align-items:center;gap:16px;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0}
.gpu-row.off{opacity:.72}
.gpu-row .badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
.gpu-row .badge.apple{color:var(--bone)}
.gpu-row .badge.nvidia{color:#8BE37A}
.gpu-row .badge.amd{color:var(--ember)}
.gpu-row .info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.gpu-row .name{font-family:var(--head);font-weight:700;font-size:16px;line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500}
.kind.discrete,.kind.apple{color:var(--molten);border-color:rgba(255,179,92,.4)}
.kind.external{color:var(--bone)}
.gpu-row .meta{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px}
.gpu-row .meta b{color:var(--ink-2);font-weight:500}
.gpu-row .reason{font-size:12px;color:var(--ash)}
.gpu-row .msg{font-size:12px;color:var(--ember)}
.ids{display:flex;align-items:center;gap:6px;flex:0 0 auto}
.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:4px}
.ids button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);background:transparent;color:var(--bone);cursor:pointer;font-size:16px;line-height:1}
.ids button:hover{border-color:var(--ash)}
.ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:13px;user-select:text;-webkit-user-select:text}
.ids input:focus{outline:none;border-color:var(--ember)}
.ids input::-webkit-inner-spin-button,.ids input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.gpu-row.off .ids{opacity:.4;pointer-events:none}
.gpu-row .switch{padding:0;flex:0 0 auto}
.cards.compact .gpu-row{padding:12px 14px;gap:12px;border-radius:14px}
.cards.compact .gpu-row .badge{width:38px;height:38px;flex-basis:38px;border-radius:10px}
.cards.compact .gpu-row .name{font-size:14px}
.cards.compact .ids .k{display:none}
/* per-card tiles on the dashboard */
.gpu-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--gap-tile)}
.gpu-tile{background:var(--obsidian);border:1px solid var(--line);border-radius:var(--tile-r);padding:14px 16px;display:flex;flex-direction:column;gap:6px;min-width:0}
.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:14px;line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.gpu-tile .h{font-family:var(--head);font-weight:700;font-size:24px;color:var(--ember);line-height:1.1;display:flex;align-items:baseline;gap:6px;font-variant-numeric:tabular-nums}
.gpu-tile .h .unit{font-family:var(--mono);font-size:11px;color:var(--ash);font-weight:500;letter-spacing:.08em}
.gpu-tile.idle .h{color:var(--ash)}
.gpu-tile .m{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px}
.gpu-tile .m b{color:var(--ink-2);font-weight:500}
.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)}
.gpu-tile .st.mining{color:var(--molten)}
.gpu-tile .st.bad{color:var(--ember)}
.gpu-tile .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block}
.gpu-tile .msg{font-size:12px;color:var(--ash)}
.gpu-off{margin-top:12px;font-size:12px;color:var(--ash)}
/* address options */
.options{display:flex;flex-direction:column;gap:12px;margin-top:6px}
.option{display:flex;gap:16px;align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease}

View file

@ -48,7 +48,10 @@
api('api/phase', { phase: p });
}
$('btn-begin').addEventListener('click', function () { show('cards'); });
$('btn-cards-next').addEventListener('click', function () { show('address'); });
$('btn-cards-next').addEventListener('click', function () {
var choices = readCardRows($('cards-list'));
if (choices.length) api('api/cards', { cards: choices }).then(function () { show('address'); }); else show('address');
});
$('btn-cards-retry').addEventListener('click', function () { cardsSeen = false; $('cards-detecting').hidden = false; api('api/detect', {}); });
$('btn-address-back').addEventListener('click', function () { show('cards'); });
@ -93,7 +96,7 @@
function fillSettings() {
if (!state) return;
$('s-address').textContent = state.address.display || 'not set';
$('s-identities').value = state.settings.identities;
renderCardRows($('s-cards'), state.mining.cards);
$('s-vote').checked = state.settings.vote;
$('s-login').checked = state.settings.start_at_login;
$('s-version').textContent = 'Igneum Miner ' + state.version;
@ -112,9 +115,9 @@
$('s-reveal').addEventListener('click', function () {
api('api/key/reveal', {}).then(function (r) { if (r.ok) { $('s-key').textContent = r.private_key; $('s-key-box').hidden = false; } else toast(r.error || 'no key'); });
});
$('s-identities-save').addEventListener('click', function () {
var n = parseInt($('s-identities').value, 10); if (!(n >= 1 && n <= 64)) { toast('1 to 64'); return; }
api('api/settings', { identities: n }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); });
$('s-cards-save').addEventListener('click', function () {
var choices = readCardRows($('s-cards'));
api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast('Applied; only the changed workers restart'); });
});
$('s-vote').addEventListener('change', function () { api('api/settings', { vote: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); });
$('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); });
@ -196,29 +199,63 @@
requestAnimationFrame(frame);
// ---------- render ----------
// ---------- GPU rows: one per card, with a switch and an identities stepper ----------
function badgeHtml(cd) {
if (cd.vendor === 'apple') return '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg>';
return cd.vendor === 'nvidia' ? 'NV' : cd.vendor === 'amd' ? 'AMD' : 'GPU';
}
function kindWord(k) { return k === 'apple' ? 'Apple silicon' : k === 'discrete' ? 'Discrete' : k === 'integrated' ? 'Integrated' : k === 'external' ? 'External' : 'Unknown'; }
function vramText(cd) { if (!cd.vram_mb) return ''; var gb = cd.vram_mb / 1024; return (gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : ''); }
function renderCardRows(container, cards) {
container.querySelectorAll('.gpu-row').forEach(function (n) { n.remove(); });
cards.forEach(function (cd) {
var d = document.createElement('div');
d.className = 'gpu-row' + (cd.enabled ? '' : ' off');
d.dataset.key = cd.key;
var meta = '<span>worker <b>' + esc(cd.worker) + '</b></span>' + (vramText(cd) ? '<span>' + esc(vramText(cd)) + '</span>' : '') + (cd.detail && cd.kind !== 'apple' ? '<span>' + esc(cd.detail) + '</span>' : '') + (cd.detail && cd.kind === 'apple' ? '<span>' + esc(cd.detail.split(',')[0]) + '</span>' : '') + (cd.device ? '<span>device <b>' + esc(cd.device) + '</b></span>' : '');
d.innerHTML = '<div class="badge ' + esc(cd.vendor) + '">' + badgeHtml(cd) + '</div>' +
'<div class="info"><div class="name">' + esc(cd.name) + ' <span class="kind ' + esc(cd.kind) + '">' + kindWord(cd.kind) + '</span></div><div class="meta">' + meta + '</div>' +
(cd.reason ? '<div class="reason">' + esc(cd.reason) + '</div>' : '') + (cd.message ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div>' +
'<div class="ids"><span class="k">identities</span><button type="button" data-d="-1" aria-label="fewer">&minus;</button><input type="number" min="1" max="64" value="' + cd.identities + '"><button type="button" data-d="1" aria-label="more">+</button></div>' +
'<label class="switch" title="' + (cd.enabled ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox"' + (cd.enabled ? ' checked' : '') + (cd.kind === 'unknown' ? ' disabled' : '') + '><span class="track"></span></label>';
container.appendChild(d);
});
}
function readCardRows(container) {
var out = [];
container.querySelectorAll('.gpu-row').forEach(function (r) {
var n = parseInt(r.querySelector('.ids input').value, 10); if (!(n >= 1)) n = 1; if (n > 64) n = 64;
out.push({ key: r.dataset.key, enabled: r.querySelector('.switch input').checked, identities: n });
});
return out;
}
document.addEventListener('click', function (e) {
var b = e.target.closest('.ids button'); if (!b) return;
var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1;
v = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); inp.value = v;
});
document.addEventListener('change', function (e) {
if (e.target.matches('.gpu-row .switch input')) { var r = e.target.closest('.gpu-row'); r.classList.toggle('off', !e.target.checked); }
});
var cardsRendered = '';
function renderCards(s) {
var list = $('cards-list'), det = $('cards-detecting');
if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics card to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics card to report in.'; return; }
if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics cards to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics cards to report in.'; cardsRendered = ''; return; }
det.hidden = true;
var cards = s.mining.cards;
list.querySelectorAll('.gpu').forEach(function (n) { n.remove(); });
var sig = cards.map(function (c) { return c.key; }).join('|');
if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); }
if (!cards.length) {
$('cards-sub').textContent = 'No GPU this app can drive was found.';
$('cards-note').textContent = (s.detect_message || '') + ' The node can still run on its own; mining needs a supported card.';
$('cards-note').hidden = false; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining';
$('cards-note').hidden = false; $('cards-help').hidden = true; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining';
return;
}
$('cards-sub').textContent = cards.length === 1 ? 'One card found. The worker owns it while you mine.' : cards.length + ' cards found. One worker each.';
var on = cards.filter(function (c) { return c.enabled; }).length;
$('cards-sub').textContent = cards.length === 1 ? 'One card found. Switch it on to mine with it.' : cards.length + ' GPUs found. Switch on the ones you want to mine with; each gets its own worker.';
$('cards-note').hidden = !s.detect_message; $('cards-note').textContent = s.detect_message || '';
$('btn-cards-retry').hidden = true; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue';
cards.forEach(function (cd) {
var d = document.createElement('div');
d.className = 'card gpu' + (cd.enabled ? '' : ' off');
var badge = cd.vendor === 'apple' ? '' : cd.vendor === 'nvidia' ? 'NV' : cd.vendor === 'amd' ? 'AMD' : 'GPU';
var badgeHtml = cd.vendor === 'apple' ? '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg>' : badge;
d.innerHTML = '<div class="badge ' + esc(cd.vendor) + '">' + badgeHtml + '</div><div><div class="name">' + esc(cd.name) + '</div><div class="meta"><span>worker <b>' + esc(cd.worker) + '</b></span>' + (cd.detail ? '<span>' + esc(cd.detail) + '</span>' : '') + (cd.device ? '<span>device <b>' + esc(cd.device) + '</b></span>' : '') + '</div>' + (cd.message ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div><div class="tick">' + (cd.enabled ? '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#0C0C0E" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>' : '') + '</div>';
list.appendChild(d);
});
$('cards-help').hidden = false;
$('btn-cards-retry').hidden = true; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = on ? 'Continue' : 'Continue';
}
var lastEventsKey = '';
function renderDashboard(s) {
@ -238,16 +275,22 @@
$('d-found-10').textContent = m.found.filter(function (t) { return nowU - t <= 600; }).length;
$('d-found-60').textContent = m.found.length;
$('d-chain-blocks').textContent = compact(n.blocks);
// cards table
$('d-cards-eyebrow').textContent = m.cards.length + ' worker' + (m.cards.length === 1 ? '' : 's');
// per-card tiles (enabled cards), the rest in one muted line
var on = m.cards.filter(function (c) { return c.enabled; }), off = m.cards.filter(function (c) { return !c.enabled; });
$('d-cards-eyebrow').textContent = on.length + ' worker' + (on.length === 1 ? '' : 's') + (m.cards.length > on.length ? ' of ' + m.cards.length + ' GPUs' : '');
var tb = $('d-cards');
if (!m.cards.length) tb.innerHTML = '<tr><td colspan="6" class="empty">No GPU worker. The node runs on its own.</td></tr>';
else tb.innerHTML = m.cards.map(function (cd) {
if (!on.length) tb.innerHTML = '<div class="empty">' + (m.cards.length ? 'No card switched on. Open settings to pick one.' : 'No GPU worker. The node runs on its own.') + '</div>';
else tb.innerHTML = on.map(function (cd) {
var cls = cd.state === 'mining' ? 'mining' : (cd.state === 'failed' || cd.state === 'restarting') ? 'bad' : '';
var word = cd.state === 'restarting' ? ('restart in ' + cd.restart_in_s + ' s') : cd.state;
var sub = cd.message ? '<span class="sub">' + esc(cd.message) + '</span>' : (cd.ids.length ? '<span class="sub">id ' + esc(cd.ids.join(' ')) + (cd.prepared ? ' · next program ready' : '') + '</span>' : '');
return '<tr><td>' + esc(cd.name) + (cd.identities > 1 ? ' <span class="dim mono">x' + cd.identities + '</span>' : '') + '</td><td class="mono">' + esc(cd.worker) + '</td><td class="n">' + cd.hash_now.toFixed(1) + '</td><td class="n">' + cd.hash_avg.toFixed(1) + '</td><td class="n">' + cd.accepted + (cd.rejected ? ' <span class="dim">/ ' + cd.rejected + '</span>' : '') + '</td><td><span class="st ' + cls + '"><i></i>' + esc(word) + '</span>' + sub + '</td></tr>';
var extra = cd.ids.length ? '<span>id <b>' + esc(cd.ids.join(' ')) + '</b></span>' : '';
return '<div class="gpu-tile' + (cd.state === 'mining' ? '' : ' idle') + '"><div class="n">' + esc(cd.name) + ' <span class="kind ' + esc(cd.kind) + '">' + kindWord(cd.kind) + '</span></div>' +
'<div class="h">' + cd.hash_now.toFixed(1) + '<span class="unit">MH/s</span></div>' +
'<div class="m"><span>blocks <b>' + cd.accepted + '</b>' + (cd.rejected ? ' <span class="dim">/ ' + cd.rejected + ' rejected</span>' : '') + '</span><span>avg <b>' + cd.hash_avg.toFixed(1) + '</b></span><span>identities <b>' + cd.identities + '</b></span>' + extra + '</div>' +
'<span class="st ' + cls + '"><i></i>' + esc(word) + (cd.prepared ? ' · next program ready' : '') + '</span>' + (cd.message ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div>';
}).join('');
$('d-cards-off').hidden = !off.length;
$('d-cards-off').textContent = off.length ? 'off: ' + off.map(function (c) { return c.name + ' (' + kindWord(c.kind).toLowerCase() + ')'; }).join(', ') + '. Settings switches them on.' : '';
// node card
$('d-node-net').textContent = s.chain + (n.version ? ' · ' + n.version.replace(/^igneumd\s*/, '') : '');
$('n-blocks').textContent = withCommas(n.blocks); $('n-headers').textContent = withCommas(n.headers); $('n-peers').textContent = n.peers;
@ -296,7 +339,8 @@
if (first) {
first = false;
var start = forced || (s.setup_done ? 'dashboard' : 'welcome');
if (start === 'key') { show('address'); showKeySheet({ address: s.address.value || '0x0000000000000000000000000000000000000000', display: s.address.display || '0x0000000000000000000000000000000000000000', private_key: '0x' + '0'.repeat(64), wallet_file: s.address.wallet_file || (s.node_dir.replace(/[^\/\\]+$/, '') + 'app/wallet.json') }); }
if (start === 'settings') { show('dashboard'); setTimeout(function () { fillSettings(); $('settings').hidden = false; }, 300); }
else if (start === 'key') { show('address'); showKeySheet({ address: s.address.value || '0x0000000000000000000000000000000000000000', display: s.address.display || '0x0000000000000000000000000000000000000000', private_key: '0x' + '0'.repeat(64), wallet_file: s.address.wallet_file || (s.node_dir.replace(/[^\/\\]+$/, '') + 'app/wallet.json') }); }
else show(start);
if (start === 'cards') renderCards(s);
}

View file

@ -67,7 +67,7 @@
<div class="step">
<div class="eyebrow">step 1 of 2</div>
<h2>Your GPU</h2>
<p class="sub" id="cards-sub">Asking the graphics card to report in.</p>
<p class="sub" id="cards-sub">Asking the graphics cards to report in.</p>
<div class="cards" id="cards-list">
<div class="card detecting" id="cards-detecting">
<div class="spinner"></div>
@ -78,6 +78,7 @@
</div>
</div>
<p class="note" id="cards-note" hidden></p>
<p class="note" id="cards-help" hidden>Each card you switch on gets its own worker. Identities take turns on the card and each one votes and pays separately; 8 suits a big card, 2 a small one, 1 an integrated GPU.</p>
<div class="cta">
<button class="btn primary" id="btn-cards-next" disabled>Continue</button>
<button class="btn ghost" id="btn-cards-retry" hidden>Detect again</button>
@ -155,12 +156,8 @@
</div>
<div class="card">
<div class="card-head"><h3>Cards</h3><div class="eyebrow" id="d-cards-eyebrow">1 worker</div></div>
<div class="tbl">
<table>
<thead><tr><th>Card</th><th>Worker</th><th class="n">MH/s now</th><th class="n">MH/s avg</th><th class="n">Blocks</th><th>State</th></tr></thead>
<tbody id="d-cards"><tr><td colspan="6" class="empty">No card yet.</td></tr></tbody>
</table>
</div>
<div class="gpu-tiles" id="d-cards"><div class="empty">No card yet.</div></div>
<div class="gpu-off mono" id="d-cards-off" hidden></div>
</div>
</div>
<div class="col-side">
@ -233,12 +230,9 @@
<div class="box mono key" id="s-key-box" hidden><span id="s-key"></span><button class="btn tiny" data-copy="s-key">Copy</button></div>
</div>
<div class="field">
<div class="k">identities per card</div>
<div class="row">
<input type="number" min="1" max="64" id="s-identities" class="num mono">
<button class="btn small" id="s-identities-save">Apply</button>
</div>
<p class="note">One worker owns the card; identities take turns on it. 1 is right for most cards. Changing this restarts the miner.</p>
<div class="k">cards</div>
<div class="cards compact" id="s-cards"></div>
<div class="row between"><p class="note">Switch a card on or off, set its identities. Only that card's worker restarts; the node keeps running.</p><button class="btn small" id="s-cards-save">Apply</button></div>
</div>
<div class="field">
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span>Vote on finality checkpoints</span></label>

61
app/windows/BUILD-APP.bat Normal file
View file

@ -0,0 +1,61 @@
@echo off
rem Builds "Igneum Miner.exe" (the WebView2 window host) on a Windows PC. Double-click this file.
rem Needs Visual Studio with the MSVC v143 x64 component (cl.exe, rc.exe) and the internet on the first run
rem (the WebView2 SDK comes from NuGet). The output lands in dist\ and, when the extracted payload folder
rem (igneum-windows-app, with igneum-app.exe) is next to this folder or its parent, is copied into it, so
rem packaging\windows\BUILD-INSTALLER.bat ships it. Without this exe the installer still works: the Start Menu entry
rem runs igneum-app.exe --launch, which opens the dashboard in the default browser.
setlocal EnableDelayedExpansion
cd /d "%~dp0"
set "SDKVER=1.0.2903.40"
set "SDKURL=https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/%SDKVER%"
if not exist build mkdir build
if not exist dist mkdir dist
rem ---- 1. the MSVC environment ----
set "VCVARS="
for %%d in ("C:\Program Files\Microsoft Visual Studio" "C:\Program Files (x86)\Microsoft Visual Studio") do (
for /f "delims=" %%f in ('dir /s /b "%%~d\vcvarsall.bat" 2^>nul') do set "VCVARS=%%f"
)
if "%VCVARS%"=="" (
echo Visual Studio with the MSVC v143 x64 component was not found ^(no vcvarsall.bat under Program Files\Microsoft Visual Studio^).
pause
exit /b 1
)
echo [build] MSVC: "%VCVARS%"
call "%VCVARS%" x64 >nul 2>&1
where cl.exe >nul 2>nul || (echo cl.exe is not on PATH after vcvarsall; open a "x64 Native Tools" prompt and run this file from there. & pause & exit /b 1)
rem ---- 2. the WebView2 SDK (NuGet package, a zip) ----
if not exist "build\webview2\build\native\include\WebView2.h" (
echo [build] downloading the WebView2 SDK %SDKVER%
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ProgressPreference='SilentlyContinue'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%SDKURL%' -OutFile 'build\webview2.zip' -UseBasicParsing; if (Test-Path 'build\webview2') { Remove-Item -Recurse -Force 'build\webview2' }; Expand-Archive -Path 'build\webview2.zip' -DestinationPath 'build\webview2' -Force"
if not exist "build\webview2\build\native\include\WebView2.h" (echo the SDK did not unpack; see build\webview2 & pause & exit /b 1)
)
rem ---- 3. the art: brand\icons in the repo layout, or an art\ folder next to this file ----
set "ART="
if exist "..\..\brand\icons\igneum.ico" set "ART=..\..\brand\icons"
if exist "art\igneum.ico" set "ART=art"
if exist "..\brand\icons\igneum.ico" set "ART=..\brand\icons"
if "%ART%"=="" (echo igneum.ico was not found ^(brand\icons or an art\ folder^). & pause & exit /b 1)
rem ---- 4. compile ----
echo [build] rc
rc.exe /nologo /i "%ART%" /fo build\host.res host.rc || (pause & exit /b 1)
echo [build] cl
cl.exe /nologo /O2 /MT /EHsc /W3 /std:c++17 /DUNICODE /D_UNICODE /I "build\webview2\build\native\include" /Fo"build\\" host.cpp build\host.res ^
/link /SUBSYSTEM:WINDOWS /OUT:"dist\Igneum Miner.exe" "build\webview2\build\native\x64\WebView2LoaderStatic.lib" ^
user32.lib shell32.lib ole32.lib advapi32.lib gdi32.lib version.lib shlwapi.lib || (pause & exit /b 1)
echo [build] done: dist\Igneum Miner.exe
rem ---- 5. into the payload, when it is here ----
for %%p in ("igneum-windows-app" "..\igneum-windows-app" "..\..\igneum-windows-app" "..\..\packaging\windows\igneum-windows-app") do (
if exist "%%~p\igneum-app.exe" (
copy /y "dist\Igneum Miner.exe" "%%~p\" >nul
echo [build] copied into %%~p
)
)
echo.
echo Next: packaging\windows\BUILD-INSTALLER.bat builds the Setup exe with this host inside.
pause

390
app/windows/host.cpp Normal file
View file

@ -0,0 +1,390 @@
// Igneum Miner for Windows: the window around the engine. One C++ file, built on the PC by BUILD-APP.bat
// (MSVC, the WebView2 SDK from NuGet, static loader). It starts igneum-app.exe --wrapper next to it, reads
// "URL ..." / "STATE {...}" / "EXIT" from its stdout, shows the URL in a WebView2 control, keeps a tray icon with the
// state, and on quit writes "quit" to the engine's stdin and waits for EXIT (miners stop first, then the node).
// Closing the window hides it to the tray; the miner keeps running. If the WebView2 runtime is missing the dashboard
// opens in the default browser instead and the window says so. 4 October 2026.
//
// Untested on a real PC at the time of writing (written on a Mac): BUILD-APP.bat is the first run.
#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <shellapi.h>
#include <wrl.h>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include "WebView2.h"
using namespace Microsoft::WRL;
#define WM_ENGINE_LINE (WM_APP + 1)
#define WM_TRAY (WM_APP + 2)
#define ID_TRAY_OPEN 1001
#define ID_TRAY_PAUSE 1002
#define ID_TRAY_QUIT 1003
#define ID_QUIT_TIMER 7
#define IDI_APP 1
static HWND g_hwnd = nullptr;
static HANDLE g_engine = nullptr, g_engineIn = nullptr, g_engineOut = nullptr;
static ComPtr<ICoreWebView2Controller> g_controller;
static ComPtr<ICoreWebView2> g_webview;
static std::wstring g_url, g_status = L"starting the engine";
static bool g_paused = false, g_quitting = false, g_exited = false, g_webviewOk = false, g_hintShown = false;
static NOTIFYICONDATAW g_nid = {};
static std::wstring g_trayTitle = L"Igneum Miner";
static ULONGLONG g_quitStarted = 0;
static std::wstring widen(const std::string& s) {
if (s.empty()) return L"";
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
std::wstring w(n, 0);
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
return w;
}
static std::wstring exeDir() {
wchar_t buf[MAX_PATH];
GetModuleFileNameW(nullptr, buf, MAX_PATH);
std::wstring p(buf);
size_t i = p.find_last_of(L"\\/");
return i == std::wstring::npos ? L"." : p.substr(0, i);
}
static void sendEngine(const char* line) {
if (!g_engineIn) return;
DWORD w = 0;
WriteFile(g_engineIn, line, (DWORD)strlen(line), &w, nullptr);
WriteFile(g_engineIn, "\n", 1, &w, nullptr);
}
static void setTray(const std::wstring& tip) {
g_trayTitle = tip;
wcsncpy_s(g_nid.szTip, tip.c_str(), _TRUNCATE);
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
}
static void repaintStatus() {
InvalidateRect(g_hwnd, nullptr, TRUE);
}
// A tiny JSON number/string/bool reader for the STATE line (flat object, known keys).
static std::string jsonField(const std::string& j, const char* key) {
std::string k = std::string("\"") + key + "\":";
size_t i = j.find(k);
if (i == std::string::npos) return "";
i += k.size();
while (i < j.size() && j[i] == ' ') i++;
if (i < j.size() && j[i] == '"') {
size_t e = j.find('"', i + 1);
return e == std::string::npos ? "" : j.substr(i + 1, e - i - 1);
}
size_t e = i;
while (e < j.size() && j[e] != ',' && j[e] != '}') e++;
return j.substr(i, e - i);
}
static void applyState(const std::string& j) {
std::string mining = jsonField(j, "mining"), node = jsonField(j, "node"), phase = jsonField(j, "phase");
g_paused = jsonField(j, "paused") == "true";
double hash = atof(jsonField(j, "hash_total").c_str());
std::string blocks = jsonField(j, "blocks"), accepted = jsonField(j, "accepted_total");
wchar_t tip[128];
if (jsonField(j, "quitting") == "true") swprintf_s(tip, L"Igneum Miner: stopping");
else if (phase != "dashboard") swprintf_s(tip, L"Igneum Miner: set up");
else if (mining == "mining") swprintf_s(tip, L"Igneum Miner: %.1f MH/s, %S blocks found, node %S", hash, accepted.c_str(), node.c_str());
else if (mining == "paused") swprintf_s(tip, L"Igneum Miner: paused, node %S", node.c_str());
else swprintf_s(tip, L"Igneum Miner: %S, node %S (%S blocks)", mining.c_str(), node.c_str(), blocks.c_str());
setTray(tip);
}
static void openInBrowser(const std::wstring& url) {
ShellExecuteW(nullptr, L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
}
static void navigate() {
if (g_webview && !g_url.empty()) g_webview->Navigate((g_url + L"?host=windows").c_str());
}
static void initWebView() {
std::wstring data = L"";
wchar_t* local = nullptr;
size_t len = 0;
if (_wdupenv_s(&local, &len, L"LOCALAPPDATA") == 0 && local) { data = std::wstring(local) + L"\\igneum\\webview2"; free(local); }
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, data.empty() ? nullptr : data.c_str(), nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
if (FAILED(result) || !env) {
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
repaintStatus();
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
return S_OK;
}
env->CreateCoreWebView2Controller(g_hwnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>([](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
if (FAILED(result) || !controller) {
g_status = L"The app window could not start WebView2. The dashboard opens in your browser.";
repaintStatus();
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
return S_OK;
}
g_controller = controller;
g_controller->get_CoreWebView2(&g_webview);
g_webviewOk = true;
ComPtr<ICoreWebView2Settings> settings;
if (g_webview && SUCCEEDED(g_webview->get_Settings(&settings)) && settings) {
settings->put_AreDefaultContextMenusEnabled(FALSE);
settings->put_IsStatusBarEnabled(FALSE);
settings->put_AreDevToolsEnabled(FALSE);
}
// links off 127.0.0.1 open in the default browser
g_webview->add_NewWindowRequested(Callback<ICoreWebView2NewWindowRequestedEventHandler>([](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT {
LPWSTR uri = nullptr;
if (SUCCEEDED(args->get_Uri(&uri)) && uri) { openInBrowser(uri); CoTaskMemFree(uri); }
args->put_Handled(TRUE);
return S_OK;
}).Get(), nullptr);
g_webview->add_NavigationStarting(Callback<ICoreWebView2NavigationStartingEventHandler>([](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
LPWSTR uri = nullptr;
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
std::wstring u(uri);
CoTaskMemFree(uri);
if (u.rfind(L"http", 0) == 0 && u.find(L"127.0.0.1") == std::wstring::npos) { args->put_Cancel(TRUE); openInBrowser(u); }
}
return S_OK;
}).Get(), nullptr);
RECT rc; GetClientRect(g_hwnd, &rc);
g_controller->put_Bounds(rc);
COREWEBVIEW2_COLOR dark = { 255, 12, 12, 14 };
ComPtr<ICoreWebView2Controller2> c2;
if (SUCCEEDED(g_controller.As(&c2)) && c2) c2->put_DefaultBackgroundColor(dark);
g_status = L"";
repaintStatus();
navigate();
return S_OK;
}).Get());
return S_OK;
}).Get());
if (FAILED(hr)) {
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
repaintStatus();
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
}
}
static bool startEngine() {
SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
HANDLE outR, outW, inR, inW;
if (!CreatePipe(&outR, &outW, &sa, 0) || !CreatePipe(&inR, &inW, &sa, 0)) return false;
SetHandleInformation(outR, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(inW, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = { sizeof(si) };
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = outW;
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
si.hStdInput = inR;
PROCESS_INFORMATION pi = {};
std::wstring exe = exeDir() + L"\\igneum-app.exe";
std::wstring cmd = L"\"" + exe + L"\" --wrapper";
std::vector<wchar_t> buf(cmd.begin(), cmd.end());
buf.push_back(0);
BOOL ok = CreateProcessW(exe.c_str(), buf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, exeDir().c_str(), &si, &pi);
CloseHandle(outW);
CloseHandle(inR);
if (!ok) { CloseHandle(outR); CloseHandle(inW); return false; }
CloseHandle(pi.hThread);
g_engine = pi.hProcess;
g_engineIn = inW;
g_engineOut = outR;
std::thread([] {
std::string acc;
char chunk[4096];
DWORD n;
while (ReadFile(g_engineOut, chunk, sizeof(chunk), &n, nullptr) && n > 0) {
acc.append(chunk, n);
size_t nl;
while ((nl = acc.find('\n')) != std::string::npos) {
std::string line = acc.substr(0, nl);
acc.erase(0, nl + 1);
if (!line.empty() && line.back() == '\r') line.pop_back();
PostMessageW(g_hwnd, WM_ENGINE_LINE, 0, (LPARAM) new std::string(line));
}
}
PostMessageW(g_hwnd, WM_ENGINE_LINE, 1, 0);
}).detach();
return true;
}
static void showTrayMenu() {
HMENU m = CreatePopupMenu();
AppendMenuW(m, MF_STRING, ID_TRAY_OPEN, L"Open Igneum Miner");
AppendMenuW(m, MF_STRING, ID_TRAY_PAUSE, g_paused ? L"Resume mining" : L"Pause mining");
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
AppendMenuW(m, MF_STRING | MF_GRAYED, 0, g_trayTitle.c_str());
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
AppendMenuW(m, MF_STRING, ID_TRAY_QUIT, L"Quit Igneum Miner");
POINT pt; GetCursorPos(&pt);
SetForegroundWindow(g_hwnd);
TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, pt.x, pt.y, 0, g_hwnd, nullptr);
DestroyMenu(m);
}
static void beginQuit() {
if (g_quitting) return;
g_quitting = true;
g_quitStarted = GetTickCount64();
g_status = L"Stopping: the miner first, then the node";
setTray(L"Igneum Miner: stopping");
if (g_webview) g_webview->ExecuteScript(L"document.getElementById('pill-text').textContent='stopping'", nullptr);
if (g_engine && !g_exited) {
sendEngine("quit");
SetTimer(g_hwnd, ID_QUIT_TIMER, 500, nullptr);
} else {
DestroyWindow(g_hwnd);
}
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_SIZE:
if (g_controller) { RECT rc; GetClientRect(hwnd, &rc); g_controller->put_Bounds(rc); }
return 0;
case WM_ENGINE_LINE: {
if (wp == 1) {
g_exited = true;
if (g_quitting) { DestroyWindow(hwnd); return 0; }
g_status = L"The engine stopped. Close this window and open Igneum Miner again.";
setTray(L"Igneum Miner: stopped");
repaintStatus();
return 0;
}
std::string* line = (std::string*)lp;
if (line->rfind("URL ", 0) == 0) {
g_url = widen(line->substr(4));
if (g_webviewOk) navigate();
else if (!g_webview && g_status.find(L"WebView2") != std::wstring::npos && !g_hintShown) { openInBrowser(g_url); g_hintShown = true; }
} else if (line->rfind("STATE ", 0) == 0) {
applyState(line->substr(6));
} else if (line->rfind("FATAL ", 0) == 0) {
g_status = widen(line->substr(6));
repaintStatus();
MessageBoxW(hwnd, g_status.c_str(), L"Igneum Miner", MB_OK | MB_ICONERROR);
} else if (*line == "EXIT") {
g_exited = true;
if (g_quitting) DestroyWindow(hwnd);
}
delete line;
return 0;
}
case WM_TIMER:
if (wp == ID_QUIT_TIMER) {
DWORD code = 0;
bool gone = !g_engine || (GetExitCodeProcess(g_engine, &code) && code != STILL_ACTIVE);
if (gone || g_exited || GetTickCount64() - g_quitStarted > 45000) {
if (!gone && g_engine) TerminateProcess(g_engine, 1);
KillTimer(hwnd, ID_QUIT_TIMER);
DestroyWindow(hwnd);
}
}
return 0;
case WM_TRAY:
if (lp == WM_LBUTTONUP || lp == WM_LBUTTONDBLCLK) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); }
else if (lp == WM_RBUTTONUP || lp == WM_CONTEXTMENU) showTrayMenu();
return 0;
case WM_COMMAND:
switch (LOWORD(wp)) {
case ID_TRAY_OPEN: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); return 0;
case ID_TRAY_PAUSE: sendEngine(g_paused ? "resume" : "pause"); return 0;
case ID_TRAY_QUIT: beginQuit(); return 0;
}
return 0;
case WM_CLOSE:
// hide to the tray; the miner keeps running
ShowWindow(hwnd, SW_HIDE);
if (!g_hintShown) {
g_nid.uFlags |= NIF_INFO;
wcscpy_s(g_nid.szInfoTitle, L"Igneum Miner keeps mining");
wcscpy_s(g_nid.szInfo, L"The window is in the tray. Right-click the icon to pause or quit.");
g_nid.dwInfoFlags = NIIF_INFO;
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
g_nid.uFlags &= ~NIF_INFO;
g_hintShown = true;
}
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC dc = BeginPaint(hwnd, &ps);
RECT rc; GetClientRect(hwnd, &rc);
HBRUSH bg = CreateSolidBrush(RGB(12, 12, 14));
FillRect(dc, &rc, bg);
DeleteObject(bg);
if (!g_status.empty()) {
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, RGB(154, 154, 158));
HFONT f = CreateFontW(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
HFONT old = (HFONT)SelectObject(dc, f);
RECT tr = rc; tr.left += 40; tr.right -= 40;
DrawTextW(dc, g_status.c_str(), -1, &tr, DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOPREFIX | (g_webviewOk ? DT_BOTTOM : DT_VCENTER));
SelectObject(dc, old);
DeleteObject(f);
}
EndPaint(hwnd, &ps);
return 0;
}
case WM_DESTROY:
Shell_NotifyIconW(NIM_DELETE, &g_nid);
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumMinerWindow");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
HWND other = FindWindowW(L"IgneumMinerWindow", nullptr);
if (other) { ShowWindow(other, SW_SHOW); SetForegroundWindow(other); }
return 0;
}
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
WNDCLASSW wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInst;
wc.lpszClassName = L"IgneumMinerWindow";
wc.hIcon = LoadIconW(hInst, MAKEINTRESOURCEW(IDI_APP));
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hbrBackground = CreateSolidBrush(RGB(12, 12, 14));
RegisterClassW(&wc);
int w = 1120, h = 820;
int sx = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, sy = (GetSystemMetrics(SM_CYSCREEN) - h) / 2;
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Igneum Miner", WS_OVERLAPPEDWINDOW, sx, sy, w, h, nullptr, nullptr, hInst, nullptr);
ShowWindow(g_hwnd, SW_SHOW);
g_nid.cbSize = sizeof(g_nid);
g_nid.hWnd = g_hwnd;
g_nid.uID = 1;
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
g_nid.uCallbackMessage = WM_TRAY;
g_nid.hIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
wcscpy_s(g_nid.szTip, L"Igneum Miner: starting");
Shell_NotifyIconW(NIM_ADD, &g_nid);
if (!startEngine()) {
g_status = L"igneum-app.exe is missing next to this program. Run the installer again.";
repaintStatus();
MessageBoxW(g_hwnd, g_status.c_str(), L"Igneum Miner", MB_OK | MB_ICONERROR);
}
initWebView();
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (g_engine) { CloseHandle(g_engine); }
CloseHandle(once);
CoUninitialize();
return 0;
}

34
app/windows/host.rc Normal file
View file

@ -0,0 +1,34 @@
// Resources for Igneum Miner.exe (the window host): the coin icon and the version block.
// Compiled by BUILD-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i).
#include <winver.h>
1 ICON "igneum.ico"
1 VERSIONINFO
FILEVERSION 0,3,0,0
PRODUCTVERSION 0,3,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.3.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.3.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View file

@ -1,123 +1,78 @@
# Igneum Miner for Mac (packaging/mac)
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 except the Metal worker
(one `swiftc` call, about 7 s). 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. Version 0.2.0 (4 October 2026) is the devnet v4 build: a new chain from genesis, so the data
folder moved to `devnet-v4`; the miner takes an EVM payout address (`--evm-address`, `PAYOUT_EVM`), runs every identity
through one process (`--identities`), votes on finality by default (`VOTE=0` turns it off) and hot-swaps the hourly
program through the worker's `prepare` command.
A DMG a miner opens on an Apple silicon Mac: drag the app to Applications, open it, answer two questions, mine.
Version 0.3.0 (4 October 2026) is the app: a branded window (no Terminal), the engine that runs the node and the miner,
a menu-bar item, settings, an update check. 0.1.0 and 0.2.0 (3 and 4 October 2026) were the Terminal launchers; the
0.2.0 run script's command lines live on inside the engine (`app/igneum-app/src/engine.rs`).
## Build
packaging/mac/build-dmg.sh
Output: `packaging/mac/dist/Igneum-Miner-0.2.0.dmg` (about 16 MB, lzfse). The script takes
`vendor/igneum-node/target-integration/release/igneumd` and `igneum-miner` (the devnet-v4 integration build of
`vendor/igneum-node-v4` at dc749905, `NODE=` and `MINER=` override) and rebuilds the Metal worker from
`proto-metal/main.swift` (the hot-swap source) with `-target arm64-apple-macos11`, so it loads on any Apple silicon macOS
and not only on the build machine's (the committed `proto-metal/igneum-bench-hotswap` says minos 16.0; `REBUILD_WORKER=0`
ships it as is, `WORKER=` names another one). It copies the three 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, that the worker says `prepare 1` and that the miner is the v4 one (`--evm-address` in its usage). 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.
Output: `packaging/mac/dist/Igneum-Miner-0.3.0.dmg` (about 20 MB, lzfse). The script compiles the engine
(`app/igneum-app`, `cargo build --release`, nice 19 and 4 jobs because the devnet mines on this Mac), the window
(`app/mac/IgneumMiner.swift`, `swiftc -target arm64-apple-macos11`) and the Metal worker (`proto-metal/main.swift`,
same target), takes `igneumd` and `igneum-miner` from `vendor/igneum-node/target-integration/release` (the devnet-v4
integration build; `NODE=`, `MINER=`, `WORKER=`, `ENGINE=` override), strips and ad-hoc signs the copies, checks each
one runs, writes `igneum-app.json` (below) and lays the image out with `dmgbuild` (`pip3 install dmgbuild`; without it a
plain hdiutil image). The icon is `brand/icons/igneum.icns`, the DMG art `brand/icons/dmg-background.tiff`
(`python3 brand/icons/make-icons.py`). `build/` and `dist/` are ignored by git.
## The bundle
Igneum Miner.app/Contents/MacOS/Igneum Miner the window: WKWebView + NSStatusItem (app/mac/IgneumMiner.swift)
Igneum Miner.app/Contents/MacOS/igneum-app the engine (app/igneum-app)
Igneum Miner.app/Contents/Resources/bin/igneumd, igneum-miner, igneum-bench
Igneum Miner.app/Contents/Resources/igneum-app.json packaged configuration (packaged-config.sh writes it)
Igneum Miner.app/Contents/Resources/igneum.icns
Igneum Miner.app/Contents/Info.plist app/Info.plist (CFBundleVersion = build stamp, LSMinimumSystemVersion 11.0)
The window starts `igneum-app --wrapper`, reads `URL ...` and `STATE {...}` lines from its stdout, shows the URL in the
web view and keeps the menu-bar title (hash rate, paused, syncing). Quit writes `quit` to the engine's stdin and waits
for `EXIT` (up to 45 s): the miners stop first (8 s grace), then the node (30 s), then the last log upload. Closing
the window hides it; the app lives on in the menu bar and the Dock. The engine serves the dashboard on
`127.0.0.1:<random port>/t/<32-hex token>/`; nothing without the token answers. Links off 127.0.0.1 open in the
default browser.
`igneum-app.json` (never in the UI source): `update_manifest` = `https://dl.igneum.network/dl/<token>/igneum-app-latest.json`
with the token read from `~/.config/igneum/dl-token` at build time (missing file = update check off), the log intake
URL and key (the key only authorises uploads, as the 0.2.0 launchers shipped it), the live page. The manifest the
engine expects: `{"version":"0.3.1","mac":{"url":"...dmg","notes":"..."},"windows":{"url":"...exe","notes":"..."}}`;
a newer version shows a banner with a Download button (opens the URL in the browser). Checked 20 s after start and
every 6 h, and from Settings.
Gatekeeper: the app is unsigned (ad hoc). Right-click > Open the first time. The engine strips
`com.apple.quarantine` from the bundle's Contents on start, so the binaries inside are not refused one by one; that
needs a writable volume, hence "drag to Applications first".
## What the engine does (app/igneum-app)
Order: binaries next to the app > `igneum-app.json` > settings (`~/Library/Application Support/Igneum/app/settings.json`)
> GPU detection (the Metal worker's `ready` line gives the device name; system_profiler the cores) > first run: cards
(switch, identities) and the rewards address (a secp256k1 key made here, `app/wallet.json` mode 0600, shown once; or a
pasted 0x address) > `igneumd --devnet --appdir ~/Library/Application Support/Igneum/devnet-v4 --rpclisten 127.0.0.1:26610
--listen 0.0.0.0:26611 --addpeer 188.245.5.161:26611 --addpeer 192.168.68.64:26611 --nodnsseed --disable-upnp --nologfiles
--yes` (a node already answering on 26610 is used as is and never stopped) > `igneum-miner watch 1000000000` as one
long-lived reader (a line every 10 s: blocks, headers, daa, peers, difficulty, blue, synced) > synced (the node's own
flag with a peer, else the 0.2.0 rule) > per enabled card `igneum-miner mine grpc://127.0.0.1:26610 1 100000000
mac-<host> --worker igneum-bench --status-secs 30 --exit-on-seed-change --evm-address <addr> [--identities N]
[--no-vote] --payout-label mac-<host>` > the loop: STATUS lines into hash rate (the v4 `now=` interval rate) and
counts, ACCEPTED into blocks and the strip, `worker: prepared` into "next program compiled and ready", LOCK/VOTE into
finality, a status line every 30 s, uploads every 60 s, restarts (exit 42 at once; crashes 5 to 60 s; the node 5 to
60 s with the miners held until it is synced again). Pause stops the miners and keeps the node. Settings restart only
the affected worker.
Tests: `IGNEUM_APP_NETWORK=devnet IGNEUM_APP_DEVNET_SUFFIX=7 IGNEUM_APP_RPC_PORT=29400 IGNEUM_APP_P2P_PORT=29401
IGNEUM_APP_PEERS= IGNEUM_APP_UNSYNCED=1 IGNEUM_APP_DATA=<dir> IGNEUM_APP_LOGS=<dir> igneum-app --print-url --no-open`
runs a private devnet (its own genesis, no peers) with real mining; the window binary takes the same environment.
Screenshots: `"Igneum Miner" --snapshot out.png --url <dashboard url>?screen=welcome|cards|address|key|dashboard|settings`
(docs/design/app-screens).
## What is on the DMG
| Item | What it is |
|---|---|
| `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`) |
| `Igneum Miner.app` | the app |
| `README.txt` | 8 lines for the miner (`dmg/README.txt`), with the seed line |
| `Stop Igneum Miner.command` | asks a running copy to stop through its API, then ends leftovers (`app/Stop Igneum Miner.command`) |
| `Applications` | symlink, for the drag |
## The app bundle
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 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.
## The run script (settings at the top)
| Variable | Default | Meaning |
|---|---|---|
| `SEED_PEERS` | `188.245.5.161:26611,192.168.68.64:26611` | the seed node, then the Mac node on the project lead's LAN; comma list |
| `MINERS` | `1` | miner identities, all inside one miner process (`--identities`); 0 = node only. One worker owns the whole GPU |
| `PAYOUT_EVM` | empty | EVM payout address for the block rewards; empty = 20 bytes of SHA-256 over `igneum-evm-payout-v0:mac-<hostname>`, printed at start |
| `VOTE` | `1` | finality voting; 0 adds `--no-vote` |
| `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-v4`, `~/Library/Logs/Igneum` | the only places written outside the bundle (0.1.0 wrote `devnet`, left alone) |
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-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 (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 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)
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 ...'`.
## 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

@ -11,7 +11,7 @@
<key>CFBundleVersion</key>
<string>VERSION_STAMP</string>
<key>CFBundleShortVersionString</key>
<string>0.2.0</string>
<string>0.3.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
@ -30,6 +30,15 @@
</array>
<key>LSRequiresNativeExecution</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSHumanReadableCopyright</key>
<string>Igneum. Nothing is bought or sold.</string>
</dict>

View file

@ -1,21 +1,18 @@
#!/bin/bash
[ -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-miner.run"
script="$(sed -n 's/^script=//p' "$RUNFILE" 2>/dev/null)"
if [ -n "$script" ] && [ -x "$script" ]; then
exec "$script" --stop
# Stops a running Igneum Miner: asks the engine to quit through its local API (miner first, then the node), waits,
# then ends whatever is left from the app bundle. 0.3.0, 4 October 2026.
URL_FILE="$HOME/Library/Application Support/Igneum/app/app.url"
if [ -f "$URL_FILE" ]; then
url="$(cat "$URL_FILE")"
if curl -s -m 5 -X POST "${url}api/quit" -d '{}' >/dev/null 2>&1; then
echo "asked Igneum Miner to stop (miner first, then the node)"
for i in $(seq 1 50); do pgrep -f 'Igneum Miner.app/Contents/' >/dev/null || break; sleep 1; done
fi
fi
left="$(pgrep -f 'Igneum Miner.app/Contents/Resources/bin/' 2>/dev/null)"
left="$(pgrep -f 'Igneum Miner.app/Contents/' 2>/dev/null)"
if [ -n "$left" ]; then
echo "stopping Igneum processes: $(echo $left | tr '\n' ' ')"
pkill -TERM -f 'Igneum Miner.app/Contents/Resources/bin/' 2>/dev/null
sleep 5
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 Miner is not running."
echo "ending leftover processes: $(echo $left | tr '\n' ' ')"
pkill -TERM -f 'Igneum Miner.app/Contents/' 2>/dev/null; sleep 5
pkill -KILL -f 'Igneum Miner.app/Contents/' 2>/dev/null
fi
echo "Igneum Miner is stopped."

View file

@ -1,26 +1,32 @@
#!/bin/bash
# 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; 0.2.0 for devnet v4 on 4 October 2026.
# icons placed). 3 October 2026; 0.2.0 for devnet v4 on 4 October 2026; 0.3.0 the same day: the app (no Terminal).
#
# The bundle, 0.3.0:
# Contents/MacOS/Igneum Miner the window (app/mac/IgneumMiner.swift, WKWebView + menu-bar item), compiled here
# Contents/MacOS/igneum-app the engine (app/igneum-app, cargo --release), compiled here unless ENGINE= names one
# Contents/Resources/bin/ igneumd, igneum-miner (vendor/igneum-node/target-integration/release, the devnet-v4
# integration build) and igneum-bench (the Metal worker, rebuilt from proto-metal/main.swift
# with -target arm64-apple-macos11 so it loads on any Apple silicon macOS)
# Contents/Resources/igneum-app.json the update manifest URL (token from ~/.config/igneum/dl-token, not in the repo),
# the log intake, the live page (packaged-config.sh)
# Contents/Resources/igneum.icns
# Copies of the binaries are stripped and re-signed ad hoc (strip invalidates the linker signature and arm64 macOS
# refuses an unsigned binary); the originals are not touched.
#
# packaging/mac/build-dmg.sh build
# NODE=<path> packaging/mac/build-dmg.sh use another node binary
# 0.2.0 defaults: igneumd and igneum-miner from vendor/igneum-node/target-integration/release (the devnet-v4 integration
# build of vendor/igneum-node-v4 at dc749905) and the Metal worker rebuilt from proto-metal/main.swift (the hot-swap
# source: its ready line says "prepare 1") with `-target arm64-apple-macos11`, so the worker loads on any Apple silicon
# macOS and not only on the build machine's (the committed proto-metal/igneum-bench-hotswap carries minos 16.0).
# WORKER=<path> skips that rebuild; REBUILD_WORKER=0 uses proto-metal/igneum-bench-hotswap as is.
# 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.
# NODE=<path> MINER=<path> WORKER=<path> ENGINE=<path> use other binaries; REBUILD_WORKER=0 ships proto-metal/igneum-bench-hotswap
# Needs: brand/icons/igneum.icns and dmg-background.tiff (python3 brand/icons/make-icons.py), swiftc, cargo (rustup), 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.2.0"
VERSION="0.3.0"
NODE="${NODE:-$ROOT/vendor/igneum-node/target-integration/release/igneumd}"
MINER="${MINER:-$ROOT/vendor/igneum-node/target-integration/release/igneum-miner}"
WORKER="${WORKER:-}"
ENGINE="${ENGINE:-}"
REBUILD_WORKER="${REBUILD_WORKER:-1}"
ICONS="$ROOT/brand/icons"
BUILD="$HERE/build"
@ -29,6 +35,8 @@ DMG="$DIST/Igneum-Miner-$VERSION.dmg"
STAGE="$BUILD/dmg"
APP="$STAGE/Igneum Miner.app"
STAMP="$(date -u +%Y%m%d%H%M)"
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:$PATH"
. "$HERE/packaged-config.sh"
if [ ! -x "$NODE" ]; then
if [ -x "$ROOT/vendor/igneum-node/target/release/kaspad" ]; then
@ -43,52 +51,69 @@ for f in "$NODE" "$MINER"; 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; }
command -v swiftc >/dev/null 2>&1 || { echo "swiftc is needed for the window (xcode-select --install)"; exit 1; }
rm -rf "$BUILD"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources/bin" "$DIST"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources/bin" "$DIST" "$BUILD/worker" "$BUILD/window"
# the Metal worker: rebuilt from main.swift for macOS 11 and later (about 7 s), else the committed hot-swap binary
if [ -z "$WORKER" ]; then
if [ "$REBUILD_WORKER" = 1 ] && command -v swiftc >/dev/null 2>&1; then
mkdir -p "$BUILD/worker"
if [ "$REBUILD_WORKER" = 1 ]; then
echo "building the Metal worker from proto-metal/main.swift with -target arm64-apple-macos11"
(cd "$ROOT/proto-metal" && nice -n 19 swiftc -O -target arm64-apple-macos11 -o "$BUILD/worker/igneum-bench" main.swift -framework Metal)
WORKER="$BUILD/worker/igneum-bench"
else
WORKER="$ROOT/proto-metal/igneum-bench-hotswap"
echo "note: using $WORKER as built (its load command says minos $(otool -l "$WORKER" | awk '/minos/ { print $2; exit }'); a rebuild with -target arm64-apple-macos11 needs swiftc)"
echo "note: using $WORKER as built (its load command says minos $(otool -l "$WORKER" | awk '/minos/ { print $2; exit }'))"
fi
fi
[ -x "$WORKER" ] || { echo "missing: $WORKER"; exit 1; }
file "$WORKER" | grep -q 'arm64' || { echo "$WORKER is not an arm64 binary"; exit 1; }
echo "worker: $WORKER (minos $(otool -l "$WORKER" | awk '/minos/ { print $2; exit }'))"
# the engine (cargo, nice 19, 4 jobs: the devnet is mining on this Mac)
if [ -z "$ENGINE" ]; then
echo "building the engine (app/igneum-app, cargo --release)"
(cd "$ROOT/app/igneum-app" && nice -n 19 cargo build --release -j 4 --quiet)
ENGINE="$ROOT/app/igneum-app/target/release/igneum-app"
fi
[ -x "$ENGINE" ] || { echo "missing: $ENGINE"; exit 1; }
"$ENGINE" --version
# the window
echo "building the window (app/mac/IgneumMiner.swift)"
(cd "$ROOT/app/mac" && nice -n 19 swiftc -O -target arm64-apple-macos11 -o "$BUILD/window/Igneum Miner" IgneumMiner.swift -framework Cocoa -framework WebKit 2>&1 | grep -v 'warning\|^ *\^\|^$' || true)
[ -x "$BUILD/window/Igneum Miner" ] || { echo "the window did not build"; exit 1; }
# 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 Miner"
cp "$HERE/app/igneum-miner.sh" "$APP/Contents/Resources/igneum-miner.sh"
cp "$BUILD/window/Igneum Miner" "$APP/Contents/MacOS/Igneum Miner"
cp "$ENGINE" "$APP/Contents/MacOS/igneum-app"
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 Miner" "$APP/Contents/Resources/igneum-miner.sh" "$APP/Contents/Resources/bin/"*
write_packaged_config "$APP/Contents/Resources/igneum-app.json"
chmod 755 "$APP/Contents/MacOS/"* "$APP/Contents/Resources/bin/"*
# strip the copies, then sign them ad hoc again
for b in "$APP/Contents/Resources/bin/"*; do
# strip the copies, then sign them ad hoc again (the engine is already stripped by cargo)
for b in "$APP/Contents/Resources/bin/"* "$APP/Contents/MacOS/"*; do
before=$(stat -f %z "$b")
strip "$b" 2>/dev/null || strip -x "$b"
strip "$b" 2>/dev/null || strip -x "$b" 2>/dev/null || true
codesign -s - -f "$b" 2>/dev/null
codesign -v "$b"
echo "$(basename "$b"): $before -> $(stat -f %z "$b") bytes, signed ad hoc"
done
codesign -s - -f "$APP" 2>/dev/null || true
# 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
case "$v" in *"--evm-address"*) ;; *) echo "igneum-miner copy is not the devnet-v4 miner (no --evm-address in its usage)"; 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
case "$v" in *"prepare 1"*) ;; *) echo "note: this worker has no prepare support (no hot swap at the hour boundary); the miner falls back to exit 42" ;; esac
v="$("$APP/Contents/Resources/bin/igneum-miner" 2>&1 || true)"; case "$v" in *"--evm-address"*) ;; *) echo "igneum-miner copy is not the devnet-v4 miner (no --evm-address in its usage)"; exit 1 ;; esac
v="$("$APP/Contents/MacOS/igneum-app" --version 2>&1 || true)"; case "$v" in igneum-app*) echo "engine: $v" ;; *) echo "the engine copy does not run: $v"; exit 1 ;; esac
# the rest of the image
cp "$HERE/dmg/README.txt" "$STAGE/README.txt"

View file

@ -1,12 +1,12 @@
Igneum Miner 0.2.0 for Mac (Apple silicon only: M1 or later). Devnet v4, 4 October 2026. Test network; nothing is bought or sold.
Igneum Miner 0.3.0 for Mac (Apple silicon only: M1 or later). Devnet v4, 4 October 2026. 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.
1. Drag "Igneum Miner" into Applications, then open it from there.
2. First time: 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 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.
Block rewards go to the EVM address the window prints (set PAYOUT_EVM in the environment to use your own).
5. To stop: press Ctrl+C in that window (miner stops first, then the node). If the window was closed, open "Stop Igneum Miner".
Devnet v4 is a new chain: the data lives in ~/Library/Application Support/Igneum/devnet-v4 (an older devnet folder there is left alone).
The Mac stays awake while the window is open. Logs: ~/Library/Logs/Igneum.
3. The window walks you through two steps: your GPU (switch it on, pick the identities) and where rewards go
(make an address here, or paste your own). Then it starts the node, syncs (usually under a minute) and mines.
4. The dashboard shows your hash rate, your blocks, the node and the countdown to the next hourly program.
Your miner appears on igneum.network/live within about a minute of its first block.
5. Closing the window keeps mining in the menu bar (the flame icon). Quit from there or from the window to stop:
the miner stops first, then the node. "Stop Igneum Miner" on this disk does the same for a stuck copy.
Data: ~/Library/Application Support/Igneum (devnet-v4 chain, app/wallet.json for a made address). Logs: ~/Library/Logs/Igneum.

View file

@ -0,0 +1,27 @@
#!/bin/bash
# The configuration the packagers write next to the engine (igneum-app.json): the update manifest URL (the download
# token is read from ~/.config/igneum/dl-token and never lives in the repo), the log intake (the key only authorises
# log uploads and is meant to ship, as the 0.2.0 launchers did), the live page. Sourced by build-dmg.sh; the Windows
# make-payload.sh reads LOG_KEY from this file.
LOG_URL="https://igneum-six.vercel.app/api/log"
LOG_KEY="***INTAKE-KEY-REMOVED***"
LIVE_PAGE="https://igneum.network/live"
DOWNLOAD_PAGE="https://igneum.network/#mine"
TOKEN_FILE="$HOME/.config/igneum/dl-token"
# writes the JSON to $1
write_packaged_config() {
local out="$1" token="" manifest=""
[ -f "$TOKEN_FILE" ] && token="$(tr -d '[:space:]' < "$TOKEN_FILE")"
[ -n "$token" ] && manifest="https://dl.igneum.network/dl/$token/igneum-app-latest.json"
[ -n "$manifest" ] || echo "note: no $TOKEN_FILE; the update check is disabled in this build"
cat > "$out" <<JSON
{
"update_manifest": "$manifest",
"log_intake_url": "$LOG_URL",
"log_intake_key": "$LOG_KEY",
"live_page": "$LIVE_PAGE",
"download_page": "$DOWNLOAD_PAGE"
}
JSON
}

View file

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

View file

@ -1,112 +1,114 @@
; 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;
; 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.
; 0.3.0 (4 October 2026): the app (igneum-app.exe engine, "Igneum Miner.exe" window host when BUILD-APP.bat made it,
; igneumd.exe, igneum-miner.exe, the prebuilt GPU workers when present, the worker sources for the fallback build).
#ifndef Payload
#define Payload "igneum-windows-app"
#endif
#ifndef ArtDir
#define ArtDir "..\..\brand\icons"
#endif
#ifndef AppVersion
#define AppVersion "0.3.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 miner, and shows your hash rate, your blocks and the chain. 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 engine writes its data under %LOCALAPPDATA%\igneum; the fallback worker build writes next to the sources.
Name: "{app}"; Permissions: users-modify
[Files]
Source: "{#Payload}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion; Excludes: "*.log,*.seeds,*.DS_Store,packs\*,build\*,dist\*"
Source: "{#ArtDir}\igneum.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "wrappers\Stop Igneum Miner.cmd"; DestDir: "{app}"; Flags: ignoreversion
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; igneum-app.exe --launch hands over to "Igneum Miner.exe" (the window) when it is installed, else opens the dashboard in the browser.
Name: "{group}\Igneum Miner"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; 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 miner and the node"
Name: "{group}\Igneum Miner logs"; Filename: "{localappdata}\igneum\logs"; 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-app.exe"; Parameters: "--launch"; 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 as the signed-in user, not as administrator (the data lands in that user's %LOCALAPPDATA%).
Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; Description: "Start Igneum Miner now"; Flags: postinstall nowait skipifsilent runasoriginaluser
[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]
; The GPU workers built on this PC and the WebView2 cache 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

@ -1,11 +1,54 @@
# 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).
Inno Setup installer around the app payload. Version 0.3.0 (4 October 2026): the app (engine + window host) instead of
the console launchers of 0.1.0.
## On the Mac
packaging/windows/make-payload.sh [out.zip]
Assembles `packaging/windows/igneum-windows-app/` and zips it (default `~/Desktop/igneum-windows-app.zip`, about 27 MB):
`igneum-app.exe` (the engine, `app/igneum-app` cross-compiled with the mingw toolchain exactly as `igneumd.exe` is:
`cargo build --release --target x86_64-pc-windows-gnu` with the environment of `proto-cuda/windows-node/cross-build.sh`;
it links against system DLLs only), `igneumd.exe` and `igneum-miner.exe` (the devnet-v4 cross-build), the three mingw
runtime DLLs, the prebuilt one-click workers when they exist (`proto-cuda/nvrtc/igneum-worker-cuda.exe` with NVIDIA's
`nvrtc64_*_0.dll` and `nvrtc-builtins64_*.dll` and the licence texts, `proto-opencl/igneum-worker-opencl.exe`), the
worker sources for the fallback build (`proto-cuda\`, `proto-opencl\`), `igneum-app.json` (update manifest URL with the
token from `~/.config/igneum/dl-token`, log intake, live page), `stop-igneum.ps1`, and `app\windows\` (the window host
sources and `BUILD-APP.bat`, since WebView2 cannot be linked from the Mac).
## On the PC
1. Extract the zip next to `packaging\windows` (or anywhere: `build-installer.ps1 -Payload <folder>`; without a folder it
downloads `dl.igneum.network/igneum-windows-app.zip`).
2. Optional, for the app window: `igneum-windows-app\app\windows\BUILD-APP.bat`. Needs Visual Studio with the MSVC v143
x64 component; it fetches the WebView2 SDK from NuGet, compiles `host.cpp` with the static loader and copies
`Igneum Miner.exe` into the payload. Without it the Start Menu entry runs `igneum-app.exe --launch`, which opens the
dashboard in the default browser (same engine, same screens).
3. `packaging\windows\BUILD-INSTALLER.bat`: installs Inno Setup 6 through winget if missing, stamps the coin icon and the
version block into `igneum-app.exe`, `igneumd.exe` and `igneum-miner.exe` with rcedit unless they carry one, compiles
`Igneum-Miner.iss`, writes `dist\Igneum-Miner-Setup-0.3.0.exe`.
The installer: Program Files\Igneum Miner, Start Menu group (Igneum Miner, Stop Igneum Miner, Igneum Miner logs, Uninstall),
optional desktop icon and firewall rule for `igneumd.exe` on private networks, "Start Igneum Miner now" on the finish page
(as the signed-in user), `stop-igneum.ps1` before an upgrade and on uninstall (it POSTs `api/quit` to the running engine
through `%LOCALAPPDATA%\igneum\app\app.url`, waits, then ends what is left), licence page (MIT placeholder, pending),
finish page with the seed line. Data: `%LOCALAPPDATA%\igneum\devnet-v4` (the chain, the same folder the 0.2.0 launcher
used), `%LOCALAPPDATA%\igneum\app` (settings, wallet.json locked to the user with icacls, the hourly program packs),
`%LOCALAPPDATA%\igneum\logs`. Unsigned until Igneum has a code-signing certificate: SmartScreen warns (More info > Run anyway).
## What the engine does on Windows
GPU detection: `nvidia-smi --query-gpu=index,name,memory.total` for NVIDIA (Discrete, VRAM), the OpenCL worker's `--list`
for the rest (AMD, Intel; Integrated by name: "Radeon Graphics", "Iris", "UHD"), the WMI names as a last resort.
Discrete cards default on (8 identities at 8 GB and up, else 2), integrated off with the reason shown. Each enabled card
gets its own miner process: `igneum-miner mine grpc://127.0.0.1:26610 1 100000000 nvidia-<pc>[-n] --worker <igneum-worker-cuda.exe>
--status-secs 30 --exit-on-seed-change --prepare-packs packs\prepare --evm-address <addr> [--identities N] --payout-label <label>
--worker-args "--device N --pack packs\devnet"` (cwd `%LOCALAPPDATA%\igneum\app`, so the pack paths stay relative; the
OpenCL worker adds `--job-nonces 2097152`). Before each start the engine runs `igneum-miner export-pack` for the prebuilt
worker; without a prebuilt worker it runs today's build path (`proto-cuda\build.bat devnet sm_120` in the MSVC
environment, rebuilt at every hour boundary after exit 42), exactly as `igneum-common.ps1` falls back.
Untested at the time of writing (written on a Mac): the window host (`app/windows/host.cpp`, first run is BUILD-APP.bat),
the Inno build, nvidia-smi and OpenCL detection, the prebuilt workers under the engine. `embed-resources.sh` (windres +
relink on the Mac) still works for `igneumd.exe` and `igneum-miner.exe`; the engine's icon comes from rcedit on the PC.

View file

@ -1,163 +1,166 @@
# 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).'
# 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-app folder (igneum-app.exe, igneumd.exe, igneum-miner.exe, made by
# make-payload.sh on the Mac) next to this script, in the parent folder, or given with -Payload; otherwise the zip at
# -PayloadUrl is downloaded. "Igneum Miner.exe" (app\windows\BUILD-APP.bat) is shipped when it is in the payload.
# 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 igneum-app.exe, 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.3.0] [-NoRcedit] [-NoWinget]
param(
[string]$Payload = '',
[string]$PayloadUrl = $(if ($env:IGNEUM_PAYLOAD_URL) { $env:IGNEUM_PAYLOAD_URL } else { 'https://dl.igneum.network/igneum-windows-app.zip' }),
[string]$Version = '0.3.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 @('igneum-app.exe', '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-app'), (Join-Path $here '..\igneum-windows-app'), (Join-Path $here 'payload'), (Join-Path $here '..\..\igneum-windows-app'))) {
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 'igneum-app.exe' | Select-Object -First 1
if (-not $hit -or -not (Test-PayloadDir $hit.DirectoryName)) { throw "the zip from $PayloadUrl holds no complete payload (igneum-app.exe, igneumd.exe, igneum-miner.exe, stop-igneum.ps1)." }
$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 built per PC by the fallback path
if ($rel -match '^app\\windows\\(build|dist)(\\|$)') { return } # BUILD-APP.bat's intermediate files
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 @('igneum-app.exe', 'igneumd.exe', 'igneum-miner.exe')) {
$exe = Join-Path $payloadDir $exeName
if (-not (Test-Path $exe)) { continue }
$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 = switch ($exeName) { 'igneumd.exe' { 'Igneum Miner node' } 'igneum-app.exe' { 'Igneum Miner engine' } default { '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).'

102
packaging/windows/make-payload.sh Executable file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Assembles the Windows payload for Igneum Miner 0.3.0 on the Mac and zips it: packaging/windows/igneum-windows-app/
# and ~/Desktop/igneum-windows-app.zip (or $1). On the PC: extract the zip next to packaging\windows (or let
# build-installer.ps1 fetch it), run app\windows\BUILD-APP.bat (the WebView2 window host, optional), then
# packaging\windows\BUILD-INSTALLER.bat.
#
# What goes in:
# igneum-app.exe the engine, cross-compiled here (app/igneum-app, x86_64-pc-windows-gnu)
# igneumd.exe, igneum-miner.exe the devnet-v4 cross-build (vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release)
# lib*.dll the three mingw runtime DLLs, as igneum-windows-v4.zip ships them
# igneum-worker-cuda.exe, igneum-worker-opencl.exe, nvrtc*.dll the prebuilt GPU workers from the proto-cuda/proto-opencl
# agent when they exist (searched in a few places; IGNEUM_WORKERS_DIR overrides); without them
# the engine builds the CUDA worker from proto-cuda\ on the PC (needs the CUDA Toolkit and MSVC)
# proto-cuda\, proto-opencl\ the worker sources and build.bat for that fallback
# igneum-app.json the update manifest URL (token from ~/.config/igneum/dl-token, never in the repo), the log intake
# stop-igneum.ps1 what the installer runs before an upgrade and on uninstall
# app\windows\ host.cpp, host.rc, BUILD-APP.bat (the window host is built on the PC)
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
VERSION="0.3.0"
OUT="${1:-$HOME/Desktop/igneum-windows-app.zip}"
REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}"
ENGINE="${IGNEUM_APP_EXE:-$ROOT/app/igneum-app/target/x86_64-pc-windows-gnu/release/igneum-app.exe}"
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
STAGE="$HERE/igneum-windows-app"
TOKEN_FILE="$HOME/.config/igneum/dl-token"
[ -f "$ENGINE" ] || { echo "no $ENGINE: build it first (cd app/igneum-app && cargo build --release --target x86_64-pc-windows-gnu, see proto-cuda/windows-node/cross-build.sh for the environment)" >&2; exit 1; }
[ -f "$REL/igneumd.exe" ] || { echo "no $REL/igneumd.exe; cross-compile the node first" >&2; exit 1; }
[ -f "$REL/igneum-miner.exe" ] || { echo "no $REL/igneum-miner.exe; cross-compile the miner first" >&2; exit 1; }
rm -rf "$STAGE"
mkdir -p "$STAGE/proto-cuda/packs" "$STAGE/proto-opencl" "$STAGE/app/windows"
cp "$ENGINE" "$STAGE/igneum-app.exe"
cp "$REL/igneumd.exe" "$STAGE/igneumd.exe"
cp "$REL/igneum-miner.exe" "$STAGE/igneum-miner.exe"
for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do
name="$(basename "$dll")"
if [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true
else echo "note: $name not found in the mingw toolchain (the node exe is linked -static, so it may not need it)"; fi
done
# the prebuilt one-click workers, from where proto-cuda/windows-app/make-package.sh takes them: the NVRTC CUDA worker
# with NVIDIA's redistributable DLLs and licence texts, and the OpenCL worker (IGNEUM_WORKERS_DIR overrides with a flat folder)
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
found_workers=0
if [ -n "${IGNEUM_WORKERS_DIR:-}" ] && [ -d "$IGNEUM_WORKERS_DIR" ]; then
for f in "$IGNEUM_WORKERS_DIR"/igneum-worker-*.exe "$IGNEUM_WORKERS_DIR"/nvrtc*.dll "$IGNEUM_WORKERS_DIR"/LICENSE-*.txt "$IGNEUM_WORKERS_DIR"/THIRD-PARTY.md; do
[ -f "$f" ] && { cp "$f" "$STAGE/"; found_workers=1; }
done
else
if [ -f "$NVRTC_DIR/igneum-worker-cuda.exe" ]; then
cp "$NVRTC_DIR/igneum-worker-cuda.exe" "$STAGE/"; found_workers=1
for f in "$NVRTC_DIR"/redist/bin/nvrtc*.dll "$NVRTC_DIR"/redist/LICENSE-*.txt "$NVRTC_DIR/THIRD-PARTY.md"; do [ -f "$f" ] && cp "$f" "$STAGE/"; done
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh); the engine will not use it"
fi
if [ -f "$ROOT/proto-opencl/igneum-worker-opencl.exe" ]; then cp "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$STAGE/"; found_workers=1; fi
fi
if [ "$found_workers" = 1 ]; then echo "workers: $(cd "$STAGE" && ls igneum-worker-*.exe nvrtc*.dll 2>/dev/null | tr '\n' ' ')"
else echo "note: no prebuilt igneum-worker-cuda.exe / igneum-worker-opencl.exe found; the engine builds the CUDA worker from proto-cuda\\ on the PC (CUDA Toolkit and MSVC needed)"; fi
# the worker sources for the fallback build
cp "$ROOT/proto-cuda/host.cu" "$ROOT/proto-cuda/build.bat" "$ROOT/proto-cuda/README.md" "$STAGE/proto-cuda/"
cp "$ROOT/proto-opencl/host.c" "$ROOT/proto-opencl/build.bat" "$ROOT/proto-opencl/README.md" "$STAGE/proto-opencl/"
# the window host sources, built on the PC
cp "$ROOT/app/windows/host.cpp" "$ROOT/app/windows/host.rc" "$ROOT/app/windows/BUILD-APP.bat" "$STAGE/app/windows/"
mkdir -p "$STAGE/app/windows/art" && cp "$ROOT/brand/icons/igneum.ico" "$STAGE/app/windows/art/"
cp "$HERE/stop-igneum.ps1" "$STAGE/stop-igneum.ps1"
# the packaged configuration: the download token stays out of the repo
TOKEN=""
[ -f "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
MANIFEST=""
[ -n "$TOKEN" ] && MANIFEST="https://dl.igneum.network/dl/$TOKEN/igneum-app-latest.json"
[ -n "$MANIFEST" ] || echo "note: no $TOKEN_FILE; the update check is disabled in this build"
LOG_KEY="$(sed -n 's/^LOG_KEY="\(.*\)"/\1/p' "$ROOT/packaging/mac/packaged-config.sh")"
cat > "$STAGE/igneum-app.json" <<JSON
{
"update_manifest": "$MANIFEST",
"log_intake_url": "https://igneum-six.vercel.app/api/log",
"log_intake_key": "$LOG_KEY",
"live_page": "https://igneum.network/live",
"download_page": "https://igneum.network/#mine"
}
JSON
cat > "$STAGE/README.txt" <<TXT
Igneum Miner $VERSION for Windows (payload). Devnet v4. Test network; nothing is bought or sold.
Nobody from Igneum will ever ask for your seed.
This folder is what the installer packs. To build the installer on a PC:
1. app\\windows\\BUILD-APP.bat builds "Igneum Miner.exe" (the window; needs Visual Studio; optional)
2. packaging\\windows\\BUILD-INSTALLER.bat builds Igneum-Miner-Setup-$VERSION.exe (Inno Setup via winget)
To run without the installer: double-click igneum-app.exe (the dashboard opens in your browser).
TXT
for f in "$STAGE"/*.bat "$STAGE/README.txt" "$STAGE/stop-igneum.ps1" "$STAGE/app/windows/BUILD-APP.bat" "$STAGE/proto-cuda/build.bat" "$STAGE/proto-opencl/build.bat"; do
[ -f "$f" ] && perl -pi -e 's/\r?\n/\r\n/' "$f"
done
rm -f "$OUT"
(cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store')
echo "staged $STAGE"
ls -la "$OUT"
unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d' | head -40

View file

@ -0,0 +1,31 @@
# Stops a running Igneum Miner on this PC: asks the engine to quit through its local API (miners first, then the
# node), waits, then ends whatever is left. Run by the installer before an upgrade and on uninstall; also the
# Start Menu "Stop Igneum Miner" entry. 4 October 2026 (the app version; the 0.2.0 launcher's script is retired).
$ErrorActionPreference = 'Continue'
$urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url'
$asked = $false
if (Test-Path $urlFile) {
$url = (Get-Content $urlFile -Raw).Trim()
if ($url) {
try {
Invoke-WebRequest -Uri ($url + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null
$asked = $true
Write-Host 'asked the engine to stop (miners first, then the node)'
} catch { }
}
}
if ($asked) {
$deadline = (Get-Date).AddSeconds(50)
while ((Get-Date) -lt $deadline) {
$alive = @(Get-Process -Name 'igneum-app', 'igneumd' -ErrorAction SilentlyContinue)
if ($alive.Count -eq 0) { break }
Start-Sleep -Milliseconds 500
}
}
foreach ($name in @('Igneum Miner', 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-bench-cuda-devnet', 'igneum-bench-cl-devnet', 'igneumd')) {
Get-Process -Name $name -ErrorAction SilentlyContinue | ForEach-Object {
Write-Host "ending $($_.ProcessName) (pid $($_.Id))"
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
}
Write-Host 'Igneum Miner is stopped.'

View file

@ -1,9 +0,0 @@
@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

@ -1,5 +1,6 @@
@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"
@echo off
rem Stop Igneum Miner: the Start Menu entry. Asks the engine to stop (miner first, then the node), then ends what is left.
title Stop Igneum Miner
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0stop-igneum.ps1"
pause