From d9ae6dbcc34669e3880a1a64f538fa222a1d1657 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Mon, 17 May 2021 15:24:05 +0300 Subject: [PATCH] Pull request: improve installation script Merge in DNS/adguard-home from 2542-2462-imp-script to master Closes #2462. Updates #2542. Updates #2613. Squashed commit of the following: commit 4a7472200f2ae07aeccc3511a75a94674b655cdb Author: Eugene Burkov Date: Mon May 17 15:11:08 2021 +0300 scripts: imp naming, docs commit acbb5864a34e81d8c80767dd7ef57dffb189dc64 Author: Eugene Burkov Date: Mon May 17 15:05:57 2021 +0300 scripts: imp fix commit 64777015b6d50e7330fbe7546de1f436f4cb707f Author: Eugene Burkov Date: Mon May 17 14:54:05 2021 +0300 scripts: fix bsd commit 3308921f4c253c8670c4be70896c55f7a892ff3e Author: Eugene Burkov Date: Mon May 17 14:04:04 2021 +0300 scripts: imp requirements checking commit 453cf7a4ce676d2eb09d423304e4d5a8962ac4e5 Author: Eugene Burkov Date: Mon May 17 13:54:38 2021 +0300 scripts: fix docs commit a2229052f6cf747247c8290cd0de27cc88c14977 Merge: 0852c87b 120ba967 Author: Eugene Burkov Date: Mon May 17 13:50:45 2021 +0300 Merge branch 'master' into 2542-2462-imp-script commit 0852c87bf33b833095644e649bbcedbf89ad4f82 Author: Eugene Burkov Date: Mon May 17 13:49:27 2021 +0300 scripts: add os-specific requirements check commit 6313d8fd19f70c41b7091511ca8d979859ddb10c Merge: 1b4092d5 a031cae4 Author: Eugene Burkov Date: Fri May 14 19:46:57 2021 +0300 Merge branch 'master' into 2542-2462-imp-script commit 1b4092d5ee1de5b56c6252bf9debe8b90cdf954e Author: Eugene Burkov Date: Fri May 14 19:46:09 2021 +0300 scripts: imp install.sh a little commit 0584b30b4648b88f542fff6f2879c4a7ae042af4 Merge: 22621c86 9d788a29 Author: Eugene Burkov Date: Fri May 14 19:27:47 2021 +0300 Merge branch 'master' into 2542-2462-imp-script commit 22621c86a70e61f98850a10f0c5e450c8aeaa90f Author: Eugene Burkov Date: Fri May 14 19:25:47 2021 +0300 all: imp code commit a38f4adc4688995ca515f8d0d8d271427365623c Author: Eugene Burkov Date: Fri May 14 18:40:37 2021 +0300 all: fix url variable commit 238cb859184da4af025137d99216d51d3b481dc0 Author: Eugene Burkov Date: Fri May 14 18:27:13 2021 +0300 imp code quality commit 0e36c125369d47612fd97c2841a2a688bed5841d Author: Eugene Burkov Date: Fri May 14 16:34:49 2021 +0300 all: fix typos commit b20f0b72141f5abb5e598324a3f2bd750164e612 Author: Eugene Burkov Date: Thu Apr 29 14:02:40 2021 +0300 all: imp installation script --- CHANGELOG.md | 5 + HACKING.md | 7 +- README.md | 10 +- scripts/install.sh | 724 ++++++++++++++++++++++++++++++--------------- 4 files changed, 510 insertions(+), 236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d14f836..c6c2106b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,16 +19,21 @@ and this project adheres to ### Added +- Support for reinstall (`-r`) and uninstall (`-u`) flags in the installation + script ([#2462]). - Support for DHCP `DECLINE` and `RELEASE` message types ([#3053]). ### Fixed +- Error when using installation script on some ARMv7 devices ([#2542]). - DHCP leases validation ([#3107], [#3127]). - Local PTR request recursion in Docker containers ([#3064]). - Ignoring client-specific filtering settings when filtering is disabled in general settings ([#2875]). - Disallowed domains are now case-insensitive ([#3115]). +[#2462]: https://github.com/AdguardTeam/AdGuardHome/issues/2462 +[#2542]: https://github.com/AdguardTeam/AdGuardHome/issues/2542 [#2875]: https://github.com/AdguardTeam/AdGuardHome/issues/2875 [#3053]: https://github.com/AdguardTeam/AdGuardHome/issues/3053 [#3064]: https://github.com/AdguardTeam/AdGuardHome/issues/3064 diff --git a/HACKING.md b/HACKING.md index e1b3bebb..a979a885 100644 --- a/HACKING.md +++ b/HACKING.md @@ -313,6 +313,8 @@ on GitHub and most other Markdown renderers. --> * Avoid bashisms and GNUisms, prefer POSIX features only. + * Avoid spaces between patterns of the same `case` condition. + * Prefer `'raw strings'` to `"double quoted strings"` whenever possible. * Put spaces within `$( cmd )`, `$(( expr ))`, and `{ cmd; }`. @@ -328,10 +330,10 @@ on GitHub and most other Markdown renderers. --> * UPPERCASE names for external exported variables, lowercase for local, unexported ones. - * Use `set -e -f -u` and also `set -x` in verbose mode. - * Use `readonly` liberally. + * Use `set -e -f -u` and also `set -x` in verbose mode. + * Use the `"$var"` form instead of the `$var` form, unless word splitting is required. @@ -356,6 +358,7 @@ on GitHub and most other Markdown renderers. --> * When using `test` (aka `[`), spell compound conditions with `&&`, `||`, and `!` **outside** of `test` instead of `-a`, `-o`, and `!` inside of `test` correspondingly. The latter ones are pretty much deprecated in POSIX. + Also, prefer `!= ''` form instead of `-n` to check if string is empty. See also: “[Problems With the `test` Builtin: What Does `-a` Mean?]”. diff --git a/README.md b/README.md index 5402a482..20de93f7 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,17 @@ It operates as a DNS server that re-routes tracking domains to a "black hole", t ### Automated install (Linux and Mac) Run the following command in your terminal: ``` -curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh +curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v ``` +The script also accepts some options: +* `-c ` to use specified channel. +* `-r` to reinstall AdGuard Home; +* `-u` to uninstall AdGuard Home; +* `-v` for verbose output; + +Note that options `-r` and `-u` are mutually exclusive. + ### Alternative methods #### Manual installation diff --git a/scripts/install.sh b/scripts/install.sh index 7a410cee..3d5403aa 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,249 +1,507 @@ #!/bin/sh # AdGuard Home Installation Script -# -# 1. Download the package -# 2. Unpack it -# 3. Install as a service -# -# Requirements: -# . bash -# . which -# . printf -# . uname -# . id -# . head, tail -# . curl -# . tar or unzip -# . rm -set -e - -log_info() -{ - printf "[info] %s\\n" "$1" -} - -log_error() -{ - printf "[error] %s\\n" "$1" -} - -# Get OS -# Return: darwin, linux, freebsd -detect_os() -{ - UNAME_S="$(uname -s)" - OS= - case "$UNAME_S" in - Linux) - OS=linux - ;; - - FreeBSD) - OS=freebsd - ;; - - Darwin) - OS=darwin - ;; - - *) - return 1 - ;; - - esac - - echo $OS -} - -# Get CPU endianness -# Return: le, "" -cpu_little_endian() -{ - ENDIAN_FLAG="$(head -c 6 /bin/bash | tail -c 1)" - if [ "$ENDIAN_FLAG" = "$(printf '\001')" ]; then - echo 'le' - return 0 +# 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 } -# Get CPU -# Return: amd64, 386, armv5, armv6, armv7, arm64, mips_softfloat, mipsle_softfloat, mips64_softfloat, mips64le_softfloat -detect_cpu() -{ - UNAME_M="$(uname -m)" - CPU= +# 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 - case "$UNAME_M" 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) - LE=$(cpu_little_endian) - CPU=mips${LE}_softfloat - ;; - - mips64) - LE=$(cpu_little_endian) - CPU=mips64${LE}_softfloat - ;; - - *) - return 1 - - esac - - echo "${CPU}" -} - -# Get package file name extension -# Return: tar.gz, zip -package_extension() -{ - if [ "$OS" = "darwin" ]; then - echo "zip" - return 0 - fi - echo "tar.gz" -} - -# Download data to a file -# Use: download URL OUTPUT -download() -{ - log_info "Downloading package from $1 -> $2" - if is_command curl ; then - curl -s "$1" --output "$2" || error_exit "Failed to download $1" - else - error_exit "curl is necessary to install AdGuard Home" - fi -} - -# Unpack package to a directory -# Use: unpack INPUT OUTPUT_DIR PKG_EXT -unpack() -{ - log_info "Unpacking package from $1 -> $2" - mkdir -p "$2" - if [ "$3" = "zip" ]; then - unzip -qq "$1" -d "$2" || return 1 - elif [ "$3" = "tar.gz" ]; then - tar xzf "$1" -C "$2" || return 1 - else - return 1 - fi -} - -# Print error message and exit -# Use: error_exit MESSAGE -error_exit() -{ - log_error "$1" exit 1 } -# Check if command exists -# Use: is_command COMMAND +# 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 is_command checks if the command exists on the machine. is_command() { - check_command="$1" - command -v "${check_command}" >/dev/null 2>&1 + command -v "$1" >/dev/null 2>&1 } -# Entry point -main() { - log_info "Starting AdGuard Home installation script" - - CHANNEL=${1} - if [ "${CHANNEL}" != "beta" ] && [ "${CHANNEL}" != "edge" ]; then - CHANNEL=release - fi - log_info "Channel ${CHANNEL}" - - OS=$(detect_os) || error_exit "Cannot detect your OS" - CPU=$(detect_cpu) || error_exit "Cannot detect your CPU" - - # TODO: Remove when Mac M1 native support is added - if [ "${OS}" = "darwin" ] && [ "${CPU}" = "arm64" ]; then - CPU="amd64" - log_info "Use ${CPU} build on Mac M1 until the native ARM support is added" - fi - - PKG_EXT=$(package_extension) - PKG_NAME=AdGuardHome_${OS}_${CPU}.${PKG_EXT} - - SCRIPT_URL="https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh" - URL="https://static.adguard.com/adguardhome/${CHANNEL}/${PKG_NAME}" - OUT_DIR="/opt" - if [ "${OS}" = "darwin" ]; then - # It may be important to install AdGuard Home to /Applications on MacOS - # Otherwise, it may not grant enough privileges to it - OUT_DIR="/Applications" - fi - - AGH_DIR="${OUT_DIR}/AdGuardHome" - - # Root check - if [ "$(id -u)" -eq 0 ]; then - log_info "Script called with root privileges" - else - if is_command sudo ; then - log_info "Please note, that AdGuard Home requires root privileges to install using this script." - log_info "Restarting with root privileges" - - exec curl -sSL ${SCRIPT_URL} | sudo sh -s "$@" - exit $? - else - log_info "Root privileges are required to install AdGuard Home using this installer." - log_info "Please, re-run this script as root." - exit 1 - fi - fi - - log_info "AdGuard Home will be installed to ${AGH_DIR}" - - [ -d "${AGH_DIR}" ] && [ -n "$(ls -1 -A -q ${AGH_DIR})" ] && error_exit "Directory ${AGH_DIR} is not empty, abort installation" - - download "${URL}" "${PKG_NAME}" || error_exit "Cannot download the package" - - if [ "${OS}" = "darwin" ]; then - # TODO: remove this after v0.106.0 release - mkdir "${AGH_DIR}" - unpack "${PKG_NAME}" "${AGH_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" - else - unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" - fi - - # Install AdGuard Home service and run it. - ( cd "${AGH_DIR}" && ./AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" ) - - rm "${PKG_NAME}" - - log_info "AdGuard Home is now installed and running." - log_info "You can control the service status with the following commands:" - log_info " sudo ${AGH_DIR}/AdGuardHome -s start|stop|restart|status|install|uninstall" +# Function is_little_endian checks if the CPU is little-endian. +is_little_endian() { + [ "$( head -c 6 /bin/sh | tail -c 1 )" = "$( printf '\001' )" ] } -main "$@" +# Function check_required checks if the required software is available on the +# machine. The required software: +# +# curl +# unzip (macOS) / tar (other unices) +# +check_required() { + readonly required_darwin="unzip" + readonly required_unix="tar" + + # Split with space. + required="curl" + if [ "$os" = 'linux' ] || [ "$os" = 'freebsd' ] + then + required="$required $required_unix" + elif [ "$os" = 'darwin' ] + then + required="$required $required_darwin" + fi + + # Don't use quotes to get word splitting. + for cmd in ${required} + do + echo "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" = '1' ] && [ "$reinstall" = '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 + ('Linux') + os='linux' + ;; + ('FreeBSD') + os='freebsd' + ;; + ('Darwin') + os='darwin' + ;; + esac + fi + + # Validate. + case "$os" + in + ('linux'|'freebsd'|'darwin') + # 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" + ;; + 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. +fix_darwin() { + if ! [ "$os" = 'darwin' ] + then + return 0 + fi + + # TODO: Remove when Mac M1 native support is added. + if [ "$cpu" = 'arm64' ] + then + cpu='amd64' + log "use $cpu build on Mac M1 until the native ARM support is added." + fi + + # Set the package extension. + pkg_ext='zip' + + # It is important to install AdGuard Home into the /Applications + # directory on MacOS. Otherwise, it may not grant 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 + + readonly rcd='/usr/local/etc/rc.d' + if ! [ -d "$rcd" ] + then + mkdir "$rcd" + fi +} + +# Function configure sets the script's configuration. +configure() { + set_channel + set_os + set_cpu + fix_darwin + check_out_dir + + readonly pkg_name="AdGuardHome_${os}_${cpu}.${pkg_ext}" + readonly url="https://static.adguard.com/adguardhome/${channel}/${pkg_name}" + readonly agh_dir="${out_dir}/AdGuardHome" + + log "AdGuard Home will be installed into $agh_dir" +} + +# Function is_root checks for root privileges to be granted. +is_root() { + if [ "$( id -u )" = '0' ] + then + log 'script is executed with root privileges' + + return 0 + fi + + if is_command sudo + 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 and tries to run it with root +# privileges. It also uses the configuration that already set. +# +# TODO(e.burkov): Try to avoid restarting. +rerun_with_root() { + readonly script_url=\ +'https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh' + + flags='' + if [ "$reinstall" = '1' ] + then + flags="${flags} -r" + fi + if [ "$uninstall" = '1' ] + then + flags="${flags} -u" + fi + if [ "$verbose" = '1' ] + then + flags="${flags} -v" + fi + + readonly opts="-c $channel -C $cpu -O $os -o $out_dir $flags" + + log 'restarting with root privileges' + + curl -L -S -s "$script_url" | sudo sh -s -- $opts + exit $? +} + +# Function download downloads the file from the URL and saves it to the +# specified filepath. +download() { + log "downloading package from $url -> $pkg_name" + + if ! curl -s "$url" --output "$pkg_name" + then + error_exit "cannot download the package from $url into $pkg_name" + fi +} + +# Function unpack unpacks the passed archive depending on it's extension. +unpack() { + log "unpacking package from $pkg_name into $out_dir" + if ! mkdir -p "$out_dir" + then + error_exit "cannot create directory at the $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 + + if [ "$?" != '0' ] + then + error_exit "cannot unpack the package into $out_dir" + fi + + 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" = '1' ] + then + exit 0 + fi + + return 0 + fi + + if [ "$( ls -1 -A -q $agh_dir )" != '' ] + then + log 'the existing AdGuard Home installation is detected' + + if [ "$reinstall" != '1' ] && [ "$uninstall" != '1' ] + then + error_exit \ +"to reinstall/uninstall the AdGuard Home using\this script specify one of the '-r' or '-u' flags" + fi + + if ( cd "$agh_dir" && ! ./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" = '1' ] + then + exit 0 + fi +} + +# Function install_service tries to install AGH as service. +install_service() { + # TODO(e.burkov): Think about AGH's output suppressing with no verbose + # flag. + if ( cd "$agh_dir" && ./AdGuardHome -s install ) + then + return 0 + fi + + 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 + +# Exit the script if a pipeline fails (-e), prevent accidental filename +# expansion (-f), and consider undefined variables as errors (-u). +set -e -f -u + +# Set default values of configuration variables. +channel='release' +reinstall='0' +uninstall='0' +verbose='0' +cpu='' +os='' +out_dir='/opt' +pkg_ext='tar.gz' +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 + +echo "\ +AdGuard Home is now installed and running +you can control the service status with the following commands: +sudo ${agh_dir}/AdGuardHome -s start|stop|restart|status|install|uninstall"