← back to Exo
nix: add dashboard build with dream2nix
3671528fa46a0633fc7853a89e68e863273d0c3e · 2026-01-13 22:19:45 +0000 · Jake Hillion
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
```
Files touched
M .github/workflows/build-app.ymlA dashboard/dashboard.nixA dashboard/parts.nixM flake.lockM flake.nix
Diff
commit 3671528fa46a0633fc7853a89e68e863273d0c3e
Author: Jake Hillion <jake@hillion.co.uk>
Date: Tue Jan 13 22:19:45 2026 +0000
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(-)
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
];
← e6434ec4 nix: add Rust builds with crane and fenix
·
back to Exo
·
add glm-47, minimax-m21 (#1147) 82ba42ba →