Max Fierke

is probably debugging something

Personal software engineering principles

Max Fierke

July 28, 2023

Me: Chat GPT write me an intro about principles that avoids making obvious statements, listing the dictionary definition of principle, or regurgitating someone else's intro
Chat GPT: Sorry, I cannot do that. As a language model, I lack the ability to do anything other than clever regurgitation and recombination, or a restatement of scraped information.
Me: Well, bummer.
Me: Okay, Chat GPT write me a segue for this conversation into a blog post about principles that is mostly a list for me
Chat GPT:

As I get further in my career, I'm starting to find that across different organizations, different tools, different types of software, it's easy for me to lose track of what exactly my engineering principles are and what I find important. Earlier in my career, it was easier to keep these sort of nebulous and to myself because my day-to-day was centered around code and delivery of that code. I was less involved with the totality of the code, and more with what I was writing for a particular piece of work. These days, I'm often either working to define some strategy to be implemented with my team or working with other engineers to define strategy that will be implemented by many teams over an extended period of time. I'm still in the code, but I'm not in everyone's code and there are many more people and opinions around to consider.

This post is primarily for my future benefit as, sometimes, I need to be reminded that I have opinions too and experiences from having built some good software (and plenty of bad software) and that I need to advocate for those! I'm skipping over many of the principles that many would consider fundamental, generally good practices (e.g. SOLID for OOP), and sticking to those I feel strongly about.

Here's my MySpace Top 8 of these principles and why they matter to me:

Naming is everything.

I think this has maybe started catching on much more with the increased popularity of domain-driven design and "ubiquitous language", but it's long been joked that naming is one of the hardest problems in computer science. For many things, naming is incredibly important to get right, as changing it can be expensive after a certain point (e.g. domain objects/concepts, database tables, identifiers) or a painful nuisance (API fields, interfaces). I think about names a lot before I commit to them. I think about all the times I've seen a "we should rename this" comment in something written 5+ years ago.

Interfaces tell the story. Tell a good story.

Designing good interfaces and APIs is closely related to naming, but there's a broader goal here around being able to understand what something does by looking at its interface. What are the methods on the object / functions in a module? Do they all make sense together? Can I figure out what this object or module is for by reading them? How does the object or module interact with the rest of the system? Do the data structures make sense as data or do I need to know a lot about the implementation? Do conceptually-linked functions feature similar signatures and work on the same types? Can I predict the name of a method, function, or data type I might need without going back to the docs?

All of these are core elements of telling the story of what, where, why, and how. Hopefully the who... is you! (I'm sorry, couldn't resist.)

Solve it simply first / do it the dumb way before doing it the smart way

This is basically a combination of Gall's Law ("A complex system that works is invariably found to have evolved from a simple system that worked.") and "You Ain't Gonna Need It" (abbr. YAGNI).

Doing things simply is better than doing things complexly. Most people probably don't start out doing things complexly, but there's certainly engineering organizations that worship complexity on some level and so it's important to remember that its not something to emulate if you want to have a good time writing software. Software is all about managing complexity, so there will of course always be some complexity. Just try to keep it related to your domain and interesting complexity you should be solving, and not "how do I turn this reasonably sized service into 30-40 serverless functions I have to orchestrate". Offload complexity where you can.

Doing things complexly also has a tendency of taking a long time, and you shouldn't take a long time to do things you don't know if you'll need. Sometimes that means doing something really simple and stupid to validate that the idea is even worth doing properly. At the very least if it fails, you didn't spend a lot of time writing it and you still get to delete something. Win-win.

The continued existence of both Unix and Windows is a paradox that both validates and invalidates this entire principle.

Boring tools over new shiny tools

New shiny tools are exciting, but play with them on your own unless they solve a real problem you have and there's isn't a better supported existing tool or one you know better. Playing with new shiny tools is fun to do at a hackathon, post about in the blogosphere, hack on late in the evening, talk about at a meetup, etc., but most shiny tools come with the burden of educating your team on it, tying your application to something that hasn't yet proved its longevity, and significantly fewer support resources for when you encounter problems. There's also the inevitable V2 release that solves everything with V1 but breaks every API in a way that is most inconvenient to your application specifically.

There's always exceptions to this, and there's differences based on who's making the new shiny thing, etc, but if you're building a product that isn't "use new shiny tool dot biz" it's best to stick with what you know.

tl;dr: Just use Postgres

Design for easy deletion

This could also be called something like "design for re-usability" but that's boring and can easily encourage the wrong thing. It's fun to think of all the ways something might get re-used but there's a lot of stuff that won't ever get re-used. I like to think about designing for ease of deletion, because there's nothing better than confidently deleting something you know isn't being used anymore and can easily remove because it's not tightly coupled to a bunch of other stuff.

The advice for either re-usability to easy deletion is the same: write Good™ interfaces, separate concerns/build it as a library, etc.

Duplication is better than the wrong abstraction

This is sort of the flip-side to the one above and one of the many lessons I took from Sandi Metz, who coined the phrase. I think every programmer has a phase in their career where "DRY" is gospel and any duplication must immediately be vanquished. There's a lot of good reasons not to do this from "that seems like a lot of work and it's only in three places and they're not related" to "oh god why are there so many nested conditionals to build out this copy string". But the best reason is that sometimes stuff just isn't super well-defined yet and a little duplication while you're figuring things out is not such a naughty thing.

A big reason DRY caught on is that it comes from an era where tooling was less mature, certain disciplines were less mature, and people were trying to avoid ever having to update inline styles on 150+ individual HTML files that contained the same layout boilerplate. DRY is a good goal of course, but it is mostly telling you to use a template engine appropriately.

Have a point-of-view

a.k.a. the thing I remind myself: "have a fucking opinion!"

