diff --git a/.github/workflows/nix-desktop.yml b/.github/workflows/nix-desktop.yml new file mode 100644 index 00000000000..d3e6fbf13de --- /dev/null +++ b/.github/workflows/nix-desktop.yml @@ -0,0 +1,29 @@ +name: nix desktop + +on: + pull_request: + branches: [dev] + workflow_dispatch: + +jobs: + build-desktop: + strategy: + fail-fast: false + matrix: + os: + - blacksmith-4vcpu-ubuntu-2404 + - macos-latest + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@v21 + + - name: Build desktop via flake + run: | + set -euo pipefail + nix --version + nix build .#desktop -L diff --git a/flake.nix b/flake.nix index a6614a5dc9c..6e09922cc70 100644 --- a/flake.nix +++ b/flake.nix @@ -66,10 +66,10 @@ mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hash = nodeModulesHash; }; - mkPackage = pkgs.callPackage ./nix/opencode.nix { }; - in - { - default = mkPackage { + mkOpencode = pkgs.callPackage ./nix/opencode.nix { }; + mkDesktop = pkgs.callPackage ./nix/desktop.nix { }; + + opencodePkg = mkOpencode { version = packageJson.version; src = ./.; scripts = ./nix/scripts; @@ -77,6 +77,18 @@ modelsDev = "${modelsDev.${system}}/dist/_api.json"; mkNodeModules = mkNodeModules; }; + + desktopPkg = mkDesktop { + version = packageJson.version; + src = ./.; + scripts = ./nix/scripts; + mkNodeModules = mkNodeModules; + opencode = opencodePkg; + }; + in + { + default = opencodePkg; + desktop = desktopPkg; } ); diff --git a/nix/desktop.nix b/nix/desktop.nix new file mode 100644 index 00000000000..4b659413aaa --- /dev/null +++ b/nix/desktop.nix @@ -0,0 +1,145 @@ +{ + lib, + stdenv, + rustPlatform, + bun, + pkg-config, + dbus ? null, + openssl, + glib ? null, + gtk3 ? null, + libsoup_3 ? null, + webkitgtk_4_1 ? null, + librsvg ? null, + libappindicator-gtk3 ? null, + cargo, + rustc, + makeBinaryWrapper, + nodejs, + jq, +}: +args: +let + scripts = args.scripts; + mkModules = + attrs: + args.mkNodeModules ( + attrs + // { + canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; + normalizeBinsScript = scripts + "/normalize-bun-binaries.ts"; + } + ); +in +rustPlatform.buildRustPackage rec { + pname = "opencode-desktop"; + version = args.version; + + src = args.src; + + # We need to set the root for cargo, but we also need access to the whole repo. + postUnpack = '' + # Update sourceRoot to point to the tauri app + sourceRoot+=/packages/desktop/src-tauri + ''; + + cargoLock = { + lockFile = ../packages/desktop/src-tauri/Cargo.lock; + allowBuiltinFetchGit = true; + }; + + node_modules = mkModules { + version = version; + src = src; + }; + + nativeBuildInputs = [ + pkg-config + bun + makeBinaryWrapper + cargo + rustc + nodejs + jq + ]; + + buildInputs = [ + openssl + ] + ++ lib.optionals stdenv.isLinux [ + dbus + glib + gtk3 + libsoup_3 + webkitgtk_4_1 + librsvg + libappindicator-gtk3 + ]; + + preBuild = '' + # Restore node_modules + pushd ../../.. + + # Copy node_modules from the fixed-output derivation + # We use cp -r --no-preserve=mode to ensure we can write to them if needed, + # though we usually just read. + cp -r ${node_modules}/node_modules . + cp -r ${node_modules}/packages . + + # Ensure node_modules is writable so patchShebangs can update script headers + chmod -R u+w node_modules + # Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo) + chmod -R u+w packages + # Patch shebangs so scripts can run + patchShebangs node_modules + + # Copy sidecar + mkdir -p packages/desktop/src-tauri/sidecars + targetTriple=${stdenv.hostPlatform.rust.rustcTarget} + cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple + + # Merge prod config into tauri.conf.json + if ! jq -s '.[0] * .[1]' \ + packages/desktop/src-tauri/tauri.conf.json \ + packages/desktop/src-tauri/tauri.prod.conf.json \ + > packages/desktop/src-tauri/tauri.conf.json.tmp; then + echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2 + exit 1 + fi + mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json + + # Build the frontend + cd packages/desktop + + # The 'build' script runs 'bun run typecheck && vite build'. + bun run build + + popd + ''; + + # Tauri bundles the assets during the rust build phase (which happens after preBuild). + # It looks for them in the location specified in tauri.conf.json. + + postInstall = lib.optionalString stdenv.isLinux '' + # Wrap the binary to ensure it finds the libraries + wrapProgram $out/bin/opencode-desktop \ + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + gtk3 + webkitgtk_4_1 + librsvg + glib + libsoup_3 + ] + } + ''; + + meta = with lib; { + description = "OpenCode Desktop App"; + homepage = "https://opencode.ai"; + license = licenses.mit; + maintainers = with maintainers; [ ]; + mainProgram = "opencode-desktop"; + platforms = platforms.linux ++ platforms.darwin; + }; +} diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 7b22ef8e7da..cb1366ee452 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,18 +1,27 @@ -{ hash, lib, stdenvNoCC, bun, cacert, curl }: +{ + hash, + lib, + stdenvNoCC, + bun, + cacert, + curl, +}: args: stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; version = args.version; src = args.src; - impureEnvVars = - lib.fetchers.proxyImpureEnvVars - ++ [ - "GIT_PROXY_COMMAND" - "SOCKS_SERVER" - ]; + impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; - nativeBuildInputs = [ bun cacert curl ]; + nativeBuildInputs = [ + bun + cacert + curl + ]; dontConfigure = true; diff --git a/nix/opencode.nix b/nix/opencode.nix index 87b3f17ba99..5a8e5a52368 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -1,4 +1,10 @@ -{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }: +{ + lib, + stdenvNoCC, + bun, + ripgrep, + makeBinaryWrapper, +}: args: let scripts = args.scripts;