| 1 | #!/bin/sh |
| 2 | # Install budctl and budctl-gui on Linux, or remove them. |
| 3 | # |
| 4 | # curl -fsSL https://github.com/krolyxon/budctl/releases/latest/download/install.sh | sh |
| 5 | # ./install.sh in an unpacked release: installs the binaries next to it |
| 6 | # ./install.sh --source in a checkout: builds with cargo, then installs |
| 7 | # ./install.sh --uninstall |
| 8 | # |
| 9 | # Installs into ~/.local by default (bin/, and the launcher entry and its icon in share/), so no |
| 10 | # root is needed. |
| 11 | |
| 12 | set -eu |
| 13 | |
| 14 | REPO=krolyxon/budctl |
| 15 | PREFIX=$HOME/.local |
| 16 | VERSION=latest |
| 17 | ACTION=install |
| 18 | FROM=auto |
| 19 | |
| 20 | usage() { |
| 21 | cat <<'EOF' |
| 22 | Install budctl (command line) and budctl-gui (desktop app), or remove them. |
| 23 | |
| 24 | Usage: |
| 25 | install.sh [--prefix DIR] [--version X.Y.Z] [--source] |
| 26 | install.sh --uninstall [--prefix DIR] |
| 27 | |
| 28 | Options: |
| 29 | --prefix DIR Install into DIR/bin and DIR/share (default: ~/.local) |
| 30 | --version X.Y.Z Download this release instead of the latest |
| 31 | --source Build from this checkout with cargo instead of downloading |
| 32 | --uninstall Remove what install.sh installed. Your config (~/.config/budctl) |
| 33 | and log (~/.local/state/budctl) are left alone. |
| 34 | -h, --help Show this help |
| 35 | EOF |
| 36 | } |
| 37 | |
| 38 | fail() { |
| 39 | printf 'install.sh: %s\n' "$1" >&2 |
| 40 | exit 1 |
| 41 | } |
| 42 | |
| 43 | say() { |
| 44 | printf '%s\n' "$1" |
| 45 | } |
| 46 | |
| 47 | while [ "$#" -gt 0 ]; do |
| 48 | case "$1" in |
| 49 | --prefix) |
| 50 | [ "$#" -ge 2 ] && [ -n "$2" ] || fail "--prefix needs a directory" |
| 51 | PREFIX=$2 |
| 52 | shift 2 |
| 53 | ;; |
| 54 | --version) |
| 55 | [ "$#" -ge 2 ] && [ -n "$2" ] || fail "--version needs a version, like 0.1.1" |
| 56 | VERSION=${2#v} |
| 57 | shift 2 |
| 58 | ;; |
| 59 | --source) |
| 60 | FROM=source |
| 61 | shift |
| 62 | ;; |
| 63 | --uninstall) |
| 64 | ACTION=uninstall |
| 65 | shift |
| 66 | ;; |
| 67 | -h | --help) |
| 68 | usage |
| 69 | exit 0 |
| 70 | ;; |
| 71 | *) |
| 72 | fail "unknown option: $1 (see --help)" |
| 73 | ;; |
| 74 | esac |
| 75 | done |
| 76 | |
| 77 | [ "$(uname -s)" = Linux ] || fail "budctl runs on Linux only" |
| 78 | |
| 79 | BIN_DIR=$PREFIX/bin |
| 80 | APP_DIR=$PREFIX/share/applications |
| 81 | ICON_THEME=$PREFIX/share/icons/hicolor |
| 82 | ICON_DIR=$ICON_THEME/scalable/apps |
| 83 | |
| 84 | # Use sudo only when the prefix cannot be written as this user (such as /usr/local). |
| 85 | SUDO= |
| 86 | writable_dir=$PREFIX |
| 87 | while [ ! -d "$writable_dir" ]; do |
| 88 | writable_dir=$(dirname "$writable_dir") |
| 89 | done |
| 90 | if [ ! -w "$writable_dir" ] && [ "$(id -u)" -ne 0 ]; then |
| 91 | command -v sudo >/dev/null 2>&1 || fail "$PREFIX is not writable and sudo was not found" |
| 92 | SUDO=sudo |
| 93 | fi |
| 94 | |
| 95 | if [ "$ACTION" = uninstall ]; then |
| 96 | for file in "$BIN_DIR/budctl" "$BIN_DIR/budctl-gui" "$APP_DIR/budctl.desktop" "$ICON_DIR/budctl.svg"; do |
| 97 | if [ -e "$file" ]; then |
| 98 | $SUDO rm -f "$file" |
| 99 | say "Removed $file" |
| 100 | fi |
| 101 | done |
| 102 | say "Your config (~/.config/budctl) and log (~/.local/state/budctl) were left alone." |
| 103 | exit 0 |
| 104 | fi |
| 105 | |
| 106 | WORK=$(mktemp -d) |
| 107 | trap 'rm -rf "$WORK"' EXIT INT TERM |
| 108 | |
| 109 | # Where the files to install come from: a release unpacked next to this script, this checkout |
| 110 | # built with cargo, or a release downloaded from GitHub. |
| 111 | SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) |
| 112 | if [ "$FROM" = auto ]; then |
| 113 | if [ -f "$SCRIPT_DIR/install.sh" ] && [ -x "$SCRIPT_DIR/budctl" ] && [ -x "$SCRIPT_DIR/budctl-gui" ]; then |
| 114 | FROM=bundle |
| 115 | else |
| 116 | FROM=download |
| 117 | fi |
| 118 | fi |
| 119 | |
| 120 | case "$FROM" in |
| 121 | bundle) |
| 122 | FILES=$SCRIPT_DIR |
| 123 | DESKTOP=$SCRIPT_DIR/budctl.desktop |
| 124 | ICON=$SCRIPT_DIR/budctl.svg |
| 125 | ;; |
| 126 | source) |
| 127 | [ -f "$SCRIPT_DIR/Cargo.toml" ] || fail "--source needs a budctl checkout next to install.sh" |
| 128 | command -v cargo >/dev/null 2>&1 || fail "cargo was not found; install Rust from https://rustup.rs/" |
| 129 | say "Building budctl in $SCRIPT_DIR" |
| 130 | cargo build --release --locked --manifest-path "$SCRIPT_DIR/Cargo.toml" -p budctl -p budctl-gui |
| 131 | FILES=$SCRIPT_DIR/target/release |
| 132 | DESKTOP=$SCRIPT_DIR/contrib/budctl.desktop |
| 133 | ICON=$SCRIPT_DIR/docs/icon.svg |
| 134 | ;; |
| 135 | download) |
| 136 | case "$(uname -m)" in |
| 137 | x86_64 | amd64) TARGET=x86_64-unknown-linux-gnu ;; |
| 138 | aarch64 | arm64) TARGET=aarch64-unknown-linux-gnu ;; |
| 139 | *) fail "no release build for $(uname -m); build from a checkout with ./install.sh --source" ;; |
| 140 | esac |
| 141 | ARCHIVE=budctl-$TARGET.tar.gz |
| 142 | if [ "$VERSION" = latest ]; then |
| 143 | URL=https://github.com/$REPO/releases/latest/download |
| 144 | else |
| 145 | URL=https://github.com/$REPO/releases/download/v$VERSION |
| 146 | fi |
| 147 | |
| 148 | if command -v curl >/dev/null 2>&1; then |
| 149 | fetch() { curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"; } |
| 150 | elif command -v wget >/dev/null 2>&1; then |
| 151 | fetch() { wget -q -O "$2" "$1"; } |
| 152 | else |
| 153 | fail "curl or wget is needed to download budctl" |
| 154 | fi |
| 155 | |
| 156 | say "Downloading $URL/$ARCHIVE" |
| 157 | fetch "$URL/$ARCHIVE" "$WORK/$ARCHIVE" || fail "download failed: $URL/$ARCHIVE" |
| 158 | fetch "$URL/$ARCHIVE.sha256" "$WORK/$ARCHIVE.sha256" || fail "download failed: $URL/$ARCHIVE.sha256" |
| 159 | (cd "$WORK" && sha256sum -c --quiet "$ARCHIVE.sha256") || fail "checksum mismatch for $ARCHIVE" |
| 160 | tar -xzf "$WORK/$ARCHIVE" -C "$WORK" |
| 161 | FILES=$WORK/budctl-$TARGET |
| 162 | DESKTOP=$FILES/budctl.desktop |
| 163 | ICON=$FILES/budctl.svg |
| 164 | ;; |
| 165 | esac |
| 166 | |
| 167 | for name in budctl budctl-gui; do |
| 168 | [ -x "$FILES/$name" ] || fail "$name was not found in $FILES" |
| 169 | done |
| 170 | [ -f "$DESKTOP" ] || fail "budctl.desktop was not found at $DESKTOP" |
| 171 | |
| 172 | # The launcher entry runs the binary by its full path, so it works even when the launcher's PATH |
| 173 | # lacks the install directory. |
| 174 | sed "s|^Exec=budctl-gui|Exec=$BIN_DIR/budctl-gui|" "$DESKTOP" >"$WORK/budctl.desktop" |
| 175 | |
| 176 | $SUDO install -d "$BIN_DIR" "$APP_DIR" |
| 177 | $SUDO install -m 0755 "$FILES/budctl" "$FILES/budctl-gui" "$BIN_DIR/" |
| 178 | $SUDO install -m 0644 "$WORK/budctl.desktop" "$APP_DIR/budctl.desktop" |
| 179 | # Releases from before the icon carry none; their launcher entry names a stock one instead. |
| 180 | ICON_INSTALLED= |
| 181 | if [ -f "$ICON" ]; then |
| 182 | $SUDO install -d "$ICON_DIR" |
| 183 | $SUDO install -m 0644 "$ICON" "$ICON_DIR/budctl.svg" |
| 184 | # A theme with an icon cache only sees a new icon once the cache is rebuilt. |
| 185 | if [ -f "$ICON_THEME/icon-theme.cache" ] && command -v gtk-update-icon-cache >/dev/null 2>&1; then |
| 186 | $SUDO gtk-update-icon-cache -q -t "$ICON_THEME" || true |
| 187 | fi |
| 188 | ICON_INSTALLED=yes |
| 189 | fi |
| 190 | |
| 191 | say "Installed $("$BIN_DIR/budctl" --version) to $BIN_DIR (budctl, budctl-gui)" |
| 192 | say "Installed the launcher entry to $APP_DIR/budctl.desktop" |
| 193 | if [ -n "$ICON_INSTALLED" ]; then |
| 194 | say "Installed the icon to $ICON_DIR/budctl.svg" |
| 195 | fi |
| 196 | case ":$PATH:" in |
| 197 | *":$BIN_DIR:"*) ;; |
| 198 | *) say "Note: $BIN_DIR is not on your PATH. Add it to run budctl from a terminal." ;; |
| 199 | esac |