Emacs is open source. Like, you know, really open source. GPLv3-or-later open source.
Which is neat in some abstract continuity of software freedom of rights sense. But it’s also neat in a very concrete sense: we just ran into a problem with Emacs, and we can fix it.
Quick refresher: I’m trying to write tests with pictures in them, but when I scale the pictures up, they get really blurry:
Seems like an easy fix, right? I’m not going to try to make this behavior configurable or alter the image API or something complicated. I’m just going to change Emacs to unconditionally use nearest-neighbor resampling instead of unconditionally using bilinear resampling. Should be a one-line change.
But, of course, that one line change carries with it a heavy price.
Because we’re going to have to figure out how to build Emacs.
how hard could it be
Well, let’s see.
The installation instructions span multiple pages. There’s no explicit list of dependencies that we need, but reading between the lines it seems like we definitely need the GCC toolchain, HarfBuzz, maybe something called intlfonts
… and some subset of these libraries too, depending on which image formats we want to support:
libXaw3d
libxpm
libpng
libz
libjpeg
libtiff
libgif
librsvg2
And then maybe we want to compile… --with-imagemagick
? But it says that’s not the default because of security reasons. So maybe we don’t want to do that?
I dunno; we can go through this whole file and eventually get to a working incantation and we can finally build Emacs and start working on the image change.
But our build just immediately segfaults, so we give up.
oh no
Okay, that was a walkthrough of what would have happened if I tried to make this change a year ago.1
Actually, I probably wouldn’t have tried to make this change a year ago, because I’ve been using computers long enough to know that it would take me like six hours to actually figure out how to build Emacs, and this tiny little change is not worth six hours of fighting with autoconf
. That’s just… that’s not how I want to spend my weekend. I want to make a little game.
But now?
Now I have a secret weapon.
A secret weapon called Nix.
wait no come back
Earlier this year I spent some time learning how to use Nix. It was every bit as weird and complicated and difficult to learn as everyone said it was going to be, but I stuck with it, and I’m glad I did.
Because now I don’t need to figure out how to build Emacs. I can just ask Nix how to build Emacs, and it will tell me. I don’t need to struggle through a long document telling me in English words how to get it building – someone has already translated the build instructions into a Nix expression that describes exactly what dependencies I need and exactly what to do with them.
But of course… that’s true of any system package manager, right?
Like, if I were still using Homebrew, I could have looked at the Homebrew formula. It specifies its dependencies; it specifies the exact build invocation it uses. If I wanted to change something about Emacs, why wouldn’t I start there?
Honestly, it would never have even occurred to me to use Homebrew to patch Emacs. I’m sure it’s possible. I’m sure there’s some way to fork the formula locally and figure out how to build it from source. But I never would have thought to do that.
I think of Homebrew as just a command line way to download binaries from the internet. I’ve never had to package something for Homebrew; I’ve never tried to write formulae for my own software. When I was using Homebrew, there was a sharp line between “software I install with Homebrew” and “software I build from source” (which included any software I wrote myself).
But with Nix… well, you don’t use Nix because it lets you download binaries from the internet. You use Nix because it makes it easier to build software – because it lets you specify your systems dependencies in a cross-platform, computer-readable way.
And in order to use Nix at all, you have to understand quite a lot about how it works. You have to understand how to package software, how to fix problems with buggy packages, how to apply patches like this – things that you can happily ignore if you’re using Homebrew.
So while I think of the ability to easily patch Emacs as a superpower that Nix gave me, in reality I think it’s a superpower that a deep understanding of any system package manager would give you. I’m sure there is a Homebrew equivalent, and a pacman-equivalent, and an rpm equivalent of all the things that we’re about to do.
let’s get to it
So the patch itself is trivial. It was, in fact, a one-line change:
--- a/src/macterm.c
+++ b/src/macterm.c
@@ -1927,2 +1927,3 @@ mac_draw_image_foreground (struct glyph_string *s)
flags |= MAC_DRAW_CG_IMAGE_2X;
+ flags |= MAC_DRAW_CG_IMAGE_NO_INTERPOLATION;
mac_draw_cg_image (s->f, s->gc, s->img->cg_image, s->img->cg_transform,
And, you know, it took a little while to look through the source and figure out what was going on and to write that one line. But that wasn’t very interesting work – I just grepped around for references to image drawing until I found a function that looked promising. I did not, like, learn very much about how Emacs works in the process.
But actually applying that patch was a little trickier.
And I know I was just talking about how Nix makes it easy to patch things and run your own software and package stuff and all that. And that’s… usually true.
But it’s less true in this particular case. Because I don’t run mainline Emacs – I actually run Mitsuharu Yamamoto’s macOS-augmented fork. It’s great and I would highly recommend it, but… building it is a little more complicated than building vanilla Emacs.
Not that much more complicated. But I had to stumble a little bit before I could get it working.
At a very high level, the emacsMacport
derivation works like this:
- Download the Emacs source
- Download a patch containing all of the macOS-specific changes
- Apply the patch to the Emacs source
- Actually build the patched source
So there’s really nothing crazy about that, right? It’s pretty simple. Nix is good at patching things.
But the emacsMacport
happens not to use Nix’s built-in patching mechanism. Why? I don’t know. Probably because of the shape of the Mac patch or something that requires some manual re-shuffling. There is a little bit of strange directory structure re-arranging happening in the derivation.
But I wanted to use Nix’s built-in patching mechanism, because it’s nice and I’m lazy. But the emacsMacport
derivation was written in a way that was incompatible with that.
Which is not a problem – it was very easy to fix. I ended up putting the following in my config.nix
:
$ cat ~/.config/nixpkgs/config.nix
let overridePackageAttrs = overrides: pkgs:
let update = with pkgs.lib; (flip recursiveUpdate); in
pkgs.lib.mapAttrs (packageName: attrsOrUpdater:
let updater =
if builtins.isAttrs attrsOrUpdater
then update attrsOrUpdater
else attrsOrUpdater;
in
pkgs.${packageName}.overrideAttrs updater)
overrides;
in
let
noInterpolationPatch = ''
diff --git a/src/macterm.c b/src/macterm.c
index 2cdcd6eb11..627c15bed4 100644
--- a/src/macterm.c
+++ b/src/macterm.c
@@ -1927,2 +1927,3 @@ mac_draw_image_foreground (struct glyph_string *s)
flags |= MAC_DRAW_CG_IMAGE_2X;
+ flags |= MAC_DRAW_CG_IMAGE_NO_INTERPOLATION;
mac_draw_cg_image (s->f, s->gc, s->img->cg_image, s->img->cg_transform,
'';
in
{
allowUnfree = true;
packageOverrides = overridePackageAttrs {
emacsMacport = attrs: {
prePatch = attrs.postPatch;
postPatch = "";
patches = [ (builtins.toFile "no-interpolation.patch" noInterpolationPatch) ];
};
};
}
Okay, so overridePackageAttrs
is a little helper I wrote that just makes it easier to declare overrides. Nothing fancy there.
And note that I had to move the mac patch to the prePatch
phase in order to use the built-in patches
mechanism. Also note that I was lazy and just put the patch in as a string literal instead of in a separate file.
And it turns out that was a big mistake.
See, when I was first trying to build Emacs with this patch, I kept getting errors about the patch not applying. And I checked it again and I made sure that the patch really did apply and that I did everything right and I was looking at the same version of the code as Nix and all that stuff.
And yep, I was looking at exactly the same version of the code. I applied to patches myself and made sure the line numbers matched up and all that.
I’m not actually sure how I finally realized my mistake, but eventually it hit me:
When I pasted that patch into my config.nix
file, my editor converted the tabs to spaces. And it took me a long time to figure that out, because I honestly can’t remember the last time I’ve even seen an actual tab character in source code. But the Emacs macport code? It’s full of them.
Of the roughly 6200 lines in macterm.c
– the native Mac drawing code – around 1200 are indented with tabs. The rest are indented with spaces.
And the line I added just so happened to sit between one line indented with tabs and one line indented with spaces.
:(
So here’s the actual patch I had to apply:
let
tab = "\t";
noInterpolationPatch = ''
diff --git a/src/macterm.c b/src/macterm.c
index 2cdcd6eb11..627c15bed4 100644
--- a/src/macterm.c
+++ b/src/macterm.c
@@ -1927,2 +1927,3 @@ mac_draw_image_foreground (struct glyph_string *s)
${tab}flags |= MAC_DRAW_CG_IMAGE_2X;
+ flags |= MAC_DRAW_CG_IMAGE_NO_INTERPOLATION;
mac_draw_cg_image (s->f, s->gc, s->img->cg_image, s->img->cg_transform,
'';
in
cool story ian
Okay yeah look I know that watching me struggle to apply a trivial patch to a text editor you don’t even use is not the most exciting thing.
But stop and think a little bit about the bigger picture idea here.
You use software all the time. You use git and ripgrep and fzf and tmux and jq and zsh and all kinds of things that make your life better.
And all of that software is open source. Which means that if something doesn’t work the way you want it to, you can, in theory, dive in and fix it.
But there’s a pretty high cost associated with that. Every project has its own weird unique build system; every project has its own way of documenting its dependencies. There’s this high upfront cost that you have to pay just to start making changes to any software that you want to work differently.
And then – even if you do fork something and build it yourself – now there’s this additional ongoing cost of maintaining your fork. You have to pull upstream and rebase your changes on top of it. You have to keep on top of new releases, so you don’t miss security patches or whatever. You can’t just run brew upgrade
anymore.
So even if something bothers you about the software you use every day – even if you find a bug in tmux
, or wish that you had some additional jq
function or whatever – you probably aren’t going to do anything about it. It’s not worth it; it’s too hard.
I am obviously projecting here: that is the way that I thought before I started using Nix. I never would have tried to change this Emacs behavior back then.
But now?
Now I can get a working build environment for any package I want, just by running nix-shell
. I can write a patch, and I can ask Nix to automatically apply that patch to the latest version of the package. I don’t have to worry about maintaining my own fork or manually updating it – I can keep using all the same commands to list and upgrade and uninstall packages, regardless of what I’ve done to them.
It’s very cool, but it’s not without its downsides. I talked about how emacsMacport
is a little weird, but it is far from the weirdest package out there. Sometimes you might spend more time fighting weird Nix things than you would if you just built the package from scratch… especially if a project has Python dependencies. And yeah, Nix can apply patches for you, but if the patches fail you’ve gotta do a little work to regenerate them.
I point out those downsides because because I don’t want you to think that I’m trying to convert you to my weird cult. I really like Nix, and I feel like it’s allowed me to do things that I wouldn’t have tried without it. But you could probably get the same psychological benefit by mastering apt-get or whatever.
Nix has the advantage of being cross-platform, which is nice for me: I split my time between macOS and Linux, and even on Linux I like having a distribution-agnostic package manager. And I do think that Nix has a lot going for it that other package managers don’t… but I can’t swear to that, because I know a lot more about Nix than I do about any other package manager.
So: maybe give it a shot? Check it out? I dunno. You might like it. You might not. If you’re curious to learn more, I wrote some words about my experience learning Nix earlier this year: How to Learn Nix, A Tragic Love Story.
aren’t we supposed to be writing a game
Okay gosh yes okay. That was a long tangent.
Let’s get back to the game. We wrote the patch. We got it building. But did it actually work?
Heck yeah.
We did it.
We wrote a test framework. We wrote a test. We patched Emacs to make our test look slightly prettier. We talked a lot about macros and hygiene and weird quirks of Janet.
Now maybe – just maybe – we could try to do something actually a little bit game-related?
-
And I did try it to build it like this, just to see if I was being unfair to Emacs. But I stopped as soon as I produced my segfaulting binary. I’m sure I did something very dumb, and I know that you have no problem building Emacs from scratch, but I have to find ways to work within my ability. ↩︎