6 use List
::Util
qw(first);
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;
17 AUR::Depends - Resolve dependencies from AUR package information
21 use AUR::Depends qw(vercmp extract depends prune graph);
27 Alad Wenter <https://github.com/AladW/aurutils>
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 $!;
41 waitpid($child_pid, 0);
43 die __PACKAGE__
. ": vercmp failure" if $?
;
49 '<' => sub { $_[0] < $_[1] },
50 '>' => sub { $_[0] > $_[1] },
51 '<=' => sub { $_[0] <= $_[1] },
52 '>=' => sub { $_[0] >= $_[1] },
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.
70 my ($ver1, $ver2, $op) = @_;
71 my %cmp = vercmp_ops
();
73 if (not defined $ver2 or not defined $op) {
74 return "true"; # unversioned dependency
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);
84 croak
"invalid vercmp operation";
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.
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
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";
141 # Retrieve next level of dependencies from results
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)
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)
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)";
191 return \
%results, \
%pkgdeps, \
%pkgmap;
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.
223 # XXX: <results> only used for versions and checking if AUR target
225 my ($results, $pkgdeps, $pkgmap, $verify, $provides) = @_;
226 my (%dag, %dag_foreign);
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;
265 say STDERR
"invalid node: $prov_name=$prov_ver (required: $dep_op$dep_req by: $name)";
269 # Dependency is foreign
271 $dag_foreign{$dep_name}{$name} = $dep_type;
275 if (not $dag_valid) {
278 return \
%dag, \
%dag_foreign;
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.
301 my ($dag, $installed) = @_;
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
334 High-level function which combines C<depends>, C<prune> and C<graph>.
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);
363 # Remove virtual dependencies from dependency graph (#1063)
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: