March 2025
Recently I’ve been working with repos that have multiple languages and build systems rolled into one. Talking about iOS/Android/Rust/Node/Flutter. The plethora of build systems, tools and quirks brings a lot of complexity, specially when working within a team, where every one needs to be able to run the same commands to build, test, lint, etc.
From working with other teams, each language/ecosystem have their own way of doing things. Some of the common patterns I’ve seen are:
make
, rake
, Ninja
, etc.You can see the wild west that this endevaour is. These all work but require too much finagling to get right. I wanted something that was simple, easy to read, and easy to write. Preferible in a language/ecosystem that I know. JS is the easiest one, but then it’s one more tool in the chain that my team needs to install, but then found a tooling pair that allows for one install command that takes care of everything.
I’m a big fan of tool managers. Journey started with asdf
and I’m now using mise
. mise
is a tool manager that allows you to install tools from a single file. It’s like a package manager for languages/runtimes/tasks/etc. It basically allows you to define a single mise.toml
where you can have per folder tools.
Let’s say, I can have the specific bun/node version that I need my team to have. Without having to seat down with them to uninstall their manual node installation, install nvm, then update their Rust version. etc etc.
zx
is a package from Google that allows to write better scripts in JS. Basically invoke shell commands within a JS file without dealing with the idiosincracies of bash/zsh/whatever shell you are using.
At the end, I have a mise.toml
that looks like this (this is just an example):
[tools]
node = "14.18.1"
bun = "0.1.0"
rust = "1.58.0"
[hooks]
postinstall = "bun install"
[tasks]
build = "bun zx scripts/build.mjs"
And a scripts/build.mjs
that looks like this:
import "zx/globals";
// You can do more things here, like parse the arguments, import other files, etc
await $`cargo build --release`;
await $`flutter build ios`;
Then on the README for my team it gets simple boils down to:
brew install mise
mise settings experimental=true
, this is needed to enable the hooks, will go away in the futuremise install
mise build
to build the project. On my project I have extended this with params like mise build ios debug
to build the iOS app in debug mode.No more fighting with tool versions, no more fighting with build systems. Everyone is on the same versions, with a reproducible yet native environment, with a single entry point for scripts and the scripts themselves written in a non-retarded language.