#!/bin/bash # # repo-add - add a package to a given repo database file # repo-remove - remove a package entry from a given repo database file # @configure_input@ # # Copyright (c) 2006-2011 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='@localedir@' myver='@PACKAGE_VERSION@' confdir='@sysconfdir@' QUIET=0 DELTA=0 WITHFILES=0 SIGN=0 VERIFY=0 REPO_DB_FILE= LOCKFILE= CLEAN_LOCK=0 # ensure we have a sane umask set umask 0022 m4_include(library/output_format.sh) # print usage instructions usage() { cmd=${0##*/} printf "%s (pacman) %s\n\n" "$cmd" "$myver" if [[ $cmd == "repo-add" ]] ; 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 " -d, --delta generate and add delta for package update\n")" printf "$(gettext " -f, --files update database's file list\n")" elif [[ $cmd == "repo-remove" ]] ; 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 " -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" ]] ; then printf "$(gettext "Example: repo-add /path/to/repo.db.tar.gz pacman-3.0.0-1-i686.pkg.tar.gz\n")" elif [[ $cmd == "repo-remove" ]] ; then printf "$(gettext "Example: repo-remove /path/to/repo.db.tar.gz kernel26\n")" fi } version() { cmd=${0##*/} printf "%s (pacman) %s\n\n" "$cmd" "$myver" printf "$(gettext "\ Copyright (c) 2006-2011 Pacman Development Team \n\n\ 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/tree/$pkgname*; do name=${pkgentry##*/} if [[ ${name%-*-*} = $pkgname ]]; then echo $pkgentry return 0 fi done return 1 } # Get the package name from the delta filename get_delta_pkgname() { local tmp tmp=${1##*/} echo ${tmp%-*-*_to*} } # write a delta entry # arg1 - path to delta file db_write_delta() { deltafile="$1" pkgname="$(get_delta_pkgname $deltafile)" pkgentry=$(find_pkgentry $pkgname) if [[ -z $pkgentry ]]; then error "$(gettext "No database entry for package '%s'.")" "$pkgname" return 1 fi deltas="$pkgentry/deltas" if [[ ! -f $deltas ]]; then echo -e "%DELTAS%" >$deltas fi # get md5sum and compressed size of package md5sum="$(openssl dgst -md5 "$deltafile")" md5sum="${md5sum##* }" csize=$(@SIZECMD@ "$deltafile") oldfile=$(xdelta3 printhdr $deltafile | grep "XDELTA filename (source)" | sed 's/.*: *//') newfile=$(xdelta3 printhdr $deltafile | grep "XDELTA filename (output)" | sed 's/.*: *//') if grep -q "$oldfile.*$newfile" $deltas; then sed -i.backup "/$oldfile.*$newfile/d" $deltas && rm -f $deltas.backup fi msg2 "$(gettext "Adding 'deltas' entry : %s -> %s")" "$oldfile" "$newfile" echo ${deltafile##*/} $md5sum $csize $oldfile $newfile >> $deltas return 0 } # end db_write_delta # remove a delta entry # arg1 - path to delta file db_remove_delta() { deltafile="$1" filename=${deltafile##*/} pkgname="$(get_delta_pkgname $deltafile)" pkgentry=$(find_pkgentry $pkgname) if [[ -z $pkgentry ]]; then return 1 fi deltas="$pkgentry/deltas" if [[ ! -f $deltas ]]; then return 1 fi if grep -q "$filename" $deltas; then sed -i.backup "/$filename/d" $deltas && rm -f $deltas.backup msg2 "$(gettext "Removing existing entry '%s'...")" "$filename" return 0 fi return 1 } # end db_remove_delta 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 } # sign the package database once repackaged create_signature() { (( ! SIGN )) && return local dbfile="$1" local ret=0 msg "$(gettext "Signing database...")" local SIGNWITHKEY="" if [[ -n $GPGKEY ]]; then SIGNWITHKEY="-u ${GPGKEY}" fi gpg --detach-sign --use-agent ${SIGNWITHKEY} "$dbfile" &>/dev/null || ret=$? if (( ! ret )); then msg2 "$(gettext "Created signature file %s.")" "${dbfile##*/}.sig" else warning "$(gettext "Failed to sign package database.")" 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 repofile=$1 case "$repofile" in *.@(db|files).tar.gz) TAR_OPT="z" ;; *.@(db|files).tar.bz2) TAR_OPT="j" ;; *.@(db|files).tar.xz) TAR_OPT="J" ;; *.@(db|files).tar.Z) TAR_OPT="Z" ;; *.@(db|files).tar) TAR_OPT="" ;; *) error "$(gettext "'%s' does not have a valid archive extension.")" \ "$repofile" exit 1 ;; esac printf '%s' "$TAR_OPT" } # 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 local pkgname 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") ;; 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/tree/$pkgname-$pkgver ]]; then warning "$(gettext "An entry for '%s' already existed")" "$pkgname-$pkgver" else if (( DELTA )); then pkgentry=$(find_pkgentry $pkgname) if [[ -n $pkgentry ]]; then local oldfilename=$(grep -A1 FILENAME $pkgentry/desc | tail -n1) local oldfile="$(dirname $1)/$oldfilename" fi fi fi # compute base64'd PGP signature if [[ -f "$pkgfile.sig" ]]; then pgpsigsize=$(@SIZECMD@ "$pkgfile.sig") if (( pgpsigsize > 16384 )); then error "$(gettext "Invalid package signature file '%s'.")" "$pkgfile.sig" return 1 fi msg2 "$(gettext "Adding package signature...")" pgpsig=$(openssl base64 -in "$pkgfile.sig" | tr -d '\n') fi csize=$(@SIZECMD@ "$pkgfile") # compute checksums msg2 "$(gettext "Computing checksums...")" md5sum="$(openssl dgst -md5 "$pkgfile")" md5sum="${md5sum##* }" sha256sum="$(openssl dgst -sha256 "$pkgfile")" sha256sum="${sha256sum##* }" # remove an existing entry if it exists, ignore failures db_remove_entry "$pkgname" # create package directory pushd "$tmpdir/tree" >/dev/null mkdir "$pkgname-$pkgver" pushd "$pkgname-$pkgver" >/dev/null # restore an eventual deltas file [[ -f ../$pkgname.deltas ]] && mv "../$pkgname.deltas" deltas # 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[@]}" } >'desc' # create depends entry msg2 "$(gettext "Creating '%s' db entry...")" 'depends' { format_entry "DEPENDS" "${_depends[@]}" format_entry "CONFLICTS" "${_conflicts[@]}" format_entry "PROVIDES" "${_provides[@]}" format_entry "OPTDEPENDS" "${_optdepends[@]}" } >'depends' popd >/dev/null popd >/dev/null # create files file if wanted if (( WITHFILES )); then msg2 "$(gettext "Creating '%s' db entry...")" 'files' local files_path="$tmpdir/tree/$pkgname-$pkgver/files" echo "%FILES%" >$files_path bsdtar --exclude='^.*' -tf "$pkgfile" >>$files_path fi # create a delta file if (( DELTA )); then if [[ -n $oldfilename ]]; then if [[ -f $oldfile ]]; then delta=$(pkgdelta -q $oldfile $1) if [[ -f $delta ]]; then db_write_delta $delta fi else warning "$(gettext "Old package file not found: %s")" "$oldfilename" fi fi 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 if [[ -f $pkgentry/deltas ]]; then mv "$pkgentry/deltas" "$tmpdir/tree/$pkgname.deltas" fi msg2 "$(gettext "Removing existing entry '%s'...")" \ "${pkgentry##*/}" rm -rf $pkgentry 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 | openssl base64 -d | gzip -d } check_repo_db() { local repodir # ensure the path to the DB exists if [[ "$LOCKFILE" == /* ]]; then repodir=${LOCKFILE%/*}/ else repodir=$PWD/$LOCKFILE repodir=${repodir%/*}/ fi 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 if [[ -f $REPO_DB_FILE ]]; 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 "$REPO_DB_FILE" '*/desc' >/dev/null 2>&1; then # check empty case if [[ -n $(bsdtar -tqf "$REPO_DB_FILE" '*' 2>/dev/null) ]]; then error "$(gettext "Repository file '%s' is not a proper pacman database.")" "$REPO_DB_FILE" exit 1 fi fi verify_signature "$REPO_DB_FILE" msg "$(gettext "Extracting database to a temporary location...")" bsdtar -xf "$REPO_DB_FILE" -C "$tmpdir/tree" else case "$cmd" in repo-remove) error "$(gettext "Repository file '%s' was not found.")" "$REPO_DB_FILE" exit 1 ;; repo-add) # check if the file can be created (write permission, directory existence, etc) if ! touch "$REPO_DB_FILE"; then error "$(gettext "Repository file '%s' could not be created.")" "$REPO_DB_FILE" exit 1 fi rm -f "$REPO_DB_FILE" ;; esac fi } add() { if [[ ! -f $1 ]]; then error "$(gettext "File '%s' not found.")" "$1" return 1 fi if [[ ${1##*.} == "delta" ]]; then deltafile=$1 msg "$(gettext "Adding delta '%s'")" "$deltafile" if ! type xdelta3 &>/dev/null; then error "$(gettext "Cannot find the xdelta3 binary! Is xdelta3 installed?")" exit 1 fi if db_write_delta "$deltafile"; then return 0 else return 1 fi 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() { if [[ ${1##*.} == "delta" ]]; then deltafile=$1 msg "$(gettext "Searching for delta '%s'...")" "$deltafile" if db_remove_delta "$deltafile"; then return 0 else error "$(gettext "Delta matching '%s' not found.")" "$deltafile" return 1 fi fi pkgname=$1 msg "$(gettext "Searching for package '%s'...")" "$pkgname" if db_remove_entry "$pkgname"; then rm -f "$tmpdir/tree/$pkgname.deltas" return 0 else error "$(gettext "Package matching '%s' not found.")" "$pkgname" return 1 fi } 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" && $cmd != "repo-remove" ]]; then error "$(gettext "Invalid command name '%s' specified.")" "$cmd" exit 1 fi tmpdir=$(mktemp -d /tmp/repo-tools.XXXXXXXXXX) || (\ error "$(gettext "Cannot create temp directory for database building.")"; \ exit 1) mkdir $tmpdir/tree 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 success=0 # parse arguments while (( $# )); do case "$1" in -q|--quiet) QUIET=1;; -d|--delta) DELTA=1;; -f|--files) WITHFILES=1;; -s|--sign) check_gpg SIGN=1 if ! gpg --list-key ${GPGKEY} &>/dev/null; then if [[ ! -z $GPGKEY ]]; then error "$(gettext "The key ${GPGKEY} does not exist in your keyring.")" else error "$(gettext "There is no key in your keyring.")" fi exit 1 fi ;; -k|--key) check_gpg shift GPGKEY="$1" if ! gpg --list-key ${GPGKEY} &>/dev/null; then error "$(gettext "The key ${GPGKEY} does not exist in your keyring.")" exit 1 fi ;; -v|--verify) check_gpg VERIFY=1 ;; *) args+=("$1") ;; esac shift done REPO_DB_FILE=${args[0]} if [[ -z $REPO_DB_FILE ]]; then usage exit 1 fi LOCKFILE=$REPO_DB_FILE.lck verify_repo_extension "$REPO_DB_FILE" >/dev/null check_repo_db for arg in "${args[@]:1}"; do case "$cmd" in repo-add) add "$arg" ;; repo-remove) remove "$arg" ;; esac && success=1 done # if at least one operation was a success, re-zip database if (( success )); then msg "$(gettext "Creating updated database file '%s'")" "$REPO_DB_FILE" TAR_OPT=$(verify_repo_extension "$REPO_DB_FILE") filename=${REPO_DB_FILE##*/} pushd "$tmpdir/tree" >/dev/null if ( shopt -s nullglob; files=(*); (( ${#files[*]} )) ); then bsdtar -c${TAR_OPT}f "$tmpdir/$filename" * else # we have no packages remaining? zip up some emptyness warning "$(gettext "No packages remain, creating empty database.")" bsdtar -c${TAR_OPT}f "$tmpdir/$filename" -T /dev/null fi popd >/dev/null create_signature "$tmpdir/$filename" [[ -f $REPO_DB_FILE ]] && mv -f "$REPO_DB_FILE" "${REPO_DB_FILE}.old" if [[ -f $REPO_DB_FILE.sig ]]; then mv -f "$REPO_DB_FILE.sig" "$REPO_DB_FILE.old.sig" else rm -f "$REPO_DB_FILE.old.sig" fi [[ -f $tmpdir/$filename ]] && mv "$tmpdir/$filename" "$REPO_DB_FILE" [[ -f $tmpdir/$filename.sig ]] && mv "$tmpdir/$filename.sig" "$REPO_DB_FILE.sig" dblink="${REPO_DB_FILE%.tar*}" target=${REPO_DB_FILE##*/} rm -f "$dblink" "$dblink.sig" ln -s "$target" "$dblink" 2>/dev/null || \ ln "$target" "$dblink" 2>/dev/null || \ cp "$REPO_DB_FILE" "$dblink" if [[ -f "$REPO_DB_FILE.sig" ]]; then ln -s "$target.sig" "$dblink.sig" 2>/dev/null || \ ln "$target.sig" "$dblink.sig" 2>/dev/null || \ cp "$REPO_DB_FILE.sig" "$dblink.sig" fi else msg "$(gettext "No packages modified, nothing to do.")" exit 1 fi exit 0 # vim: set ts=2 sw=2 noet: