diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbdfd47..f3991dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -146,12 +146,19 @@ jobs: # --prerelease on its own only sets the flag on a still-unpublished # draft. Nothing here goes the other way: a release that is already # published is never pushed back to a draft. + # + # A draft has no tag yet: Publish makes it from the draft's target. + # Rebuilding a draft from a newer commit has to move the target + # too, or Publish tags the commit before the change while the + # binaries were built after it. + target="" + if [ "$state" = "--draft" ]; then target="--target $GITHUB_SHA"; fi if [ "${{ inputs.prerelease }}" = "true" ] && [ "$state" = "--draft" ]; then echo "release $V is a draft and a prerelease was asked for — publishing it" state="--prerelease --draft=false" fi echo "release $V exists ($state) — updating in place" - gh release edit "$V" $state --title "$V" --notes-file RELEASE_NOTES.md + gh release edit "$V" $state $target --title "$V" --notes-file RELEASE_NOTES.md # Cleared by name first: a run cancelled mid-upload leaves a # half-created asset, and the API then answers 400 for a fresh # upload of that name rather than replacing it. diff --git a/Cargo.lock b/Cargo.lock index f4d0809..f988312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -560,6 +569,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", ] @@ -1031,8 +1042,10 @@ version = "0.1.0" dependencies = [ "eframe", "egui", + "haru-core", "haru-ui", "image", + "rfd", ] [[package]] @@ -1730,7 +1743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -1746,6 +1759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "objc2 0.6.4", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -1758,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -1770,7 +1784,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -1782,7 +1796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -1817,7 +1831,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -1829,7 +1843,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -1848,7 +1862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -1882,7 +1896,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -1895,7 +1909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -1907,7 +1921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -1930,7 +1944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -1950,7 +1964,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -1962,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2287,6 +2301,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3793,7 +3828,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.13.1", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases 0.2.2", diff --git a/Cargo.toml b/Cargo.toml index bddf7cd..ba5b7cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ image = { version = "0.25", default-features = false, features = [ "png", ] } qrcode = { version = "0.14", default-features = false } +rfd = { version = "0.15", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "sync"] } diff --git a/crates/haru/Cargo.toml b/crates/haru/Cargo.toml index ea0f426..80a55bc 100644 --- a/crates/haru/Cargo.toml +++ b/crates/haru/Cargo.toml @@ -14,8 +14,14 @@ path = "src/main.rs" [dependencies] eframe = { workspace = true } egui = { workspace = true } +haru-core = { workspace = true } haru-ui = { workspace = true } image = { workspace = true } +# The message box that says why haru stopped, since a release build on +# Windows has no console to print to. +[target.'cfg(windows)'.dependencies] +rfd = { workspace = true } + [lints] workspace = true diff --git a/crates/haru/src/failure.rs b/crates/haru/src/failure.rs new file mode 100644 index 0000000..7ab4d09 --- /dev/null +++ b/crates/haru/src/failure.rs @@ -0,0 +1,122 @@ +//! Saying why haru stopped, somewhere a person will see it. +//! +//! A release build on Windows has no console, so everything haru printed on +//! the way out went nowhere: a window that could not open, or a panic, looked +//! exactly like double-clicking and nothing happening. Every way `main` gives +//! up now goes through `report`, which still prints, also appends to a log in +//! haru's data folder, and on Windows puts up a message box. + +use std::io::Write as _; +use std::path::PathBuf; +use std::process::ExitCode; +use std::sync::Mutex; + +/// The last panic, kept for `main` to report once the event loop has unwound. +static LAST_PANIC: Mutex> = Mutex::new(None); + +/// Log every panic, and keep the last one for `panicked`. +/// +/// Rust's own hook still runs first, so a console, when there is one, shows +/// what it always did. The message box is left to `main`: putting one up from +/// inside the hook would run a modal loop while winit is still unwinding. +pub fn record_panics() { + let default = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + default(info); + let thread = std::thread::current(); + let said = format!( + "haru crashed in thread {}: {info}", + thread.name().unwrap_or("unnamed") + ); + let _ = log(&said); + if let Ok(mut last) = LAST_PANIC.lock() { + *last = Some(said); + } + })); +} + +/// Report the panic that ended the event loop. +pub fn panicked() -> ExitCode { + let said = LAST_PANIC + .lock() + .ok() + .and_then(|mut last| last.take()) + .unwrap_or_else(|| "haru crashed".to_owned()); + // Already logged by the hook. + show(&said, log_path().as_deref()); + ExitCode::FAILURE +} + +/// Say why haru could not go on, everywhere it might be seen. +pub fn report(message: &str) -> ExitCode { + eprintln!("haru: {message}"); + let logged = log(message).ok(); + show(message, logged.as_deref()); + ExitCode::FAILURE +} + +/// Why eframe could not open the window, in words that point at the fix. +pub fn window_error(error: &eframe::Error) -> String { + match error { + eframe::Error::Wgpu(_) => format!( + "haru could not find a graphics driver it can draw with (Vulkan or OpenGL). \ + Updating the graphics driver usually fixes this.\n\n({error})" + ), + error => format!("haru could not open its window.\n\n({error})"), + } +} + +fn log_path() -> Option { + Some(haru_core::data_home()?.join("haru").join("haru.log")) +} + +/// Append to haru's log, returning where it went. +fn log(message: &str) -> std::io::Result { + let path = log_path().ok_or_else(|| std::io::Error::other("no data folder"))?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let seconds = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|since| since.as_secs()) + .unwrap_or(0); + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path)?; + writeln!(file, "[unix time {seconds}] {message}")?; + Ok(path) +} + +#[cfg(windows)] +fn show(message: &str, logged: Option<&std::path::Path>) { + let description = match logged { + Some(path) => format!("{message}\n\nThis was also saved to {}", path.display()), + None => message.to_owned(), + }; + let _ = rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Error) + .set_title("haru") + .set_description(description) + .set_buttons(rfd::MessageButtons::Ok) + .show(); +} + +// Elsewhere haru is started from a terminal often enough, and a desktop +// launcher keeps what it printed in the journal or Console. +#[cfg(not(windows))] +const fn show(_message: &str, _logged: Option<&std::path::Path>) {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_missing_graphics_driver_is_named_as_such() { + let said = window_error(&eframe::Error::Wgpu( + eframe::egui_wgpu::WgpuError::NoSuitableAdapterFound, + )); + assert!(said.contains("graphics driver"), "{said}"); + assert!(said.contains("no suitable adapter"), "{said}"); + } +} diff --git a/crates/haru/src/main.rs b/crates/haru/src/main.rs index 1d01713..573856b 100644 --- a/crates/haru/src/main.rs +++ b/crates/haru/src/main.rs @@ -3,6 +3,8 @@ // somewhere while developing. #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] +mod failure; + use std::process::ExitCode; use haru_ui::{Haru, Tab}; @@ -76,13 +78,12 @@ fn parse(arguments: &[String]) -> Result { } fn main() -> ExitCode { + failure::record_panics(); + let arguments: Vec = std::env::args().skip(1).collect(); let opened = match parse(&arguments) { Ok(opened) => opened, - Err(message) => { - eprintln!("haru: {message}"); - return ExitCode::FAILURE; - } + Err(message) => return failure::report(&message), }; let mut viewport = egui::ViewportBuilder::default() @@ -97,30 +98,36 @@ fn main() -> ExitCode { let options = eframe::NativeOptions { viewport, + // The backend is left to wgpu (Vulkan, then OpenGL; `WGPU_BACKEND` + // overrides it). Asking for DX12 alone found no adapter at all, + // because eframe builds wgpu without its DX12 backend, and the window + // never opened. wgpu_options: eframe::egui_wgpu::WgpuConfiguration { present_mode: present_mode(), desired_maximum_frame_latency: Some(2), - supported_backends: backends(), ..eframe::egui_wgpu::WgpuConfiguration::default() }, ..eframe::NativeOptions::default() }; - match eframe::run_native( - "haru", - options, - Box::new(move |cc| { - haru_ui::theme::apply(&cc.egui_ctx); - Ok(Box::new(App { - haru: Haru::opening_on_item(opened.tab, opened.search, opened.item), - })) - }), - ) { - Ok(()) => ExitCode::SUCCESS, - Err(error) => { - eprintln!("haru: {error}"); - ExitCode::FAILURE - } + // winit carries a panic in the event loop out of `run_native`, so catching + // it here reports it after the window is gone rather than inside it. + let ran = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + eframe::run_native( + "haru", + options, + Box::new(move |cc| { + haru_ui::theme::apply(&cc.egui_ctx); + Ok(Box::new(App { + haru: Haru::opening_on_item(opened.tab, opened.search, opened.item), + })) + }), + ) + })); + match ran { + Ok(Ok(())) => ExitCode::SUCCESS, + Ok(Err(error)) => failure::report(&failure::window_error(&error)), + Err(_) => failure::panicked(), } } @@ -135,18 +142,6 @@ fn present_mode() -> eframe::wgpu::PresentMode { } } -// Left to itself, wgpu takes the first adapter that answers among Vulkan, DX12 -// and WGL, so which renderer haru got depended on the driver, and the Vulkan -// and GL paths are where egui draws wrong on Windows. DX12 is on every Windows -// 10 machine haru supports. `WGPU_BACKEND` still overrides this for testing. -fn backends() -> eframe::wgpu::Backends { - if cfg!(windows) { - eframe::wgpu::util::backend_bits_from_env().unwrap_or(eframe::wgpu::Backends::DX12) - } else { - eframe::egui_wgpu::WgpuConfiguration::default().supported_backends - } -} - struct App { haru: Haru, }