~clarkema >_

Fixing Raku’s Readline module to work on macOS

01 December 2023

Due to a series of regrettable life choices that we don’t need to go into here, I find myself wanting to run Raku via Nix on macOS.

In theory I could put together a relatively straightforward flake.nix to drop myself into an environment that has Rakudo and zef available, then zef install any modules I want and I’m all set.

Something like this should do the trick:

{
  description = "An environment with Rakudo, zef, and readline";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs, flake-utils, ... }@inputs:
    let
      forAllSystems = function:
        nixpkgs.lib.genAttrs [
          "x86_64-linux"
          "aarch64-linux"
          "aarch64-darwin"
        ] (system: function nixpkgs.legacyPackages.${system});
    in {
      devShell = forAllSystems (pkgs:
        let
          rakudo_env = pkgs.buildEnv {
            name = "rakudo-env";
            paths = with pkgs; [
              rakudo
              zef
            ];
            pathsToLink = [ "/bin" "/lib" "/share" ];
          };
        in
          pkgs.mkShell {
            buildInputs = [
              rakudo_env
            ];

            shellHook = ''
              export LD_LIBRARY_PATH=${pkgs.readline}/lib
            '';
          }
      );
  };
}

The first module that I want is, of course, Readline, as recommended when starting a fresh Rakudo REPL.

Using modules that rely on native libraries involves a bit of extra work in Nix. By design, the packages that you install don’t interfere with each other. One consequence of this is that installing Rakudo and readline doesn’t automatically mean that any Rakudo modules will be able to see the readline library.

Adding ${pkgs.readline}/lib to LD_LIBRARY_PATH in the shellHook should allow Raku modules to discover the readline shared library, and in fact it works fine on Linux.

Unfortunately, on macOS we get an error:

$ zef install Readline
===> Searching for: Readline
===> Staging Readline:ver<0.1.6>:auth<cpan:fooist>
===SORRY!===
Failed to get the directory contents of '/usr/local/opt/readline/lib': Failed to open dir: No such file or directory

/usr/local/opt...? Hmmmmm...

Skipping past some tiresome investigation, it turns out that the Raku Readline module hides this little gem:

when 'darwin' {
  # needs homebrew installed version of readline
  # macOS ships with incompatible libedit
  $library-match = rx/:i libreadline\.(\d+)\.dylib $/;
  # set version to v0.0 so we can check if it was found
  $version = v0.0;
  # homebrew directory
  @library-path = ('/usr/local/opt/readline/lib')
}

Yup. If you’re on macOS it only recognises the Homebrew version of Readline, and only at that specific path. This is broken even on Homebrew, which nowadays uses /opt/homebrew for new installations. Obviously it’s a complete non-starter within a Nix environment.

Let’s see if we can make this work with some tweaks to Readline.pm’s library-hunting logic.

Step 1: clone the perl6-readline repo locally:

$ git clone https://github.com/lathropd/perl6-readline.git
$ cd perl6-readline.get

Step 2: add a special case for Nix on Darwin before falling back to the existing logic. I don’t know why the Readline module does all this manual fiddling around looking for the library instead of relying on Raku’s NativeCall, but I’m not inclined to mess with it any more than I need to.

when 'darwin' {
  if $*EXECUTABLE.starts-with("/nix/store") {
    $version = v8;
    @library-path = %*ENV<LD_LIBRARY_PATH>.split(":");
  }
  else {
    # needs homebrew installed version of readline
    # macOS ships with incompatible libedit
    $library-match = rx/:i libreadline\.(\d+)\.dylib $/;
    # set version to v0.0 so we can check if it was found
    $version = v0.0;
    # homebrew directory
    @library-path = ('/usr/local/opt/readline/lib')
  }
}

Step 3: we can now install this and give it a try. Fortunately, Zef makes it easy to install a module straight from a local directory:

  $ zef install $HOME/git/perl6-readline
  # Successful installation!
  $ raku
  # ^a, ^e, etc, all work as expected.  Yay.

Success! With this change to the Readline module I can enter a Nix environment on macOS with nix develop, and use Raku -- installed from nixpkgs -- with Readline support.

Step 4: for bonus points, let’s fix Homebrew usage while we’re here. I raised a PR for this a couple of years ago which just replaces the line starting @library-path... with the following:

@library-path = $*SPEC.join($,
  (%*ENV<HOMEBREW_PREFIX> || '/usr/local'),
  'opt/readline/lib'
)

This supports both current and legacy Homebrew layouts.

Publishing changes

After some discussion and encouragement on the #raku IRC channel, I tidied up these two changes and pushed them as Readline v0.1.7. New users on macOS who just run zef install Readline as recommended by the documentation and REPL should now have working line editing.