Opinionated software is software that has a clear point of view as to what problems its solving and behaves consistently. Maybe it provides some knobs, but generally things should have well-paved, blessed paths, and not a build-your-own Smörgåsbord for different use-cases (unless of course you're building Smörg.ly, the Smörgåsbord of enterprise market analysis).

Sometimes this means saying no to people who suggest or want particular changes. You or your team has probably spent a lot of time thinking about what you're making, so don't be afraid to say no (politely) if it doesn't match the point of view you're software is taking.

If someone has a differing point of view for what they think the software should be doing, they can either convince you or make their own software. It's fine to have different ways of doing things! (Obviously, this works differently in open source vs. at a business.)

Pragmatism over dogma

Hey, that opinion thing I said? Yeah, turns out a lot of people also have those. Sometimes you need to just agree on something, get on-board, and ship something Deliver User Value. We're trying to solve problems here, not win a theological debate. Solve the problem.

There are reasons to put your foot down and wrong ways to go about solving problems, but many things can be revisited.

A story of rabbit holes and stack climbing: mstrap v0.5.0

Max Fierke

October 28, 2022

Last Updated

November 2, 2022

I have this habit of starting projects, coming across some sort of roadblock while working on them, spending a bunch of time going down the rabbit hole to address the roadblock, and then working my way back up the stack. That's an egregious misuse of mixed metaphors, but I'm sure it's a phenomena many are familiar with. mstrap v0.5.0 has been tagged and released, and it is one such journey of roadblocks, rabbit holes, and stack climbing. The changelog is relatively meager for a release of software with over a year's worth of changes. There's a few new features, some bugfixes, but overall the bulk of the work was on one thing: Apple Silicon support.

Poor timing

I'm a big fan of the move towards ARM64 generally. The Apple Silicon chips have been really impressive and I was excited to support them in mstrap, despite the first M1 laptops releasing right around the time the first public mstrap version was released.

I wasn't psyched about the idea of a tool designed specifically around bootstrapping new machines not having support for the new machines people would be setting up from then on out, so I got to work on determining what was needed for Apple Silicon support. Unfortunately, the first roadblock came almost immediately: support within Crystal, the language mstrap is implemented in.

Language support for Apple Silicon was iffy during this period of late 2020/early 2021, in which partial support existed in some languages from folks contributing with the Apple A12-based dev kits prior to the release of the first M1 machines. However, with a language like Crystal, where the ecosystem is still rather small and the community is developing, there hadn't been any work on Apple Silicon during the dev kit period, and so it was not until the M1 machines came out that issues started being opened around adding Apple Silicon support.

Adding Apple Silicon support to Crystal

Romain Franceschini opened that issue on the Crystal issue tracker in December 2020 with the first important piece for supporting Apple Silicon: the libc bindings. Luckily, the ABI support was mostly already there. Apple's ARM64 implementation has a few differences from the main ARM64 ABI, but Romain had linked the relevant Apple documentation that explained those differences and they mostly did not affect Crystal's implementation.

I was able to follow along with what he had done so far to cross-compile a mostly-working Crystal compiler, along with figuring out the pieces to support Apple's choice to use their own target triple (arm64-apple-darwin) and not supporting something like aarch64-apple-darwin (aarch64 being the canonical term for ARM64, at least in the LLVM world.) This was merged in February 2021 in time for Crystal's 1.0.0 release, but this was just the beginning for supporting Apple Silicon with Crystal.

Cross-compiling from source

At this time, in order to cross-compile a Crystal program, you would need to first compile Crystal yourself because the official macOS builds did not ship with ARM64 support. After that, you would also need to cross-compile something called libcrystal.a, which was a small C library responsible for setting up a signal handler for segfaults in Crystal.

Unfortunately, this added an annoying hurdle to cross-compiling, which is already an annoying endeavor that requires you to cross-compile other dependencies. Crystal itself provides some great facilities for binding to C libraries, including libc, and so it became clear that it would be possible to port this to Crystal and remove the need for this awkward build step. This required exploring how signals are implemented across a few platforms in order to add bindings for various signal data structures, but the result was a nice simplification of the build process.

I wrote up a blog post describing the process for cross-compiling the compiler for ARM64, so at least those who were interested could follow a charted path, but it was still inconvenient compared to an official binary release.

Distribution is hard

Crystal was still only setup to ship amd64 releases for macOS, which meant that anyone who wanted to run the compiler needed to run it through Rosetta. In addition to that, the version of LLVM shipping with the release builds was a version too old to support ARM64 code generation for macOS (LLVM 7). I worked with Brian Cardiff on the Crystal core team to update the version to LLVM 10, which didn't officially support Apple Silicon, but supported enough of the pieces for it to work. LLVM 11, unfortunately, carried a bug that affected the ability to for a Crystal compiler compiled with it to compile (try saying that ten times fast.) Next, we had to recompile LLVM 10 again to add in support for targeting AArch64.

At this point, I was a bit stumped on how best to get an ARM64 build out of CI. There was talk about eventual CircleCI macOS ARM64 runners, but it seemed unlikely those would materialize soon. Instead, I started looking at cross-compiling for ARM64 from x86_64 and getting that into CI somehow. During that process, I tried a few attempts at separate native and cross-compiling builds, but that duplicated the amount of macOS resources needed, and felt more complex that it needed to be. Eventually, I stumbled upon a tweet from Apple compiler engineer Kuba Mracek:

Apple Silicon Mac porting tip #8: For Makefile-based, CMake-based and other non-Xcode projects, building universal is often just about adding CFLAGS="-arch arm64 -arch x86_64", and the compiler and the linker will handle that — they will create universal .o files, link them...

Kuba (Brecka) Mracek (@kubamracek), June 23, 2020

I had forgotten about universal macOS binaries and that they had been effectively revived from the PowerPC-to-Intel days to now take a role in the new era of Intel-to-ARM64 transition. This tip meant that we could create so-called "fat" libraries targeting both architectures without changing much about the overall build infrastructure, so it was a relatively easy solve for the various C dependencies needed. Unfortunately, it was a little bit more complicated for the Crystal compiler itself.

Crystal itself doesn't support building universal binaries, and I didn't feel like taking a crack at enhancing the compiler to do code generation for two targets at once, but luckily I found out about the lipo tool. lipo takes two binaries for different architectures and spits out a single universal binary. This meant all that was needed then was to compile Crystal natively, then cross-compile it for ARM64, and then pass both builds through to lipo. There were a few tricks to doing this, but ultimately we got it working and it was merged in September 2021, and the first universal macOS build of Crystal with support for ARM64 was released with Crystal 1.2.0.

Adding Apple Silicon support to mstrap

Making mstrap itself support Apple Silicon was actually quite easy. There wasn't a whole lot to do here, other than ensure that Homebrew on Apple Silicon got installed to the right place. By Crystal 1.2.0's release, Docker Desktop had come out with support for Apple Silicon and had squashed most of the bugs that affected the preview releases in the winter and spring of 2021, so there was little to do there. Unfortunately, again, distribution and cross-compiling were the chief roadblocks.

Cross-compiling mstrap

mstrap, being a Crystal program, also depends on the same things Crystal depends on. This means that libraries like libpcre, libevent, libgc (bdw-gc), and openssl needed to be cross-compiled for the target architecture. These are shipped in official tarball releases, but are absent in the more commonly used Homebrew releases. The official tarball releases are mostly useful for compiler bootstrapping and don't track the latest library versions, so the shipped dual architecture libraries are not really intended for consumption by anything other than the compiler. mstrap also has some specific requirements around statically linking as much as possible, as many libraries may not be present on a new machine, adding another layer of complexity to an already complex story.

Unfortunately, cross-compiling mstrap prior to v0.5.0 involved a Docker-based setup for cross-compiling static binaries within an Alpine container for both amd64 and x86_64 on Linux. This wouldn't help for macOS, and frankly the whole setup was slow and cludgy anyway. I wanted something better and didn't want to reinvent a bunch of cross-compilation machinery. Tools like Autotools and CMake, which are used by most of the libraries depended on by mstrap, already support cross-compiling and you just have to bring a suitable compiler. Luckily, clang is already a cross-compiler, so I really just needed to bring the libraries and build something to enable that same kind of cross-compiling of mstrap and all of its dependencies without specialization across platforms.

In my experience, Autotools is pretty easy to use as a user: you run a ./configure script in the project, maybe customize some options, run make, and at the end, you get your output. Autotools supports a bunch of cross-compiling options for a bunch of targets that are the same no matter what project you're compiling, so you can be reasonably sure cross-compiling is at least supported by the build tooling. Unfortunately, Autotools is also a) incredibly complex and b) doesn't solve the problem of cross-compiling dependencies for me, and writing it or debugging it is a terrible way to spend an afternoon.

CMake solves a number of the same problems as Autotools with a more approachable configuration language than Autotools macros and supports sub-projects, but writing CMake configuration also did not excite me.

Enter Meson

The Meson build system had been on my radar for a bit, after seeing lots of projects starting to migrate to it. I wasn't entirely aware of how it was different, but I knew it existed and decided to take a look. It's ethos was exactly what I was after:

The main design point of Meson is that every moment a developer spends writing or debugging build definitions is a second wasted. So is every second spent waiting for the build system to actually start compiling code. mesonbuild.com

Meson is similar to CMake in a number of ways: it supports sub-projects, can automatically find dependencies, supports cross-compiling, and lots of other cool stuff. What really sold me on trying it, though, was its concept of Wraps. Wraps provide a way to wrap and package the rules for building various dependencies with Meson turning it into a library package manager of sorts. Instead of writing Meson rules for compiling different libraries, I can just pull in community-provided rules. Many of the projects I need are served by the Meson WrapDB: bdw-gc, libpcre, openssl, and zlib. They're not the latest versions, but they're recent enough. The only two not covered were readline/libedit and libevent. Two packages seemed doable.

One nicety of Meson is that it provides some integration with CMake. There are some caveats, but more or less you can include a CMake project into your Meson build and let Meson do the heavy work of configuring and building it, assuming there's nothing too complex going on. libevent supports CMake, so it was relatively straightforward to integrate into the project's meson.build.

Readline (or libedit, the BSD version) was the last remaining library. Prior to v0.5.0, it was used for a handful of interactive prompts in mstrap. However, I quickly learned that cross-compiling libedit might be one of the hardest problems in computing, and after spending way too long wading through and reverse engineering various parts of the build configuration, I thought "There's Gotta Be A Better Way™", and replaced it with Term::Prompt, a terminal solution built in Crystal without readline/libedit (well, almost)

With all the native libraries cross-compiling, it was finally possible to wire up Meson and update the build to support a more robust cross-compiling (and static linking) experience, culminating in a pleasing revamp of the build system that was merged in April of 2022. All that remained was to release a new version of mstrap with builds for both macOS on x86_64 and arm64. For some reason, this took me six months to do. Whoops.

What's next?

There's still more to do on better support Apple Silicon in Crystal itself. LLVM bugs have caused some issues, and the official builds are still pinned to LLVM 10 (though support for LLVM 12, 13, and 14, have been added). Ironically, mstrap itself will not compile on ARM64 with the official Crystal builds built with LLVM 10 due to bugs not fixed until LLVM 13, and there are likely other code generation bugs hiding or outstanding. Perhaps I will find myself diving back down this particular rabbit hole again.

mstrap: a tool for automating your development machine

Max Fierke

June 4, 2021

So I've been a bit withholding of something, for now over 2 years, and I think it's time to let it out into the world more broadly and get some use.

mstrap (short for “machine bootstrap”) is a tool for provisioning and managing a development environment. It is a convention-over-configuration tool, which aims to leverage existing ecosystem tools to provide a one-command provisioning experience for a new machine.

animation of an mstrap demo run

What does it do?

Very broadly, the tool is built to not use any dependencies so there's absolutely nothing to install on a fresh machine. You can provide it with a remote config file written in HCL (e.g. in a gist or elsewhere) to bootstrap some information about yourself and optionally different profiles that define your projects, where to find them, and how to run them. mstrap will setup your system with some sensible defaults using either Strap (on macOS) or strap-linux (on Linux), and then proceed to pull in your projects, setup their various language runtimes with ASDF and install dependencies, and setup an NGINX config and locally-signed cert (using mkcert) for any web projects. There's much more to it all than that, but I will let the documentation site speak for itself (linked at the end of this post).

How usable is it?

I've been using it on all my personal machines for the past couple years. I'm overall pleased with how it works. There's still lots more opportunities for things to build, but I'm particularly proud of the fact it can automate the setup from out-of-the-box OS to functioning development machine in 30 mins to an hour (not a promise, just how it tends to work out). It does this all by leveraging lots of other tools, written by smart, dedicated people, particularly the Homebrew, ASDF, Docker, mkcert, and ruby/node/python-build folks. While it's an amalgamation of opinions I happen to have about development environment setup, I will try my best to make it easy to opt-out of things that are not core to the experience (e.g. use of Docker, mkcert, nginx are all optional) and that might have lots of competing opinions.

Why now?

I have at various points tried to do big pushes to get it ready for a big v1.0.0 release or something on a New Years or whatever other arbitrary point on the calendar I decide to be significant. All of these have fallen through for one reason or another, and I was planning to delay it again in hopes of completing Apple M1 support. Sharing it with others before tagging a v1.0.0 seems particularly prudent in order to get feedback on some of the assumptions and opinions embedded in some of the design decisions, which I would like to get feedback on before declaring any level of stability. To end a short story made long, I'm sharing it now, mstrap v0.4.0.

Read more about mstrap on mstrap.dev or view the code on GitHub

So You Want to Compile Crystal on ARM64 (macOS)

Max Fierke

April 13, 2021

Last Updated

November 11, 2021

UPDATE 2021-10-15: Crystal 1.2.0 shipped with an official macOS Universal build with both x86_64 and arm64 built-in, and there is now a native arm64 bottle available in Homebrew. Going forward, you don't need to do any of the below, unless you're really curious to start from scratch.


Mid-February, I was able to get a PR merged upstream into Crystal for aarch64-apple-darwin support (i.e. Apple Silicon) based on some great work by Romain Franceschini to get the libc bindings and a lot of the foundational stuff ready. This PR also shipped with Crystal 1.0.0, and I was hoping that the cross-compilation story would get a lot easier, so I put off writing a post on targeting arm64. Unfortunately, there's been some speed-bumps I won't get into here, but suffice to say it's not quite possible to brew install crystal via Homebrew yet or run via an official M1 binary build either.

However, I wanted to revisit the process for cross-compiling your copy of the Crystal compiler on the Apple M1 for those that are daring and not willing to wait. My hope is to keep this somewhat updated, so that as things evolve and change, eventually this'll be a pretty simple set of steps. Already, writing this in early/mid April, things have gotten a little bit easier. bdw-gc in Homebrew now ships with the necessary multi-threading patch for Crystal. In addition, I landed another PR to rewrite the Crystal segfault handler in Crystal, instead of C, making it easier to cross-compile (this shipped in 1.1.0.)

Environment preparation

Clone Crystal's source code (if you don't already have it)

