μpkg is a package manager written in bash with just the bare minimum of features.
Its primary focus is allowing bash scripts to source dependencies like small
logging functions or commands that shouldn't be tracked with those scripts.
μpkg supports installation to a local project with a upkg.json
in its root,
and global installation for user- or system-wide usage.
- Dependencies
- Installation
- Usage
- Authoring packages
- Things that μpkg does not and will not support
- Things that μpkg might support in the future
- Alternatives
- bash>=v4.4
- git
- jq
Replace bash -c ...
with sudo bash -c ...
to install system-wide.
You can also paste this directly into a Dockerfile RUN
command, no escaping needed.
bash -ec 'src=$(wget -qO- https://raw.githubusercontent.com/orbit-online/upkg/v0.14.0/upkg.sh); \
shasum -a 256 -c <(printf "8312d0fa0e47ff22387086021c8b096b899ff9344ca8622d80cc0d1d579dccff -") <<<"$src"; \
set - install -g orbit-online/upkg@v0.14.0; eval "$src"'
Installation dependencies are ca-certificates
, wget
, and shasum
.
For Debian based systems these dependencies are installable with
apt-get install -y ca-certificates
(wget
and shasum
are already installed).
For alpine docker images use apk add --update ca-certificates bash perl-utils
(wget
is already installed).
For Red Hat based systems use dnf install -y ca-certificates bash perl-utils wget
(shasum
is already installed).
In GitHub workflows you can install μpkg with an action. The action version also determines the μpkg version that will be installed.
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: orbit-online/upkg@<VERSION>
You can upgrade μpkg with μpkg (prefix with sudo
if installed system-wide):
upkg install -g orbit-online/upkg@<VERSION>
Use stable
for <VERSION>
if you don't care about the specific version number
and would just like to upgrade to the latest stable version.
μpkg - A minimalist package manager
Usage:
upkg install [-n] [-g [remoteurl]user/pkg@<version>]
upkg uninstall -g user/pkg
upkg list [-g]
Options:
-g Act globally
-n Dry run, $?=1 if install/upgrade is required
upkg install
looks for a upkg.json
upwards from the current directory and
recursively installs the specified dependencies (see
dependencies). Use -n
to check whether all dependencies are
up-to-date without installing/upgrading anything, note that branch version are
always considered out-of-date (see Upgrading packages).
upkg install -g
installs the specified package and version (either a full git
remote URL or a GitHub user/pkg shorthand) to /usr/local/lib/upkg
(when root)
or $HOME/.local
(when not).
upkg uninstall -g
uninstalls a globally installed package (must be shorthand)
and all its commands (see commands).
upkg list
shows the installed packages. When using -g
for "global", package
dependencies are not listed.
You can suppress processing output by setting UPKG_SILENT=true
.
When stderr is not a TTY, μpkg switches to outputting a line for each processing
step instead of overwriting the same line.
Errors will always be output and cannot be silenced (use 2>/dev/null
to do
that).
Check out PACKAGES.md for a curated list of available packages.
You can also use the upkg
topic to
look for other packages on GitHub.
If you take a closer look at how upkg is installed you will
notice that upkg.sh
is the install script for μpkg itself. The three lines
of code do the following:
- Download
upkg.sh
- Compare the download against the hardcoded checksum
- Inject the installation parameters for
orbit-online/upkg
as if μpkg was called from the commandline - Evaluate the download
With that in mind, you can modify the package name from point #3 and on the third line to install any package you like. For example:
bash -ec 'src=$(wget -qO- https://raw.githubusercontent.com/orbit-online/upkg/v0.14.0/upkg.sh); \
shasum -a 256 -c <(printf "8312d0fa0e47ff22387086021c8b096b899ff9344ca8622d80cc0d1d579dccff -") <<<"$src"; \
set - install -g orbit-online/bitwarden-tools@v1.4.9; eval "$src"'
You can also install dependencies for a script with an accompanying upkg.json
that you copied into e.g. a docker image like this:
COPY upkg.json /service
COPY --chmod=0755 my-script.sh /service
bash -ec 'src=$(wget -qO- https://raw.githubusercontent.com/orbit-online/upkg/v0.14.0/upkg.sh); \
shasum -a 256 -c <(printf "8312d0fa0e47ff22387086021c8b096b899ff9344ca8622d80cc0d1d579dccff -") <<<"$src"; \
set - install; eval "$src"'
When hosting a package on GitHub, add the upkg
topic to make it discoverable
via search.
Additionally you can send a PR that updates PACKAGES.md with a
link to your package.
You can run upkg install
to upgrade all packages that have a moving version
(i.e. a git branch). It is advisable to use tags or commit hashes as versions
when publishing something that other packages may rely on to avoid bumping past
breaking changes (tags are quickest to install since μpkg can shallow clone the
repo).
All packages that were installed using a commit hash or git tag as the version
and are still referenced with the same version will be skipped during upgrade.
This means a upkg install
can almost become a no-op and be run automatically
without sacrificing performance. Conversely branch versions will always be
reinstalled, even when they are a dependency of a parent package that is version
pinned via a tag or commit hash.
μpkg tries very hard to ensure that either everything is installed/upgraded or nothing is. Unhandled violations include (and are limited to) broken permissions (e.g. inconsistent ownership of files), insufficient diskspace, closure of stderr, or process termination.
To determine the package root path use ${BASH_SOURCE[0]}
.
Use realpath
to ensure that symlinks and relative paths are resolved.
PKGROOT=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
Alternatively you can use a short until
loop to find the ancestor directory that contains upkg.json
.
This way you can move the script around without having to adjust the relative path:
until [[ -e $PKGROOT/upkg.json || $PKGROOT = '/' ]]; do PKGROOT=$(dirname "${PKGROOT:-$(realpath "${BASH_SOURCE[0]}")}"); done
Here's an example of a short script with a useful preamble:
#!/usr/bin/env bash
# shellcheck source-path=../
set -eo pipefail; shopt -s inherit_errexit
PKGROOT=$(realpath "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/..")
PATH=$("$PKGROOT/.upkg/.bin/path_prepend" "$PKGROOT/.upkg/.bin")
source "$PKGROOT/.upkg/orbit-online/records.sh/records.sh"
my_fn() {
if ! do_something "$1"; then
warning "Failed to do something do_something, trying something else"
do_something_else "$1"
fi
}
my_fn "$@"
The following things are happening here (line-by-line):
- Bash shebang
- Inform shellcheck about the package root so it can follow
source
calls - Setup bash to fail on
$? != 0
, even when piping. Make sure that subshells inheritset -e
. - Determine the package root (the second
realpath
is to resolve the/..
) - Use path-tools to prepend
.upkg/.bin
(likePATH=...:$PATH
but if the path already exists it is moved to the front) - Include records.sh for log tooling
- Rest: Define the function (and call it afterwards)
For scripts that you don't install via μpkg, checking whether dependencies are
up to date can be done with the -n
dry-run switch:
#!/usr/bin/env bash
PKGROOT=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
(cd "$PKGROOT/core"; UPKG_SILENT=true upkg install -n || {
printf "my-script.sh: Dependencies are out of date. Run \`upkg install\` in \`%s\`\n" "$PKGROOT" >&2
return 1
})
When creating scripts not installed via μpkg, use
PKGROOT=$HOME/.local/lib/upkg; [[ $EUID != 0 ]] || PKGROOT=/usr/local/lib/upkg
to get the path to the global installation.
Use local pkgroot; pkgroot=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
inside your function if you are building a library (as opposed to a command)
to avoid $PKGROOT
being overwritten by external code.
You only need to specify # shellcheck source-path=
if your script is not
located in $pkgroot
(source-path
is ./
by default).
If you are calling scripts from the same package consider using
path-tools to avoid prepending
the same .upkg/bin
PATH multiple times (i.e. use
PATH=$("$pkgroot/.upkg/.bin/path_prepend" "$pkgroot/.upkg/.bin")
instead).
upkg.json
has no package name, version or description.
There are 3 config keys you can specify (none are mandatory, but at least one
key must be present).
It is highly discouraged to specify non-standard keys for your own usage in this
file.
Dependencies of a package. A dictionary of git cloneable URLs or GitHub shorthands as keys and git branches/tags/commits as values.
Dependencies will be installed under .upkg
next to upkg.json
.
{
...
"dependencies": {
"orbit-online/records.sh": "v0.9.2",
"git@github.com:andsens/docopt.sh": "v1.0.0-upkg",
"orbit-online/bitwarden-tools": "master"
},
...
}
List of files and folders the package consists of. An array of paths relative to the repository root. Only items listed here and in commands will be part of the final package installation. All listed paths must exist and folders must have a trailing slash.
{
...
"assets": [
"lib/common.sh",
"lib/commands.sh",
"bin/"
],
...
}
List of commands this package provides. A dictionary of command names as keys and paths relative to the repository root. Note that the specified files must be marked as executable.
{
...
"commands": {
"parse-spec": "bin/parse.sh",
"buildit": "bin/build.sh",
"checkit": "tools/check.sh"
},
...
}
When installing globally, the listed commands will be installed as symlinks to
/usr/local/bin
(when root) or $HOME/.local/bin
(when not).
When uninstalling a package these symlinks are removed as well (provided they
still point at the package).
When installing locally, the listed commands will be installed to .upkg/.bin
.
This field will be populated by μpkg with the version specified in the global
install command or the dependency specification. It is used to determine whether
an install command should overwrite or skip the package.
You must not specify it.
{
...
"version": "refs/tags/v0.9.2",
...
}
upkg run command
(modify$PATH
instead)upkg add/remove usr/package@version
toupkg.json
~
,^
or other version specifiers (use branches for that)- Package version locking
- Package aliases (i.e. non user namespaced package names)
- Installing packages via e.g. raw.githubusercontent.com or the local filesystem
to avoid the git dependency (would require
name
to be present inupkg.json
) - Using something like
JSON.sh
to avoid the jq dependency
https://github.com/bpkg/bpkg
https://github.com/basherpm/basher