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
|
package Zef {
our sub zrun(*@_, *%_) is export { run (|@_).grep(*.?chars), |%_ }
our sub zrun-async(*@_, *%_) is export { Proc::Async.new( (|@_).grep(*.?chars), |%_ ) }
# rakudo must be able to parse json, so it doesn't
# make sense to require a dependency to parse it
our sub from-json($text) { ::("Rakudo::Internals::JSON").from-json($text) }
our sub to-json(|c) { ::("Rakudo::Internals::JSON").to-json(|c) }
enum LEVEL is export <FATAL ERROR WARN INFO VERBOSE DEBUG TRACE>;
enum STAGE is export <RESOLVE FETCH EXTRACT FILTER BUILD TEST INSTALL REPORT>;
enum PHASE is export <BEFORE START LIVE STOP AFTER>;
# A way to avoid printing everything to make --quiet option more universal between plugins
# Need to create a messaging format to include the phase, file, verbosity level, progress,
# etc we may or may not display as necessary. It's current usage is not finalized and
# any suggestions for this are well taken
role Messenger is export {
has $.stdout = Supplier.new;
has $.stderr = Supplier.new;
}
# Get a resource located at a uri and save it to the local disk
role Fetcher is export {
method fetch($uri, $save-as) { ... }
method fetch-matcher($uri) { ... }
}
# As a post-hook to the default fetchers we will need to extract zip
# files. `git` does this itself, so a git based Fetcher wouldn't need this
# although we could possibly add `--no-checkout` to `git`s fetch and treat
# Extract as the action of `--checkout $branch` (allowing us to "extract"
# a specific version from a commit/tag)
role Extractor is export {
method extract($archive-file, $target-dir) { ... }
method ls-files($archive-file) { ... }
method extract-matcher($path) { ... }
}
# test a single file OR all the files in a directory (recursive optional)
role Tester is export {
method test($path, :@includes) { ... }
method test-matcher($path) { ... }
}
role Builder is export {
method build($dist, :@includes) { ... }
method build-matcher($path) { ... }
}
role Installer is export {
method install($dist, :$cur, :$force) { ... }
method install-matcher($dist) { ... }
}
role Reporter is export {
method report($dist) { ... }
}
role Candidate is export {
has $.dist;
has $.as; # Requested as (maybe a url, maybe an identity, maybe a path)
has $.from; # Recommended from (::Ecosystems, ::MetaCPAN, ::LocalCache)
has $.uri is rw; # url, file path, etc
has Bool $.is-dependency is rw; # I don't think we use/need this anymore...
has $.build-results is rw;
has $.test-results is rw;
}
role PackageRepository is export {
# An identifier like .^name but intended to differentiate between instances of the same class
# For instance: ::Ecosystems<p6c> and ::Ecosystems<cpan> which would otherwise share the
# same .^name of ::Ecosystems
method id { $?CLASS.^name.split('+', 2)[0] }
# max-results is meant so we can :max-results(1) when we are interested in using it like
# `.candidates` (i.e. 1 match per identity) so we can stop iterating search plugins earlier
method search(:$max-results, *@identities, *%fields --> Iterable) { ... }
# Optional method currently being called after a search/fetch
# to assist ::Repository::LocalCache in updating its MANIFEST path cache.
# The concept needs more thought, but for instance a GitHub related repositories
# could commit changes or push to a remote branch, and (as is now) the cs
# ::LocalCache to update MANIFEST so we don't *have* to do a recursive folder search
#
# method store(*@dists) { }
# Optional method for listing available packages. For p6c style repositories
# where we have an index file this is easy. For metacpan style where we
# make a remote query not so much (maybe it could list the most recent X
# modules... or maybe it just doesn't implement it at all)
# method available { }
# Optional method that tells a repository to 'sync' its database.
# Useful for repositories that store the database / file locally.
# Not useful for query based resolution e.g. metacpan
# method update { }
}
# Used by the phase's loader (i.e Zef::Fetch) to test that the plugin can
# be used. for instance, ::Shell wrappers probe via `cmd --help`. Note
# that the result of .probe is cached by each phase loader
role Probeable is export {
method probe (--> Bool) { ... }
}
role Pluggable is export {
has $!plugins;
has @.backends;
sub DEBUG($plugin, $message) {
say "[Plugin - {$plugin<short-name> // $plugin<module> // qq||}] $message"\
if ?%*ENV<ZEF_PLUGIN_DEBUG>;
}
method plugins(*@short-names) {
my $all-plugins := self!list-plugins;
return $all-plugins unless +@short-names;
my @plugins;
for $all-plugins -> @plugin-group {
if @plugin-group.grep(-> $plugin { dd $plugin.short-name; $plugin.short-name ~~ any(@short-names) }) -> @filtered-group {
push @plugins, @filtered-group;
}
}
return @plugins;
}
method !list-plugins(@backends = @!backends) {
# @backends used to only be an array of hash. However now the ::Repository
# section of the config an an array of an array of hash and thus the logic
# below was adapted (it wasn't designed this way from the start).
my @plugins;
for @backends -> $backend {
if $backend ~~ Hash {
if self!try-load($backend) -> $class {
push @plugins, $class;
}
}
else {
my @group;
for @$backend -> $plugin {
if self!try-load($plugin) -> $class {
push @group, $class;
}
}
push( @plugins, @group ) if +@group;
}
}
return @plugins;
}
method !try-load(Hash $plugin) {
my $module = $plugin<module>;
DEBUG($plugin, "Checking: {$module}");
# default to enabled unless `"enabled" : 0`
if $plugin<enabled>:exists && (!$plugin<enabled> || $plugin<enabled> eq "0") {
DEBUG($plugin, "\t(SKIP) Not enabled");
return;
}
if (try require ::($ = $module)) ~~ Nil {
DEBUG($plugin, "\t(SKIP) Plugin could not be loaded");
return;
}
DEBUG($plugin, "\t(OK) Plugin loaded successful");
if ::($ = $module).^find_method('probe') {
unless ::($ = $module).probe {
DEBUG($plugin, "\t(SKIP) Probing failed");
return;
}
DEBUG($plugin, "\t(OK) Probing successful")
}
# add attribute `short-name` here to make filtering by name slightly easier
# until a more elegant solution can be integrated into plugins themselves
my $class = ::($ = $module).new(|($plugin<options> // []))\
but role :: { has $.short-name = $plugin<short-name> // '' };
unless ?$class {
DEBUG($plugin, "(SKIP) Plugin unusable: initialization failure");
return;
}
DEBUG($plugin, "(OK) Plugin is now usable: {$module}");
return $class;
}
}
}
class X::Zef::UnsatisfiableDependency is Exception {
method message() {
'Failed to resolve some missing dependencies'
}
}
|