I had a 3-day weekend and decided to learn Rust while also putting together an idea I’ve had for a few years now: chim.

Background Link to heading

The problem I’ve wanted to solve is bootstrapping projects for local dev and CI/CD. Essentially, how tool versions are defined and installed—tools being things like programming language runtimes, package managers, and linters. For example, a project might require node-v18.8.0, jq-v1.6, and shellcheck-v0.8.0. Other versions of these tools may not be compatible.

Local Dev Link to heading

Locally you might use asdf1 or nvm2. They’re good for making it when you cd into a different project change the local versions—for instance one project might be running node-18 and another might be running node-16.

The downside is they rely either on complex shims or shell extensions in order to work—sometimes both. They’re often a source of problems for beginners and experienced developers alike since they break expectations of what $PATH should be doing. To illustrate: you typically can’t just run which node to see exactly which node runtime is being used—asdf just say ~/.asdf/shims/node.

Production Link to heading

In production, we generally only need a single version and automatically switching on cd is not helpful. asdf and similar tools usually just get in the way and are more work to configure.

It’s common in production to install the dependencies in a Dockerfile. This can be done locally, but many devs (myself included) find trying to code inside a Docker container painful. It’s either a chore to get files syncing correctly, forgetting that you’re in (or not in) the container, or you’re dealing with poor performance emulating x64 on an ARM Mac.

Even if Docker isn’t used, production still likely has some way of installing dependencies like that is cumbersome locally (e.g.: ansible or chef).

Dev/Prod Parity Link to heading

Ultimately what we want is the ability to synchronize the toolset versions across dev and prod. For example, there should be a single file that defines which version of node is being used in my project and if I change that setting, it will change the version in production as well as for my fellow developers when they pull the latest changes.

This is the goal of chim.

What chim does Link to heading

A “chim” is a universal binary shim that (typically) defines a tarball, checksum, and path to a platform-specific binary inside the tarball. For example, this is a chim for nodejs (imagine it’s localed at ./bin/node):

#!/usr/bin/env chim
url = 'https://nodejs.org/dist/v18.8.0/node-v18.8.0-darwin-arm64.tar.gz'
path = 'node-v18.8.0-darwin-arm64/bin/node'
checksum = '4952a8ec7ca07328571ba0b06d228c2a8220041a6f07df4f46765c341a80ccd4'

url = 'https://nodejs.org/dist/v18.8.0/node-v18.8.0-linux-x64.tar.gz'
path = 'node-v18.8.0-linux-x64/bin/node'
checksum = '01c2060503bb42caa1c6cc2ee4b432f80c0b38ad46b4eed956774fb36302f46e'

url = 'https://nodejs.org/dist/v18.7.0/node-v18.7.0-win-x64.zip'
path = 'node-v18.7.0-win-x64\node.exe'
checksum = '9c0abfe32291dd5bed717463cb3590004289f03ab66011e383daa0fcec674683'

All that you need to do is install chim, create this file, make it executable, then it can be run with ./bin/node. Other developers can add this directory to $PATH (possibly with direnv automatically when entering a project directory).

When the chim is run for the first time, it downloads the tarball, verifies the checksum, extracts it to a cache directory, then executes node. Future invocations will just execute node directly. I’ve measured the overhead for these future runs at ~1ms on my machine (thanks to Rust) so it shouldn’t impact performance significantly. (For reference, node -v without chim takes ~15ms on my box.)

This shim can easily be used in production as well. Just add the directory to $PATH and now it’s using the same shims in production.

Chimstrap Link to heading

For a single binary this isn’t terribly helpful since instead of installing node we now need to install chim before we can run the shim. To alleviate that, there is a bootstrapping script that provides a platform independent executable of chim itself called the “chimstrap”.

The chimstrap is just a shell script that behaves similarly to chim. It will download chim from the GitHub Releases, and store it in a cache directory for future invocations, and execute chim. It can be used like this:

$ curl -o ./chim https://chim.sh/chimstrap
$ chmod +x ./chim
$ ./chim -v
chim 1.0.0

chim checksums Link to heading

Updating checksums is bit of yak shaving that can be avoided with chim checksums. Update the URLs3, run chim checksums ./path/to/chim and it will update all of the checksums in the chim.

Let me know what you think! Link to heading

It’s a simple project, so it should be easy for me to support. Let me know if it works for you or if it’s missing something that would be helpful for your use-case.

Footnotes Link to heading

  1. asdf in particular is a poor tool because it increases the time to launch binaries by 150ms (!). This can quickly make working with a project that uses asdf very slow. ↩︎

  2. Only works for node, not other languages and tools. ↩︎

  3. Or just leave the checksums out and chim checksums will add them. ↩︎