AdGuardHome/scripts/install.sh
Ainar Garipov ac5a96fada Pull request 2302: upd-all
Squashed commit of the following:

commit f920006277f39b74c803139af2a9039aa45effae
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 8 16:14:41 2024 +0300

    all: fix pre-commit; upd dnsproxy

commit 391f79b244
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Nov 7 18:53:28 2024 +0300

    scripts: imp install

commit 35324db80b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Nov 7 18:20:23 2024 +0300

    all: imp docs, scripts

commit d2724cfaef
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Nov 7 17:26:23 2024 +0300

    all: upd go, deps, tools, scripts
2024-11-08 17:18:16 +03:00

602 lines
13 KiB
Bash

#!/bin/sh
# AdGuard Home Installation Script
# Exit the script if a pipeline fails (-e), prevent accidental filename
# expansion (-f), and consider undefined variables as errors (-u).
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller
# requested verbosity level greater than 0. Otherwise, it does nothing.
log() {
if [ "$verbose" -gt '0' ]; then
echo "$1" 1>&2
fi
}
# Function error_exit is an echo wrapper that writes to stderr and stops the
# script execution with code 1.
error_exit() {
echo "$1" 1>&2
exit 1
}
# Function usage prints the note about how to use the script.
#
# TODO(e.burkov): Document each option.
usage() {
echo 'install.sh: usage: [-c channel] [-C cpu_type] [-h] [-O os] [-o output_dir]' \
'[-r|-R] [-u|-U] [-v|-V]' 1>&2
exit 2
}
# Function maybe_sudo runs passed command with root privileges if use_sudo isn't
# equal to 0.
#
# TODO(e.burkov): Use everywhere the sudo_cmd isn't quoted.
maybe_sudo() {
if [ "$use_sudo" -eq 0 ]; then
"$@"
else
"$sudo_cmd" "$@"
fi
}
# Function is_command checks if the command exists on the machine.
is_command() {
command -v "$1" >/dev/null 2>&1
}
# Function is_little_endian checks if the CPU is little-endian.
#
# See https://serverfault.com/a/163493/267530.
is_little_endian() {
# The ASCII character "I" has the octal code of 111. In the two-byte octal
# display mode (-o), hexdump will print it either as "000111" on a little
# endian system or as a "111000" on a big endian one. Return the sixth
# character to compare it against the number '1'.
#
# Do not use echo -n, because its behavior in the presence of the -n flag is
# explicitly implementation-defined in POSIX. Use hexdump instead of od,
# because OpenWrt and its derivatives have the former but not the latter.
is_little_endian_result="$(
printf 'I' \
| hexdump -o \
| awk '{ print substr($2, 6, 1); exit; }'
)"
readonly is_little_endian_result
[ "$is_little_endian_result" -eq '1' ]
}
# Function check_required checks if the required software is available on the
# machine. The required software:
#
# unzip (macOS) / tar (other unixes)
#
# curl/wget are checked in function configure.
check_required() {
required_darwin="unzip"
required_unix="tar"
readonly required_darwin required_unix
case "$os" in
'freebsd' | 'linux' | 'openbsd')
required="$required_unix"
;;
'darwin')
required="$required_darwin"
;;
*)
# Generally shouldn't happen, since the OS has already been validated.
error_exit "unsupported operating system: '$os'"
;;
esac
readonly required
# Don't use quotes to get word splitting.
for cmd in $required; do
log "checking $cmd"
if ! is_command "$cmd"; then
log "the full list of required software: [$required]"
error_exit "$cmd is required to install AdGuard Home via this script"
fi
done
}
# Function check_out_dir requires the output directory to be set and exist.
check_out_dir() {
if [ "$out_dir" = '' ]; then
error_exit 'output directory should be presented'
fi
if ! [ -d "$out_dir" ]; then
log "$out_dir directory will be created"
fi
}
# Function parse_opts parses the options list and validates it's combinations.
parse_opts() {
while getopts "C:c:hO:o:rRuUvV" opt "$@"; do
case "$opt" in
C)
cpu="$OPTARG"
;;
c)
channel="$OPTARG"
;;
h)
usage
;;
O)
os="$OPTARG"
;;
o)
out_dir="$OPTARG"
;;
R)
reinstall='0'
;;
U)
uninstall='0'
;;
r)
reinstall='1'
;;
u)
uninstall='1'
;;
V)
verbose='0'
;;
v)
verbose='1'
;;
*)
log "bad option $OPTARG"
usage
;;
esac
done
if [ "$uninstall" -eq '1' ] && [ "$reinstall" -eq '1' ]; then
error_exit 'the -r and -u options are mutually exclusive'
fi
}
# Function set_channel sets the channel if needed and validates the value.
set_channel() {
# Validate.
case "$channel" in
'development' | 'edge' | 'beta' | 'release')
# All is well, go on.
;;
*)
error_exit "invalid channel '$channel'
supported values are 'development', 'edge', 'beta', and 'release'"
;;
esac
# Log.
log "channel: $channel"
}
# Function set_os sets the os if needed and validates the value.
set_os() {
# Set if needed.
if [ "$os" = '' ]; then
os="$(uname -s)"
case "$os" in
'Darwin')
os='darwin'
;;
'FreeBSD')
os='freebsd'
;;
'Linux')
os='linux'
;;
'OpenBSD')
os='openbsd'
;;
*)
error_exit "unsupported operating system: '$os'"
;;
esac
fi
# Validate.
case "$os" in
'darwin' | 'freebsd' | 'linux' | 'openbsd')
# All right, go on.
;;
*)
error_exit "unsupported operating system: '$os'"
;;
esac
# Log.
log "operating system: $os"
}
# Function set_cpu sets the cpu if needed and validates the value.
set_cpu() {
# Set if needed.
if [ "$cpu" = '' ]; then
cpu="$(uname -m)"
case "$cpu" in
'x86_64' | 'x86-64' | 'x64' | 'amd64')
cpu='amd64'
;;
'i386' | 'i486' | 'i686' | 'i786' | 'x86')
cpu='386'
;;
'armv5l')
cpu='armv5'
;;
'armv6l')
cpu='armv6'
;;
'armv7l' | 'armv8l')
cpu='armv7'
;;
'aarch64' | 'arm64')
cpu='arm64'
;;
'mips' | 'mips64')
if is_little_endian; then
cpu="${cpu}le"
fi
cpu="${cpu}_softfloat"
;;
*)
error_exit "unsupported cpu type: $cpu"
;;
esac
fi
# Validate.
case "$cpu" in
'amd64' | '386' | 'armv5' | 'armv6' | 'armv7' | 'arm64')
# All right, go on.
;;
'mips64le_softfloat' | 'mips64_softfloat' | 'mipsle_softfloat' | 'mips_softfloat')
# That's right too.
;;
*)
error_exit "unsupported cpu type: $cpu"
;;
esac
# Log.
log "cpu type: $cpu"
}
# Function fix_darwin performs some configuration changes for macOS if needed.
#
# TODO(a.garipov): Remove after the final v0.107.0 release.
#
# See https://github.com/AdguardTeam/AdGuardHome/issues/2443.
fix_darwin() {
if [ "$os" != 'darwin' ]; then
return 0
fi
# Set the package extension.
pkg_ext='zip'
# It is important to install AdGuard Home into the /Applications directory
# on macOS. Otherwise, it may grant not enough privileges to the AdGuard
# Home.
out_dir='/Applications'
}
# Function fix_freebsd performs some fixes to make it work on FreeBSD.
fix_freebsd() {
if ! [ "$os" = 'freebsd' ]; then
return 0
fi
rcd='/usr/local/etc/rc.d'
readonly rcd
if ! [ -d "$rcd" ]; then
mkdir "$rcd"
fi
}
# download_curl uses curl(1) to download a file. The first argument is the URL.
# The second argument is optional and is the output file.
download_curl() {
curl_output="${2:-}"
if [ "$curl_output" = '' ]; then
curl -L -S -s "$1"
else
curl -L -S -o "$curl_output" -s "$1"
fi
}
# download_wget uses wget(1) to download a file. The first argument is the URL.
# The second argument is optional and is the output file.
download_wget() {
wget_output="${2:--}"
wget --no-verbose -O "$wget_output" "$1"
}
# download_fetch uses fetch(1) to download a file. The first argument is the
# URL. The second argument is optional and is the output file.
download_fetch() {
fetch_output="${2:-}"
if [ "$fetch_output" = '' ]; then
fetch -o '-' "$1"
else
fetch -o "$fetch_output" "$1"
fi
}
# Function set_download_func sets the appropriate function for downloading
# files.
set_download_func() {
if is_command 'curl'; then
# Go on and use the default, download_curl.
return 0
elif is_command 'wget'; then
download_func='download_wget'
elif is_command 'fetch'; then
download_func='download_fetch'
else
error_exit "either curl or wget is required to install AdGuard Home via this script"
fi
}
# Function set_sudo_cmd sets the appropriate command to run a command under
# superuser privileges.
set_sudo_cmd() {
case "$os" in
'openbsd')
sudo_cmd='doas'
;;
'darwin' | 'freebsd' | 'linux')
# Go on and use the default, sudo.
;;
*)
error_exit "unsupported operating system: '$os'"
;;
esac
}
# Function configure sets the script's configuration.
configure() {
set_channel
set_os
set_cpu
fix_darwin
set_download_func
set_sudo_cmd
check_out_dir
pkg_name="AdGuardHome_${os}_${cpu}.${pkg_ext}"
url="https://static.adtidy.org/adguardhome/${channel}/${pkg_name}"
agh_dir="${out_dir}/AdGuardHome"
readonly pkg_name url agh_dir
log "AdGuard Home will be installed into $agh_dir"
}
# Function is_root checks for root privileges to be granted.
is_root() {
user_id="$(id -u)"
if [ "$user_id" -eq '0' ]; then
log 'script is executed with root privileges'
return 0
fi
if is_command "$sudo_cmd"; then
log 'note that AdGuard Home requires root privileges to install using this script'
return 1
fi
error_exit 'root privileges are required to install AdGuard Home using this script
please, restart it with root privileges'
}
# Function rerun_with_root downloads the script, runs it with root privileges,
# and exits the current script. It passes the necessary configuration of the
# current script to the child script.
#
# TODO(e.burkov): Try to avoid restarting.
rerun_with_root() {
script_url='https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh'
readonly script_url
r='-R'
if [ "$reinstall" -eq '1' ]; then
r='-r'
fi
u='-U'
if [ "$uninstall" -eq '1' ]; then
u='-u'
fi
v='-V'
if [ "$verbose" -eq '1' ]; then
v='-v'
fi
readonly r u v
log 'restarting with root privileges'
# Group curl/wget together with an echo, so that if the former fails before
# producing any output, the latter prints an exit command for the following
# shell to execute to prevent it from getting an empty input and exiting
# with a zero code in that case.
{ "$download_func" "$script_url" || echo 'exit 1'; } \
| $sudo_cmd sh -s -- -c "$channel" -C "$cpu" -O "$os" -o "$out_dir" "$r" "$u" "$v"
# Exit the script. Since if the code of the previous pipeline is non-zero,
# the execution won't reach this point thanks to set -e, exit with zero.
exit 0
}
# Function download downloads the file from the URL and saves it to the
# specified filepath.
download() {
log "downloading package from $url to $pkg_name"
if ! "$download_func" "$url" "$pkg_name"; then
error_exit "cannot download the package from $url into $pkg_name"
fi
log "successfully downloaded $pkg_name"
}
# Function unpack unpacks the passed archive depending on it's extension.
unpack() {
log "unpacking package from $pkg_name into $out_dir"
# shellcheck disable=SC2174
if ! mkdir -m 0700 -p "$out_dir"; then
error_exit "cannot create directory $out_dir"
fi
case "$pkg_ext" in
'zip')
unzip "$pkg_name" -d "$out_dir"
;;
'tar.gz')
tar -C "$out_dir" -f "$pkg_name" -x -z
;;
*)
error_exit "unexpected package extension: '$pkg_ext'"
;;
esac
unpacked_contents="$(
echo
ls -l -A "$agh_dir"
)"
log "successfully unpacked, contents: $unpacked_contents"
rm "$pkg_name"
}
# Function handle_existing detects the existing AGH installation and takes care
# of removing it if needed.
handle_existing() {
if ! [ -d "$agh_dir" ]; then
log 'no need to uninstall'
if [ "$uninstall" -eq '1' ]; then
exit 0
fi
return 0
fi
existing_adguard_home="$(ls -1 -A "$agh_dir")"
if [ "$existing_adguard_home" != '' ]; then
log 'the existing AdGuard Home installation is detected'
if [ "$reinstall" -ne '1' ] && [ "$uninstall" -ne '1' ]; then
error_exit \
"to reinstall/uninstall the AdGuard Home using this script specify one of the '-r' or '-u' flags"
fi
# TODO(e.burkov): Remove the stop once v0.107.1 released.
if (cd "$agh_dir" && ! ./AdGuardHome -s stop || ! ./AdGuardHome -s uninstall); then
# It doesn't terminate the script since it is possible that AGH just
# not installed as service but appearing in the directory.
log "cannot uninstall AdGuard Home from $agh_dir"
fi
rm -r "$agh_dir"
log 'AdGuard Home was successfully uninstalled'
fi
if [ "$uninstall" -eq '1' ]; then
exit 0
fi
}
# Function install_service tries to install AGH as service.
install_service() {
# Installing the service as root is required at least on FreeBSD.
use_sudo='0'
if [ "$os" = 'freebsd' ]; then
use_sudo='1'
fi
if (cd "$agh_dir" && maybe_sudo ./AdGuardHome -s install); then
return 0
fi
log "installation failed, removing $agh_dir"
rm -r "$agh_dir"
# Some devices detected to have armv7 CPU face the compatibility issues with
# actual armv7 builds. We should try to install the armv5 binary instead.
#
# See https://github.com/AdguardTeam/AdGuardHome/issues/2542.
if [ "$cpu" = 'armv7' ]; then
cpu='armv5'
reinstall='1'
log "trying to use $cpu cpu"
rerun_with_root
fi
error_exit 'cannot install AdGuardHome as a service'
}
# Entrypoint
# Set default values of configuration variables.
channel='release'
reinstall='0'
uninstall='0'
verbose='0'
cpu=''
os=''
out_dir='/opt'
pkg_ext='tar.gz'
download_func='download_curl'
sudo_cmd='sudo'
parse_opts "$@"
echo 'starting AdGuard Home installation script'
configure
check_required
if ! is_root; then
rerun_with_root
fi
# Needs rights.
fix_freebsd
handle_existing
download
unpack
install_service
printf '%s\n' \
'AdGuard Home is now installed and running' \
'you can control the service status with the following commands:' \
"$sudo_cmd ${agh_dir}/AdGuardHome -s start|stop|restart|status|install|uninstall"