diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72b74423..48079012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,24 @@ jobs: - name: Test run: cargo xtask test + - name: Check embedded executable icon + shell: pwsh + run: | + cargo build --release --locked -p disktree-app + Add-Type @' + using System; + using System.Runtime.InteropServices; + public static class ExecutableIcons { + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + public static extern uint ExtractIconEx( + string file, int index, IntPtr[] large, IntPtr[] small, uint count); + } + '@ + $exe = (Resolve-Path target/release/disktree.exe).Path + if ([ExecutableIcons]::ExtractIconEx($exe, -1, $null, $null, 0) -lt 1) { + throw "disktree.exe has no embedded icon" + } + # The other half of each release: the same tests on the architectures # the release builds for but the jobs above do not run on. other-architectures: diff --git a/assets/disktree.ico b/assets/disktree.ico new file mode 100644 index 00000000..77ca68b3 Binary files /dev/null and b/assets/disktree.ico differ diff --git a/crates/disktree-app/build.rs b/crates/disktree-app/build.rs new file mode 100644 index 00000000..b3e6868e --- /dev/null +++ b/crates/disktree-app/build.rs @@ -0,0 +1,65 @@ +#[cfg(windows)] +fn main() { + use std::{env, fs, path::PathBuf, process::Command}; + + let icon = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("../../assets/disktree.ico") + .canonicalize() + .expect("Windows icon asset is missing"); + let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let rc = out.join("disktree.rc"); + let res = out.join("disktree.res"); + + // rc.exe treats backslashes inside quoted paths as separators, not escapes. + let icon_path = icon.to_string_lossy().replace('\\', "/"); + fs::write(&rc, format!("1 ICON \"{icon_path}\"\n")) + .expect("could not write Windows icon resource script"); + + // Cargo can run outside a Visual Studio prompt, where the SDK bin is not + // on PATH even though the MSVC linker and Windows SDK are installed. + let rc_exe = env::var_os("DISKTREE_RC").map_or_else( + || { + let sdk_bin = env::var_os("WindowsSdkDir") + .map(PathBuf::from) + .or_else(|| { + env::var_os("ProgramFiles(x86)") + .map(|root| PathBuf::from(root).join("Windows Kits/10")) + }) + .expect("Windows SDK is required to embed the icon") + .join("bin"); + let host = match env::consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + "x86" => "x86", + other => panic!("unsupported Windows build host: {other}"), + }; + let mut versions: Vec<_> = fs::read_dir(&sdk_bin) + .expect("Windows SDK bin directory is missing") + .filter_map(Result::ok) + .map(|entry| entry.path().join(host).join("rc.exe")) + .filter(|path| path.is_file()) + .collect(); + versions.sort(); + versions + .pop() + .expect("rc.exe is missing from the Windows SDK") + }, + PathBuf::from, + ); + let status = Command::new(rc_exe) + .arg("/nologo") + .arg(format!("/fo{}", res.display())) + .arg(&rc) + .status() + .expect("could not start rc.exe to embed the Windows icon"); + assert!( + status.success(), + "rc.exe failed to compile the Windows icon" + ); + + println!("cargo:rerun-if-changed={}", icon.display()); + println!("cargo:rustc-link-arg-bin=disktree={}", res.display()); +} + +#[cfg(not(windows))] +fn main() {}