From 3671528fa46a0633fc7853a89e68e863273d0c3e Mon Sep 17 00:00:00 2001 From: Jake Hillion Date: Tue, 13 Jan 2026 22:19:45 +0000 Subject: [PATCH] nix: add dashboard build with dream2nix Continue working towards a fully Nix based build by building the dashboard with Nix. Continuing the theme of using the existing lock files, use dream2nix to parse the lock file and build the tree of dependency derivations. dream2nix doesn't like the bundleDependencies, so we apply a small patch to the lock file that drops all dependencies that are bundled. This should ideally be contributed upstream but that can be done later. Use this new dashboard build in the build-app CI workflow, meaning future macOS apps will include this reproducible dashboard. Test plan: - Built a DMG, shipped to a cluster, loaded in a browser with no cache and the dashboard looks good. - Directory layout is as expected: ``` $ nix build .#dashboard $ find result/ ... result/_app/immutable/entry result/_app/immutable/entry/app.CTPAnMjf.js result/_app/immutable/entry/start.fUSEa-2O.js result/_app/immutable/nodes result/_app/immutable/nodes/3.DqQr1Obm.js result/_app/immutable/nodes/0.DgEY44RO.js result/_app/immutable/nodes/2.BjZg_lJh.js result/_app/immutable/nodes/1.D6vGUYYT.js result/_app/env.js result/_app/version.json result/exo-logo.png result/favicon.ico result/index.html ``` --- .github/workflows/build-app.yml | 17 +++++- dashboard/dashboard.nix | 60 ++++++++++++++++++ dashboard/parts.nix | 44 +++++++++++++ flake.lock | 105 ++++++++++++++++++++++++++++++++ flake.nix | 6 ++ 5 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 dashboard/dashboard.nix create mode 100644 dashboard/parts.nix diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 44e9c77e..542dc800 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -113,11 +113,22 @@ jobs: uv python install uv sync --locked + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Configure Cachix + uses: cachix/cachix-action@v14 + with: + name: exo + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: Build dashboard run: | - cd dashboard - npm ci - npm run build + DASHBOARD_OUT=$(nix build .#dashboard --print-build-logs --no-link --print-out-paths) + mkdir -p dashboard/build + cp -r "$DASHBOARD_OUT"/* dashboard/build/ - name: Install Sparkle CLI run: | diff --git a/dashboard/dashboard.nix b/dashboard/dashboard.nix new file mode 100644 index 00000000..6ecd1490 --- /dev/null +++ b/dashboard/dashboard.nix @@ -0,0 +1,60 @@ +{ lib +, config +, dream2nix +, ... +}: +let + # Read and parse the lock file + rawLockFile = builtins.fromJSON (builtins.readFile "${config.deps.dashboardSrc}/package-lock.json"); + + # For packages with bundleDependencies, filter out deps that are bundled + # (bundled deps are inside the tarball, not separate lockfile entries) + fixedPackages = lib.mapAttrs + (path: entry: + if entry ? bundleDependencies && entry.bundleDependencies != [ ] + then entry // { + dependencies = lib.filterAttrs + (name: _: !(lib.elem name entry.bundleDependencies)) + (entry.dependencies or { }); + } + else entry + ) + (rawLockFile.packages or { }); + + fixedLockFile = rawLockFile // { packages = fixedPackages; }; +in +{ + imports = [ + dream2nix.modules.dream2nix.nodejs-package-lock-v3 + dream2nix.modules.dream2nix.nodejs-granular-v3 + ]; + + name = "exo-dashboard"; + version = "1.0.0"; + + mkDerivation = { + src = config.deps.dashboardSrc; + + buildPhase = '' + runHook preBuild + npm run build + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + cp -r build $out/build + runHook postInstall + ''; + }; + + deps = { nixpkgs, ... }: { + inherit (nixpkgs) stdenv; + dashboardSrc = null; # Injected by parts.nix + }; + + nodejs-package-lock-v3 = { + # Don't use packageLockFile - provide the fixed lock content directly + packageLock = fixedLockFile; + }; +} diff --git a/dashboard/parts.nix b/dashboard/parts.nix new file mode 100644 index 00000000..80fbb156 --- /dev/null +++ b/dashboard/parts.nix @@ -0,0 +1,44 @@ +{ inputs, ... }: +{ + perSystem = + { pkgs, lib, ... }: + let + # Filter source to only include dashboard directory + src = lib.cleanSourceWith { + src = inputs.self; + filter = + path: type: + let + baseName = builtins.baseNameOf path; + inDashboardDir = + (lib.hasInfix "/dashboard/" path) + || (lib.hasSuffix "/dashboard" (builtins.dirOf path)) + || (baseName == "dashboard" && type == "directory"); + in + inDashboardDir; + }; + + # Build the dashboard with dream2nix (includes node_modules in output) + dashboardFull = inputs.dream2nix.lib.evalModules { + packageSets.nixpkgs = pkgs; + modules = [ + ./dashboard.nix + { + paths.projectRoot = inputs.self; + paths.projectRootFile = "flake.nix"; + paths.package = inputs.self + "/dashboard"; + } + # Inject the filtered source + { + deps.dashboardSrc = lib.mkForce "${src}/dashboard"; + } + ]; + }; + in + { + # Extract just the static site from the full build + packages.dashboard = pkgs.runCommand "exo-dashboard" { } '' + cp -r ${dashboardFull}/build $out + ''; + }; +} diff --git a/flake.lock b/flake.lock index 41de94a6..3c7b28e1 100644 --- a/flake.lock +++ b/flake.lock @@ -15,6 +15,28 @@ "type": "github" } }, + "dream2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "purescript-overlay": "purescript-overlay", + "pyproject-nix": "pyproject-nix" + }, + "locked": { + "lastModified": 1765953015, + "narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=", + "owner": "nix-community", + "repo": "dream2nix", + "rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "dream2nix", + "type": "github" + } + }, "fenix": { "inputs": { "nixpkgs": [ @@ -36,6 +58,22 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -88,9 +126,54 @@ "type": "github" } }, + "purescript-overlay": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "dream2nix", + "nixpkgs" + ], + "slimlock": "slimlock" + }, + "locked": { + "lastModified": 1728546539, + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "dream2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763017646, + "narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "47bd6f296502842643078d66128f7b5e5370790c", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, "root": { "inputs": { "crane": "crane", + "dream2nix": "dream2nix", "fenix": "fenix", "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", @@ -115,6 +198,28 @@ "type": "github" } }, + "slimlock": { + "inputs": { + "nixpkgs": [ + "dream2nix", + "purescript-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688756706, + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", + "owner": "thomashoneyman", + "repo": "slimlock", + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "slimlock", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index a9ae9ca7..1c049537 100644 --- a/flake.nix +++ b/flake.nix @@ -21,6 +21,11 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + dream2nix = { + url = "github:nix-community/dream2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Pinned nixpkgs for swift-format (swift is broken on x86_64-linux in newer nixpkgs) nixpkgs-swift.url = "github:NixOS/nixpkgs/08dacfca559e1d7da38f3cf05f1f45ee9bfd213c"; }; @@ -41,6 +46,7 @@ imports = [ inputs.treefmt-nix.flakeModule + ./dashboard/parts.nix ./rust/parts.nix ];