#!/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() { if [ "$( id -u )" -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 -> $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" if ! mkdir -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 log "successfully unpacked, contents: $( echo; ls -l -A "$agh_dir" )" 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 if [ "$( ls -1 -A "$agh_dir" )" != '' ] 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 echo "\ 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"