#!/usr/bin/env bash # # pkgix: Prefix environment and simple package management tool. # # Copyright (C) 2012-2016, Marco Elver # # This program 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. # # This program 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 this program. If not, see . # set -o errtrace set -o errexit set -o nounset shopt -s extglob #============================================================================ # Constants # PROG_VERSION="0.6.0" PROGNAME="pkgix" DB_PREFIX="var/lib/${PROGNAME}/local" LOG_FILE="var/log/${PROGNAME}.log" LOCK_FILE="var/lock/${PROGNAME}.lock" REMOTE_AVAIL_FILE=".avail" URL_SEP=";" METAPKG_DUMMY="_dummy" REASON_EXPLICIT="explicit" REASON_DEPENDENCY="dependency" readonly PROG_VERSION PROGNAME DB_PREFIX LOG_FILE URL_SEP METAPKG_DUMMY readonly REASON_EXPLICIT REASON_DEPENDENCY #============================================================================ # Exit codes # EXIT_OK=0 EXIT_ERR=1 EXIT_INVALID_OPTION=2 EXIT_USER_ABORT=3 readonly EXIT_OK EXIT_ERR EXIT_INVALID_OPTION EXIT_USER_ABORT #============================================================================ # Function return codes # RET_OK=0 RET_ERR=1 RET_INVALID_OPTION=2 RET_USER_ABORT=3 readonly RET_OK RET_ERR RET_INVALID_OPTION RET_USER_ABORT #============================================================================ # abspath path # Get absolute path; normalizes the path if dir or file exists. # # Declared here for use in defining default values for configurable variables, # or can even be used in the RC-file. # # *IMPORTANT* General note on paths: If a variable is used to construct a path, # usually by chaining `${a}/${b}/..`, if a slash is added after a variable, # then a trailing slash should be removed from the variable; this is so we do # not get paths with double-slashes if the user did not ask for them, as POSIX # defines double slashes at the root of a path to be interpreted implementation # specific -- do NOT break this! # If a variable could potentially contain a trailing slash, construct paths # like so: ${a%/}/${b%/}/.. # # The only known case where this is usually a problem, is if pkgix_prefix is '/'. # The rationale behind removing the trailing '/' only when constructing a path is # so that we do not set pkgix_prefix to the null string! # abspath() { #{{{ local path="$1" # If empty, do not do anything if [[ -n "$path" ]]; then local _path # Remove trailing slashes; only remove trailing slashes, and do not # turn '//' into '/', as according to POSIX, '//' could be interpreted # in an implementation specific way. _path="${path%%+(/)}" [[ -n "$_path" ]] && path="$_path" || : if [[ -d "$path" ]]; then path="$( cd -- "$path" && pwd )" elif [[ -f "$path" ]]; then if [[ "${path}" =~ "/" ]]; then path="$( cd -- "${path%/*}" && pwd )/${path##*/}" else path="$(pwd)/${path}" fi else # Doesn't seem to exist, try best guess if [[ "${path:0:1}" == "/" ]]; then path="${path}" else path="$(pwd)/${path}" fi fi fi echo "$path" } #}}} #============================================================================ # Configurable variables, settable via environment or RC-file # ## Variables with a default value (see below): # # PKGIXRC: RC-file, which is sourced and can be used to set configurable # variables. # # PKGIX_REPOS: List of repository URLs; either a URL_SEP separated string of # URLs or an array. Note that additional URLs specified with `-r` are prepended # if this is a string, and appended if it's an array. # # PKGIX_SHELL: Which shell should be invoked for chenv and is also used to # determine the showenv format. # # PKGIX_FETCH: External program to download files. # # PKGIX_COLOR: Set to 1 (default) for colored output, 0 for color-less output. # # PKGIX_UPGRADE_IGNORE: An array of package names, which should be ignored on # upgrade. Can only be set via the RC-file. # ## Variables without a default value: # # PKGIX_PREFIX: If set, PKGIX_PREFIX is being used as the default prefix (no need to # specify for each command). # # PKGIX_BUILD_DIR: If set, uses ${PKGIX_BUILD_DIR}/{pkgix-build,pkgix-install} # to build packages in, instead of the default of # /{.pkgix-build,.pkgix-install}. # : ${PKGIXRC:="${HOME}/.${PROGNAME}rc"} ## # Set variables which can only be set from the RC-file here, but need to be set # even if the RC-file does not set them: PKGIX_UPGRADE_IGNORE=() ## # Set configurable variables from RC-file first, so we do not unnecessarily # set some of these twice. [[ -r "$PKGIXRC" ]] && source "$PKGIXRC" || : ## # Set variable defaults which have not been defined by either environment or # RC-file: : ${PKGIX_SHELL:="$SHELL"} : ${PKGIX_FETCH:="wget -O %s"} : ${PKGIX_COLOR:=1} # PKGIX_REPOS must be declared; check with declare -p, instead of ${..:-}, # so that an empty array will not be assigned anything else (in this case the # first element would be set to the default). if ! declare -p PKGIX_REPOS &> /dev/null; then [[ -d "${HOME}/${PROGNAME}-repo/pkgs" ]] && PKGIX_REPOS="${HOME}/${PROGNAME}-repo/pkgs" || PKGIX_REPOS="" fi #============================================================================ # Setup gettext ($"...") # Need to set TEXTDOMAIN first, then TEXTDOMAINDIR -- see BASH 4.2 source # TEXTDOMAIN="$PROGNAME" TEXTDOMAINDIR="$(abspath "${0%/*}/../share/locale")" #============================================================================ # Color config # # Check if color is enabled and we have stderr/in on terminal if [[ "$PKGIX_COLOR" != "0" && -t 2 && -t 1 ]]; then # {{{ # Check if we have tput if type -p tput > /dev/null && tput -V 2>&1 | grep -q "ncurses"; then Crst="$(tput sgr0)" Cred="$(tput setaf 1)" Cgrn="$(tput setaf 2)" Cylw="$(tput setaf 3)" Cblu="$(tput setaf 4)" Cpur="$(tput setaf 5)" Ccyn="$(tput setaf 6)" _tput_bold="$(tput bold)" CredB="${_tput_bold}${Cred}" CgrnB="${_tput_bold}${Cgrn}" CylwB="${_tput_bold}${Cylw}" CbluB="${_tput_bold}${Cblu}" CpurB="${_tput_bold}${Cpur}" CcynB="${_tput_bold}${Ccyn}" unset _tput_bold else # Fallback Crst='\e[0m' Cred='\e[0;31m' Cgrn='\e[0;32m' Cylw='\e[0;33m' Cblu='\e[0;34m' Cpur='\e[0;35m' Ccyn='\e[0;36m' CredB='\e[1;31m' CgrnB='\e[1;32m' CylwB='\e[1;33m' CbluB='\e[1;34m' CpurB='\e[1;35m' CcynB='\e[1;36m' fi else Crst= Cred= Cgrn= Cylw= Cblu= Cpur= Ccyn= CredB= CgrnB= CylwB= CbluB= CpurB= CcynB= fi # }}} #============================================================================ # printf wrappers # msg_printf() { #{{{ printf "${Cpur}[${PROGNAME}]${Crst} ""$@" 1>&2 } #}}} verbose_printf() { #{{{ (( arg_verbose )) && msg_printf "$@" || : } #}}} error_printf() { #{{{ msg_printf "${Cred}"$"ERROR:""${Crst} ""$@" } #}}} warning_printf() { #{{{ msg_printf "${Cylw}"$"WARNING:""${Crst} ""$@" } #}}} #============================================================================ # die [printf_args..] # Print message with error_printf then exit; if no message is specified, show a # default error message. # die() { #{{{ if (( $# > 0 )); then error_printf "$@" else error_printf $"An error occurred @ %s\n" "$(caller 0)" fi exit $EXIT_ERR } #}}} #============================================================================ # Sanitize environment variables (done here, as *_printf may be needed) # if [[ -n "${PKGIX_BUILD_DIR:-}" ]]; then if [[ ! -d "$PKGIX_BUILD_DIR" ]]; then die $"PKGIX_BUILD_DIR='%s' is not a valid directory!\n" "$PKGIX_BUILD_DIR" fi PKGIX_BUILD_DIR="$(abspath "$PKGIX_BUILD_DIR")" fi #============================================================================ # log_printf pkgix_prefix args... # log_printf() { #{{{ local log_file="${1%/}/${LOG_FILE}" shift [[ ! -d "${log_file%/*}" ]] && mkdir -p "${log_file%/*}" || : printf "[$(date)] ""$@" >> "$log_file" } #}}} #============================================================================ # in_list needle hay... # in_list() { #{{{ local needle="$1" shift local elem for elem in "$@"; do [[ "$elem" == "$needle" ]] && return $RET_OK || : done return $RET_ERR } #}}} #============================================================================ # resolv_dir_symlink # Resolves directory symlinks only; we can use the bash builtin pwd with the # -P option for this, instead of using external tools such as readlink or # realpath. # resolv_dir_symlink() { #{{{ ( cd -- "$1" && pwd -P ) } #}}} #============================================================================ # OS dependent functions, detect platform and set variables accordingly # ## pkgix tries to find defaults, but can be set from environment: # # CARCH: processor architecture, as the OS identified it # CHOST: --- # ## Always set by pkgix: # # ostype: consistend value representing the OS, regardless of variation # architecture: consistent identifier of processor arch, regardless of OS # # {{{ #---------------------------------------------------------------------------- # digest type [file_name] # If file_name is "-" or not provided, read from stdin. # if type -p openssl > /dev/null; then digest() { local result case "$1" in md5|sha1|sha224|sha256|sha384|sha512|whirlpool) if [[ "${2:--}" == "-" ]]; then result="$(openssl dgst -${1})" else result="$(openssl dgst -${1} "$2")" fi echo "${result##* }" ;; *) die $"Digest '%s' not supported!\n" "$1" ;; esac } else if type -p sha256sum > /dev/null; then digest() { [[ "$1" != "sha256" ]] && die || : local result="$(sha256sum "${2:--}")" echo "${result%% *}" } elif type -p sha256 > /dev/null; then digest() { [[ "$1" != "sha256" ]] && die || : local result if [[ "${2:--}" == "-" ]]; then result="$(sha256)" else result="$(sha256 < "$2")" fi echo "${result%% *}" } else die $"No suitable SHA-256 binary nor '%s' found!\n" "openssl" fi warning_printf $"Cannot find '%s', only SHA-256 checksums are supported.\n" "openssl" fi case "$(uname -s)" in *[fF]ree[bB][sS][dD]*) : ${CARCH:="$(uname -m)"} ostype="freebsd" : ${CHOST:="${CARCH}-unknown-${ostype}-unknown"} case "$CARCH" in amd64) architecture="x86_64";; *) architecture="$CARCH" ;; esac find_printbase() { find "$@" | sed "s:^${1}/*::" } ;; *[lL]inux*) : ${CARCH:="$(uname -m)"} ostype="linux" architecture="$CARCH" : ${CHOST:="${CARCH}-unknown-${ostype}-gnu"} find_printbase() { find "$@" -printf "%P\n" } ;; *) # Guess : ${CARCH:="$(uname -m)"} ostype="$(uname -s)" architecture="$CARCH" : ${CHOST:="${CARCH}-unknown-${ostype}-unknown"} find_printbase() { find "$@" | sed "s:^${1}/*::" } ;; esac show_platform_vars() { echo "CARCH=\"$CARCH\"" echo "CHOST=\"$CHOST\"" echo "ostype=\"$ostype\"" echo "architecture=\"$architecture\"" echo } #}}} #============================================================================ # Bash 4 specific functions # Should still aim at providing a Bash < 4 compatible version. # # {{{ if [[ "$BASH_VERSION" < "4" ]]; then warning_printf $"Your Bash is older than version 4; some features will not work.\n" dep_topo_sort() { cat; } # Meow else #----------------------------------------------------------------------- # dep_topo_sort pkgix_prefix # # Reads list of packages from stdin and list all dependecies for a package # and then the package itself; will not output a package name twice. # This essentially results in a topological sort of the dependency graph # for all requested packages. # Only lists installed packages. # dep_topo_sort() { local pkgix_prefix="$1" ( declare -A pkgs_visited=() local pkg_name while IFS= read -r pkg_name; do __dep_topo_sort_visit "$pkgix_prefix" "$pkg_name" done ) } __dep_topo_sort_visit() { local pkgix_prefix="$1" local pkg_name="$2" db_isinstalled "$pkgix_prefix" "$pkg_name" || return set_db_vars "$pkgix_prefix" "$pkg_name" || pkg_name="$real_pkg_name" [[ -n "${pkgs_visited["$pkg_name"]:-}" ]] && return || : pkgs_visited["$pkg_name"]=1 source "$db_info_file" local dep_name_ver for dep_name_ver in "${depends[@]:+${depends[@]}}" \ "${builddepends[@]:+${builddepends[@]}}" \ "${checkdepends[@]:+${checkdepends[@]}}"; do __dep_topo_sort_visit "$pkgix_prefix" "${dep_name_ver%%[<>=]*}" done echo "$pkg_name" unset_db_info unset_db_vars } fi # }}} #============================================================================ # lock_prefix pkgix_prefix # Checks for existing lock, creates a lock file in pkgix_prefix if not locked. # # !! Should not call from subshells !! # __lock_held="" lock_prefix() { #{{{ local pkgix_prefix="$(abspath "$1")" local lock_file="${pkgix_prefix%/}/${LOCK_FILE}" if [[ -f "$lock_file" ]]; then error_printf $"%s exists!\n" "$lock_file" return $RET_ERR fi [[ ! -d "${lock_file%/*}" ]] && mkdir -p "${lock_file%/*}" 2> /dev/null || : if ! { : > "$lock_file"; } 2> /dev/null; then error_printf $"Cannot create '%s'!\n" "$lock_file" return $RET_ERR fi __lock_held="${pkgix_prefix}" return $RET_OK } #}}} #============================================================================ # unlock_prefix pkgix_prefix # !! Should not call from subshells !! # unlock_prefix() { #{{{ local pkgix_prefix="$(abspath "$1")" local lock_file="${pkgix_prefix%/}/${LOCK_FILE}" rm -f "$lock_file" __lock_held="" } #}}} #============================================================================ # extract_archive path_to_archive # extract_archive() { #{{{ local archive_name="${1:-}" if [[ -f "$archive_name" ]] ; then msg_printf $"Extracting '%s'...\n" "$archive_name" case "$archive_name" in *.tar.bz2) tar xjf "$archive_name" ;; *.tar.gz) tar xzf "$archive_name" ;; *.tar.lzma) tar --lzma -xf "$archive_name" ;; *.tar.xz) tar xJf "$archive_name" ;; *.tar) tar xf "$archive_name" ;; *.tbz2) tar xjf "$archive_name" ;; *.tgz) tar xzf "$archive_name" ;; *.txz) tar xJf "$archive_name" ;; *.bz2) bunzip2 "$archive_name" ;; *.rar) unrar x "$archive_name" ;; *.gz) gunzip "$archive_name" ;; *.zip) unzip "$archive_name" ;; *.Z) uncompress "$archive_name" ;; *.7z) 7z x "$archive_name" ;; *) die $"'%s' cannot be extracted (unknown format)!\n" "$archive_name" ;; esac else die $"'%s' is not a valid file !\n" "$archive_name" fi } #}}} #============================================================================ # fetch_remote url file_name # Wrapper around remote-fetch utility. # Download from url and save to file_name. # fetch_remote() { #{{{ local url="$1" local file_name="$2" # Set up common vars case "$url" in git+*|git://*|hg+*|svn+*) local repo_url="${url#@(git|hg|svn)+}" repo_url="${repo_url%%#*}" local meta="${url#*#}" if [[ "${meta}" == "${url}" ]]; then meta= fi verbose_printf $"Found special URL: %s, META: %s\n" "$repo_url" "$meta" ;; *) ;; esac # Fetch case "$url" in git+*|git://*) git clone "${repo_url}" "${file_name}" || return $RET_ERR if [[ -n "$meta" ]]; then local ref case "${meta%%=*}" in commit|tag) ref="${meta##*=}" ;; branch) ref="origin/${meta##*=}" ;; *) die $"Unrecognized reference: %s\n" "$meta" ;; esac ( cd "${file_name}" && git checkout "$ref" ) || return $RET_ERR fi ;; hg+*) local ref=tip if [[ -n "$meta" ]]; then case "${meta%%=*}" in branch|revision|tag) ref="${meta##*=}" ;; *) die $"Unrecognized reference: %s\n" "$meta" ;; esac fi hg clone -u "$ref" "${repo_url}" "${file_name}" || return $RET_ERR ;; svn+*) local ref=HEAD if [[ -n "$meta" ]]; then case "${meta%%=*}" in revision) ref="${meta##*=}" ;; *) die $"Unrecognized reference: %s\n" "$meta" ;; esac fi svn checkout -r "$ref" "${repo_url}" "${file_name}" || return $RET_ERR ;; *) local fetch_cmd printf -v fetch_cmd -- "$PKGIX_FETCH" "$file_name" $fetch_cmd "$url" || return $RET_ERR ;; esac } #}}} #============================================================================ # verify_checksum checksum file_name # # checksum can be prefixed by = to denote the digest type. # The default digest-type is sha256, if not specified. # verify_checksum() { #{{{ local checksum="$1" local file_name="$2" local digest_type local file_checksum if [[ "$checksum" =~ "=" ]]; then IFS="=" read -r digest_type checksum <<< "$checksum" else digest_type="sha256" fi file_checksum="$(digest "$digest_type" "$file_name")" if [[ -n "$checksum" ]]; then if (( ! arg_ignore_checksums )); then if [[ "$file_checksum" != "$checksum" ]]; then die $"Checksum (%s) of '%s' [%s] does not match [%s]!\n" \ "$digest_type" "$file_name" "$file_checksum" "$checksum" else msg_printf $"Valid checksum: %s\n" "$file_name" fi else warning_printf $"Ignoring checksum for '%s'...\n" "$file_name" fi else if (( ! arg_ignore_checksums )); then die $"No checksum for '%s' [%s]!\n" "$file_name" "$file_checksum" else warning_printf $"No checksum for '%s' [%s]!\n" "$file_name" "$file_checksum" fi fi } #}}} #============================================================================ # get_fetch_target url # Returns filename in URL. # get_fetch_target() { #{{{ local url="$1" local target="${url%%#*}" target="${target%%"?"*}" target="${target##*/}" case "$url" in git+*) target="${target%%.git}" ;; *) ;; esac echo "$target" } #}}} #============================================================================ # fetch url [checksum] [file_name] # Function for use in package description files. # fetch() { #{{{ local url="${1:-}" local checksum="${2:-}" local file_name="${3:-$(get_fetch_target "$url")}" [[ -z "$url" ]] && die $"Invalid URL\n" || : if [[ ! -e "$file_name" ]]; then if [[ "$url" =~ ^/ ]]; then # Is a local path, just copy msg_printf $"Copying '%s'...\n" "$url" if ! cp -- "$url" "$file_name" &> /dev/null; then die $"Copying '%s' failed!\n" "$url" fi else msg_printf $"Fetching '%s'...\n" "$url" if ! fetch_remote "$url" "$file_name"; then die $"Fetching '%s' failed!\n" "$url" fi fi fi # If file is a folder, ignore. if [[ ! -d "$file_name" ]]; then verify_checksum "$checksum" "$file_name" fi } #}}} #============================================================================ # fetch_extract url checksum [archive_name] # Function for use in package description files. # fetch_extract() { #{{{ local url="${1:-}" local checksum="${2:-}" local archive_name="${3:-$(get_fetch_target "$url")}" fetch "$url" "$checksum" "$archive_name" extract_archive "$archive_name" } #}}} #============================================================================ # remove_empty_dirs path [depth] # Remove empty directories in path. # remove_empty_dirs() { #{{{ local path="$1" local depth="${2:-}" local find_args [[ -z "$depth" ]] && find_args=() || find_args=("-maxdepth" "$depth") find "$path" "${find_args[@]:+${find_args[@]}}" -type d -empty -delete } #}}} #============================================================================ # remove_invalid_symlinks path [depth] # Remove invalid symlinks in path. # remove_invalid_symlinks() { #{{{ local path="$1" local depth="${2:-}" local find_args [[ -z "$depth" ]] && find_args=() || find_args=("-maxdepth" "$depth") find "$path" "${find_args[@]:+${find_args[@]}}" -type l | while IFS= read -r lpath; do [[ ! -e "$lpath" ]] && rm "$lpath" || : done } #}}} #============================================================================ # prompt_continue # Prompt user for "Continue ? [Y/n]" # Return code will be RET_OK for y and RET_ERR for n. # prompt_continue() { #{{{ (( arg_noconfirm )) && return $RET_OK local answer printf "${CbluB}"$"Continue ?"" [Y/n] ${Crst}" read -r answer if [[ ! "$answer" =~ ^[yY]|^$ ]]; then verbose_printf $"User aborted.\n" return $RET_ERR fi return $RET_OK } #}}} #============================================================================ # source_prefix_rc pkgix_prefix # Source a prefix-specific RC-file. # # If the RC-file contains variables which are exported then they will be usable # in e.g., chenv, while compiling; otherwise, the values set will only be # valid in this session, but can be used to provide extra variables to # package description files. # source_prefix_rc() { #{{{ local pkgix_prefix="$1" [[ ! -r "${pkgix_prefix%/}/etc/${PROGNAME}rc" ]] && return || : source "${pkgix_prefix%/}/etc/${PROGNAME}rc" # Export the list of sourced RC-files, to have the list remain accessible # inside a chenv. if [[ -z "${_PKGIX_PREFIX_RCS:-}" ]]; then export _PKGIX_PREFIX_RCS="${pkgix_prefix%/}/etc/${PROGNAME}rc" else export _PKGIX_PREFIX_RCS="${pkgix_prefix%/}/etc/${PROGNAME}rc:${_PKGIX_PREFIX_RCS:-}" fi } #}}} #============================================================================ # export_env pkgix_prefix [reset] # __old_env_saved=0 export_env() { #{{{ local pkgix_prefix="$(abspath "$1")" local reset="${2:-0}" [[ ! -d "$pkgix_prefix" ]] && return $RET_ERR if (( ! __old_env_saved )); then __old_env_saved=1 __old_PKGIX_PREFIX="${PKGIX_PREFIX:-}" __old_PATH="${PATH:-}" __old_LD_RUN_PATH="${LD_RUN_PATH:-}" __old_LIBRARY_PATH="${LIBRARY_PATH:-}" __old_PKG_CONFIG_PATH="${PKG_CONFIG_PATH:-}" __old_CPATH="${CPATH:-}" __old__PKGIX_PREFIX_RCS="${_PKGIX_PREFIX_RCS:-}" elif (( reset )); then reset_env fi if [[ -z "${PKGIX_PREFIX:-}" ]]; then # Export so calls to pkgix don't need the prefix; appending, so that # installing takes the first prefix. export PKGIX_PREFIX="$pkgix_prefix" else # Check if this prefix has already been exported. [[ "$PKGIX_PREFIX" =~ ^(.*:)?"${pkgix_prefix}"(:.*)?$ ]] && return $RET_OK export PKGIX_PREFIX="$PKGIX_PREFIX:${pkgix_prefix}" fi verbose_printf $"Exporting environment: %s\n" "$pkgix_prefix" local base_path local _base_path local library_path for _base_path in "${pkgix_prefix}" "${pkgix_prefix%/}/usr"; do base_path="${_base_path%/}" library_path="${base_path}/lib" # Don't necessarily want lib64 or lib32 on the path, especially if the setup is pure, # therefore check and only add them if they exist. local libdir for libdir in lib64 lib32; do if [[ -d "${base_path}/${libdir}" ]]; then library_path+=":${base_path}/${libdir}" fi done # LD_LIBRARY_PATH is harmful, as it makes programs from the system use # the libraries found in $base_path/lib, which can result in strange # behaviour. # For more info: http://xahlee.info/UnixResource_dir/_/ldpath.html # Instead, make programs include the correct search path when they are # compiled (--rpath). if [[ -z "${LD_RUN_PATH:-}" ]]; then export LD_RUN_PATH="${library_path}" else export LD_RUN_PATH="${library_path}:$LD_RUN_PATH" fi # Prepending to PATH export PATH="${base_path}/bin:${PATH:-}" export PATH="${base_path}/sbin:${PATH:-}" # LD_RUN_PATH needs to be set for compilation even if the path does # not exist yet, as a package might install its own library in a # non-existent base-path and this needs to be encoded in the binaries; # # PATH may be needed in postinstall hooks; # # below variables should only be useful if paths they refer to exist. [[ ! -d "$_base_path" ]] && continue || : # And make the linker look in the right paths (-L) if [[ -z "${LIBRARY_PATH:-}" ]]; then export LIBRARY_PATH="${library_path}" else export LIBRARY_PATH="${library_path}:$LIBRARY_PATH" fi if [[ -z "${PKG_CONFIG_PATH:-}" ]]; then export PKG_CONFIG_PATH="${base_path}/lib/pkgconfig" else export PKG_CONFIG_PATH="${base_path}/lib/pkgconfig:$PKG_CONFIG_PATH" fi # C/C++ preprocessor search paths (-I) if [[ -z "${CPATH:-}" ]]; then export CPATH="${base_path}/include" else export CPATH="${base_path}/include:$CPATH" fi done source_prefix_rc "$pkgix_prefix" return $RET_OK } #}}} #============================================================================ # reset_env # reset_env() { if (( __old_env_saved )); then [[ -n "${__old_PKGIX_PREFIX}" ]] && export PKGIX_PREFIX="${__old_PKGIX_PREFIX}" || unset PKGIX_PREFIX [[ -n "${__old_PATH}" ]] && export PATH="${__old_PATH}" || unset PATH [[ -n "${__old_LD_RUN_PATH}" ]] && export LD_RUN_PATH="${__old_LD_RUN_PATH}" || unset LD_RUN_PATH [[ -n "${__old_LIBRARY_PATH}" ]] && export LIBRARY_PATH="${__old_LIBRARY_PATH}" || unset LIBRARY_PATH [[ -n "${__old_PKG_CONFIG_PATH}" ]] && export PKG_CONFIG_PATH="${__old_PKG_CONFIG_PATH}" || unset PKG_CONFIG_PATH [[ -n "${__old_CPATH}" ]] && export CPATH="${__old_CPATH}" || unset CPATH [[ -n "${__old__PKGIX_PREFIX_RCS}" ]] && export _PKGIX_PREFIX_RCS="${__old__PKGIX_PREFIX_RCS}" || unset _PKGIX_PREFIX_RCS fi } #============================================================================ # cmd_showenv pkgix_prefix... # cmd_showenv() { #{{{ local pkgix_prefix for pkgix_prefix in "$@"; do export_env "$pkgix_prefix" || : done (( arg_debug )) && { show_platform_vars; } || : local source_cmd case "$PKGIX_SHELL" in *csh) [[ -n "${PKGIX_PREFIX:-}" ]] && echo "setenv PKGIX_PREFIX \"$PKGIX_PREFIX\"" || : [[ -n "${PATH:-}" ]] && echo "setenv PATH \"$PATH\"" || : [[ -n "${LD_RUN_PATH:-}" ]] && echo "setenv LD_RUN_PATH \"$LD_RUN_PATH\"" || : [[ -n "${LIBRARY_PATH:-}" ]] && echo "setenv LIBRARY_PATH \"$LIBRARY_PATH\"" || : [[ -n "${PKG_CONFIG_PATH:-}" ]] && echo "setenv PKG_CONFIG_PATH \"$PKG_CONFIG_PATH\"" || : [[ -n "${CPATH:-}" ]] && echo "setenv CPATH \"$CPATH\"" || : source_cmd="source" ;; *) # Assume sh compatible shell (bash, zsh, ..) [[ -n "${PKGIX_PREFIX:-}" ]] && echo "export PKGIX_PREFIX=\"$PKGIX_PREFIX\"" || : [[ -n "${PATH:-}" ]] && echo "export PATH=\"$PATH\"" || : [[ -n "${LD_RUN_PATH:-}" ]] && echo "export LD_RUN_PATH=\"$LD_RUN_PATH\"" || : [[ -n "${LIBRARY_PATH:-}" ]] && echo "export LIBRARY_PATH=\"$LIBRARY_PATH\"" || : [[ -n "${PKG_CONFIG_PATH:-}" ]] && echo "export PKG_CONFIG_PATH=\"$PKG_CONFIG_PATH\"" || : [[ -n "${CPATH:-}" ]] && echo "export CPATH=\"$CPATH\"" || : source_cmd="source" ;; esac if [[ -n "${_PKGIX_PREFIX_RCS:-}" ]]; then local pkgix_prefix_rcs local rcfile IFS=":" read -ra pkgix_prefix_rcs <<< "${_PKGIX_PREFIX_RCS}" for rcfile in "${pkgix_prefix_rcs[@]}"; do echo "${source_cmd} \"${rcfile}\"" done fi } #}}} #============================================================================ # is_pkg_pointer pkg_file # is_pkg_pointer() { #{{{ [[ "$(wc -l < "$1")" =~ ^[^0-9]*[01][^0-9]*$ ]] } #}}} #============================================================================ # source_pkg_file pkg_file # source_pkg_file() { #{{{ local pkg_name="$1" local pkg_file="$2" if is_pkg_pointer "$pkg_file"; then local resolved_pkg_name="$(<$pkg_file)" if [[ -z "${resolved_pkg_name:-}" ]]; then error_printf $"Empty package description file!\n" return $RET_ERR fi verbose_printf $"Pointer: %s -> %s\n" "$pkg_name" "$resolved_pkg_name" source_pkg "$resolved_pkg_name" else source "$pkg_file" fi } #}}} #============================================================================ # source_pkg pkg_name [prefer_repo] # # As source_pkg will be called in subshells, need to initialize repo_cache_dir # here; only created if needed. if type -p mktemp > /dev/null; then __repo_cache_dir="$(mktemp -du)-${PROGNAME}" else __repo_cache_dir="/tmp/.${PROGNAME}-repo-cache" fi source_pkg() { #{{{ local pkg_name="$1" local prefer_repo="${2:-}" local pkg_file # Don't set repo as local here, as it can then be used in the package # description files to get information about its source repository, and # e.g., download extra files such as patches from the repository. # The exception is if prefer_repo is set, in which case it is assumed that # source_pkg is called from a package description file itself and repo must # already be set. [[ -n "$prefer_repo" ]] && local repo || : for repo in "$prefer_repo" "${pkgix_repos[@]:+${pkgix_repos[@]}}"; do [[ -z "$repo" ]] && continue || : pkg_file="${repo}/$pkg_name" case "$pkg_file" in http://*|https://*|ftp://*) local repo_hash="$(digest sha256 <<< "$repo")" local local_pkg_file="${__repo_cache_dir}/${repo_hash}/${pkg_name}" [[ ! -d "${local_pkg_file%/*}" ]] && mkdir -p "${local_pkg_file%/*}" || : verbose_printf $"Trying: %s\n" "$pkg_file" if [[ -f "$local_pkg_file" ]]; then verbose_printf $"Source:"" ${Cgrn}%s${Crst} (%s)\n" "$pkg_file" $"cached" else if fetch_remote "$pkg_file" "$local_pkg_file" 2> /dev/null; then verbose_printf $"Source:"" ${Cgrn}%s${Crst}\n" "$pkg_file" else rm -f "$local_pkg_file" fi fi if [[ -f "$local_pkg_file" ]]; then source_pkg_file "$pkg_name" "$local_pkg_file" break fi ;; *) verbose_printf $"Trying: %s\n" "$pkg_file" if [[ -f "$pkg_file" ]]; then verbose_printf $"Source:"" ${Cgrn}%s${Crst}\n" "$pkg_file" source_pkg_file "$pkg_name" "$pkg_file" break fi ;; esac done [[ -n "${version:-}" || "${metapkg:-}" == "$METAPKG_DUMMY" ]] } #}}} #============================================================================ # sourced_pkg_call funcname [pkgix_prefix [install_dir]] # Wrap sourced function call execution. # # Disable 'nounset' for calling sourced functions, as the complexity of package # description files should be small enough to trade some code safety for # convenience. Enable inside the functions if neccessary. # # Makes the following extra variables available to package description file: # prefix=${pkgix_prefix%/} -- if pkgix_prefix is '/' the prefix passed is empty! # destdir=${install_dir} # dest_prefix=${destdir}${prefix} # # Otherwise potentially useful variables which are available from parent scope: # build_dir - absolute path to build-dir # old_version - if package is being upgraded (see prefix_upgrade) # repo - the URL where the package description file is located # # Useful functions for use in package description files: # fetch, fetch_extract, die # # See platform detection above for variables identifying the host platform. # # Due to the way BASH behaves, propagating even local variables from a parent # function to its children, ALL variables from parents will be available. # However, as the function to be called is executed in a subshell, none of # these variables can be affected after completion of sourced_pkg_call. # sourced_pkg_call() { #{{{ local funcname=$1 if ! type -p $funcname > /dev/null; then local error_msg printf -v error_msg $"Package file does not provide function: %s\n" "$funcname" case "$funcname" in isinstalled) # Default to installing if isinstalled not provided warning_printf "$error_msg" return $RET_ERR ;; iscompat) # Default to installing if iscompat not provided warning_printf "$error_msg" return $RET_OK ;; *) die "$error_msg" ;; esac fi ( # Optional Arguments, passed as globals if [[ -n "${2:-}" ]]; then prefix="${2%/}" if [[ -n "${3:-}" ]]; then destdir="$3" dest_prefix="${destdir}${prefix}" fi fi (( ! arg_debug )) && set +o nounset || : $funcname ) } #}}} #============================================================================ # unset_sourced_pkg # unsets all fields that could be set by a package file # unset_sourced_pkg() { #{{{ unset metapkg unset satisfied unset version unset license unset website unset description unset depends unset builddepends unset checkdepends unset provides unset iscompat unset isinstalled unset prepare unset build unset check unset installenv unset postinstall unset preremove unset postremove unset forceinstall unset backup } #}}} #============================================================================ # strcmp_op lhs op_rhs # Compares lhs with op_rhs, which is in the format of: # ("<="|">="|"<"|">"|"=") # then a bash string comparison (sorted lexicographically) will be performed. # strcmp_op() { #{{{ local lhs="$1" local op_rhs="$2" case "$op_rhs" in "<="*) [[ "$lhs" == "${op_rhs##<=}" || "$lhs" < "${op_rhs##<=}" ]] \ && return $RET_OK \ || return $RET_ERR ;; ">="*) [[ "$lhs" == "${op_rhs##>=}" || "$lhs" > "${op_rhs##>=}" ]] \ && return $RET_OK \ || return $RET_ERR ;; "<"*) [[ "$lhs" < "${op_rhs##<}" ]] \ && return $RET_OK \ || return $RET_ERR ;; ">"*) [[ "$lhs" > "${op_rhs##>}" ]] \ && return $RET_OK \ || return $RET_ERR ;; "="*) [[ "$lhs" == "${op_rhs##=}" ]] \ && return $RET_OK \ || return $RET_ERR ;; *) die ;; esac } #}}} #============================================================================ # db_isinstalled pkgix_prefix pkg_name [cmp_version] # Optionally compares pkg_name against version in cmp_version using # strcmp_op. # db_isinstalled() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" local cmp_version="${3:-}" local db_prefix_pkg="${pkgix_prefix%/}/${DB_PREFIX}/${pkg_name}" if (( arg_dry_run )) && dry_db_isinstalled "$pkgix_prefix" "$pkg_name"; then # Check new dry installed packages first, then check actually installed packages. if [[ -n "$cmp_version" ]]; then strcmp_op "$(dry_db_info_query "$pkgix_prefix" "$pkg_name" "version")" "$cmp_version" \ && return $RET_OK \ || return $RET_ERR else return $RET_OK fi fi if [[ -f "${db_prefix_pkg}/version" && -f "${db_prefix_pkg}/files" ]]; then if [[ -n "$cmp_version" ]]; then strcmp_op "$(<"${db_prefix_pkg}/version")" "$cmp_version" \ && return $RET_OK \ || return $RET_ERR else return $RET_OK fi fi return $RET_ERR } #}}} #============================================================================ # set_db_vars pkgix_prefix pkg_name # # Sets variables required to access package information from db. # # Returns true if $pkg_name is not a provides target, false # otherwise and resolves the real_pkg_name variable, which can be # used to get the real package name. # set_db_vars() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" db_prefix="${pkgix_prefix%/}/${DB_PREFIX}" db_prefix_pkg="${db_prefix}/${pkg_name}" db_files_file="${db_prefix_pkg}/files" db_backup_file="${db_prefix_pkg}/files.backup" db_version_file="${db_prefix_pkg}/version" db_info_file="${db_prefix_pkg}/info" db_hooks_file="${db_prefix_pkg}/hooks" # Check if valid symlink to db dir (provides target) if [[ -L "$db_prefix_pkg" && -d "$db_prefix_pkg" ]]; then # Resolve both path to pkg-dir and db-prefix, in case there are other # symlink along the way. db_prefix_pkg="$(resolv_dir_symlink "$db_prefix_pkg")" db_prefix="$(resolv_dir_symlink "$db_prefix")" real_pkg_name="${db_prefix_pkg##${db_prefix}/}" set_db_vars "$pkgix_prefix" "$real_pkg_name" || : return $RET_ERR fi return $RET_OK } #}}} #============================================================================ # unset_db_vars # unset_db_vars() { #{{{ unset db_prefix unset db_prefix_pkg unset db_files_file unset db_backup_file unset db_version_file unset db_info_file unset db_hooks_file unset real_pkg_name } #}}} #============================================================================ # db_install pkgix_prefix pkg_name # # Notes about the file, created or updated at the end of this function: # The db file should maintain the order in which packages were installed; # upgrade performs package upgrade in the order they were installed. Entries # are not deleted upon removal, so that if they are reinstalled, the order is # maintained. This does not yet fully solve the problem of package upgrades # which pull new dependencies; for this, upgrade itself, must order # dependencies first, regardless of initial install order. # db_install() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" local reason="$3" local staging_files_file="$4" local pkgnew_files_file="$5" local staging_backup_file="$6" set_db_vars "$pkgix_prefix" "$pkg_name" || die # Create installed database folders mkdir -p "${db_prefix_pkg}" # Set up provides symlinks local provides_name local db_provides_target for provides_name in "${provides[@]:+${provides[@]}}"; do [[ "$provides_name" == "$pkg_name" ]] && continue local db_provides_target="${db_prefix}/${provides_name}" if [[ -e "$db_provides_target" ]]; then warning_printf $"'%s' provides '%s', which already exists!\n" "$pkg_name" "$provides_name" else mkdir -p "${db_provides_target%/*}" ln -s "${db_prefix_pkg}" "${db_provides_target}" fi done # Use previously created list of files and pkgnew files cat "$staging_files_file" "$pkgnew_files_file" > "$db_files_file" rm "$staging_files_file" "$pkgnew_files_file" # Use previously created list of backup files mv "$staging_backup_file" "$db_backup_file" echo "${version:-}" > "$db_version_file" [[ -z "${version:-}" ]] && warning_printf $"Empty 'version' field in package description file of '%s'!\n" "$pkg_name" { echo "description=\"${description:-}\"" echo "installdate=\"$(date)\"" echo "website=\"${website:-}\"" printf "license=(" local license_elem for license_elem in "${license[@]:+${license[@]}}"; do printf " '%s'" "$license_elem" done printf " )\n" printf "depends=(" local dep_name for dep_name in "${depends[@]:+${depends[@]}}"; do printf " '%s'" "$dep_name" done printf " )\n" printf "builddepends=(" for dep_name in "${builddepends[@]:+${builddepends[@]}}"; do printf " '%s'" "$dep_name" done printf " )\n" printf "checkdepends=(" for dep_name in "${checkdepends[@]:+${checkdepends[@]}}"; do printf " '%s'" "$dep_name" done printf " )\n" printf "provides=(" local provides_name for provides_name in "${provides[@]:+${provides[@]}}"; do printf " '%s'" "$provides_name" done printf " )\n" echo "reason=\"${reason}\"" } > "$db_info_file" # Store *remove functions in hooks file; note that only the functions are # written, and no other variables/functions are saved, which means that the # *remove functions should not make use of any other variables/functions in # the package description file! { type -p preremove > /dev/null && declare -f preremove || : type -p postremove > /dev/null && declare -f postremove || : } > "$db_hooks_file" [[ -z "${description:-}" ]] && warning_printf $"Empty 'description' field in package description file of '%s'!\n" "$pkg_name" [[ -z "${license[*]:+${license[*]}}" ]] && warning_printf $"Empty 'license' field in package description file of '%s'!\n" "$pkg_name" [[ -z "${website:-}" ]] && warning_printf $"Empty 'website' field in package description file of '%s'!\n" "$pkg_name" local db_file="${db_prefix}.db" if [[ -f "$db_file" ]] && grep -q "^.|${pkg_name//./\\.}\$" "$db_file"; then sed -i "s:^.|${pkg_name//./\\.}\$:i|${pkg_name}:" "$db_file" else echo "i|$pkg_name" >> "$db_file" fi unset_db_vars } #}}} #============================================================================ # unset_db_info # unset_db_info() { #{{{ unset description unset website unset license unset depends unset builddepends unset checkdepends unset provides unset installdate unset reason } #}}} #============================================================================ # db_info_query pkgix_prefix pkg_name property [default] # # Only use this function to query a single property, as it would be inefficient # for querying multiple properties in sequence; for this source the db_info_file # and subsequently call unset_db_info. # # Only echos the property as a string, and should not be used if the property # type is an array. # db_info_query() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" local property="$3" local default="${4:-}" if (( arg_dry_run )) && dry_db_isinstalled "$pkgix_prefix" "$pkg_name"; then case "$property" in version) dry_db_info_query "$pkgix_prefix" "$pkg_name" "version" ;; reason) dry_db_info_query "$pkgix_prefix" "$pkg_name" "reason" ;; *) die ;; esac return fi ( local result="" set_db_vars "$pkgix_prefix" "$pkg_name" || : case "$property" in version) [[ -f "$db_version_file" ]] && result="$(<"$db_version_file")" || : [[ -z "$result" ]] && echo "$default" || echo "$result" ;; *) [[ -f "$db_info_file" ]] && source "$db_info_file" || : echo "${!property:-${default}}" unset_db_info ;; esac unset_db_vars ) } #}}} #============================================================================ # db_list_installed pkgix_prefix [from_backup] # If from_backup is 1, then copy the existing db file, create a backup of it # and then read from it instead. # db_list_installed() { #{{{ local pkgix_prefix="$1" local from_backup="${2:-0}" local db_file="${pkgix_prefix%/}/${DB_PREFIX}.db" if [[ -f "$db_file" ]]; then if (( from_backup )); then cp -- "$db_file" "${db_file}.bak" db_file="${db_file}.bak" fi grep "^i|" "$db_file" | while IFS= read -r pkg_name; do pkg_name="${pkg_name##i|}" if ! db_isinstalled "$pkgix_prefix" "$pkg_name"; then warning_printf $"Inconsistent database entry: %s\n" "$pkg_name" continue fi echo "$pkg_name" done fi } #}}} #============================================================================ # dry_db_* pkgix_prefix pkg_name [options] # # {{{ if type -p mktemp > /dev/null; then __dry_db_dir="$(mktemp -du)-${PROGNAME}" else __dry_db_dir="/tmp/.${PROGNAME}-dry-db" fi dry_db_install() { local dry_db_dir="${__dry_db_dir}/${1%/}/${2}" local reason="$3" mkdir -p "$dry_db_dir" echo "${version:-}" > "${dry_db_dir}/version" echo "$reason" > "${dry_db_dir}/reason" } dry_db_isinstalled() { [[ -d "${__dry_db_dir}/${1%/}/${2}" ]] } dry_db_info_query() { case "$3" in version) echo "$(<"${__dry_db_dir}/${1%/}/${2}/version")" ;; reason) echo "$(<"${__dry_db_dir}/${1%/}/${2}/reason")" ;; *) die ;; esac } # }}} #============================================================================ # pkg_install pkgix_prefix pkg_name [reinstall [reason]] # # pkgix_prefix: the prefix path # pkg_name: full package name as found in repository # reinstall: if set to 1, reinstalls an already installed package # reason: $REASON_EXPLICIT or $REASON_DEPENDENCY; # for tracking install reason in the db. # pkg_install() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" local reinstall="${3:-0}" local reason="${4:-$REASON_EXPLICIT}" local is_reinstall=0 if [[ ! -d "$pkgix_prefix" ]]; then die $"'%s' is not a valid directory!\n" "$pkgix_prefix" fi if db_isinstalled "$pkgix_prefix" "$pkg_name"; then if (( ! reinstall )); then local _fn_printf [[ "$reason" == "$REASON_EXPLICIT" ]] && _fn_printf="msg_printf" || _fn_printf="verbose_printf" $_fn_printf "${Cgrn}%s${Crst} "$"is available\n" "$pkg_name" return $RET_OK else # Is reinstall just says if the package is already in the prefix # but should be reinstalled. is_reinstall=1 # Set reason based on user-override if provided, otherwise preserve previous reason if (( ! arg_reason_dependency && ! arg_reason_explicit )); then reason="$(db_info_query "$pkgix_prefix" "$pkg_name" "reason" "$reason")" fi fi fi # Only allow forcing reason for explicitly selected packages. if [[ "$reason" == "$REASON_EXPLICIT" ]]; then if (( arg_reason_dependency )); then reason="$REASON_DEPENDENCY" elif (( arg_reason_explicit )); then reason="$REASON_EXPLICIT" fi fi verbose_printf "${CcynB}=> %s${Crst}\n" "$pkg_name" ( export_env "$pkgix_prefix" 1 # Source package description file and try to traverse until a non-meta # package description is found. local next_pkg_name="$pkg_name" # preserve requested package name while :; do # Unset previous package variables, otherwise we might end in an # endless loop when resolving dependencies, metapkgs, or might even # keep unset variables/functions. unset_sourced_pkg if ! source_pkg "$next_pkg_name"; then die $"Cannot find valid package description file for '%s'!\n" "$next_pkg_name" fi # Not a meta-package, continue normally [[ -z "${metapkg:-}" ]] && break # Is metapackage, check if satisfied if (( ! is_reinstall )) && sourced_pkg_call satisfied "$pkgix_prefix"; then if (( ! arg_reinstall )) || [[ "$reason" != "$REASON_EXPLICIT" ]] \ || [[ "$metapkg" == "$METAPKG_DUMMY" ]]; then msg_printf $"Metapackage '%s' satisfied.\n" "$next_pkg_name" exit $EXIT_OK else warning_printf $"Forcing installation of metapackage '%s', despite satisfied...\n" "$next_pkg_name" fi fi # If this metapackage is only a dummy to check if a dependence is satisfied, # and the above 'satisfied' function failed, this is a dead end. if [[ "$metapkg" == "$METAPKG_DUMMY" ]]; then die $"Metapackage '%s' cannot be satisfied!\n" "$next_pkg_name" fi # Not satisfied and not a dummy, resolve this by trying to install # the suggested package metapkg. msg_printf $"Metapackage: %s -> %s\n" "$next_pkg_name" "$metapkg" next_pkg_name="$metapkg" done if (( ! is_reinstall )) && sourced_pkg_call isinstalled "$pkgix_prefix"; then if (( ! arg_reinstall )) || [[ "$reason" != "$REASON_EXPLICIT" ]]; then local which_printf [[ "$reason" == "$REASON_EXPLICIT" ]] \ && which_printf=msg_printf \ || which_printf=verbose_printf $which_printf $"'%s' already available outside of '%s', skipping...\n" "$pkg_name" "$pkgix_prefix" exit $EXIT_OK else msg_printf $"Forcing installation of '%s', despite already available outside of '%s'...\n" "$pkg_name" "$pkgix_prefix" fi fi if ! sourced_pkg_call iscompat; then if (( ! arg_force )); then die $"'%s' is not compatible with your system!\n" "$pkg_name" else warning_printf $"Trying installation of '%s', despite system incompatibility...\n" "$pkg_name" fi fi if (( ! arg_nodeps )); then msg_printf "${Cgrn}%s (%s)${Crst} "$"resolving dependencies...\n" "$pkg_name" "${version:-}" local dep_name_ver local dep_name local dep_cmp_version local dep_additional=() if (( ! arg_ignore_check )); then dep_additional=("${checkdepends[@]:+${checkdepends[@]}}") fi for dep_name_ver in "${depends[@]:+${depends[@]}}" \ "${builddepends[@]:+${builddepends[@]}}" \ "${dep_additional[@]:+${dep_additional[@]}}"; do dep_name="${dep_name_ver%%[<>=]*}" dep_cmp_version="${dep_name_ver##${dep_name}}" if db_isinstalled "$pkgix_prefix" "$dep_name" \ && ! db_isinstalled "$pkgix_prefix" "$dep_name" "$dep_cmp_version"; then # The package is installed, but the required version is # not; note that we only check for the installed version if # this dependency is already installed. If a package # installs a package as dependecy, but the version is # wrong, we won't complain, as this should be solved in the # package file. die $"Package dependency cannot be resolved: %s (installed: %s)\n" \ "$dep_name_ver" "$(db_info_query "$pkgix_prefix" "$dep_name" "version" "None")" fi pkg_install "$pkgix_prefix" "$dep_name" 0 "$REASON_DEPENDENCY" done else # arg_nodeps can be set to anything larger than 1 by a plugin to # suppress this message. (( arg_nodeps == 1 )) && warning_printf $"Ignoring dependencies.\n" || : fi if (( arg_dry_run )); then dry_db_install "$pkgix_prefix" "$pkg_name" "$reason" msg_printf "${CgrnB}%s (%s)${Crst} "$"dry install complete.\n" "$pkg_name" "${version:-}" exit $EXIT_OK fi # Reset and set environment again after installing dependencies, so # that the environment variables capture all folders that may have been # installed by the dependencies. export_env "$pkgix_prefix" 1 #local build_dir # excplicitly allow to be read in called functions local install_dir if [[ -n "${PKGIX_BUILD_DIR:-}" ]]; then build_dir="${PKGIX_BUILD_DIR}/${PROGNAME}-build" install_dir="${PKGIX_BUILD_DIR}/${PROGNAME}-install" else build_dir="${pkgix_prefix%/}/.${PROGNAME}-build" install_dir="${pkgix_prefix%/}/.${PROGNAME}-install" fi mkdir -p "$build_dir" mkdir -p "$install_dir" pushd "$build_dir" &> /dev/null # Setup cleanup trap, as this is executed in subshell, it does not # affect the parent shell's trap which will be executed normally on exit. local trap_cmd="{ popd &> /dev/null; rm -rf \"$install_dir\";" (( ! arg_keep_build_dir )) && trap_cmd+=" rm -rf \"$build_dir\";" trap_cmd+=" }" trap "$trap_cmd" EXIT # Run prepare if available if type -p prepare > /dev/null; then msg_printf "${Cgrn}%s (%s)${Crst} "$"preparing...\n" "$pkg_name" "${version:-}" if ! sourced_pkg_call prepare "$pkgix_prefix" "$install_dir"; then die $"Preparing build of '%s' failed! Aborting.\n" "$pkg_name" fi fi msg_printf "${Cgrn}%s (%s)${Crst} "$"building...\n" "$pkg_name" "${version:-}" if ! sourced_pkg_call build "$pkgix_prefix" "$install_dir"; then die $"Building '%s' failed! Aborting.\n" "$pkg_name" fi # Run check if available if type -p check > /dev/null; then msg_printf "${Cgrn}%s (%s)${Crst} "$"running check...\n" "$pkg_name" "${version:-}" if ! sourced_pkg_call check "$pkgix_prefix" "$install_dir"; then if (( ! arg_ignore_check )); then die $"'%s' failed check!\n" "$pkg_name" else warning_printf $"Running check failed, '%s' might not work as expected!\n" "$pkg_name" fi fi fi msg_printf "${Cgrn}%s (%s)${Crst} "$"installing into staging area...\n" "$pkg_name" "${version:-}" if ! sourced_pkg_call installenv "$pkgix_prefix" "$install_dir"; then die $"Installation of '%s' failed! Aborting.\n" "$pkg_name" fi if (( is_reinstall )); then # Remove after build/installenv, so that if building/installing # a package depends on itself it is used rather than the system # version (e.g., gcc, make). If the system version should be used # again: remove->install manually. pkg_remove "$pkgix_prefix" "$pkg_name" fi # Create the list of files to be installed local staging_files_file="${build_dir}/.staging.files" : > "$staging_files_file" local install_prefix="${install_dir}${pkgix_prefix}" if [[ -d "${install_prefix}" ]]; then # As prefix is $pkgix_prefix, the complete folder structure from / is maintained inside $install_dir find_printbase "${install_prefix}" > "$staging_files_file" else warning_printf $"Package did not install any files.\n" fi # Require files.backup file; can be empty. # Perform this step before moving files to .pkgnew! local staging_backup_file="${build_dir}/.staging.files.backup" : > "$staging_backup_file" local backup_path local backup_checksum for backup_path in "${backup[@]:+${backup[@]}}"; do # Only do checksum based backup with real files if [[ -f "${install_prefix%/}/${backup_path}" ]]; then backup_checksum="$(digest sha256 "${install_prefix%/}/${backup_path}")" else backup_checksum="" fi echo "${backup_path}:${backup_checksum}" >> "$staging_backup_file" done # Check for file conflicts before copying from install_dir to pkgix_prefix local pkgnew_files_file="${build_dir}/.pkgnew.files" : > "$pkgnew_files_file" if (( ! arg_force )); then msg_printf "${Cgrn}%s (%s)${Crst} "$"checking for file conflicts...\n" "$pkg_name" "${version:-}" while IFS= read -r path; do if [[ ! -d "${pkgix_prefix%/}/${path}" && -e "${pkgix_prefix%/}/${path}" ]]; then in_list "$path" "${forceinstall[@]:+${forceinstall[@]}}" && continue || : # Only if the file already exists in pkgix_prefix and in # the backup list, should it be installed as a .pkgnew file. if in_list "$path" "${backup[@]:+${backup[@]}}"; then mv "${install_prefix%/}/${path}" "${install_prefix%/}/${path}.pkgnew" echo "${path}.pkgnew" >> "$pkgnew_files_file" warning_printf $"Installing '%s' as '%s'\n" "$path" "${path}.pkgnew" continue fi die $"Conflict detected: %s\n" "$path" fi done < "$staging_files_file" else warning_printf $"Skipping file conflict check.\n" fi msg_printf "${Cgrn}%s (%s)${Crst} "$"installing...\n" "$pkg_name" "${version:-}" # Perform actual installation if [[ -d "${install_prefix}" ]]; then if ! cp -a -- "${install_prefix%/}"/* "${pkgix_prefix}"; then die $"Installation of '%s' failed! Aborting.\n" "$pkg_name" fi fi db_install "$pkgix_prefix" "$pkg_name" "$reason" \ "$staging_files_file" "$pkgnew_files_file" "$staging_backup_file" trap - EXIT popd &> /dev/null (( ! arg_keep_build_dir )) && rm -rf "$build_dir" rm -rf "$install_dir" # Run *install hooks if type -p postinstall > /dev/null; then msg_printf "${Cgrn}%s (%s)${Crst} "$"post-install hook...\n" "$pkg_name" "${version:-}" if ! sourced_pkg_call postinstall "$pkgix_prefix"; then warning_printf $"Post-install hook failed!\n" fi fi msg_printf "${CgrnB}%s (%s)${Crst} "$"successfully installed.\n" "$pkg_name" "${version:-}" log_printf "$pkgix_prefix" "%s (%s) "$"installed\n" "$pkg_name" "${version:-}" ) || exit $? return $RET_OK } #}}} #============================================================================ # pkg_remove pkgix_prefix pkg_name # pkg_remove() { #{{{ local pkgix_prefix="$1" local pkg_name="$2" local purge="${3:-0}" set_db_vars "$pkgix_prefix" "$pkg_name" || pkg_name="$real_pkg_name" if ! db_isinstalled "$pkgix_prefix" "$pkg_name"; then die $"'%s' not found!\n" "$pkg_name" fi local version="$(<"$db_version_file")" # Create an empty files.backup file if it does not exist [backwards compatibility]. if (( purge )) || [[ ! -f "$db_backup_file" ]]; then : > "$db_backup_file" fi msg_printf "${Cgrn}%s (%s)${Crst} "$"removing...\n" "$pkg_name" "$version" if [[ -f "$db_hooks_file" ]]; then ( source "$db_hooks_file" if type -p preremove > /dev/null; then msg_printf "${Cgrn}%s (%s)${Crst} "$"pre-remove hook...\n" "$pkg_name" "$version" if ! sourced_pkg_call preremove "$pkgix_prefix"; then warning_printf $"Pre-remove hook failed!\n" fi fi ) fi local backup_path local backup_checksum local backup_checksum_f while IFS= read -r backup_path; do # (0) Check for changed files which require backup backup_checksum="${backup_path##*:}" backup_path="${backup_path%:*}" if [[ -f "${pkgix_prefix%/}/${backup_path}" ]]; then backup_checksum_f="$(digest sha256 "${pkgix_prefix%/}/${backup_path}")" # If file changed, exclude from deletion if [[ "$backup_checksum" != "$backup_checksum_f" ]]; then verbose_printf $"Keeping modified '%s'\n" "$backup_path" echo "${backup_path}" fi else # Exclude by default echo "${backup_path}" fi done < "$db_backup_file" | grep -Fxvf - "$db_files_file" | while IFS= read -r filename; do # (1) First pass delete files and get directory depths if [[ ! -d "${pkgix_prefix%/}/${filename}" ]]; then rm -f "${pkgix_prefix%/}/${filename}" else # Get path depth for sorting: If we don't do this, directories # which are higher up in the hierarchy are being tried to be # deleted before its subdirectories, which could be empty, but # because the subdirectories still exist, the parent won't be # deleted. IFS="/" read -ra tmp <<< "$filename" echo "${#tmp[@]}:${filename}" fi done | sort -rn | while IFS= read -r dirname; do # (2) Then check for empty directories and delete if empty dirname="${dirname#*:}" remove_empty_dirs "${pkgix_prefix%/}/${dirname}" 0 done if [[ -f "$db_hooks_file" ]]; then ( source "$db_hooks_file" if type -p postremove > /dev/null; then msg_printf "${Cgrn}%s (%s)${Crst} "$"post-remove hook...\n" "$pkg_name" "$version" if ! sourced_pkg_call postremove "$pkgix_prefix"; then warning_printf $"Post-remove hook failed!\n" fi fi ) fi # Remove meta files rm -rf "${db_prefix_pkg}" # Clean up db remove_invalid_symlinks "${db_prefix}" remove_empty_dirs "${db_prefix}" # Set package as removed sed -i "s:^.|${pkg_name//./\\.}\$:r|${pkg_name}:" "${db_prefix}.db" msg_printf "${CgrnB}%s (%s)${Crst} "$"successfully removed.\n" "$pkg_name" "$version" log_printf "$pkgix_prefix" "%s (%s) "$"removed\n" "$pkg_name" "$version" unset_db_vars } #}}} #============================================================================ # cmd_install pkgix_prefix pkg_name... # cmd_install() { #{{{ local pkgix_prefix="$(abspath "$1")" shift if [[ -z "${1:-}" ]]; then die $"You need to specify at least one package to install.\n" fi if [[ ! -d "${pkgix_prefix}" ]]; then if ! mkdir -p "${pkgix_prefix}" 2> /dev/null; then die $"Cannot create directory '%s'!\n" "$pkgix_prefix" fi # dir exists now, normalize path just in case it contained `..`s pkgix_prefix="$(abspath "$pkgix_prefix")" fi lock_prefix "$pkgix_prefix" || exit $EXIT_ERR msg_printf $"Installing into %s: %s\n" "$pkgix_prefix" "$*" prompt_continue || exit $EXIT_USER_ABORT local pkg_name for pkg_name in "$@"; do pkg_install "$pkgix_prefix" "$pkg_name" "$arg_reinstall" done unlock_prefix "$pkgix_prefix" } #}}} #============================================================================ # prefix_upgrade pkgix_prefix # prefix_upgrade() { #{{{ local pkgix_prefix="$(abspath "$1")" shift if [[ ! -f "${pkgix_prefix%/}/${DB_PREFIX}.db" ]]; then warning_printf $"'%s' is not a valid prefix!\n" "$pkgix_prefix" return $RET_INVALID_OPTION fi msg_printf $"Upgrading %s ...\n" "$pkgix_prefix" prompt_continue || return $RET_USER_ABORT lock_prefix "$pkgix_prefix" || exit $EXIT_ERR db_list_installed "$pkgix_prefix" 1 | dep_topo_sort "$pkgix_prefix" | while IFS= read -r pkg_name; do # If package list provided, filter by that list if (( $# > 0 )); then in_list "$pkg_name" "$@" || continue fi ( old_version="$(db_info_query "$pkgix_prefix" "$pkg_name" "version")" # Source prefix RC-file, so that we could use PKGIX_UPGRADE_IGNORE # from this RC-file in source_pkg, if set. source_prefix_rc "$pkgix_prefix" if ! source_pkg "$pkg_name"; then warning_printf $"Cannot find package description file for '%s', skipping upgrade...\n" "$pkg_name" elif [[ "${version:-}" != "$old_version" ]] || (( arg_reinstall )); then if in_list "$pkg_name" "${PKGIX_UPGRADE_IGNORE[@]:+${PKGIX_UPGRADE_IGNORE[@]}}"; then msg_printf "${Cylw}"$"Skipping upgrade""${Crst} %s (%s -> %s)\n" "$pkg_name" "$old_version" "${version:-}" else msg_printf "${Ccyn}"$"Upgrading""${Crst} %s (%s -> %s)" "$pkg_name" "$old_version" "${version:-}" (( ! arg_reinstall )) && printf "\n" || printf " - "$"reinstall\n" (( ! arg_dry_run )) && log_printf "$pkgix_prefix" $"upgrading"" %s (%s -> %s)\n" "$pkg_name" "$old_version" "${version:-}" || : pkg_install "$pkgix_prefix" "$pkg_name" 1 fi else verbose_printf "${CcynB}=> %s (%s)${Crst} "$"is up to date\n" "$pkg_name" "$old_version" fi ) || exit $? # Break out of pipe-subshell done || { local ret_code=$? ; # Take return code if pipe-subshell exited with errors unlock_prefix "$pkgix_prefix" ; return $ret_code ; } msg_printf $"done upgrading.\n" unlock_prefix "$pkgix_prefix" return $RET_OK } #}}} #============================================================================ # cmd_upgrade pkgix_prefix... # cmd_upgrade() { #{{{ local pkgix_prefixes=() while [[ -n "${1:-}" && "$1" != "--" ]]; do pkgix_prefixes+=("$1") shift || break done shift || : local pkgix_prefix local ret_code for pkgix_prefix in "${pkgix_prefixes[@]}"; do prefix_upgrade "$pkgix_prefix" "$@" && ret_code=$? || ret_code=$? if (( ret_code != RET_OK && ret_code != RET_INVALID_OPTION && ret_code != RET_USER_ABORT )); then exit $EXIT_ERR fi done } #}}} #============================================================================ # cmd_remove pkgix_prefix pkg_name... # cmd_remove() { #{{{ local pkgix_prefix="$(abspath "$1")" shift || die $"No targets specified!\n" if [[ ! -d "$pkgix_prefix" ]]; then die $"'%s' is not a valid directory!\n" "$pkgix_prefix" fi lock_prefix "$pkgix_prefix" || exit $EXIT_ERR msg_printf $"Removing from %s: %s\n" "$pkgix_prefix" "$*" prompt_continue || exit $EXIT_USER_ABORT local pkg_name for pkg_name in "$@"; do pkg_remove "$pkgix_prefix" "$pkg_name" "$arg_purge" done unlock_prefix "$pkgix_prefix" } #}}} #============================================================================ # cmd_avail [pkg_name...] # cmd_avail() { #{{{ if (( $# == 0 )); then local repo for repo in "${pkgix_repos[@]:+${pkgix_repos[@]}}"; do (( ! arg_quiet )) && printf "%b" "${CbluB}-> ${CgrnB}${repo}${Crst}\n" 1>&2 || : case "$repo" in http://*|https://*|ftp://*) ( if ! fetch_remote "${repo}/$REMOTE_AVAIL_FILE" "-" 2> /dev/null; then warning_printf $"Fetching '%s' failed!\n" "${repo}/$REMOTE_AVAIL_FILE" fi ) | while IFS= read -r pkg_short_info; do if (( arg_quiet )); then echo "$repo ${pkg_short_info%% *}" else echo "$pkg_short_info" fi done ;; *) # Do not show hidden files and do not descend into hidden folders. # Need trailing slash, in case the repo is the path to a symlink. find_printbase "${repo}/" ! \( -name ".*" -prune \) -a \( -type f \) 2> /dev/null | sort | while IFS= read -r pkg_name; do if (( arg_quiet )); then echo "$repo $pkg_name" else if is_pkg_pointer "${repo}/$pkg_name"; then printf "%-42s %-18s %s\n" "$pkg_name" "->" "$(<"${repo}/$pkg_name")" else ( source "${repo}/$pkg_name" printf "%-42s %-18s %s\n" "$pkg_name" "${version:-}" "${description:-}" ) fi fi done ;; esac done else local pkg_name for pkg_name in "$@"; do ( if source_pkg "$pkg_name"; then echo "$pkg_name (${version:-}): ${repo}" echo " Description : ${description:-}" if [[ -z "${metapkg:-}" ]]; then echo " Website : ${website:-}" echo " License : ${license[*]:+${license[*]}}" echo " Depends : ${depends[*]:+${depends[*]}}" echo " Build deps. : ${builddepends[*]:+${builddepends[*]}}" echo " Check deps. : ${checkdepends[*]:+${checkdepends[*]}}" echo " Provides : ${provides[*]:+${provides[*]}}" else if [[ "$metapkg" != "$METAPKG_DUMMY" ]]; then echo " Metapackage -> $metapkg" fi fi else msg_printf $"'%s' not found!\n" "$pkg_name" fi echo ) done fi } #}}} #============================================================================ # cmd_list pkgix_prefix... # cmd_list() { #{{{ local pkgix_prefix for pkgix_prefix in "$@"; do [[ -d "$pkgix_prefix" ]] || continue && pkgix_prefix="$(abspath "$pkgix_prefix")" (( ! arg_quiet )) && printf "%b" "${CbluB}-> ${CgrnB}${pkgix_prefix}${Crst}\n" 1>&2 || : db_list_installed "$pkgix_prefix" | sort | while IFS= read -r pkg_name; do ( set_db_vars "$pkgix_prefix" "$pkg_name" || : if (( ! arg_quiet || arg_list_explicit || arg_list_depends || arg_list_unrequired )); then source "$db_info_file" fi if (( arg_list_explicit && ! arg_list_depends )); then [[ "${reason:-}" != "$REASON_EXPLICIT" ]] && continue || : fi if (( ! arg_list_explicit && arg_list_depends )); then [[ "${reason:-}" != "$REASON_DEPENDENCY" ]] && continue || : fi if (( arg_list_unrequired )); then ( # Check if any package in any provided pkgix_prefix # depends on the current package; if the current # package is required, continue while loop. Note that # builddepends/checkdepends is ignored, as this allows # to clean the system of packages which were installed # only for build/check. for _pkgix_prefix in "$@"; do [[ -d "$_pkgix_prefix" ]] || continue && _pkgix_prefix="$(abspath "$_pkgix_prefix")" db_list_installed "$_pkgix_prefix" | while IFS= read -r _pkg_name; do set_db_vars "$_pkgix_prefix" "$_pkg_name" || : # Tried using grep here, but the BASH-only # version is roughly 2x faster. source "$db_info_file" local dep_name_ver local dep_name for dep_name_ver in "${depends[@]:+${depends[@]}}"; do dep_name="${dep_name_ver%%[<>=]*}" set_db_vars "$pkgix_prefix" "$dep_name" || dep_name="$real_pkg_name" if [[ "$pkg_name" == "$dep_name" ]]; then verbose_printf $"'%s: %s' required by '%s: %s'\n" \ "$pkgix_prefix" "$pkg_name" "$_pkgix_prefix" "$_pkg_name" exit $EXIT_ERR fi done done || exit $? done ) || continue fi if (( arg_quiet )); then echo "$pkgix_prefix $pkg_name" else printf "%-42s %-18s %s\n" "${pkg_name}" "$(<$db_version_file)" "${description:-}" fi ) done done } #}}} #============================================================================ # cmd_show pkgix_prefix pkg_name... # cmd_show() { #{{{ local pkgix_prefixes=() if in_list "--" "$@"; then while [[ "$1" != "--" ]]; do pkgix_prefixes+=("$1") shift || break done else pkgix_prefixes+=("${1:-}") fi shift || die $"No targets specified!\n" local pkg_name local pkgix_prefix for pkg_name in "$@"; do for pkgix_prefix in "${pkgix_prefixes[@]}"; do pkgix_prefix="$(abspath "$pkgix_prefix")" if [[ ! -d "$pkgix_prefix" ]]; then die $"'%s' is not a valid directory!\n" "$pkgix_prefix" fi set_db_vars "$pkgix_prefix" "$pkg_name" || pkg_name="$real_pkg_name" if db_isinstalled "$pkgix_prefix" "$pkg_name"; then if (( arg_list_files )); then local prefix_line="" (( ! arg_quiet )) && prefix_line="$pkg_name " || : local path while IFS= read -r path; do [[ -n "$path" ]] && echo "${prefix_line}${pkgix_prefix%/}/${path}" || : done < "$db_files_file" else source "$db_info_file" echo "$pkg_name ($(<$db_version_file)): ${pkgix_prefix}" echo " Description : ${description:-}" echo " Website : ${website:-}" echo " License : ${license[*]:+${license[*]}}" echo " Depends : ${depends[*]:+${depends[*]}}" echo " Build deps. : ${builddepends[*]:+${builddepends[*]}}" echo " Check deps. : ${checkdepends[*]:+${checkdepends[*]}}" echo " Provides : ${provides[*]:+${provides[*]}}" echo " Install date : ${installdate:-}" echo " Reason : ${reason:-}" echo unset_db_info fi # found, stop searching further pkgix_prefixes break else # indicate not yet found, continue search db_prefix_pkg="" fi done if [[ -z "$db_prefix_pkg" ]]; then die $"'%s' not found!\n" "$pkg_name" fi unset_db_vars done } #}}} #============================================================================ # cmd_chenv pkgix_prefix... # cmd_chenv() { #{{{ local pkgix_prefix for pkgix_prefix in "$@"; do if ! export_env "$pkgix_prefix"; then die $"'%s' is not a valid directory!\n" "$pkgix_prefix" fi done msg_printf $"Spawning shell\n" exec "$PKGIX_SHELL" } #}}} #============================================================================ # sanitize_repos # Turn PKGIX_REPOS into pkgix_repos array; sanitize URLs. # sanitize_repos() { #{{{ local -a old_repos # Check if it is already an array (user set in RC-file) if [[ "$(declare -p PKGIX_REPOS)" =~ ^"declare -a" ]]; then old_repos=("${PKGIX_REPOS[@]:+${PKGIX_REPOS[@]}}") else IFS="$URL_SEP" read -ra old_repos <<< "$PKGIX_REPOS" fi pkgix_repos=() local repo for repo in "${old_repos[@]:+${old_repos[@]}}"; do [[ "$repo" =~ ^file:// ]] && repo="${repo##file://}" repo=${repo%/} pkgix_repos+=("${repo}") done } #}}} #============================================================================ # exit_cleanup # exit_cleanup() { #{{{ [[ -n "$__lock_held" ]] && unlock_prefix "$__lock_held" || : [[ -n "$__repo_cache_dir" && -d "$__repo_cache_dir" ]] && rm -rf "$__repo_cache_dir" || : [[ -n "$__dry_db_dir" && -d "$__dry_db_dir" ]] && rm -rf "$__dry_db_dir" || : trap - EXIT } #}}} #============================================================================ # trap_ERR errcode lineno command [traceback...] # trap_ERR() { #{{{ local errcode="$1" local lineno="$2" local command="$3" shift 3 local traceback="" [[ -n "$*" ]] && printf -v traceback "\n ${CredB}=>${Crst} in %s" "$@" error_printf "line %s - '%s' failed (code=%s)%s\n" "$lineno" "$command" "$errcode" "$traceback" exit 42 } #}}} #============================================================================ # reset_traps # Resets or initializes traps # reset_traps() { #{{{ trap '{ msg_printf $"TERM signal caught, exiting...\n"; exit 42; }' TERM HUP QUIT trap '{ msg_printf $"User aborted.\n"; exit 42; }' INT trap 'trap_ERR "$?" "$LINENO" "$BASH_COMMAND" "${FUNCNAME[@]:+${FUNCNAME[@]}}"' ERR trap 'exit_cleanup' EXIT } #}}} #============================================================================ # prog_version # prog_version() { #{{{ echo "$PROGNAME $PROG_VERSION" echo echo "Copyright (C) 2012-2016, Marco Elver " echo echo "This is free software; see the source for copying conditions." echo "There is NO WARRANTY, to the extent permitted by law." } #}}} #============================================================================ # prog_usage # Use *spaces* to indent printed text!! # prog_usage() { #{{{ if [[ -z "${1:-}" ]]; then printf -- $"Usage: %s [-r|--repo ] [-n|--noconfirm]\n\ [-v|--verbose] [--debug]\n\ []\n" "$PROGNAME" echo printf -- $"Commands available:\n" printf -- $" install Install a package in a prefix environment\n" printf -- $" remove Remove an installed package\n" printf -- $" upgrade Upgrade a prefix environment\n" printf -- $" avail List packages available to install\n" printf -- $" list List installed packages\n" printf -- $" show Show information about installed packages\n" printf -- $" showenv Show prefix environment parameters\n" printf -- $" chenv Switch to a prefix environment\n" # Generate plugin-commands compgen -c "${PROGNAME}-" | while IFS= read -r plugin_cmd; do printf -- " %-10s %s\n" "${plugin_cmd#${PROGNAME}-}" "$($plugin_cmd _summary)" done echo printf -- $"Options:\n" printf -- $"\ -r, --repo\n\ Specify additional package repository URLs; use '%s' to\n\ separate multiple repositories. Currently supported protocols are:\n\ file:// (default), http://, https://, ftp://\n" "$URL_SEP" printf -- $"\ -n, --noconfirm\n\ Proceed without asking the user for confirmation.\n" printf -- $"\ -v, --verbose\n\ Be more verbose.\n" printf -- $"\ -q, --quiet\n\ Be less verbose; some commands can show less information,\n\ which makes their output more easily parsable by other tools.\n" printf -- $"\ -d, --debug\n\ Debug mode: enable nounset for package files\n" printf -- $"\ -h, --help\n\ Display this help message.\n" printf -- $"\ -V, --version\n\ Display version information.\n" echo printf -- $"See '%s help ' for more information on a command.\n" "$PROGNAME" else local help_cmd="$(get_real_cmd "${1:-}")" case "$help_cmd" in install|upgrade) printf -- $"Usage: %s %s [-f|--force] [-d|--nodeps] [-r|--reinstall]\n\ [-k|--keep-build-dir] [--ignore-checksums]\n\ [--ignore-check] [--asdeps] [--asexplicit]\n" "$PROGNAME" "$help_cmd" if [[ "$help_cmd" == "install" ]]; then printf -- $" ...\n" else printf -- $" ... [-- ...]\n" fi echo printf -- $"Options:\n" printf -- $"\ -f, --force\n\ Force installation of packages that are: incompatible with the current\n\ system; conflicting with already installed files. Also forces\n\ overwriting files which were normally preserved (backup).\n" printf -- $"\ -d, --nodeps\n\ Ignore dependenciy check and do not install dependencies.\n" printf -- $"\ -r, --reinstall\n\ Reinstalls packages that are: already available outside of ;\n\ satisfied metapackages; already installed. When upgrading, all\n\ packages being upgraded will be reinstalled, even if up to date.\n" printf -- $"\ -k, --keep-build-dir\n\ Keeps the build directory.\n" printf -- $"\ -D, --dry\n\ Dry run only; do not install or upgrade anything.\n\ Useful to show what would be installed, including dependencies.\n" printf -- $"\ --ignore-checksums\n\ Ignores checksums for remotely fetched files.\n" printf -- $"\ --ignore-check\n\ Ignores the result of the package check; installs even if check failed.\n" printf -- $"\ --asdeps\n\ Set reason for all selected packages to installed as a dependency.\n" printf -- $"\ --asexplicit\n\ Set reason for all selected packages to explicitly installed.\n" ;; remove) printf -- $"Usage: %s %s [-P|--purge]\n" "$PROGNAME" "$help_cmd" echo printf -- $"Options:\n" printf -- $"\ -P, --purge\n\ Force removal of all installed files, even those marked for backup.\n" ;; show) printf -- $"Usage: %s %s [-l|--list] (... --|) ...\n" "$PROGNAME" "$help_cmd" echo printf -- $"Options:\n" printf -- $"\ -l, --list\n\ List installed package files.\n" ;; list) printf -- $"Usage: %s %s [-e|--explicit] [-d|--depends] [-u|--unrequired] ...\n" "$PROGNAME" "$help_cmd" echo printf -- $"Options:\n" printf -- $"\ -e, --explicit\n\ List only explicitly installed packages.\n" printf -- $"\ -d, --depends\n\ List only packages installed as a dependency for another package.\n" printf -- $"\ -u, --unrequired\n\ List unrequired packages: packages not required by any other package;\n\ checks dependencies between all listed .\n" ;; showenv|chenv) printf -- $"Usage: %s %s ...\n" "$PROGNAME" "$help_cmd" ;; avail) printf -- $"Usage: %s %s [...]\n" "$PROGNAME" "$help_cmd" echo printf -- $"\ If a list of is provided, displays detailed information about the\n\ packages found; otherwise, a short list of all packages in available\n\ repositories is displayed.\n" ;; help) printf -- $"Usage: %s %s \n" "$PROGNAME" "$help_cmd" ;; *) if type -p "${PROGNAME}-${help_cmd}" > /dev/null;then "${PROGNAME}-${help_cmd}" help else printf -- $"Please specify valid command.\n" exit $EXIT_INVALID_OPTION fi ;; esac fi } #}}} #============================================================================ # expand_short_opts opts arg # Sets global array expanded_short_opts # # Rewrites a block of short options into an array, which can then be used # to replace the current positional arguments with the expanded version # so that they can be parsed normally, while still allowing GNU-style # long options below. # expand_short_opts() { #{{{ OPTIND=1 expanded_short_opts=() local opts="$1" local arg="$2" local opt while getopts ":${opts}" opt "$arg"; do case "$opt" in :|\?) prog_usage "${cmd:-}" exit $EXIT_INVALID_OPTION ;; *) expanded_short_opts+=("-$opt") ;; esac done } #}}} #============================================================================ # run_cmd args.. # command wrapper # run_cmd() { #{{{ local cmd_prefix # Check if the command we run is overridden by a plugin if type -p "${PROGNAME}-plugin-${cmd}" > /dev/null; then PKGIX_PLUGIN="${cmd}" source "$(type -p "${PROGNAME}-plugin-${cmd}")" cmd_prefix=cmd_plugin else cmd_prefix=cmd fi case "$cmd" in install|remove) if [[ -z "${PKGIX_PREFIX:-}" ]]; then if [[ -z "${1:-}" ]]; then die $"You need to specify a prefix!\n" fi ${cmd_prefix}_$cmd "$@" else # Commands which can only accept one prefix, pass first one only # Remove leading ':' local args_prefix="${PKGIX_PREFIX##:}" ${cmd_prefix}_$cmd "${args_prefix%%:*}" "$@" fi ;; upgrade|list|show|showenv|chenv) if [[ -z "${PKGIX_PREFIX:-}" ]]; then case "$cmd" in # List of commands which are useful without a prefix showenv|chenv) ;; *) if [[ -z "${1:-}" ]]; then die $"You need to specify at least one prefix!\n" fi ;; esac ${cmd_prefix}_$cmd "$@" else local -a args_prefix IFS=":" read -ra args_prefix <<< "$PKGIX_PREFIX" case "$cmd" in show|upgrade) ${cmd_prefix}_$cmd "${args_prefix[@]}" -- "$@" ;; *) ${cmd_prefix}_$cmd "${args_prefix[@]}" "$@" ;; esac fi ;; avail) ${cmd_prefix}_$cmd "$@" ;; *) die $"Unknown command: %s\n" "$cmd" ;; esac } #}}} #============================================================================ # get_real_cmd cmd # Get real command name if alias is provided, else just returns $cmd. # get_real_cmd() { #{{{ local _cmd="$1" # Command aliases case "$_cmd" in in) echo "install" ;; up) echo "upgrade" ;; rm) echo "remove" ;; ls) echo "list" ;; *) echo "$_cmd" ;; esac } #}}} #============================================================================ # MAIN #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # {{{ reset_traps #============================================================================ # Get global options # arg_verbose=0 arg_quiet=0 arg_noconfirm=0 arg_debug=0 ## # Only used for some commands, but still needed by functions called # by commands not making use of these options. # arg_dry_run=0 # install, upgrade while :; do case "${1:-}" in -r|--repo) [[ -z "${2:-}" ]] && { prog_usage; exit $EXIT_INVALID_OPTION; } if [[ "$(declare -p PKGIX_REPOS)" =~ ^"declare -a" ]]; then # append PKGIX_REPOS+=("$2") else # prepend PKGIX_REPOS="${2}${URL_SEP}${PKGIX_REPOS}" fi shift ;; -v|--verbose) arg_verbose=1 ;; -q|--quiet) arg_quiet=1 ;; -n|--noconfirm) arg_noconfirm=1 ;; -d|--debug) arg_debug=1 ;; -h|--help) prog_usage; exit $EXIT_OK ;; -V|--version) prog_version; exit $EXIT_OK ;; --) shift; break ;; -*) expand_short_opts "rvqndhV" "$1" set -- "${expanded_short_opts[@]:+${expanded_short_opts[@]}}" "${@:2}" continue ;; *) break ;; esac shift done sanitize_repos cmd="$(get_real_cmd "${1:-}")" shift || : #============================================================================ # Get command specific options case "$cmd" in install|upgrade) arg_force=0 arg_nodeps=0 arg_reinstall=0 arg_keep_build_dir=0 arg_ignore_checksums=0 arg_ignore_check=0 arg_reason_dependency=0 arg_reason_explicit=0 while :; do case "${1:-}" in -f|--force) arg_force=1 ;; -d|--nodeps) arg_nodeps=1 ;; -r|--reinstall) arg_reinstall=1 ;; -k|--keep-build-dir) arg_keep_build_dir=1 ;; -D|--dry) arg_dry_run=1 ;; --ignore-checksums) arg_ignore_checksums=1 ;; --ignore-check) arg_ignore_check=1 ;; --asdeps) arg_reason_dependency=1 ;; --asexplicit) arg_reason_explicit=1 ;; --) shift; break ;; -*) expand_short_opts "fdrkD" "$1" set -- "${expanded_short_opts[@]:+${expanded_short_opts[@]}}" "${@:2}" continue ;; *) break ;; esac shift done run_cmd "$@" ;; remove) arg_purge=0 while :; do case "${1:-}" in -P|--purge) arg_purge=1 ;; --) shift; break ;; -*) expand_short_opts "P" "$1" set -- "${expanded_short_opts[@]:+${expanded_short_opts[@]}}" "${@:2}" continue ;; *) break ;; esac shift done run_cmd "$@" ;; show) arg_list_files=0 while :; do case "${1:-}" in -l|--list) arg_list_files=1 ;; --) shift; break ;; -*) expand_short_opts "l" "$1" set -- "${expanded_short_opts[@]:+${expanded_short_opts[@]}}" "${@:2}" continue ;; *) break ;; esac shift done run_cmd "$@" ;; list) arg_list_explicit=0 arg_list_depends=0 arg_list_unrequired=0 while :; do case "${1:-}" in -e|--explicit) arg_list_explicit=1 ;; -d|--depends) arg_list_depends=1 ;; -u|--unrequired) arg_list_unrequired=1 ;; --) shift; break ;; -*) expand_short_opts "edu" "$1" set -- "${expanded_short_opts[@]:+${expanded_short_opts[@]}}" "${@:2}" continue ;; *) break ;; esac shift done run_cmd "$@" ;; showenv|chenv|avail) run_cmd "$@" ;; help) prog_usage "$@" ;; *) # Check if this is a plugin if type -p "${PROGNAME}-${cmd}" > /dev/null; then PKGIX_PLUGIN="${cmd}" if "${PROGNAME}-${cmd}" _source; then source "$(type -p "${PROGNAME}-${cmd}")" else export PKGIX_PLUGIN "${PROGNAME}-${cmd}" "$@" || exit $? fi else prog_usage exit $EXIT_INVALID_OPTION fi ;; esac exit $EXIT_OK # }}} # vim: set noet foldmarker={{{,}}} foldlevel=0 fen fdm=marker :