#!/bin/bash # This file is part of The RetroPie Project # # The RetroPie Project is the legal property of its developers, whose names are # too numerous to list here. Please refer to the COPYRIGHT.md file distributed with this source. # # See the LICENSE.md file at the top-level directory of this distribution and # at https://raw.githubusercontent.com/RetroPie/RetroPie-Setup/master/LICENSE.md # ## @file helpers.sh ## @brief RetroPie helpers library ## @copyright GPLv3 ## @fn printMsgs() ## @param type style of display to use - dialog, console or heading ## @param message string or array of messages to display ## @brief Prints messages in a variety of ways. function printMsgs() { local type="$1" shift if [[ "$__nodialog" == "1" && "$type" == "dialog" ]]; then type="console" fi for msg in "$@"; do [[ "$type" == "dialog" ]] && dialog --backtitle "$__backtitle" --cr-wrap --no-collapse --msgbox "$msg" 20 60 >/dev/tty [[ "$type" == "console" ]] && echo -e "$msg" [[ "$type" == "heading" ]] && echo -e "\n= = = = = = = = = = = = = = = = = = = = =\n$msg\n= = = = = = = = = = = = = = = = = = = = =\n" done return 0 } ## @fn printHeading() ## @param message string or array of messages to display ## @brief Calls PrintMsgs with "heading" type. function printHeading() { printMsgs "heading" "$@" } ## @fn fatalError() ## @param message string or array of messages to display ## @brief Calls PrintMsgs with "heading" type, and exits immediately. function fatalError() { printHeading "Error" echo -e "$1" joy2keyStop exit 1 } # @fn fnExists() # @param name name of function to check for # @brief Checks if function name exists. # @retval 0 if the function name exists # @retval 1 if the function name does not exist function fnExists() { declare -f "$1" > /dev/null return $? } function ask() { echo -e -n "$@" '[y/n] ' ; read ans case "$ans" in y*|Y*) return 0 ;; *) return 1 ;; esac } ## @fn runCmd() ## @param command command to run ## @brief Calls command and record any non zero return codes for later printing. ## @return whatever the command returns. function runCmd() { local ret "$@" ret=$? if [[ "$ret" -ne 0 ]]; then md_ret_errors+=("Error running '$*' - returned $ret") fi return $ret } ## @fn hasFlag() ## @param string string to search in ## @param flag flag to search for ## @brief Checks for a flag in a string (consisting of space separated flags). ## @retval 0 if the flag was found ## @retval 1 if the flag was not found function hasFlag() { local string="$1" local flag="$2" [[ -z "$string" || -z "$flag" ]] && return 1 if [[ "$string" =~ (^| )$flag($| ) ]]; then return 0 else return 1 fi } ## @fn isPlatform() ## @param platform ## @brief Test for current platform / platform flags. function isPlatform() { local flag="$1" if hasFlag "${__platform_flags[*]}" "$flag"; then return 0 fi return 1 } ## @fn addLineToFile() ## @param line line to add ## @param file file to add line to ## @brief Adds a new line of text to a file. function addLineToFile() { if [[ -f "$2" ]]; then cp -p "$2" "$2.bak" else sed -i --follow-symlinks '$a\' "$2" fi echo "$1" >> "$2" } ## @fn editFile() ## @param file file to edit ## @brief Opens an editing dialog for specified file. function editFile() { local file="$1" local cmd=(dialog --backtitle "$__backtitle" --editbox "$file" 22 76) local choice=$("${cmd[@]}" 2>&1 >/dev/tty) [[ -n "$choice" ]] && echo "$choice" >"$file" } ## @fn inputBox() ## @param title title of dialog ## @param text default text ## @param minchars minimum chars to accept ## @brief Opens an inputbox dialog and echoes resulting text. Uses the OSK if installed. ## @details The input dialog has OK/Cancel buttons and can be cancelled by the user. ## The dialog will enforce the minimum number of characters expected, re-prompting the user. ## @retval 0 when the user entered the text and chose the OK button ## @retval != 0 when the user chose the Cancel button function inputBox() { local title="$1" local text="$2" local minchars="$3" [[ -z "$minchars" ]] && minchars=0 local params=(--backtitle "$__backtitle" --inputbox "Enter the $title") local osk="$(rp_getInstallPath joy2key)/osk.py" if [[ -f "$osk" ]]; then params+=(--minchars "$minchars") text=$(python3 "$osk" "${params[@]}" "$text" 2>&1 >/dev/tty) || return $? else while true; do text=$(dialog "${params[@]}" 10 60 "$text" 2>&1 >/dev/tty) || return $? [[ "${#text}" -ge "$minchars" ]] && break dialog --msgbox "$title must have at least $minchars characters" 8 60 2>&1 >/dev/tty done fi echo "$text" } ## @fn hasPackage() ## @param package name of Debian package ## @param version requested version (optional) ## @param comparison type of comparison - defaults to `ge` (greater than or equal) if a version parameter is provided. ## @brief Test for an installed Debian package / package version. ## @retval 0 if the requested package / version was installed ## @retval 1 if the requested package / version was not installed function hasPackage() { local pkg="$1" local req_ver="$2" local comp="$3" [[ -z "$comp" ]] && comp="ge" local ver local status # extract the first line only (for cases where both amd64 & i386 versions of a package are installed) local out=$(dpkg-query -W --showformat='${Status} ${Version}\n' $1 2>/dev/null | head -n1) if [[ "$?" -eq 0 ]]; then ver="${out##* }" status="${out% *}" fi local installed=0 [[ "$status" == *"ok installed" ]] && installed=1 # if we are not checking version if [[ -z "$req_ver" ]]; then # if the package is installed return true [[ "$installed" -eq 1 ]] && return 0 else # if checking version and the package is not installed we need to clear "ver" as it may contain # the version number of a removed package and give a false positive with compareVersions. # we still need to do the version check even if not installed due to the varied boolean operators [[ "$installed" -eq 0 ]] && ver="" compareVersions "$ver" "$comp" "$req_ver" && return 0 fi return 1 } ## @fn aptUpdate() ## @brief Calls apt-get update (if it has not been called before). function aptUpdate() { if [[ "$__apt_update" != "1" ]]; then apt-get update --allow-releaseinfo-change __apt_update="1" fi } ## @fn aptInstall() ## @param packages package / space separated list of packages to install ## @brief Calls apt-get install with the packages provided. function aptInstall() { aptUpdate apt-get install -y "$@" return $? } ## @fn aptRemove() ## @param packages package / space separated list of packages to install ## @brief Calls apt-get remove with the packages provided. function aptRemove() { aptUpdate apt-get remove -y "$@" return $? } function _mapPackage() { local pkg="$1" case "$pkg" in libraspberrypi-bin) isPlatform "osmc" && pkg="rbp-userland-osmc" isPlatform "xbian" && pkg="xbian-package-firmware" ;; libraspberrypi-dev) isPlatform "osmc" && pkg="rbp-userland-dev-osmc" isPlatform "xbian" && pkg="xbian-package-firmware" ;; mali-fbdev) isPlatform "vero4k" && pkg="" ;; # handle our custom package alias LINUX-HEADERS LINUX-HEADERS) if isPlatform "rpi"; then if [[ "$__os_debian_ver" -lt 12 ]]; then pkg="raspberrypi-kernel-headers" else # on RaspiOS bookworm and later, kernel packages are separated by arch and model isPlatform "rpi0" || isPlatform "rpi1" && pkg="linux-headers-rpi-v6" if isPlatform "32bit"; then isPlatform "rpi2" || isPlatform "rpi3" && pkg="linux-headers-rpi-v7" isPlatform "rpi4" && pkg="linux-headers-rpi-v7l" else isPlatform "rpi3" || isPlatform "rpi4" && pkg="linux-headers-rpi-v8" isPlatform "rpi5" && pkg="linux-headers-rpi-2712" fi fi elif isPlatform "armbian"; then local branch="$(grep -oP "BRANCH=\K.*" /etc/armbian-release)" local family="$(grep -oP "LINUXFAMILY=\K.*" /etc/armbian-release)" pkg="linux-headers-${branch}-${family}" elif [[ -z "$__os_ubuntu_ver" ]]; then pkg="linux-headers-$(uname -r)" else pkg="linux-headers-generic" fi ;; # map libpng-dev to libpng12-dev for Jessie libpng-dev) [[ "$__os_debian_ver" -lt 9 ]] && pkg="libpng12-dev" ;; libsdl1.2-dev) rp_isEnabled "sdl1" && pkg="RP sdl1 $pkg" ;; libsdl2-dev) if rp_isEnabled "sdl2"; then # check whether to use our own sdl2 - can be disabled to resolve issues with # mixing custom 64bit sdl2 and os distributed i386 version on multiarch local own_sdl2=1 # default to off for x11 targets due to issues with dependencies with recent # Ubuntu (19.04). eg libavdevice58 requiring exactly 2.0.9 sdl2. isPlatform "x11" && own_sdl2=0 iniConfig " = " '"' "$configdir/all/retropie.cfg" iniGet "own_sdl2" if [[ "$ini_value" == "1" ]]; then own_sdl2=1 elif [[ "$ini_value" == "0" ]]; then own_sdl2=0 fi [[ "$own_sdl2" -eq 1 ]] && pkg="RP sdl2 $pkg" fi ;; libfreetype6-dev) [[ "$__os_debian_ver" -gt 10 ]] || compareVersions "$__os_ubuntu_ver" gt 23.04 && pkg="libfreetype-dev" ;; esac echo "$pkg" } ## @fn getDepends() ## @param packages package / space separated list of packages to install ## @brief Installs packages if they are not installed. ## @retval 0 on success ## @retval 1 on failure function getDepends() { local own_pkgs=() local apt_pkgs=() local all_pkgs=() local pkg for pkg in "$@"; do pkg=($(_mapPackage "$pkg")) # manage our custom packages (pkg = "RP module_id pkg_name") if [[ "${pkg[0]}" == "RP" ]]; then # if removing, check if any version is installed and queue for removal via the custom module if [[ "$md_mode" == "remove" ]]; then if hasPackage "${pkg[2]}"; then own_pkgs+=("${pkg[1]}") all_pkgs+=("${pkg[2]}(custom)") fi else # if installing check if our version is installed and queue for installing via the custom module if hasPackage "${pkg[2]}" $(get_pkg_ver_${pkg[1]}) "ne"; then own_pkgs+=("${pkg[1]}") all_pkgs+=("${pkg[2]}(custom)") fi fi continue fi if [[ "$md_mode" == "remove" ]]; then # add package to apt_pkgs for removal if installed if hasPackage "$pkg"; then apt_pkgs+=("$pkg") all_pkgs+=("$pkg") fi else # add package to apt_pkgs for installation if not installed if ! hasPackage "$pkg"; then apt_pkgs+=("$pkg") all_pkgs+=("$pkg") fi fi done # return if no packages required [[ ${#apt_pkgs[@]} -eq 0 && ${#own_pkgs[@]} -eq 0 ]] && return # if we are removing, then remove packages, do an autoremove to clean up additional packages and return if [[ "$md_mode" == "remove" ]]; then printMsgs "console" "Removing dependencies: ${all_pkgs[*]}" for pkg in ${own_pkgs[@]}; do rp_callModule "$pkg" remove done apt-get remove --purge -y "${apt_pkgs[@]}" apt-get autoremove --purge -y return 0 fi printMsgs "console" "Did not find needed dependencies: ${all_pkgs[*]}. Trying to install them now." # install any custom packages for pkg in ${own_pkgs[@]}; do rp_callModule "$pkg" _auto_ done aptInstall --no-install-recommends "${apt_pkgs[@]}" local failed=() # check the required packages again rather than return code of apt-get, # as apt-get might fail for other reasons (eg other half installed packages) for pkg in ${apt_pkgs[@]}; do if ! hasPackage "$pkg"; then # workaround for installing samba in a chroot (fails due to failed smbd service restart) # we replace the init.d script with an empty script so the install completes if [[ "$pkg" == "samba" && "$__chroot" -eq 1 ]]; then mv /etc/init.d/smbd /etc/init.d/smbd.old echo "#!/bin/sh" >/etc/init.d/smbd chmod u+x /etc/init.d/smbd apt-get -f install mv /etc/init.d/smbd.old /etc/init.d/smbd else failed+=("$pkg") fi fi done if [[ ${#failed[@]} -gt 0 ]]; then md_ret_errors+=("Could not install package(s): ${failed[*]}.") return 1 fi return 0 } ## @fn rpSwap() ## @param command *on* to add swap if needed and *off* to remove later ## @param memory total memory needed (swap added = memory needed - available memory) ## @brief Adds additional swap to the system if needed. function rpSwap() { local command=$1 local swapfile="$__swapdir/swap" case $command in on) rpSwap off local needed=$2 local size=$((needed - __memory_avail)) mkdir -p "$__swapdir/" if [[ $size -ge 0 ]]; then echo "Adding $size MB of additional swap" fallocate -l ${size}M "$swapfile" chmod 600 "$swapfile" mkswap "$swapfile" swapon "$swapfile" fi ;; off) echo "Removing additional swap" swapoff "$swapfile" 2>/dev/null rm -f "$swapfile" ;; esac } ## @fn gitPullOrClone() ## @param dest destination directory ## @param repo repository to clone or pull from ## @param branch branch to clone or pull from (optional) ## @param commit specific commit to checkout (optional - requires branch to be set) ## @param depth depth parameter for git. (optional) ## @brief Git clones or pulls a repository. ## @details depth parameter will default to 1 (shallow clone) so long as __persistent_repos isn't set. ## A depth parameter of 0 will do a full clone with all history. function gitPullOrClone() { local dir="$1" [[ -z "$dir" ]] && dir="$md_build" local repo="$2" local branch="$3" local commit="$4" local depth="$5" # if repo is blank then use the rp_module_repo info if [[ -z "$repo" && -n "$md_repo_url" ]]; then repo="$(rp_resolveRepoParam "$md_repo_url")" branch="$(rp_resolveRepoParam "$md_repo_branch")" commit="$(rp_resolveRepoParam "$md_repo_commit")" fi [[ -z "$repo" ]] && return 1 [[ -z "$branch" ]] && branch="master" # if no depth is provided default to shallow clone (depth 1) [[ -z "$depth" ]] && depth=1 # if we are using persistent repos or checking out a specific commit, don't shallow clone if [[ "$__persistent_repos" -eq 1 || -n "$commit" ]]; then depth=0 fi # record the source directory in __mod_info[ID/repo_dir] if not previously set which will be used # by the packaging functions later to grab repository information if [[ -z "${__mod_info[$md_id/repo_dir]}" ]]; then __mod_info[$md_id/repo_dir]="$dir" fi if [[ -d "$dir/.git" ]]; then pushd "$dir" > /dev/null # if we are using persistent repos, fetch the latest remote changes and clean the source so # any patches can be re-applied as needed. if [[ "$__persistent_repos" -eq 1 ]]; then runCmd git fetch runCmd git reset --hard runCmd git clean -f -d fi runCmd git checkout "$branch" # only try to pull if we are on a tracking branch if [[ -n "$(git config --get branch.$branch.merge)" ]]; then runCmd git pull --ff-only runCmd git submodule update --init --recursive fi popd > /dev/null else local git="git clone --recursive" if [[ "$depth" -gt 0 ]]; then git+=" --depth $depth --shallow-submodules" fi git+=" --branch $branch" printMsgs "console" "$git \"$repo\" \"$dir\"" runCmd $git "$repo" "$dir" fi if [[ -n "$commit" ]]; then printMsgs "console" "Winding back $repo->$branch to commit: #$commit" git -C "$dir" branch -D "$commit" &>/dev/null runCmd git -C "$dir" checkout -f "$commit" -b "$commit" fi branch=$(runCmd git -C "$dir" rev-parse --abbrev-ref HEAD) commit=$(runCmd git -C "$dir" rev-parse HEAD) printMsgs "console" "HEAD is now in branch '$branch' at commit '$commit'" } # @fn setupDirectories() # @brief Makes sure some required retropie directories and files are created. function setupDirectories() { mkdir -p "$rootdir" mkUserDir "$datadir" mkUserDir "$romdir" mkUserDir "$biosdir" mkUserDir "$configdir" mkUserDir "$configdir/all" # some home folders for configs that modules rely on mkUserDir "$home/.cache" mkUserDir "$home/.config" mkUserDir "$home/.local" mkUserDir "$home/.local/share" # make sure we have inifuncs.sh in place and that it is up to date mkdir -p "$rootdir/lib" local helper for helper in inifuncs.sh archivefuncs.sh; do if [[ ! -f "$rootdir/lib/$helper" || "$rootdir/lib/$helper" -ot "$scriptdir/scriptmodules/$helper" ]]; then cp --preserve=timestamps "$scriptdir/scriptmodules/$helper" "$rootdir/lib/$helper" fi done # create template for autoconf.cfg and make sure it is owned by $user local config="$configdir/all/autoconf.cfg" if [[ ! -f "$config" ]]; then echo "# this file can be used to enable/disable retropie autoconfiguration features" >"$config" fi chown $user:$user "$config" } ## @fn rmDirExists() ## @param dir directory to remove ## @brief Removes a directory and all contents if it exists. function rmDirExists() { if [[ -d "$1" ]]; then rm -rf "$1" fi } ## @fn mkUserDir() ## @param dir directory to create ## @brief Creates a directory owned by the current user. function mkUserDir() { mkdir -p "$1" chown $user:$user "$1" } ## @fn mkRomDir() ## @param dir rom directory to create ## @brief Creates a directory under $romdir owned by the current user. function mkRomDir() { mkUserDir "$romdir/$1" if [[ "$1" == "megadrive" ]]; then pushd "$romdir" ln -snf "$1" "genesis" popd fi } ## @fn moveConfigDir() ## @param from source directory ## @param to destination directory ## @brief Moves the contents of a folder and symlinks to the new location. function moveConfigDir() { local from="$1" local to="$2" # if we are in remove mode - remove the symlink if [[ "$md_mode" == "remove" ]]; then [[ -h "$from" ]] && rm -f "$from" return fi mkUserDir "$to" # move any old configs to the new location if [[ -d "$from" && ! -h "$from" ]]; then cp -a "$from/." "$to/" rm -rf "$from" fi ln -snf "$to" "$from" # set ownership of the actual link to $user chown -h $user:$user "$from" } ## @fn moveConfigFile() ## @param from source file ## @param to destination file ## @brief Moves the file and symlinks to the new location. function moveConfigFile() { local from="$1" local to="$2" # if we are in remove mode - remove the symlink if [[ "$md_mode" == "remove" && -h "$from" ]]; then rm -f "$from" return fi # move old file if [[ -f "$from" && ! -h "$from" ]]; then mv "$from" "$to" fi ln -sf "$to" "$from" # set ownership of the actual link to $user chown -h $user:$user "$from" } ## @fn diffFiles() ## @param file1 file to compare ## @param file2 file to compare ## @brief Compares two files using diff. ## @retval 0 if the files were the same ## @retval 1 if they were not ## @retval >1 an error occurred function diffFiles() { diff -q "$1" "$2" >/dev/null return $? } ## @fn compareVersions() ## @param version first version to compare ## @param operator operator to use (lt le eq ne ge gt) ## @brief version second version to compare ## @retval 0 if the comparison was true ## @retval 1 if the comparison was false function compareVersions() { dpkg --compare-versions "$1" "$2" "$3" >/dev/null return $? } ## @fn dirIsEmpty() ## @param path path to directory ## @param files_only set to 1 to ignore sub directories ## @retval 0 if the directory is empty ## @retval 1 if the directory is not empty function dirIsEmpty() { if [[ "$2" -eq 1 ]]; then [[ -z "$(ls -lA1 "$1" | grep "^-")" ]] && return 0 else [[ -z "$(ls -A "$1")" ]] && return 0 fi return 1 } ## @fn copyDefaultConfig() ## @param from source file ## @param to destination file ## @brief Copies a default configuration. ## @details Copies from the source file to the destination file if the destination ## file doesn't exist. If the destination is the same nothing is done. If different ## the source is copied to `$destination.rp-dist`. function copyDefaultConfig() { local from="$1" local to="$2" # if the destination exists, and is different then copy the config as name.rp-dist if [[ -f "$to" ]]; then if ! diffFiles "$from" "$to"; then to+=".rp-dist" printMsgs "console" "Copying new default configuration to $to" cp "$from" "$to" fi else printMsgs "console" "Copying default configuration to $to" cp "$from" "$to" fi chown $user:$user "$to" } ## @fn renameModule() ## @param from source file ## @param to destination file ## @brief Renames an existing module. ## @details Renames an existing module, moving it's install folder to the new location ## and changing any references to it in `emulators.cfg`. function renameModule() { local from="$1" local to="$2" # move from old location and update emulators.cfg if [[ -d "$rootdir/$md_type/$from" ]]; then rm -rf "$rootdir/$md_type/$to" mv "$rootdir/$md_type/$from" "$rootdir/$md_type/$to" # replace any default = "$from" sed -i --follow-symlinks "s/\"$from\"/\"$to\"/g" "$configdir"/*/emulators.cfg # replace any $from = "cmdline" sed -i --follow-symlinks "s/^$from\([ =]\)/$to\1/g" "$configdir"/*/emulators.cfg # replace any paths with /$from/ sed -i --follow-symlinks "s|/$from/|/$to/|g" "$configdir"/*/emulators.cfg fi } ## @fn addUdevInputRules() ## @brief Creates a udev rule to adjust input device permissions. ## @details Creates a udev rule in `/etc/udev/rules.d/99-input.rules` to ## make everything in `/dev/input` it writable by any user in group `input`. function addUdevInputRules() { if [[ ! -f /etc/udev/rules.d/99-input.rules ]]; then echo 'SUBSYSTEM=="input", GROUP="input", MODE="0660"' > /etc/udev/rules.d/99-input.rules fi # remove old 99-evdev.rules rm -f /etc/udev/rules.d/99-evdev.rules } ## @fn setBackend() ## @param emulator.cfg key to configure backend for ## @param backend name of the backend to set ## @param force set to 1 to force the change ## @brief Set a backend rendering driver for a module ## @details Set a backend rendering driver for a module - can be currently default, dispmanx or x11. ## This function will only set a backend if ## - It's not already configured, or ## - The 3rd parameter (force) is set to 1 ## The emulator.cfg key is usually the module_id but some modules add multiple emulator.cfg entries ## which are all handled separately. A module can use a _backend_set_MODULE function hook which is called ## from the backends module to handle calling setBackend for additional emulator.cfg entries. ## See "fuse" scriptmodule for an example. function setBackend() { local config="$configdir/all/backends.cfg" local id="$1" local mode="$2" local force="$3" iniConfig "=" "\"" "$config" iniGet "$id" if [[ "$force" -eq 1 || -z "$ini_value" ]]; then iniSet "$id" "$mode" chown $user:$user "$config" fi } ## @fn getBackend() ## @param emulator.cfg key to get backend for ## @brief Get a backend rendering driver for a module ## @details Get a backend rendering driver for a module ## The function echos the result so the value can be captured using var=$(getBackend "$module_id") function getBackend() { local config="$configdir/all/backends.cfg" local id="$1" iniConfig " = " '"' "$config" iniGet "$id" if [[ -n "$ini_value" ]]; then # translate old value of 1 as dispmanx for backward compatibility [[ "$ini_value" == "1" ]] && ini_value="dispmanx" else ini_value="default" fi echo "$ini_value" } ## @fn setDispmanx() ## @param module_id name of module to add dispmanx flag for ## @param status initial status of flag (0 or 1) ## @brief Sets a dispmanx flag for a module. This function is deprecated. ## @details Set a dispmanx flag for a module as to whether it should use the ## sdl1 dispmanx backend by default or not (0 for framebuffer, 1 for dispmanx). ## This function is deprecated and instead setBackend should be used. function setDispmanx() { isPlatform "dispmanx" || return setBackend "$1" "dispmanx" } ## @fn iniFileEditor() ## @param delim ini file delimiter eg. ' = ' ## @param quote ini file quoting character eg. '"' ## @param config ini file to edit ## @brief Allows editing of ini files with a user friendly dialog based gui. ## @details Some arrays need to be configured before calling this, which are ## used to display what can be edited and the options available. ## ## The first array is `$ini_titles` which provides the titles for each ## entry.. ## ## The second array is `$ini_descs` which contains a help description for each ## entry. ## ## The third array is `$ini_options` which contains multiple space separated ## strings in each element to control how each entry should be managed. ## ## The `$ini_options` array is constructed as follows: ## ## If the first string is `_function_` then the next string should be a function ## name that will handle that entry. The function will be called with a parameter ## `get` or `set`. The function should return the value for get via `echo` ## and should handle any gui functionality when called with `set`. This can be ## used for example to build custom dialogs. ## ## If the first option is anything else, it is assumed to be a key name, followed ## by a control type and a list of parameters. ## ## Control types are: ## * `_id_` map the following values to an id ## * `_string_` allow the value to be inputted by the user ## * `_file_` select from a list of files. The following values are wildcard, ## then file path. ## ## If none of the above, then the rest of the array element should be a list of ## possible values for the key. ## ## Some examples for ini_options: ## ## ini_options=('video_smooth true false') ## Allow setting of the key `video_smooth` with the values of *true* or *false* ## ## ini_options=('aspect_ratio_index _id_ 4:3 16:9 16:10) ## Allow setting of the key `aspect_ratio_index` with the values 0 1 or 2 which ## correspond to the ratios. The user is shown the ratios, but the ini configuration ## is set to the id (4:3 = 0, 16:9 = 1, 16:10 = 2). ## ## ini_options=('_function_ _video_fullscreen_configedit') ## The function `_video_fullscreen_configedit` is called with *get* or *set* ## to manage this entry. ## ## ini_options=("video_shader _file_ *.*p $rootdir/emulators/retroarch/shader") ## The key `video_shader` will be able to be set to a list of files in ## `$rootdir/emulators/retroarch/shader` that match the wildcard `*.*p` ## ## For more examples you can check out the code in supplementary/configedit.sh function iniFileEditor() { local delim="$1" local quote="$2" local config="$3" [[ ! -f "$config" ]] && return iniConfig "$delim" "$quote" "$config" local sel local value local option local title while true; do local options=() local params=() local values=() local keys=() local i=0 # generate menu from options for option in "${ini_options[@]}"; do # split into new array (globbing safe) read -ra option <<<"$option" key="${option[0]}" keys+=("$key") params+=("${option[*]:1}") # if the first parameter is _function_ we call the second parameter as a function # so we can handle some options with a custom menu etc if [[ "$key" == "_function_" ]]; then value="$(${option[1]} get)" else # get current value iniGet "$key" if [[ -n "$ini_value" ]]; then value="$ini_value" else value="unset" fi fi values+=("$value") # add the matching value to our id in _id_ lists if [[ "${option[1]}" == "_id_" && "$value" != "unset" ]]; then value+=" - ${option[value+2]}" fi # use custom title if provided if [[ -n "${ini_titles[i]}" ]]; then title="${ini_titles[i]}" else title="$key" fi options+=("$i" "$title ($value)" "${ini_descs[i]}") ((i++)) done local cmd=(dialog --backtitle "$__backtitle" --default-item "$sel" --item-help --help-button --menu "Please choose the setting to modify in $config" 22 76 16) sel=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) if [[ "${sel[@]:0:4}" == "HELP" ]]; then printMsgs "dialog" "${sel[@]:5}" continue fi [[ -z "$sel" ]] && break # if the key is _function_ we handle the option with a custom function if [[ "${keys[sel]}" == "_function_" ]]; then "${params[sel]}" set "${values[sel]}" continue fi # process the editing of the option i=0 options=("U" "unset") local default="" # split into new array (globbing safe) read -ra params <<<"${params[sel]}" local mode="${params[0]}" case "$mode" in _string_) options+=("E" "Edit (Currently ${values[sel]})") ;; _file_) local match="${params[1]}" local path="${params[*]:2}" local file while read file; do [[ "${values[sel]}" == "$file" ]] && default="$i" file="${file//$path\//}" options+=("$i" "$file") ((i++)) done < <(find -L "$path" -type f -name "$match" | sort) ;; _id_|*) [[ "$mode" == "_id_" ]] && params=("${params[@]:1}") for option in "${params[@]}"; do if [[ "$mode" == "_id_" ]]; then [[ "${values[sel]}" == "$i" ]] && default="$i" else [[ "${values[sel]}" == "$option" ]] && default="$i" fi options+=("$i" "$option") ((i++)) done ;; esac [[ -z "$default" ]] && default="U" # display values cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --menu "Please choose the value for ${keys[sel]}" 22 76 16) local choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) # if it is a _string_ type we will open an inputbox dialog to get a manual value if [[ -z "$choice" ]]; then continue elif [[ "$choice" == "E" ]]; then [[ "${values[sel]}" == "unset" ]] && values[sel]="" cmd=(dialog --backtitle "$__backtitle" --inputbox "Please enter the value for ${keys[sel]}" 10 60 "${values[sel]}") value=$("${cmd[@]}" 2>&1 >/dev/tty) elif [[ "$choice" == "U" ]]; then value="" else if [[ "$mode" == "_id_" ]]; then value="$choice" else # get the actual value from the options array local index=$((choice*2+3)) if [[ "$mode" == "_file_" ]]; then value="$path/${options[index]}" else value="${options[index]}" fi fi fi if [[ "$choice" == "U" ]]; then iniUnset "${keys[sel]}" "$value" else iniSet "${keys[sel]}" "$value" fi done } ## @fn setESSystem() ## @param fullname full name of system ## @param name short name of system ## @param path rom path ## @param extension file extensions to show ## @param command command to run ## @param platform name of platform (used by es for scraping) ## @param theme name of theme to use ## @brief Adds a system entry for Emulation Station (to /etc/emulationstation/es_systems.cfg). function setESSystem() { local function for function in $(compgen -A function _add_system_); do "$function" "$@" done } ## @fn ensureSystemretroconfig() ## @param system system to create retroarch.cfg for ## @brief Deprecated - use defaultRAConfig ## @details Creates a default retroarch.cfg for specified system in `$configdir/$system/retroarch.cfg`. function ensureSystemretroconfig() { # don't do any config work on module removal [[ "$md_mode" == "remove" ]] && return # reset "$md_conf_root" to "$configdir" as defaultRAConfig handles this whereas ensureSystemretroconfig # expects system to include any subdirectory in the first parameter such as "ports/$system". local save_conf_root="$md_conf_root" md_conf_root="$configdir" defaultRAConfig "$1" md_conf_root="$save_conf_root" } ## @fn defaultRAConfig() ## @param system system to create retroarch.cfg for ## @param ... optional key then value parameters to be used in the config ## @brief Creates a default retroarch.cfg for specified system in `$md_root_dir/$system/retroarch.cfg`. ## @details Additional default configuration values can be provided as parameters to the function - eg. "fps_show" "true" ## as two parameters would add a default entry of fps_show = "true" to the default configuration. ## This function uses $md_conf_root as a base, so there is no need to use "ports/$system" for libretro ports as with ## the older ensureSystemretroconfig function defaultRAConfig() { # don't do any config work on module removal [[ "$md_mode" == "remove" ]] && return local system="$1" shift local defaults=("$@") local config_path="$md_conf_root/$system" [[ ! -d "$config_path" ]] && mkUserDir "$config_path" local config="$(mktemp)" # add the initial comment regarding include order echo -e "# Settings made here will only override settings in the global retroarch.cfg if placed above the #include line\n" >"$config" # add the per system default settings iniConfig " = " '"' "$config" iniSet "input_remapping_directory" "$config_path" # add any additional config key / values from function parameters local key local value while read key value; do [[ -n "$key" ]] && iniSet "$key" "$value" done <<< "${defaults[@]}" # include the main retroarch config echo -e "\n#include \"$configdir/all/retroarch.cfg\"" >>"$config" copyDefaultConfig "$config" "$config_path/retroarch.cfg" rm "$config" } ## @fn setRetroArchCoreOption() ## @param option option to set ## @param value value to set ## @brief Sets a retroarch core option in `$configdir/all/retroarch-core-options.cfg`. function setRetroArchCoreOption() { local option="$1" local value="$2" iniConfig " = " "\"" "$configdir/all/retroarch-core-options.cfg" iniGet "$option" if [[ -z "$ini_value" ]]; then iniSet "$option" "$value" fi chown $user:$user "$configdir/all/retroarch-core-options.cfg" } ## @fn setConfigRoot() ## @param dir directory under $configdir to use ## @brief Sets module config root `$md_conf_root` to subfolder from `$configdir` ## @details This is used for ports that are not actually in scriptmodules/ports ## as they would get the wrong config root otherwise. function setConfigRoot() { local dir="$1" md_conf_root="$configdir" [[ -n "$dir" ]] && md_conf_root+="/$dir" mkUserDir "$md_conf_root" } ## @fn loadModuleConfig() ## @param params space separated list of key=value parameters ## @brief Load the settings for a module. ## @details This allows modules to quickly load some settings from an ini file. ## It can provide a shortcut way to load a set of keys from an ini file into ## variables. ## ## It requires iniConfig to be called first to specify the format and file. ## eg. ## ## iniConfig " = " '"' "$configdir/all/mymodule.cfg" ## eval $(loadModuleConfig \ ## 'some_option=1' \ ## 'another_option=2' ## ## This would load the keys `some_option` and `another_option` into local ## variables `some_option` and `another_option`. If the keys did not exist ## in mymodule.cfg the variables would be initialised to 1 and 2. function loadModuleConfig() { local options=("$@") local option local key local value for option in "${options[@]}"; do option=(${option/=/ }) key="${option[0]}" value="${option[@]:1}" iniGet "$key" if [[ -z "$ini_value" ]]; then iniSet "$key" "$value" echo "local $key=\"$value\"" else echo "local $key=\"$ini_value\"" fi done } ## @fn applyPatch() ## @param patch filename of patch to apply ## @brief Apply a patch if it has not already been applied to current folder. ## @details This is used for applying patches against upstream code. ## @retval 0 on success ## @retval 1 on failure function applyPatch() { local patch="$1" local patch_applied="${patch##*/}.applied" if [[ ! -f "$patch_applied" ]]; then if patch -f -p1 <"$patch"; then touch "$patch_applied" printMsgs "console" "Successfully applied patch: $patch" else md_ret_errors+=("$md_id patch $patch failed to apply") return 1 fi fi return 0 } ## @fn runCurl() ## @params ... commandline arguments to pass to curl ## @brief Run curl with chosen parameters and handle curl errors ## @details Runs curl with the provided parameters, whilst also capturing the output and extracting ## any error message, which is stored in the global variable __NET_ERRMSG. Function returns the return ## code provided by curl. The environment variable __curl_opts can be set to override default curl ## parameters, eg - timeouts etc. ## @retval curl return value function runCurl() { local params=("$@") # add any user supplied curl opts - timeouts can be overridden as curl uses the last parameters given [[ -n "$__curl_opts" ]] && params+=($__curl_opts) local cmd_err local ret # get the last non zero exit status (ignoring tee) set -o pipefail # set up additional file descriptor for stdin exec 3>&1 # capture stderr - while passing both stdout and stderr to terminal # curl like wget outputs the progress meter to stderr, so we will extract the error line later cmd_err=$(curl "${params[@]}" 2>&1 1>&3 | tee /dev/stderr) ret="$?" # remove stdin copy exec 3>&- set +o pipefail # if there was an error, extract it and put in __NET_ERRMSG if [[ "$ret" -ne 0 ]]; then # as we also capture the curl progress output, extract the last line which contains the error __NET_ERRMSG="${cmd_err##*$'\n'}" else __NET_ERRMSG="" fi return "$ret" } ## @fn download() ## @param url url of file ## @param dest destination name (optional), use - for stdout ## @brief Download a file ## @details Download a file - if the dest parameter is omitted, the file will be downloaded to the current directory. ## If the destination name is a hyphen (-), then the file will be outputted to stdout, for piping to another command ## or retrieving the contents directly to a variable. If the destination is a folder, extract with the basename from ## the url to the destination folder. ## @retval 0 on success function download() { local url="$1" local dest="$2" local file="${url##*/}" # if no destination, get the basename from the url [[ -z "$dest" ]] && dest="${PWD}/$file" # if the destination is a folder, download to that with filename from url [[ -d "$dest" ]] && dest="$dest/$file" local params=(--location) if [[ "$dest" == "-" ]]; then params+=(--silent --no-buffer) else printMsgs "console" "Downloading $url to $dest ..." params+=(-o "$dest") fi params+=(--connect-timeout 10 --speed-limit 1 --speed-time 60 --fail) # add the url params+=("$url") local ret runCurl "${params[@]}" ret="$?" # if download failed, remove file, log error and return error code if [[ "$ret" -ne 0 ]]; then # remove dest if not set to stdout and exists [[ "$dest" != "-" && -f "$dest" ]] && rm "$dest" md_ret_errors+=("URL $url failed to download.\n\n$__NET_ERRMSG") fi return "$ret" } ## @fn downloadAndVerify() ## @param url url of file ## @param dest destination file (optional) ## @brief Download a file and a corresponding .asc signature and verify the contents ## @details Download a file and a corresponding .asc signature and verify the contents. ## The .asc file will be downloaded to verify the file, but will be removed after downloading. ## If the dest parameter is omitted, the file will be downloaded to the current directory ## @retval 0 on success function downloadAndVerify() { local url="$1" local dest="$2" local file="${url##*/}" # if no destination, get the basename from the url (supported by GNU basename) [[ -z "$dest" ]] && dest="${PWD}/$file" local cmd_out local ret=1 if download "${url}.asc" "${dest}.asc"; then if download "$url" "$dest"; then cmd_out="$(gpg --verify "${dest}.asc" 2>&1)" ret="$?" if [[ "$ret" -ne 0 ]]; then md_ret_errors+=("$dest failed signature check:\n\n$cmd_out") fi fi fi return "$ret" } ## @fn downloadAndExtract() ## @param url url of archive ## @param dest destination folder for the archive ## @param optional additional parameters to pass to the decompression tool. ## @brief Download and extract an archive ## @details Download and extract an archive. ## @retval 0 on success function downloadAndExtract() { local url="$1" local dest="$2" shift 2 local opts=("$@") local ext="${url##*.}" local file="${url##*/}" local temp="$(mktemp -d)" # download file, removing temporary folder and returning on error if ! download "$url" "$temp/$file"; then rm -rf "$temp" return 1 fi mkdir -p "$dest" local ret case "$ext" in exe|zip) runCmd unzip "${opts[@]}" -o "$temp/$file" -d "$dest" ;; *) tar -xvf "$temp/$file" -C "$dest" "${opts[@]}" ;; esac ret=$? rm -rf "$temp" return $ret } ## @fn ensureFBMode() ## @param res_x width of mode ## @param res_y height of mode ## @brief Add a framebuffer mode to /etc/fb.modes ## @details Useful for adding specific resolutions used by emulators so SDL1 can ## use them and utilise the RPI hardware scaling. Without for example a 320x240 ## mode in fb.modes many of the emulators that output to the framebuffer and ## were not set to use the dispmanx SDL1 backend would just show in a small ## area of the screen. function ensureFBMode() { [[ ! -f /etc/fb.modes ]] && return local res_x="$1" local res_y="$2" local res="${res_x}x${res_y}" sed -i --follow-symlinks "/$res mode/,/endmode/d" /etc/fb.modes cat >> /etc/fb.modes <<_EOF_ # added by RetroPie-Setup - $res mode for emulators mode "$res" geometry $res_x $res_y $res_x $res_y 16 timings 0 0 0 0 0 0 0 endmode _EOF_ } ## @fn joy2keyStart() ## @param left mapping for left ## @param right mapping for right ## @param up mapping for up ## @param down mapping for down ## @param but1 mapping for button 1 ## @param but2 mapping for button 2 ## @param but3 mapping for button 3 ## @param butX mapping for button X ... ## @brief Start joy2key process in background to map joystick presses to keyboard ## @details Arguments are curses capability names or hex values starting with '0x' ## see: http://pubs.opengroup.org/onlinepubs/7908799/xcurses/terminfo.html function joy2keyStart() { # don't start on SSH sessions # (check for bracket in output - ip/name in brackets over a SSH connection) [[ "$(who -m)" == *\(* ]] && return local params=("$@") # if joy2key is installed, run it if rp_isInstalled "joy2key"; then "$(rp_getInstallPath joy2key)/joy2key" start "${params[@]}" 2>/dev/null && return 0 fi return 1 } ## @fn joy2keyStop() ## @brief Stop previously started joy2key process. function joy2keyStop() { # if joy2key is installed, stop it if rp_isInstalled "joy2key"; then "$(rp_getInstallPath joy2key)/joy2key" stop fi } ## @fn getPlatformConfig() ## @param key key to look up ## @brief gets a config from a platforms.cfg ini ## @details gets a config from a platforms.cfg ini first looking in ## `$configdir/all/platforms.cfg` then `$scriptdir/platforms.cfg` ## allowing users to override any parts of `$scriptdir/platforms.cfg` function getPlatformConfig() { local key="$1" local conf for conf in "$configdir/all/platforms.cfg" "$scriptdir/platforms.cfg"; do [[ ! -f "$conf" ]] && continue iniConfig "=" '"' "$conf" iniGet "$key" [[ -n "$ini_value" ]] && break done # workaround for RetroPie platform [[ "$key" == "retropie_fullname" ]] && ini_value="RetroPie" echo "$ini_value" } ## @fn addSystem() ## @param system system to add ## @brief adds an emulator entry / system ## @param fullname optional fullname for the frontend (if not present in platforms.cfg) ## @param exts optional extensions for the frontend (if not present in platforms.cfg) ## @details Adds a system to one of the frontend launchers function addSystem() { local system="$1" local fullname="$2" local exts=($3) local platform="$system" local theme="$system" local cmd local path # if removing and we don't have an emulators.cfg we can remove the system from the frontends if [[ "$md_mode" == "remove" ]] && [[ ! -f "$md_conf_root/$system/emulators.cfg" ]]; then delSystem "$system" "$fullname" return fi # set system / platform / theme for configuration based on data in names field if [[ "$system" == "ports" ]]; then cmd="bash %ROM%" path="$romdir/ports" else cmd="$rootdir/supplementary/runcommand/runcommand.sh 0 _SYS_ $system %ROM%" path="$romdir/$system" fi exts+=("$(getPlatformConfig "${system}_exts")") local temp temp="$(getPlatformConfig "${system}_theme")" if [[ -n "$temp" ]]; then theme="$temp" else theme="$system" fi temp="$(getPlatformConfig "${system}_platform")" if [[ -n "$temp" ]]; then platform="$temp" else platform="$system" fi temp="$(getPlatformConfig "${system}_fullname")" [[ -n "$temp" ]] && fullname="$temp" exts="${exts[*]}" # add the extensions again as uppercase exts+=" ${exts^^}" setESSystem "$fullname" "$system" "$path" "$exts" "$cmd" "$platform" "$theme" } ## @fn delSystem() ## @param system system to delete ## @brief Deletes a system ## @details deletes a system from all frontends. function delSystem() { local system="$1" local fullname="$2" local temp temp="$(getPlatformConfig "${system}_fullname")" [[ -n "$temp" ]] && fullname="$temp" local function for function in $(compgen -A function _del_system_); do "$function" "$fullname" "$system" done } ## @fn addPort() ## @param id id of the module / command ## @param port name of the port ## @param name display name for the launch script ## @param cmd commandline to launch ## @param game rom/game parameter (optional) ## @brief Adds a port to the emulationstation ports menu. ## @details Adds an emulators.cfg entry as with addSystem but also creates a launch script in `$datadir/ports/$name.sh`. ## ## Can also optionally take a game parameter which can be used to create multiple launch ## scripts for different games using the same engine - eg for quake ## ## addPort "lr-tyrquake" "quake" "Quake" "$emudir/retroarch/bin/retroarch -L $md_inst/tyrquake_libretro.so --config $md_conf_root/quake/retroarch.cfg %ROM%" "$romdir/ports/quake/id1/pak0.pak" ## addPort "lr-tyrquake" "quake" "Quake Mission Pack 2 (rogue)" "$emudir/retroarch/bin/retroarch -L $md_inst/tyrquake_libretro.so --config $md_conf_root/quake/retroarch.cfg %ROM%" "$romdir/ports/quake/id1/rogue/pak0.pak" ## ## Would add an entry in $configdir/ports/quake/emulators.cfg for lr-tyrquake (setting it to default if no default set) ## and create a launch script in $romdir/ports for each game. function addPort() { local id="$1" local port="$2" local file="$romdir/ports/$3.sh" local cmd="$4" local game="$5" # move configurations from old ports location if [[ -d "$configdir/$port" ]]; then mv "$configdir/$port" "$md_conf_root/" fi # remove the emulator / port if [[ "$md_mode" == "remove" ]]; then delEmulator "$id" "$port" # remove launch script if in remove mode and the ports emulators.cfg is empty [[ ! -f "$md_conf_root/$port/emulators.cfg" ]] && rm -f "$file" # if there are no more port launch scripts we can remove ports from emulation station if [[ "$(find "$romdir/ports" -maxdepth 1 -name "*.sh" | wc -l)" -eq 0 ]]; then delSystem "ports" fi return fi mkUserDir "$romdir/ports" cat >"$file" << _EOF_ #!/bin/bash "$rootdir/supplementary/runcommand/runcommand.sh" 0 _PORT_ "$port" "$game" _EOF_ chown $user:$user "$file" chmod +x "$file" [[ -n "$cmd" ]] && addEmulator 1 "$id" "$port" "$cmd" addSystem "ports" } ## @fn addEmulator() ## @param default 1 to make the emulator / command default for the system if no default already set ## @param id unique id of the module / command ## @param name name of the system to add the emulator to ## @param cmd commandline to launch ## @brief Adds a new emulator for a system. ## @details This is the primary function for adding emulators to a system which can be ## switched between via the runcommand launch menu ## ## addEmulator 1 "vice-x64" "c64" "$md_inst/bin/x64 %ROM%" ## addEmulator 0 "vice-xvic" "c64" "$md_inst/bin/xvic %ROM%" ## ## Would add two optional emulators for the c64 - with vice-x64 being the default if no default ## was already set. This adds entries to `$configdir/$system/emulators.cfg` with ## ## id = "cmd" ## default = id ## ## Which are then selectable from runcommand when launching roms ## ## For libretro emulators, cmd needs to only contain the path to the libretro library. ## ## eg. for the lr-fcuemm module ## ## addEmulator 1 "$md_id" "nes" "$md_inst/fceumm_libretro.so" function addEmulator() { local default="$1" local id="$2" local system="$3" local cmd="$4" # check if we are removing the system if [[ "$md_mode" == "remove" ]]; then delEmulator "$id" "$system" return fi # automatically add parameters for libretro modules if [[ "$id" == lr-* && "$cmd" =~ ^"$md_inst"[^[:space:]]*\.so ]]; then cmd="$emudir/retroarch/bin/retroarch -L $cmd --config $md_conf_root/$system/retroarch.cfg %ROM%" fi # create a config folder for the system / port mkUserDir "$md_conf_root/$system" # add the emulator to the $conf_dir/emulators.cfg if a commandline exists (not used for some ports) if [[ -n "$cmd" ]]; then iniConfig " = " '"' "$md_conf_root/$system/emulators.cfg" iniSet "$id" "$cmd" # set a default unless there is one already set iniGet "default" if [[ -z "$ini_value" && "$default" -eq 1 ]]; then iniSet "default" "$id" fi chown $user:$user "$md_conf_root/$system/emulators.cfg" fi } ## @fn delEmulator() ## @param id id of emulator to delete ## @param system system to delete from ## @brief Deletes an emulator entry / system ## @details Delete the entry for the id from `$configdir/$system/emulators.cfg`. ## If there are no more emulators for the system present, it will also ## delete the system entry from the installed frontends. function delEmulator() { local id="$1" local system="$2" local config="$md_conf_root/$system/emulators.cfg" # remove from apps list for system if [[ -f "$config" && -n "$id" ]]; then # delete emulator entry iniConfig " = " '"' "$config" iniDel "$id" # if it is the default - remove it - runcommand will prompt to select a new default iniGet "default" [[ "$ini_value" == "$id" ]] && iniDel "default" # if we no longer have any entries in the emulators.cfg file we can remove it grep -q "=" "$config" || rm -f "$config" fi } ## @fn patchVendorGraphics() ## @param filename file to patch ## @details replace declared dependencies of old vendor graphics libraries with new names ## Temporary compatibility workaround for legacy software to work on new Raspberry Pi firmwares. function patchVendorGraphics() { local filename="$1" # patchelf is not available on Raspbian Jessie [[ "$__os_debian_ver" -lt 9 ]] && return getDepends patchelf printMsgs "console" "Applying vendor graphics patch: $filename" patchelf --replace-needed libEGL.so libbrcmEGL.so \ --replace-needed libGLES_CM.so libbrcmGLESv2.so \ --replace-needed libGLESv1_CM.so libbrcmGLESv2.so \ --replace-needed libGLESv2.so libbrcmGLESv2.so \ --replace-needed libOpenVG.so libbrcmOpenVG.so \ --replace-needed libWFC.so libbrcmWFC.so "$filename" } ## @fn dkmsManager() ## @param mode dkms operation type ## @module_name name of dkms module ## @module_ver version of dkms module ## Helper function to manage DKMS modules installed by RetroPie function dkmsManager() { local mode="$1" local module_name="$2" local module_ver="$3" local kernel="$(uname -r)" local ver case "$mode" in install) if dkms status | grep -q "^$module_name"; then dkmsManager remove "$module_name" "$module_ver" fi ln -sf "$md_inst" "/usr/src/${module_name}-${module_ver}" dkms install --no-initrd --force -m "$module_name" -v "$module_ver" -k "$kernel" if ! dkms status "$module_name/$module_ver" -k "$kernel" | grep -q installed; then # Force building for any kernel that has source/headers local k_ver while read k_ver; do if [[ -d "$(realpath /lib/modules/$k_ver/build)" ]]; then dkms install --no-initrd --force -m "$module_name/$module_ver" -k "$k_ver" fi done < <(ls -r1 /lib/modules) fi if ! dkms status "$module_name/$module_ver" | grep -q installed; then md_ret_errors+=("Failed to install $md_id") return 1 fi ;; remove) for ver in $(dkms status "$module_name" | cut -d"," -f2 | cut -d":" -f1); do dkms remove -m "$module_name" -v "$ver" --all rm -f "/usr/src/${module_name}-${ver}" done dkmsManager unload "$module_name" "$module_ver" ;; reload) dkmsManager unload "$module_name" "$module_ver" # No reason to load modules in chroot if [[ "$__chroot" -eq 0 ]]; then modprobe "$module_name" fi ;; unload) if [[ -n "$(lsmod | grep ${module_name/-/_})" ]]; then rmmod "$module_name" fi ;; esac } ## @fn getIPAddress() ## @param dev optional specific network device to use for address lookup ## @brief Obtains the current externally routable source IP address of the machine ## @details This function first tries to obtain an external IPv4 route and ## otherwise tries an IPv6 route if the IPv4 route can not be determined. ## If no external route can be determined, nothing will be returned. ## This function uses Google's DNS servers as the external lookup address. function getIPAddress() { local dev="$1" local ip_route # first try to obtain an external IPv4 route ip_route=$(ip -4 route get 8.8.8.8 ${dev:+dev $dev} 2>/dev/null) if [[ -z "$ip_route" ]]; then # if there is no IPv4 route, try to obtain an IPv6 route instead ip_route=$(ip -6 route get 2001:4860:4860::8888 ${dev:+dev $dev} 2>/dev/null) fi # if an external route was found, report its source address [[ -n "$ip_route" ]] && grep -oP "src \K[^\s]+" <<< "$ip_route" } ## @fn isConnected() ## @brief Simple check to see if there is a connection to the Internet. ## @details Uses the getIPAddress function to check if we have a route to the Internet. Also sets ## __NET_ERRMSG with an error message for use in packages / setup to display to the user if not. ## @retval 0 on success ## @retval 1 on failure function isConnected() { local ip="$(getIPAddress)" if [[ -z "$ip" ]]; then __NET_ERRMSG="Not connected to the Internet" return 1 fi return 0 } ## @fn adminRsync() ## @param src src folder on local system - eg "$__tmpdir/stats/" ## @param dest destination folder on remote system - eg "stats/" ## @param params additional rsync parameters - eg --delete ## @brief Rsyncs data to remote host for admin modules ## @details Used to rsync data to our server for admin modules. Default remote ## user is retropie, host is $__binary_host and default port is 22. These can be overridden with ## env vars __upload_user __upload_host and __upload_port ## ## The default parameters for rsync are "-av --delay-updates" - more can be added via the 3rd+ argument function adminRsync() { local src="$1" local dest="$2" shift 2 local params=("$@") local remote_user="$__upload_user" [[ -z "$remote_user" ]] && remote_user="retropie" local remote_host="$__upload_host" [[ -z "$remote_host" ]] && remote_host="$__binary_host" local remote_port="$__upload_port" [[ -z "$remote_port" ]] && remote_port=22 rsync -av --delay-updates -e "ssh -p $remote_port" "${params[@]}" "$src" "$remote_user@$remote_host:$dest" } ## @fn signFile() ## @param file path to file to sign ## @brief signs file with $__gpg_signing_key ## @details signs file with $__gpg_signing_key creating corresponding .asc file in the same folder function signFile() { local file="$1" local cmd_out cmd_out=$(gpg --default-key "$__gpg_signing_key" --detach-sign --armor --yes "$1" 2>&1) if [[ "$?" -ne 0 ]]; then md_ret_errors+=("Failed to sign $1\n\n$cmd_out") return 1 fi return 0 }