Skip to content

Latest commit

 

History

183 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

shlib

curl | sh installers with portable shell functions

lint linux macos freebsd openbsd netbsd dragonflybsd sunos alpine windows runtimes

You publish binaries on GitHub Releases and want your users to run one line:

curl -sSfL https://raw.githubusercontent.com/OWNER/REPO/master/install.sh | sh -s -- -b /usr/local/bin

shlib generates that install.sh. It is the replacement for the archived godownloader

The generated script detects the platform, resolves latest or a pinned tag, downloads the right asset, verifies its SHA-256 against its checksums file, unpacks it, and installs the binary — on everything in the test matrix.

Build an installer

An install script is two files concatenated:

config.sh              yours, ~12 lines
dist/install-base.sh   ours: the shlib functions + the installer flow

The config describes the release naming. This is install/examples/gosec.sh:

OWNER=securego
REPO=gosec
BINARY=gosec
FORMAT=tar.gz
BINDIR=${BINDIR:-./bin}

# declare the build matrix
PLATFORMS="darwin/amd64 darwin/arm64
           linux/amd64 linux/arm64 linux/ppc64le linux/s390x
           windows/amd64 windows/arm64"

archive_name()  { echo "${BINARY}_${VERSION}_${OS}_${ARCH}"; }
checksum_name() { echo "${BINARY}_${VERSION}_checksums.txt"; }

To make the install.sh, do this in your release process. cat the config with the latest install-base.sh:

curl -sSfL -o /tmp/base.sh \
  https://raw.githubusercontent.com/client9/shlib/master/dist/install-base.sh
cat config.sh /tmp/base.sh > install.sh

Commit install.sh:

sh install.sh                 # latest release, into ./bin
sh install.sh -b /usr/local/bin
sh install.sh -b ./bin v2.22.0   # a specific tag

If your filenames are unusual

Everything is an ordinary shell function, which can be over-ridden.

adjust_format() { case ${OS} in windows) FORMAT=zip ;; esac; }
adjust_os()     { case ${OS} in darwin) OS=macOS ;; esac; }
adjust_arch()   { case ${ARCH} in amd64) ARCH=x86_64 ;; esac; }
binary_path()   { echo "${NAME}/$1"; }   # binary nested inside the archive
unpack()        { :; }                   # the asset IS the binary, no archive

Real Examples

Some sample curl|sh installers for live projects:

Repo Example Variations
securego/gosec gosec.sh the simple case — no hooks at all
ory/hydra hydra.sh renamed OS and arch, .zip on windows
go-task/task task.sh no version in the archive name
golangci/golangci-lint golangci-lint.sh 27 platforms, binary nested in a versioned directory
gohugoio/hugo hugo.sh BSDs, Solaris, illumos, and build variants
koalaman/shellcheck shellcheck.sh not a Go project — raw x86_64/aarch64 names
hadolint/hadolint hadolint.sh no archive at all — the asset is the binary

See docs/INSTALLERS.md for details.

The building blocks

The installer is assembled from a library of standalone functions, and you can use them directly — for a build script, a bootstrap, a CI step. Each lives in its own <name>.sh and is meant to be concatenated into your script, not sourced as a dependency: a curl | sh installer cannot have dependencies, so the code has to travel with it.

uname_os uname_arch http_download hash_sha256_verify untar install_exe mktmpdir github_release log_* — 33 in total, indexed in docs/API.md.

Pre-built bundles are committed here and attached to every release:

file what it is
dist/install-base.sh shlib + installer logic; prepend your config
dist/shlib.sh just the functions
curl -sSfL -o vendor/shlib.sh \
  https://raw.githubusercontent.com/client9/shlib/master/dist/shlib.sh

Fetch a bundle at build time rather than pasting a copy that goes staledocs/EMBEDDING.md explains why that matters more than it sounds like it should.

Or build a custom subset — cat is the whole build step. List the files in dependency order; most functions report errors through log_err / log_crit, so include echoerr.sh and log.sh whenever you include something that can fail:

cat \
  license.sh \
  is_command.sh \
  echoerr.sh \
  log.sh \
  uname_os.sh \
  uname_arch.sh \
  untar.sh \
  mktmpdir.sh \
  http_download.sh \
  hash_sha256.sh \
  license_end.sh > vendor/shlib.sh

Tested where it actually has to run

Every push runs the whole suite against each shell below.

platform shells tested
Linux (glibc) dash bash ksh93 mksh yash posh busybox ash
Alpine (musl) busybox ash
macOS sh bash 3.2 ksh zsh dash
FreeBSD 14, 15 sh dash bash ksh93 mksh yash zsh
OpenBSD 7.9, 7.8 sh (OpenBSD ksh)
NetBSD 11.0, 10.1 sh ksh
DragonFly 6.4 sh
Solaris 11.4, OmniOS sh
Windows git bash msys2
python:3-slim sh — no downloader at all; python3 only
node:22-slim sh — no downloader at all; node only

Several legs also assert something the suite alone cannot:

  • Solaris and OmniOS, that SunOS resolves to solaris and illumos respectively -- both systems report that ancient name, and telling them apart has been the most bug-prone mapping in this library.
  • FreeBSD, OpenBSD, NetBSD and DragonFly, that curl and wget really are absent. They are the only legs that exercise http_download's fetch(1) and ftp(1) branches, and a dependency quietly installing curl would stop that without anything going red.
  • OmniOS builds a real installer from install/examples/hugo.sh, runs it, and executes the downloaded binary.
  • python:3-slim and node:22-slim assert that curl, wget, fetch and ftp are all absent, then run the whole suite and a real install through http_download_python / http_download_node. Container images routinely ship a language runtime and no downloader at all.

The library recognises a wider set than it can practically test: 17 operating systems and 16 architectures. See Platforms for the generated lists.

make test                      # everything under /bin/sh
make test TEST_SHELL=dash      # ... under one specific shell
make test-all                  # ... under every shell installed locally
make lint                      # shellcheck (sh, bash, dash, ksh) + shfmt

Documentation

document what it covers
docs/INSTALLERS.md building a curl | sh installer
docs/API.md generated index of all 33 functions
docs/EMBEDDING.md vendoring shlib without going stale
docs/PORTABILITY.md which shells break which idiom, which systems ship pre-POSIX tools
docs/RELEASING.md cutting a release
CONTRIBUTING.md changing shlib itself
CHANGELOG.md what changed

Why this exists

I've sadly written a lot of shell scripts. Mostly for installers on completely alien environments.

Really shell code should only be used for boot-strapping to something sane. Until then you might need some truly portable functions. I hope you never need to use them, but if you do they are public domain. Do whatever you'd like with them.

However acknowledgement (and pull requests) are appreciated. You can optionally include license.sh so the next person knows where to find them.

About

generate a `curl | sh` installer for 30+ target platforms

Topics

Resources

Contributing

Stars

431 stars

Watchers

5 watching

Forks

Releases

Packages

Contributors

Languages