This step and the rest of this post is going to assume you're comfortable with compiling things from scratch, basics of Git, and know a little bit about using compilers and linkers, as you'll need to lean on this knowledge if anything has deviated since the time this post was written/updated. So, let's get started by cloning a copy of Crystal's compiler. I'm going to clone it in ~/src/crystal but you can clone it anywhere you wish:

$ cd ~/src # Or whereever you keep your source code
$ git clone https://github.com/crystal-lang/crystal.git
$ git checkout 1.1.1 # The latest version as of this post being updated

Installing Homebrew(s)

We'll need two Homebrews, one for each CPU architecture, in order to cross-compile Crystal on the same machine. This is because we need to bootstrap the compiler from somewhere, and what better way than to bootstrap it from the x86_64 compiler via Rosetta 2, which we can also run on the machine.

Install x86_64 Homebrew (aka. Intel Homebrew)

Run the following to install Intel Homebrew, which we will occasionally refer to as ibrew throughout this post:

$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Once this finishes, you should have a working Homebrew install in the default x86_64 prefix of /usr/local

ibrew calls specified later will be in reference to this Intel Homebrew install.

Add an alias for Intel Homebrew

In your shell or your shell's config (.bashrc, .zshrc, etc.), add an alias for ibrew:

alias ibrew="arch -x86_64 /usr/local/bin/brew"

