A self-hosted Expo EAS Mac Mini Builder
It's been almost a month since my last blog post - sorry about that. I had the same problem I have with many of my projects: novelty factor gone. I said it before, but I'll say it again: I'm not stressing over this blog - I'll contribute as and when I want. Anyway, moving on.
Background
For a while (OK, a very long time), I've been using the EAS --local flag when building Expo apps. Even before EAS was a thing, I'm sure I remember using a local turtle runner to build Expo apps (though to be fair, it's been a long time and the world is just... gestures at everything ... for the past few years).
I do this for 2 primary reasons: time, money.
I work in a company that's worked with a huge number of clients over the years. I can't recall the exact number of clients, but if I was to guess, it's probably been in the region of 45 clients over 5 years, and some clients had multiple projects. Some of these projects will have been web-only, but a large portion of them included a mobile app, and I've only dealt with React Native apps.
In most cases, I'm given enough permissions to setup certificates for signing and distributing apps in order to, y'know, do my job. However, most clients don't want to pay for an EAS subscription.
Most of the clients I work with aren't big names. I don't know the specifics of investment for most clients, but I know that they don't typically want to spend extra money. They don't mind paying for a Supabase Pro plan before production release, but paying for Expo during development doesn't offer most of them a benefit. I have no problem with this.
Unfortunately, I am impatient at times. If I want to test some development feature that needs native code, I have to make a dev build. If I want to do this with the Expo servers, it typically means waiting some time - maybe a few minutes, maybe a few hours.
For reference, I use a 2021 M1 Pro Macbook Pro with 64GB RAM and 2TB storage. It's a powerhouse even today.
Thankfully, Expo allows EAS apps to be built with a --local flag, running the build on your own system. For most clients, I've done this for the past few years. There's been a couple of situations where I haven't been able to do it - such as if I was invited to a project which already had credentials setup and I wasn't given permission to access them - but in general, I usually have the ability to set them up and thus use --local.
So, what are we doing here?
Automation. Sort of. So, my goal is to create a setup where I can use a Mac Mini hosted at home to act as a local builder. I want to create a CLI which will bundle up my code, send it over to the mac mini, and run the build there.
Why not just build it on my laptop? I could do that, as it's what I already do, but my thinking is that I'd rather it be done on a 'static' system where I can run a persistent web server, rather than on a development laptop which is subject to software changes.
I'm a self-host fanatic, but that doesn't mean existing solutions do everything I want. EAS doesn't provide a way to specify a remote URL for a builder, so you either do locally or build on their servers.
What will it actually look like?
I've got a couple of ideas.
The first part is to use a custom config file which might specify things like an EAS access token to enable auth.
The other part would be something like a piped CLI, though maybe 'pipe' is the wrong term. In an ideal world, it would be something like eas build --platform ios --profile production --with-remote-builder https some [dot] builder which would send the build over to the runner at that URL. However, it may need to be something different, such as a CLI which takes the command (+args) as a string.
However, that last one feels particularly problematic and prone to command injection, so my thinking is that I make use of defined commands, such as profile, platform, etc.
What about env variables?
So, this would be potentially problematic, and require spinning up some sort of secure storage. However, I think I have a solution for this: encryption.
Let's say the config file for my CLI is treated like a secret. This file wouldn't be commited to git, but it would be sent along with a request to my CLI.
Within this config file, I could have a secret, something like a private key or other crytographically-secure random value.
I'd zip up my 'userland' code - i.e. the things that I wrote as a developer but not things like node_modules or similar. This zip would be encrypted with the secret value from the config. Within the zip, my config file would be included, but so would env files.
The zip would be sent in the body of the request, while a header would include the secret.
The zip is decrypted, then we try to read the config file, and check if we can parse it, and whether the key matches. If not, we know that the key provided was incorrect (because the parsing would fail).
The cool think about this is that it also opens up the ability to do something very cool: Specify which env file to use.
Why is that useful?
OK, so bear with me here. I love EAS and Expo, and the fact that env variables use common standard resolutions is great, but that doesn't mean it's not sometimes frustrating trying to get it right. Sometimes, I just want the option to provide --env-file along with eas build, because maybe I want a testflight-ready build that's linked to a staging environment without having to manually rename the env files (Which could break things for future builds) and without having to write yet another EAS profile.
I know EAS includes ways to manage env secrets, but I feel more comfortable managing them locally, but having multiple env files is still a groan-worthy point.
Therefore, this CLI could enable me to choose which specific .env file to use when building, perhaps even only including the specific env file in the zip (and excluding any others).
What happens on the builder?
So, picking up where we left, once the request is received and the bundle decrypted, it would run the eas build command with the --local and --output flags, as well as other specified flags. It'd need to auth to EAS of course, which would be done with an access token, which would be in the secret config file, but then the build would be done locally.
Additionally, in the past for one project, I actually wrote a build script which create HTML files which enabled downloading of even internal iOS builds (assuming the device I was downloading it on was provisioned for it during building) even if you're not in the EU. It's the same method that Expo uses to enable downloading via their website. There's some caveats about that, so I can't guarantee it'd work completely, but it's worth exploring.
With this CLI, I could reintroduce that approach to more projects, and perhaps even build an entire web UI around it. Of course, I'd need some auth around it, but since the goal is for it to be entirely internal and self-hostable, I'd have to look at options - maybe PassportJS is still a solid option for auth. Then again, Pocketbase is a database with auth, and is absolutely stellar in my opinion, so maybe that's worth considering.
When?
I'm not sure. I've not started work on it yet, and this blog post is mainly me putting ideas down on paper so they don't fall through my sieve-like memory.
I don't think the concept will be difficult to put together, but like most projects, finessing the finer details is where the longest 10% of time will live.