Managing dev tools across projects can be messy: The version you installed from Homebrew is outdated or your project actually requires an ancient version of your tools to build. I feel like this era of “lets cross our fingers I get this to work” is now over. At least for me, mise is my new go-to for managing development tools.

Mise-en-place is a tool for managing your development tools, similar to asdf, nvm or rvm. At work, we started using it a few months ago when tuist migrated to it in favor of their homegrown solution. Under the hood, mise is even compatible with a list of backends that give it a vast number of tools to install.

I was reluctant to add it into our project at first since we were already managing our dev tools using Mint and Homebrew. Introducing another tool creates more friction with existing or new developers. Since I didn’t have a choice at the time, we began using it for tuist only.

Today, I have migrated all our development tools over to mise, making it our only dev tool manager. Having one tool with great ease of use makes me very happy. In this post, I want to show how you can manage your iOS development tools using mise, and what pitfalls you may need to look out for.

Why you need a tool like mise

Let’s start at the beginning. Why would you want to specifically manage your development tools and their versions? After all, if you require swiftlint in your project, you could get it via homebrew install swiftlint and call it a day.

When you are switching between different projects, they might require distinct versions of the same tool. Most of the time, this is probably fine but sometimes you are required to use vastly different versions with differences in behavior.

Using Homebrew each time you switch between projects to uninstall the tool and install the correct version of it is tedious and error-prone. Having to manage those tools globally per machine, and individually per developer, even via homebrew, can be automated with tools like mise.

Additionally, mise can make sure that all developers on your team use tools from the same source at the exact same version.

mise in action: How it works

If you are unfamiliar, an environment manager does not install your development tools in your global environment. Instead, it manages the installation separately and makes sure that the correct version of your tools are available when you invoke their command in the respective project directory.

The best thing for me is that mise automatically changes your environment whenever you change your terminal project directory. That is not particularly special to mise but I found it to be the most reliable implementation of the ones I tried in the past.

If a tool is not installed, mise will tell you that it is missing. This even works if you switch between branches and your dependency definition changes.

Setting up

To get started using mise, follow the Getting Started guide. Make sure your mise environment is correctly activated for your shell.

To begin using your project tools, run the use command to install and save the tools to a mise.toml configuration file:

1
$ mise use swiftlint@0.59.1

If you don’t specify a version, it will default to latest. You want to avoid this so you can make conscious tool upgrades and keep your team in sync.

This call will result in a configuration file like this:

1
2
[tools]
swiftlint = "0.59.1"

Now, you can call swiftlint directly from your project directory. Leaving the directory will make it unavailable again. Entering a different project with a mise configuration will use that project’s swiftlint version.

Syncing tools across projects and team members

If there is a change in your mise.toml configuration, mise will automatically change to the required tool at its intended version. However, the tool needs to be installed for that to work, for example because you have used the same version in another project before. Mise will not automatically install any software for you. If it is not available, mise will tell you in your terminal. To correct this, you simply run the install command.

1
2
3
mise WARN  missing: swiftlint@0.59.1
$ mise install
mise swiftlint@0.59.1      ✓ installed

To see the tools and versions you have installed, and what of those the current project requests, run mise ls. The -c parameter limits this to the current project scope.

Managing iOS development tools

With the setup out of the way, you can already manage quite a few tools that you can use for iOS development. Those include:

There is even the option to manage swift itself! However, you probably have a specific swift version available from your active Xcode installation, and the compiler is generally good at compiling old language versions. Its good to have the option here as well, though.

Managing Swift Packages

What really made mise click for me last week was when I discovered that mise now has (experimental) support for Swift Package Manager packages. It will clone the repository and build the swift tool from source.

This is huge since it doesn’t require the tool to be built and available in another supported backend before you can use mise to manage it. Since swift command line tools are often distributed in source form, this is a great way of managing all development dependencies with a single tool. With this change, Mint was out of our toolchain.

Cloning against the GitHub API rate limits

Now, the grass is not always greener on the other side. Since migrating to mise, we have noticed that builds failed because we frequently hit GitHub’s API limit. You want to provide an API token so that you don’t run into any rate limits. It’s still free but needs to be there for your CI/CD pipelines to keep cloning and building your tools.

Activation in CI/CD scripts

To make your tools available in continuous integration environments, you need to activate it explicity. In Xcode build scripts, this is the activation that is needed.

1
2
3
4
# Activate mise and its tools
eval "$($HOME/.local/bin/mise activate -C $SRCROOT bash --shims)"
# Run your tool, like swiftlint
swiftlint

Conclusion

Other than the GitHub API limits on CI/CD runs, I didn’t really find anything that bothers me with mise yet — and the SPM integration is still experimental! Mise is simple to use, yet powerful — and once you have it set up, it gets out of your way, reminding you occasionally to install new versions if needed, and then disappearing again.

While writing this post, I even migrated my homebrew-installed version of Hugo over to mise so that I no longer am surprised by unnoticed tool upgrades after a longer hiatus from the website repository.

This is really the key for me: Having one tool that is generic enough to manage development dependencies for different languages and frameworks, while being powerful enough to handle all these differences for me, and exposing a simple and coherent interface on top of it.

This combination allows for sharing of tools at specific versions across team members, local and CI environments, as well as time (“what version did I use to build this back then?”). Enabling a reproducible development and build environment helps developers focus on the things that matter: building better apps for our users.