diff --git a/repo-add.sh b/repo-add.sh new file mode 100755 index 0000000000..2b66a2e508 --- /dev/null +++ b/repo-add.sh @@ -0,0 +1,690 @@ +#!/bin/bash +# +# repo-add.sh - add a package to a given repo database file +# repo-remove.sh - remove a package entry from a given repo database file +# +# Copyright (c) 2006-2021 Pacman Development Team +# +# 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 2 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 . + +shopt -s extglob + +# gettext initialization +export TEXTDOMAIN='pacman-scripts' +export TEXTDOMAINDIR='/usr/share/locale' + +declare -r myver='6.0.0' +declare -r confdir='/etc' + +QUIET=0 +ONLYADDNEW=0 +RMEXISTING=0 +SIGN=0 +KEY=0 +VERIFY=0 +REPO_DB_FILE= +REPO_DB_PREFIX= +REPO_DB_SUFFIX= +LOCKFILE= +CLEAN_LOCK=0 +USE_COLOR='y' +PREVENT_DOWNGRADE=0 + +# Import libmakepkg +source scripts/db/compress.sh +source scripts/db/message.sh + +# ensure we have a sane umask set +umask 0022 + +# print usage instructions +usage() { + cmd=${0##*/} + printf -- "%s (pacman) %s\n\n" "$cmd" "$myver" + if [[ $cmd == "repo-add.sh" ]] ; then + printf -- "$(gettext "Usage: repo-add [options] ...\n")" + printf -- "\n" + printf -- "$(gettext "\ +repo-add will update a package database by reading a package file.\n\ +Multiple packages to add can be specified on the command line.\n")" + printf -- "\n" + printf -- "$(gettext "Options:\n")" + printf -- "$(gettext " -n, --new only add packages that are not already in the database\n")" + printf -- "$(gettext " -R, --remove remove old package file from disk after updating database\n")" + printf -- "$(gettext " -p, --prevent-downgrade do not add package to database if a newer version is already present\n")" + elif [[ $cmd == "repo-remove.sh" ]] ; then + printf -- "$(gettext "Usage: repo-remove [options] ...\n")" + printf -- "\n" + printf -- "$(gettext "\ +repo-remove will update a package database by removing the package name\n\ +specified on the command line from the given repo database. Multiple\n\ +packages to remove can be specified on the command line.\n")" + printf -- "\n" + printf -- "$(gettext "Options:\n")" + else + printf -- "$(gettext "Please move along, there is nothing to see here.\n")" + return + fi + printf -- "$(gettext " --nocolor turn off color in output\n")" + printf -- "$(gettext " -q, --quiet minimize output\n")" + printf -- "$(gettext " -s, --sign sign database with GnuPG after update\n")" + printf -- "$(gettext " -k, --key use the specified key to sign the database\n")" + printf -- "$(gettext " -v, --verify verify database's signature before update\n")" + printf -- "$(gettext "\n\ +See %s(8) for more details and descriptions of the available options.\n")" $cmd + printf "\n" + if [[ $cmd == "repo-add.sh" ]] ; then + printf -- "$(gettext "Example: repo-add.sh /path/to/repo.db.tar.gz pacman-3.0.0-1-i686.pkg.tar.gz\n")" + elif [[ $cmd == "repo-remove.sh" ]] ; then + printf -- "$(gettext "Example: repo-remove.sh /path/to/repo.db.tar.gz kernel26\n")" + fi +} + +version() { + cmd=${0##*/} + printf "%s (pacman) %s\n\n" "$cmd" "$myver" + printf -- "Copyright (c) 2006-2021 Pacman Development Team .\n" + printf '\n' + printf -- "$(gettext "\ +This is free software; see the source for copying conditions.\n\ +There is NO WARRANTY, to the extent permitted by law.\n")" +} + + +# format a metadata entry +# arg1 - Entry name +# ... - value(s) +format_entry() { + local field=$1; shift + + if [[ $1 ]]; then + printf '%%%s%%\n' "$field" + printf '%s\n' "$@" + printf '\n' + fi +} + +find_pkgentry() { + local pkgname=$1 + local pkgentry + + for pkgentry in "$tmpdir/db/$pkgname"*; do + name=${pkgentry##*/} + if [[ ${name%-*-*} = "$pkgname" ]]; then + echo $pkgentry + return 0 + fi + done + return 1 +} + +check_gpg() { + if ! type -p gpg >/dev/null; then + error "$(gettext "Cannot find the gpg binary! Is GnuPG installed?")" + exit 1 # $E_MISSING_PROGRAM + fi + + if (( ! VERIFY )); then + if ! gpg --list-secret-key ${GPGKEY:+"$GPGKEY"} &>/dev/null; then + if [[ ! -z $GPGKEY ]]; then + error "$(gettext "The key ${GPGKEY} does not exist in your keyring.")" + elif (( ! KEY )); then + error "$(gettext "There is no key in your keyring.")" + fi + exit 1 + fi + fi +} + +# sign the package database once repackaged +create_signature() { + (( ! SIGN )) && return + local dbfile=$1 + local ret=0 + msg "$(gettext "Signing database '%s'...")" "${dbfile##*/.tmp.}" + + local SIGNWITHKEY=() + if [[ -n $GPGKEY ]]; then + SIGNWITHKEY=(-u "${GPGKEY}") + fi + gpg --detach-sign --use-agent --no-armor "${SIGNWITHKEY[@]}" "$dbfile" &>/dev/null || ret=$? + + if (( ! ret )); then + msg2 "$(gettext "Created signature file '%s'")" "${dbfile##*/.tmp.}.sig" + else + warning "$(gettext "Failed to sign package database file '%s'")" "${dbfile##*/.tmp.}" + fi +} + +# verify the existing package database signature +verify_signature() { + (( ! VERIFY )) && return + local dbfile=$1 + local ret=0 + msg "$(gettext "Verifying database signature...")" + + if [[ ! -f $dbfile.sig ]]; then + warning "$(gettext "No existing signature found, skipping verification.")" + return + fi + gpg --verify "$dbfile.sig" || ret=$? + if (( ! ret )); then + msg2 "$(gettext "Database signature file verified.")" + else + error "$(gettext "Database signature was NOT valid!")" + exit 1 + fi +} + +verify_repo_extension() { + local junk=() + if [[ $1 = *.db.tar* ]] && get_compression_command "$1" junk; then + return 0 + fi + + error "$(gettext "'%s' does not have a valid database archive extension.")" "$1" + exit 1 +} + +# write an entry to the pacman database +# arg1 - path to package +db_write_entry() { + # blank out all variables + local pkgfile=$1 + local -a _groups _licenses _replaces _depends _conflicts _provides \ + _optdepends _makedepends _checkdepends + local pkgname pkgbase pkgver pkgdesc csize size url arch builddate packager \ + md5sum sha256sum pgpsig pgpsigsize + + # read info from the zipped package + local line var val + while read -r line; do + [[ ${line:0:1} = '#' ]] && continue + IFS=' =' read -r var val < <(printf '%s\n' "$line") + + # normalize whitespace with an extglob + declare "$var=${val//+([[:space:]])/ }" + case $var in + group) _groups+=("$group") ;; + license) _licenses+=("$license") ;; + replaces) _replaces+=("$replaces") ;; + depend) _depends+=("$depend") ;; + conflict) _conflicts+=("$conflict") ;; + provides) _provides+=("$provides") ;; + optdepend) _optdepends+=("$optdepend") ;; + makedepend) _makedepends+=("$makedepend") ;; + checkdepend) _checkdepends+=("$checkdepend") ;; + esac + done< <(bsdtar -xOqf "$pkgfile" .PKGINFO) + + # ensure $pkgname and $pkgver variables were found + if [[ -z $pkgname || -z $pkgver ]]; then + error "$(gettext "Invalid package file '%s'.")" "$pkgfile" + return 1 + fi + + if [[ -d $tmpdir/db/$pkgname-$pkgver ]]; then + warning "$(gettext "An entry for '%s' already existed")" "$pkgname-$pkgver" + if (( ONLYADDNEW )); then + return 0 + fi + else + pkgentry=$(find_pkgentry "$pkgname") + if [[ -n $pkgentry ]]; then + + local version=$(sed -n '/^%VERSION%$/ {n;p;q}' "$pkgentry/desc") + if (( $(vercmp "$version" "$pkgver") > 0 )); then + warning "$(gettext "A newer version for '%s' is already present in database")" "$pkgname" + if (( PREVENT_DOWNGRADE )); then + return 0 + fi + fi + if (( RMEXISTING )); then + local oldfilename="$(sed -n '/^%FILENAME%$/ {n;p;q;}' "$pkgentry/desc")" + local oldfile="$(dirname "$1")/$oldfilename" + fi + fi + fi + + # compute base64'd PGP signature + if [[ -f "$pkgfile.sig" ]]; then + if grep -q 'BEGIN PGP SIGNATURE' "$pkgfile.sig"; then + error "$(gettext "Cannot use armored signatures for packages: %s")" "$pkgfile.sig" + return 1 + fi + pgpsigsize=$(wc -c < "$pkgfile.sig") + if (( pgpsigsize > 16384 )); then + error "$(gettext "Invalid package signature file '%s'.")" "$pkgfile.sig" + return 1 + fi + msg2 "$(gettext "Adding package signature...")" + pgpsig=$(base64 "$pkgfile.sig" | tr -d '\n') + fi + + csize=$(wc -c < "$pkgfile") + + # compute checksums + msg2 "$(gettext "Computing checksums...")" + md5sum=$(md5sum "$pkgfile") + md5sum=${md5sum%% *} + sha256sum=$(sha256sum "$pkgfile") + sha256sum=${sha256sum%% *} + + # remove an existing entry if it exists, ignore failures + db_remove_entry "$pkgname" + + # create package directory + pushd "$tmpdir/db" >/dev/null + mkdir "$pkgname-$pkgver" + pushd "$pkgname-$pkgver" >/dev/null + + # create desc entry + msg2 "$(gettext "Creating '%s' db entry...")" 'desc' + { + format_entry "FILENAME" "${1##*/}" + format_entry "NAME" "$pkgname" + format_entry "BASE" "$pkgbase" + format_entry "VERSION" "$pkgver" + format_entry "DESC" "$pkgdesc" + format_entry "GROUPS" "${_groups[@]}" + format_entry "CSIZE" "$csize" + format_entry "ISIZE" "$size" + + # add checksums + format_entry "MD5SUM" "$md5sum" + format_entry "SHA256SUM" "$sha256sum" + + # add PGP sig + format_entry "PGPSIG" "$pgpsig" + + format_entry "URL" "$url" + format_entry "LICENSE" "${_licenses[@]}" + format_entry "ARCH" "$arch" + format_entry "BUILDDATE" "$builddate" + format_entry "PACKAGER" "$packager" + format_entry "REPLACES" "${_replaces[@]}" + format_entry "CONFLICTS" "${_conflicts[@]}" + format_entry "PROVIDES" "${_provides[@]}" + + format_entry "DEPENDS" "${_depends[@]}" + format_entry "OPTDEPENDS" "${_optdepends[@]}" + format_entry "MAKEDEPENDS" "${_makedepends[@]}" + format_entry "CHECKDEPENDS" "${_checkdepends[@]}" + } >'desc' + + popd >/dev/null + popd >/dev/null + + # copy updated package entry into "files" database + cp -a "$tmpdir/db/$pkgname-$pkgver" "$tmpdir/files/$pkgname-$pkgver" + + # create files file + msg2 "$(gettext "Creating '%s' db entry...")" 'files' + local files_path="$tmpdir/files/$pkgname-$pkgver/files" + echo "%FILES%" >"$files_path" + bsdtar --exclude='^.*' -tf "$pkgfile" | LC_ALL=C sort -u >>"$files_path" + + if (( RMEXISTING )); then + msg2 "$(gettext "Removing old package file '%s'")" "$oldfilename" + rm -f ${oldfile} ${oldfile}.sig + fi + + return 0 +} # end db_write_entry + +# remove existing entries from the DB +# arg1 - package name +db_remove_entry() { + local pkgname=$1 + local notfound=1 + local pkgentry=$(find_pkgentry "$pkgname") + while [[ -n $pkgentry ]]; do + notfound=0 + + msg2 "$(gettext "Removing existing entry '%s'...")" \ + "${pkgentry##*/}" + rm -rf "$pkgentry" + + # remove entries in "files" database + local filesentry=$(echo "$pkgentry" | sed 's/\(.*\)\/db\//\1\/files\//') + rm -rf "$filesentry" + + pkgentry=$(find_pkgentry "$pkgname") + done + return $notfound +} # end db_remove_entry + +elephant() { + case $(( RANDOM % 2 )) in + 0) printf '%s\n' "H4sIAL3qBE4CAyWLwQ3AMAgD/0xh5UPzYiFUMgjq7LUJsk7yIQNAQTAikFUDnqkr" \ + "OQFOUm0Wd9pHCi13ONjBpVdqcWx+EdXVX4vXvGv5cgztB9+fJxZ7AAAA" + ;; + + 1) printf '%s\n' "H4sIAJVWBU4CA21RMQ7DIBDbeYWrDgQJ7rZ+IA/IB05l69alcx5fc0ASVXUk4jOO" \ + "7yAAUWtorygwJ4hlMii0YkJKKRKGvsMsiykl1SalvrMD1gUXyXRkGZPx5OPft81K" \ + "tNAiAjyGjYO47h1JjizPkJrCWbK/4C+uLkT7bzpGc7CT9bmOzNSW5WLSO5vexjmH" \ + "ZL9JFFZeAa0a2+lKjL2anpYfV+0Zx9LJ+/MC8nRayuDlSNy2rfAPibOzsiWHL0jL" \ + "SsjFAQAA" + ;; + esac | base64 -d | gzip -d +} + +prepare_repo_db() { + local repodir dbfile + + # ensure the path to the DB exists; $LOCKFILE is always an absolute path + repodir=${LOCKFILE%/*}/ + + if [[ ! -d $repodir ]]; then + error "$(gettext "%s does not exist or is not a directory.")" "$repodir" + exit 1 + fi + + # check lock file + if ( set -o noclobber; echo "$$" > "$LOCKFILE") 2> /dev/null; then + CLEAN_LOCK=1 + else + error "$(gettext "Failed to acquire lockfile: %s.")" "$LOCKFILE" + [[ -f $LOCKFILE ]] && error "$(gettext "Held by process %s")" "$(cat "$LOCKFILE")" + exit 1 + fi + + for repo in "db" "files"; do + dbfile=${repodir}/$REPO_DB_PREFIX.$repo.$REPO_DB_SUFFIX + + if [[ -f $dbfile ]]; then + # there are two situations we can have here: + # a DB with some entries, or a DB with no contents at all. + if ! bsdtar -tqf "$dbfile" '*/desc' >/dev/null 2>&1; then + # check empty case + if [[ -n $(bsdtar -tqf "$dbfile" '*' 2>/dev/null) ]]; then + error "$(gettext "Repository file '%s' is not a proper pacman database.")" "$dbfile" + exit 1 + fi + fi + verify_signature "$dbfile" + msg "$(gettext "Extracting %s to a temporary location...")" "${dbfile##*/}" + bsdtar -xf "$dbfile" -C "$tmpdir/$repo" + else + case $cmd in + repo-remove.sh) + # only a missing "db" database is currently an error + # TODO: remove if statement + if [[ $repo == "db" ]]; then + error "$(gettext "Repository file '%s' was not found.")" "$dbfile" + exit 1 + fi + ;; + repo-add.sh) + # check if the file can be created (write permission, directory existence, etc) + if ! touch "$dbfile"; then + error "$(gettext "Repository file '%s' could not be created.")" "$dbfile" + exit 1 + fi + rm -f "$dbfile" + ;; + esac + fi + done +} + +add() { + if [[ ! -f $1 ]]; then + error "$(gettext "File '%s' not found.")" "$1" + return 1 + fi + + pkgfile=$1 + if ! bsdtar -tqf "$pkgfile" .PKGINFO >/dev/null 2>&1; then + error "$(gettext "'%s' is not a package file, skipping")" "$pkgfile" + return 1 + fi + + msg "$(gettext "Adding package '%s'")" "$pkgfile" + + db_write_entry "$pkgfile" +} + +remove() { + pkgname=$1 + msg "$(gettext "Searching for package '%s'...")" "$pkgname" + + if ! db_remove_entry "$pkgname"; then + error "$(gettext "Package matching '%s' not found.")" "$pkgname" + return 1 + fi + + return 0 +} + +rotate_db() { + dirname=${LOCKFILE%/*} + + pushd "$dirname" >/dev/null + + for repo in "db" "files"; do + filename=${REPO_DB_PREFIX}.${repo}.${REPO_DB_SUFFIX} + tempname=$dirname/.tmp.$filename + + # hardlink or move the previous version of the database and signature to .old + # extension as a backup measure + if [[ -f $filename ]]; then + ln -f "$filename" "$filename.old" 2>/dev/null || \ + mv -f "$filename" "$filename.old" + + if [[ -f $filename.sig ]]; then + ln -f "$filename.sig" "$filename.old.sig" 2>/dev/null || \ + mv -f "$filename.sig" "$filename.old.sig" + else + rm -f "$filename.old.sig" + fi + fi + + # rotate the newly-created database and signature into place + mv "$tempname" "$filename" + if [[ -f $tempname.sig ]]; then + mv "$tempname.sig" "$filename.sig" + fi + + dblink=${filename%.tar*} + rm -f "$dblink" "$dblink.sig" + ln -s "$filename" "$dblink" 2>/dev/null || \ + ln "$filename" "$dblink" 2>/dev/null || \ + cp "$filename" "$dblink" + if [[ -f "$filename.sig" ]]; then + ln -s "$filename.sig" "$dblink.sig" 2>/dev/null || \ + ln "$filename.sig" "$dblink.sig" 2>/dev/null || \ + cp "$filename.sig" "$dblink.sig" + fi + done + + popd >/dev/null +} + +create_db() { + # $LOCKFILE is already guaranteed to be absolute so this is safe + dirname=${LOCKFILE%/*} + + for repo in "db" "files"; do + filename=${REPO_DB_PREFIX}.${repo}.${REPO_DB_SUFFIX} + # this ensures we create it on the same filesystem, making moves atomic + tempname=$dirname/.tmp.$filename + + pushd "$tmpdir/$repo" >/dev/null + local files=(*) + if [[ ${files[*]} = '*' ]]; then + # we have no packages remaining? zip up some emptyness + warning "$(gettext "No packages remain, creating empty database.")" + files=(-T /dev/null) + fi + bsdtar -cf - "${files[@]}" | compress_as "$filename" > "$tempname" + popd >/dev/null + + create_signature "$tempname" + done +} + +trap_exit() { + # unhook all traps to avoid race conditions + trap '' EXIT TERM HUP QUIT INT ERR + + echo + error "$@" + clean_up 1 +} + +clean_up() { + local exit_code=${1:-$?} + + # unhook all traps to avoid race conditions + trap '' EXIT TERM HUP QUIT INT ERR + + [[ -d $tmpdir ]] && rm -rf "$tmpdir" + (( CLEAN_LOCK )) && [[ -f $LOCKFILE ]] && rm -f "$LOCKFILE" + + exit $exit_code +} + + +# PROGRAM START + +# determine whether we have gettext; make it a no-op if we do not +if ! type gettext &>/dev/null; then + gettext() { + echo "$@" + } +fi + +case $1 in + -h|--help) usage; exit 0;; + -V|--version) version; exit 0;; +esac + +# figure out what program we are +cmd=${0##*/} +if [[ $cmd == "repo-elephant" ]]; then + elephant + exit 0 +fi + +if [[ $cmd != "repo-add.sh" && $cmd != "repo-remove.sh" ]]; then + error "$(gettext "Invalid command name '%s' specified.")" "$cmd" + exit 1 +fi + +tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/repo-tools.XXXXXXXXXX") || (\ + error "$(gettext "Cannot create temp directory for database building.")"; \ + exit 1) + +for repo in "db" "files"; do + mkdir "$tmpdir/$repo" +done + +trap 'clean_up' EXIT +for signal in TERM HUP QUIT; do + trap "trap_exit \"$(gettext "%s signal caught. Exiting...")\" \"$signal\"" "$signal" +done +trap 'trap_exit "$(gettext "Aborted by user! Exiting...")"' INT +trap 'trap_exit "$(gettext "An unknown error has occurred. Exiting...")"' ERR + +declare -a args +# parse arguments +while (( $# )); do + case $1 in + -q|--quiet) QUIET=1;; + -n|--new) ONLYADDNEW=1;; + -R|--remove) RMEXISTING=1;; + --nocolor) USE_COLOR='n';; + -s|--sign) + SIGN=1 + ;; + -k|--key) + KEY=1 + shift + GPGKEY=$1 + ;; + -v|--verify) + VERIFY=1 + ;; + -p|--prevent-downgrade) + PREVENT_DOWNGRADE=1 + ;; + *) + args+=("$1") + ;; + esac + shift +done + +# check if messages are to be printed using color +if [[ -t 2 && $USE_COLOR != "n" ]]; then + colorize +else + unset ALL_OFF BOLD BLUE GREEN RED YELLOW +fi + +REPO_DB_FILE=${args[0]} +if [[ -z $REPO_DB_FILE ]]; then + usage + exit 1 +fi + +if [[ $REPO_DB_FILE == /* ]]; then + LOCKFILE=$REPO_DB_FILE.lck +else + LOCKFILE=$PWD/$REPO_DB_FILE.lck +fi + +verify_repo_extension "$REPO_DB_FILE" + +REPO_DB_PREFIX=${REPO_DB_FILE##*/} +REPO_DB_PREFIX=${REPO_DB_PREFIX%.db.*} +REPO_DB_SUFFIX=${REPO_DB_FILE##*.db.} + +if (( SIGN || VERIFY )); then + check_gpg +fi + +if (( VERIFY && ${#args[@]} == 1 )); then + for repo in "db" "files"; do + dbfile=${repodir}/$REPO_DB_PREFIX.$repo.$REPO_DB_SUFFIX + + if [[ -f $dbfile ]]; then + verify_signature "$dbfile" + fi + done + exit 0 +fi + +prepare_repo_db + +fail=0 +for arg in "${args[@]:1}"; do + case $cmd in + repo-add.sh) add "$arg" ;; + repo-remove.sh) remove "$arg" ;; + esac || fail=1 +done + +# if the whole operation was a success, re-zip and rotate databases +if (( !fail )); then + msg "$(gettext "Creating updated database file '%s'")" "$REPO_DB_FILE" + create_db + rotate_db +else + msg "$(gettext "No packages modified, nothing to do.")" + exit 1 +fi + +exit 0 diff --git a/repo-remove.sh b/repo-remove.sh new file mode 120000 index 0000000000..964348a7d0 --- /dev/null +++ b/repo-remove.sh @@ -0,0 +1 @@ +repo-add.sh \ No newline at end of file diff --git a/scripts/db/compress.sh b/scripts/db/compress.sh new file mode 100644 index 0000000000..e55c8e854d --- /dev/null +++ b/scripts/db/compress.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# +# compress.sh - functions to compress archives in a uniform manner +# +# Copyright (c) 2017-2021 Pacman Development Team +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_UTIL_COMPRESS_SH" ]] && return +LIBMAKEPKG_UTIL_COMPRESS_SH=1 + +source scripts/db/message.sh +source scripts/db/pkgbuild.sh + + +# Wrapper around many stream compression formats, for use in the middle of a +# pipeline. A tar archive is passed on stdin and compressed to stdout. +compress_as() { + # $1: final archive filename extension for compression type detection + + local cmd ext=${1#${1%.tar*}} + + if ! get_compression_command "$ext" cmd; then + warning "$(gettext "'%s' is not a valid archive extension.")" "${ext:-${1##*/}}" + cat + else + "${cmd[@]}" + fi +} + +# Retrieve the compression command for an archive extension, or cat for .tar, +# and save it to an existing array name. If the extension cannot be found, +# clear the array and return failure. +get_compression_command() { + local extarray ext=$1 outputvar=$2 + local resolvecmd=() fallback=() + + case "$ext" in + *.tar.gz) fallback=(gzip -c -f -n) ;; + *.tar.bz2) fallback=(bzip2 -c -f) ;; + *.tar.xz) fallback=(xz -c -z -) ;; + *.tar.zst) fallback=(zstd -c -z -q -) ;; + *.tar.lrz) fallback=(lrzip -q) ;; + *.tar.lzo) fallback=(lzop -q) ;; + *.tar.Z) fallback=(compress -c -f) ;; + *.tar.lz4) fallback=(lz4 -q) ;; + *.tar.lz) fallback=(lzip -c -f) ;; + *.tar) fallback=(cat) ;; + # do not respect unknown COMPRESS* env vars + *) array_build "$outputvar" resolvecmd; return 1 ;; + esac + + ext=${ext#*.tar.} + # empty the variable for plain tar archives so we fallback to cat + ext=${ext#*.tar} + + if [[ -n $ext ]]; then + extarray="COMPRESS${ext^^}[@]" + resolvecmd=("${!extarray}") + fi + if (( ${#resolvecmd[@]} == 0 )); then + resolvecmd=("${fallback[@]}") + fi + + array_build "$outputvar" resolvecmd +} diff --git a/scripts/db/error.sh b/scripts/db/error.sh new file mode 100644 index 0000000000..4e1fb037c5 --- /dev/null +++ b/scripts/db/error.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# +# error.sh.in - error variable definitions for makepkg +# +# Copyright (c) 2006-2021 Pacman Development Team +# Copyright (c) 2002-2006 by Judd Vinet +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_UTIL_ERROR_SH" ]] && return +LIBMAKEPKG_UTIL_ERROR_SH=1 + +E_OK=0 +E_FAIL=1 # Generic error +E_CONFIG_ERROR=2 +E_INVALID_OPTION=3 +E_USER_FUNCTION_FAILED=4 +E_PACKAGE_FAILED=5 +E_MISSING_FILE=6 +E_MISSING_PKGDIR=7 +E_INSTALL_DEPS_FAILED=8 +E_REMOVE_DEPS_FAILED=9 +E_ROOT=10 +E_FS_PERMISSIONS=11 +E_PKGBUILD_ERROR=12 +E_ALREADY_BUILT=13 +E_INSTALL_FAILED=14 +E_MISSING_MAKEPKG_DEPS=15 +E_PRETTY_BAD_PRIVACY=16 diff --git a/scripts/db/message.sh b/scripts/db/message.sh new file mode 100644 index 0000000000..3df21fc8a1 --- /dev/null +++ b/scripts/db/message.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# +# message.sh - functions for outputting messages in makepkg +# +# Copyright (c) 2006-2021 Pacman Development Team +# Copyright (c) 2002-2006 by Judd Vinet +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_UTIL_MESSAGE_SH" ]] && return +LIBMAKEPKG_UTIL_MESSAGE_SH=1 + + +colorize() { + # prefer terminal safe colored and bold text when tput is supported + if tput setaf 0 &>/dev/null; then + ALL_OFF="$(tput sgr0)" + BOLD="$(tput bold)" + BLUE="${BOLD}$(tput setaf 4)" + GREEN="${BOLD}$(tput setaf 2)" + RED="${BOLD}$(tput setaf 1)" + YELLOW="${BOLD}$(tput setaf 3)" + else + ALL_OFF="\e[0m" + BOLD="\e[1m" + BLUE="${BOLD}\e[34m" + GREEN="${BOLD}\e[32m" + RED="${BOLD}\e[31m" + YELLOW="${BOLD}\e[33m" + fi + readonly ALL_OFF BOLD BLUE GREEN RED YELLOW +} + +# plainerr/plainerr are primarily used to continue a previous message on a new +# line, depending on whether the first line is a regular message or an error +# output + +plain() { + (( QUIET )) && return + local mesg=$1; shift + printf "${BOLD} ${mesg}${ALL_OFF}\n" "$@" +} + +plainerr() { + plain "$@" >&2 +} + +msg() { + (( QUIET )) && return + local mesg=$1; shift + printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" +} + +msg2() { + (( QUIET )) && return + local mesg=$1; shift + printf "${BLUE} ->${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" +} + +ask() { + local mesg=$1; shift + printf "${BLUE}::${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}" "$@" +} + +warning() { + local mesg=$1; shift + printf "${YELLOW}==> $(gettext "WARNING:")${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2 +} + +error() { + local mesg=$1; shift + printf "${RED}==> $(gettext "ERROR:")${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2 +} diff --git a/scripts/db/pkgbuild.sh b/scripts/db/pkgbuild.sh new file mode 100644 index 0000000000..5a1989122e --- /dev/null +++ b/scripts/db/pkgbuild.sh @@ -0,0 +1,266 @@ +#!/bin/bash +# +# pkgbuild.sh - functions to extract information from PKGBUILD files +# +# Copyright (c) 2009-2021 Pacman Development Team +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_UTIL_PKGBUILD_SH" ]] && return +LIBMAKEPKG_UTIL_PKGBUILD_SH=1 + +source scripts/db/schema.sh + + +have_function() { + declare -f "$1" >/dev/null +} + +grep_function() { + { declare -f "$1" || declare -f package; } 2>/dev/null | grep -E "$2" +} + +array_build() { + local dest=$1 src=$2 i keys values + + # it's an error to try to copy a value which doesn't exist. + declare -p "$2" &>/dev/null || return 1 + + # Build an array of the indices of the source array. + eval "keys=(\"\${!$2[@]}\")" + + # Clear the destination array + eval "$dest=()" + + # Read values indirectly via their index. This approach gives us support + # for associative arrays, sparse arrays, and empty strings as elements. + for i in "${keys[@]}"; do + values+=("printf -v '$dest[$i]' %s \"\${$src[$i]}\";") + done + + eval "${values[*]}" +} + +extract_global_variable() { + # $1: variable name + # $2: multivalued + # $3: name of output var + + local attr=$1 isarray=$2 outputvar=$3 ref + + if (( isarray )); then + array_build ref "$attr" + (( ${#ref[@]} )) && array_build "$outputvar" "$attr" + else + [[ ${!attr} ]] && printf -v "$outputvar" %s "${!attr}" + fi +} + +extract_function_variable() { + # $1: function name + # $2: variable name + # $3: multivalued + # $4: name of output var + + local funcname=$1 attr=$2 isarray=$3 outputvar=$4 attr_regex= decl= r=1 + + if (( isarray )); then + printf -v attr_regex '^[[:space:]]* %s\+?=\(' "$2" + else + printf -v attr_regex '^[[:space:]]* %s\+?=[^(]' "$2" + fi + + # this function requires extglob - save current status to restore later + local shellopts=$(shopt -p extglob) + shopt -s extglob + + while read -r; do + # strip leading whitespace and any usage of declare + decl=${REPLY##*([[:space:]])} + eval "${decl/#$attr/$outputvar}" + + # entering this loop at all means we found a match, so notify the caller. + r=0 + done < <(grep_function "$funcname" "$attr_regex") + + eval "$shellopts" + + return $r +} + +exists_function_variable() { + # $1: function name + # $2: variable name + + local funcname=$1 attr=$2 out + extract_function_variable "$funcname" "$attr" 0 out || + extract_function_variable "$funcname" "$attr" 1 out +} + +get_pkgbuild_attribute() { + # $1: package name + # $2: attribute name + # $3: multivalued + # $4: name of output var + + local pkgname=$1 attrname=$2 isarray=$3 outputvar=$4 + + if (( isarray )); then + eval "$outputvar=()" + else + printf -v "$outputvar" %s '' + fi + + if [[ $pkgname ]]; then + extract_global_variable "$attrname" "$isarray" "$outputvar" + extract_function_variable "package_$pkgname" "$attrname" "$isarray" "$outputvar" + else + extract_global_variable "$attrname" "$isarray" "$outputvar" + fi +} + +get_pkgbuild_all_split_attributes() { + local attrname=$1 outputvar=$2 all_list list + + if extract_global_variable "$attrname" 1 list; then + all_list+=("${list[@]}") + fi + for a in "${arch[@]}"; do + if extract_global_variable "${attrname}_$a" 1 list; then + all_list+=("${list[@]}") + fi + done + + for name in "${pkgname[@]}"; do + if extract_function_variable "package_$name" "$attrname" 1 list; then + all_list+=("${list[@]}") + fi + + for a in "${arch[@]}"; do + if extract_function_variable "package_$name" "${attrname}_$a" 1 list; then + all_list+=("${list[@]}") + fi + done + done + + (( ${#all_list[@]} )) && array_build "$outputvar" all_list +} + +## +# usage : get_full_version() +# return : full version spec, including epoch (if necessary), pkgver, pkgrel +## +get_full_version() { + if (( epoch > 0 )); then + printf "%s\n" "$epoch:$pkgver-$pkgrel" + else + printf "%s\n" "$pkgver-$pkgrel" + fi +} + +## +# usage : get_pkg_arch( [$pkgname] ) +# return : architecture of the package +## +get_pkg_arch() { + if [[ -z $1 ]]; then + if [[ $arch = "any" ]]; then + printf "%s\n" "any" + else + printf "%s\n" "$CARCH" + fi + else + local arch_override + get_pkgbuild_attribute "$1" arch 1 arch_override + (( ${#arch_override[@]} == 0 )) && arch_override=("${arch[@]}") + if [[ $arch_override = "any" ]]; then + printf "%s\n" "any" + else + printf "%s\n" "$CARCH" + fi + fi +} + +print_all_package_names() { + local version=$(get_full_version) + local architecture pkg opts a + for pkg in ${pkgname[@]}; do + architecture=$(get_pkg_arch $pkg) + printf "%s/%s-%s-%s%s\n" "$PKGDEST" "$pkg" "$version" "$architecture" "$PKGEXT" + done + if check_option "debug" "y" && check_option "strip" "y"; then + architecture=$(get_pkg_arch) + printf "%s/%s-%s-%s-%s%s\n" "$PKGDEST" "$pkgbase" "debug" "$version" "$architecture" "$PKGEXT" + fi +} + +get_all_sources() { + local aggregate l a + + if array_build l 'source'; then + aggregate+=("${l[@]}") + fi + + for a in "${arch[@]}"; do + if array_build l "source_$a"; then + aggregate+=("${l[@]}") + fi + done + + array_build "$1" "aggregate" +} + +get_all_sources_for_arch() { + local aggregate l + + if array_build l 'source'; then + aggregate+=("${l[@]}") + fi + + if array_build l "source_$CARCH"; then + aggregate+=("${l[@]}") + fi + + array_build "$1" "aggregate" +} + +get_integlist() { + local integ + local integlist=() + + for integ in "${known_hash_algos[@]}"; do + # check for e.g. "sha256sums" + local sumname="${integ}sums[@]" + if [[ -n ${!sumname} ]]; then + integlist+=("$integ") + continue + fi + + # check for e.g. "sha256sums_x86_64" + for a in "${arch[@]}"; do + local sumname="${integ}sums_${a}[@]" + if [[ -n ${!sumname} ]]; then + integlist+=("$integ") + break + fi + done + done + + if (( ${#integlist[@]} > 0 )); then + printf "%s\n" "${integlist[@]}" + else + printf "%s\n" "${INTEGRITY_CHECK[@]}" + fi +} diff --git a/scripts/db/schema.sh b/scripts/db/schema.sh new file mode 100644 index 0000000000..c0e48bf581 --- /dev/null +++ b/scripts/db/schema.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# schema.sh - declare specific groups of pkgbuild variables +# +# Copyright (c) 2015-2021 Pacman Development Team +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_SCHEMA_SH" ]] && return +LIBMAKEPKG_SCHEMA_SH=1 + +source scripts/db/util.sh + + +known_hash_algos=({ck,md5,sha{1,224,256,384,512},b2}) + +pkgbuild_schema_arrays=(arch backup checkdepends conflicts depends groups + license makedepends noextract optdepends options + provides replaces source validpgpkeys + "${known_hash_algos[@]/%/sums}") + +pkgbuild_schema_strings=(changelog epoch install pkgbase pkgdesc pkgrel pkgver + url) + +pkgbuild_schema_arch_arrays=(checkdepends conflicts depends makedepends + optdepends provides replaces source + "${known_hash_algos[@]/%/sums}") + +pkgbuild_schema_package_overrides=(pkgdesc arch url license groups depends + optdepends provides conflicts replaces + backup options install changelog) + +readonly -a known_hash_algos pkgbuild_schema_arrays \ + pkgbuild_schema_strings pkgbuild_schema_arch_arrays \ + pkgbuild_schema_package_overrides diff --git a/scripts/db/util.sh b/scripts/db/util.sh new file mode 100644 index 0000000000..b8bd98d972 --- /dev/null +++ b/scripts/db/util.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# +# util.sh - general utility functions +# +# Copyright (c) 2006-2021 Pacman Development Team +# Copyright (c) 2002-2006 by Judd Vinet +# +# 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 2 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 . +# + +[[ -n "$LIBMAKEPKG_UTIL_UTIL_SH" ]] && return +LIBMAKEPKG_UTIL_UTIL_SH=1 + +source scripts/db/error.sh +source scripts/db/message.sh + +## +# usage : in_array( $needle, $haystack ) +# return : 0 - found +# 1 - not found +## +in_array() { + local needle=$1; shift + local item + for item in "$@"; do + [[ $item = "$needle" ]] && return 0 # Found + done + return 1 # Not Found +} + +# tests if a variable is an array +is_array() { + local v=$1 + local ret=1 + + if [[ ${!v@a} = *a* ]]; then + ret=0 + fi + + return $ret +} + +# Canonicalize a directory path if it exists +canonicalize_path() { + local path="$1" + + if [[ -d $path ]]; then + ( + cd_safe "$path" + pwd -P + ) + else + printf "%s\n" "$path" + fi +} + +dir_is_empty() { + ( + shopt -s dotglob nullglob + files=("$1"/*) + (( ${#files} == 0 )) + ) +} + +cd_safe() { + if ! cd "$1"; then + error "$(gettext "Failed to change to directory %s")" "$1" + plainerr "$(gettext "Aborting...")" + exit 1 + fi +} + +# Try to create directory if one does not yet exist. Fails if the directory +# exists but has no write permissions, or if there is an existing file with +# the same name. +ensure_writable_dir() { + local dirtype="$1" dirpath="$2" + + if ! mkdir -p "$dirpath" 2>/dev/null; then + error "$(gettext "Failed to create the directory \$%s (%s).")" "$dirtype" "$dirpath" + return 1 + elif [[ ! -w $dirpath ]]; then + error "$(gettext "You do not have write permission for the directory \$%s (%s).")" "$dirtype" "$dirpath" + return 1 + fi + + return 0 +} + +# source a file and fail if it does not succeed +source_safe() { + local shellopts=$(shopt -p extglob) + shopt -u extglob + + if ! source "$@"; then + error "$(gettext "Failed to source %s")" "$1" + exit $E_MISSING_FILE + fi + + eval "$shellopts" +}