Depends.pm: prune(): return unique elements
[aurutils.git] / perl / AUR / Depends.pm
blobbeec61d44d6e77d4d38f111d3bf8f6c7a3bfdf0d
1 package AUR::Depends;
2 use strict;
3 use warnings;
4 use v5.20;
6 use List::Util qw(first);
7 use Carp;
8 use Exporter qw(import);
9 our @EXPORT_OK = qw(vercmp extract prune graph get);
10 our $VERSION = 'unstable';
12 # Maximum number of calling the callback
13 our $aur_callback_max = $ENV{AUR_DEPENDS_CALLBACK_MAX} // 30;
15 =head1 NAME
17 AUR::Depends - Resolve dependencies from AUR package information
19 =head1 SYNOPSIS
21 use AUR::Depends qw(vercmp extract depends prune graph);
23 =head1 DESCRIPTION
25 =head1 AUTHORS
27 Alad Wenter <https://github.com/AladW/aurutils>
29 =cut
31 sub vercmp_run {
32 if (defined $ENV{'AUR_DEBUG'}) {
33 say STDERR __PACKAGE__ . ': vercmp ' . join(" ", @_);
35 my @command = ('vercmp', @_);
36 my $child_pid = open(my $fh, "-|", @command) or die $!;
37 my $num;
39 if ($child_pid) {
40 $num = <$fh>;
41 waitpid($child_pid, 0);
43 die __PACKAGE__ . ": vercmp failure" if $?;
44 return $num;
47 sub vercmp_ops {
48 my %ops = (
49 '<' => sub { $_[0] < $_[1] },
50 '>' => sub { $_[0] > $_[1] },
51 '<=' => sub { $_[0] <= $_[1] },
52 '>=' => sub { $_[0] >= $_[1] },
54 return %ops;
57 =head2 vercmp()
59 This function provides a simple way to call C<vercmp(8)> from perl code.
60 Instead of ordering versions on the command-line, this function takes
61 an explicit comparison operator (<, >, =, <= or >=) as argument.
63 Under the hood, this function calls the C<vercmp> binary explicitly.
64 This avoids any rebuilds for C<libalpm.so> soname bumps. To keep the approach
65 performant, C<vercmp> is only called when input versions differ.
67 =cut
69 sub vercmp {
70 my ($ver1, $ver2, $op) = @_;
71 my %cmp = vercmp_ops();
73 if (not defined $ver2 or not defined $op) {
74 return "true"; # unversioned dependency
76 elsif ($op eq '=') {
77 return $ver1 eq $ver2;
79 elsif (defined $cmp{$op}) {
80 # check if cmp(ver1, ver2) holds
81 return $cmp{$op}->(vercmp_run($ver1, $ver2), 0);
83 else {
84 croak "invalid vercmp operation";
88 =head2 extract()
90 Extracts dependency (C<$pkgdeps>) and provider (C<$pkgmap>)
91 information from an array of package information hashes, retrieved
92 through a callback function. An example is <callback_query> from
93 C<Query.pm> combined with <parse_json_aur> from C<Json.pm>.
95 Dependencies are tallied and only queried when newly encountered.
97 Verifying if any versioned dependencies can be fulfilled can be done
98 subsequently with the C<graph> function.
100 Parameters:
102 =over
104 =item C<$targets>
106 =item C<$types>
108 =item C<$callback>
110 =back
112 =cut
114 sub extract {
115 my ($targets, $types, $callback) = @_;
116 my @depends = @{$targets};
118 my (%results, %pkgdeps, %pkgmap, %tally);
120 # Populate depends map with command-line targets (#1136)
121 for my $target (@{$targets}) {
122 push(@{$pkgdeps{$target}}, [$target, 'Self']);
125 # XXX: return $a for testing number of requests, e.g. 7 for ros-noetic-desktop
126 my $a = 1;
127 while ($a < $aur_callback_max)
129 if (defined $ENV{'AUR_DEBUG'}) {
130 say STDERR join(" ", "callback: [$a]", @depends);
132 # Use callback to retrieve new hash of results
133 my @level = $callback->(\@depends);
135 if (not scalar(@level) and $a == 1) {
136 say STDERR __PACKAGE__ . ": no packages found";
137 exit(1);
139 $a++;
141 # Retrieve next level of dependencies from results
142 @depends = ();
144 for my $node (@level) {
145 my $name = $node->{'Name'};
146 my $version = $node->{'Version'};
147 $results{$name} = $node;
149 # Iterate over explicit provides
150 for my $spec (@{$node->{'Provides'} // []}) {
151 my ($prov, $prov_version) = split(/=/, $spec);
153 # XXX: the first provider takes precedence
154 # keep multiple providers and warn on ambiguity instead
155 if (not defined $pkgmap{$prov} and $prov ne $name) {
156 $pkgmap{$prov} = [$name, $prov_version];
160 # Filter out dependency types early (#882)
161 $tally{$name} = $a;
163 for my $deptype (@{$types}) {
164 next if (not defined($node->{$deptype})); # no dependency of this type
166 for my $spec (@{$node->{$deptype}}) {
167 # Push versioned dependency to depends map
168 push(@{$pkgdeps{$name}}, [$spec, $deptype]);
170 # Valid operators (important: <= before <)
171 my ($dep, $op, $ver) = split(/(<=|>=|<|=|>)/, $spec);
173 # Avoid querying duplicate packages (#4)
174 next if defined $tally{$dep};
175 push(@depends, $dep);
177 # Mark as incomplete (retrieved in next level or repo package)
178 $tally{$dep} = $a;
182 if (not scalar(@depends)) {
183 last; # no further results
186 # Check if request limits have been exceeded
187 if ($a == $aur_callback_max) {
188 say STDERR __PACKAGE__ . ": total requests: $a (out of range)";
189 exit(34);
191 return \%results, \%pkgdeps, \%pkgmap;
194 =head2 graph()
196 For a set of package-dependency relations (C<$pkgdeps>) and providers
197 (C<$pkgmap>), verify if all dependencies and their versions can be
198 fulfilled by the available set of packages. Version relations are
199 checked with C<vercmp>.
201 Two hashes are kept: one for packages in the set (C<$dag>), and
202 another for packages outside it (C<$dag_foreign>). Only relations in
203 the former are checked.
205 Parameters:
207 =over
209 =item C<$results>
211 =item C<$pkgdeps>
213 =item C<$pkgmap>
215 =item C<$verify>
217 =item C<$provides>
219 =back
221 =cut
223 # XXX: <results> only used for versions and checking if AUR target
224 sub graph {
225 my ($results, $pkgdeps, $pkgmap, $verify, $provides) = @_;
226 my (%dag, %dag_foreign);
228 my $dag_valid = 1;
229 $verify //= 1; # run vercmp by default
231 # Iterate over packages
232 for my $name (keys %{$pkgdeps}) {
233 # Add a loop to command-line targets (#402, #1065, #1136)
234 if (defined $pkgdeps->{$name} and $pkgdeps->{$name} eq $name) {
235 $dag{$name}{$name} = 'Self';
238 # Iterate over dependencies
239 for my $dep (@{$pkgdeps->{$name}}) {
240 my ($dep_spec, $dep_type) = @{$dep}; # ['foo>=1.0', 'Depends']
242 # Retrieve dependency requirements
243 my ($dep_name, $dep_op, $dep_req) = split(/(<=|>=|<|=|>)/, $dep_spec);
245 if (defined $results->{$dep_name}) {
246 # Split results version to pkgver and pkgrel
247 my @dep_ver = split("-", $results->{$dep_name}->{'Version'}, 2);
249 # Provides take precedence over regular packages, unless
250 # $provides is false.
251 my ($prov_name, $prov_ver) = ($dep_name, $dep_ver[0]);
253 if ($provides and defined $pkgmap->{$dep_name}) {
254 ($prov_name, $prov_ver) = @{$pkgmap->{$dep_name}};
257 # Run vercmp with provider and versioned dependency
258 # XXX: a dependency can be both fulfilled by a package and a
259 # different package (provides). In this case an error should
260 # only be returned if neither fulfill the version requirement.
261 if (not $verify or vercmp($prov_ver, $dep_req, $dep_op)) {
262 $dag{$prov_name}{$name} = $dep_type;
264 else {
265 say STDERR "invalid node: $prov_name=$prov_ver (required: $dep_op$dep_req by: $name)";
266 $dag_valid = 0;
269 # Dependency is foreign
270 else {
271 $dag_foreign{$dep_name}{$name} = $dep_type;
275 if (not $dag_valid) {
276 exit(1);
278 return \%dag, \%dag_foreign;
281 =head2 prune()
283 Remove specified nodes from a dependency graph. Every dependency is
284 checked against every pkgname provided (quadratic complexity).
286 The keys of removed nodes are returned in an array.
288 Parameters:
290 =over
292 =item C<$dag>
294 =item C<$installed>
296 =back
298 =cut
300 sub prune {
301 my ($dag, $installed) = @_;
302 my @removals;
304 # Remove reverse dependencies for installed targets
305 for my $dep (keys %{$dag}) { # list returned by `keys` is a copy
306 for my $name (keys %{$dag->{$dep}}) {
307 my $found = first { $name eq $_ } @{$installed};
309 if (defined $found) {
310 delete $dag->{$dep}->{$found};
314 for my $dep (keys %{$dag}) {
315 if (not scalar keys %{$dag->{$dep}}) {
316 delete $dag->{$dep}; # remove targets that are no longer required
317 push(@removals, $dep);
319 my $found = first { $dep eq $_ } @{$installed};
321 if (defined $found) {
322 delete $dag->{$dep}; # remove targets that are installed
323 push(@removals, $dep);
326 # Remove non-unique elements
327 @removals = keys %{{ map { $_ => 1 } @removals }};
328 # XXX: return complement dict instead of array
329 return \@removals;
332 =head2 get()
334 High-level function which combines C<depends>, C<prune> and C<graph>.
336 Parameters:
338 =over
340 =item C<$targets>
342 =item C<$types>
344 =item C<$callback>
346 =item C<$opt_verify>
348 =back
350 =cut
352 sub get {
353 my ($targets, $types, $callback, $opt_verify, $opt_provides, $opt_installed) = @_;
355 # Retrieve AUR results (JSON -> dict -> extract depends -> repeat until none)
356 my ($results, $pkgdeps, $pkgmap) = extract($targets, $types, $callback);
358 # Verify dependency requirements
359 my ($dag, $dag_foreign) = graph($results, $pkgdeps, $pkgmap,
360 $opt_verify, $opt_provides);
361 my $removals = [];
363 # Remove virtual dependencies from dependency graph (#1063)
364 if ($opt_provides) {
365 my @virtual = keys %{$pkgmap};
367 # XXX: assumes <pkgmap> only contains keys with provides != pkgname
368 $removals = prune($dag, \@virtual);
370 # Remove transitive dependencies for installed targets (#592)
371 # XXX: prune from $dag_foreign as well?
372 if (scalar @{$opt_installed}) {
373 $removals = prune($dag, $opt_installed);
375 # Remove packages no longer in graph from results
376 if (scalar @{$removals}) {
377 map { delete $results->{$_} } @{$removals};
379 # Return $dag for subsequent application of C<prune>
380 return $results, $dag, $dag_foreign;
383 # vim: set et sw=4 sts=4 ft=perl: