Distribute your CLI application in just a few simple steps
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.
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:
#!/usr/bin/env bashset -epackage_name='application-name'platforms=("windows/amd64" "darwin/amd64" "linux/amd64")out='./out/'if [[ ! -f $out ]]; thenmkdir -p $outfifor platform in "${platforms[@]}"doplatform_split=(${platform//\// })GOOS=${platform_split[0]}GOARCH=${platform_split[1]}output_name=$package_name'-'$GOOS'-'$GOARCHif [ $GOOS = "windows" ]; thenoutput_name+='.exe'fienv GOOS=$GOOS GOARCH=$GOARCH go build -o $out$output_name ./cmdif [ $? -ne 0 ]; thenecho "Could not build for ${platform}"exit 1fidone
For every combination of a system and an architecture, it makes a binary and puts it to the .out folder.
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 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.
#!/usr/bin/env bashset -euAPP_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>&1return $?}ensure_command() {if ! check_command "$1"; thenerror_and_exit "Command not found: '$1'"fi}ensure_success() {$* > /dev/nullif [[ $? -ne 0 ]]; thenerror_and_exit "Command execution failed: $*"fi}get_architecture() {local os="$(uname -s)"local cpu="$(uname -m)"case "$os" inLinux)local os="linux";;Darwin)local os="darwin";;MINGW* | MSYS* | CYGWIN*)local os="windows";;*)error_and_exit "OS type is not supported: $os";;esaccase "$cpu" inx86_64)local cpu="amd64";;*)error_and_exit "CPU architecture is not supported: $cpu";;esacRETVAL="${os}-${cpu}"}download_and_run() {ensure_success mkdir -p "$DST"get_architecturelocal architecture=$RETVALecho $architecturelocal 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 mkdirensure_command curldownload_and_run || exit 1echo ""echo "Success. Make sure to add this line to your rc file: "echo "export PATH=\$HOME${FOLDER_PATH}:\$PATH;"
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.
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 :)
name: Build and Releaseon:push:tags:- 'v*'jobs:build:runs-on: ubuntu-lateststeps:- name: Checkout repositoryuses: actions/checkout@v3- name: Buildrun: make build- name: Create changelogid: github_releaseuses: mikepenz/release-changelog-builder-action@v3env:GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}- name: Create releaseuses: ncipollo/release-action@v1with:skipIfReleaseExists: trueartifacts: "out/*"body: ${{steps.github_release.outputs.changelog}}token: ${{ secrets.RELEASE_TOKEN }}- name: Extract versionuses: mad9000/actions-find-and-replace-string@3id: extract_versionwith:source: ${{ github.ref }}find: "refs/tags/v"replace: ""- uses: actions/checkout@v3with:ref: installertoken: ${{ secrets.RELEASE_TOKEN }}- run: |sed -e "s/#VERSION#/${{ steps.extract_version.outputs.value }}/g" ./script/install.template.sh > ./script/install.shgit config user.name github-actionsgit config user.email github-actions@github.comgit add .git commit -m "Next version installer"git push origin installer
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.0git push origin v1.0.0
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.
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!
Sergei Gannochenko
Golang, React, TypeScript, Docker, AWS, Jamstack.
19+ years in dev.