Try to improve nix caching (#48297)

Conrad Irwin created

Release Notes:

- N/A

Change summary

.github/workflows/release_nightly.yml          | 23 ++++++++
.github/workflows/run_tests.yml                | 23 ++++++++
flake.nix                                      |  2 
tooling/xtask/src/tasks/workflows/nix_build.rs | 54 ++++++++++++++++++-
tooling/xtask/src/tasks/workflows/steps.rs     | 11 ++++
5 files changed, 108 insertions(+), 5 deletions(-)

Detailed changes

.github/workflows/release_nightly.yml 🔗

@@ -387,6 +387,10 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - name: steps::cache_nix_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        cache: nix
     - name: nix_build::build_nix::install_nix
       uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
       with:
@@ -417,10 +421,20 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - name: steps::cache_nix_store_macos
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        path: ~/nix-cache
     - name: nix_build::build_nix::install_nix
       uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
       with:
         github_access_token: ${{ secrets.GITHUB_TOKEN }}
+    - name: nix_build::build_nix::configure_local_nix_cache
+      run: |
+        mkdir -p ~/nix-cache
+        echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
+        echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
+        sudo launchctl kickstart -k system/org.nixos.nix-daemon
     - name: nix_build::build_nix::cachix_action
       uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
       with:
@@ -429,6 +443,15 @@ jobs:
         cachixArgs: -v
     - name: nix_build::build_nix::build
       run: nix build .#default -L --accept-flake-config
+    - name: nix_build::build_nix::export_to_local_nix_cache
+      if: always()
+      run: |
+        if [ -L result ]; then
+          echo "Copying build closure to local binary cache..."
+          nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
+        else
+          echo "No build result found, skipping cache export."
+        fi
     timeout-minutes: 60
     continue-on-error: true
   update_nightly_tag:

.github/workflows/run_tests.yml 🔗

@@ -478,6 +478,10 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - name: steps::cache_nix_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        cache: nix
     - name: nix_build::build_nix::install_nix
       uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
       with:
@@ -508,10 +512,20 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - name: steps::cache_nix_store_macos
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        path: ~/nix-cache
     - name: nix_build::build_nix::install_nix
       uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
       with:
         github_access_token: ${{ secrets.GITHUB_TOKEN }}
+    - name: nix_build::build_nix::configure_local_nix_cache
+      run: |
+        mkdir -p ~/nix-cache
+        echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
+        echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
+        sudo launchctl kickstart -k system/org.nixos.nix-daemon
     - name: nix_build::build_nix::cachix_action
       uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
       with:
@@ -521,6 +535,15 @@ jobs:
         pushFilter: -zed-editor-[0-9.]*-nightly
     - name: nix_build::build_nix::build
       run: nix build .#debug -L --accept-flake-config
+    - name: nix_build::build_nix::export_to_local_nix_cache
+      if: always()
+      run: |
+        if [ -L result ]; then
+          echo "Copying build closure to local binary cache..."
+          nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
+        else
+          echo "No build result found, skipping cache export."
+        fi
     timeout-minutes: 60
     continue-on-error: true
   check_postgres_and_protobuf_migrations:

flake.nix 🔗

@@ -1,5 +1,5 @@
 {
-  description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter";
+  description = "Zed is a minimal code editor crafted for speed and collaboration with humans and AI.";
 
   inputs = {
     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

tooling/xtask/src/tasks/workflows/nix_build.rs 🔗

@@ -44,6 +44,32 @@ pub(crate) fn build_nix(
         ))
     }
 
+    // After install-nix, register ~/nix-cache as a local binary cache
+    // substituter so nix pulls from it on demand during builds (no bulk
+    // import). Also restart the daemon so it picks up the new config.
+    pub fn configure_local_nix_cache() -> Step<Run> {
+        named::bash(indoc::indoc! {r#"
+            mkdir -p ~/nix-cache
+            echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
+            echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
+            sudo launchctl kickstart -k system/org.nixos.nix-daemon
+        "#})
+    }
+
+    // Incrementally copy only new store paths from the build result's
+    // closure into the local binary cache for the next run.
+    pub fn export_to_local_nix_cache() -> Step<Run> {
+        named::bash(indoc::indoc! {r#"
+            if [ -L result ]; then
+              echo "Copying build closure to local binary cache..."
+              nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
+            else
+              echo "No build result found, skipping cache export."
+            fi
+        "#})
+        .if_condition(Expression::new("always()"))
+    }
+
     let runner = match platform {
         Platform::Windows => unimplemented!(),
         Platform::Linux => runners::LINUX_X86_BUNDLER,
@@ -67,10 +93,30 @@ pub(crate) fn build_nix(
         job = job.needs(deps.iter().map(|d| d.name.clone()).collect::<Vec<String>>());
     }
 
-    job = job
-        .add_step(install_nix())
-        .add_step(cachix_action(cachix_filter))
-        .add_step(build(&flake_output));
+    // On Linux, `cache: nix` uses bind-mounts so the /nix store is available
+    // before install-nix-action runs — no extra steps needed.
+    //
+    // On macOS, `/nix` lives on a read-only root filesystem and the nscloud
+    // cache action cannot mount or symlink there. Instead we cache a
+    // user-writable directory (~/nix-cache) as a local binary cache and
+    // register it as a nix substituter. Nix then pulls paths from it on
+    // demand during builds (zero-copy at startup), and after building we
+    // incrementally copy new paths into the cache for the next run.
+    job = match platform {
+        Platform::Linux => job
+            .add_step(steps::cache_nix_dependencies_namespace())
+            .add_step(install_nix())
+            .add_step(cachix_action(cachix_filter))
+            .add_step(build(&flake_output)),
+        Platform::Mac => job
+            .add_step(steps::cache_nix_store_macos())
+            .add_step(install_nix())
+            .add_step(configure_local_nix_cache())
+            .add_step(cachix_action(cachix_filter))
+            .add_step(build(&flake_output))
+            .add_step(export_to_local_nix_cache()),
+        Platform::Windows => unimplemented!(),
+    };
 
     NamedJob {
         name: format!("build_nix_{platform}_{arch}"),

tooling/xtask/src/tasks/workflows/steps.rs 🔗

@@ -138,6 +138,17 @@ pub fn cache_rust_dependencies_namespace() -> Step<Use> {
         .add_with(("path", "~/.rustup"))
 }
 
+pub fn cache_nix_dependencies_namespace() -> Step<Use> {
+    named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "nix"))
+}
+
+pub fn cache_nix_store_macos() -> Step<Use> {
+    // On macOS, `/nix` is on a read-only root filesystem so nscloud's `cache: nix`
+    // cannot mount or symlink there. Instead we cache a user-writable directory and
+    // use nix-store --import/--export in separate steps to transfer store paths.
+    named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("path", "~/nix-cache"))
+}
+
 pub fn setup_linux() -> Step<Run> {
     named::bash("./script/linux")
 }