Install arm64 Homebrew (aka. ARM Homebrew)

Run the same line again, this time removing the explicit arch call in front of it. It should run under arm64 by default, unless your Terminal is setup to run under Rosetta (Stop doing that now, if you are using your Terminal in Rosetta mode. It'll make the rest so much harder/impossible, and I cannot help you.)

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

This should prompt for installing into /opt/homebrew, the default prefix for Apple Silicon Homebrew.

Regular brew calls specified later will be in reference to ARM Homebrew.

Add ARM homebrew to your PATH

In your shell or your shell's config, add ARM brew to your path:

$ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

This is to ensure ARM software from Homebrew runs before Intel software.

Install Crystal dependencies

LLVM 10+ is required for targeting Apple Silicon, so we need to make sure we install LLVM on both installations of Homebrew, and all the other dependent libraries needed, including build-time dependencies.

$ ibrew install llvm@11 bdw-gc gmp libevent libyaml [email protected] pcre pkg-config
$ brew install llvm@11 bdw-gc gmp libevent libyaml [email protected] pcre pkg-config

Grab an official macOS binary build of Crystal for x86_64

We'll need this for later, when compiling the compiler the first time. You could also install with ibrew as well.

$ mkdir -p ~/crystal-prebuilt
$ curl -fsSL https://github.com/crystal-lang/crystal/releases/download/1.1.1/crystal-1.1.1-1-darwin-x86_64.tar.gz > ~/crystal-prebuilt/crystal-1.1.1-1-darwin-x86_64.tar.gz
$ cd ~/crystal-prebuilt
$ tar -xf crystal-1.1.1-1-darwin-x86_64.tar.gz

Compiling Crystal for ARM64

Setup compile environment variables

Since we'll be targeting two different CPU architectures, we need to be extra careful about what build environment we specify. Intel Homebrew lives in a higher priority directory for most build tools, so it is often checked first and will be used instead of ARM Homebrew's directories unless we specify ARM Homebrew's build tools instead. We'll need to do this a lot, so lets specify some environment variables up-front:

$ export INTEL_BREW_PREFIX="$(ibrew --prefix)"
$ export INTEL_LLVM_ROOT="$INTEL_BREW_PREFIX/opt/llvm@11"
$ export INTEL_LLVM_CONFIG="$INTEL_LLVM_ROOT/bin/llvm-config"

$ export ARM_BREW_PREFIX="$(brew --prefix)"
$ export ARM_LLVM_ROOT="$ARM_BREW_PREFIX/opt/llvm@11"
$ export ARM_LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config"

Compile for x86_64

The first step is to compile for x86_64. Yep, x86_64. We need to do this, because the official binary builds of the Crystal compiler ship compiled against LLVM without the ability to cross-compile for AArch64/ARM64 on Darwin, so we need to re-compile the compiler against LLVM 11 so that we can have the ability to cross-compile for arm64. In the future, this may not be necessary but for now it is.

$ LLVM_CONFIG="$INTEL_LLVM_CONFIG" \
   LDFLAGS="-L$INTEL_LLVM_ROOT/lib" \
   CPPFLAGS="-I$INTEL_LLVM_ROOT/include" \
   CC="$INTEL_LLVM_ROOT/bin/clang" \
   AR="$INTEL_LLVM_ROOT/bin/llvm-ar" \
   CRYSTAL="$HOME/crystal-prebuilt/crystal-1.1.1-1/bin/crystal" \
   arch -x86_64 make

Cross-compile to arm64

The next step is to take our x86_64 compiler and use it to cross-compile for the arm64 target triple (aarch64-apple-darwin).

$ LLVM_CONFIG="$INTEL_LLVM_CONFIG" \
   LDFLAGS="-L$INTEL_LLVM_ROOT/lib" \
   CRYSTAL_CONFIG_TARGET=aarch64-apple-darwin \
   CRYSTAL_CONFIG_LIBRARY_PATH="$ARM_BREW_PREFIX/lib" \
   arch -x86_64 ./bin/crystal build src/compiler/crystal.cr --cross-compile --target aarch64-apple-darwin -Dwithout_playground

Next, we need to compile the LLVM extension libraries for arm64:

$ LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
   LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
   $ARM_LLVM_ROOT/bin/clang -I$ARM_LLVM_ROOT/include -c -o src/llvm/ext/llvm_ext.o src/llvm/ext/llvm_ext.cc

Finally, we need to link our cross-compiled compiler object (crystal.o) with our arm64 llvm_ext.o and the rest of our arm64 libraries, in order to get a final arm64 executable for the Crystal compiler.

$ LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
   LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
   CPPFLAGS="-I$ARM_LLVM_ROOT/include" \
   $ARM_LLVM_ROOT/bin/clang crystal.o -o .build/crystal  -rdynamic -L$ARM_BREW_PREFIX/lib src/llvm/ext/llvm_ext.o `"$ARM_LLVM_ROOT/bin/llvm-config" --libs --system-libs --ldflags 2> /dev/null` -lstdc++ -lpcre -lgc -lpthread -L$(brew --prefix libevent)/lib -levent -liconv -ldl

In this final step, it's super easy for x86_64 libraries to get picked up instead of or in addition to the arm64 libraries. If you get a working compiler at the end but have a bunch of warnings about the linker trying to link against an x86_64 library, you can safely ignore those. If you don't get a working compiler at the end, it's possible you're missing an arm64 version of a required library, and you'll need to specify a more direct path via the linker arguments.

Verifying the compiled compiler

Now, at the end, we should be able to verify that we have a working arm64 crystal compiler:

$ file .build/crystal
.build/crystal: Mach-O 64-bit executable arm64

(Optional): Running compiler and stdlib test suite

This step is probably optional if you just want to compile some of your Crystal programs for arm64, but if you want to be extra sure the compiler works okay or are working on Crystal compiler or standard library development, you may want to run the test suite.

We're doing this directly to avoid triggering unnecessary rebuilds via the Makefile, but it's likely possible to just call make spec with the same environment variables specified and have the right thing happen.

Compiling and running the compiler specs
$ CRYSTAL_PATH=$(pwd)/src \
   CRYSTAL_LIBRARY_PATH=$ARM_BREW_PREFIX/lib \
   CC="$ARM_LLVM_ROOT/bin/clang" \
   LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
   LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
   CPPFLAGS="-I$ARM_LLVM_ROOT/include" \
   PKG_CONFIG_PATH="$(brew --prefix [email protected])/lib/pkgconfig" \
   ./bin/crystal build -o spec/compiler_spec spec/compiler_spec.cr -Di_know_what_im_doing

$ CRYSTAL_PATH=$(pwd)/src \
   CRYSTAL_LIBRARY_PATH=$ARM_BREW_PREFIX/lib \
   CC="$ARM_LLVM_ROOT/bin/clang" \
   LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
   LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
   CPPFLAGS="-I$ARM_LLVM_ROOT/include" \
   PKG_CONFIG_PATH="$(brew --prefix [email protected])/lib/pkgconfig" \
   spec/compiler_spec
Compiling and running the stdlib specs
$ CRYSTAL_PATH=$(pwd)/src \
  CRYSTAL_LIBRARY_PATH=$ARM_BREW_PREFIX/lib \
  CC="$ARM_LLVM_ROOT/bin/clang" \
  LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
  LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
  CPPFLAGS="-I$ARM_LLVM_ROOT/include" \
  PKG_CONFIG_PATH="$(brew --prefix [email protected])/lib/pkgconfig" \
  ./bin/crystal build -o spec/std_spec spec/std_spec.cr --target aarch64-apple-darwin -Di_know_what_im_doing

$ CRYSTAL_PATH=$(pwd)/src \
   CRYSTAL_LIBRARY_PATH=$ARM_BREW_PREFIX/lib \
   CC="$ARM_LLVM_ROOT/bin/clang" \
   LLVM_CONFIG="$ARM_LLVM_ROOT/bin/llvm-config" \
   LDFLAGS="-L$ARM_LLVM_ROOT/lib" \
   CFLAGS="--target aarch64-apple-darwin" \
   CPPFLAGS="-I$ARM_LLVM_ROOT/include" \
   PKG_CONFIG_PATH="$(brew --prefix [email protected])/lib/pkgconfig"
   spec/std_spec

Compiling Crystal programs on arm64

Now that we've got a working Crystal compiler for arm64, we can go about compiling our arm64 binaries.

Or we could. But we probably won't get very far yet. We still need to compile shards for arm64 so that you can install dependencies. However, compiling shards is a fairly good demo for how to compile any Crystal program for arm64, as it is just another Crystal program.

Compiling shards

$ git clone [email protected]:crystal-lang/shards.git
$ cd shards
# If $HOME/src/crystal isn't where Crystal is cloned for you, update the path in CRYSTAL_PATH and CRYSTAL
$ CRYSTAL_PATH=lib:$HOME/src/crystal/src \
   CRYSTAL_LIBRARY_PATH=$ARM_BREW_PREFIX/lib \
   CRYSTAL=$HOME/src/crystal/.build/crystal \
   make install

This should install it into /usr/local/bin, which we can verify by running the below command:

$ which shards
/usr/local/bin/shards

$ file $(which shards)
/usr/local/bin/shards: Mach-O 64-bit executable arm64

Compiling other programs

In general, you can probably get most of the way there with your programs by providing CRYSTAL_PATH, CRYSTAL_LIBRARY_PATH, and CRYSTAL as specified above if you have a Makefile-type setup where CRYSTAL can be provided. Otherwise, you can invoke the arm64-compiled crystal binary directly while providing CRYSTAL_PATH and CRYSTAL_LIBRARY_PATH. These two values are needed, because we're not using an omnibus build where these values would already be set by a wrapper script. Instead, we have to provide them and be explicit so that the compiler knows where to look up Crystal code and linkable libraries, respectively.

Depending on the libraries used in your application, you may need to supply other typical environment variables that you might be expected to supply anyone, the most common being for OpenSSL 1.1, e.g. PKG_CONFIG_PATH="$(brew --prefix [email protected])/lib/pkgconfig"

HACK: Mimicking the Omnibus Crystal wrapper

Instead of passing the CRYSTAL_PATH and CRYSTAL_LIBRARY_PATH all over, we can also add a thin shell wrapper to provide those as part of the crystal command. This is similar to what the official omnibus builds do, where there's a top-level crystal shell script which sets the environment properly and passes off to the actual crystal binary.

Add this script to your PATH somewhere (don't forget to make it executable):

#!/bin/sh

# Again, if you installed somewhere other than $HOME/src/crystal, change it:
export CRYSTAL_PATH=lib:$HOME/src/crystal/src
# This should be ARM homebrew's lib
export CRYSTAL_LIBRARY_PATH=$(brew --prefix)/lib
# Mad temporary. You might want to copy this build else where and point it to that less-temporary location
export CRYSTAL=$HOME/src/crystal/.build/crystal

exec "$CRYSTAL" "$@"

This is a bit of a temporary improvement to ergonomics, but should reduce the amount of pain when working in Crystal regularly on an M1.

Future Work

In future, I am working on trying to get aarch64/arm64 macOS support into the omnibus setup, but long-term this will require having Apple Silicon in CI, which would require some involvement from the core team.

In the meantime, there is an open PR for building universal binaries for macOS, which will hopefully get merged by the Crystal core team when they're able.

Accomplishments & Challenges in 2018

Max Fierke

December 30, 2018

Hi there. How are you? Good? Good. Glad to see you here. How are your parents? Still trapped in that Winnebago, huh? Have you tried WD-40? Oh, they like it in there? Okay, moving on...

As we go into 2019, one of my resolutions is to use this, my personal site & blog, more and platforms like Twitter and Medium less. I'd like to kick that off by writing up this post on my accomplishments for the year 2018. I don't think I've ever written one of these before, but it seems like something that would be helpful to have for posterity.

Accomplishments

2018 was ultimately a pretty good year for me personally & professionally in a number of ways. I referenced some of these on Twitter.com. There were also of course not-so-great things to happen as well, but here's what I'm proud of:

  1. I finally acquired a puppy. Her name is Tessa. She's a little over a year old now. I'm not 100% sure what breed(s) she is, but my current guess is a Beagle & Rat Terrier mix (which the internet has told me is called a Raggle). She's adorkable and you can see photos of her on my Instagram
  2. In May of 2018, I returned to Germany for the first time in 18 years to see relatives, many of whom who had not seen me since then. It was my first trip there as an adult with an ability to speak some amount of German. My family is all from the former East Germany, so none learned to speak English in school growing up. I was not as good of a conversationalist in German as I was hoping, but I was able to listen and understand most conversations, which was incredibly valuable. I'm hoping to return in 2019.
  3. I was promoted to a "Senior Engineer" at work, which felt very validating. Because I started programming in junior high and did a lot of open source work before I worked professionally, I've always had a hard time with titles and determining where my non-professional & open-source experience fits in with my professional experience. Now it feels like my title more accurately reflects what I feel my capabilities are and that's rad. Mentoring is now a more explicit part of my job, as well, which has been a great so far!
  4. Moved into an apartment for humans, rather than ants. For the last two years, I lived in a <700 sqft 2bd apartment with a roommate. It was small and was difficult to keep all my things in. In July/August, I moved into a ~1200 sqft 2bd apartment, which allowed me to get a puppy! Yay!
  5. Automated a ton of things so I could spend fewer weekends messing with configuration and rebuilding servers and such. I got all of my side-project infrastructure provisioning centralized into one repo with a bunch of Ansible roles & playbooks. It's easy as a few playbook runs for me to recreate any of my side-project infrastructure, and I have done so this year. I also automated installation of my dotfiles across all my machines, so every machine I use works as I expect (It's FANTASTIC.)
  6. Found an artistic outlet in building a video game. Technically, I started this in late 2017, but I did a lot of artwork creation in the early part of 2018. Unfortunately, a lot of this went on hold by the summer, but I'm going to make it more of a priority in 2019.
  7. Caught up in Game of Thrones. I'd been stuck on Season 2 for like 5 years. Finally got HBO Now and binged my way through the rest of it. Very excited to see how it ends in 2019.
  8. Restarted my vinyl collecting in earnest. Another benefit of moving into a new, larger apartment, is that I finally have a place to store my vinyl and a permanent place for my record player. I've picked up a number of really cool things this year, including an 1967 pressing of Sgt. Peppers Lonely Hearts Club Band in very good condition, a translucent green pressing of Green Day's Warning, and finally, after six months of scouring local record shops, a copy of Gil Scott-Heron's Pieces of a Man.
  9. Traveled a ton (mostly for work). I went to Boston a handful of times. I spent two weeks in Germany & Switzerland. I went to Chicago a couple times. I experienced the Cincinnati airport (It's a vegetable-less ghost town.) I spent a week in Denver.
  10. I contributed a bunch of stuff! I wrote a blog post on ember-concurrency with GIFs. I released an ember addon called ember-concurrency-retryable. I became a maintainer for ember-tooltips. I contributed to the crystal compiler. I built a couple things for funsies, like an elevator simulator and a steganography tool. I made progress on porting reptyr to Rust.

Not So Great Stuff About 2018 That I Hope to Improve in 2019

2018 had some downs as well. Here's some of those:

  1. Experienced some health stuff & wasn't drinking coffee for most of 2018. I had some weird gut stuff going on towards the end of 2017, that led to me having stomach pains whenever I consumed coffee, kombucha, and alcohol, among other things. It seems to be all better now. It seemed to be partly due to the side-effects of a medication I'd been taking. And partly because I had developed a small hernia near my navel. I switched medications and had the hernia repaired, and I'm able to consume all those things again.
  2. I was much more of a hermit in 2018. This is a bit of a perennial problem, but I'd made some improvements in years past. This year, it was partly due to health stuff, since I often see people at bars and such, and not really being able to drink made that difficult. More frequent travel also contributed. As did my killer AV setup and PS4 that I acquired in the beginning of 2018. I also have a tendency to be singularly-focused when I'm working on something I'm excited about, so it's easy for weeks to go by before I realize I've not been seeing people as much as I should.
  3. I stopped working out. In 2017, I was probably in the best shape I'd been since high school. My old apartment's rec room was less than 10 feet away from my apartment door, which served as a constant reminder to work out. In my new apartment, it's on the other side of the building. The first few months of living in the new apartment with a new puppy were a bit hectic and I had to be thoughtful about when I left her alone. She's mellowed out a lot recently and can be trusted to be left alone & out of her crate in the apartment for a couple hours, so I'm hoping to get back into working out regularly in the new year.

That's it! I'm going to sit down and plan out some goals for 2019.

A New Concurrency Primitive

Max Fierke

October 14, 2017

No code is more concurrent than code that does not run and then runs all at once, suddenly.

In these modern times, it seems everyone is catching the concurrency bug. At EmberConf 2017, everyone and their elderly friend from down the street named John was talking about ember-concurrency. There were at least 10 talks that were mostly or only about the add-on wunderkind. Furthermore, articles by the author of the add-on have been declaring ember-concurrency as the magic bullet to solve problems you didn’t even know you had. Even Twitter was alight with admiration for this hot new craze.

ember-concurrency is good

Before ember-concurrency, I had to think about things like how to handle failure gracefully and what to do about retrying asynchronous work. I would think about all those things. And then not do them. Because programming is hard and I already spent so much time getting the happy path to work. If the user experienced a network error, that was just their fault for not plugging directly into the Ethernet port outside of Amazon’s data center, like I do. I thought that I shouldn’t have to adapt my applications to workaround every common failure case. But I was wrong. Very wrong. Bad Max. ember-concurrency really saved my bacon. Literally. I used it in the app for my newly-announced startup BaconSaver, which allowed me to launch several requests to grocery APIs, searching for the best bacon prices. Managing all that asynchronous work would have been impossible without ember-concurrency. But it’s still too complex. I have think about concurrency strategies. Should I drop or keepLatest? And then, it has to run code. Ugh. It’s 2017, people. We pretend servers don’t exist now.

But…

I have a Computer Science-related undergraduate degree from an average-ranked public university, so I know what concurrency means. (But I won’t tell you.) Because I definitely know what it means, I am qualified to say that concurrency (and therefore ember-concurrency, a special kind of concurrency) could be a lot better in Ember. As someone who knows what concurrency means, I can tell you that having to run code drastically reduces concurrency, probably. Luckily, I have a solution. I’m announcing in this blog post ember-procrastination. ember-procrastination improves the concurrency story of our Ember apps by optimizing when code gets run.

Did you know?

Did you know that almost no language implements procrastination primitives? Of course, Java offers a ProcrastinationFactory, .NET has System.Enterprise.ProcrastinationServices and Haskell has that powerful Procrastinate monad. However, to date no one has ever implemented anything like this for Ember or JavaScript. Sad.

How to use Ember-Procrastination?

(Check out this demo at Ember-Twiddle)

ember-procrastination introduces a new concurrency primitive called a someday . A someday is a lot like a task from ember-concurrency, but has the special property that it will only schedule and do work when prompted several times. This means that only work the user truly wants done will get completed. But be careful: if you ask too much, it may get mad and cancel work already in-progress. Such truly concurrent code can be finicky. ember-procrastination also leverages the best in lazy code loading technology to ensure that we don’t execute expensive operations until the last possible moment. To do this, ember-procrastination uses an advanced Just-In-Time (JIT) feature present in modern JavaScript: the beforeunload event. When ember-procrastination detects this event, all code that has been previously prompted to run that has not yet been run will run, ensuring that all work is completed. And it all happens concurrently. Amazing.

You can try it today. Simply yarn add --dev ember-procrastination to your project.

Seize The Future, Someday

This revolutionary add-on puts the control back into your user’s hands. Technology does too much for them already anyway. Give your users a chance to go outside and experience nature. Before it all starts on fire.

Max’s Tips for Very Good Air Travel

Max Fierke

March 24, 2017

You may be traveling without even realizing these ten very important things.

These days, I work for a company where I sit in a chair and do complicated brain things. Occasionally, they tell me to go get on a large metal sky bird, or airplane, to sit and not do brain things until I get to another place, where I promptly return to sitting and doing brain things. It’s an exciting life and I am very lucky and happy. I have experienced much of this so-called air travel during the past year or so, so I am sharing some of my tips free of charge, because I am also wonderful and mildly attractive.

Tip #1: Arrive early for the airport.

Airlines are all about timing. To ensure that you are one of the lucky ones who makes it to the plane and does not get eaten by Bernhardt the Senior Flight Attendant, make sure you aim to get there two hours before your flight, but be sure to arrive at a multiple of three and get to the TSA checkpoint on a multiple of five. They are very strict about the timing and will not hesitate to send you to the back of the line. If you’re worried, try to bring your friend Jerry who always ends his sentences with “I am Jerry, I was a Mathsss major”.

Airport

source: upload.wikimedia.org

Tip #2: Stop at the Duty-Free

These days, it’s really rare to find fine products that aren’t covered in shit. The Duty-Free stores at the airport carry only products not covered in shit, so it is a good opportunity to stock up.

Tip #3: Say “Hi” to Sylvester

You will inevitably run into someone from your past on a work flight and it will probably be Sylvester. Be sure to say “Hi” when you see him and then promptly ignore him for the rest of your life. You have given him all you can give. If you acknowledge him later, it will open up a portal to Hell and fill the next 7 years with darkness and despair, so please don’t fuck it up for everyone.

Samsung DVD-C500 DVD Player with HD Upconversion. source: BestBuy.com

Tip #4: Do not bring your Samsung DVD-C500 DVD Player with HD Upconversion onboard

This summer, the FAA banned the Samsung DVD-C500 from all American passenger aircraft, as it poses a serious fire hazard. Do not bring your Samsung DVD-C500 with you when you fly. It will still be at home when you get back and you can return to watching the Season 2 DVD of Reba.

Tip #5: Say no to babies

Sometimes, the airline will offer you to take a baby at the gate. This might seem cool and it might seem fun to get to show all your friends at the bar your cool new baby you got from your Southwest flight, but do not take it. It is another airline trick. Whoever takes the baby has to sit in the baby seat with the baby who also does not like airplanes, probably because it’s parents are the airline. Unfortunately, there’s someone on every flight who takes the free baby and you will be forced to hear their cries “I did not mean to take the baby, waaaaahhhh!”

Tip #6: Do not shout “I LOVE SEAT 5F” from the top of your lungs as soon as you sit down

Obviously, seat 5F is the best of the best when it comes to airline seating and most people know it. However, be really careful and avoid declaring your continuing love for seat 5F at the top of your lungs. It may seem crazy, but some people did not know about seat 5F and not everyone got to be seated in 5F, so shut the fuck up, you lucky little so-and-so, or Bernhardt the Senior Flight Attendant will have your head.

Sky coffee will not look as good, though. source: upload.wikimedia.org

Tip #7: Ask for the hottest coffee available

This is a certified TravelHack™. When the flight attendants come around with beverages, ask for the hottest coffee available on the aircraft. The flight attendants will be impressed with your moxie and will return from the hold with a piping hot pot of coffee, flaming at the top. All of the other passengers will cheer and your legacy will finally be sealed.

Tip #8: Do not eat the peanuts

Do not eat the peanuts, they are a classic airline trap. If you eat the peanuts, you will become Bernhardt’s personal assistant, tasked with playing doubles tennis with him on his private court for eternity, until someone says “Excuse me, do you think it would be okay if I continue using my phone on this flight, I have really important business things to say and I am in first-class”, which will break the spell.

This is what clapping looks like. source: Flickr

Tip #9: Clap Enthusiastically When the Plane Lands

It’s very important that you clap enthusiastically when the plane lands at your destination. It is the only way for the pilot and co-pilot to know that their spell worked and the plane landed successfully. It is also the only way for them to exit their trance without someone obtaining ginger root and four elven toes, placing them directly in the pilot’s mouth, and chanting “Nickelback is a proper musical group and not a community service project”.

Tip #10: Be sure to exit the plane

I know, sometimes it seems hard. The plane is so comfortable, warm, and sponge-like, but you must resist the urge to stay on the plane and take up residence. Only Bernhardt is allowed lodging on the plane and he is not looking for roommates. You must leave the plane.