Feedback: darwin.installApplication

A big problem I’ve run into with using nix on darwin is the paucity of macOS-specific applications. I’ve seen a number of other people comment on this as well. However, macOS has pretty common distribution methods for .app files.

I was thinking it might be useful to develop a way to lower the threshold of knowledge needed to add a standard macOS application to nixpkgs. It turns out that jwiegley had already developed something for his config (nix-config/30-apps.nix at c8f18be4a6beb7c621c411e7b7c3548282ea8abb · jwiegley/nix-config · GitHub). With permission, I used it as a base. I’ve modified it further the goal of maximizing simplicity.

A resulting darwin.installApplication-based expression could look like so:

{ darwin }:

darwin.installApplication rec {
  name = "f.lux";
  version = "41.1";

  app = "Flux.app";
  url = "https://justgetflux.com/mac/Flux${version}.zip";
  sha256 = "181aycdjvvmzvzm9jdb17593z7rxlcmj1x9xi7064hw6lipylarb";

  description = "Reddens computer screen in the evening";
  homepage = "https://justgetflux.com/";
  maintainers = [ "spease" ];
  license = "free";
}

The two places I decided to make tradeoffs from being idiomatic to being more user-friendly:

  • I swap out src/fetchurl for url and sha256 attributes. I figure this eliminates any need to understand or know of fetchurl or function calls. If it proves to be problematic (lots of packages that need to set src to something other than fetchurl), I figure it’s relatively trivial to mechanically convert installApplication calls to use fetchurl. If there’s a way to make installApplication require either src or url and sha256, I’d be happy to know.
  • I inline the meta attribute and index the maintainers and license attributes inside of installApplication. Again, this just reduces the nix-specific syntax involved. There’s no need to bring in lib or have a with statement this way, and since lib.license and lib.maintainers do get indexed in darwin.installApplication, any constraints are just as enforced as if the user had referenced the variables directly. This works fine for licenses, although in the case of maintainers, it doesn’t look like you need to enter something that exists.

With both these changes, I feel this reduces the syntactic complexity to <= that of a brew file. For comparison, here’s the brew cask for f.lux:

Actual implementation of installApplication:

{ fetchurl, lib, stdenvNoCC, undmg, unzip}:
{ app, description, homepage, license, maintainers, name, sha256, url, version, postInstall ? "", ... }:

stdenvNoCC.mkDerivation {
  inherit name version;

  nativeBuildInputs = [ undmg unzip ];

  sourceRoot = app;
  src = fetchurl {
    inherit url sha256;
  };

  phases = [ "unpackPhase" "installPhase" ];

  installPhase = ''
    mkdir -p "$out/Applications/${app}"
    mv * "$out/Applications/${app}"
  '' + postInstall;

  meta = with lib; {
    description = description;
    homepage = homepage;
    license = licenses."${license}";
    maintainers = forEach maintainers (x: maintainers."${maintainer}");
    platforms = platforms.darwin;
  };
}

Note that one other change I make is using mv instead of cp -pR - I did this because I figured mv will be more performant than cp for large files, and will implicitly preserve any file attributes, but if this will break something down the line I’m happy to change it.

Also, would love to know what the current accepted practice is to handle “.” in a package name…

Thanks!

1 Like

Don’t see a reason not to have this in nixpkgs.

This has been working pretty well so far.

I’ve refined it a bit to deal with some (but not all) spaces issues, and .pkg files.

{ cpio, fetchurl, fixDarwinDylibNames, lib, stdenvNoCC, undmg, unpkg, unzip }:
s@{ description, homepage, license, maintainers, name, version, postInstall ? "", ... }:

stdenvNoCC.mkDerivation rec {
  inherit name version;

  nativeBuildInputs = [ cpio fixDarwinDylibNames undmg unpkg unzip ];

  sourceRoot = ".";
  src = if builtins.hasAttr "src" s then s.src else fetchurl {
    name = builtins.replaceStrings [ "%20" ] [ "-" ] (builtins.head (builtins.match ".*/([^/]+)" s.url));
    url=s.url;
    sha256=s.sha256;
  };

  phases = [ "unpackPhase" "installPhase" ];

  installPhase = if builtins.hasAttr "installPhase" s then s.installPhase else ''
      # .dmg files or compressed Applications
      app=( ./*.app )
      if [ ! -z "$app" ]; then
        mkdir -p $out/Applications
        mv -n "$app" $out/Applications/
      fi

      # .pkg files
      if [ -d "./usr/local" ]; then
        mv -n ./usr/local/* $out/
      fi

      if [ ! -L "./Applications" ] && [ -d "./Applications" ]; then
        mkdir -p $out/Applications
        mv -n ./Applications/* $out/Applications/
      fi
    '' + postInstall;

  meta = with lib; {
    description = description;
    homepage = homepage;
    license = licenses."${license}";
    maintainers = forEach maintainers (x: maintainers."${maintainer}");
    platforms = platforms.darwin;
  };
}

where unpkg takes after undmg:

{ lib, stdenvNoCC, xar }:

stdenvNoCC.mkDerivation rec {
  version = "1.0.0";
  pname = "unpkg";

  nativeBuildInputs = [ xar ];
  setupHook = ./setup-hook.sh;

  src = ./.;

  installPhase = ''
    mkdir -p $out/bin
    ln -s $(command -v xar) $out/bin/xar
  '';

  meta = with lib; {
    description = "Extract a pkg file";
    license = licenses.gpl3;
    platforms = platforms.all;
    maintainers = with maintainers; [ spease ];
  };
}
unpackCmdHooks+=(_tryUnpackPkg)
_tryUnpackPkg() {
  if ! [[ "${curSrc}" =~ \.pkg$ ]]; then return 1; fi
  xar --dump-header -f "${curSrc}" | grep -q "^magic:\s\+[0-9a-z]\+\s\+(OK)$"
  [ $? -ne 0 ] && return 1
  xar -tf "${curSrc}" | grep -q "/Payload$"
  dir="$(mktemp -d)"
  xar -xf "${curSrc}" -C "${dir}"
  zcat ${dir}/*/Payload | cpio -idm --no-absolute-filenames
  rm -rf --preserve-root "${dir}"
}

I’m not happy with the tmpdir since if zcat / cpio fails, I’m concerned it will leave the tmpdir hanging. But I don’t know of a good way to fix this.

Another thing I’ve been considering is whether to split it into ‘installApplication’ and ‘installPackage’ (for .pkg files)? The .pkg handling could hypothetically be a lot more sophisticated.

Finally, I wonder if for some packages it might make sense to run install_name_tool. eg for one package which hardcoded a path to a library, I had to do:

preFixup = lib.optional targetPlatform.isDarwin ''
    install_name_tool -add_rpath "${jlink.outPath}/lib" $out/bin/nrfjprog
  '';

which could be automated to iterate through the binaries in the final bin directory and add the lib directories of all the buildInputs to the rpaths.

The issue in that case was that the library needed to be in a path discoverable by dlopen (according to the documentation), and the fixDylib package wasn’t sufficient. This was decidedly non-discoverable and took me awhile to figure out.

EDIT: Also note that the app attribute is no longer needed.

To allow either url or src, you can use optional arguments like { ... src ? null ...}: ... and use assert to ensure that there’s nothing missing/conflicting.

I think meta should be done this way too.