Adventures in installing dmg apps with nix-darwin

16 May 2024

TLDR - Having experimented with pure nix ways of handling dmg images, I finally had to settle on an outside-sandbox approach using the MacOS's built-in hdiutil tool. Skip to the Final section to see how it all ties together.

Having learned about the superb zed editor from the creators of Atom, I wanted to install it on my mac machine.

My mac runs a nix-darwin setup with home-manager on top. A lovely combo, albeit a bit more difficult to update compared to competing solutions.

My whole setup is relatively small and simple. And so normally, adding a new package is just extending my home.packages list.

Looking for zed in the nixos packages search tool yielded some unrelated stuff initially. No editor package in sight.

But I quickly realized the problem was I looked in the stable channel (23.11 atm). Upon switching to Unstable, I found what I was looking for. The name of the package is zed-editor in nix world.

So I just added it to my home-manager packages:

home.packages = [
    ... # other packages
    pkgs.zed-editor
];

Sadly, upon running home-manager switch I learned it won't do because it's an unstable channel and I haven't enabled unstable packages.

But to at least try, I tried to install zed-editor in the current session:

nix-shell -p zed-editor

This failed because the zed-editor build process on darwin is broken as of the time of writing. Looking up the underlying issue I found out it was due a darwin-specific dependency on LiveKitBridge (Swift) that fails to build.

So, inspired by the helpful error message suggesting I may try to enable broken packages, just in case it turns out to be working, I tried again:

export NIXPKGS_ALLOW_BROKEN=1
nix-shell -p zed-editor

This took a lot more time but eventually, the build failed due to a similar error as described in the github issue.

Core of the search

This made me think there must be a way to install this somehow as I checked that the pre-built distribution of the editor works fine on Macs.

There are pre-build .dmg packages (images) available on zed's github releases page. So I started looking into installing those. This turned out to be a spiral of doom as the continued efforts of the community over the last almost 10 years in reverse-engineering Apple's Volume formats were relentlessly shut down by Apple's ever-changing volume spec, starting with HFS then over to HFS+ and now finally at APFS.

I tried using libdmg-hfsplus , undmg, xpwn, apfs-fuse and p7zip. All of these turned out to either not work with the current format of the APFS images or me not being able to shake them (here, eg, xpwn) into doing the thing. This took me several iterations over a config that essentially looks something like this in my darwin.nix:

environment.systemPackages = with pkgs; [
  # ... other packages
    (pkgs.stdenv.mkDerivation {
      name = "zed";
      src = pkgs.fetchurl {
        url = "https://github.com/zed-industries/zed/releases/download/v0.135.2/Zed-x86_64.dmg";
        sha256 = "01e6ede9d5ab7e21e54e448e0a13f9d123b1106ce768a22483a8bac9285f083d";
      };
      buildInputs = [ pkgs.undmg pkgs.p7zip ];
      phases = [ "unpackPhase" "installPhase" ];
      unpackPhase = ''
        undmg $src
      '';
      installPhase = ''
        mkdir -p $out/Applications
        cp -r $out/Zed.app $out/Applications/Zed.app
      '';
    })
  ];

The most promising candidate was p7zip, but as of the time of writing, the required update to extract APFS is merged into master, albeit not yet released.

Having spent hours tweaking and wrangling the config, I wanted to find out if there perhaps is a way to reach out into the system's native tools outside the sandbox, leave the pure functional world, and just install the package.

Final

Turns out there is a relatively easy way to install this, you can execute arbitrary scripts in the post-setup stage, so as a last resort I just 'bash'ed this into a temporary form.

I wrote a little install_zed.sh script that attempts to download, mount, and install the Zed release package:

#!/usr/bin/env bash
set -e
# Debugging: Print each command being executed
set -x
LOG_FILE="/tmp/install_zed.log"

{
    ZED_VERSION = "0.135.2"
    if [ -f "/Applications/Zed.app/Contents/MacOS/cli" ]; then
        INSTALLED_VERSION=$(/Applications/Zed.app/Contents/MacOS/cli --version)
        if [ "$INSTALLED_VERSION" == "Zed $ZED_VERSION - /Applications/Zed.app" ]; then
            echo "Chosen Zed version is already installed in /Applications"
            exit 0
        fi
    fi

    echo "Installing Zed v$ZED_VERSION..."

    URL="https://github.com/zed-industries/zed/releases/download/v$ZED_VERSION/Zed-x86_64.dmg"
    MOUNT_POINT="/Volumes/Zed"
    DEST_DIR="/Applications"
    DMG_FILE=$(mktemp)

    echo "Downloading DMG to $DMG_FILE"
    curl -L $URL -o $DMG_FILE

    echo "Verifying DMG..."
    hdiutil verify $DMG_FILE

    echo "Mounting DMG..."
    hdiutil attach $DMG_FILE -mountpoint $MOUNT_POINT

    echo "Copying Zed to $DEST_DIR"
    cp -r $MOUNT_POINT/Zed.app $DEST_DIR/Zed.app

    echo "Unmounting DMG..."
    hdiutil detach $MOUNT_POINT

    echo "Cleaning up..."
    rm $DMG_FILE

    if [ -f /usr/local/bin/zed ]; then
        sudo unlink /usr/local/bin/zed
    fi
    sudo ln -s /Applications/Zed.app/Contents/MacOS/cli /usr/local/bin/zed

    echo "Zed installation complete!"
} 2>&1 | tee $LOG_FILE

Here we use hdiutil, the native and pre-installed tool for mounting the MacOS DMG application images. I added a log file output redirection, trivial version control, and simple binary linking.

This script lives in the same directory as all the rest of my nix config, and in my darwin.nix (so, nix-darwin config) I added this new system.activationScripts instance:

{ config, pkgs, lib, ... }:

{
    # ... other config
    system.activationScripts.installZed.text = builtins.readFile /Users/tomaszsobota/h/install_zed.sh;
}

Then, I ran darwin-rebuild switch, and it worked like a charm. Zed is now available in both my Spotlight search (Cmd+Space) and from cli with just zed.

Thanks, see you in the next one.