Instead of asking your users to install the entire Rust toolchain just to compile your program with cargo, it may be easier to let them install it through npm. Here's how to set it up.
I recently wrote a CLI tool in Rust, called Sweep. I published it on crates.io, the Rust package manager, so people can install it with
cargo install swp. Great! It's published! But this is a program that everyone could use, not just Rust developers, so I wanted to provide a way for non-Rust developers to install it.
Step 0: Research
There are several good solutions to publish binaries to npm, but the most common is using pre-compiled binaries that are downloaded as-needed. The npm package will just be a tiny wrapper that downloads the appropriate binary when it's installed. A lot of native packages on npm use a this approach because it's easy to set up and maintain.
We'll be using a package called binary-install. It's built by a Rust developer and used for the npm package of wasm-pack, a popular tool to compile Rust programs to WebAssemly.
Step 1: Building the binaries
I implemented cross-compilation for Windows, Linux and Mac OS through Github Actions for Project Cleanup. You can check out the build workflow but for this post I'm going to focus mainly on the npm part.
binary-install package requires your files to be in a specific structure:
- Your release should be a
- Inside that archive should be a directory, containing the binary
- The archive filename and the directory name should be the same
So you should end up with something like this:
This is somewhat of a convention to use when releasing binaries, especially on linux platforms. But it's good to note that
binary-install expects exactly this structure.
Creating the files
The following is a simplified version of the build script I use. Adjust paths, filenames and targets where necessary to fit your program.
1. Compile the binary
First of all, we need to build our Rust program.
cargo build --release --target=x86_64-pc-windows-gnu
2. Create the directory structure
Next, create a new directory and copy the compiled binary into that directory. This is the directory that we'll put in our release archive.
It's best to use a separate directory outside of the
target directory, because there are a lot of other files in there that don't need to end up in our release.
mkdir -p builds/my-program-win64 # If you haven't set a target, the path will just be 'target/release/...' cp target/x86_64-pc-windows-gnu/release/my-program.exe builds/my-program-win64
3. Pack the archive
Now all we need to do is pack it all into a nice
tar -C builds -czvf my-program-win64.tar.gz my-program-win64
You should have a file called
my-program-win64.tar.gz now. Repeat as needed for every target you want to support.
Step 2: Creating a release
Once you have all your release archives, create a new release on Github. You can do this by creating a tag locally and pushing it to Github, or by creating a release via the web interface. Check out the Github documentation for more help.
.tar.gz files that you created to the release, write some release notes if you want, then click "Publish release" when you're ready.
Your binaries are ready! Now let's make an npm package that can install them.
Step 3: Making the npm package
I like to keep the npm package and the Rust project together in the same repository, but you can also create a separate project for the npm package if you prefer.
Create a new node project and add the
yarn init -y yarn add binary-install
The example project in the
binary-install repository is very clear and shows us what we need to do — although we're going to make a few tweaks.
Let's also create a new directory called
npm to put our scripts.
The Binary class
If we want to do anything, we first need to create an instance of the
Binary class, provided by the
Let's create a function that returns the proper
We get the current version from the
package.json file, so users can install a specific version of our program. This means that we have to keep our npm package's version synchronised with our release versions.
Don't forget to change the values in
Now you've probably spotted that this will always download the Windows binaries, even on non-Windows platforms. So let's do something about that. We can use the built-in Node.js module
os to get information about the current platform.
That should do the trick. This function will return the proper
Binary instance for each supported platform. If you have different platform support, or different filenames, you can adjust the values and checks in the
Binary instance, we can do 3 things:
run. So let's create a script for each of those.
It may seem a little excessive to create a separate file for each function, but it's the easiest way to call them from a script in
Now that we have the js files for these commands, we'll need to call them from our
package.json. We'll install the binary with the
Next, if our program is already installed and the user installs a new version, we should uninstall the old version first. Let's use the
preinstall hook for that.
Once installed, we want the user to be able to run
my-program from anywhere. Luckily, npm supports a
bin field in
package.json. We just need to point it to a script.
We also need to let the system know that this is an executable script, and to run it with
node. We do this by adding a shebang to the top of the file.
Shebangs are a Unix concept, but npm will take care of the Windows side for us.
First install problem
We're almost done, but there's one more thing. I only learned this after releasing the my npm package, because it's not exactly obvious if you've followed these steps in order.
When installing locally, the
preinstall script is executed before any dependencies are installed. But our script
require()s a dependency. You can probably see where this is going — another developer who clones the repository and runs
yarn install will get an error that dependencies are missing, and they'll be unable to install those dependencies.
We'll solve this with a simple
try/catch and swallow the error. After all, if the dependencies are not found it means the package wasn't installed yet, and that means there's no binary to uninstall in the first place.
Step 4: Publishing
Now our package is ready to publish. But let's make sure we only publish what's necessary for the npm package to work. We can use the
files property in
package.json to define which files get uploaded to npm. The package.json itself is always included, so we just need to add our scripts in the
And that's it. Let's publish!
Join the conversation