From: Date: Sun, 17 Sep 2023 16:34:48 -0400 Subject: [PATCH] pkgadd: apply rejection rules against old files when upgrading This should fix FS#1074, without the drawback of letting stale files "leak" into sysroot. When a new version of a package decides to store its config files under a different directory layout, pkgadd -u will respect the UPGRADE NO directives by moving the outdated paths into /var/lib/pkg/rejected. It then becomes the user's responsibility to run rejmerge and decide what to do with any files that are no longer claimed by the new version of the package. Test case: mpv#0.36.0-1.pkg.tar.xz contains /etc/mpv/encoding-profiles.conf whereas mpv#0.36.0-2.pkg.tar.xz has instead /usr/etc/mpv/encoding-profiles.conf, and the directive "UPGRADE ^etc/.*$ NO" appears in pkgadd.conf. Running pkgadd -u mpv#0.36.0-2.pkg.tar.xz moves the old config file to /var/lib/pkg/rejected/etc/mpv/encoding-profiles.conf, and deletes the (now-empty) directory /etc/mpv. The UPGRADE NO directive has been respected in a way that satisfies the original bug report, without any "leak" of stale files into the sysroot. diff --git a/pkgadd.cc b/pkgadd.cc index 231f5ba3..946b9faf 100644 --- a/pkgadd.cc +++ b/pkgadd.cc @@ -99,17 +99,19 @@ void pkgadd::run(int argc, char** argv) } } - set keep_list; + set keep_new; if (o_upgrade) { - keep_list = make_keep_list(package.second.files, config_rules); - db_rm_pkg(package.first, keep_list); + keep_new = make_keep_list(package.second.files, config_rules); + set files_old = packages[package.first].files; + set keep_old = make_keep_list(files_old, config_rules); + db_rm_pkg(package.first, keep_old, keep_new); } db_add_pkg(package.first, package.second); db_commit(); try { - pkg_install(o_package, keep_list, non_install_files, installed); + pkg_install(o_package, keep_new, non_install_files, installed); } catch (runtime_error&) { if (!installed) { db_rm_pkg(package.first); diff --git a/pkgutil.cc b/pkgutil.cc index bf1a0aa6..0e7143a1 100644 --- a/pkgutil.cc +++ b/pkgutil.cc @@ -22,6 +22,7 @@ #include "pkgutil.h" #include +#include #include #include #include @@ -199,7 +200,12 @@ void pkgutil::db_rm_pkg(const string& name) } } -void pkgutil::db_rm_pkg(const string& name, const set& keep_list) +/* Three-argument db_rm_pkg (to address FS#1074) + * arg1: name of the package being upgraded + * arg2: keep list from the old version of the package + * arg3: keep list from the new version of the package */ +void pkgutil::db_rm_pkg(const string& name, const set& keep_old, + const set& keep_new) { set files = packages[name].files; packages.erase(name); @@ -210,9 +216,33 @@ void pkgutil::db_rm_pkg(const string& name, const set& keep_list) cerr << endl; #endif - // Don't delete files found in the keep list - for (set::const_iterator i = keep_list.begin(); i != keep_list.end(); ++i) - files.erase(*i); + // Files common to both old and new packages, matching an "UPGRADE NO" + // rule, can be left in place (won't be clobbered later by pkg_install). + // Files that only exist in the old package will be stashed in reject_dir. + const string reject_dir = trim_filename(root + string("/") + string(PKG_REJECTED)); + for (set::const_iterator i = keep_old.begin(); i != keep_old.end(); ++i) { + // Exempt directories, which might be shared among other ports, + // but if db_rm_files empties them out, allow them to be deleted. + if ((*i)[i->length()-1] == '/') { + continue; + } else { + files.erase(*i); + } + + if ( keep_new.find(*i) == keep_new.end() ) { + const string filename = root + *i; + const string savname = trim_filename(reject_dir + filename); + char* savpath = strdup(const_cast( savname.c_str() )); + const string savdir = dirname(savpath); + std::filesystem::create_directories(savdir); + if ( file_exists(filename) && + rename(filename.c_str(),savname.c_str()) == -1 ) { + const char* msg = strerror(errno); + cerr << utilname << ": could not rename " << filename + << ": " << msg << endl; + } + } + } #ifndef NDEBUG cerr << "Removing package phase 2 (files that is in the keep list excluded):" << endl; diff --git a/pkgutil.h b/pkgutil.h index 7d18c5ea..d5be600d 100644 --- a/pkgutil.h +++ b/pkgutil.h @@ -65,7 +65,7 @@ protected: void db_add_pkg(const string& name, const pkginfo_t& info); bool db_find_pkg(const string& name); void db_rm_pkg(const string& name); - void db_rm_pkg(const string& name, const set& keep_list); + void db_rm_pkg(const string& name, const set& keep_old, const set& keep_new); void db_rm_files(set files, const set& keep_list); set db_find_conflicts(const string& name, const pkginfo_t& info); diff --git a/rejmerge.in b/rejmerge.in index ae93489a..2f5c5e35 100644 --- a/rejmerge.in +++ b/rejmerge.in @@ -155,6 +155,32 @@ diff_menu() { : > "$TMPFILE" } +relic_menu() { + while true; do + info "$(basename "$1") has been disowned by the package that installed it." + file "$1" + while true; do + info_n "[R]estore [M]ove to other location [D]elete? " + read -n1 CMD + echo + + case "$CMD" in + m|M) info_n "New (absolute) path?" + read DEST + mv "$1" "$DEST" || { info "unable to write to $DEST"; break 1; } + break 2 + ;; + r|R) mv "$1" "${1##$REJECTED_DIR}" || { info "unable to restore ${1##$REJECTED_DIR}"; break 1; } + break 2 + ;; + d|D) rm -f "$1" + break 2 + ;; + esac + done + done +} + file_menu() { while true; do info "$1" @@ -253,9 +279,9 @@ main() { for REJECTED_FILE in $(find $REJECTED_DIR ! -type d); do INSTALLED_FILE="$REJMERGE_ROOT${REJECTED_FILE##$REJECTED_DIR}" - # Remove rejected file if there is no installed version + # If there is no copy on sysroot, the file is probably stale if [ ! -e "$INSTALLED_FILE" ]; then - rm -f "$REJECTED_FILE" + relic_menu "$REJECTED_FILE" continue fi