diff options
Diffstat (limited to 'src/cpt-lib.in')
-rw-r--r-- | src/cpt-lib.in | 313 |
1 files changed, 216 insertions, 97 deletions
diff --git a/src/cpt-lib.in b/src/cpt-lib.in index 23a75ec..27d57e4 100644 --- a/src/cpt-lib.in +++ b/src/cpt-lib.in @@ -9,7 +9,7 @@ # Currently maintained by Cem Keylan. version() { - out "Carbs Packaging Tools, version @VERSION@" \ + out "Carbs Packaging Tools, version $cpt_version" \ @LICENSE@ exit 0 @@ -25,11 +25,12 @@ log() { # # All messages are printed to stderr to allow the user to hide build # output which is the only thing printed to stdout. - # - # '${3:-->}': If the 3rd argument is missing, set prefix to '->'. - # '${2:+colorb}': If the 2nd argument exists, set text style of '$1'. - printf '%b%s %b%b%s%b %s\n' \ - "$colory" "${3:-->}" "$colre" "${2:+$colorb}" "$1" "$colre" "$2" >&2 + case $# in + 1) printf '%b->%b %s\n' "$colory" "$colre" "$1" ;; + 2) printf '%b->%b %b%s%b %s\n' "$colory" "$colre" "$colorb" "$1" "$colre" "$2" ;; + 3) printf '%b%s%b %b%s%b %s\n' "$colory" "${3:-->}" "$colre" "$colorb" "$1" "$colre" "$2" ;; + *) return 1 + esac >&2 } warn() { @@ -75,6 +76,47 @@ colors_enabled() { esac } +_dep_append() { + dep_graph=$(printf '%s\n%s %s\n' "$dep_graph" "$@" ;) +} + +_tsort() { + # Return a linear reverse topological sort of the piped input, so we + # generate a proper build order. Returns 1 if a dependency cycle occurs. + # + # I was really excited when I saw POSIX specified a tsort(1) implementation, + # but the specification is quite vague, it doesn't specify cycles as a + # reason of error, and implementations differ on how it's handled. coreutils + # tsort(1) exits with an error, while openbsd tsort(1) doesn't. Both + # implementations are correct according to the specification. This leaves us + # with the following awk script, because the POSIX shell is not up for the + # job without super ugly hacks. + awk 'function fv(s) { + for (sp in e) { + split (e[sp],t) + for (j in t) if (s == t[j]) return 0 + } return 1 + } + function el(_l) {for(i in e){_l=_l" "i;}; return _l;} + function ce(t) {if (!(t in e)) e[t]="";} + function err(s) {print "Dependency cycle deteced between: " s; exit 1;} + {ce($1);$1!=$2&&e[$1]=e[$1]" "$2;} + END { + do {p=el() + for (s in e) { + if (fv(s)) { + pr=s" "pr + split(e[s],t) + for(i in t){ce(t[i]);} + delete e[s] + } + } c=el() + } while (p != c) + if (length(p)!=0) err(p); + print pr + }' +} + trap_set() { # Function to set the trap value. case ${1:-cleanup} in @@ -90,20 +132,16 @@ trap_set() { esac } -sepchar() ( +sepchar() { # Seperate every character on the given string without resorting to external # processes. [ "$1" ] || return 0; str=$1; set -- while [ "$str" ]; do - str_tmp=$str - for i in $(_seq $(( ${#str} - 1 ))); do - str_tmp=${str_tmp%?} - done - set -- "$@" "$str_tmp" - str=${str#"$str_tmp"} + set -- "$@" "${str%"${str#?}"}" + str=${str#?} done printf '%s\n' "$@" -) +} _re() { # Check that the string supplied in $2 conforms to the regular expression @@ -510,6 +548,9 @@ as_root() { # We are exporting package manager variables, so that we still have the # same repository paths / access to the same cache directories etc. + # + # It doesn't matter whether CPT_HOOK is defined or not. + # shellcheck disable=2153 set -- HOME="$HOME" \ USER="$user" \ XDG_CACHE_HOME="$XDG_CACHE_HOME" \ @@ -551,23 +592,26 @@ pop() { } run_hook() { - # Store the CPT_HOOK variable so that we can revert it if it is changed. - oldCPT_HOOK=$CPT_HOOK - - # If a fourth parameter 'root' is specified, source the hook from a - # predefined location to avoid privilige escalation through user scripts. - [ "$4" ] && CPT_HOOK=$CPT_ROOT/etc/cpt-hook - - [ -f "$CPT_HOOK" ] || { CPT_HOOK=$oldCPT_HOOK; return 0 ;} - - if [ "$2" ]; then - logv "$2" "Running $1 hook" - else - logv "Running $1 hook" - fi + # Check that hooks exist before announcing that we are running a hook. + set +f + for hook in "$cpt_confdir/hooks/"* "$CPT_HOOK"; do + [ -f "$hook" ] && { + if [ "$2" ]; then + logv "$2" "Running $1 hook" + else + logv "Running $1 hook" + fi + break + } + done - TYPE=${1:-null} PKG=${2:-null} DEST=${3:-null} . "$CPT_HOOK" - CPT_HOOK=$oldCPT_HOOK + # Run all the hooks found in the configuration directory, and the user + # defined hook. + for hook in "$cpt_confdir/hooks/"* "$CPT_HOOK"; do + set -f + [ -f "$hook" ] || continue + TYPE=${1:-null} PKG=${2:-null} DEST=${3:-null} . "$hook" + done } # An optional argument could be provided to enforce a compression algorithm. @@ -844,12 +888,9 @@ pkg_depends() { # Resolve all dependencies and generate an ordered list. # This does a depth-first search. The deepest dependencies are # listed first and then the parents in reverse order. - contains "$deps" "$1" || { - # Filter out non-explicit, aleady installed dependencies. - # Only filter installed if called from 'pkg_build()'. - [ "$pkg_build" ] && [ -z "$2" ] && - (pkg_list "$1" >/dev/null) && return - + contains "$pkgs" "$1" || { + pkgs="$pkgs $1 " + [ "$2" = raw ] && _dep_append "$1" "$1" while read -r dep type || [ "$dep" ]; do # Skip comments and empty lines. [ "${dep##\#*}" ] || continue @@ -862,6 +903,16 @@ pkg_depends() { make) [ "$2" = tree ] && [ -z "${3#first-nomake}" ] && continue esac + # Filter out non-explicit, already installed dependencies if called + # from 'pkg_build()'. + [ "$pkg_build" ] && (pkg_list "$dep" >/dev/null) && continue + + if [ "$2" = explicit ] || [ "$3" ]; then + _dep_append "$dep" "$dep" + else + _dep_append "$1" "$dep" + fi + # Recurse through the dependencies of the child packages. Forward # the 'tree' operation. if [ "$2" = tree ]; then @@ -871,12 +922,14 @@ pkg_depends() { fi done 2>/dev/null < "$(pkg_find "$1")/depends" ||: - # After child dependencies are added to the list, - # add the package which depends on them. - [ "$2" = explicit ] || [ "$3" ] || deps="$deps $1 " } } +pkg_depends_commit() { + # Set deps, and cleanup dep_graph, pkgs + deps=$(printf '%s\n' "$dep_graph" | _tsort) dep_graph='' pkgs='' || warn "Dependency cycle detected" +} + pkg_order() { # Order a list of packages based on dependence and # take into account pre-built tarballs if this is @@ -884,9 +937,10 @@ pkg_order() { order=; redro=; deps= for pkg do case $pkg in - *.tar.*) deps="$deps $pkg " ;; + *.tar.*) _dep_append "$pkg" "$pkg" ;; *) pkg_depends "$pkg" raw esac done + pkg_depends_commit # Filter the list, only keeping explicit packages. # The purpose of these two loops is to order the @@ -945,11 +999,10 @@ pkg_fix_deps() { # simplify path building. cd "$pkg_dir/$1/$pkg_db/$1" - # Make a copy of the depends file if it exists to have a - # reference to 'diff' against. + # Make a copy of the depends file if it exists to have a reference to 'diff' + # against. if [ -f depends ]; then - cp -f depends "$mak_dir/d" - dep_file=$mak_dir/d + dep_file=$(_tmp_cp depends) else dep_file=/dev/null fi @@ -1087,6 +1140,7 @@ pkg_build() { # separately from those detected as dependencies. explicit="$explicit $pkg " } done + pkg_depends_commit [ "$pkg_update" ] || explicit_build=$explicit @@ -1321,6 +1375,9 @@ pkg_conflicts() { # Check to see if a package conflicts with another. log "$1" "Checking for package conflicts" + c_manifest=$(_tmp_create conflict-manifest) + c_conflicts=$(_tmp_create conflicts) + # Filter the tarball's manifest and select only files # and any files they resolve to on the filesystem # (/bin/ls -> /usr/bin/ls). @@ -1342,7 +1399,7 @@ pkg_conflicts() { # temporary manifest to be parsed. printf '%s/%s\n' "${dirname#"$CPT_ROOT"}" "${file##*/}" - done < "$tar_dir/$1/$pkg_db/$1/manifest" > "$CPT_TMPDIR/$pid/manifest" + done < "$tar_dir/$1/$pkg_db/$1/manifest" > "$c_manifest" p_name=$1 @@ -1351,7 +1408,7 @@ pkg_conflicts() { # shellcheck disable=2046,2086 set -- $(set +f; pop "$sys_db/$p_name/manifest" from "$sys_db"/*/manifest) - [ -s "$CPT_TMPDIR/$pid/manifest" ] || return 0 + [ -s "$c_manifest" ] || return 0 # In rare cases where the system only has one package installed # and you are reinstalling that package, grep will try to read from @@ -1367,12 +1424,12 @@ pkg_conflicts() { # Store the list of found conflicts in a file as we will be using the # information multiple times. Storing it in the cache dir allows us # to be lazy as they'll be automatically removed on script end. - sed '/\/$/d' "$@" | sort "$CPT_TMPDIR/$pid/manifest" - | uniq -d > "$CPT_TMPDIR/$pid/conflict" ||: + sed '/\/$/d' "$@" | sort "$c_manifest" - | uniq -d > "$c_conflicts" ||: # Enable alternatives automatically if it is safe to do so. # This checks to see that the package that is about to be installed # doesn't overwrite anything it shouldn't in '/var/db/cpt/installed'. - "$grep" -q "/var/db/cpt/installed/" "$CPT_TMPDIR/$pid/conflict" || + "$grep" -q "/var/db/cpt/installed/" "$c_conflicts" || choice_auto=1 # Use 'grep' to list matching lines between the to @@ -1423,13 +1480,13 @@ pkg_conflicts() { log "this must be fixed in $p_name. Contact the maintainer" die "by checking 'git log' or by running 'cpt-maintainer'" } - done < "$CPT_TMPDIR/$pid/conflict" + done < "$c_conflicts" # Rewrite the package's manifest to update its location # to its new spot (and name) in the choices directory. pkg_manifest "$p_name" "$tar_dir" 2>/dev/null - elif [ -s "$CPT_TMPDIR/$pid/conflict" ]; then + elif [ -s "$c_conflicts" ]; then log "Package '$p_name' conflicts with another package" "" "!>" log "Run 'CPT_CHOICE=1 cpt i $p_name' to add conflicts" "" "!>" die "as alternatives." @@ -1491,13 +1548,13 @@ pkg_etc() { mkdir -p "$CPT_ROOT/$dir" done - digest=$(_get_digest "$mak_dir/c") || digest=b3sum + digest=$(_get_digest "$_etcsums") || digest=b3sum # Handle files in /etc/ based on a 3-way checksum check. find etc ! -type d | while read -r file; do { sum_new=$("$digest" "$file") sum_sys=$(cd "$CPT_ROOT/"; "$digest" "$file") - sum_old=$("$grep" "$file$" "$mak_dir/c"); } 2>/dev/null ||: + sum_old=$("$grep" "$file$" "$_etcsums"); } 2>/dev/null ||: logv "$pkg_name" "Doing 3-way handshake for $file" outv "Previous: ${sum_old:-null}" @@ -1562,10 +1619,11 @@ pkg_remove() { # remove anything from packages that create empty directories for a # purpose (such as baselayout). manifest_list="$(set +f; pop "$sys_db/$1/manifest" from "$sys_db/"*/manifest)" + dirs="$(_tmp_name "directories")" # shellcheck disable=2086 - [ "$manifest_list" ] && grep -h '/$' $manifest_list | sort -ur > "$mak_dir/dirs" + [ "$manifest_list" ] && grep -h '/$' $manifest_list | sort -ur > "$dirs" - run_hook pre-remove "$1" "$sys_db/$1" root + run_hook pre-remove "$1" "$sys_db/$1" while read -r file; do # The file is in '/etc' skip it. This prevents the package @@ -1573,7 +1631,7 @@ pkg_remove() { [ "${file##/etc/*}" ] || continue if [ -d "$CPT_ROOT/$file" ]; then - "$grep" -Fxq "$file" "$mak_dir/dirs" 2>/dev/null && continue + "$grep" -Fxq "$file" "$dirs" 2>/dev/null && continue rmdir "$CPT_ROOT/$file" 2>/dev/null || continue else rm -f "$CPT_ROOT/$file" @@ -1584,7 +1642,7 @@ pkg_remove() { # we no longer need to block 'Ctrl+C'. trap_set cleanup - run_hook post-remove "$1" "$CPT_ROOT/" root + run_hook post-remove "$1" "$CPT_ROOT/" log "$1" "Removed successfully" } @@ -1647,7 +1705,7 @@ pkg_install() { [ "$install_dep" ] && die "$1" "Package requires ${install_dep%, }" - run_hook pre-install "$pkg_name" "$tar_dir/$pkg_name" root + run_hook pre-install "$pkg_name" "$tar_dir/$pkg_name" pkg_conflicts "$pkg_name" log "$pkg_name" "Installing package incrementally" @@ -1659,8 +1717,8 @@ pkg_install() { # If the package is already installed (and this is an upgrade) make a # backup of the manifest and etcsums files. - cp -f "$sys_db/$pkg_name/manifest" "$mak_dir/m" 2>/dev/null ||: - cp -f "$sys_db/$pkg_name/etcsums" "$mak_dir/c" 2>/dev/null ||: + _manifest=$(_tmp_cp "$sys_db/$pkg_name/manifest" 2>/dev/null) ||: + _etcsums=$(_tmp_cp "$sys_db/$pkg_name/etcsums" 2>/dev/null) ||: # This is repeated multiple times. Better to make it a function. pkg_rsync() { @@ -1675,7 +1733,7 @@ pkg_install() { pkg_etc # Remove any leftover files if this is an upgrade. - "$grep" -vFxf "$sys_db/$pkg_name/manifest" "$mak_dir/m" 2>/dev/null | + "$grep" -vFxf "$sys_db/$pkg_name/manifest" "$_manifest" 2>/dev/null | while read -r file; do file=$CPT_ROOT/$file @@ -1712,7 +1770,7 @@ pkg_install() { "$sys_db/$pkg_name/post-install" ||: fi - run_hook post-install "$pkg_name" "$sys_db/$pkg_name" root + run_hook post-install "$pkg_name" "$sys_db/$pkg_name" log "$pkg_name" "Installed successfully" } @@ -1947,7 +2005,12 @@ pkg_updates(){ # an update. [ "$CPT_FETCH" = 0 ] || pkg_fetch - log "Checking for new package versions" + # Be quiet if we are doing self update, no need to print the same + # information twice. We add this basic function, because we will be using it + # more than once. + _not_update () { [ "$cpt_self_update" ] || "$@" ;} + + _not_update log "Checking for new package versions" set +f @@ -1961,7 +2024,7 @@ pkg_updates(){ # Compare installed packages to repository packages. [ "$db_ver-$db_rel" != "$re_ver-$re_rel" ] && { - printf '%s\n' "$pkg_name $db_ver-$db_rel ==> $re_ver-$re_rel" + _not_update printf '%s\n' "$pkg_name $db_ver-$db_rel ==> $re_ver-$re_rel" outdated="$outdated$pkg_name " } done @@ -1982,6 +2045,13 @@ pkg_updates(){ exit 0 } + [ "$outdated" ] || { + log "Everything is up to date" + return + } + + _not_update log "Packages to update: ${outdated% }" + contains "$outdated" cpt && { log "Detected package manager update" log "The package manager will be updated first" @@ -1992,18 +2062,17 @@ pkg_updates(){ cpt-install cpt log "Updated the package manager" - log "Re-run 'cpt update' to update your system" - - exit 0 + log "Re-executing the package manager to continue the update" + + # We export this variable so that cpt knows it's running for the second + # time. We make the new process promptless, and we avoid fetching + # repositories. We are assuming that the user was already prompted once, + # and that their repositories are up to date, or they have also passed + # the '-y' or '-n' flags themselves which leads to the same outcome. + export cpt_self_update=1 + exec cpt-update -yn } - [ "$outdated" ] || { - log "Everything is up to date" - return - } - - log "Packages to update: ${outdated% }" - # Tell 'pkg_build' to always prompt before build. pkg_update=1 @@ -2019,12 +2088,12 @@ pkg_updates(){ } pkg_get_base() ( - # Print the packages defined in the /etc/cpt-base file. + # Print the packages defined in the CPT base file. # If an argument is given, it prints a space seperated list instead # of a list seperated by newlines. - # cpt-base is an optional file, return with success if it doesn't exist. - [ -f "$CPT_ROOT/etc/cpt-base" ] || return 0 + # CPT base is an optional file, return with success if it doesn't exist. + [ -f "$cpt_base" ] || return 0 # If there is an argument, change the format to use spaces instead of # newlines. @@ -2035,13 +2104,20 @@ pkg_get_base() ( # subshell. That is our purpose here, thank you very much. # shellcheck disable=SC2030 while read -r pkgname _; do + # Ignore comments [ "${pkgname##\#*}" ] || continue + + # Store the package list in arguments set -- "$@" "$pkgname" + + # Retrieve the dependency tree of the package, so they are listed as + # base packages too. This ensures that no packages are broken in a + # "base reset", and the user has a working base. deps=$(pkg_gentree "$pkgname" xn) for dep in $deps; do contains "$*" "$dep" || set -- "$@" "$dep" done - done < "$CPT_ROOT/etc/cpt-base" + done < "$cpt_base" # Format variable is intentional. # shellcheck disable=2059 @@ -2116,12 +2192,36 @@ pkg_clean() { rm -rf -- "${CPT_TMPDIR:=$cac_dir/proc}/$pid" } +_tmp_name() { + # Name a temporary file/directory + out "$tmp_dir/$1" +} + +_tmp_cp() { + # Copy given file to the temporary directory and return its name. If a + # second argument is not given, use the basename of the copied file. + _ret=${2:-${1##*/}} + _ret=$(_tmp_name "$_ret") + cp "$1" "$_ret" + out "$_ret" +} + +_tmp_create() { + # Create given file to the temporary directory and return its name + create_tmp + _ret=$(_tmp_name "$1") + # False positive, we are not reading from the file. + # shellcheck disable=2094 + out "$_ret" 3>> "$_ret" +} + create_tmp() { # Create the required temporary directories and set the variables which # point to them. - mkdir -p "${mak_dir:=$tmp_dir/build}" \ - "${pkg_dir:=$tmp_dir/pkg}" \ - "${tar_dir:=$tmp_dir/export}" + mak_dir=$tmp_dir/build + pkg_dir=$tmp_dir/pkg + tar_dir=$tmp_dir/export + mkdir -p "$mak_dir" "$pkg_dir" "$tar_dir" } create_cache() { @@ -2135,6 +2235,9 @@ create_cache() { { set -ef + # Package manager version. + cpt_version=@VERSION@ + # If a parser definition exists, let's run it ourselves. This makes sure we # get the variables as soon as possible. command -v parser_definition >/dev/null && { @@ -2149,25 +2252,34 @@ create_cache() { # that it doesn't change beneath us. pid=${CPT_PID:-$$} - # Create the cache directories for CPT and set the variables which point - # to them. This is seperate from temporary directories created in - # create_cache(). That's because we need these variables set on most - # occasions. - # # A temporary directory can be specified apart from the cache directory in # order to build in a user specified directory. /tmp could be used in order - # to build on ram, useful on SSDs. The user can specify CPT_TMPDIR for this. - # We create the temporary directory here to avoid permission issues that can - # arise from functions that call as_root(). - mkdir -p "${cac_dir:=${CPT_CACHE:=${XDG_CACHE_HOME:-$HOME/.cache}/cpt}}" \ - "${CPT_TMPDIR:=$cac_dir/proc}" \ - "${src_dir:=$cac_dir/sources}" \ - "${log_dir:=$cac_dir/logs}" \ - "${bin_dir:=$cac_dir/bin}" - - # We don't create the temporary $pid directory until `create_tmp()` is - # called, but we still declare its variable here. - : "${tmp_dir:=${CPT_TMPDIR:=$cac_dir/proc}/$pid}" + # to build on ram, useful on SSDs. The user can specify $CPT_TMPDIR for + # this. We now also support the usage of $XDG_RUNTIME_DIR, so the directory + # naming can be confusing to some. Here are possible $tdir names (by order + # of preference): + # + # 1. $CPT_TMPDIR + # 2. $XDG_RUNTIME_DIR/cpt + # 3. $XDG_CACHE_DIR/cpt/proc + # 4. $HOME/.cache/cpt/proc + # + # We create the main temporary directory here to avoid permission issues + # that can arise from functions that call as_root(). However, the + # $pid directories are special for each process and aren't created unless + # `create_tmp()` is used. + # + # We used to assign and create the directories at the same time using a + # shell hack, but it made the variables editable outside of the package + # manager, but we don't actually want that. Variables that are lower case + # aren't meant to be interacted or set by the user. + cac_dir=${CPT_CACHE:=${XDG_CACHE_HOME:-${HOME:?}/.cache}}/cpt + src_dir=$cac_dir/sources + log_dir=$cac_dir/logs + bin_dir=$cac_dir/bin + tdir=${CPT_TMPDIR:=${XDG_RUNTIME_DIR:-$cac_dir/proc}${XDG_RUNTIME_DIR:+/cpt}} + tmp_dir=$tdir/$pid + mkdir -p "$cac_dir" "$src_dir" "$log_dir" "$bin_dir" "$tdir" # Set the location to the repository and package database. pkg_db=var/db/cpt/installed @@ -2233,6 +2345,13 @@ create_cache() { # the get go. It will be created as needed by package installation. sys_db=$CPT_ROOT/$pkg_db + # CPT system configuration directory + cpt_confdir=$CPT_ROOT@SYSCONFDIR@/cpt + + # Backwards compatibility for the old cpt-base location + cpt_base=$CPT_ROOT/etc/cpt-base + [ -f "$cpt_confdir/base" ] && cpt_base=$cpt_confdir/base + # Regular expression used in pkg_checksums() and pkg_sources() in order to # identify VCS and comments re_vcs_or_com='^(#|(fossil|git|hg)\+)' |