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:
parent
cbe003e38b
commit
a7d7f5da2a
30 changed files with 1469 additions and 527 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(", ")))
|
||||
|
|
|
|||
|
|
@ -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 ¬es {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">−</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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
61
app/windows/BUILD-APP.bat
Normal 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
390
app/windows/host.cpp
Normal 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
34
app/windows/host.rc
Normal 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 |
BIN
docs/design/app-screens/mac-settings.png
Normal file
BIN
docs/design/app-screens/mac-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
27
packaging/mac/packaged-config.sh
Normal file
27
packaging/mac/packaged-config.sh
Normal 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
|
||||
}
|
||||
1
packaging/windows/.gitignore
vendored
1
packaging/windows/.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
# built outputs: the windres objects, the assembled payload, the downloaded tools, the installer
|
||||
build/
|
||||
dist/
|
||||
igneum-windows-app/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
102
packaging/windows/make-payload.sh
Executable 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
|
||||
31
packaging/windows/stop-igneum.ps1
Normal file
31
packaging/windows/stop-igneum.ps1
Normal 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.'
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue