Homenazrin ltd
Bending Zed to render my beautiful fonts with Nix

I like Zed. Save its AI features that I banished from my mind the second people started talking about them, it seems to me to be a pretty decent hybrid between a keyboard-centric vimmish editor, and an actual piece of software that supports best practice ecosystem features like LSPs and tree-sitter support (without having to configure them). People have been making those for years now, to be clear, but I like Zed's UI and speed on top of all that, too.

Zed is pretty easy to install using NixOS, provided that you decide between one of the four ways to do so:

  1. $ nix-env -iA nixpkgs.zed-editor (standard issue Nix profile management, ad-hoc)
  2. $ nix profile install nixpkgs#zed-editor (flake-enabled Nix profile management, ad-hoc)
  3. home.packages = with pkgs; [ zed-editor ]; (using home-manager)
  4. environment.systemPackages = with pkgs; [ zed-editor ]; (using NixOS)

There's one problem though: I'm a big fan of using a custom font for everything. My favorite font is Recursive, which I slap on almost anything that touches my computer screen. Zed, for some reason, does not render this font after setting it in its configuration:

empty zed editor window using the default monospace font instead of my custom one

This is very clearly unusable, so for the time being, I had to switch to VSCode, an inferior product. This cannot stand, so why does Zed do this? The font is installed and available through fontconfig:

$  fc-list | rg -i "Recursive" | wc -l
193

So what's the deal? Doing some searching leads me to this issue. Apparently, this library cosmic-text is what Zed uses to wrangle its font rendering, and one of its dependencies had a regression that caused it to be unable to render symlinked fonts. Welp. Nix is a heavy user of symlinks, so this definitely makes sense.

Does This Really Fix It

I'm going to be honest: I could have created an issue somewhere or commented on an issue that probably exists on nixpkgs, but I'm on holiday and left my Yubikey that I use for FIDO 2FA at home, so I can't actually log into GitHub. Or maybe that's an excuse I'm making for myself. In any event, I figured it might be a good thing to patch this up for my use, since it may get me to learn a new facet of Nix that I haven't used before.

The first thing I did is that I tried to patch it manually. Doing so involved cloning both Zed and cosmic-text.

$ git clone https://github.com/pop-os/cosmic-text
$ git clone https://github.com/zed-industries/zed

Should now be as easy as applying the patch from the PR to cosmic-text...

cd cosmic-text
curl -L https://github.com/pop-os/cosmic-text/pull/296.patch -o update-deps.patch

Wait! This is on the latest main, which I'm sure is not what Zed is actually tracking as a dependency. Let's have a look...

$ rg cosmic-text ../zed/
# ...
../zed/crates/gpui/Cargo.toml
119:cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c" }
# ...

Aha! Good to know. Let's go to that rev and branch off there, and then apply our patch.

$ git checkout 542b20c
$ git switch -c apply-patch
$ git apply update-deps.patch

This should now build successfully, so now it's time to update the dependency in Zed:

$ cd ../zed
# edit crates/gpui/Cargo.toml
$ git diff
diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml
index 4ce44ffce0..434076128f 100644
--- a/crates/gpui/Cargo.toml
+++ b/crates/gpui/Cargo.toml
@@ -116,7 +116,7 @@ as-raw-xcb-connection = "1"
 ashpd.workspace = true
 calloop = "0.13.0"
 calloop-wayland-source = "0.3.0"
-cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c" }
+cosmic-text = { path = "../../../cosmic-text" }
 flume = "0.11"
 wayland-backend = { version = "0.3.3", features = ["client_system", "dlopen"] }
 wayland-client = { version = "0.31.2" }

Phew, all done. Now, let's see if it builds! Luckily, Zed ships with a Nix flake, so I can just run nix develop and get a shell with everything I need to build. In Zed's case, that is a single cargo run. If you're following along, grab a drink and wait. We're compiling 1200 crates here. This also takes up a fair bit of space! When doing this on my laptop, I actually ran out of space on my tmpfs 😭

After a few minutes, this popped up. Success!

image.png (1×1 px, 29 KB)

Making It Reproducible (In A Broad Interpretation Of The Term)

Great, now we have a Zed build that works for my purposes. I would, however, like to have this on all my NixOS machines without having to go through this process all the time. We're also missing out on things that the nixpkgs derivation for Zed would give us by default, like a .desktop file!

Before we can do anything fancy using Nix, we have the issue of the patched cosmic-text. While Cargo is perfectly fine with building it from a local path, this is bad for reproducability's sake. The next best thing is to throw it into a personal Git repo, which I have conveniently done. Changing the dependency in Zed and rerunning cargo fetch results in the following patch (I cropped the lockfile part out for readability's sake:

diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml
index 3d285ce59e..fe0eb79122 100644
--- a/crates/gpui/Cargo.toml
+++ b/crates/gpui/Cargo.toml
@@ -50,7 +50,7 @@ parking = "2.0.0"
 parking_lot.workspace = true
 postage.workspace = true
 profiling.workspace = true
-rand = { optional = true, workspace = true}
+rand = { optional = true, workspace = true }
 raw-window-handle = "0.6"
 refineable.workspace = true
 resvg = { version = "0.41.0", default-features = false }
@@ -116,7 +116,7 @@ as-raw-xcb-connection = "1"
 ashpd.workspace = true
 calloop = "0.13.0"
 calloop-wayland-source = "0.3.0"
-cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c" }
+cosmic-text = { git = "https://codeberg.org/shadows_withal/cosmic-text-with-pr-296", branch = "296" }
 flume = "0.11"
 wayland-backend = { version = "0.3.3", features = ["client_system", "dlopen"] }
 wayland-client = { version = "0.31.2" }

Now, I hear Nix lets you patch stuff by using something called an overlay. This seems to do exactly what I want: It can add your own packages into the package attribute set, or patch existing packages from upstream nixpkgs. Some people that I, ahem, let my NixOS configuration be inspired by also use this for similar reasons to why I want to use it.

The good thing is that the NixOS config template I used already ships with an overlay/default.nix file! Copy the diff from our Zed repo over (e.g. by doing git diff > /my/nix/config/overlays/zed-use-custom-cosmic-text.patch). Now we would just have to do something like this...

modifications = final: prev: {
  zed-editor = prev.zed-editor.overrideAttrs
    (oldAttrs: {
      version = "0.151.1";
      patches = (oldAttrs.patches or [ ])
        ++ [
        ./zed-use-custom-cosmic-text.patch
      ];
    });
};

Let's try to include this in our profile. Running nixos-rebuild --switch...

error: builder for '/nix/store/bgw53sz10krwczg30qfl27byycg7jicm-zed-0.151.1.drv' failed with exit code 1;
       last 25 log lines:
       > ---
       > > version = "0.2.0"
       > 12204c12191
       > < checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
       > ---
       > > checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
       > 12208c12195
       > < version = "0.3.0"
       > ---
       > > version = "0.2.0"
       > 12210c12197
       > < checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
       > ---
       > > checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
       >
       > ERROR: cargoHash or cargoSha256 is out of date
       >
       > Cargo.lock is not the same in /build/cargo-vendor-dir
       >
       > To fix the issue:
       > 1. Set cargoHash/cargoSha256 to an empty string: `cargoHash = "";`
       > 2. Build the derivation and wait for it to fail with a hash mismatch
       > 3. Copy the "got: sha256-..." value back into the cargoHash field
       >    You should have: cargoHash = "sha256-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=";
       >
       For full logs, run 'nix log /nix/store/bgw53sz10krwczg30qfl27byycg7jicm-zed-0.151.1.drv'.

It seems that Nix isn't particularly happy about us having shuffled around the dependencies, which would probably produce a different checksum. Fine. I guess let's see if we can override this checksum in our patch. I started digging in the nixpkgs derivation for Zed, and found this:

cargoLock = {
   lockFile = ./Cargo.lock;
   outputHashes = {
     "alacritty_terminal-0.24.1-dev" = "sha256-b4oSDhsAAYjpYGfFgA1Q1642JoJQ9k5RTsPgFUpAFmc=";
     "async-pipe-0.1.3" = "sha256-g120X88HGT8P6GNCrzpS5SutALx5H+45Sf4iSSxzctE=";
     "blade-graphics-0.4.0" = "sha256-LmJ8f1VVzPfjpZp2T5WKxjEdzE/avfWmA3dq/V27eJc=";
     "cosmic-text-0.11.2" = "sha256-TLPDnqixuW+aPAhiBhSvuZIa69vgV3xLcw32OlkdCcM=";
     "font-kit-0.14.1" = "sha256-qUKvmi+RDoyhMrZ7T6SoVAyMc/aasQ9Y/okzre4SzXo=";
     "lsp-types-0.95.1" = "sha256-N4MKoU9j1p/Xeowki/+XiNQPwIcTm9DgmfM/Eieq4js=";
     "nvim-rs-0.8.0-pre" = "sha256-VA8zIynflul1YKBlSxGCXCwa2Hz0pT3mH6OPsfS7Izo=";
     "tree-sitter-0.22.6" = "sha256-P9pQcofDCIhOYWA1OC8TzB5UgWpD5GlDzX2DOS8SsH0=";
     "tree-sitter-gomod-1.0.2" = "sha256-/sjC117YAFniFws4F/8+Q5Wrd4l4v4nBUaO9IdkixSE=";
     "tree-sitter-gowork-0.0.1" = "sha256-803ujH5qwejQ2vQDDpma4JDC9a+vFX8ZQmr+77VyL2M=";
     "tree-sitter-heex-0.0.1" = "sha256-VakMZtWQ/h7dNy5ehk2Bh14a5s878AUgwY3Ipq8tPec=";
     "tree-sitter-md-0.2.3" = "sha256-Fa73P1h5GvKV3SxXr0KzHuNp4xa5wxUzI8ecXbGdrYE=";
     "xim-0.4.0" = "sha256-vxu3tjkzGeoRUj7vyP0vDGI7fweX8Drgy9hwOUOEQIA=";
     "xkbcommon-0.7.0" = "sha256-2RjZWiAaz8apYTrZ82qqH4Gv20WyCtPT+ldOzm0GWMo=";
   };
 };

Huh, a vendored Cargo.lock. Okay, so we could maybe counter-vendor our own lockfile? But why can't we just use cargoHash in the first place? From the nixpkgs docs:

Exception: If the application has cargo git dependencies, the cargoHash/cargoSha256 approach will not work, and you will need to copy the Cargo.lock file of the application to nixpkgs and continue with the next section for specifying the options of the cargoLock section.

Right, we need the sub-checksums for the Git dependencies. Okay, let's vendor the lockfile then. Having copied the lockfile from our manual build experiment, we can override all the way to the buildRustPackage attributes like this:

modifications = final: prev: {
    zed-editor = final.callPackage prev.zed-editor.override {
      rustPlatform = final.rustPlatform // {
        buildRustPackage = args: final.rustPlatform.buildRustPackage (args // {
          patches = (args.patches or [ ] ++ [
            ./zed-use-custom-cosmic-text.patch
          ]);
          cargoLock = {
            lockFile = ./zed.Cargo.lock;
            outputHashes = args.cargoLock.outputHashes // {
              "cosmic-text-0.11.2" = "sha256-ld9mrvtZIEftenn1D5IuXFQikJU2GAil6MCsrIh9o14=";
            };
          };
        });
      };
    };
  };
NOTE: Yes, you do need to explicitly list the checksum for the patched cosmic-text crate. If yours differs, you can find out the real checksum by using lib.fakeSha256 in place of a string for your first build. The error you get from that will tell you what the checksum needs to be.

It's crucial that we hook our patch into the derivation itself, if we don't do it, there's a bunch of errors with regards to fetching our custom crate, not having write permissions for cloning, and so on. But, running this, besides taking 5 million billion years due to the Nix overhead, will eventually install our patched Zed onto our system. Yay!!!

Was this worth it? Probably not, but it did teach me a bunch about how Nix derivations and nixpkgs are built, and it made my laptop heat up a bunch on an otherwise cold day.

P.S. If you want to copy this approach more easily, here's the commit that includes most of the stuff I talked about in this post: rLNX0b8783c25d9939fe739fb74921fb4347ca92f366

Written by shadows_withal on Mon, Sep 9, 9:44 PM.
the
Projects
None
Subscribers
None

Event Timeline