mirror of
https://github.com/wweir/sower.git
synced 2026-10-20 08:16:35 +00:00
Installing sower meant grabbing a tarball and wiring the service up by hand, and the packaged unit was too fragile for something a resolver depends on (on-failure restart after 1min, no memory limit). Add install.sh: detects the platform and init system, installs or upgrades the binary atomically, keeps an existing config, verifies the release SHA256SUMS unless explicitly opted out, and registers the service on systemd, OpenWrt procd or macOS launchd. Every network call carries an explicit timeout, the OpenWrt init script ships in Linux tarballs, the release workflow generates SHA256SUMS, the README documents the installer, and make test syntax-checks the scripts.
426 lines
11 KiB
Bash
426 lines
11 KiB
Bash
#!/bin/sh
|
|
# sower one-shot installer.
|
|
#
|
|
# curl -fsSL https://raw.githubusercontent.com/sower-proxy/sower/master/install.sh | sudo sh
|
|
#
|
|
# Downloads the release tarball for this machine, installs the sower client
|
|
# binary plus a starter config, and wires up the platform service:
|
|
#
|
|
# * Linux with systemd -> /etc/systemd/system/sower.service
|
|
# * OpenWrt (procd) -> /etc/init.d/sower
|
|
# * macOS (launchd) -> /Library/LaunchDaemons/sower.plist
|
|
#
|
|
# Re-running the script upgrades in place and keeps the existing config.
|
|
# Only POSIX sh features are used so busybox ash on routers works too.
|
|
|
|
set -eu
|
|
|
|
REPO="sower-proxy/sower"
|
|
TMP=""
|
|
PLATFORM=""
|
|
PREFIX=""
|
|
CONFIG_DIR=""
|
|
|
|
VERSION="latest"
|
|
MIRROR=""
|
|
SERVICE="auto"
|
|
WITH_SERVER="no"
|
|
DRY_RUN="no"
|
|
TARBALL=""
|
|
INSECURE="no"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Install or upgrade the sower client.
|
|
|
|
Usage: install.sh [options]
|
|
|
|
Options:
|
|
-v, --version <tag> release tag to install (default: latest)
|
|
-m, --mirror <url> prefix added to the download URL, e.g.
|
|
https://ghfast.top/ (for hosts that cannot reach GitHub)
|
|
-p, --prefix <dir> binary directory (default: /usr/local/bin)
|
|
-c, --config-dir <dir> config directory (default: /etc/sower,
|
|
macOS: /usr/local/etc/sower)
|
|
-s, --service <name> service backend: auto, systemd, procd, launchd, none
|
|
-t, --tarball <path> install from a local release tarball (offline)
|
|
--with-server also install the sowerd binary (no service)
|
|
--insecure-skip-verify
|
|
install even when the release publishes no
|
|
SHA256SUMS (remote installs verify by default)
|
|
--dry-run print every action without touching the system
|
|
-h, --help show this help
|
|
|
|
Examples:
|
|
curl -fsSL .../install.sh | sudo sh
|
|
sudo sh install.sh --version v1.6.0
|
|
sudo sh install.sh --mirror https://ghfast.top/
|
|
sudo sh install.sh --service none --prefix "$HOME/.local/bin"
|
|
EOF
|
|
}
|
|
|
|
log() { printf '%s\n' "$*"; }
|
|
|
|
die() {
|
|
printf 'install.sh: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
run() {
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] $*"
|
|
return 0
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-v | --version)
|
|
shift
|
|
[ $# -gt 0 ] || die "--version needs a value"
|
|
VERSION="$1"
|
|
;;
|
|
-m | --mirror)
|
|
shift
|
|
[ $# -gt 0 ] || die "--mirror needs a value"
|
|
MIRROR="$1"
|
|
;;
|
|
-p | --prefix)
|
|
shift
|
|
[ $# -gt 0 ] || die "--prefix needs a value"
|
|
PREFIX="$1"
|
|
;;
|
|
-c | --config-dir)
|
|
shift
|
|
[ $# -gt 0 ] || die "--config-dir needs a value"
|
|
CONFIG_DIR="$1"
|
|
;;
|
|
-s | --service)
|
|
shift
|
|
[ $# -gt 0 ] || die "--service needs a value"
|
|
SERVICE="$1"
|
|
;;
|
|
-t | --tarball)
|
|
shift
|
|
[ $# -gt 0 ] || die "--tarball needs a value"
|
|
TARBALL="$1"
|
|
;;
|
|
--with-server) WITH_SERVER="yes" ;;
|
|
--insecure-skip-verify) INSECURE="yes" ;;
|
|
--dry-run) DRY_RUN="yes" ;;
|
|
-h | --help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*) die "unknown option: $1 (see --help)" ;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
detect_platform() {
|
|
os=$(uname -s)
|
|
arch=$(uname -m)
|
|
|
|
case "$os" in
|
|
Linux) os="linux" ;;
|
|
Darwin) os="darwin" ;;
|
|
*) die "unsupported OS: $os (see README for manual install)" ;;
|
|
esac
|
|
|
|
case "$arch" in
|
|
x86_64 | amd64) arch="amd64" ;;
|
|
aarch64 | arm64) arch="arm64" ;;
|
|
armv7l | armv7 | armv6l | arm) arch="arm" ;;
|
|
riscv64) arch="riscv64" ;;
|
|
loongarch64 | loong64) arch="loong64" ;;
|
|
mips) arch="mips" ;;
|
|
mipsel | mipsle) arch="mipsle" ;;
|
|
*) die "unsupported architecture: $arch (see README for manual install)" ;;
|
|
esac
|
|
|
|
PLATFORM="$os-$arch"
|
|
}
|
|
|
|
detect_service() {
|
|
[ "$SERVICE" != "auto" ] && return 0
|
|
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
SERVICE="launchd"
|
|
elif [ -f /etc/openwrt_release ] || command -v ubus >/dev/null 2>&1; then
|
|
SERVICE="procd"
|
|
elif [ -d /run/systemd/system ]; then
|
|
SERVICE="systemd"
|
|
else
|
|
SERVICE="none"
|
|
fi
|
|
}
|
|
|
|
default_paths() {
|
|
# Keep the defaults aligned with the packaged service files.
|
|
if [ -z "$PREFIX" ]; then
|
|
case "$SERVICE" in
|
|
procd) PREFIX="/usr/sbin" ;;
|
|
*) PREFIX="/usr/local/bin" ;;
|
|
esac
|
|
fi
|
|
if [ -z "$CONFIG_DIR" ]; then
|
|
case "$SERVICE" in
|
|
launchd) CONFIG_DIR="/usr/local/etc/sower" ;;
|
|
*) CONFIG_DIR="/etc/sower" ;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
need_root() {
|
|
[ "$DRY_RUN" = "yes" ] && return 0
|
|
[ "$(id -u)" = "0" ] && return 0
|
|
# A user-local install without a service needs no privileges.
|
|
[ "$SERVICE" = "none" ] && return 0
|
|
die "must run as root (use sudo), or pass --service none --prefix/--config-dir"
|
|
}
|
|
|
|
download() {
|
|
# download <url> <dest>
|
|
# Every network call is bounded: the installer may run unattended as root.
|
|
if command -v curl >/dev/null 2>&1; then
|
|
run curl -fL --retry 3 --connect-timeout 15 --max-time 600 -o "$2" "$1"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
run wget -T 30 -t 3 -O "$2" "$1"
|
|
else
|
|
die "need curl or wget to download the release"
|
|
fi
|
|
}
|
|
|
|
release_url() {
|
|
# release_url <file>
|
|
if [ "$VERSION" = "latest" ]; then
|
|
printf '%s' "${MIRROR}https://github.com/${REPO}/releases/latest/download/$1"
|
|
else
|
|
printf '%s' "${MIRROR}https://github.com/${REPO}/releases/download/${VERSION}/$1"
|
|
fi
|
|
}
|
|
|
|
fetch_tarball() {
|
|
if [ -n "$TARBALL" ]; then
|
|
[ -f "$TARBALL" ] || die "no such tarball: $TARBALL"
|
|
log "using local tarball $TARBALL"
|
|
cp "$TARBALL" "$TMP/release.tar.gz"
|
|
return 0
|
|
fi
|
|
|
|
asset="sower-${PLATFORM}.tar.gz"
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] download sower-${PLATFORM}.tar.gz ($VERSION)"
|
|
return 0
|
|
fi
|
|
|
|
log "downloading $asset ($VERSION)"
|
|
download "$(release_url "$asset")" "$TMP/release.tar.gz"
|
|
verify_checksum "$asset"
|
|
}
|
|
|
|
skip_verify() {
|
|
# A remote archive is installed as root, so a missing checksum is an error
|
|
# unless the operator explicitly opts out.
|
|
[ "$INSECURE" = "yes" ] && return 0
|
|
die "$1 (re-run with --insecure-skip-verify to override)"
|
|
}
|
|
|
|
verify_checksum() {
|
|
sums="$TMP/SHA256SUMS"
|
|
if ! download "$(release_url "SHA256SUMS")" "$sums" 2>/dev/null || [ ! -f "$sums" ]; then
|
|
log "WARNING: release $VERSION publishes no SHA256SUMS"
|
|
skip_verify "cannot verify the downloaded archive"
|
|
return 0
|
|
fi
|
|
|
|
expected=$(awk -v name="$1" '$2 == name { print $1 }' "$sums" 2>/dev/null || true)
|
|
if [ -z "$expected" ]; then
|
|
log "WARNING: SHA256SUMS does not list $1"
|
|
skip_verify "cannot verify the downloaded archive"
|
|
return 0
|
|
fi
|
|
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
actual=$(sha256sum "$TMP/release.tar.gz" | awk '{ print $1 }')
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
actual=$(shasum -a 256 "$TMP/release.tar.gz" | awk '{ print $1 }')
|
|
else
|
|
log "WARNING: neither sha256sum nor shasum is available"
|
|
skip_verify "cannot verify the downloaded archive"
|
|
return 0
|
|
fi
|
|
|
|
[ "$actual" = "$expected" ] || die "checksum mismatch for $1 (want $expected, got $actual)"
|
|
log "checksum verified"
|
|
}
|
|
|
|
unpack() {
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] tar -xzf release.tar.gz"
|
|
return 0
|
|
fi
|
|
extract="$TMP/extract"
|
|
mkdir -p "$extract"
|
|
tar -xzf "$TMP/release.tar.gz" -C "$extract"
|
|
[ -f "$extract/sower" ] || die "release tarball has no sower binary"
|
|
}
|
|
|
|
install_binaries() {
|
|
# Replace atomically: a running process keeps the old inode, so a plain
|
|
# copy over it fails with "text file busy".
|
|
for name in sower sowerd; do
|
|
if [ "$name" = "sowerd" ] && [ "$WITH_SERVER" != "yes" ]; then
|
|
continue
|
|
fi
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] install $PREFIX/$name"
|
|
continue
|
|
fi
|
|
src="$TMP/extract/$name"
|
|
[ -f "$src" ] || die "release tarball has no $name binary"
|
|
log "installing $PREFIX/$name"
|
|
mkdir -p "$PREFIX"
|
|
cp "$src" "$PREFIX/$name.new"
|
|
chmod 0755 "$PREFIX/$name.new"
|
|
mv "$PREFIX/$name.new" "$PREFIX/$name"
|
|
done
|
|
}
|
|
|
|
install_config() {
|
|
dst="$CONFIG_DIR/sower.toml"
|
|
|
|
if [ -f "$dst" ]; then
|
|
log "keeping existing config $dst"
|
|
return 0
|
|
fi
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] write starter config $dst"
|
|
return 0
|
|
fi
|
|
|
|
src="$TMP/extract/sower.toml"
|
|
[ -f "$src" ] || die "release tarball has no sower.toml"
|
|
log "writing starter config $dst"
|
|
mkdir -p "$CONFIG_DIR"
|
|
# The config carries the upstream password, keep it private.
|
|
cp "$src" "$dst"
|
|
chmod 0600 "$dst"
|
|
}
|
|
|
|
render_service() {
|
|
# render_service <template> <dest> : rewrite the packaged paths. The
|
|
# placeholders keep a rewritten path from matching a later rule (e.g.
|
|
# /usr/local/etc/sower already contains /etc/sower).
|
|
sed -e "s|/usr/local/bin/sower|__PREFIX__/sower|g" \
|
|
-e "s|/usr/sbin/sower|__PREFIX__/sower|g" \
|
|
-e "s|/usr/local/etc/sower|__CONFIG__|g" \
|
|
-e "s|/etc/sower|__CONFIG__|g" \
|
|
-e "s|__PREFIX__|$PREFIX|g" \
|
|
-e "s|__CONFIG__|$CONFIG_DIR|g" \
|
|
"$1" >"$2"
|
|
}
|
|
|
|
write_root_file() {
|
|
# write_root_file <src> <dst> <mode>
|
|
log "writing $2"
|
|
mkdir -p "$(dirname "$2")"
|
|
cp "$1" "$2"
|
|
chmod "$3" "$2"
|
|
}
|
|
|
|
install_systemd() {
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] write /etc/systemd/system/sower.service"
|
|
log "[dry-run] systemctl daemon-reload && systemctl enable --now sower"
|
|
return 0
|
|
fi
|
|
tpl="$TMP/extract/sower.service"
|
|
[ -f "$tpl" ] || die "release tarball has no sower.service"
|
|
render_service "$tpl" "$TMP/sower.service.gen"
|
|
write_root_file "$TMP/sower.service.gen" "/etc/systemd/system/sower.service" 0644
|
|
run systemctl daemon-reload
|
|
run systemctl enable sower
|
|
run systemctl restart sower
|
|
}
|
|
|
|
install_procd() {
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] write /etc/init.d/sower"
|
|
log "[dry-run] /etc/init.d/sower enable && /etc/init.d/sower restart"
|
|
return 0
|
|
fi
|
|
tpl="$TMP/extract/sower.init"
|
|
[ -f "$tpl" ] || die "release tarball has no sower.init"
|
|
render_service "$tpl" "$TMP/sower.init.gen"
|
|
write_root_file "$TMP/sower.init.gen" "/etc/init.d/sower" 0755
|
|
run /etc/init.d/sower enable
|
|
run /etc/init.d/sower restart
|
|
}
|
|
|
|
install_launchd() {
|
|
if [ "$DRY_RUN" = "yes" ]; then
|
|
log "[dry-run] write /Library/LaunchDaemons/sower.plist"
|
|
log "[dry-run] launchctl load -w /Library/LaunchDaemons/sower.plist"
|
|
return 0
|
|
fi
|
|
tpl="$TMP/extract/sower.plist"
|
|
[ -f "$tpl" ] || die "release tarball has no sower.plist"
|
|
render_service "$tpl" "$TMP/sower.plist.gen"
|
|
plist="/Library/LaunchDaemons/sower.plist"
|
|
write_root_file "$TMP/sower.plist.gen" "$plist" 0644
|
|
run launchctl unload -w "$plist" 2>/dev/null || true
|
|
run launchctl load -w "$plist"
|
|
}
|
|
|
|
install_service() {
|
|
case "$SERVICE" in
|
|
systemd) install_systemd ;;
|
|
procd) install_procd ;;
|
|
launchd) install_launchd ;;
|
|
none) log "no service requested, skipping service setup" ;;
|
|
*) die "unknown service backend: $SERVICE" ;;
|
|
esac
|
|
}
|
|
|
|
print_summary() {
|
|
log ""
|
|
log "sower installed:"
|
|
log " binary: $PREFIX/sower"
|
|
log " config: $CONFIG_DIR/sower.toml"
|
|
log " service: $SERVICE"
|
|
log ""
|
|
log "Next: set remote.addr and remote.password in the config, then restart:"
|
|
case "$SERVICE" in
|
|
systemd) log " systemctl restart sower" ;;
|
|
procd) log " /etc/init.d/sower restart" ;;
|
|
launchd) log " launchctl kickstart -k system/sower" ;;
|
|
*) log " $PREFIX/sower -c $CONFIG_DIR/sower.toml" ;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
detect_platform
|
|
detect_service
|
|
default_paths
|
|
need_root
|
|
|
|
TMP=$(mktemp -d) || die "cannot create a temporary directory"
|
|
trap 'rm -rf "$TMP"' EXIT INT TERM
|
|
|
|
log "platform: $PLATFORM, service: $SERVICE, prefix: $PREFIX, config: $CONFIG_DIR"
|
|
|
|
fetch_tarball
|
|
unpack
|
|
install_binaries
|
|
install_config
|
|
install_service
|
|
print_summary
|
|
}
|
|
|
|
main "$@"
|