Publishing a Go Binary via NPM: A Simple Approach
Dec 12, 2024
When distributing CLI tools, NPM's vast ecosystem can be an excellent distribution channel - even for non-JavaScript tools. Here's how I published my Go binary cmt
to NPM using a straightforward installation approach.
The Setup
The key components are:
- A
preinstall
script inpackage.json
- A bash installation script (
install.sh
)
Package Configuration
First, I set up my package.json
to handle the installation:
1 2 3 4 5 6 7 8 9
{ "name": "@segersniels/cmt", "bin": { "cmt": "./bin/cmt" }, "scripts": { "preinstall": "mkdir -p $(npm root -g)/@segersniels/cmt/bin && ./scripts/install.sh $(npm root -g)/@segersniels/cmt/bin" } }
Installation Script
The heavy lifting is done by install.sh
, which:
- Detects the user's OS and architecture
- Downloads the appropriate binary from GitHub releases
- Makes it executable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
#!/bin/bash # Define the package name PACKAGE_NAME="cmt" # Base URL for downloading binaries BASE_URL="https://github.com/segersniels/${PACKAGE_NAME}/releases/latest/download" # Default destination directory (current directory) DEST_DIR="." # Check if a command-line argument was provided for the destination if [ "$#" -eq 1 ]; then # If a destination directory is provided, use it DEST_DIR="$1" fi # Identify OS and Architecture OS="$(uname -s)" ARCH="$(uname -m)" case "${OS}" in Linux*) os=linux ;; Darwin*) os=darwin ;; *) echo "Unsupported OS. Exiting..." exit 1 ;; esac case "${ARCH}" in x86_64*) arch=amd64 ;; arm64*) arch=arm64 ;; *) echo "Unsupported architecture. Exiting..." exit 1 ;; esac # Construct binary name and download URL BIN_NAME="${PACKAGE_NAME}-${os}-${arch}" DOWNLOAD_URL="${BASE_URL}/${BIN_NAME}" # Full path to the target binary FULL_PATH="${DEST_DIR}/${PACKAGE_NAME}" echo "Downloading ${BIN_NAME} to ${FULL_PATH}..." # Download the binary if command -v curl >/dev/null; then curl -L -o "${FULL_PATH}" "${DOWNLOAD_URL}" elif command -v wget >/dev/null; then wget -O "${FULL_PATH}" "${DOWNLOAD_URL}" else echo "Error: curl or wget is required to download the binary." exit 1 fi # Make the binary executable chmod +x "${FULL_PATH}" echo "Download completed. The binary is available at ${FULL_PATH}"
Why This Approach?
This setup has several advantages:
- Simplicity: No complex Node.js wrapper code needed
- Platform Independence: Automatically detects and downloads the right binary
- Automatic Updates: Users get the latest version when they install/update
- NPM Integration: Works with standard NPM workflows (
npm install -g
)
Publishing
With this setup, publishing is as simple as:
1
npm publish
When users install your package globally, NPM runs the preinstall
script, which creates the necessary directory and downloads the appropriate binary for their system.
Conclusion
While there are more sophisticated approaches to publishing native binaries via NPM (like node-pre-gyp
or pkg
), this simple setup works well for straightforward Go binaries. It's particularly effective when you're already hosting releases on GitHub and want to leverage NPM as an additional distribution channel.
Remember to maintain your GitHub releases with properly named binaries that match the patterns in your install script!