1# ex:ts=8 sw=4:
2# $OpenBSD: UpdateSet.pm,v 1.85 2019/07/04 09:47:09 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 strict;
45use warnings;
46
47# hints should behave like locations
48package OpenBSD::hint;
49sub new
50{
51	my ($class, $name) = @_;
52	bless {name => $name}, $class;
53}
54
55sub pkgname
56{
57	return shift->{name};
58}
59
60package OpenBSD::hint2;
61our @ISA = qw(OpenBSD::hint);
62
63package OpenBSD::DeleteSet;
64use OpenBSD::Error;
65
66sub new
67{
68	my ($class, $state) = @_;
69	return bless {older => {}}, $class;
70}
71
72sub add_older
73{
74	my $self = shift;
75	for my $h (@_) {
76		$self->{older}{$h->pkgname} = $h;
77		$h->{is_old} = 1;
78	}
79	return $self;
80}
81
82sub older
83{
84	my $self = shift;
85	return values %{$self->{older}};
86}
87
88sub older_names
89{
90	my $self = shift;
91	return keys %{$self->{older}};
92}
93
94sub all_handles
95{
96	&older;
97}
98
99sub changed_handles
100{
101	&older;
102}
103
104sub mark_as_finished
105{
106	my $self = shift;
107	$self->{finished} = 1;
108}
109
110sub cleanup
111{
112	my ($self, $error, $errorinfo) = @_;
113	for my $h ($self->all_handles) {
114		$h->cleanup($error, $errorinfo);
115	}
116	if (defined $error) {
117		$self->{error} //= $error;
118		$self->{errorinfo} //= $errorinfo;
119	}
120	delete $self->{solver};
121	delete $self->{known_mandirs};
122	delete $self->{known_displays};
123	$self->mark_as_finished;
124}
125
126sub has_error
127{
128	&OpenBSD::Handle::has_error;
129}
130
131sub smart_join
132{
133	my $self = shift;
134	if (@_ <= 1) {
135		return join('+', @_);
136	}
137	my ($k, @stems);
138	for my $l (@_) {
139		my ($stem, @rest) = OpenBSD::PackageName::splitname($l);
140		my $k2 = join('-', @rest);
141		$k //= $k2;
142		if ($k2 ne $k) {
143			return join('+', sort @_);
144		}
145		push(@stems, $stem);
146	}
147	return join('+', sort @stems).'-'.$k;
148}
149
150sub print
151{
152	my $self = shift;
153	return $self->smart_join($self->older_names);
154}
155
156sub todo_names
157{
158	&older_names;
159}
160
161sub short_print
162{
163	my $self = shift;
164	my $result = $self->smart_join($self->todo_names);
165	if (length $result > 30) {
166		return substr($result, 0, 27)."...";
167	} else {
168		return $result;
169	}
170}
171
172sub real_set
173{
174	my $set = shift;
175	while (defined $set->{merged}) {
176		$set = $set->{merged};
177	}
178	return $set;
179}
180
181sub merge_set
182{
183	my ($self, $set) = @_;
184	$self->add_older($set->older);
185	$set->mark_as_finished;
186	# XXX and mark it as merged, for eventual updates
187	$set->{merged} = $self;
188}
189
190# Merge several deletesets together
191sub merge
192{
193	my ($self, $tracker, @sets) = @_;
194
195	# Apparently simple, just add the missing parts
196	for my $set (@sets) {
197		next if $set eq $self;
198		$self->merge_set($set);
199		$tracker->handle_set($set);
200	}
201	# then regen tracker info for $self
202	$tracker->todo($self);
203	return $self;
204}
205
206sub match_locations
207{
208	return [];
209}
210
211OpenBSD::Auto::cache(solver,
212    sub {
213    	require OpenBSD::Dependencies;
214	return OpenBSD::Dependencies::Solver->new(shift);
215    });
216
217OpenBSD::Auto::cache(conflict_cache,
218    sub {
219    	require OpenBSD::Dependencies;
220	return OpenBSD::ConflictCache->new;
221    });
222
223package OpenBSD::UpdateSet;
224our @ISA = qw(OpenBSD::DeleteSet);
225
226sub new
227{
228	my ($class, $state) = @_;
229	my $o = $class->SUPER::new($state);
230	$o->{newer} = {};
231	$o->{kept} = {};
232	$o->{repo} = $state->repo;
233	$o->{hints} = [];
234	$o->{updates} = 0;
235	return $o;
236}
237
238sub path
239{
240	my $set = shift;
241
242	return $set->{path};
243}
244
245sub add_repositories
246{
247	my ($set, @repos) = @_;
248
249	if (!defined $set->{path}) {
250		$set->{path} = $set->{repo}->path;
251	}
252	$set->{path}->add(@repos);
253}
254
255sub merge_paths
256{
257	my ($set, $other) = @_;
258
259	if (defined $other->path) {
260		if (!defined $set->path) {
261			$set->{path} = $other->path;
262		} elsif ($set->{path} ne $other->path) {
263			$set->add_path(@{$other->{path}});
264		}
265	}
266}
267
268sub match_locations
269{
270	my ($set, @spec) = @_;
271	my $r = [];
272	if (defined $set->{path}) {
273		$r = $set->{path}->match_locations(@spec);
274	}
275	if (@$r == 0) {
276		$r = $set->{repo}->match_locations(@spec);
277	}
278	return $r;
279}
280
281sub add_newer
282{
283	my $self = shift;
284	for my $h (@_) {
285		$self->{newer}{$h->pkgname} = $h;
286		$self->{updates}++;
287	}
288	return $self;
289}
290
291sub add_kept
292{
293	my $self = shift;
294	for my $h (@_) {
295		$self->{kept}->{$h->pkgname} = $h;
296	}
297	return $self;
298}
299
300sub move_kept
301{
302	my $self = shift;
303	for my $h (@_) {
304		delete $self->{older}{$h->pkgname};
305		delete $self->{newer}{$h->pkgname};
306		$self->{kept}{$h->pkgname} = $h;
307		if (!defined $h->{location}) {
308			$h->{location} =
309			    $self->{repo}->installed->find($h->pkgname);
310		}
311		$h->complete_dependency_info;
312		$h->{update_found} = $h;
313	}
314	return $self;
315}
316
317sub add_hints
318{
319	my $self = shift;
320	for my $h (@_) {
321		push(@{$self->{hints}}, OpenBSD::hint->new($h));
322	}
323	return $self;
324}
325
326sub add_hints2
327{
328	my $self = shift;
329	for my $h (@_) {
330		push(@{$self->{hints}}, OpenBSD::hint2->new($h));
331	}
332	return $self;
333}
334
335sub newer
336{
337	my $self = shift;
338	return values %{$self->{newer}};
339}
340
341sub kept
342{
343	my $self = shift;
344	return values %{$self->{kept}};
345}
346
347sub hints
348{
349	my $self = shift;
350	return @{$self->{hints}};
351}
352
353sub newer_names
354{
355	my $self = shift;
356	return keys %{$self->{newer}};
357}
358
359sub kept_names
360{
361	my $self = shift;
362	return keys %{$self->{kept}};
363}
364
365sub all_handles
366{
367	my $self = shift;
368	return ($self->older, $self->newer, $self->kept);
369}
370
371sub changed_handles
372{
373	my $self = shift;
374	return ($self->older, $self->newer);
375}
376
377sub hint_names
378{
379	my $self = shift;
380	return map {$_->pkgname} $self->hints;
381}
382
383sub older_to_do
384{
385	my $self = shift;
386	# XXX in `combined' updates, some dependencies may remove extra
387	# packages, so we do a double-take on the list of packages we
388	# are actually replacing... for now, until we merge update sets.
389	require OpenBSD::PackageInfo;
390	my @l = ();
391	for my $h ($self->older) {
392		if (OpenBSD::PackageInfo::is_installed($h->pkgname)) {
393			push(@l, $h);
394		}
395	}
396	return @l;
397}
398
399sub print
400{
401	my $self = shift;
402	my $result = "";
403	if ($self->kept > 0) {
404		$result = "[".$self->smart_join($self->kept_names)."]";
405	}
406	my ($old, $new);
407	if ($self->older > 0) {
408		$old = $self->SUPER::print;
409	}
410	if ($self->newer > 0) {
411		$new = $self->smart_join($self->newer_names);
412	}
413	# XXX common case
414	if (defined $old && defined $new) {
415		my ($stema, @resta) = OpenBSD::PackageName::splitname($old);
416		my $resta = join('-', @resta);
417		my ($stemb, @restb) = OpenBSD::PackageName::splitname($new);
418		my $restb = join('-', @restb);
419		if ($stema eq $stemb && $resta !~ /\+/ && $restb !~ /\+/) {
420			return $result .$old."->".$restb;
421		}
422	}
423
424	if (defined $old) {
425		$result .= $old."->";
426	}
427	if (defined $new) {
428		$result .= $new;
429	} elsif ($self->hints > 0) {
430		$result .= $self->smart_join($self->hint_names);
431	}
432	return $result;
433}
434
435sub todo_names
436{
437	my $self = shift;
438	if ($self->newer > 0) {
439		return $self->newer_names;
440	} else {
441		return $self->kept_names;
442	}
443}
444
445sub validate_plists
446{
447	my ($self, $state) = @_;
448	$state->{problems} = 0;
449	delete $state->{overflow};
450
451	$state->{current_set} = $self;
452
453	for my $o ($self->older_to_do) {
454		require OpenBSD::Delete;
455		OpenBSD::Delete::validate_plist($o->{plist}, $state);
456	}
457	$state->{colliding} = [];
458	for my $n ($self->newer) {
459		require OpenBSD::Add;
460		OpenBSD::Add::validate_plist($n->{plist}, $state, $self);
461	}
462	if (@{$state->{colliding}} > 0) {
463		require OpenBSD::CollisionReport;
464
465		OpenBSD::CollisionReport::collision_report($state->{colliding}, $state, $self);
466	}
467	if (defined $state->{overflow}) {
468		$state->vstat->tally;
469		$state->vstat->drop_changes;
470		# nothing to try if we don't have existing stuff to remove
471		return 0 if $self->older == 0;
472		# we already tried the other way around...
473		return 0 if $state->{delete_first};
474		if ($state->defines('deletefirst') ||
475		    $state->confirm_defaults_to_no(
476			"Delete older packages first")) {
477			# okay we recurse doing things the other way around
478			$state->{delete_first} = 1;
479			return $self->validate_plists($state);
480		}
481	}
482	if ($state->{problems}) {
483		$state->vstat->drop_changes;
484		return 0;
485	} else {
486		$state->vstat->synchronize;
487		return 1;
488	}
489}
490
491sub cleanup_old_shared
492{
493	my ($set, $state) = @_;
494	my $h = $set->{old_shared};
495
496	for my $d (sort {$b cmp $a} keys %$h) {
497		OpenBSD::SharedItems::wipe_directory($state, $h, $d) ||
498		    $state->fatal("Can't continue");
499		delete $state->{recorder}{dirs}{$d};
500	}
501}
502
503my @extra = qw(solver conflict_cache);
504sub mark_as_finished
505{
506	my $self = shift;
507	for my $i (@extra, 'sha') {
508		delete $self->{$i};
509	}
510	$self->SUPER::mark_as_finished;
511}
512
513sub merge_if_exists
514{
515	my ($self, $k, @extra) = @_;
516
517	my @list = ();
518	for my $s (@extra) {
519		if ($s ne $self && defined $s->{$k}) {
520			push(@list, $s->{$k});
521		}
522	}
523	$self->$k->merge(@list);
524}
525
526sub merge_set
527{
528	my ($self, $set) = @_;
529	$self->SUPER::merge_set($set);
530	$self->add_newer($set->newer);
531	$self->add_kept($set->kept);
532	$self->merge_paths($set);
533	$self->{updates} += $set->{updates};
534	$set->{updates} = 0;
535}
536
537# Merge several updatesets together
538sub merge
539{
540	my ($self, $tracker, @sets) = @_;
541
542	for my $i (@extra) {
543		$self->merge_if_exists($i, @sets);
544	}
545	return $self->SUPER::merge($tracker, @sets);
546}
547
5481;
549