perl: use EXPORT_OK
[aurutils.git] / perl / AUR / Depends.pm
blobb047eacf0de7a098855f3ed485e3ca5a6bdb3d9c
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 depends prune graph);
10 our $VERSION = 'unstable';
12 =head1 NAME
14 AUR::Depends - Resolve dependencies from AUR package information
16 =head1 SYNOPSIS
18 use AUR::Depends qw(vercmp extract depends prune graph);
20 =head1 DESCRIPTION
22 =head1 AUTHORS
24 Alad Wenter <https://github.com/AladW/aurutils>
26 =cut
28 sub vercmp_run {
29 say STDERR __PACKAGE__ . ': vercmp ' . join(" ", @_)
30 if defined $ENV{'AUR_DEBUG'};
32 my @command = ('vercmp', @_);
33 my $child_pid = open(my $fh, "-|", @command) or die $!;
34 my $num;
36 if ($child_pid) {
37 $num = <$fh>;
38 waitpid($child_pid, 0);
40 die __PACKAGE__ . ": vercmp failure" if $?;
41 return $num;
44 sub vercmp_ops {
45 my %ops = (
46 '<' => sub { $_[0] < $_[1] },
47 '>' => sub { $_[0] > $_[1] },
48 '<=' => sub { $_[0] <= $_[1] },
49 '>=' => sub { $_[0] >= $_[1] },
51 return %ops;
54 =item vercmp
56 This function provides a simple way to call C<vercmp(8)> from perl code.
57 Instead of ordering versions on the command-line, this function takes
58 an explicit comparison operator (<, >, =, <= or >=) as argument.
60 Under the hood, this function calls the C<vercmp> binary explicitly.
61 This avoids any rebuilds for C<libalpm.so> soname bumps. To keep the approach
62 performant, C<vercmp> is only called when input versions differ.
64 =cut
66 sub vercmp {
67 my ($ver1, $ver2, $op) = @_;
68 my %cmp = vercmp_ops();
70 if (not defined $ver2 or not defined $op) {
71 return "true"; # unversioned dependency
73 elsif ($op eq '=') {
74 return $ver1 eq $ver2;
76 elsif (defined $cmp{$op}) {
77 # check if cmp(ver1, ver2) holds
78 return $cmp{$op}->(vercmp_run($ver1, $ver2), 0);
80 else {
81 croak __PACKAGE__ . "invalid vercmp operation";
85 =item extract()
87 Extracts dependency (C<$pkgdeps>) and provider (C<$pkgmap>)
88 information from an array of package information hashes, such as
89 those from C<Srcinfo.pm> or C<Query.pm>.
91 Any I<new> dependencies are added to the returned array value. A
92 dependency is considered I<new> if it has no existing entry in the
93 C<$results> hash ref. This makes it efficient to use this function
94 iteratively for retrieving the dependency graph of a set of targets.
96 Verifying if any versioned dependencies can be fulfilled can be done
97 subsequently with the C<graph> function.
99 =cut
101 sub extract {
102 # hash refs modified in place
103 my ($results, $pkgdeps, $pkgmap, $types, @level) = @_;
104 my @depends = ();
106 for my $node (@level) {
107 my $name = $node->{'Name'};
108 my $version = $node->{'Version'};
109 $results->{$name} = $node;
111 # Iterate over explicit provides
112 for my $spec (@{$node->{'Provides'} // []}) {
113 my ($prov, $prov_version) = split(/=/, $spec);
115 # XXX: the first provider takes precedence
116 # keep multiple providers and warn on ambiguity instead
117 if (not defined $pkgmap->{$prov} and $prov ne $name) {
118 $pkgmap->{$prov} = [$name, $prov_version];
121 # Filter out dependency types early (#882)
122 for my $deptype (@{$types}) {
123 next if (not defined($node->{$deptype})); # no dependency of this type
125 for my $spec (@{$node->{$deptype}}) {
126 # Push versioned dependency to global depends
127 push(@{$pkgdeps->{$name}}, [$spec, $deptype]);
129 # Valid operators (important: <= before <)
130 my ($dep, $op, $ver) = split(/(<=|>=|<|=|>)/, $spec);
132 # Avoid querying duplicate packages (#4)
133 next if defined $results->{$dep};
134 push(@depends, $dep);
136 # Mark as incomplete (retrieved in next step or repo package)
137 # XXX: do not write directly into <results>, but some other
138 # dict shared between <extract> calls
139 $results->{$dep} = 'None';
143 return @depends;
146 =item depends()
148 Iteratively call C<extract()> with a callback function. The
149 number of times the callback function may be called is specified as a
150 separate parameter.
152 =cut
154 sub depends {
155 my ($targets, $types, $callback, $callback_max_a) = @_;
156 my @depends = @{$targets};
158 my (%results, %pkgdeps, %pkgmap);
160 # XXX: return $a for testing number of requests, e.g. 7 for ros-noetic-desktop
161 for my $a (1..$callback_max_a) {
162 say STDERR join(" ", "callback: [$a]", @depends) if defined $ENV{'AUR_DEBUG'};
164 # Check if request limits have been exceeded
165 if ($a == $callback_max_a) {
166 say STDERR __PACKAGE__ . ": total requests: $a (out of range)";
167 exit(34);
170 # Use callback to retrieve new hash of results
171 my @level = $callback->(\@depends);
173 if (not scalar(@level) and $a == 1) {
174 say STDERR __PACKAGE__ . ": no packages found";
175 exit(1);
178 # Retrieve next level of dependencies from results
179 @depends = extract(\%results, \%pkgdeps, \%pkgmap, $types, @level);
181 if (not scalar(@depends)) {
182 last; # no further results
185 # XXX: workaround for extract() tallying packages in <results> dict
186 for my $pkg (keys %results) {
187 delete $results{$pkg} if $results{$pkg} eq 'None';
189 return \%results, \%pkgdeps, \%pkgmap;
192 =item graph()
194 For a set of package-dependency relations (C<$pkgdeps>) and providers
195 (C<$pkgmap>), verify if all dependencies and their versions can be
196 fulfilled by the available set of packages. Version relations are
197 checked with C<vercmp>.
199 Two hashes are kept: one for packages in the set (C<$dag>), and
200 another for packages outside it (C<$dag_foreign>). Only relations in
201 the former are checked.
203 =cut
205 # XXX: <results> only used for versions and checking if AUR target
206 sub graph {
207 my ($results, $pkgdeps, $pkgmap, $verify, $provides) = @_;
208 my (%dag, %dag_foreign);
210 my $dag_valid = 1;
211 $verify //= 1; # run vercmp by default
213 # Iterate over packages
214 for my $name (keys %{$pkgdeps}) {
215 # Add a loop to isolated nodes (#402, #1065)
216 # XXX: distinguish between explicit (command-line) and
217 # implicit (dependencies) targets
218 $dag{$name}{$name} = 'Self';
220 # Iterate over dependencies
221 for my $dep (@{$pkgdeps->{$name}}) {
222 my ($dep_spec, $dep_type) = @{$dep}; # ['foo>=1.0', 'Depends']
224 # Retrieve dependency requirements
225 my ($dep_name, $dep_op, $dep_req) = split(/(<=|>=|<|=|>)/, $dep_spec);
227 if (defined $results->{$dep_name}) {
228 my $dep_ver = $results->{$dep_name}->{'Version'};
230 # Provides take precedence over regular packages,
231 # unless $provides is false.
232 my ($prov_name, $prov_ver) = ($dep_name, $dep_ver);
234 if ($provides and defined $pkgmap->{$dep_name}) {
235 ($prov_name, $prov_ver) = @{$pkgmap->{$dep_name}};
238 # Run vercmp with provider and versioned dependency
239 # XXX: a dependency can be both fulfilled by a package
240 # and a different package (provides). In this case an
241 # error should only be returned if neither fulfill the
242 # version requirement.
243 if (not $verify or vercmp($prov_ver, $dep_req, $dep_op)) {
244 $dag{$prov_name}{$name} = $dep_type;
246 else {
247 say STDERR "invalid node: $prov_name=$prov_ver (required: $dep_op$dep_req by: $name)";
248 $dag_valid = 0;
251 # Dependency is foreign
252 else {
253 $dag_foreign{$dep_name}{$name} = $dep_type;
257 if (not $dag_valid) {
258 exit(1);
260 return \%dag, \%dag_foreign;
263 =item prune()
265 Remove specified nodes from a dependency graph. Every dependency is
266 checked against every pkgname provided (quadratic complexity).
268 The keys of removed nodes are returned in an array.
270 =cut
272 # XXX: return complement dict instead of array
273 sub prune {
274 my ($dag, $installed) = @_;
275 my @removals;
277 # Remove reverse dependencies for installed targets
278 for my $dep (keys %{$dag}) { # list returned by `keys` is a copy
279 for my $name (keys %{$dag->{$dep}}) {
280 my $found = first { $name eq $_ } @{$installed};
282 if (defined $found) {
283 delete $dag->{$dep}->{$found};
288 for my $dep (keys %{$dag}) {
289 if (not scalar keys %{$dag->{$dep}}) {
290 delete $dag->{$dep}; # remove targets that are no longer required
291 push(@removals, $dep);
293 my $found = first { $dep eq $_ } @{$installed};
295 if (defined $found) {
296 delete $dag->{$dep}; # remove targets that are installed
297 push(@removals, $dep);
300 return \@removals;
303 =item levels()
305 =cut
307 # TODO: compute dependency levels (bfs)
308 # sub levels {
312 # vim: set et sw=4 sts=4 ft=perl: