diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95200ca6..a22cf762 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -128,7 +128,10 @@ jobs: build-nix: needs: ["check", "test"] - runs-on: macos-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v6 with: diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index f9088845..2cdb5173 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -1850,8 +1850,7 @@ dependencies = [ [[package]] name = "dioxus-sdk-window" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b12fdce3ba6233cc8a39decd5850c5ac2add9246e95c768786bbf86036421c0" +source = "git+https://github.com/iynaix/dioxus-sdk?rev=28fc2264#28fc22642f726b98ac89c92dd3813a8d51e3a33c" dependencies = [ "dioxus", "dioxus-config-macro", @@ -5460,7 +5459,9 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.2", + "pollster", "raw-window-handle 0.6.2", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6770,6 +6771,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 593eb789..3425ee7d 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -29,7 +29,7 @@ open = "5.3.2" parking_lot = "0.12" percent-encoding = "2.3" pulldown-cmark = "0.13.0" -rfd = { version = "0.15.4", default-features = false, features = ["tokio"] } +rfd = { version = "0.15.4", default-features = false, features = ["tokio", "xdg-portal"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" @@ -70,3 +70,8 @@ inherits = "dev" [profile.android-dev] inherits = "dev" + +[patch.crates-io] +# Fix overlapping cfg conditions that cause duplicate function definitions on Linux. +# https://github.com/DioxusLabs/sdk/pull/103 +dioxus-sdk-window = { git = "https://github.com/iynaix/dioxus-sdk", rev = "28fc2264" } diff --git a/desktop/Dioxus.toml b/desktop/Dioxus.toml index b3ec6e9f..58e3a801 100644 --- a/desktop/Dioxus.toml +++ b/desktop/Dioxus.toml @@ -5,7 +5,7 @@ default_platform = "desktop" [bundle] identifier = "com.lambdalisue.Arto" publisher = "lambdalisue" -icon = ["../extras/mac/arto-app.icns"] +icon = ["../extras/mac/arto-app.icns", "assets/arto-app.png"] copyright = "Copyright 2025 lambdalisue" category = "Utility" short_description = "A GitHub Markdown viewer" diff --git a/desktop/justfile b/desktop/justfile index 4b7ab3ed..25d42acb 100644 --- a/desktop/justfile +++ b/desktop/justfile @@ -18,15 +18,27 @@ verify: fmt check test clean: cargo clean +[macos] build: @rm -rf target/dx/arto/release/macos/Arto.app/Contents/Resources/assets @rm -rf target/dx/arto/bundle/macos/bundle/macos/Arto.app/Contents/Resources/assets dx bundle --release --macos +[linux] +build: + @rm -rf target/dx/arto/release/linux/app/assets + dx bundle --release --linux --package-types deb + +[macos] open: ./target/dx/arto/bundle/macos/bundle/macos/Arto.app/Contents/MacOS/arto +[linux] +open: + ./target/dx/arto/release/linux/app/arto + [confirm] +[macos] install: @rm -rf /Applications/Arto.app @cp -af target/dx/arto/bundle/macos/bundle/macos/Arto.app /Applications/. diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 826b8753..cd86d826 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -159,6 +159,7 @@ fn init_tracing() { .with(fmt_layer); // On macOS, log to Console.app via oslog + #[cfg(target_os = "macos")] let registry = registry.with( tracing_oslog::OsLogger::new("com.lambdalisue.Arto", "default").with_filter(silence_filter), ); diff --git a/desktop/src/menu.rs b/desktop/src/menu.rs index c15d5018..04828ec6 100644 --- a/desktop/src/menu.rs +++ b/desktop/src/menu.rs @@ -152,6 +152,7 @@ fn menu_action_for_id(id: MenuId) -> Option<&'static str> { /// Build the application menu bar pub fn build_menu() -> Menu { + #[cfg(target_os = "macos")] disable_automatic_window_tabbing(); let menu = Menu::new(); @@ -511,6 +512,7 @@ fn pick_directory() -> Option { dir } +#[cfg(target_os = "macos")] fn disable_automatic_window_tabbing() { use objc2::MainThreadMarker; use objc2_app_kit::NSWindow; diff --git a/desktop/src/theme.rs b/desktop/src/theme.rs index 0a730972..b6d77bb7 100644 --- a/desktop/src/theme.rs +++ b/desktop/src/theme.rs @@ -25,10 +25,17 @@ pub fn resolve_theme(theme: Theme) -> DioxusTheme { // We cannot use dioxus_sdk_window::theme::get_theme here because // it requires a Dioxus runtime and cannot be called from outside // of Dioxus context. That's why we use dark_light crate instead. - Theme::Auto => match dark_light::detect() { - Ok(dark_light::Mode::Light) => DioxusTheme::Light, - Ok(dark_light::Mode::Dark) => DioxusTheme::Dark, - Ok(dark_light::Mode::Unspecified) | Err(_) => DioxusTheme::Light, + // On Linux, dark_light uses D-Bus (zbus) which requires a Tokio + // runtime. This function may be called before the runtime starts + // (e.g. from build_custom_index in main()), so catch_unwind + // prevents the panic and falls back to Light. + Theme::Auto => match std::panic::catch_unwind(dark_light::detect) + .ok() + .and_then(|r| r.ok()) + { + Some(dark_light::Mode::Light) => DioxusTheme::Light, + Some(dark_light::Mode::Dark) => DioxusTheme::Dark, + Some(dark_light::Mode::Unspecified) | None => DioxusTheme::Light, }, Theme::Light => DioxusTheme::Light, Theme::Dark => DioxusTheme::Dark, diff --git a/desktop/src/utils/file_operations.rs b/desktop/src/utils/file_operations.rs index 1c77ead0..c3d5aaae 100644 --- a/desktop/src/utils/file_operations.rs +++ b/desktop/src/utils/file_operations.rs @@ -1,4 +1,6 @@ use std::path::Path; + +#[cfg(target_os = "macos")] use std::process::Command; /// Reveal a file in Finder (macOS) or file explorer diff --git a/flake.nix b/flake.nix index 8d001b6f..29a72386 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,9 @@ let systems = [ "aarch64-darwin" + "aarch64-linux" "x86_64-darwin" + "x86_64-linux" ]; eachSystem = nixpkgs.lib.genAttrs systems; in @@ -46,11 +48,18 @@ # Platform detection isDarwin = pkgs.stdenv.hostPlatform.isDarwin; + isLinux = pkgs.stdenv.hostPlatform.isLinux; # App bundle paths (used in build and apps) appBundleName = "Arto.app"; appExecutableName = "arto"; # lowercase executable name - dxBundlePath = "target/dx/${packageMeta.pname}/bundle/macos/bundle/macos"; + dxBundlePath = + if isDarwin then + "target/dx/${packageMeta.pname}/bundle/macos/bundle/macos" + # dx build (not bundle) outputs here; bundle fails in Nix sandbox + # due to permission errors in the .deb/.AppImage packagers. + else + "target/dx/${packageMeta.pname}/release/linux/app"; renderer-assets = pkgs.stdenvNoCC.mkDerivation (finalAttrs: { pname = "${packageMeta.pname}-renderer-assets"; @@ -104,9 +113,21 @@ strictDeps = true; # Pass version to build.rs via environment variable ARTO_BUILD_VERSION = artoVersion; - buildInputs = lib.optionals isDarwin [ - pkgs.libiconv + nativeBuildInputs = lib.optionals isLinux [ + pkgs.pkg-config ]; + buildInputs = + lib.optionals isDarwin [ + pkgs.libiconv + ] + ++ lib.optionals isLinux [ + pkgs.webkitgtk_4_1 + pkgs.gtk3 + pkgs.libsoup_3 + pkgs.glib + pkgs.openssl + pkgs.xdotool + ]; }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; @@ -145,6 +166,10 @@ ] ++ lib.optionals isDarwin [ pkgs.darwin.autoSignDarwinBinariesHook + ] + ++ lib.optionals isLinux [ + pkgs.pkg-config + pkgs.wrapGAppsHook3 ]; postPatch = '' @@ -160,12 +185,19 @@ # Use buildPhaseCargoCommand instead of cargoBuildCommand because crane's # additional build argument `--message-format` cannot be passed to dioxus-cli properly. # https://crane.dev/API.html#cranelibbuildpackage - buildPhaseCargoCommand = '' - dx bundle --release --platform desktop --package-types macos - ''; - - # The build output is a macOS .app bundle, and crane cannot infer the install - # destination, so we manually install without capturing cargoBuildLog in buildPhase. + buildPhaseCargoCommand = + if isDarwin then + '' + dx bundle --release --platform desktop --package-types macos + '' + else + '' + dx build --release --platform desktop + ''; + + # The build output is a platform-specific bundle, and crane cannot infer the + # install destination, so we manually install without capturing cargoBuildLog + # in buildPhase. # https://crane.dev/API.html#cranelibinstallfromcargobuildloghook doNotPostBuildInstallCargoBinaries = true; @@ -186,6 +218,24 @@ # Create symlink for CLI usage (enables `arto` command in PATH) mkdir -p $out/bin ln -s "$out/Applications/${appBundleName}/Contents/MacOS/${appExecutableName}" "$out/bin/${appExecutableName}" + '' + + lib.optionalString isLinux '' + app_dir="${dxBundlePath}" + + if [[ ! -d "$app_dir" ]]; then + echo "Error: Expected build output not found at $app_dir" + echo "Searching for build output in target/dx..." + find target/dx -type d 2>/dev/null || true + exit 1 + fi + + # Install the entire app directory (binary + assets) since + # Dioxus asset!() macro resolves paths relative to the binary. + mkdir -p $out/lib/${appExecutableName} + cp -r "$app_dir"/. $out/lib/${appExecutableName}/ + + mkdir -p $out/bin + ln -s $out/lib/${appExecutableName}/${appExecutableName} $out/bin/${appExecutableName} ''; } ); @@ -201,13 +251,18 @@ let # Access packageMeta from packages let-binding inherit (self.packages.${system}) arto; + pkgs = nixpkgs.legacyPackages.${system}; appBundleName = "Arto.app"; appExecutableName = "arto"; in { default = { type = "app"; - program = "${arto}/Applications/${appBundleName}/Contents/MacOS/${appExecutableName}"; + program = + if pkgs.stdenv.hostPlatform.isDarwin then + "${arto}/Applications/${appBundleName}/Contents/MacOS/${appExecutableName}" + else + "${arto}/bin/${appExecutableName}"; }; } ); @@ -237,6 +292,15 @@ ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [ pkgs.libiconv + ] + ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.pkg-config + pkgs.webkitgtk_4_1 + pkgs.gtk3 + pkgs.libsoup_3 + pkgs.glib + pkgs.openssl + pkgs.xdotool ]; # Workaround: Nix sets DEVELOPER_DIR to its apple-sdk, which breaks `just build` dmg creation. diff --git a/justfile b/justfile index 8f742414..12b348bd 100644 --- a/justfile +++ b/justfile @@ -24,4 +24,5 @@ build: renderer::assets desktop::build open: desktop::open +[macos] install: desktop::install