Avatar

Distribute your CLI application in just a few simple steps

← Back to list
Posted on 13.05.2023
Image by AI on Midjourney
Refill!

Let's say you made an amazing CLI tool that will for sure change the world one day, and make you a millionaire eventually. But before it turns you into a next Jeff Bezos, you need to distribute the tool.

Surely, social media posts and contextual ad can be of great help here, but even before putting your app out there, you need to make sure, that the target audience can actually get your application.

This is the bare minimum of measures you need to take:

  • Write a build script
  • Write an installation script
  • Automate the creation of the releases
  • Make the repository public, write a README.md file (in case of the application is opensource)

So, let me show you how all that could be achieved.

Writing the build script

My tool was written in Go, which supports compiling for a variety of operating systems and architectures. In this case I have chosen to support MacOS, Linux and Windows, all on the amd64 architecture.

So, the build script may look something similar to this:

👉 📃  script/build.sh
$
#!/usr/bin/env bash
set -e
package_name='application-name'
platforms=("windows/amd64" "darwin/amd64" "linux/amd64")
out='./out/'
if [[ ! -f $out ]]; then
mkdir -p $out
fi
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name=$package_name'-'$GOOS'-'$GOARCH
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi
env GOOS=$GOOS GOARCH=$GOARCH go build -o $out$output_name ./cmd
if [ $? -ne 0 ]; then
echo "Could not build for ${platform}"
exit 1
fi
done
The code is licensed under the MIT license

For every combination of a system and an architecture, it makes a binary and puts it to the .out folder.

Writing the installation script

When the application is getting more and more popular, it totally makes sense to invest efforts into to making it available via all popular package managers, such as Homebrew, Chocolatey, Yum, APT, etc. However, at the very beginning writing the installation script may be just enough.

It is common nowadays to see something like this in the README file:

curl -sSL https://example.com/install.sh | sh
The code is licensed under the MIT license

The curl request above downloads the script and sends it directly for execution. We are gonna write a simple script for our application. This script will install the latest version of the application for the user's operating system and architecture.

👉 📃  script/install.template.sh
$
#!/usr/bin/env bash
set -eu
APP_NAME="aplication-name"
FOLDER_PATH="/.${APP_NAME}/bin"
FILE_PATH="/${APP_NAME}"
DST="${HOME}${FOLDER_PATH}"
DST_FILE="${DST}${FILE_PATH}"
VERSION="#VERSION#"
error_and_exit() {
echo "$1"
exit 1
}
check_command() {
command -v "$1" > /dev/null 2>&1
return $?
}
ensure_command() {
if ! check_command "$1"; then
error_and_exit "Command not found: '$1'"
fi
}
ensure_success() {
$* > /dev/null
if [[ $? -ne 0 ]]; then
error_and_exit "Command execution failed: $*"
fi
}
get_architecture() {
local os="$(uname -s)"
local cpu="$(uname -m)"
case "$os" in
Linux)
local os="linux"
;;
Darwin)
local os="darwin"
;;
MINGW* | MSYS* | CYGWIN*)
local os="windows"
;;
*)
error_and_exit "OS type is not supported: $os"
;;
esac
case "$cpu" in
x86_64)
local cpu="amd64"
;;
*)
error_and_exit "CPU architecture is not supported: $cpu"
;;
esac
RETVAL="${os}-${cpu}"
}
download_and_run() {
ensure_success mkdir -p "$DST"
get_architecture
local architecture=$RETVAL
echo $architecture
local url="https://github.com/gannochenko/${APP_NAME}/releases/download/v${VERSION}/${APP_NAME}-${architecture}"
rm -f "${DST_FILE}"
ensure_success curl -LJ -o "${DST_FILE}" ${url}
ensure_success chmod +x "$DST_FILE"
}
echo "Installing ${APP_NAME}, version v${VERSION}"
echo ""
ensure_command mkdir
ensure_command curl
download_and_run || exit 1
echo ""
echo "Success. Make sure to add this line to your rc file: "
echo "export PATH=\$HOME${FOLDER_PATH}:\$PATH;"
The code is licensed under the MIT license

As you can see, the script uses some advanced shell scripting syntax. Also, you may notice the #VERSION# placeholder. This placeholder will be replaced with the latest version number after every creation of a release.

Automating the creation of the releases

To automate the release creation I am going to write a Github Actions workflow. I wasn't good at it, so after 50+ attempts I could make it work :)

👉 📃  .github/workflows/build-and-release.yml
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build
run: make build
- name: Create changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v3
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
- name: Create release
uses: ncipollo/release-action@v1
with:
skipIfReleaseExists: true
artifacts: "out/*"
body: ${{steps.github_release.outputs.changelog}}
token: ${{ secrets.RELEASE_TOKEN }}
- name: Extract version
uses: mad9000/actions-find-and-replace-string@3
id: extract_version
with:
source: ${{ github.ref }}
find: "refs/tags/v"
replace: ""
- uses: actions/checkout@v3
with:
ref: installer
token: ${{ secrets.RELEASE_TOKEN }}
- run: |
sed -e "s/#VERSION#/${{ steps.extract_version.outputs.value }}/g" ./script/install.template.sh > ./script/install.sh
git config user.name github-actions
git config user.email github-actions@github.com
git add .
git commit -m "Next version installer"
git push origin installer
The code is licensed under the MIT license

What it essentially does is on every tag push to the master branch, it first builds the application using the script we have prepared before.

$
git tag v1.0.0
git push origin v1.0.0
The code is licensed under the MIT license

Then it makes a release and attaches the binaries to that release, so any time users can not only get the latest version, but the pre-built binaries of every earlier version.

Then the workflow replaces the version placeholder with the actual version number and pushes the changes back, but into a different branch. You need to have this branch created beforehand.

Write the README.md file

The last thing you need to do is to make a README.md file. For a CLI application the README.md file usually contains these sections:

  • The title and a short description
  • How to install
  • How to use
  • How to contribute
  • The license

I asked ChatGPT to prepare some commonly used template, the result you can see here.

And that's it. Now it is good time to start advertising. Post the link into your Telegram channel, send it to your colleagues in Slack, create a YouTube video, whatever channel suits you best. Good luck!


Avatar

Sergei Gannochenko

Business-oriented fullstack engineer, in ❤️ with Tech.
Golang, React, TypeScript, Docker, AWS, Jamstack.
15+ years in dev.