diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..1d1b798 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +bundle-macos = "packager --release --packages bds-ui --formats app --formats dmg" +bundle-windows = "packager --release --packages bds-ui --formats nsis" +bundle-linux = "packager --release --packages bds-ui --formats deb --formats appimage" diff --git a/BUILD.md b/BUILD.md index b67d33f..0349ad5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,5 +1,25 @@ # RuDS — Build Prerequisites +## Native desktop packages + +Install the pinned Cargo Packager CLI once: + +```sh +cargo install cargo-packager --locked --version 0.11.8 +``` + +Build packages on their native operating system from the repository root: + +```sh +cargo bundle-macos # Blogging Desktop Server.app and .dmg +cargo bundle-windows # NSIS .exe installer +cargo bundle-linux # .deb and .AppImage +``` + +Packages are written below `target/release`. The macOS bundle uses the ICNS icon, Windows embeds the ICO in the application executable and installer, and Linux packages install the 1024×1024 PNG with their desktop entry. + +Windows packaging requires the MSVC Rust toolchain, Visual Studio Build Tools with C++ support, and the Windows SDK. Build each package on its target operating system; these commands do not cross-package installers. + ## macOS system requirements - macOS 13 (Ventura) or later @@ -12,7 +32,7 @@ ```sh # Debian/Ubuntu -sudo apt install build-essential cmake pkg-config libgtk-3-dev libxdo-dev libdbus-1-dev +sudo apt install build-essential cmake pkg-config libgtk-3-dev libxdo-dev libdbus-1-dev libwebkit2gtk-4.1-dev ``` ## Install Homebrew packages (macOS) diff --git a/Cargo.lock b/Cargo.lock index bc34d93..fa769e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -858,6 +858,7 @@ dependencies = [ "tempfile", "tokio", "uuid", + "winresource", "wry", ] @@ -7461,6 +7462,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -7533,6 +7549,12 @@ dependencies = [ "winnow 1.0.4", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tower" version = "0.5.3" @@ -9081,6 +9103,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml 1.1.3+spec-1.1.0", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/crates/bds-core/src/db/connection.rs b/crates/bds-core/src/db/connection.rs index 1764fd0..039dac5 100644 --- a/crates/bds-core/src/db/connection.rs +++ b/crates/bds-core/src/db/connection.rs @@ -67,7 +67,7 @@ impl Database { let mut conn = SqliteConnection::establish(database_url)?; // SQLite connection configuration is backend-specific and not expressible in Diesel's DSL. conn.batch_execute(if wal { - "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;" + "PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;" } else { "PRAGMA foreign_keys=ON;" })?; diff --git a/crates/bds-ui/Cargo.toml b/crates/bds-ui/Cargo.toml index 60acf82..c06b010 100644 --- a/crates/bds-ui/Cargo.toml +++ b/crates/bds-ui/Cargo.toml @@ -29,3 +29,24 @@ objc2-app-kit = "0.3" fluent-syntax = "0.12" syn = { version = "2", features = ["full", "visit"] } tempfile = "3" + +[build-dependencies] +winresource = "0.1" + +[package.metadata.packager] +product-name = "Blogging Desktop Server" +identifier = "de.rfc1437.bds2" +description = "A desktop application for writing and publishing static blogs." +before-packaging-command = "cargo build --release -p bds-ui" +icons = [ + "assets/app-icons/bds.icns", + "assets/app-icons/bds.ico", + "assets/app-icons/bds.png", +] + +[package.metadata.packager.macos] +minimum-system-version = "13.0" +signing-identity = "-" + +[package.metadata.packager.nsis] +installer-icon = "assets/app-icons/bds.ico" diff --git a/crates/bds-ui/assets/app-icons/bds.icns b/crates/bds-ui/assets/app-icons/bds.icns new file mode 100644 index 0000000..2459528 Binary files /dev/null and b/crates/bds-ui/assets/app-icons/bds.icns differ diff --git a/crates/bds-ui/assets/app-icons/bds.ico b/crates/bds-ui/assets/app-icons/bds.ico new file mode 100644 index 0000000..aa5ff97 Binary files /dev/null and b/crates/bds-ui/assets/app-icons/bds.ico differ diff --git a/crates/bds-ui/assets/app-icons/bds.png b/crates/bds-ui/assets/app-icons/bds.png new file mode 100644 index 0000000..bae4932 Binary files /dev/null and b/crates/bds-ui/assets/app-icons/bds.png differ diff --git a/crates/bds-ui/build.rs b/crates/bds-ui/build.rs new file mode 100644 index 0000000..d554c7c --- /dev/null +++ b/crates/bds-ui/build.rs @@ -0,0 +1,10 @@ +fn main() { + println!("cargo:rerun-if-changed=assets/app-icons/bds.ico"); + + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + winresource::WindowsResource::new() + .set_icon("assets/app-icons/bds.ico") + .compile() + .expect("compile Windows application icon"); + } +} diff --git a/crates/bds-ui/src/main.rs b/crates/bds-ui/src/main.rs index dbea182..a546183 100644 --- a/crates/bds-ui/src/main.rs +++ b/crates/bds-ui/src/main.rs @@ -1,9 +1,19 @@ +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + use bds_ui::BdsApp; fn main() -> iced::Result { + let icon = + iced::window::icon::from_file_data(include_bytes!("../assets/app-icons/bds.png"), None) + .expect("bundled application icon must be valid"); + iced::application("bDS", BdsApp::update, BdsApp::view) .subscription(BdsApp::subscription) .theme(|_| iced::Theme::Dark) - .window_size((1200.0, 800.0)) + .window(iced::window::Settings { + size: iced::Size::new(1200.0, 800.0), + icon: Some(icon), + ..Default::default() + }) .run_with(BdsApp::new) } diff --git a/crates/bds-ui/tests/packaging_assets.rs b/crates/bds-ui/tests/packaging_assets.rs new file mode 100644 index 0000000..8b08a0f --- /dev/null +++ b/crates/bds-ui/tests/packaging_assets.rs @@ -0,0 +1,41 @@ +use std::{fs, path::Path}; + +const CRATE_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +#[test] +fn desktop_packages_have_native_icons_and_cargo_commands() { + let assets = Path::new(CRATE_DIR).join("assets/app-icons"); + let png = fs::read(assets.join("bds.png")).expect("Linux PNG icon"); + let ico = fs::read(assets.join("bds.ico")).expect("Windows ICO icon"); + let icns = fs::read(assets.join("bds.icns")).expect("macOS ICNS icon"); + + assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n"); + assert_eq!(u32::from_be_bytes(png[16..20].try_into().unwrap()), 1024); + assert_eq!(u32::from_be_bytes(png[20..24].try_into().unwrap()), 1024); + assert_eq!(&ico[..4], &[0, 0, 1, 0]); + assert!(u16::from_le_bytes(ico[4..6].try_into().unwrap()) >= 6); + assert_eq!(&icns[..4], b"icns"); + + let manifest = fs::read_to_string(Path::new(CRATE_DIR).join("Cargo.toml")).unwrap(); + for required in [ + "[package.metadata.packager]", + "assets/app-icons/bds.png", + "assets/app-icons/bds.ico", + "assets/app-icons/bds.icns", + "signing-identity = \"-\"", + ] { + assert!(manifest.contains(required), "missing {required}"); + } + + let cargo_config = fs::read_to_string(Path::new(CRATE_DIR).join("../../.cargo/config.toml")) + .expect("Cargo packaging aliases"); + for alias in ["bundle-macos", "bundle-windows", "bundle-linux"] { + assert!(cargo_config.contains(alias), "missing cargo {alias}"); + } + + let main = fs::read_to_string(Path::new(CRATE_DIR).join("src/main.rs")).unwrap(); + assert!(main.contains("assets/app-icons/bds.png")); + + let build = fs::read_to_string(Path::new(CRATE_DIR).join("build.rs")).unwrap(); + assert!(build.contains("set_icon(\"assets/app-icons/bds.ico\")")); +}