#!/usr/bin/env sh set -eu role="${DOSH_ROLE:-both}" repo="${DOSH_REPO:-}" server="${DOSH_SERVER:-}" dosh_host="${DOSH_HOST:-${DOSH_DOSH_HOST:-}}" port="${DOSH_PORT:-50000}" prefix="${PREFIX:-$HOME/.local}" from_current=0 start_server=1 force_config=0 update_cache="${DOSH_UPDATE_CACHE:-$HOME/.cache/dosh/source}" quiet="${DOSH_UPDATE_QUIET:-0}" use_prebuilt="${DOSH_USE_PREBUILT:-1}" binary_url="${DOSH_BINARY_URL:-}" binary_base="${DOSH_BINARY_BASE:-}" binary_name="${DOSH_BINARY_NAME:-}" binary_version="${DOSH_BINARY_VERSION:-latest}" binary_required="${DOSH_BINARY_REQUIRED:-0}" usage() { cat <<'EOF' Usage: install.sh [server|client|both] [options] Options: --repo URL Git repository to clone when not run from a checkout --server HOST Default SSH target for client config, for example user@host --dosh-host HOST UDP host for Dosh packets when different from SSH target --port PORT Dosh UDP port; default 50000 --prefix DIR Install prefix; default ~/.local --update-cache DIR Persistent source checkout for incremental updates --no-start Install server but do not start it --force-config Rewrite existing ~/.config/dosh/*.toml files Environment alternatives: DOSH_REPO, DOSH_ROLE, DOSH_SERVER, DOSH_HOST, DOSH_PORT, PREFIX, DOSH_UPDATE_CACHE DOSH_USE_PREBUILT=0 Build from source instead of trying a release tarball first DOSH_BINARY_URL URL Exact release tarball URL to install DOSH_BINARY_BASE URL Release download base; otherwise derived from the latest tag DOSH_BINARY_NAME NAME Release tarball name; defaults to dosh-OS-ARCH.tar.gz DOSH_BINARY_VERSION TAG Release tag when deriving DOSH_BINARY_BASE; default latest DOSH_BINARY_REQUIRED=1 Fail instead of falling back to source when binary install fails EOF } while [ "$#" -gt 0 ]; do case "$1" in server|client|both) role="$1" ;; --repo) repo="$2" shift ;; --server) server="$2" shift ;; --dosh-host) dosh_host="$2" shift ;; --port) port="$2" shift ;; --prefix) prefix="$2" shift ;; --update-cache) update_cache="$2" shift ;; --from-current) from_current=1 ;; --no-start) start_server=0 ;; --force-config) force_config=1 ;; -h|--help) usage exit 0 ;; *) echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esac shift done case "$role" in server|client|both) ;; *) echo "role must be server, client, or both" >&2 exit 2 ;; esac need() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 exit 1 fi } ensure_cargo() { if command -v cargo >/dev/null 2>&1; then return fi need curl echo "cargo not found; installing Rust toolchain with rustup" >&2 curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y # shellcheck disable=SC1091 . "$HOME/.cargo/env" } cleanup() { if [ -n "${tmpdir:-}" ]; then rm -rf "$tmpdir" fi } trap cleanup EXIT INT TERM bindir="$prefix/bin" config_dir="$HOME/.config/dosh" data_dir="$HOME/.local/share/dosh" systemd_user_dir="$HOME/.config/systemd/user" src_dir="" mkdir -p "$bindir" "$config_dir" "$data_dir" normalize_os() { case "$(uname -s)" in Darwin) printf '%s\n' macos ;; Linux) printf '%s\n' linux ;; FreeBSD) printf '%s\n' freebsd ;; *) uname -s | tr '[:upper:]' '[:lower:]' ;; esac } normalize_arch() { case "$(uname -m)" in x86_64|amd64) printf '%s\n' x86_64 ;; arm64|aarch64) printf '%s\n' aarch64 ;; armv7l) printf '%s\n' armv7 ;; *) uname -m | tr '[:upper:]' '[:lower:]' ;; esac } repo_web_base() { repo_base="$1" case "$repo_base" in http://*|https://*) repo_base="${repo_base%.git}" printf '%s\n' "${repo_base%/}" ;; *) return 1 ;; esac } release_artifact_name() { if [ -n "$binary_name" ]; then printf '%s\n' "$binary_name" else printf 'dosh-%s-%s.tar.gz\n' "$(normalize_os)" "$(normalize_arch)" fi } release_download_url() { if [ -n "$binary_url" ]; then printf '%s\n' "$binary_url" return 0 fi if [ -n "$binary_base" ]; then printf '%s/%s\n' "${binary_base%/}" "$(release_artifact_name)" return 0 fi if [ -z "$repo" ]; then return 1 fi web_base="$(repo_web_base "$repo")" || return 1 if [ "$binary_version" = "latest" ]; then printf '%s/releases/latest/download/%s\n' "$web_base" "$(release_artifact_name)" else printf '%s/releases/download/%s/%s\n' "$web_base" "$binary_version" "$(release_artifact_name)" fi } release_latest_tag_download_url() { if [ -n "$binary_url" ] || [ -n "$binary_base" ] || [ "$binary_version" != "latest" ] || [ -z "$repo" ]; then return 1 fi web_base="$(repo_web_base "$repo")" || return 1 latest_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' "$web_base/releases/latest" 2>/dev/null || true)" case "$latest_url" in "$web_base"/releases/tag/*) tag="${latest_url##"$web_base"/releases/tag/}" [ -n "$tag" ] || return 1 printf '%s/releases/download/%s/%s\n' "$web_base" "$tag" "$(release_artifact_name)" ;; *) return 1 ;; esac } version_part() { version="$1" index="$2" printf '%s\n' "$version" \ | sed 's/[^0-9][^0-9]*/ /g' \ | awk -v index="$index" '{ value=$index; if (value == "") value=0; print value }' } version_less_than() { left="$1" right="$2" for index in 1 2 3 4 5 6 7 8; do lpart="$(version_part "$left" "$index")" rpart="$(version_part "$right" "$index")" if [ "$lpart" -lt "$rpart" ]; then return 0 fi if [ "$lpart" -gt "$rpart" ]; then return 1 fi done return 1 } current_source_version() { if [ -f Cargo.toml ]; then sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p' return 0 fi [ -n "$repo" ] || return 1 web_base="$(repo_web_base "$repo")" || return 1 curl -fsSL "$web_base/raw/branch/main/Cargo.toml" 2>/dev/null \ | sed -n 's/^version = "\(.*\)"/\1/p' \ | sed -n '1p' } latest_release_tag() { [ -n "$repo" ] || return 1 web_base="$(repo_web_base "$repo")" || return 1 latest_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' "$web_base/releases/latest" 2>/dev/null || true)" case "$latest_url" in "$web_base"/releases/tag/*) tag="${latest_url##"$web_base"/releases/tag/}" [ -n "$tag" ] || return 1 printf '%s\n' "$tag" ;; *) return 1 ;; esac } latest_release_is_stale() { if [ -n "$binary_url" ] || [ -n "$binary_base" ] || [ "$binary_version" != "latest" ]; then return 1 fi latest="$(latest_release_tag || true)" current="$(current_source_version || true)" [ -n "$latest" ] && [ -n "$current" ] || return 1 latest="${latest#v}" if version_less_than "$latest" "$current"; then echo "latest release $latest is older than source $current; skipping stale prebuilt" >&2 return 0 fi return 1 } find_extracted_binary() { find "$1" -type f -name "$2" 2>/dev/null | sed -n '1p' } install_binary() { src="$1" dst="$2" dst_dir="$(dirname "$dst")" dst_base="$(basename "$dst")" tmp="$dst_dir/.$dst_base.tmp.$$" install -m 0755 "$src" "$tmp" if ! mv -f "$tmp" "$dst"; then rm -f "$tmp" return 1 fi } install_extracted_binary() { found="$(find_extracted_binary "$1" "$2")" if [ -z "$found" ]; then echo "prebuilt archive missing $2" >&2 return 1 fi install_binary "$found" "$3" } sha256_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else return 1 fi } verify_archive_checksum() { url="$1" archive="$2" checksum_file="$3" if ! curl -fsL "$url.sha256" -o "$checksum_file"; then echo "prebuilt checksum unavailable; continuing without sidecar verification" >&2 return 0 fi expected="$(awk '{print $1}' "$checksum_file" | sed -n '1p')" actual="$(sha256_file "$archive")" || { echo "sha256sum/shasum not found for checksum verification" >&2 return 1 } if [ "$expected" != "$actual" ]; then echo "prebuilt checksum mismatch for $url" >&2 return 1 fi } expected_archive_version() { url="$1" case "$url" in */releases/download/v*/*) tag="${url#*/releases/download/v}" tag="${tag%%/*}" printf '%s\n' "$tag" return 0 ;; esac if [ -z "$binary_url" ] && [ -z "$binary_base" ] && [ "$binary_version" != "latest" ]; then printf '%s\n' "${binary_version#v}" return 0 fi return 1 } verify_archive_version() { extract_dir="$1" url="$2" expected="$(expected_archive_version "$url" || true)" [ -n "$expected" ] || return 0 version_file="$(find "$extract_dir" -type f -name VERSION 2>/dev/null | sed -n '1p')" if [ -z "$version_file" ]; then echo "prebuilt archive missing VERSION for $url" >&2 return 1 fi actual="$(tr -d '\r\n' <"$version_file")" if [ "$actual" != "$expected" ]; then echo "prebuilt archive version mismatch for $url: expected $expected, got $actual" >&2 return 1 fi } try_install_prebuilt() { if latest_release_is_stale; then return 1 fi download_url="$(release_latest_tag_download_url || release_download_url)" || return 1 tmpdir="$(mktemp -d)" archive="$tmpdir/$(release_artifact_name)" checksum_file="$archive.sha256" [ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)" need curl need tar if ! curl -fsL "$download_url" -o "$archive" 2>/dev/null; then alt_download_url="$(release_download_url || true)" if [ -z "$alt_download_url" ] || ! curl -fsL "$alt_download_url" -o "$archive"; then echo "prebuilt unavailable: $download_url" >&2 [ -z "$alt_download_url" ] || echo "prebuilt unavailable: $alt_download_url" >&2 return 1 fi download_url="$alt_download_url" fi verify_archive_checksum "$download_url" "$archive" "$checksum_file" || return 1 mkdir -p "$tmpdir/extract" if ! tar -xzf "$archive" -C "$tmpdir/extract"; then echo "prebuilt archive could not be extracted: $download_url" >&2 return 1 fi verify_archive_version "$tmpdir/extract" "$download_url" || return 1 install_extracted_binary "$tmpdir/extract" dosh-client "$bindir/dosh-client" || return 1 ln -sf dosh-client "$bindir/dosh" if [ "$role" = "server" ] || [ "$role" = "both" ]; then install_extracted_binary "$tmpdir/extract" dosh-server "$bindir/dosh-server" || return 1 install_extracted_binary "$tmpdir/extract" dosh-auth "$bindir/dosh-auth" || return 1 fi if found_bench="$(find_extracted_binary "$tmpdir/extract" dosh-bench)" && [ -n "$found_bench" ]; then install_binary "$found_bench" "$bindir/dosh-bench" fi return 0 } install_from_source() { ensure_cargo if [ "$from_current" -eq 1 ] || [ -f Cargo.toml ]; then src_dir="$(pwd)" else if [ -z "$repo" ]; then echo "DOSH_REPO or --repo is required when running the installer from curl" >&2 exit 2 fi case "$update_cache" in ""|"/"|"$HOME"|"$HOME/"|"$HOME/.cache") echo "refusing unsafe update cache path: $update_cache" >&2 exit 2 ;; esac need git mkdir -p "$(dirname "$update_cache")" if [ -d "$update_cache/.git" ]; then [ "$quiet" = "1" ] && echo "Updating Dosh source" git -C "$update_cache" remote set-url origin "$repo" if [ "$quiet" = "1" ]; then git -C "$update_cache" fetch -q --depth 1 origin main else git -C "$update_cache" fetch --depth 1 origin main fi git -C "$update_cache" checkout -q -B main FETCH_HEAD else [ "$quiet" = "1" ] && echo "Downloading Dosh source" rm -rf "$update_cache" if [ "$quiet" = "1" ]; then git clone -q --depth 1 --branch main "$repo" "$update_cache" >/dev/null else git clone --depth 1 --branch main "$repo" "$update_cache" >/dev/null fi fi src_dir="$update_cache" fi cd "$src_dir" [ "$quiet" = "1" ] && echo "Building Dosh $role" if [ "$role" = "client" ]; then if [ "$quiet" = "1" ]; then cargo build -q --release --bin dosh-client else cargo build --release --bin dosh-client fi else if [ "$quiet" = "1" ]; then cargo build -q --release else cargo build --release fi fi install_binary target/release/dosh-client "$bindir/dosh-client" ln -sf dosh-client "$bindir/dosh" if [ "$role" = "server" ] || [ "$role" = "both" ]; then install_binary target/release/dosh-server "$bindir/dosh-server" install_binary target/release/dosh-auth "$bindir/dosh-auth" fi if [ -f target/release/dosh-bench ]; then install_binary target/release/dosh-bench "$bindir/dosh-bench" fi } write_systemd_service() { if [ -n "$src_dir" ] && [ -f "$src_dir/packaging/systemd/dosh-server.service" ]; then sed "s#ExecStart=%h/.local/bin/dosh-server serve#ExecStart=$bindir/dosh-server serve#" \ "$src_dir/packaging/systemd/dosh-server.service" >"$systemd_user_dir/dosh-server.service" else cat >"$systemd_user_dir/dosh-server.service" <"$server_config" </dev/null 2>&1; then mkdir -p "$systemd_user_dir" write_systemd_service if [ "$start_server" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1; then systemctl --user enable dosh-server.service systemctl --user restart dosh-server.service fi elif [ "$start_server" -eq 1 ]; then nohup "$bindir/dosh-server" serve >"$data_dir/dosh-server.log" 2>&1 & fi fi if [ "$role" = "client" ] || [ "$role" = "both" ]; then case ":$PATH:" in *":$bindir:"*) ;; *) if [ "$(uname -s)" = "Darwin" ] || [ "$(basename "${SHELL:-}")" = "zsh" ]; then shell_rc="$HOME/.zshrc" else shell_rc="$HOME/.profile" fi mkdir -p "$(dirname "$shell_rc")" if [ ! -f "$shell_rc" ] || ! grep -F "$bindir" "$shell_rc" >/dev/null 2>&1; then printf '\nexport PATH="%s:$PATH"\n' "$bindir" >>"$shell_rc" fi ;; esac client_config="$config_dir/client.toml" if [ "$force_config" -eq 1 ] || [ ! -f "$client_config" ]; then default_server="${server:-user@example.com}" update_repo="${repo:-https://git.palav.dev/Palav/dosh.git}" if [ -n "$dosh_host" ]; then dosh_host_line="dosh_host = \"$dosh_host\"" else dosh_host_line="# dosh_host = \"public.example.com\"" fi cat >"$client_config" <"$hosts_config" < $bindir/dosh update --check Client config: $config_dir/client.toml $config_dir/hosts.toml EOF fi if [ "$role" = "server" ] || [ "$role" = "both" ]; then cat <