niels segers

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:

  1. A preinstall script in package.json
  2. 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:

  1. Detects the user's OS and architecture
  2. Downloads the appropriate binary from GitHub releases
  3. 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:

  1. Simplicity: No complex Node.js wrapper code needed
  2. Platform Independence: Automatically detects and downloads the right binary
  3. Automatic Updates: Users get the latest version when they install/update
  4. 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!