From b960594d44a1b52d3dfb07ea2d279e1329e4c15c Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Fri, 31 Jul 2026 16:37:18 -0700 Subject: [PATCH 1/6] fix(nix): shrink desktop runtime output (#2550) Package only the traced desktop and daemon runtime graph, preserve required runtime entrypoints, and exclude the npm Electron distribution supplied by nixpkgs. --- nix/desktop-package.nix | 52 ++++++++++++++++++++++++++++++---------- scripts/trace-daemon.mjs | 25 ++++++++++++++++++- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/nix/desktop-package.nix b/nix/desktop-package.nix index cd9c6ca5c2..053fadbd09 100644 --- a/nix/desktop-package.nix +++ b/nix/desktop-package.nix @@ -5,6 +5,7 @@ nodejs_22, python3, makeWrapper, + autoPatchelfHook, copyDesktopItems, makeDesktopItem, electron, @@ -69,9 +70,15 @@ buildNpmPackage rec { python3 # for node-gyp (node-pty) makeWrapper copyDesktopItems + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + autoPatchelfHook ]; - buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ libuv ]; + buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ + libuv + stdenv.cc.cc.lib # libstdc++ for sherpa-onnx prebuilt binaries + ]; dontNpmBuild = true; @@ -108,19 +115,38 @@ buildNpmPackage rec { mkdir -p $out/share/paseo-desktop $out/bin - # Preserve the monorepo layout so main.js's dev-mode path resolution - # (`__dirname/../../app/dist`, `__dirname/../assets/icon.png`) works - # without patching: invoked unpackaged via `electron path/to/main.js`, - # `app.isPackaged` is false, so these relative paths are used. - # - # Copy the entire packages/ tree (not just built artifacts) because npm - # creates workspace symlinks from node_modules/@getpaseo/* into packages/*. - # Missing any workspace package leaves dangling symlinks and fails the - # noBrokenSymlinks output check. The cleanSourceWith filter above already - # drops the big platform-specific things (android/ios, website, tests). + # Materialize only the desktop and daemon runtime graphs. Copying the + # complete monorepo used to ship every build-time dependency (including + # Electron, Expo tooling, and cross-platform builder binaries), making the + # desktop output larger than 2 GiB. + PASEO_TRACE_DESKTOP=1 node scripts/trace-daemon.mjs > desktop-files.txt + + while IFS= read -r path; do + [ -z "$path" ] && continue + mkdir -p "$out/share/paseo-desktop/$(dirname "$path")" + cp -a "$path" "$out/share/paseo-desktop/$path" + done < desktop-files.txt + + # Keep the same unpackaged monorepo layout expected by main.js. cp package.json $out/share/paseo-desktop/ - cp -a packages $out/share/paseo-desktop/ - cp -a node_modules $out/share/paseo-desktop/ + mkdir -p $out/share/paseo-desktop/packages/app + cp -a packages/app/dist $out/share/paseo-desktop/packages/app/ + + for runtime_path in \ + packages/desktop/dist/main.js \ + packages/desktop/dist/preload.js \ + packages/desktop/dist/features/browser-keyboard/guest-preload.js \ + packages/desktop/package.json; do + if [ ! -e "$out/share/paseo-desktop/$runtime_path" ]; then + echo "desktop runtime trace omitted $runtime_path" >&2 + exit 1 + fi + done + + if [ -e $out/share/paseo-desktop/node_modules/electron ]; then + echo "desktop runtime trace included npm Electron" >&2 + exit 1 + fi # Skills directory referenced at runtime by some agents if [ -d skills ]; then diff --git a/scripts/trace-daemon.mjs b/scripts/trace-daemon.mjs index 31351e547d..a4ece424f2 100644 --- a/scripts/trace-daemon.mjs +++ b/scripts/trace-daemon.mjs @@ -28,14 +28,24 @@ const { sherpaPlatformPackageName } = await import( ).href ); +const traceDesktop = process.env.PASEO_TRACE_DESKTOP === "1"; + // Daemon entry points. Workers forked into their own Node processes have // independent require trees; nft does not follow fork boundaries, so trace -// them separately. +// them separately. The desktop derivation opts into tracing its Electron main +// process and preloads as well. const entries = [ "packages/cli/dist/index.js", "packages/server/dist/scripts/supervisor-entrypoint.js", "packages/server/dist/server/terminal/terminal-worker-process.js", "packages/server/dist/server/server/speech/providers/local/worker-process.js", + ...(traceDesktop + ? [ + "packages/desktop/dist/main.js", + "packages/desktop/dist/preload.js", + "packages/desktop/dist/features/browser-keyboard/guest-preload.js", + ] + : []), ]; // Files read at runtime via fs APIs rather than `require`. nft only @@ -59,6 +69,16 @@ const additionalInputs = [ // Copy the wrapper plus the host platform package explicitly. "node_modules/sherpa-onnx-node/**", `node_modules/${sherpaPlatformPackageName()}/**`, + ...(traceDesktop + ? [ + // The unpackaged Nix launcher resolves these beside desktop/dist. + "packages/desktop/package.json", + "packages/desktop/assets/**", + // resolveExternalCliEntrypoint() looks up the workspace through this + // link at runtime; nft traces the target files but not the link. + "node_modules/@getpaseo/cli", + ] + : []), ]; // Trace. @@ -83,6 +103,9 @@ const { fileList, warnings } = await nodeFileTrace(entries, { // tries to walk into them via index files. Belt and suspenders. "**/*.test.js", "**/*.e2e.test.js", + // The Nix desktop package runs under nixpkgs' Electron. Tracing the npm + // package would duplicate the complete Electron distribution in $out. + ...(traceDesktop ? ["node_modules/electron/**"] : []), ], }); From b151dfb88d21c659efb207ebb6d60c350e3bdb72 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Jul 2026 10:46:21 -0700 Subject: [PATCH 2/6] Add Darwin desktop package to flake --- docs/development.md | 13 +++++ flake.nix | 6 -- nix/desktop-package.nix | 120 ++++++++++++++++++++++++++++++---------- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/docs/development.md b/docs/development.md index cd96239bac..d008f38147 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,6 +21,19 @@ Root checkout dev is intentionally split across terminals: `npm run dev` is only a shorthand for `npm run dev:server`. Keep `127.0.0.1:6767` for the packaged app and production-style `~/.paseo` state. +## Nix desktop package + +The flake exposes `packages..desktop` on Linux and macOS: + +```bash +nix build .#desktop +``` + +Linux produces the `paseo-desktop` launcher and desktop entry. macOS produces +`Applications/Paseo.app` plus the `paseo-desktop` launcher. Both use the nixpkgs +Electron runtime and the checkout's built daemon, client, and renderer rather +than downloading a published desktop release. + ### PASEO_HOME `PASEO_HOME` is the directory that holds runtime state (agents, worktrees, workspace config, sockets, daemon log). Resolution rules: diff --git a/flake.nix b/flake.nix index 64958eb1ce..5d9d240cc4 100644 --- a/flake.nix +++ b/flake.nix @@ -26,16 +26,10 @@ let pkgs = pkgsFor system; paseo = pkgs.callPackage ./nix/package.nix { }; - isLinux = nixpkgs.lib.elem system [ - "x86_64-linux" - "aarch64-linux" - ]; in { default = paseo; paseo = paseo; - } - // nixpkgs.lib.optionalAttrs isLinux { desktop = pkgs.callPackage ./nix/desktop-package.nix { inherit paseo; }; } ); diff --git a/nix/desktop-package.nix b/nix/desktop-package.nix index 053fadbd09..633915fca6 100644 --- a/nix/desktop-package.nix +++ b/nix/desktop-package.nix @@ -17,9 +17,47 @@ paseo, }: -buildNpmPackage rec { - pname = "paseo-desktop"; +let version = (builtins.fromJSON (builtins.readFile ../package.json)).version; + darwinInfoPlist = builtins.toFile "Paseo-Info.plist" ( + lib.generators.toPlist { escape = true; } { + CFBundleDisplayName = "Paseo"; + CFBundleExecutable = "Paseo"; + CFBundleIconFile = "Paseo.icns"; + CFBundleIdentifier = "sh.paseo.desktop"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = "Paseo"; + CFBundlePackageType = "APPL"; + CFBundleShortVersionString = version; + CFBundleURLTypes = [ + { + CFBundleURLName = "Paseo agent link"; + CFBundleURLSchemes = [ "paseo" ]; + } + ]; + CFBundleVersion = version; + LSApplicationCategoryType = "public.app-category.developer-tools"; + LSEnvironment.MallocNanoZone = "0"; + LSMinimumSystemVersion = "12.0"; + NSAppTransportSecurity.NSAllowsArbitraryLoads = true; + NSAudioCaptureUsageDescription = "Paseo needs access to audio capture for voice input."; + NSBluetoothAlwaysUsageDescription = "Paseo needs access to Bluetooth audio devices."; + NSBluetoothPeripheralUsageDescription = "Paseo needs access to Bluetooth audio devices."; + NSCameraUsageDescription = "Paseo needs access to the camera for image attachments."; + NSHighResolutionCapable = true; + NSMainNibFile = "MainMenu"; + NSMicrophoneUsageDescription = "Paseo needs access to the microphone for voice input."; + NSPrefersDisplaySafeAreaCompatibilityMode = false; + NSPrincipalClass = "AtomApplication"; + NSQuitAlwaysKeepsWindows = false; + NSRequiresAquaSystemAppearance = false; + NSSupportsAutomaticGraphicsSwitching = true; + } + ); +in +buildNpmPackage { + pname = "paseo-desktop"; + inherit version; src = lib.cleanSourceWith { src = ./..; @@ -66,14 +104,15 @@ buildNpmPackage rec { # npm rebuild. We manually rebuild only node-pty in buildPhase. npmRebuildFlags = [ "--ignore-scripts" ]; - nativeBuildInputs = [ - python3 # for node-gyp (node-pty) - makeWrapper - copyDesktopItems - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ - autoPatchelfHook - ]; + nativeBuildInputs = + [ + python3 # for node-gyp (node-pty) + makeWrapper + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + autoPatchelfHook + copyDesktopItems + ]; buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ libuv @@ -153,31 +192,52 @@ buildNpmPackage rec { cp -a skills $out/share/paseo-desktop/ fi - # Hicolor icon for desktop environments - install -Dm644 packages/desktop/assets/icon.png \ - $out/share/icons/hicolor/512x512/apps/paseo-desktop.png + ${lib.optionalString stdenv.hostPlatform.isLinux '' + # Hicolor icon for desktop environments + install -Dm644 packages/desktop/assets/icon.png \ + $out/share/icons/hicolor/512x512/apps/paseo-desktop.png - # Launcher wraps nixpkgs electron. - # --no-sandbox: Chromium's setuid sandbox can't live in /nix/store - # (immutable, no setuid). Acceptable for v1; a follow-up can wire - # `security.wrappers` via a NixOS module for users who want the sandbox. - # - # EXPO_DEV_URL: We run unpackaged via `electron path/to/main.js`, so - # `app.isPackaged` is false. In that mode main.ts loads `DEV_SERVER_URL` - # (defaults to http://localhost:8081 — the Expo dev server, which doesn't - # exist here). Point it at the `paseo://` protocol handler instead, which - # serves from `__dirname/../../app/dist` (our install layout matches). - makeWrapper ${electron}/bin/electron $out/bin/paseo-desktop \ - --add-flags "$out/share/paseo-desktop/packages/desktop/dist/main.js" \ - --add-flags "--no-sandbox" \ - --set EXPO_DEV_URL "paseo://app/" + # Chromium's setuid sandbox cannot live in the immutable Nix store. + makeWrapper ${electron}/bin/electron $out/bin/paseo-desktop \ + --add-flags "$out/share/paseo-desktop/packages/desktop/dist/main.js" \ + --add-flags "--no-sandbox" \ + --set EXPO_DEV_URL "paseo://app/" - copyDesktopItems + copyDesktopItems + ''} + + ${lib.optionalString stdenv.hostPlatform.isDarwin '' + app="$out/Applications/Paseo.app" + mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" + + # Reuse nixpkgs' Electron framework through store symlinks so the Paseo + # output stays small while still presenting a normal application bundle + # to LaunchServices. + ln -s ${electron}/Applications/Electron.app/Contents/Frameworks \ + "$app/Contents/Frameworks" + ln -s ${electron}/Applications/Electron.app/Contents/MacOS/Electron \ + "$app/Contents/MacOS/Electron" + for resource in ${electron}/Applications/Electron.app/Contents/Resources/*; do + ln -s "$resource" "$app/Contents/Resources/$(basename "$resource")" + done + + cp ${darwinInfoPlist} "$app/Contents/Info.plist" + cp ${electron}/Applications/Electron.app/Contents/PkgInfo "$app/Contents/PkgInfo" + cp packages/desktop/assets/icon.icns "$app/Contents/Resources/Paseo.icns" + + # Keep the same unpackaged runtime layout as the Linux Nix package. The + # outer .app supplies native macOS identity, icon, and URL handling while + # the wrapper points Electron at the immutable built main process. + makeWrapper "$app/Contents/MacOS/Electron" "$app/Contents/MacOS/Paseo" \ + --add-flags "$out/share/paseo-desktop/packages/desktop/dist/main.js" \ + --set EXPO_DEV_URL "paseo://app/" + ln -s ../Applications/Paseo.app/Contents/MacOS/Paseo "$out/bin/paseo-desktop" + ''} runHook postInstall ''; - desktopItems = [ + desktopItems = lib.optionals stdenv.hostPlatform.isLinux [ (makeDesktopItem { name = "paseo-desktop"; desktopName = "Paseo"; @@ -195,6 +255,6 @@ buildNpmPackage rec { homepage = "https://github.com/getpaseo/paseo"; license = lib.licenses.agpl3Plus; mainProgram = "paseo-desktop"; - platforms = lib.platforms.linux; + platforms = lib.platforms.linux ++ lib.platforms.darwin; }; } From 6e74c924e61f246b144e15864d4ee34de1ef22ff Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Jul 2026 11:39:18 -0700 Subject: [PATCH 3/6] Package the Darwin desktop with electron-builder --- nix/desktop-package.nix | 187 ++++++++++++++++------------------------ 1 file changed, 74 insertions(+), 113 deletions(-) diff --git a/nix/desktop-package.nix b/nix/desktop-package.nix index 633915fca6..a646ef2535 100644 --- a/nix/desktop-package.nix +++ b/nix/desktop-package.nix @@ -16,57 +16,16 @@ # the upstream hash via `paseo.override { npmDepsHash = "..."; }`. paseo, }: - -let - version = (builtins.fromJSON (builtins.readFile ../package.json)).version; - darwinInfoPlist = builtins.toFile "Paseo-Info.plist" ( - lib.generators.toPlist { escape = true; } { - CFBundleDisplayName = "Paseo"; - CFBundleExecutable = "Paseo"; - CFBundleIconFile = "Paseo.icns"; - CFBundleIdentifier = "sh.paseo.desktop"; - CFBundleInfoDictionaryVersion = "6.0"; - CFBundleName = "Paseo"; - CFBundlePackageType = "APPL"; - CFBundleShortVersionString = version; - CFBundleURLTypes = [ - { - CFBundleURLName = "Paseo agent link"; - CFBundleURLSchemes = [ "paseo" ]; - } - ]; - CFBundleVersion = version; - LSApplicationCategoryType = "public.app-category.developer-tools"; - LSEnvironment.MallocNanoZone = "0"; - LSMinimumSystemVersion = "12.0"; - NSAppTransportSecurity.NSAllowsArbitraryLoads = true; - NSAudioCaptureUsageDescription = "Paseo needs access to audio capture for voice input."; - NSBluetoothAlwaysUsageDescription = "Paseo needs access to Bluetooth audio devices."; - NSBluetoothPeripheralUsageDescription = "Paseo needs access to Bluetooth audio devices."; - NSCameraUsageDescription = "Paseo needs access to the camera for image attachments."; - NSHighResolutionCapable = true; - NSMainNibFile = "MainMenu"; - NSMicrophoneUsageDescription = "Paseo needs access to the microphone for voice input."; - NSPrefersDisplaySafeAreaCompatibilityMode = false; - NSPrincipalClass = "AtomApplication"; - NSQuitAlwaysKeepsWindows = false; - NSRequiresAquaSystemAppearance = false; - NSSupportsAutomaticGraphicsSwitching = true; - } - ); -in buildNpmPackage { pname = "paseo-desktop"; - inherit version; + version = (builtins.fromJSON (builtins.readFile ../package.json)).version; src = lib.cleanSourceWith { src = ./..; - filter = - path: type: - let - baseName = builtins.baseNameOf path; - relPath = lib.removePrefix (toString ./..) path; - in + filter = path: type: let + baseName = builtins.baseNameOf path; + relPath = lib.removePrefix (toString ./..) path; + in # Exclude mobile-only platform code (we only need the web/electron build) !(lib.hasPrefix "/packages/app/android" relPath) && !(lib.hasPrefix "/packages/app/ios" relPath) @@ -102,15 +61,15 @@ buildNpmPackage { # Prevent onnxruntime-node's install script from running during automatic # npm rebuild. We manually rebuild only node-pty in buildPhase. - npmRebuildFlags = [ "--ignore-scripts" ]; + npmRebuildFlags = ["--ignore-scripts"]; nativeBuildInputs = [ python3 # for node-gyp (node-pty) - makeWrapper ] ++ lib.optionals stdenv.hostPlatform.isLinux [ autoPatchelfHook + makeWrapper copyDesktopItems ]; @@ -143,56 +102,75 @@ buildNpmPackage { # Expo web export for the Electron renderer ( cd packages/app && PASEO_WEB_PLATFORM=electron npx expo export --platform web ) - # Desktop main process (tsc only — NOT electron-builder) + # Desktop main process npm run build:main --workspace=@getpaseo/desktop + ${lib.optionalString stdenv.hostPlatform.isDarwin '' + # Let electron-builder create the native bundle layout (including helper + # app names and bundle identifiers), but source Electron from nixpkgs + # instead of downloading a release at build time. + ( + cd packages/desktop + CSC_IDENTITY_AUTO_DISCOVERY=false \ + ../../node_modules/.bin/electron-builder \ + --config electron-builder.yml \ + --dir \ + --mac \ + --publish never \ + --config.electronDist=${electron}/Applications \ + --config.mac.notarize=false + ) + ''} + runHook postBuild ''; installPhase = '' runHook preInstall - mkdir -p $out/share/paseo-desktop $out/bin - - # Materialize only the desktop and daemon runtime graphs. Copying the - # complete monorepo used to ship every build-time dependency (including - # Electron, Expo tooling, and cross-platform builder binaries), making the - # desktop output larger than 2 GiB. - PASEO_TRACE_DESKTOP=1 node scripts/trace-daemon.mjs > desktop-files.txt - - while IFS= read -r path; do - [ -z "$path" ] && continue - mkdir -p "$out/share/paseo-desktop/$(dirname "$path")" - cp -a "$path" "$out/share/paseo-desktop/$path" - done < desktop-files.txt - - # Keep the same unpackaged monorepo layout expected by main.js. - cp package.json $out/share/paseo-desktop/ - mkdir -p $out/share/paseo-desktop/packages/app - cp -a packages/app/dist $out/share/paseo-desktop/packages/app/ - - for runtime_path in \ - packages/desktop/dist/main.js \ - packages/desktop/dist/preload.js \ - packages/desktop/dist/features/browser-keyboard/guest-preload.js \ - packages/desktop/package.json; do - if [ ! -e "$out/share/paseo-desktop/$runtime_path" ]; then - echo "desktop runtime trace omitted $runtime_path" >&2 - exit 1 - fi - done - - if [ -e $out/share/paseo-desktop/node_modules/electron ]; then - echo "desktop runtime trace included npm Electron" >&2 - exit 1 - fi - - # Skills directory referenced at runtime by some agents - if [ -d skills ]; then - cp -a skills $out/share/paseo-desktop/ - fi + mkdir -p $out/bin ${lib.optionalString stdenv.hostPlatform.isLinux '' + mkdir -p $out/share/paseo-desktop + + # Materialize only the desktop and daemon runtime graphs. Copying the + # complete monorepo used to ship every build-time dependency (including + # Electron, Expo tooling, and cross-platform builder binaries), making the + # desktop output larger than 2 GiB. + PASEO_TRACE_DESKTOP=1 node scripts/trace-daemon.mjs > desktop-files.txt + + while IFS= read -r path; do + [ -z "$path" ] && continue + mkdir -p "$out/share/paseo-desktop/$(dirname "$path")" + cp -a "$path" "$out/share/paseo-desktop/$path" + done < desktop-files.txt + + # Keep the same unpackaged monorepo layout expected by main.js. + cp package.json $out/share/paseo-desktop/ + mkdir -p $out/share/paseo-desktop/packages/app + cp -a packages/app/dist $out/share/paseo-desktop/packages/app/ + + for runtime_path in \ + packages/desktop/dist/main.js \ + packages/desktop/dist/preload.js \ + packages/desktop/dist/features/browser-keyboard/guest-preload.js \ + packages/desktop/package.json; do + if [ ! -e "$out/share/paseo-desktop/$runtime_path" ]; then + echo "desktop runtime trace omitted $runtime_path" >&2 + exit 1 + fi + done + + if [ -e $out/share/paseo-desktop/node_modules/electron ]; then + echo "desktop runtime trace included npm Electron" >&2 + exit 1 + fi + + # Skills directory referenced at runtime by some agents + if [ -d skills ]; then + cp -a skills $out/share/paseo-desktop/ + fi + # Hicolor icon for desktop environments install -Dm644 packages/desktop/assets/icon.png \ $out/share/icons/hicolor/512x512/apps/paseo-desktop.png @@ -207,30 +185,13 @@ buildNpmPackage { ''} ${lib.optionalString stdenv.hostPlatform.isDarwin '' - app="$out/Applications/Paseo.app" - mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" - - # Reuse nixpkgs' Electron framework through store symlinks so the Paseo - # output stays small while still presenting a normal application bundle - # to LaunchServices. - ln -s ${electron}/Applications/Electron.app/Contents/Frameworks \ - "$app/Contents/Frameworks" - ln -s ${electron}/Applications/Electron.app/Contents/MacOS/Electron \ - "$app/Contents/MacOS/Electron" - for resource in ${electron}/Applications/Electron.app/Contents/Resources/*; do - ln -s "$resource" "$app/Contents/Resources/$(basename "$resource")" - done - - cp ${darwinInfoPlist} "$app/Contents/Info.plist" - cp ${electron}/Applications/Electron.app/Contents/PkgInfo "$app/Contents/PkgInfo" - cp packages/desktop/assets/icon.icns "$app/Contents/Resources/Paseo.icns" - - # Keep the same unpackaged runtime layout as the Linux Nix package. The - # outer .app supplies native macOS identity, icon, and URL handling while - # the wrapper points Electron at the immutable built main process. - makeWrapper "$app/Contents/MacOS/Electron" "$app/Contents/MacOS/Paseo" \ - --add-flags "$out/share/paseo-desktop/packages/desktop/dist/main.js" \ - --set EXPO_DEV_URL "paseo://app/" + app="$(find packages/desktop/release -maxdepth 3 -type d -name Paseo.app -print -quit)" + if [ -z "$app" ]; then + echo "electron-builder did not produce Paseo.app" >&2 + exit 1 + fi + mkdir -p "$out/Applications" + cp -R "$app" "$out/Applications/Paseo.app" ln -s ../Applications/Paseo.app/Contents/MacOS/Paseo "$out/bin/paseo-desktop" ''} @@ -245,7 +206,7 @@ buildNpmPackage { comment = "Self-hosted daemon for AI coding agents"; exec = "paseo-desktop"; icon = "paseo-desktop"; - categories = [ "Development" ]; + categories = ["Development"]; startupWMClass = "Paseo"; }) ]; From 7d4100a02161047f2c1c8220c4191ff1e073897e Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Jul 2026 11:44:15 -0700 Subject: [PATCH 4/6] Stage writable Electron distribution on Darwin --- nix/desktop-package.nix | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nix/desktop-package.nix b/nix/desktop-package.nix index a646ef2535..ee5a02446e 100644 --- a/nix/desktop-package.nix +++ b/nix/desktop-package.nix @@ -108,7 +108,13 @@ buildNpmPackage { ${lib.optionalString stdenv.hostPlatform.isDarwin '' # Let electron-builder create the native bundle layout (including helper # app names and bundle identifiers), but source Electron from nixpkgs - # instead of downloading a release at build time. + # instead of downloading a release at build time. electron-builder edits + # the copied helper plists, so stage a writable distribution rather than + # pointing it directly at the read-only Nix store. + electron_dist="$NIX_BUILD_TOP/electron-dist" + mkdir -p "$electron_dist" + cp -R ${electron}/Applications/Electron.app "$electron_dist/" + chmod -R u+w "$electron_dist/Electron.app" ( cd packages/desktop CSC_IDENTITY_AUTO_DISCOVERY=false \ @@ -117,7 +123,7 @@ buildNpmPackage { --dir \ --mac \ --publish never \ - --config.electronDist=${electron}/Applications \ + --config.electronDist="$electron_dist" \ --config.mac.notarize=false ) ''} From a8637d4bbcd9c71c5453613be498f60f4cdfaff8 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Jul 2026 11:50:07 -0700 Subject: [PATCH 5/6] Skip code signing for the Darwin Nix package --- nix/desktop-package.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/desktop-package.nix b/nix/desktop-package.nix index ee5a02446e..3494afa6af 100644 --- a/nix/desktop-package.nix +++ b/nix/desktop-package.nix @@ -117,6 +117,8 @@ buildNpmPackage { chmod -R u+w "$electron_dist/Electron.app" ( cd packages/desktop + # The Nix output is not a distributable DMG, so leave it unsigned and + # disable the hardened runtime that requires a matching signature. CSC_IDENTITY_AUTO_DISCOVERY=false \ ../../node_modules/.bin/electron-builder \ --config electron-builder.yml \ @@ -124,6 +126,8 @@ buildNpmPackage { --mac \ --publish never \ --config.electronDist="$electron_dist" \ + --config.mac.identity=null \ + --config.mac.hardenedRuntime=false \ --config.mac.notarize=false ) ''} From 70ed70d36e2eabd3876a22931954319ec202c902 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 31 Jul 2026 23:40:28 +0000 Subject: [PATCH 6/6] ci(nix): build the desktop package on macOS --- .github/workflows/nix.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 4bf1ae8076..6da8385187 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -104,3 +104,27 @@ jobs: - name: Build Nix desktop package run: nix build .#desktop -o result-desktop + + build-desktop-darwin: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Build Nix desktop package + run: nix build .#desktop -o result-desktop + + - name: Verify macOS application bundle + run: | + set -euo pipefail + + app="result-desktop/Applications/Paseo.app" + test -x "$app/Contents/MacOS/Paseo" + test -L result-desktop/bin/paseo-desktop + test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist")" = "sh.paseo.desktop" + find "$app" -name Info.plist -print0 | xargs -0 -n 1 plutil -lint