Add APT package hold management to Package Updates

- Add an APT-only “Held updates” view while keeping held packages excluded from normal and scheduled updates.

- Allow administrators to hold or unhold packages from package details.

- Allow explicitly selected held packages to be updated once, then restore all existing package holds.

- Preserve configured `aptitude` behavior for regular installs while using `apt-get` for the held-package override.

Fixes #2689
This commit is contained in:
Ilia Ross
2026-08-14 03:43:50 +02:00
parent 10094a7efb
commit e695fbd0c7
12 changed files with 404 additions and 67 deletions
+2
View File
@@ -1,3 +1,5 @@
---- Changes since 2.641 ----
Added a Held updates view with controls to hold, unhold or explicitly update APT-held packages.
---- Changes since 1.490 ----
First version of this module.
---- Changes since 1.500 ----
+23 -12
View File
@@ -9,6 +9,7 @@ if ($in{'clear'}) {
$in{'search'} = '';
}
$has_repos = defined(&software::list_package_repos);
$has_holds = &supports_package_holds();
# Start of mode tabs
print &ui_tabs_start([ [ 'pkgs', $text{'index_tabpkgs'} ],
@@ -23,7 +24,10 @@ $in{'mode'} ||= 'updates';
# Show mode selector (all, updates only, updates and new)
@grid = ( );
foreach $m ('current', 'updates', 'security', 'new') {
@modes = ('current', 'updates');
push(@modes, 'held') if ($has_holds);
push(@modes, 'security', 'new');
foreach $m (@modes) {
$mmsg = $text{'index_mode_'.$m};
if ($in{'mode'} eq $m) {
push(@mlinks, "<b>$mmsg</b>");
@@ -68,15 +72,21 @@ foreach $p (sort { $a->{'name'} cmp $b->{'name'} } (@current, @avail)) {
$c = $current{$p->{'name'}."/".$p->{'system'}};
$a = $avail{$p->{'name'}."/".$p->{'system'}};
if ($a && $c && (&compare_versions($a, $c) > 0 || $upmode)) {
if ($a && $c && (&compare_versions($a, $c) > 0 || $upmode ||
$in{'mode'} eq 'held')) {
# An update is available
$msg = "<b><font color=#00aa00>".
&text('index_new', $a->{'version'})."</font></b>";
$need = 1;
$msg = $a->{'held'} ?
"<b><font color=#ffaa00>".
&text('index_held', $c->{'version'}, $a->{'version'}).
"</font></b>" :
"<b><font color=#00aa00>".
&text('index_new', $a->{'version'})."</font></b>";
$need = $a->{'held'} ? 0 : 1;
next if ($in{'mode'} eq 'security' && !$a->{'security'});
next if ($in{'mode'} ne 'updates' &&
$in{'mode'} ne 'current' &&
$in{'mode'} ne 'security');
$in{'mode'} ne 'security' &&
$in{'mode'} ne 'held');
}
elsif ($a && !$c) {
# Could be installed, but isn't currently
@@ -143,18 +153,20 @@ if ($in{'mode'} eq 'new' && !$in{'search'}) {
}
else {
# Show the packages, if any
$update_label = $in{'mode'} eq 'new' ? $text{'index_install'} :
$in{'mode'} eq 'held' ? $text{'index_updateheld'} :
$text{'index_update'};
if (@rows) {
print &text('index_count', scalar(@rows)),"<br>\n";
print &ui_form_start("update.cgi", "post");
print &ui_submit($in{'mode'} eq 'new' ? $text{'index_install'}
: $text{'index_update'}, "ok_top" );
print &ui_submit($update_label, "ok_top" );
print &ui_submit($text{'index_refresh'}, "refresh_top"), "<br>";
}
@buttons = ( [ "ok", $update_label ] );
push(@buttons, [ "refresh", $text{'index_refresh'} ]);
print &ui_form_columns_table(
"",
[ [ "ok", $in{'mode'} eq 'new' ? $text{'index_install'}
: $text{'index_update'} ],
[ "refresh", $text{'index_refresh'} ] ],
\@buttons,
1,
undef,
[ [ "mode", $in{'mode'} ],
@@ -296,4 +308,3 @@ if ($has_repos) {
print &ui_tabs_end(1);
&ui_print_footer("/", $text{'index'});
+22
View File
@@ -6,8 +6,10 @@ index_source=Source
index_bad2=Update to version $1 not yet available : $1
index_bad=Update to version $1 advised : $2
index_new=New version $1
index_held=Held at version $1; version $2 is available
index_ok=Running latest $1
index_update=Update Selected Packages
index_updateheld=Update Selected Held Packages
index_install=Install Selected Packages
index_return=package list
index_header=Scheduled checking options
@@ -39,6 +41,7 @@ index_webmintheme=Webmin theme $1
index_mode=States to display:
index_mode_current=Installed
index_mode_updates=Only updates
index_mode_held=Held updates
index_mode_new=Only new
index_mode_security=Only security updates
index_allsel=Packages to show:
@@ -46,6 +49,7 @@ index_all_0=Only Virtualmin related
index_all_1=All packages
index_none_all=No packages managed by an update system were found on your system!
index_none_updates=No packages available to be updated were found.
index_none_held=No held packages with available updates were found.
index_none_both=No new packages or packages available to be updated were found.
index_none_new=No new packages for installation were found.
index_none_security=No packages available for security updates were found.
@@ -100,6 +104,10 @@ update_rusure=Are you sure you wish to install the $1 packages listed below? Thi
update_oldver=Current version
update_newver=New version
update_confirm=Install Now
update_confirmheld=Update Held Packages
update_heldnote=These packages are held. This action explicitly updates them once, and leaves them held for future updates.
update_enotheld=Package $1 is not currently held by APT
update_enoheldops=No update operation was found for the selected held packages. Refresh the package list and try again.
update_none=None
update_ops=Building complete list of packages ..
update_rebootdesc=One of the installed packages requires a reboot to be fully applied.
@@ -114,6 +122,8 @@ log_schedup=Background installed $1 updated packages
log_sched=Enabled scheduled updates
log_unsched=Disabled scheduled updates
log_refresh=Refreshed available packages
log_hold=Held updates for $1 packages
log_unhold=Unheld updates for $1 packages
log_enable_repos=Enabled $1 package repositories
log_disable_repos=Disabled $1 package repositories
log_delete_repos=Deleted $1 package repositories
@@ -140,6 +150,10 @@ view_source=Installation source
view_changelog=Changelog for available version
view_software=Manage Package
view_update=Update Package
view_updateheld=Update Held Package
view_hold=Hold Package
view_unhold=Unhold Package
view_held=Held at version $1
view_install=Install Package
system_yum=YUM
@@ -159,4 +173,12 @@ repos_title=Delete Repositories
repos_rusure=Are you sure you want to delete the $1 selected package repositories? Packages installed from them will still be available, but may not be updatable.
repos_ok=Delete Now
hold_enotsupported=The active package update system does not support package holds
hold_enone=No packages were selected
hold_enotinstalled=Package $1 is not installed
hold_enotheld=Package $1 is not currently held
hold_esystem=Package $1 is not managed by the active package update system
hold_efailed=Failed to hold packages: $1
unhold_efailed=Failed to unhold packages: $1
__norefs=1
+3
View File
@@ -21,6 +21,9 @@ elsif ($action eq 'update') {
elsif ($action eq 'schedup') {
return &text('log_schedup', $object);
}
elsif ($action eq 'hold' || $action eq 'unhold') {
return &text('log_'.$action, $object);
}
elsif ($action eq 'sched') {
return $text{$object ? 'log_sched' : 'log_unsched'};
}
+69 -15
View File
@@ -21,6 +21,7 @@ eval "use WebminCore;";
$available_cache_file = &cache_file_path("available.cache");
$current_cache_file = &cache_file_path("current.cache");
$updates_cache_file = &cache_file_path("updates.cache");
$held_updates_cache_file = &cache_file_path("held-updates.cache");
$cron_cmd = "$module_config_directory/update.pl";
$yum_cache_file = &cache_file_path("yumcache");
@@ -308,30 +309,74 @@ sub supports_updates_available
return defined(&software::update_system_updates);
}
# updates_available(no-cache)
# supports_package_holds()
# Returns true if the current update system can list and change package holds.
sub supports_package_holds
{
return defined(&software::list_update_system_holds) &&
defined(&software::update_system_hold);
}
# list_package_holds()
# Returns the package names currently held by the update system.
sub list_package_holds
{
return ( ) if (!&supports_package_holds());
return &software::list_update_system_holds();
}
# package_is_held(package, [holds])
# Returns true if a package is in a supplied or freshly-read list of holds.
sub package_is_held
{
my ($name, $holds) = @_;
my @holds = $holds ? @$holds : &list_package_holds();
return 1 if (grep { $_ eq $name } @holds);
if ($software::update_system eq 'apt' &&
defined(&software::strip_apt_package_arch)) {
my $base = &software::strip_apt_package_arch($name);
return 1 if (grep {
&software::strip_apt_package_arch($_) eq $base
} @holds);
}
return 0;
}
# update_package_holds(&packages, hold)
# Holds or unholds packages. Returns undef on success, or an error message.
sub update_package_holds
{
my ($packages, $hold) = @_;
return $text{'hold_enotsupported'} if (!&supports_package_holds());
return &software::update_system_hold($packages, $hold);
}
# updates_available(no-cache, [include-held])
# Returns an array of hash refs of package updates available, according to
# the update system, with caching.
sub updates_available
{
my ($nocache) = @_;
if (!scalar(@updates_available_cache)) {
if ($nocache || &cache_expired($updates_cache_file)) {
my ($nocache, $include_held) = @_;
my $cache_file = $include_held ? $held_updates_cache_file :
$updates_cache_file;
my $cache = $include_held ? \@held_updates_available_cache :
\@updates_available_cache;
if (!scalar(@$cache)) {
if ($nocache || &cache_expired($cache_file)) {
# Get from original source
@updates_available_cache = &software::update_system_updates();
foreach my $a (@updates_available_cache) {
@$cache = &software::update_system_updates($include_held);
foreach my $a (@$cache) {
$a->{'update'} = $a->{'name'};
$a->{'system'} = $software::update_system;
}
&write_cache_file($updates_cache_file,
\@updates_available_cache);
&write_cache_file($cache_file, $cache);
}
else {
# Use on-disk cache
@updates_available_cache =
&read_cache_file($updates_cache_file);
@$cache = &read_cache_file($cache_file);
}
}
return @updates_available_cache;
return @$cache;
}
# package_install(package-name, [system], [new-install], [flags])
@@ -343,12 +388,14 @@ my ($name, $system, $install, $flags) = @_;
$system ||= $software::update_system;
my @rv;
my $pkg;
my $include_held = $system eq 'apt' && defined($flags) &&
$flags eq '--allow-change-held-packages';
# First get from list of updates
($pkg) = grep { $_->{'update'} eq $name &&
($_->{'system'} eq $system || !$system) }
sort { &compare_versions($b, $a) }
&list_possible_updates(0);
&list_possible_updates(0, 0, $include_held);
if (!$pkg) {
# Then try list of all available packages
($pkg) = grep { $_->{'update'} eq $name &&
@@ -458,14 +505,14 @@ if (defined(&software::update_system_operations)) {
return ( );
}
# list_possible_updates([nocache], [nocache-no-data])
# list_possible_updates([nocache], [nocache-no-data], [include-held])
# Returns a list of updates that are available. Each element in the array
# is a hash ref containing a name, version, description and severity flag.
# Intended for calling from themes. Nocache 0=cache everything, 1=flush all
# caches, 2=flush only current. Nocache-no-data prohibits collecting data
sub list_possible_updates
{
my ($nocache, $nocache_no_data) = @_;
my ($nocache, $nocache_no_data, $include_held) = @_;
my @rv;
return @rv if ($nocache_no_data);
my @current = &list_current($nocache);
@@ -476,9 +523,10 @@ if (&supports_updates_available()) {
foreach my $c (@current) {
$currentmap{$c->{'name'},$c->{'system'}} ||= $c;
}
foreach my $a (&updates_available($nocache == 1)) {
foreach my $a (&updates_available($nocache == 1, $include_held)) {
my $c = $currentmap{$a->{'name'},$a->{'system'}};
next if (!$c);
next if ($a->{'held'} && !$include_held);
next if ($a->{'version'} eq $c->{'version'} &&
$a->{'epoch'} eq $c->{'epoch'});
push(@rv, { 'name' => $a->{'name'},
@@ -489,6 +537,7 @@ if (&supports_updates_available()) {
'epoch' => $a->{'epoch'},
'oldepoch' => $c->{'epoch'},
'security' => $a->{'security'},
'held' => $a->{'held'},
'source' => $a->{'source'},
'desc' => $c->{'desc'} || $a->{'desc'} });
}
@@ -700,10 +749,13 @@ sub flush_package_caches
{
unlink($current_cache_file);
unlink($updates_cache_file);
unlink($held_updates_cache_file);
unlink($available_cache_file);
unlink($available_cache_file.'0');
unlink($available_cache_file.'1');
@packages_available_cache = ( );
@updates_available_cache = ( );
@held_updates_available_cache = ( );
%read_cache_file_cache = ( );
}
@@ -713,6 +765,8 @@ unlink($available_cache_file.'1');
sub list_for_mode
{
my ($mode, $nocache) = @_;
return grep { $_->{'held'} }
&list_possible_updates($nocache, 0, 1) if ($mode eq 'held');
return $mode eq 'updates' || $mode eq 'security' ?
&list_possible_updates($nocache) : &list_available($nocache);
}
+8 -2
View File
@@ -8,8 +8,14 @@ if ($in{'software'}) {
&redirect("../software/edit_pack.cgi?package=".&urlize($in{'name'}).
"&version=".&urlize($in{'version'}));
}
else {
elsif ($in{'hold'} || $in{'unhold'}) {
$action = $in{'hold'} ? "hold" : "unhold";
&redirect("update.cgi?u=".&urlize($in{'name'}."/".$in{'system'}).
"&all=$in{'all'}&mode=$in{'mode'}");
"&$action=1&mode=".&urlize($in{'mode'}));
}
else {
$mode = $in{'held'} ? "held" : $in{'mode'};
&redirect("update.cgi?u=".&urlize($in{'name'}."/".$in{'system'}).
"&all=$in{'all'}&mode=".&urlize($mode));
}
+56 -4
View File
@@ -19,7 +19,38 @@ else {
$redir =~ /\?/ ? "$redir&tab=pkgs" : "$redir?tab=pkgs";
}
if ($in{'refresh'} || $in{'refresh_top'}) {
$hold_action = $in{'hold'} ? 1 : $in{'unhold'} ? 0 : undef;
if (defined($hold_action)) {
# Hold or unhold selected packages
&supports_package_holds() || &error($text{'hold_enotsupported'});
@holdpkgs = split(/\0/, $in{'u'});
@holdpkgs || &error($text{'hold_enone'});
@current = &list_current(1);
%current = map { $_->{'name'}."/".$_->{'system'}, 1 } @current;
@held = &list_package_holds();
@holdnames = ( );
foreach $ps (@holdpkgs) {
($p, $s) = split(/\//, $ps, 2);
$current{$p."/".$s} || &error(&text('hold_enotinstalled', $p));
$s eq $software::update_system ||
&error(&text('hold_esystem', $p));
if (!$hold_action && !&package_is_held($p, \@held)) {
&error(&text('hold_enotheld', $p));
}
push(@holdnames, $p);
}
@holdnames = &unique(@holdnames);
$err = &update_package_holds(\@holdnames, $hold_action);
&error(&text($hold_action ? 'hold_efailed' : 'unhold_efailed', $err))
if ($err);
&flush_package_caches();
$logaction = $hold_action ? 'hold' : 'unhold';
&webmin_log($logaction, "packages", scalar(@holdnames),
{ 'packages' => \@holdnames });
&redirect("index.cgi?mode=".&urlize($in{'mode'}).
"&search=".&urlize($in{'search'}));
}
elsif ($in{'refresh'} || $in{'refresh_top'}) {
&ui_print_unbuffered_header(undef, $text{'refresh_title'}, "");
# Clear all caches
@@ -40,6 +71,21 @@ else {
# Upgrade some packages
my @pkgs = split(/\0/, $in{'u'});
@pkgs || &error($text{'update_enone'});
$allow_held = 0;
if ($in{'mode'} eq 'held') {
# The held-updates page is the only UI that can explicitly
# override an APT hold for a single update transaction.
&supports_package_holds() || &error($text{'hold_enotsupported'});
@held = &list_package_holds();
foreach $ps (@pkgs) {
($p, $s) = split(/\//, $ps, 2);
$s eq 'apt' && &package_is_held($p, \@held) ||
&error(&text('update_enotheld', $p));
}
$allow_held = 1;
}
$install_flags = $allow_held ? '--allow-change-held-packages' :
$in{'flags'};
&ui_print_unbuffered_header(undef,
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
@@ -57,6 +103,7 @@ else {
push(@pkgnames, $p);
}
@ops = &list_package_operations(join(" ", @pkgnames), $s);
&error($text{'update_enoheldops'}) if (!@ops && $allow_held);
}
if (@ops) {
@@ -74,9 +121,14 @@ else {
foreach $ps (@pkgs) {
$confform .= &ui_hidden("u", $ps);
}
$confform .= &ui_alert_box($text{'update_heldnote'},
'warn', undef, undef, '')
if ($allow_held && !$bottom);
$confform .= &text('update_rusure', scalar(@ops)),"<p>\n"
if (!$bottom);
$confform .= &ui_form_end([ [ "confirm", $text{'update_confirm'} ] ]);
$confform .= &ui_form_end([ [ "confirm",
$allow_held ? $text{'update_confirmheld'} :
$text{'update_confirm'} ] ]);
};
print &$getconfform();
@@ -127,7 +179,7 @@ else {
"<br>\n";
print "<ul data-package-updates='1'>\n";
@got = &package_install_multiple(
\@pkgnames, $pkgsystem, $in{'mode'} eq 'new', $in{'flags'});
\@pkgnames, $pkgsystem, $in{'mode'} eq 'new', $install_flags);
print "</ul><br>\n";
}
else {
@@ -138,7 +190,7 @@ else {
print &text($msg, "<tt>@{[&html_escape($p)]}</tt>"),"<br>\n";
print "<ul data-package-updates='2'>\n";
@pgot = &package_install(
$p, $s, $in{'mode'} eq 'new', $in{'flags'});
$p, $s, $in{'mode'} eq 'new', $install_flags);
foreach $g (@pgot) {
$donedep{$g}++;
}
+16 -1
View File
@@ -13,12 +13,17 @@ require './package-updates-lib.pl';
($c) = grep { $_->{'name'} eq $in{'name'} &&
$_->{'system'} eq $in{'system'} } @current;
$p = $a || $c;
$has_holds = &supports_package_holds();
$held = $has_holds && $c &&
$c->{'system'} eq $software::update_system &&
&package_is_held($p->{'name'});
print &ui_form_start("save_view.cgi");
print &ui_hidden("name", $p->{'name'});
print &ui_hidden("system", $p->{'system'});
print &ui_hidden("version", $p->{'version'});
print &ui_hidden("mode", $in{'mode'});
print &ui_hidden("held", $held);
print &ui_table_start($text{'view_header'}, undef, 2);
# Package name and type
@@ -29,6 +34,11 @@ print &ui_table_row($text{'view_desc'}, $p->{'desc'});
# Current state
print &ui_table_row($text{'view_state'},
$held && $a && $c && &compare_versions($a, $c) > 0 ?
"<font color=#ffaa00>".
&text('index_held', $c->{'version'}, $a->{'version'})."</font>" :
$held && $c ? "<font color=#ffaa00>".
&text('view_held', $c->{'version'})."</font>" :
$a && !$c ? "<font color=#00aa00>$text{'index_caninstall'}</font>" :
!$a && $c ? "<font color=#ffaa00>".
&text('index_noupdate', $c->{'version'})."</font>" :
@@ -69,11 +79,16 @@ if ($c && &foreign_available("software") && $c->{'software'}) {
push(@buts, [ "software", $text{'view_software'} ]);
}
if ($a && $c && &compare_versions($a, $c) > 0) {
push(@buts, [ "update", $text{'view_update'} ]);
push(@buts, [ "update", $held ? $text{'view_updateheld'} :
$text{'view_update'} ]);
}
elsif ($a && !$c) {
push(@buts, [ "update", $text{'view_install'} ]);
}
if ($c && $has_holds && $c->{'system'} eq $software::update_system) {
push(@buts, [ $held ? "unhold" : "hold",
$held ? $text{'view_unhold'} : $text{'view_hold'} ]);
}
print &ui_form_end(\@buts);
&ui_print_footer("index.cgi?mode=$in{'mode'}&search=".
+1
View File
@@ -1,5 +1,6 @@
---- Changes since 2.641 ----
Fix Alpine Linux mysql/mariadb package installs names due missing server utils (means at least Alpine Linux package installation is supported since Alpine linux v 3.16 up to edge)
Added APT functions for listing, holding, unholding and explicitly updating held packages.
---- Changes since 1.130 ----
Packages can now be installed directly from yum, if installed.
The entire system can also be upgraded from yum.
+93 -31
View File
@@ -20,20 +20,33 @@ $name =~ s/:[A-Za-z0-9][A-Za-z0-9._-]*$//;
return $name;
}
# update_system_install([package], [&in], [no-force])
# update_system_install([package], [&in], [no-force], [flags])
# Install some package with apt
sub update_system_install
{
local $update = $_[0] || $in{'update'};
local $force = !$_[2];
local $flags = $_[3];
local (@rv, @newpacks);
# Only accept the one flag needed for an explicit update of held packages.
# Other update systems use this argument for their own package-manager flags,
# but APT historically ignored it.
local $holdflag = defined($flags) &&
$flags eq '--allow-change-held-packages'
? ' --allow-change-held-packages'
: '';
local $install_command = $holdflag ? 'apt-get' : $apt_get_command;
local @rehold = $holdflag ? &list_update_system_holds() : ( );
# Build the command to run
$ENV{'UCF_FORCE_CONFFOLD'} = 'YES';
$ENV{'DEBIAN_FRONTEND'} = 'noninteractive';
local $uicmd = "$apt_get_command -y ".($force ? " -f" : "")." install $update";
local $uicmd = "$install_command -y".$holdflag.
($force ? " -f" : "")." install $update";
$update = join(" ", map { quotemeta($_) } split(/\s+/, $update));
local $cmd = "$apt_get_command -y ".($force ? " -f" : "")." install $update";
local $cmd = "$install_command -y".$holdflag.
($force ? " -f" : "")." install $update";
print &text('apt_install', "<tt>".&html_escape($uicmd)."</tt>"),"\n";
print "<pre data-installer>";
&additional_log('exec', undef, $cmd);
@@ -68,14 +81,27 @@ while(<CMD>) {
print &html_escape("$_");
}
close(CMD);
local $status = $?;
# Restore holds after --allow-change-held-packages, which applies to the whole
# transaction and can clear holds on selected packages or dependencies.
if (@rehold) {
local $rehold_error = &update_system_hold(\@rehold, 1);
if ($rehold_error) {
print &text('apt_reholdfailed',
"<tt>".&html_escape(join(" ", @rehold))."</tt>",
&html_escape($rehold_error)),"<p>\n";
}
}
&reset_environment();
if (!@rv && $config{'package_system'} ne 'debian' && !$?) {
if (!@rv && $config{'package_system'} ne 'debian' && !$status) {
# Other systems don't list the packages installed!
@rv = @newpacks;
}
print "</pre>\n";
if ($?) { print "$text{'apt_failed'}<p>\n"; }
if ($status) { print "$text{'apt_failed'}<p>\n"; }
else { print "$text{'apt_ok'}<p>\n"; }
$? = $status;
return @rv;
}
@@ -239,21 +265,13 @@ return @rv;
# Returns a list of available package updates
sub update_system_updates
{
my ($include_holds) = @_;
&execute_command("$apt_get_command update");
# Find held packages by dpkg
local %holds;
if ($config{'package_system'} eq 'debian') {
&clean_language();
&open_execute_command(HOLDS, "dpkg --get-selections", 1, 1);
while(<HOLDS>) {
if (/^(\S+)\s+hold/) {
$holds{$1}++;
}
}
close(HOLDS);
&reset_environment();
}
# Find held packages. By default these remain excluded, but callers can ask
# for them so that a dedicated held-updates view can display them.
local %holds = map { &strip_apt_package_arch($_), 1 }
&list_update_system_holds();
if (&has_command("apt-show-versions")) {
# This awesome command can give us all updates in one hit, and takes
@@ -264,7 +282,7 @@ if (&has_command("apt-show-versions")) {
&open_execute_command(PKGS, "apt-show-versions 2>/dev/null", 1, 1);
while(<PKGS>) {
if (/^(\S+)\/(\S+)\s+upgradeable\s+from\s+(\S+)\s+to\s+(\S+)/ &&
!$holds{$1}) {
($include_holds || !$holds{&strip_apt_package_arch($1)})) {
# Old format
local $pkg = { 'name' => $1,
'source' => $2,
@@ -272,9 +290,12 @@ if (&has_command("apt-show-versions")) {
if ($pkg->{'version'} =~ s/^(\S+)://) {
$pkg->{'epoch'} = $1;
}
$pkg->{'held'} = 1
if ($holds{&strip_apt_package_arch($pkg->{'name'})});
push(@rv, $pkg);
}
elsif (/^(\S+):(\S+)\/(\S+)\s+(\S+)\s+upgradeable\s+to\s+(\S+)/ && !$holds{$1}) {
elsif (/^(\S+):(\S+)\/(\S+)\s+(\S+)\s+upgradeable\s+to\s+(\S+)/ &&
($include_holds || !$holds{$1})) {
# New format, like
# libgomp1:i386/unstable 4.8.2-2 upgradeable to 4.8.2-4
local $pkg = { 'name' => $1,
@@ -284,12 +305,14 @@ if (&has_command("apt-show-versions")) {
if ($pkg->{'version'} =~ s/^(\S+)://) {
$pkg->{'epoch'} = $1;
}
$pkg->{'held'} = 1 if ($holds{$pkg->{'name'}});
push(@rv, $pkg);
}
}
close(PKGS);
&reset_environment();
@rv = &filter_held_packages(@rv);
@rv = grep { !$holds{&strip_apt_package_arch($_->{'name'})} } @rv
if (!$include_holds);
foreach my $pkg (@rv) {
$pkg->{'security'} = 1 if ($pkg->{'source'} =~ /security/i);
}
@@ -301,7 +324,8 @@ elsif (&has_command("apt")) {
&clean_language();
&open_execute_command(PKGS, "apt list --upgradable 2>/dev/null", 1, 1);
while(<PKGS>) {
if (/^(\S+)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable\s+from:\s+(\S+)\]/ && !$holds{$1}) {
if (/^(\S+)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable\s+from:\s+(\S+)\]/ &&
($include_holds || !$holds{&strip_apt_package_arch($1)})) {
local $pkg = { 'name' => $1,
'source' => $2,
'version' => $3,
@@ -310,12 +334,15 @@ elsif (&has_command("apt")) {
$pkg->{'epoch'} = $1;
}
$pkg->{'source'} =~ s/,.*$//;
$pkg->{'held'} = 1
if ($holds{&strip_apt_package_arch($pkg->{'name'})});
push(@rv, $pkg);
}
}
close(PKGS);
&reset_environment();
@rv = &filter_held_packages(@rv);
@rv = grep { !$holds{&strip_apt_package_arch($_->{'name'})} } @rv
if (!$include_holds);
foreach my $pkg (@rv) {
$pkg->{'security'} = 1 if ($pkg->{'source'} =~ /security/i);
}
@@ -334,7 +361,8 @@ else {
$currentmap{$pkg->{'name'}} ||= $pkg;
}
local @rv;
local @names = grep { !$holds{$_} } keys %currentmap;
local @names = $include_holds ? keys %currentmap :
grep { !$holds{$_} } keys %currentmap;
while(scalar(@names)) {
local @somenames;
if (scalar(@names) > 100) {
@@ -371,6 +399,8 @@ else {
&compare_versions($pkg->{'version'},
$pkg->{'oldversion'});
if ($newer > 0) {
$pkg->{'held'} = 1
if ($holds{$pkg->{'name'}});
push(@rv, $pkg);
}
}
@@ -378,7 +408,8 @@ else {
close(PKGS);
&reset_environment();
}
@rv = &filter_held_packages(@rv);
@rv = grep { !$holds{&strip_apt_package_arch($_->{'name'})} } @rv
if (!$include_holds);
&set_pinned_versions(\@rv);
return @rv;
}
@@ -411,11 +442,10 @@ close(PKGS);
&reset_environment();
}
# filter_held_packages(package, ...)
# Returns a list of package updates, minus those that are held
sub filter_held_packages
# list_update_system_holds()
# Returns the unique names of all packages currently held by APT or dpkg.
sub list_update_system_holds
{
my @pkgs = @_;
my %hold;
# Get holds from dpkg
@@ -447,15 +477,47 @@ if (&has_command("apt-mark")) {
&clean_language();
&open_execute_command(PKGS, "apt-mark showhold 2>/dev/null", 1, 1);
while(<PKGS>) {
if (/^([^:\s]+)/) {
if (/^(\S+)/) {
$hold{$1} = 1;
}
}
close(PKGS);
&reset_environment();
}
return sort keys %hold;
}
return grep { !$hold{$_->{'name'}} } @pkgs;
# update_system_hold(&packages, hold)
# Holds or unholds a list of packages. Returns undef on success, or an error.
sub update_system_hold
{
my ($packages, $hold) = @_;
return "The apt-mark command is not installed"
if (!&has_command("apt-mark"));
my @packages = &unique(@$packages);
return "No packages were specified" if (!@packages);
my $action = $hold ? 'hold' : 'unhold';
my $cmd = "apt-mark $action ".
join(" ", map { quotemeta($_) } @packages);
my $out;
&clean_language();
my $status = &execute_command_logged($cmd, undef, \$out, \$out);
&reset_environment();
if ($status) {
$out = &trim($out);
return $out || "apt-mark $action failed";
}
return undef;
}
# filter_held_packages(package, ...)
# Returns a list of package updates, minus those that are held
sub filter_held_packages
{
my @pkgs = @_;
my %hold = map { &strip_apt_package_arch($_), 1 }
&list_update_system_holds();
return grep { !$hold{&strip_apt_package_arch($_->{'name'})} } @pkgs;
}
# list_package_repos()
+1
View File
@@ -235,6 +235,7 @@ apt_input=Package from APT
apt_install=Installing package(s) with command $1 ..
apt_ok=.. install complete
apt_failed=.. install failed!
apt_reholdfailed=.. failed to restore the hold on $1 : $2
apt_form=Upgrade All Packages
apt_header=APT package upgrade options
apt_update=Resynchronize package list (<tt>update</tt>)
+110 -2
View File
@@ -8,6 +8,7 @@ use File::Spec;
use Cwd qw(abs_path);
our %config;
our $apt_get_command;
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..'));
chdir($root) or die "chdir($root): $!";
@@ -39,11 +40,21 @@ is($ops[0]->{'name'}, 'libtinfo6',
no warnings qw(once redefine);
my $apt_output = "Setting up libtinfo6:amd64 (6.3-2ubuntu0.2) ...\n";
my $yes_input = "";
my $executed_command = "";
my @reheld;
local *additional_log = sub { };
local *backquote_logged = sub { return ""; };
local *clean_language = sub { };
local *html_escape = sub { return $_[0]; };
local *reset_environment = sub { };
local *list_update_system_holds = sub {
return ('libtinfo6:amd64', 'held-dependency:i386');
};
local *update_system_hold = sub {
my ($packages, $hold) = @_;
@reheld = @$packages if ($hold);
return undef;
};
local *text = sub { return $_[0]; };
local *transname = sub { return "/tmp/software-apt-test-yes"; };
local *open_tempfile = sub {
@@ -64,19 +75,116 @@ local *close_tempfile = sub {
close(ref($fh) ? $fh : \*{$fh});
};
local *open_execute_command = sub {
my ($fh) = @_;
my ($fh, $command) = @_;
$executed_command = $command;
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, "<", \$apt_output)
or die "open simulated apt output: $!";
};
local $config{'package_system'} = 'debian';
local $apt_get_command = 'aptitude';
my $printed = "";
open(my $stdout, ">", \$printed) or die "open captured stdout: $!";
local *STDOUT = $stdout;
my @installed = update_system_install('libtinfo6', undef, 1);
my @installed = update_system_install(
'libtinfo6', undef, 1, '--allow-change-held-packages');
is_deeply(\@installed, [ 'libtinfo6' ],
'normalizes package names returned by apt install output');
like($executed_command, qr/apt-get -y --allow-change-held-packages install/,
'uses apt-get for the held-package override even in aptitude mode');
is_deeply(\@reheld, [ 'libtinfo6:amd64', 'held-dependency:i386' ],
'restores all exact holds after explicitly updating a held package');
$executed_command = "";
@reheld = ( );
update_system_install('libtinfo6', undef, 1);
like($executed_command, qr/^aptitude -y install/,
'continues using configured aptitude mode for regular installs');
is_deeply(\@reheld, [ ], 'does not reapply holds after a regular install');
}
{
no warnings qw(once redefine);
local *clean_language = sub { };
local *reset_environment = sub { };
local *has_command = sub {
return $_[0] eq 'aptitude' || $_[0] eq 'apt-mark';
};
local *open_execute_command = sub {
my ($fh, $command) = @_;
my $output = $command =~ /^dpkg / ?
"alpha hold\ndelta:amd64 hold\n" :
$command =~ /^aptitude / ?
".h beta 1.0 installed\n" :
$command =~ /^apt-mark / ?
"gamma\ndelta:amd64\n" : "";
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
or die "open simulated holds: $!";
};
is_deeply([ list_update_system_holds() ],
[ qw(alpha beta delta:amd64 gamma) ],
'combines held packages without discarding architecture qualifiers');
}
{
no warnings qw(once redefine);
my $apt_output =
"Listing...\n".
"held-pkg/stable 2.0 amd64 [upgradable from: 1.0]\n".
"regular-pkg/stable 3.0 amd64 [upgradable from: 2.0]\n";
local *clean_language = sub { };
local *reset_environment = sub { };
local *execute_command = sub { return 0; };
local *list_update_system_holds = sub { return ('held-pkg'); };
local *has_command = sub { return $_[0] eq 'apt'; };
local *open_execute_command = sub {
my ($fh, $command) = @_;
my $output = $command =~ /^apt list / ? $apt_output : "";
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
or die "open simulated updates: $!";
};
my @normal = update_system_updates(0);
is_deeply([ map { $_->{'name'} } @normal ], [ 'regular-pkg' ],
'default APT updates still exclude held packages');
my @with_holds = update_system_updates(1);
is_deeply([ map { $_->{'name'} } @with_holds ],
[ 'held-pkg', 'regular-pkg' ],
'held-update query includes regular and held packages');
ok($with_holds[0]->{'held'}, 'marks the held update');
ok(!$with_holds[1]->{'held'}, 'does not mark a regular update as held');
}
{
no warnings qw(once redefine);
my $command;
local *has_command = sub { return '/usr/bin/apt-mark'; };
local *unique = sub {
my %seen;
return grep { !$seen{$_}++ } @_;
};
local *trim = sub {
my ($value) = @_;
$value =~ s/^\s+|\s+$//g;
return $value;
};
local *clean_language = sub { };
local *reset_environment = sub { };
local *execute_command_logged = sub {
my ($cmd, undef, $stdout) = @_;
$command = $cmd;
$$stdout = "";
return 0;
};
is(update_system_hold([ 'webmin-virtual-server', 'webmin-virtual-server' ], 1),
undef, 'holds packages successfully');
is($command, 'apt-mark hold webmin\-virtual\-server',
'builds a quoted apt-mark hold command without duplicates');
}
done_testing();