#!/bin/sh # Copyright 2013-2022 Arx Libertatis Team (see the AUTHORS file) # # This file is part of Arx Libertatis. # # Arx Libertatis is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Arx Libertatis is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Arx Libertatis. If not, see . ########################################################################################## # Install script for Arx Fatalis data files to be used with Arx Libertatis # Usage: just run the damned script, maybe check --help # This scripts targets Linux and FreeBSD, but may also work on other UNIX-like systems. # Is this a multi-thousand-line bas^H^H^HPOSIX shell script? # Sure looks like it. # Am I mad? # Most likely. # If you want to edit the required files and checksums, scroll to the end. ########################################################################################## # Colors disable_color() { red='' ; green='' ; yellow='' ; blue='' ; pink='' ; cyan='' ; white='' dim_red='' ; dim_green='' ; dim_yellow='' ; dim_blue='' ; dim_pink='' dim_cyan='' ; dim_white='' ; reset='' } disable_color if [ -z "${NO_COLOR:-1}" ] && [ -t 1 ] && [ "$(tput colors 2> /dev/null)" != -1 ] ; then red="$(printf '\033[1;31m')" green="$(printf '\033[1;32m')" yellow="$(printf '\033[1;33m')" blue="$(printf '\033[1;34m')" pink="$(printf '\033[1;35m')" cyan="$(printf '\033[1;36m')" white="$(printf '\033[1;37m')" dim_red="$(printf '\033[0;31m')" dim_green="$(printf '\033[0;32m')" dim_yellow="$(printf '\033[0;33m')" dim_blue="$(printf '\033[0;34m')" dim_pink="$(printf '\033[0;35m')" dim_cyan="$(printf '\033[0;36m')" dim_white="$(printf '\033[0;37m')" reset="$(printf '\033[m')" fi ########################################################################################## # Constants # Name and download locations for the 1.21 patch patch_ver='1.21' patch_name="ArxFatalis_${patch_ver}_MULTILANG.exe" patch_name_localized="ArxFatalis_${patch_ver}_%s.exe" patch_url_path="arxfatalis/patches/${patch_ver}/${patch_name}" patch_url_master="https://cdn.bethsoft.com/${patch_url_path}" patch_urls="https://arx.vg/${patch_name} ${patch_url_master}" patch_urls="$patch_urls https://web.archive.org/web/${patch_url_master}" # Name and download locations for the Japanese 1.02j patch patch_jp_ver='1.02j' patch_jp_name="arx_jpn_patch_${patch_jp_ver}.exe" patch_jp_url_master="http://www.capcom.co.jp/pc/arx/patch/${patch_jp_name}" patch_jp_urls="https://arx.vg/${patch_jp_name}" # master URL is no longer available patch_jp_urls="$patch_jp_urls https://web.archive.org/web/${patch_jp_url_master}" # Name and store page for the GOG.com download gog_names='"setup_arx_fatalis.exe" "setup_arx_fatalis_2.0.0.7.exe" "setup_arx_fatalis_1.21_(21994).exe" "setup_arx_fatalis_1.22_(38577).exe" "setup_arx_fatalis_1.22_(french)_(38577).exe" "setup_arx_fatalis_1.22_(german)_(38577).exe" "setup_arx_fatalis_1.22_(italian)_(38577).exe" "setup_arx_fatalis_1.22_(russian)_(38577).exe" "setup_arx_fatalis_1.22_(spanish)_(38577).exe"' gog_url='https://www.gog.com/gamecard/arx_fatalis' # Store page for the Steam download steam_appid='1700' steam_url="https://store.steampowered.com/app/$steam_appid/Arx_Fatalis/" steam_install_dir="Arx Fatalis" # Name and wiki page for the demo download demo_names="arx_demo_english.zip arxdemoenglish.zip arx_demo_english.exe" demo_names="$demo_names arx_fatalis_demo_fr.zip arx_demo_german.exe arx_jpn_demo.exe" demo_url='https://arx.vg/demo-data' bug_report_url='https://arx.vg/bug' cabextract_url='https://cabextract.org.uk/' innoextract_url='https://constexpr.org/innoextract/' ########################################################################################## # Standard directories user_pwd="$PWD" user_pwd="${user_pwd%/}" platform="$(uname)" command="$(basename "$0")" scommand="$(printf '%s' "$command" | tr - _)" if [ "$platform" = 'Darwin' ] ; then # macOS data_dirs='/Applications' data_home="$HOME/Library/Application Support" config_home="$HOME/Library/Application Support" data_dir_suffixes='ArxLibertatis' user_dir_suffixes='ArxLibertatis' config_dir_suffixes='ArxLibertatis' downloads_dir="$HOME/Downloads" else # Linux, FreeBSD, ... data_dirs="${XDG_DATA_DIRS:-"/usr/local/share/:/usr/share/"}:/opt" data_home="${XDG_DATA_HOME:-"$HOME/.local/share"}" config_home="${XDG_CONFIG_HOME:-"$HOME/.config"}" data_dir_suffixes='games/arx:arx' user_dir_suffixes='arx' config_dir_suffixes='arx' [ -f "${config_home}/user-dirs.dirs" ] && . "${config_home}/user-dirs.dirs" downloads_dir="${XDG_DOWNLOAD_DIR:-"$HOME/Downloads"}" fi downloads_dir="${downloads_dir%/}" tempdir="${TMPDIR:-"/tmp"}" tempdir="${tempdir%/}" [ -d "$tempdir" ] || tempdir="$PWD" eval "data_path=\"\$${scommand}_PATH\"" [ -z "$data_path" ] && data_path="$arx_PATH" ########################################################################################## # Helper functions exec 4>&2 # fd to the original stderr (we redirect output to a log file in some cases) logfile='' # log file receiving sdout and stderr true=0 # Return value / exit status that evaluates to true false=1 # Return value / exit status that evaluates to false # 1 if the script is being run as root, false otherwise if [ "$(id -u)" = 0 ] ; then is_root=1 ; else is_root=0 ; fi # Print one line of text, without escape codes or other shell-specific shenanigans. # Seriously, shells, you can't even agree on a consistent implementation of echo? # Usage: print print() { printf '%s\n' "$1" } puts() { printf '%s' "$1" } disabled_commands=' ' # List of commands that should not be used, even if they exist # Make `have` return false for a command # Usage: disable_command disable_command() { disabled_commands="$disabled_commands$1 " } # Check if a command is available. # Usage: have # Return: $true if the command is available, $false otherwise have() { case "$disabled_commands" in *" $1 "*) return $false ; esac command -v "$1" > /dev/null 2>&1 } # Make a path absolute no matter if it is relative or not # Usage: abspath # Too bad we can't just use readlink -m abspath() { case "$1" in /*) print "$1" ;; *) print "$PWD/$1" ;; esac } # Get the canonical representation of an existing path # Usage: canonicalize # Too bad we can't just use readlink -f if have realpath ; then canonicalize() { realpath "$1" ; } elif have grealpath ; then canonicalize() { grealpath "$1" ; } elif have greadlink ; then canonicalize() { greadlink -f "$1" ; } else canonicalize() { _canonicalize_old_pwd="$PWD" _canonicalize_file="$1" while true ; do cd "$(dirname "$_canonicalize_file")" _canonicalize_file="$(basename "$_canonicalize_file")" [ -L "$_canonicalize_file" ] || break; _canonicalize_file="$(readlink "$_canonicalize_file")" done echo "$(pwd -P)/$_canonicalize_file" cd "$_canonicalize_old_pwd" } fi cleanup_functions='' # List of functions to be run on exit # Add a function to run on exit. # Functions are run in the order they are added. # Usage: on_exit # Cleanup functions will receive one argument: the exit message if any or an empty string. on_exit() { [ -z "$cleanup_functions" ] || cleanup_functions=" $cleanup_functions" cleanup_functions="$1$cleanup_functions" } # Run exit runctions. cleanup() { _cleanup_functions="$cleanup_functions" ; cleanup_functions='' [ -z "$_cleanup_functions" ] && return eval "for _cleanup_func in $_cleanup_functions ; do \"\$_cleanup_func\" \"\$@\" ; done" } # Register our cleanup handler. trap "cleanup" EXIT # Some shells don't have their own (non-libc) SIGINT handler, but the EXIT trap # won't trigger if there is none! trap 'print >&4 ; quit 1' INT # Run cleanup functions with a possible message and then exit. # Usage: quit [] quit() { cleanup "$2" exit $1 } # Exit with a non-zero status and optionally print a message. # Usage: die [...] die() { _die_message='' if [ $# -gt 0 ] ; then _die_message="$1" ; shift for _die_arg ; do _die_message="$_die_message $_die_arg" ; done _die_message="$_die_message If you think this is a bug in the install script please report the complete output at $bug_report_url" if [ -n "$logfile" ] && [ -f "$logfile" ] ; then _die_message="$_die_message Also attach the contents of $logfile" logfile='' # so that we don't remove it on exit printf "${red}%s${reset}\\n" "$_die_message" >&4 # also print to priginal stdout printf '\n%s\n' 'Preserving log file.' >&4 fi printf "${red}%s${reset}\\n" "$_die_message" fi quit 1 "$_die_message" } # Escape a string from stdin for use in a whitespace-separated list. # Usage: print | escape_pipe escape_pipe() { sed "s:[^a-zA-Z0-9/_.$1]:\\\\&:g" } # Escape a string for use in a whitespace-separated list. # Usage: escape escape() { print "$1" | escape_pipe "$2" } # Convert a colon-separated list into an escaped whitespace-separated list. # Usage: to_list to_list() { escape "$1" | sed 's/\\:/ /g' } # Line-based output into a list # Usage: ls | lines_to_list lines_to_list() { escape_pipe | tr '\n' ' ' } # Check if a whitespace separated list contains a string. # Usage: list_contains list_contains() { eval "_list_contents=\"\$$1\"" [ -z "$_list_contents" ] && return $false eval "for _list_contains_entry in $_list_contents ; do" \ " [ \"\$_list_contains_entry\" = \"\$2\" ] && return \$true ; done" return $false } # Append a string to a whitespace separated list. # Usage: list_append [comment] # Whitespace separated lists can be loaded into the argument list using: # eval "set -- $var" list_append() { _list_entry="$(escape "$2")" eval "_list_contents=\"\$$1\"" if [ -z "$_list_contents" ] then eval "$1=\"\$_list_entry\"" else eval "$1=\"\$_list_contents \$_list_entry\"" fi eval "[ -z \"\$$1__list_count\" ] && $1__list_count=0" eval "_list_count=\$$1__list_count" eval "$1__list_comment_$_list_count=\"\$3\"" eval "$1__list_count=\$(($1__list_count + 1))" } # Append one list to another, preserving comments. # Usage: list_merge list_merge() { eval "_list_append=\"\$$2\"" [ -z "$_list_append" ] && return eval " _list_merge_i=0 for _list_merge_entry in $_list_append ; do list_append $1 \"\$_list_merge_entry\" \"\$(list_comment $2 \$_list_merge_i)\" _list_merge_i=\$((_list_merge_i + 1)) done " } # Get a comment associated with alist entry # Usage: list_comment list_comment() { eval "print \"\$$1__list_comment_$2\"" } # Set a comment associated with alist entry # Usage: list_comment set_list_comment() { eval "$1__list_comment_$2=\"\$3\"" } # Append a string to a whitespace separated list if it isn't already in the list. # Usage: set_append [comment] set_append() { if ! list_contains "$1" "$2" ; then list_append "$1" "$2" "$3" fi } # Check if a directory contains a file while ignoring case differences. # Usage: icontains icontains() { [ -n "$(find "$1" -mindepth 1 -maxdepth 1 -iname "$2")" ] } # Check if a directory or file is writable or can be created. # Usage: is_writable is_writable() { [ -w "$1" ] && return $true [ ! -e "$1" ] && is_writable "$(dirname "$1")" } # Create a directory and die with a message on error. # Usage: create_dir create_dir() { mkdir -p "$1" || die "Could not create $2 directory: $1" } probe_file_dirs='' set_append probe_file_dirs "$user_pwd" set_append probe_file_dirs "$downloads_dir" set_append probe_file_dirs "$HOME" set_append probe_file_dirs "$tempdir" # Find a file in standard directories. # Usage: probe_file [comment] # Will call `command ` for each file found. probe_file() { eval "for _probe_file_d in $probe_file_dirs ; do [ -f \"\$_probe_file_d/\$2\" ] && \$1 \"\$_probe_file_d/\$2\" \"\$3\" && return \$true ; done" } # Find files in standard directories. # Usage: probe_file [comment] # Will call `command ` for each file found. probe_files() { [ -z "$2" ] && return $false eval "for _probe_files_file in $2 ; do probe_file \"\$1\" \"\$_probe_files_file\" \"\$3\" && return \$true ; done" return $false } ########################################################################################## # Parse command-line arguments extract_zip_reqs='' list_append extract_zip_reqs 'bsdtar' 'libarchive' list_append extract_zip_reqs 'unzip' list_append extract_zip_reqs '7za' list_append extract_zip_reqs '7z' 'p7zip' extract_ms_cab_reqs='' list_append extract_ms_cab_reqs 'bsdtar' 'with libarchive 3.1+' list_append extract_ms_cab_reqs 'cabextract' "$cabextract_url" list_append extract_ms_cab_reqs '7za' list_append extract_ms_cab_reqs '7z' 'p7zip' extract_installshield_reqs='' list_append extract_installshield_reqs 'unshield' extract_rar_reqs='' list_append extract_rar_reqs 'unrar' extract_ace_reqs='' list_append extract_ace_reqs 'unace' extract_innosetup_reqs='' list_append extract_innosetup_reqs 'innoextract' "$innoextract_url" mount_cdrom_reqs='' list_append mount_cdrom_reqs 'fuseiso' extract_iso_reqs='' list_append extract_iso_reqs 'isoinfo' list_append extract_iso_reqs 'bsdtar' 'libarchive' list_append extract_iso_reqs '7z' 'p7zip' extract_cdrom_reqs='' list_merge extract_cdrom_reqs mount_cdrom_reqs list_merge extract_cdrom_reqs extract_iso_reqs download_reqs='' list_append download_reqs 'wget' list_append download_reqs 'curl' list_append download_reqs 'fetch' 'FreeBSD' printf '%s %s\n' "${white}Welome to the ${green}Arx Fatalis${white} 1.21/1.22 data" \ "install script for UNIX-like systems!${reset}" patchfile='' # Main patch file patchfile_jp='' # Japanese patch file sourcefile='' # Source file or directory datadir='' # Output data directory batch=0 # Never wait for user input gui=0 # Display a graphical user interface (command-line interface otherwise) install=1 # Install new non-patch files selected_stuff=0 # Has the user made a selection installed_stuff=0 # Have we already installed anything? patch=1 # Install patch files if needed probe_patch=1 # Look for patch files in standard locations and download if needed redirect_log=1 # Redirect standard output/error output to a log file in GUI mode # Enable compatibility with old install-* scripts. # Usage: enable_compat_mode enable_compat_mode() { print \ "${yellow}Enabling compatibility mode for ${pink}$command${yellow}.${reset} ${dim_yellow}The individual ${dim_pink}install-*${dim_yellow} scripts have been merged. Rename this script to something else (like ${dim_pink}arx-install-data${dim_yellow}) to unlock its full power!${reset} " >&2 batch=1 probe_patch=0 if [ -z "$1" ] || [ "$1" = '--help' ] || [ "$1" = '-h' ] ; then printf '%s\n\n%s\n' "$5" \ "${yellow}More options are available in the non-compatibility mode.${reset}" exit $false fi if [ -z "$2" ] ; then install=0 ; else sourcefile="$2" ; fi if [ -z "$3" ] ; then patch=0 ; else patchfile="$3" ; fi if [ -z "$4" ] ; then datadir="$user_pwd" ; else datadir="$4" ; fi } case "$command" in install-cd) [ "$1" = "--no-progress" ] && shift # ignore - not supported enable_compat_mode "$1" "$1" "$2" "$3" "\ Usage: $command path/to/mount/point/ path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir] or $command path/to/cd.iso path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir]" ;; install-copy) enable_compat_mode "$1" "$1" '' "$2" "\ Usage: $command path/to/ArxFatalis/ [output_dir]" ;; install-demo) enable_compat_mode "$1" "$1" '' "$2" "\ Usage: $command path/to/arx_demo_english.zip [output_dir]" ;; install-gog) [ "$1" = "--no-progress" ] && shift # ignore - not supported enable_compat_mode "$1" "$1" '' "$2" "\ Usage: $command path/to/setup_arx_fatalis.exe [output_dir]" ;; install-verify) enable_compat_mode "$1" '' '' "$1" "\ Usage: $command [directory]" ;; *) # non-compatibility mode # Print elements in a list, joined by ' or ' # Usage: print_help_or [color] print_help_or() { _print_help_or_var=$1 eval "_print_help_or_list=\"\$$1\"" _print_help_or_color="$2" [ -z "$1" ] && return eval " _print_help_or_i=0 for _print_help_or_entry in $_print_help_or_list ; do [ \$_print_help_or_i = 0 ] || puts ' or ' printf '%s%s' \"\$_print_help_or_color\" \"\$_print_help_or_entry\" [ -z \"\$_print_help_or_color\" ] || puts \"\$reset\" _print_help_or_comment=\"\$(list_comment \$_print_help_or_var \$_print_help_or_i)\" [ -z \"\$_print_help_or_comment\" ] || printf ' (%s)' \"\$_print_help_or_comment\" _print_help_or_i=\$((_print_help_or_i + 1)) done " } # Print elements in a list, one per line. # Usage: print_help_list # prefi-format will receive one argument: the list index starting at 1 print_help_list() { _print_help_list_prefix="$1" _print_help_list_var=$2 eval "_print_help_list_list=\"\$$2\"" [ -z "$1" ] && return eval " _print_help_list_i=0 for _print_help_list_entry in $_print_help_list_list ; do case \"\$_print_help_list_prefix\" in *%*) printf \"\$_print_help_list_prefix\" \$((_print_help_list_i + 1)) ;; *) puts \"\$_print_help_list_prefix\" esac printf \"%s\${reset}\" \"\$_print_help_list_entry\" _print_help_list_comment=\"\$(list_comment \$_print_help_list_var \$_print_help_list_i)\" [ -z \"\$_print_help_list_comment\" ] || printf ' (%s)' \"\$_print_help_list_comment\" printf '\n' _print_help_list_i=\$((_print_help_list_i + 1)) done " } # Print help output. # Usage: print_help [] print_help() { [ -z "${1-}" ] || ( printf '%s\n\n' "${red}$1${reset}" ) print " ${white}Start the script without any arguments to select paths interactively: \$ $command${reset} Usage: $command [--source] source [--patch patchfile] [[--data-dir] datadir] $command [--patch patchfile] [--data-dir datadir] $command --verify [[--data-dir] datadir] ${green}-s, --source PATH${reset} Path to the source file or directory ${cyan}-d, --data-dir DIR${reset} Where to install the data ${blue}-p, --patch FILE${reset} Path to the ${patch_ver} patch file --patch-jp FILE Path to the ${patch_jp_ver} Japanese patch file -v, --verify Only verify the files in the data-dir, don't install new ones, except for patch files. -n, --no-patch Don't use a patch file unless explicitly specified. -h, --help Print this message and maybe more -b, --batch Never ask the user questions -g, --gui Show a GUI asking the user what to do Requires ${dim_pink}KDialog${reset}, ${dim_pink}Zenity${reset}, or ${dim_pink}Xdialog${reset}. If none of them are available the script is re-launched in a terminal emulator. -c, --cli Interactively ask the user to select files/directories (no GUI) --no-redirect-log Don't redirect output to a log file when in GUI mode --disable-COMMAND Don't use the given tool, even if it exists. Valid values are 7z, 7za, aterm, bsdtar, cabextract, curl, dcop, fetch, fuseiso, fusermount, gnome-terminal, greadlink, grealpath, gtkterm, gxmessage, innoextract, isoinfo, kdialog, konsole, md5, md5sum, mount, qdbus, realpath, rxvt, umount, unace, unrar, unshield, unzip, urxvt, wget, Xdialog, xmessage, xterm, x-terminal-emulator, zenity. --gui is enabled by default if there are no arguments *and* stdin, stdout or stderr is not a terminal " [ -n "${1-}" ] && exit $false help_innosetup="$(print_help_or extract_innosetup_reqs "$dim_pink")" help_cdrom="$(print_help_or extract_cdrom_reqs "$dim_pink") or root access" help_cab="$(print_help_or extract_ms_cab_reqs "$dim_pink")" help_zip="$(print_help_or extract_zip_reqs "$dim_pink")" help_unshield="$(print_help_or extract_installshield_reqs "$dim_pink")" help_rar="$(print_help_or extract_rar_reqs "$dim_pink")" help_ace="$(print_help_or extract_ace_reqs "$dim_pink")" help_download="$(print_help_or download_reqs "$dim_pink")" help_optpatch="may use the 1.21 patch file and require ${help_innosetup} if not already patched" help_probe_file_dirs=" a) the current working directory (\$PWD): $user_pwd b) the user's downloads directory (\$XDG_DOWNLOAD_DIR): $downloads_dir c) the user's home directory (\$HOME): $HOME d) the temp directory (\$TMPDIR): $tempdir" help_probe_file_dirs_patch="${help_probe_file_dirs} e) the directory containing the source file" help_probed_files="$gog_names $demo_names" print " The ${pink}dependencies${reset} required by the ${command} script depend on the source files. However, you always need either ${dim_pink}md5sum${reset} or ${dim_pink}md5${reset}. The ${green}source${reset} can be one of many things: * ${white}Mounted Arx Fatalis ${green}cdrom${reset} requires: - ${help_cab} - ${help_innosetup} needs the 1.21 patch file * ${white}Arx Fatalis cdrom ${green}ISO${white} image / device file${reset} requires: - ${help_cdrom} - ${help_cab} - ${help_innosetup} needs the 1.21 patch file * ${white}Arx Fatalis installer from ${green}GOG.com${white}${reset} ($(print_help_or gog_names)) requires: - ${help_innosetup} never uses the 1.21 patch file get it from ${dim_green}${gog_url}${reset} * ${green}Installed${white} copy of Arx Fatalis${reset} (for example from ${green}Steam${reset}) ${help_optpatch} get it from ${dim_green}${steam_url}${reset} * ${white}Arx Fatalis ${green}demo${reset} (one of the following) $(print_help_list " - " demo_names) requires: - ${help_zip} [english/french .zip] - ${help_cab} - ${help_rar} [english .exe] - ${help_ace} [german .exe] - ${help_unshield} [japanese .exe] never uses the 1.21 patch file get it from ${dim_green}${demo_url}${reset} * ${white}Extracted Arx Fatalis demo installer${reset} requires: - ${help_cab} never uses the 1.21 patch file * ${white}Installed copy of the Arx Fatalis demo${reset} never uses the 1.21 patch file If no source is specified, these files will be probed: 1. The following files in${help_probe_file_dirs} $(print_help_list " 1.%d ${green}" help_probed_files) 2. The user's ${green}Steam${reset} library, if available 3. If \$WINEPREFIX is set, any installation in there 4. Any installation in the default WINEPREFIX (${green}~/.wine${reset}) 5. Any mounted ${green}cdrom${reset} or ISO file If no ${blue}patch${reset} file is specified, but is needed and the --no-patch option wasn't specified: 1. Try to find the following files in${help_probe_file_dirs_patch} 1.1. ${blue}${patch_name}${reset} 1.2. $(printf "$patch_name_localized" '') Where is one of EN, ES, FR, GE, IT, RU, depending on the language of the data files. 2. Downloaded from: $(print_help_list " - ${dim_blue}" patch_urls) Downloading the patch file requires ${help_download}. Extracting the ${patch_ver} patch file requires ${help_innosetup}. For the Japanese version, if no ${blue}patch-jp${reset} file is specified, but is needed and the --no-patch option wasn't specified specified: 1. Try to find ${blue}${patch_jp_name}${reset} in${help_probe_file_dirs_patch} 2. Downloaded from: $(print_help_list " - ${dim_blue}" patch_jp_urls) Downloading the patch file requires ${help_download}. Extracting the Japanese patch file requires: - ${help_unshield} - ${help_cab}. If no ${cyan}data-dir${reset} to install into is specified, one is automatically selected similarly to how Arx Libertatis would: If --verify and --no-patch are give, use the first existing directory of the following, otherwise, use the first existing writable directory or, if none exists, the first directory that can be created: 1. Any path in \$${scommand}_PATH or \$arx_PATH (for use in wrapper scripts) 2. \"\${XDG_DATA_DIRS:-\"/usr/local/share/:/usr/share/\"}:/opt\" / \"$data_dir_suffixes\":" i=1 eval "set -- $(to_list "$data_dirs")" for prefix in "$@" ; do eval "set -- $(to_list "$data_dir_suffixes")" for suffix ; do printf " 2.%d. ${dim_cyan}%s${reset}\\n" $i "$prefix/$suffix" i=$((i + 1)) done done print "3. \"\${XDG_DATA_HOME:-\"\$HOME/.local/share\"}\" / \"$user_dir_suffixes\"" i=1 eval "set -- $(to_list "$user_dir_suffixes")" for suffix ; do printf " 3.%d. ${dim_cyan}%s${reset}\\n" $i "$data_home/$suffix" i=$((i + 1)) done print exit $true } user_is_sane=1 if [ ! -t 0 ] || [ ! -t 1 ] || [ ! -t 2 ] ; then [ $# = 0 ] && gui=1 fi while [ $# -gt 0 ] ; do case "$1" in --source=*) sourcefile="${1#--source=}" ; install=1 ;; -s|--source) shift ; sourcefile="$1" ; install=1 ;; --data-dir=*) datadir="${1#--data-dir=}" ;; -d|--data-dir) shift ; datadir="$1" ;; --patch=*) patchfile="${1#--patch=}" ; patch=1 ;; -p|--patch) shift ; patchfile="$1" ; patch=1 ;; --patch-jp=*) patchfile_jp="${1#--patch-jp=}" ; patch=1 ;; --patch-jp) shift ; patchfile_jp="$1" ; patch=1 ;; -v|--verify) install=0 patch=0 ;; -n|--no-patch) [ -z "$patchfile" ] && [ -z "$patchfile_jp" ] && patch=0 probe_patch=0 ;; -b|--batch) batch=1 ;; -g|--gui) gui=1 ;; -c|--cui|--cli) batch=0 ; gui=0 ;; --no-redirect-log) redirect_log=0 ;; --i-am-insane) user_is_sane=0 ;; --disable-*) disable_command "${1#--disable-}" ;; --disable) shift ; disable_command "${1#--disable-}" ;; -h|--help) print_help ;; -*) print_help "Unknown option: $1" ;; *) if [ -z "${sourcefile-}" ] && [ $install = 1 ] ; then sourcefile="$1" elif [ -z "${datadir-}" ] ; then datadir="$1" else print_help "Too many options: $1" ; fi esac [ -z "${1-}" ] && print_help "Expected more options" shift; done print "See \`${dim_pink}$command --help${reset}\` for available options." esac # Make user-provided paths absolute [ -n "$sourcefile" ] && sourcefile="$(abspath "$sourcefile")" [ -n "$datadir" ] && datadir="$(abspath "$datadir")" [ -n "$patchfile" ] && patchfile="$(abspath "$patchfile")" # Sanity check [ $install = 1 ] && [ $batch = 1 ] && [ -z "$sourcefile" ] && [ $user_is_sane = 1 ] \ && die "You have used --batch without providing a source file! This would just pick the first source file found, which is a bad idea™. If you really want this, add the --i-am-insane option." ########################################################################################## # User interface abstraction _dialog_title="Arx Fatalis ${patch_ver} data installer" # Handle magic environment variable to tell the script that it has been launched # in its own terminal and should not try to create a GUI. if [ $batch = 0 ] && [ "$_arx_install_data_force_cli" = 1 ] ; then trap '_arx_install_data_force_cli=0 ; quit 1' INT printf "\n${yellow}%s${reset}\n\n" \ 'Note: Install KDialog, Zenity or Xdialog for a better GUI' wait_exit() { [ "$_arx_install_data_force_cli" = 1 ] && print 'Press enter to exit...' && read f exit $false } on_exit wait_exit gui=0 for var in batch install patch probe_patch sourcefile datadir \ patchfile patchfile_jp disabled_commands; do eval "$var=\"\$_arx_install_data_force_$var\"" done fi # Select the dialog backend to use if [ $gui = 1 ] ; then # Detect if we are running in a KDE session is_kde=0 case "$DESKTOP_SESSION" in *kde*|*KDE*) is_kde=1 ; esac [ -z "$KDE_FULL_SESSION" ] || is_kde=1 [ -z "$KDE_SESSION_UID" ] || is_kde=1 [ -z "$KDE_SESSION_VERSION" ] || is_kde=1 # Select the GUI backend, prefer kdialog for KDE sessions, zenity otherwise if [ $is_kde = 1 ] ; then preferred=kdialog ; else preferred=zenity ; fi for backend in $preferred zenity kdialog Xdialog ; do have $backend && gui=$backend && break done if [ $gui = 1 ] ; then # No dialog backend available # Try opening a graphical terminal and launching the script in there. print 'No GUI dialog backend is available - trying to launch a terminal emulator' term_cmd="$(abspath "$(command -v "$0" 2> /dev/null)")" # Not all terminals accept command arguments in the same way. # Instead of hacking terminal-specific code, use a magic environment # variable to tell the sub-process how to behave. _arx_install_data_force_cli=1 export _arx_install_data_force_cli for var in batch install patch probe_patch sourcefile datadir \ patchfile patchfile_jp disabled_commands ; do eval "_arx_install_data_force_$var=\"\$$var\"" eval "export _arx_install_data_force_$var" done if [ $is_kde = 1 ] ; then preferred=konsole ; else preferred=x-terminal-emulator ; fi for backend in x-terminal-emulator $preferred \ aterm urxvt rxvt konsole xterm gnome-terminal do if have $backend ; then $backend -e "$term_cmd" || continue exit $true fi done # Hm, that didn't work either - bail message="No GUI dialog backend is available" message="$message - install KDialog, Zenity or Xdialog, or use the --cli option." # Final attempt to let the user know what happened for backend in gxmessage xmessage ; do if have $backend ; then $backend -center -buttons OK "$_dialog_title $message" break fi done die "$message" fi # We don't need colors for the UI, but they may cause problems - get rid of them disable_color if [ $redirect_log = 1 ] ; then # Redirect all further output into a log file logfile="$(abspath "$(mktemp "$tempdir/arx-install-data.log.XXXXX")")" clean_logfile() { [ -z "$logfile" ] || rm -f "$logfile" } on_exit clean_logfile print "Enabling GUI mode, standard output/error saved to $logfile" print "Use the --cli option for an interactive command-line interface." exec > "$logfile" 2>&1 else print "Enabling GUI mode..." print "Use the --cli option for an interactive command-line interface." fi elif [ ! "$_arx_install_data_force_cli" = 1 ] ; then print "Enabling CLI mode, use the --gui option for a graphical interface." fi #----------------------------------------------------------------------------------------# # Functions for controlling an asynchronous process via stdin pipe_file='' pipe_pid=0 # Run a command in the background and open a pipe to pass commands to it # Usage: pipe_create [...] pipe_create() { pipe_destroy # Pipe commands via a FIFO or, if that fails, via a regular file pipe_file="$(mktemp -u "$tempdir/arx-install-data.pipe.XXXXX")" if mkfifo -m 600 "$pipe_file" 2> /dev/null ; then # Fast communication via a FIFO "$@" < "$pipe_file" & pipe_pid="$!" else # Fallback via regular file, may use polling pipe_file="$(mktemp "$tempdir/arx-install-data.pipe.XXXXX")" [ -z "$pipe_file" ] && return $false tail -f "$pipe_file" | "$@" & pipe_pid="$!" fi # Open fd 3 for writing into the pipe exec 3> "$pipe_file" return $true } # Kill the program created via pipe_create and cleanup files # Usage: pipe_destroy pipe_destroy() { # Close fd pointing to the pipe exec 3<&- # Remove the FIFO or temp file [ -z "$pipe_file" ] || rm -f "$pipe_file" > /dev/null 2>&1 pipe_file='' # Terminate the remote process [ "$pipe_pid" = 0 ] || kill "$pipe_pid" > /dev/null 2>&1 pipe_pid=0 return $true } # Check if the remote process is running # Usage: pipe_exists || print 'oh noes' pipe_exists() { [ -z "$pipe_file" ] && return $false [ "$pipe_pid" = 0 ] && return $false kill -s 0 "$pipe_pid" > /dev/null 2>&1 } # Send a message to the remote process # Usage: pipe_write pipe_write() { [ -z "$pipe_file" ] || print "$1" >&3 } #----------------------------------------------------------------------------------------# # Code for the different GUI/CLI implementations # Each implementation exposes dialog_* primitives that are used by generic functions. case $gui in #----------------------------------------------------------------------------------------# zenity) # Helper functions # Run Zenity # Usage: zenity_run [...] zenity_run() { _zenity_run_t="$1" ; shift [ -z "$_zenity_run_t" ] || _zenity_run_t="$_zenity_run_t - " zenity --title "$_zenity_run_t$_dialog_title" "$@" } # Dialog abstraction # Create the main progress window. # Usage: dialog_create dialog_create() { pipe_create zenity --title "$_dialog_title" --width 450 --progress } # Destroy the main progress window. # Usage: dialog_destroy dialog_destroy() { pipe_destroy } # Show an error dialog. # Usage: dialog_error dialog_error() { zenity_run 'Error' --error --no-wrap --text="$1" } # Show a message box. # Usage: dialog_message dialog_message() { zenity_run 'Status' --info --no-wrap --text="$1" } # Ask a yes/no question. # Usage: dialog_ask dialog_ask() { zenity_run 'Confirm' --question --no-wrap --ok-label=Yes --cancel-label=No --text="$1" } # Has the user quested to cancel the operation? # Usage: dialog_cancelled && print "cancelled" dialog_cancelled() { ! pipe_exists } # Set the status text. # Usage: dialog_set_text dialog_set_text() { pipe_write "#$1" } # Set if the progress bar should continuously animate instead of showing the value. # Usage: dialog_set_pulsate dialog_set_pulsate() { if [ $1 = 1 ] then pipe_write "pulsate:true" else pipe_write "pulsate:false" fi } # Set the current progress value. # Usage: dialog_set_value dialog_set_value() { pipe_write "$1" } # Select an entry in a list. # Usage dialog_select_entry