1# ex:ts=8 sw=4: 2# $OpenBSD: UpdateSet.pm,v 1.89 2023/06/13 09:07:17 espie Exp $ 3# 4# Copyright (c) 2007-2010 Marc Espie <espie@openbsd.org> 5# 6# Permission to use, copy, modify, and distribute this software for any 7# purpose with or without fee is hereby granted, provided that the above 8# copyright notice and this permission notice appear in all copies. 9# 10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 18 19# an UpdateSet is a list of packages to remove/install. 20# it contains several things: 21# -> a list of older packages to remove (installed locations) 22# -> a list of newer packages to add (might be very simple locations) 23# -> a list of "hints", as package names to install 24# -> a list of packages that are kept throughout an update 25# every add/remove operations manipulate UpdateSet. 26# 27# Since older packages are always installed, they're organized as a hash. 28# 29# XXX: an UpdateSet succeeds or fails "together". 30# if several packages should be removed/added, then not being able 31# to do stuff on ONE of them is enough to invalidate the whole set. 32# 33# Normal UpdateSets contain one newer package at most. 34# Bigger UpdateSets can be created through the merge operation, which 35# will be used only when necessary. 36# 37# kept packages are needed after merges, where some dependencies may 38# not need updating, and to distinguish from old packages that will be 39# removed. 40# 41# for instance, package installation will check UpdateSets for internal 42# dependencies and for conflicts. For that to work, we need kept stuff 43# 44use v5.36; 45 46# hints should behave like locations 47package OpenBSD::hint; 48sub new($class, $name) 49{ 50 bless {name => $name}, $class; 51} 52 53sub pkgname($self) 54{ 55 return $self->{name}; 56} 57 58package OpenBSD::hint2; 59our @ISA = qw(OpenBSD::hint); 60 61# Code organisation: this is the stuff that's common for actual UpdateSets 62# (used by pkg_add) and DeleteSets, a simpler version used by pkg_delete. 63# Turns out some of that stuff is identical. 64# The really juicy stuff resides in pkg_add/pkg_delete proper. 65package OpenBSD::DeleteSet; 66use OpenBSD::Error; 67 68sub new($class, $state) 69{ 70 return bless {older => {}}, $class; 71} 72 73sub add_older($self, @p) 74{ 75 for my $h (@p) { 76 $self->{older}{$h->pkgname} = $h; 77 $h->{is_old} = 1; 78 } 79 return $self; 80} 81 82sub older($self) 83{ 84 return values %{$self->{older}}; 85} 86 87sub older_names($self) 88{ 89 return keys %{$self->{older}}; 90} 91 92sub all_handles # forwarder 93{ 94 &older; 95} 96 97sub changed_handles # forwarder 98{ 99 &older; 100} 101 102sub mark_as_finished($self) 103{ 104 $self->{finished} = 1; 105} 106 107sub cleanup($self, $error = undef, $errorinfo = undef) 108{ 109 for my $h ($self->all_handles) { 110 $h->cleanup($error, $errorinfo); 111 } 112 if (defined $error) { 113 $self->{error} //= $error; 114 $self->{errorinfo} //= $errorinfo; 115 } 116 delete $self->{solver}; 117 delete $self->{known_mandirs}; 118 delete $self->{known_displays}; 119 delete $self->{dont_delete}; 120 delete $self->{known_extra}; 121 delete $self->{known_sample}; 122 $self->mark_as_finished; 123} 124 125sub has_error # forwarder 126{ 127 &OpenBSD::Handle::has_error; 128} 129 130# display code that will put together packages with the same version 131sub smart_join($self, @p) 132{ 133 if (@p <= 1) { 134 return join('+', @p); 135 } 136 my ($k, @stems); 137 for my $l (@p) { 138 my ($stem, @rest) = OpenBSD::PackageName::splitname($l); 139 my $k2 = join('-', @rest); 140 $k //= $k2; 141 if ($k2 ne $k) { 142 return join('+', sort @p); 143 } 144 push(@stems, $stem); 145 } 146 return join('+', sort @stems).'-'.$k; 147} 148 149sub print($self) 150{ 151 return $self->smart_join($self->older_names); 152} 153 154sub todo_names # forwarder 155{ 156 &older_names; 157} 158 159sub short_print($self) 160{ 161 my $result = $self->smart_join($self->todo_names); 162 if (length $result > 30) { 163 return substr($result, 0, 27)."..."; 164 } else { 165 return $result; 166 } 167} 168 169sub real_set($set) 170{ 171 while (defined $set->{merged}) { 172 $set = $set->{merged}; 173 } 174 return $set; 175} 176 177sub merge_set($self, $set) 178{ 179 $self->add_older($set->older); 180 $set->mark_as_finished; 181 # XXX and mark it as merged, for eventual updates 182 $set->{merged} = $self; 183} 184 185# Merge several deletesets together 186sub merge($self, $tracker, @sets) 187{ 188 # Apparently simple, just add the missing parts 189 for my $set (@sets) { 190 next if $set eq $self; 191 $self->merge_set($set); 192 $tracker->handle_set($set); 193 } 194 # then regen tracker info for $self 195 $tracker->todo($self); 196 return $self; 197} 198 199sub match_locations($, @) 200{ 201 return []; 202} 203 204OpenBSD::Auto::cache(solver, 205 sub($self) { 206 require OpenBSD::Dependencies; 207 return OpenBSD::Dependencies::Solver->new($self); 208 }); 209 210OpenBSD::Auto::cache(conflict_cache, 211 sub($) { 212 require OpenBSD::Dependencies; 213 return OpenBSD::ConflictCache->new; 214 }); 215 216package OpenBSD::UpdateSet; 217our @ISA = qw(OpenBSD::DeleteSet); 218 219sub new($class, $state) 220{ 221 my $o = $class->SUPER::new($state); 222 $o->{newer} = {}; 223 $o->{kept} = {}; 224 $o->{repo} = $state->repo; 225 $o->{hints} = []; 226 $o->{updates} = 0; 227 return $o; 228} 229 230# TODO this stuff is mostly unused right now (or buggy) 231sub path($set) 232{ 233 return $set->{path}; 234} 235 236sub add_repositories($set, @repos) 237{ 238 if (!defined $set->{path}) { 239 $set->{path} = $set->{repo}->path; 240 } 241 $set->{path}->add(@repos); 242} 243 244sub merge_paths($set, $other) 245{ 246 if (defined $other->path) { 247 if (!defined $set->path) { 248 $set->{path} = $other->path; 249 } elsif ($set->{path} ne $other->path) { 250 $set->add_path(@{$other->{path}}); 251 } 252 } 253} 254 255sub match_locations($set, @spec) 256{ 257 my $r = []; 258 if (defined $set->{path}) { 259 $r = $set->{path}->match_locations(@spec); 260 } 261 if (@$r == 0) { 262 $r = $set->{repo}->match_locations(@spec); 263 } 264 return $r; 265} 266 267sub add_newer($self, @p) 268{ 269 for my $h (@p) { 270 $self->{newer}{$h->pkgname} = $h; 271 $self->{updates}++; 272 } 273 return $self; 274} 275 276sub add_kept($self, @p) 277{ 278 for my $h (@p) { 279 $self->{kept}->{$h->pkgname} = $h; 280 } 281 return $self; 282} 283 284sub move_kept($self, @p) 285{ 286 for my $h (@p) { 287 delete $self->{older}{$h->pkgname}; 288 delete $self->{newer}{$h->pkgname}; 289 $self->{kept}{$h->pkgname} = $h; 290 if (!defined $h->{location}) { 291 $h->{location} = 292 $self->{repo}->installed->find($h->pkgname); 293 } 294 $h->complete_dependency_info; 295 $h->{update_found} = $h; 296 } 297 return $self; 298} 299 300sub add_hints($self, @p) 301{ 302 for my $h (@p) { 303 push(@{$self->{hints}}, OpenBSD::hint->new($h)); 304 } 305 return $self; 306} 307 308sub add_hints2($self, @p) 309{ 310 for my $h (@p) { 311 push(@{$self->{hints}}, OpenBSD::hint2->new($h)); 312 } 313 return $self; 314} 315 316sub newer($self) 317{ 318 return values %{$self->{newer}}; 319} 320 321sub kept($self) 322{ 323 return values %{$self->{kept}}; 324} 325 326sub hints($self) 327{ 328 return @{$self->{hints}}; 329} 330 331sub newer_names($self) 332{ 333 return keys %{$self->{newer}}; 334} 335 336sub kept_names($self) 337{ 338 return keys %{$self->{kept}}; 339} 340 341sub all_handles($self) 342{ 343 return ($self->older, $self->newer, $self->kept); 344} 345 346sub changed_handles($self) 347{ 348 return ($self->older, $self->newer); 349} 350 351sub hint_names($self) 352{ 353 return map {$_->pkgname} $self->hints; 354} 355 356sub older_to_do($self) 357{ 358 # XXX in `combined' updates, some dependencies may remove extra 359 # packages, so we do a double-take on the list of packages we 360 # are actually replacing... for now, until we merge update sets. 361 require OpenBSD::PackageInfo; 362 my @l = (); 363 for my $h ($self->older) { 364 if (OpenBSD::PackageInfo::is_installed($h->pkgname)) { 365 push(@l, $h); 366 } 367 } 368 return @l; 369} 370 371sub print($self) 372{ 373 my $result = ""; 374 if ($self->kept > 0) { 375 $result = "[".$self->smart_join($self->kept_names)."]"; 376 } 377 my ($old, $new); 378 if ($self->older > 0) { 379 $old = $self->SUPER::print; 380 } 381 if ($self->newer > 0) { 382 $new = $self->smart_join($self->newer_names); 383 } 384 # XXX common case 385 if (defined $old && defined $new) { 386 my ($stema, @resta) = OpenBSD::PackageName::splitname($old); 387 my $resta = join('-', @resta); 388 my ($stemb, @restb) = OpenBSD::PackageName::splitname($new); 389 my $restb = join('-', @restb); 390 if ($stema eq $stemb && $resta !~ /\+/ && $restb !~ /\+/) { 391 return $result .$old."->".$restb; 392 } 393 } 394 395 if (defined $old) { 396 $result .= $old."->"; 397 } 398 if (defined $new) { 399 $result .= $new; 400 } elsif ($self->hints > 0) { 401 $result .= $self->smart_join($self->hint_names); 402 } 403 return $result; 404} 405 406sub todo_names($self) 407{ 408 if ($self->newer > 0) { 409 return $self->newer_names; 410 } else { 411 return $self->kept_names; 412 } 413} 414 415sub validate_plists($self, $state) 416{ 417 $state->{problems} = 0; 418 delete $state->{overflow}; 419 420 $state->{current_set} = $self; 421 422 for my $o ($self->older_to_do) { 423 require OpenBSD::Delete; 424 OpenBSD::Delete::validate_plist($o->{plist}, $state); 425 } 426 $state->{colliding} = []; 427 for my $n ($self->newer) { 428 require OpenBSD::Add; 429 OpenBSD::Add::validate_plist($n->{plist}, $state, $self); 430 } 431 if (@{$state->{colliding}} > 0) { 432 require OpenBSD::CollisionReport; 433 434 OpenBSD::CollisionReport::collision_report($state->{colliding}, $state, $self); 435 } 436 if (defined $state->{overflow}) { 437 $state->vstat->tally; 438 $state->vstat->drop_changes; 439 # nothing to try if we don't have existing stuff to remove 440 return 0 if $self->older == 0; 441 # we already tried the other way around... 442 return 0 if $state->{delete_first}; 443 if ($state->defines('deletefirst') || 444 $state->confirm_defaults_to_no( 445 "Delete older packages first")) { 446 # okay we recurse doing things the other way around 447 $state->{delete_first} = 1; 448 return $self->validate_plists($state); 449 } 450 } 451 if ($state->{problems}) { 452 $state->vstat->drop_changes; 453 return 0; 454 } else { 455 $state->vstat->synchronize; 456 return 1; 457 } 458} 459 460sub cleanup_old_shared($set, $state) 461{ 462 my $h = $set->{old_shared}; 463 464 for my $d (sort {$b cmp $a} keys %$h) { 465 OpenBSD::SharedItems::wipe_directory($state, $h, $d) || 466 $state->fatal("Can't continue"); 467 delete $state->{recorder}{dirs}{$d}; 468 } 469} 470 471my @extra = qw(solver conflict_cache); 472sub mark_as_finished($self) 473{ 474 for my $i (@extra, 'sha') { 475 delete $self->{$i}; 476 } 477 $self->SUPER::mark_as_finished; 478} 479 480sub merge_if_exists($self, $k, @extra) 481{ 482 my @list = (); 483 for my $s (@extra) { 484 if ($s ne $self && defined $s->{$k}) { 485 push(@list, $s->{$k}); 486 } 487 } 488 $self->$k->merge(@list); 489} 490 491sub merge_set($self, $set) 492{ 493 $self->SUPER::merge_set($set); 494 $self->add_newer($set->newer); 495 $self->add_kept($set->kept); 496 $self->merge_paths($set); 497 $self->{updates} += $set->{updates}; 498 $set->{updates} = 0; 499} 500 501# Merge several updatesets together 502sub merge($self, $tracker, @sets) 503{ 504 for my $i (@extra) { 505 $self->merge_if_exists($i, @sets); 506 } 507 return $self->SUPER::merge($tracker, @sets); 508} 509 5101; 511