6 use List
::Util
qw(first);
8 use Exporter
qw(import);
9 our @EXPORT_OK = qw(vercmp extract depends prune graph);
10 our $VERSION = 'unstable';
14 AUR::Depends - Resolve dependencies from AUR package information
18 use AUR::Depends qw(vercmp extract depends prune graph);
24 Alad Wenter <https://github.com/AladW/aurutils>
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 $!;
38 waitpid($child_pid, 0);
40 die __PACKAGE__
. ": vercmp failure" if $?
;
46 '<' => sub { $_[0] < $_[1] },
47 '>' => sub { $_[0] > $_[1] },
48 '<=' => sub { $_[0] <= $_[1] },
49 '>=' => sub { $_[0] >= $_[1] },
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.
67 my ($ver1, $ver2, $op) = @_;
68 my %cmp = vercmp_ops
();
70 if (not defined $ver2 or not defined $op) {
71 return "true"; # unversioned dependency
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);
81 croak __PACKAGE__
. "invalid vercmp operation";
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.
102 # hash refs modified in place
103 my ($results, $pkgdeps, $pkgmap, $types, @level) = @_;
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';
148 Iteratively call C<extract()> with a callback function. The
149 number of times the callback function may be called is specified as a
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)";
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";
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;
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.
205 # XXX: <results> only used for versions and checking if AUR target
207 my ($results, $pkgdeps, $pkgmap, $verify, $provides) = @_;
208 my (%dag, %dag_foreign);
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;
247 say STDERR
"invalid node: $prov_name=$prov_ver (required: $dep_op$dep_req by: $name)";
251 # Dependency is foreign
253 $dag_foreign{$dep_name}{$name} = $dep_type;
257 if (not $dag_valid) {
260 return \
%dag, \
%dag_foreign;
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.
272 # XXX: return complement dict instead of array
274 my ($dag, $installed) = @_;
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);
307 # TODO: compute dependency levels (bfs)
312 # vim: set et sw=4 sts=4 ft=perl: