1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
use Zef;
class Zef::Repository does PackageRepository does Pluggable {
=begin pod
=title class Zef::Repository
=subtitle A configurable implementation of the Repository interface
=head1 Synopsis
=begin code :lang<raku>
use Zef::Fetch;
use Zef::Repository;
# Need a fetcher and cache for the backend repository to fetch and save to
my @fetching_backends = [
{ module => "Zef::Service::Shell::curl" },
{ module => "Zef::Service::Shell::PowerShell::download" },
];
my $fetcher = Zef::Fetch.new(:backends(@fetching_backends));
my $cache = $*TMPDIR.child("{time}") andthen { mkdir $_ unless $_.IO.e };
# Create repo using a backend that is essentially just: Zef::Repository::Ecosystems.new(|%options-shown-below)
# Note usually options are all Str so they can be set in the config file, but for the time being the cache
# and fetcher objects need to be passed in here as well (currently auto-magically done in Zef::Client but could
# be done in Zef::Repository)
my $repo = Zef::Repository.new(
backends => [
[
{
module => "Zef::Repository::Ecosystems",
options => {
cache => $cache,
fetcher => $fetcher,
name => "cpan",
auto-update => 1,
mirrors => ["https://raw.githubusercontent.com/ugexe/Perl6-ecosystems/11efd9077b398df3766eaa7cf8e6a9519f63c272/cpan.json"]
}
},
],
]
);
# Print out all available distributions from all supplied backend repositories
say $_.dist.identity for $repo.available;
# Get the best match for 'Zef'
my @matches = $repo.candidates("Zef");
say "Best match: " ~ @matches.head.dist.identity;
=end code
=head1 Description
A C<Repository> that uses 1 or more other C<Repository> instances as backends. It abstracts the logic
for e.g. sorting by version from multiple repositories. Each C<Repository> (including this one and
whatever backends this may use) can be thought of as recommendation managers, where C<Zef::Repository>
gives a recommendation based on recommendations it gets from its backends (i.e. fez, p6c, cpan, cached).
=head1 Methods
=head2 method candidates
method candidates(*@identities ($, *@) --> Array[Candidate])
Resolves each identity in C<@identities> to its best matching C<Candidate>, where the "best match" is generally
what is most consistent with how C<Raku> would otherwise load modules.
Each repository makes its own recommendations, and only then will the results be considered to make a single recommendation
for each of C<@identities> from. For instance one ecosystem might return a C<Foo:ver(1)> and C<Foo:ver(3)>, and another a C<Foo:ver(2)>
for an identity of C<Foo> -- this module emulates C<Raku> internal module resolution logic and will choose the best match (in this case
C<Foo:ver(3)>.
This module purposely does not combine multiple ecosystems into a single list to make a recommendation from; by design it a recommendation
manager for the results of recommendation managers. This allows more consistent resolution of dependencies and integration with MetaCPAN like
services (which may not just provide an entire list of modules it has -- i.e. it makes it own recommendation for a name)
One difference in how recommendations are made from raku is that repos are grouped. Given pseudo backends C<[[Eco1,Eco2],[Eco3]]> we see three
repository backends in two different groups -- ecosystems in later groups are only searched if previous groups found no matches. This allows
users to avoid dependency confusion attacks by allowing custom ecosystems to be the preferred source for whatever namespaces it provides
regardless if another ecosystem later provides a module by the same name but higher version number.
Returns an C<Array> of C<Candidate>, where each C<Candidate> matches exactly one of the provided C<@identities> (and
each C<@identities> matches zero or one of the C<Candidate>).
method search
method search(:$max-results, Bool :$strict, *@identities ($, *@), *%fields --> Array[Candidate])
Resolves each identity in C<@identities> to all of its matching C<Candidates> from all backends (with C<$max-results> applying to
each individual backend). If C<$strict> is C<False> then it will consider partial matches on module short-names (i.e. 'zef search HTTP'
will get results for e.g. C<HTTP::UserAgent>).
method store
method store(*@dists --> Nil)
Attempts to store/save/cache each C<@dist> to each backend repository that provides a C<store> method. Generally this is used when
a module is fetched from e.g. fez so that C<Zef::Repository::LocalCache> can cache it for next time. Note distributions fetched from
local paths (i.e. `zef install .`) do not generally get passed to this method.
method available
method available(*@plugins --> Array[Candidate])
Returns an C<Array> of all C<Candidate> provided by all backend repositories that support the C<available> method (http-query-per-request
repositories may not be able to provide this) and have a 'name' (as defined in its entry in C<resources/config.json>) matching any of
those in C<@names> (i.e. C<zef list fez> will only show stuff from the 'fez' backend).
method update
method update(*@plugins --> Hash)
Updates each ecosystem backend (generally downloading a p6c.json or cpan.json file, or updating the 'cached' index).
=end pod
submethod TWEAK(|) {
@ = self.plugins; # preload plugins
}
#| This is what is used to resolve dependencies.
#| Similar to .search(...), except it only returns 1 result (the best match) for each identity provided
#| i.e .search for Foo:ver<*> may return multiple versions, but .candidates would only ever return 1 even
#| if it exists in multiple repositories/backends.
method candidates(*@identities ($, *@) --> Array[Candidate]) {
# todo: have a `file` identity in Zef::Identity
my %searchable = @identities.grep({ not $_.starts-with("." | "/") }).map({ $_ => 1 });
my @unsorted-candis = eager gather GROUP: for self!plugins -> @repo-group {
my @look-for = %searchable.grep(*.value).hash.keys.sort;
my @group-results = @repo-group.hyper(:batch(1)).map: -> $repo {
my @search-for = $repo.id eq 'Zef::Repository::LocalCache' ?? @identities !! @look-for;
$repo.search(@search-for, :strict).Slip;
}
for @group-results -> $dist {
%searchable{$dist.as} = 0;
take $dist;
}
last GROUP unless %searchable.values.grep(*.so).so;
}
my @unsorted-grouped-candis = @unsorted-candis.grep(*.defined).categorize({.as}).values;
# Take the distribution with the highest version out of all matching distributions from each repository
my @sorted-candis = @unsorted-grouped-candis.map: -> $candis {
my @presorted = $candis.sort(*.dist.ver).sort(*.dist.api);
my $api = @presorted.tail.dist.api;
my $version = @presorted.tail.dist.ver;
my @sorted = @presorted.grep({ .dist.ver eq $version }).grep({ .dist.api eq $api });
@sorted.tail;
}
# dedupe things the earlier `.as` categorization won't group right, like Foo:ver<1.0+> and Foo:ver<1.1+>
my Candidate @results = @sorted-candis.unique(:as(*.dist.identity));
return @results;
}
#| This is what is used to search for identities.
#| Similar to .candidates(...), except it will return more than one result per identity as appropriate.
method search(:$max-results, Bool :$strict, *@identities ($, *@), *%fields --> Array[Candidate]) {
return Nil unless @identities || %fields;
my @searchable = @identities.grep({ not $_.starts-with("." | "/") });
my @unsorted-candis = eager gather GROUP: for self!plugins -> @repo-group {
my @group-results = @repo-group.hyper(:batch(1)).map: -> $repo {
$repo.search(@searchable, :$strict).Slip;
}
if @group-results.elems {
take $_ for @group-results;
}
}
my Candidate @results = @unsorted-candis;
return @results;
}
#| Call 'store' on any Repository that provides that interface (by default just 'cached')
method store(*@dists --> Nil) {
for self!plugins.map(*.Slip).grep(*.^can('store')) -> $storage {
$storage.?store(@dists);
}
}
#| Get all candidates/distributions from each backend
method available(*@plugins --> Array[Candidate]) {
my @can-available = self!plugins(@plugins).map(*.Slip).grep: -> $plugin {
note "Plugin '{$plugin.short-name}' does not support `.available` -- Skipping" unless $plugin.can('available'); # UNDO doesn't work here yet
$plugin.can('available');
}
my @available = @can-available.hyper(:batch(1)).map({ $_.available.Slip });
my Candidate @results = @available;
return @results;
}
#| Update each Repository / backend
method update(*@plugins --> Nil) {
my @can-update = self!plugins(@plugins).grep: -> $plugin {
note "Plugin '{$plugin.short-name}' does not support `.update` -- Skipping" unless $plugin.can('update'); # UNDO doesn't work here yet
$plugin.can('update');
}
@can-update.race(:batch(1)).map({ $_.update });
}
#| Like self.plugins this returns a list of plugins that Pluggable + @.backends provides, but also allows
#| filtering which plugins are used by short-name (short-name is set in the config per Repository) so that
#| things like `--update=fez` or `--/update=cpan` work.
method !plugins(*@short-names) {
my $all-plugins := self.plugins;
return $all-plugins unless +@short-names;
my @plugins;
for $all-plugins -> @plugin-group {
if @plugin-group.grep(-> $plugin { $plugin.short-name ~~ any(@short-names) }) -> @filtered-group {
push @plugins, @filtered-group;
}
}
return @plugins;
}
}
|