From 146792ffe892be4ef9b52510d1e58481a9cdd1eb Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Thu, 5 Feb 2026 17:04:18 -0500 Subject: [PATCH 1/7] Added mac support. --- build.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/build.rs b/build.rs index 6474163..09d4796 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,11 @@ include!("src/versions.rs"); fn download_filename() -> String { format!("electrum-{}-x86_64.AppImage", &VERSION) } + +#[cfg(all(target_os = "macos"))] +fn download_filename() -> String { + format!("electrum-{}.dmg", &VERSION) +} // other platforms are currently unsupported fn get_expected_sha256() -> Result { @@ -33,6 +38,7 @@ fn main() { if !download_dir.exists() { fs::create_dir_all(&download_dir).unwrap(); } + let filepath = download_dir.join("electrum.AppImage"); if !filepath.exists() { @@ -53,9 +59,66 @@ fn main() { .read_to_end(&mut downloaded_bytes) .unwrap(); - let downloaded_hash = sha256::Hash::hash(&downloaded_bytes); - assert_eq!(expected_hash, downloaded_hash); - fs::write(&filepath, downloaded_bytes).unwrap(); + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + { + let downloaded_hash = sha256::Hash::hash(&downloaded_bytes); + assert_eq!(expected_hash, downloaded_hash); + fs::write(&filepath, downloaded_bytes).unwrap(); + } + + #[cfg(target_os = "macos")] + { + let dmg_path = download_dir.join("electrum.dmg"); + fs::write(&dmg_path, &downloaded_bytes).unwrap(); + + // Mount the DMG (use -nobrowse to keep it out of Finder) + let output = std::process::Command::new("hdiutil") + .args(["attach", dmg_path.to_str().unwrap(), "-nobrowse"]) + .output() + .expect("failed to run hdiutil attach"); + assert!( + output.status.success(), + "hdiutil attach failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Parse mount point from hdiutil output (last column of last line) + let stdout = String::from_utf8_lossy(&output.stdout); + let mount_point = stdout + .lines() + .last() + .and_then(|line| line.split('\t').last()) + .map(|s| s.trim()) + .expect("failed to parse mount point from hdiutil output"); + + // Copy the entire Electrum.app bundle out of the DMG + let app_dest = download_dir.join("Electrum.app"); + let app_src = Path::new(mount_point).join("Electrum.app"); + let cp_output = std::process::Command::new("cp") + .args(["-R", app_src.to_str().unwrap(), app_dest.to_str().unwrap()]) + .output() + .expect("failed to copy Electrum.app"); + assert!( + cp_output.status.success(), + "cp -R failed: {}", + String::from_utf8_lossy(&cp_output.stderr) + ); + + // Symlink electrum.AppImage -> the executable inside the app bundle + std::os::unix::fs::symlink( + Path::new("Electrum.app").join("Contents").join("MacOS").join("run_electrum"), + &filepath, + ) + .expect("failed to create symlink to run_electrum"); + + // Detach the mounted DMG + let _ = std::process::Command::new("hdiutil") + .args(["detach", mount_point]) + .output(); + + // Clean up the DMG file + let _ = fs::remove_file(&dmg_path); + } // chmod +x let mut perms = fs::metadata(&filepath).unwrap().permissions(); From 99afb24a4ca6becf739b6f98a9d0301ea94f93cd Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 1 Sep 2026 20:58:41 +0000 Subject: [PATCH 2/7] Fix Clippy warning for redundant cfg expression --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 09d4796..36a9802 100644 --- a/build.rs +++ b/build.rs @@ -12,7 +12,7 @@ fn download_filename() -> String { format!("electrum-{}-x86_64.AppImage", &VERSION) } -#[cfg(all(target_os = "macos"))] +#[cfg(target_os = "macos")] fn download_filename() -> String { format!("electrum-{}.dmg", &VERSION) } From 5b6f2139386bfc877534d20d14b36fa310a04c91 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 1 Sep 2026 09:04:27 +0000 Subject: [PATCH 3/7] Use platform-specific Electrum executable paths And remove the macOS symlink hack. --- .github/workflows/test.yml | 2 +- build.rs | 11 ++--------- src/lib.rs | 5 +++-- src/versions.rs | 20 ++++++++++++++++++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f84d781..317b78b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: with: command: test args: --features ${{ matrix.feature }} - - run: echo "ELECTRUMD_EXE=$(find ./target/debug -name electrum.AppImage)" >> $GITHUB_ENV + - run: echo "ELECTRUMD_EXE=$(find ./target/debug \( -name electrum.AppImage -o -name run_electrum \))" >> $GITHUB_ENV - uses: actions-rs/cargo@v1 with: command: test diff --git a/build.rs b/build.rs index 36a9802..592e836 100644 --- a/build.rs +++ b/build.rs @@ -9,7 +9,7 @@ include!("src/versions.rs"); #[cfg(all(target_os = "linux", target_arch = "x86_64"))] fn download_filename() -> String { - format!("electrum-{}-x86_64.AppImage", &VERSION) + format!("electrum-{}-x86_64.AppImage", VERSION) } #[cfg(target_os = "macos")] @@ -39,7 +39,7 @@ fn main() { fs::create_dir_all(&download_dir).unwrap(); } - let filepath = download_dir.join("electrum.AppImage"); + let filepath = download_dir.join(EXE_REL_PATH); if !filepath.exists() { println!( @@ -104,13 +104,6 @@ fn main() { String::from_utf8_lossy(&cp_output.stderr) ); - // Symlink electrum.AppImage -> the executable inside the app bundle - std::os::unix::fs::symlink( - Path::new("Electrum.app").join("Contents").join("MacOS").join("run_electrum"), - &filepath, - ) - .expect("failed to create symlink to run_electrum"); - // Detach the mounted DMG let _ = std::process::Command::new("hdiutil") .args(["detach", mount_point]) diff --git a/src/lib.rs b/src/lib.rs index bb98d0f..f62ff9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -303,9 +303,10 @@ pub fn downloaded_exe_path() -> Result { Err(Error::NoFeature) } else { Ok(format!( - "{}/electrum/electrum-{}/electrum.AppImage", + "{}/electrum/electrum-{}/{}", env!("OUT_DIR"), - versions::VERSION + versions::VERSION, + versions::EXE_REL_PATH )) } } diff --git a/src/versions.rs b/src/versions.rs index 4e70881..57128d6 100644 --- a/src/versions.rs +++ b/src/versions.rs @@ -1,6 +1,22 @@ -pub const HAS_FEATURE: bool = cfg!(any(feature = "4_1_5", feature = "4_5_4", feature = "4_6_2", feature = "4_7_0")); +pub const HAS_FEATURE: bool = cfg!(any( + feature = "4_1_5", + feature = "4_5_4", + feature = "4_6_2", + feature = "4_7_0" +)); -#[cfg(not(any(feature = "4_1_5", feature = "4_5_4", feature = "4_6_2", feature = "4_7_0")))] +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] +pub const EXE_REL_PATH: &str = "electrum.AppImage"; + +#[cfg(target_os = "macos")] +pub const EXE_REL_PATH: &str = "Electrum.app/Contents/MacOS/run_electrum"; + +#[cfg(not(any( + feature = "4_1_5", + feature = "4_5_4", + feature = "4_6_2", + feature = "4_7_0" +)))] pub const VERSION: &str = "N/A"; #[cfg(feature = "4_1_5")] From c0de696ff2ae38929d687d37378722100fdac5e4 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 1 Sep 2026 08:23:33 +0000 Subject: [PATCH 4/7] Verify SHA256 checksums for macOS DMGs --- build.rs | 11 ++++++----- ...56SUM => electrum-4.1.5-x86_64.AppImage-SHA256SUM} | 0 sha256/electrum-4.1.5.dmg-SHA256SUM | 1 + ...56SUM => electrum-4.5.4-x86_64.AppImage-SHA256SUM} | 0 sha256/electrum-4.5.4.dmg-SHA256SUM | 1 + ...56SUM => electrum-4.6.2-x86_64.AppImage-SHA256SUM} | 0 sha256/electrum-4.6.2.dmg-SHA256SUM | 1 + ...56SUM => electrum-4.7.0-x86_64.AppImage-SHA256SUM} | 0 sha256/electrum-4.7.0.dmg-SHA256SUM | 1 + 9 files changed, 10 insertions(+), 5 deletions(-) rename sha256/{electrum-4.1.5-SHA256SUM => electrum-4.1.5-x86_64.AppImage-SHA256SUM} (100%) create mode 100644 sha256/electrum-4.1.5.dmg-SHA256SUM rename sha256/{electrum-4.5.4-SHA256SUM => electrum-4.5.4-x86_64.AppImage-SHA256SUM} (100%) create mode 100644 sha256/electrum-4.5.4.dmg-SHA256SUM rename sha256/{electrum-4.6.2-SHA256SUM => electrum-4.6.2-x86_64.AppImage-SHA256SUM} (100%) create mode 100644 sha256/electrum-4.6.2.dmg-SHA256SUM rename sha256/{electrum-4.7.0-SHA256SUM => electrum-4.7.0-x86_64.AppImage-SHA256SUM} (100%) create mode 100644 sha256/electrum-4.7.0.dmg-SHA256SUM diff --git a/build.rs b/build.rs index 592e836..357b774 100644 --- a/build.rs +++ b/build.rs @@ -18,8 +18,8 @@ fn download_filename() -> String { } // other platforms are currently unsupported -fn get_expected_sha256() -> Result { - let sha256sum_filename = format!("sha256/electrum-{}-SHA256SUM", &VERSION); +fn get_expected_sha256(download_filename: &str) -> Result { + let sha256sum_filename = format!("sha256/{}-SHA256SUM", download_filename); let contents = fs::read_to_string(sha256sum_filename).expect("SHA256SUM file to exists"); let hash = sha256::Hash::from_str(&contents).expect("SHA256SUM file to be valid"); Ok(hash) @@ -30,7 +30,7 @@ fn main() { return; } let download_filename = download_filename(); - let expected_hash = get_expected_sha256().unwrap(); + let expected_hash = get_expected_sha256(&download_filename).unwrap(); let out_dir = std::env::var_os("OUT_DIR").unwrap(); let download_dir = Path::new(&out_dir) .join("electrum") @@ -59,10 +59,11 @@ fn main() { .read_to_end(&mut downloaded_bytes) .unwrap(); + let downloaded_hash = sha256::Hash::hash(&downloaded_bytes); + assert_eq!(expected_hash, downloaded_hash); + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] { - let downloaded_hash = sha256::Hash::hash(&downloaded_bytes); - assert_eq!(expected_hash, downloaded_hash); fs::write(&filepath, downloaded_bytes).unwrap(); } diff --git a/sha256/electrum-4.1.5-SHA256SUM b/sha256/electrum-4.1.5-x86_64.AppImage-SHA256SUM similarity index 100% rename from sha256/electrum-4.1.5-SHA256SUM rename to sha256/electrum-4.1.5-x86_64.AppImage-SHA256SUM diff --git a/sha256/electrum-4.1.5.dmg-SHA256SUM b/sha256/electrum-4.1.5.dmg-SHA256SUM new file mode 100644 index 0000000..bb884af --- /dev/null +++ b/sha256/electrum-4.1.5.dmg-SHA256SUM @@ -0,0 +1 @@ +cbc98ceb3aad57e2d48ed7418d2952d0bd283749e0d27a0d3a37b959d31ac8d0 \ No newline at end of file diff --git a/sha256/electrum-4.5.4-SHA256SUM b/sha256/electrum-4.5.4-x86_64.AppImage-SHA256SUM similarity index 100% rename from sha256/electrum-4.5.4-SHA256SUM rename to sha256/electrum-4.5.4-x86_64.AppImage-SHA256SUM diff --git a/sha256/electrum-4.5.4.dmg-SHA256SUM b/sha256/electrum-4.5.4.dmg-SHA256SUM new file mode 100644 index 0000000..4a0762e --- /dev/null +++ b/sha256/electrum-4.5.4.dmg-SHA256SUM @@ -0,0 +1 @@ +767ec1917600f90d70645eb110e82b3912a89532006362bf8174dc5f0b9400d6 \ No newline at end of file diff --git a/sha256/electrum-4.6.2-SHA256SUM b/sha256/electrum-4.6.2-x86_64.AppImage-SHA256SUM similarity index 100% rename from sha256/electrum-4.6.2-SHA256SUM rename to sha256/electrum-4.6.2-x86_64.AppImage-SHA256SUM diff --git a/sha256/electrum-4.6.2.dmg-SHA256SUM b/sha256/electrum-4.6.2.dmg-SHA256SUM new file mode 100644 index 0000000..16e5872 --- /dev/null +++ b/sha256/electrum-4.6.2.dmg-SHA256SUM @@ -0,0 +1 @@ +c0cb271e9de5915d3bff740385efc6676cc802977a8218bbb3a5a83152d159fc \ No newline at end of file diff --git a/sha256/electrum-4.7.0-SHA256SUM b/sha256/electrum-4.7.0-x86_64.AppImage-SHA256SUM similarity index 100% rename from sha256/electrum-4.7.0-SHA256SUM rename to sha256/electrum-4.7.0-x86_64.AppImage-SHA256SUM diff --git a/sha256/electrum-4.7.0.dmg-SHA256SUM b/sha256/electrum-4.7.0.dmg-SHA256SUM new file mode 100644 index 0000000..492b195 --- /dev/null +++ b/sha256/electrum-4.7.0.dmg-SHA256SUM @@ -0,0 +1 @@ +7f7923abf8cac06ee33bc6f5c7c8aa2593cf211f098243dbd680aab45064fdc9 \ No newline at end of file From bd74a0daaeca143684b0c24578473234590fe7f1 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 1 Sep 2026 08:28:13 +0000 Subject: [PATCH 5/7] Detach mounted DMGs on build failures Also, mount images at an explicit path instead of parsing hdiutil output. --- build.rs | 50 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/build.rs b/build.rs index 357b774..5e684c6 100644 --- a/build.rs +++ b/build.rs @@ -3,6 +3,8 @@ use std::fs; use std::io::Read; use std::os::unix::fs::PermissionsExt; use std::path::Path; +#[cfg(target_os = "macos")] +use std::path::PathBuf; use std::str::FromStr; include!("src/versions.rs"); @@ -70,11 +72,17 @@ fn main() { #[cfg(target_os = "macos")] { let dmg_path = download_dir.join("electrum.dmg"); + let mount_point = download_dir.join("mounted-dmg"); fs::write(&dmg_path, &downloaded_bytes).unwrap(); + fs::create_dir_all(&mount_point).unwrap(); // Mount the DMG (use -nobrowse to keep it out of Finder) let output = std::process::Command::new("hdiutil") - .args(["attach", dmg_path.to_str().unwrap(), "-nobrowse"]) + .arg("attach") + .arg(&dmg_path) + .arg("-mountpoint") + .arg(&mount_point) + .arg("-nobrowse") .output() .expect("failed to run hdiutil attach"); assert!( @@ -82,19 +90,11 @@ fn main() { "hdiutil attach failed: {}", String::from_utf8_lossy(&output.stderr) ); - - // Parse mount point from hdiutil output (last column of last line) - let stdout = String::from_utf8_lossy(&output.stdout); - let mount_point = stdout - .lines() - .last() - .and_then(|line| line.split('\t').last()) - .map(|s| s.trim()) - .expect("failed to parse mount point from hdiutil output"); + let mounted_dmg = MountedDmg(mount_point); // Copy the entire Electrum.app bundle out of the DMG let app_dest = download_dir.join("Electrum.app"); - let app_src = Path::new(mount_point).join("Electrum.app"); + let app_src = mounted_dmg.0.join("Electrum.app"); let cp_output = std::process::Command::new("cp") .args(["-R", app_src.to_str().unwrap(), app_dest.to_str().unwrap()]) .output() @@ -105,10 +105,7 @@ fn main() { String::from_utf8_lossy(&cp_output.stderr) ); - // Detach the mounted DMG - let _ = std::process::Command::new("hdiutil") - .args(["detach", mount_point]) - .output(); + drop(mounted_dmg); // Clean up the DMG file let _ = fs::remove_file(&dmg_path); @@ -120,3 +117,26 @@ fn main() { fs::set_permissions(&filepath, perms).unwrap(); } } + +#[cfg(target_os = "macos")] +struct MountedDmg(PathBuf); + +#[cfg(target_os = "macos")] +impl Drop for MountedDmg { + fn drop(&mut self) { + match std::process::Command::new("hdiutil") + .arg("detach") + .arg(&self.0) + .output() + { + Ok(output) if output.status.success() => { + let _ = fs::remove_dir(&self.0); + } + Ok(output) => eprintln!( + "hdiutil detach failed during cleanup: {}", + String::from_utf8_lossy(&output.stderr) + ), + Err(error) => eprintln!("failed to run hdiutil detach during cleanup: {}", error), + } + } +} From 4f3efa7c2902e3f208b66daca22732cbea0a41c7 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 1 Sep 2026 08:23:45 +0000 Subject: [PATCH 6/7] Extend CI tests to cover macOS --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 317b78b..9ec47ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test -on: [pull_request] +on: [push, pull_request] env: CARGO_TERM_COLOR: always @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-20.04 ] + os: [ ubuntu-20.04, macos-15-intel ] feature: [ "4_1_5", "4_5_4", "4_6_2", "4_7_0" ] steps: @@ -23,13 +23,13 @@ jobs: - uses: Swatinem/rust-cache@v1.2.0 with: key: ${{ matrix.feature }}${{ matrix.os }} - if: ${{ matrix.os != 'macos-10.15' }} # issue with hard-links on mac + if: ${{ matrix.os != 'macos-15-intel' }} # issue with hard-links on mac - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - run: echo "TEMPDIR_ROOT=/dev/shm" >> $GITHUB_ENV - if: ${{ matrix.os != 'macos-10.15' }} + if: ${{ matrix.os != 'macos-15-intel' }} - uses: actions-rs/cargo@v1 with: command: test From ac641027cf082c3f6af30a149e12b191c80c3d4b Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Wed, 2 Sep 2026 07:29:45 +0000 Subject: [PATCH 7/7] Update CI to use Ubuntu 24.04 and Rust 1.88 --- .github/workflows/test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ec47ec..0685169 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-20.04, macos-15-intel ] + os: [ ubuntu-24.04, macos-15-intel ] feature: [ "4_1_5", "4_5_4", "4_6_2", "4_7_0" ] steps: @@ -28,7 +28,9 @@ jobs: with: toolchain: stable override: true - - run: echo "TEMPDIR_ROOT=/dev/shm" >> $GITHUB_ENV + - run: | + echo "TEMPDIR_ROOT=/dev/shm" >> $GITHUB_ENV + echo "APPIMAGE_EXTRACT_AND_RUN=1" >> $GITHUB_ENV if: ${{ matrix.os != 'macos-15-intel' }} - uses: actions-rs/cargo@v1 with: @@ -41,7 +43,7 @@ jobs: cosmetics: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1.2.0 @@ -57,11 +59,11 @@ jobs: run: cargo clippy -- -D warnings rust-version: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - toolchain: [ "1.70", "stable", "nightly" ] + toolchain: [ "1.88", "stable", "nightly" ] steps: - uses: actions/checkout@v2