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