File Coverage

blib/lib/Dist/Zilla/Plugin/OptionalFeature.pm
Criterion Covered Total %
statement 45 45 100.0
branch 11 12 91.6
condition 3 3 100.0
subroutine 13 13 100.0
pod 0 4 0.0
total 72 77 93.5


line stmt bran cond sub pod time code
1 8     8   8805042 use strict;
  8         19  
  8         2148  
2 8     8   41 use warnings;
  8         12  
  8         549  
3             package Dist::Zilla::Plugin::OptionalFeature; # git description: v0.020-10-ge385fd3
4             # ABSTRACT: Specify prerequisites for optional features in your distribution
5             # vim: set ts=8 sw=4 tw=78 et :
6             our $VERSION = '0.021';
7 8     8   43 use Moose;
  8         13  
  8         74  
8             with
9             'Dist::Zilla::Role::BeforeBuild',
10             'Dist::Zilla::Role::MetaProvider',
11             'Dist::Zilla::Role::PrereqSource';
12              
13 8     8   47039 use MooseX::Types::Moose qw(HashRef Bool);
  8         16  
  8         118  
14 8     8   43345 use MooseX::Types::Common::String 'NonEmptySimpleStr';
  8         457519  
  8         104  
15 8     8   22996 use Carp 'confess';
  8         20  
  8         578  
16 8     8   48 use Module::Runtime 'use_module';
  8         11  
  8         71  
17 8     8   340 use namespace::autoclean;
  8         17  
  8         73  
18              
19             has name => (
20             is => 'ro', isa => NonEmptySimpleStr,
21             required => 1,
22             );
23             has description => (
24             is => 'ro', isa => NonEmptySimpleStr,
25             required => 1,
26             );
27              
28             has always_recommend => (
29             is => 'ro', isa => Bool,
30             default => 0,
31             );
32              
33             has require_develop => (
34             is => 'ro', isa => Bool,
35             default => 1,
36             );
37              
38             has prompt => (
39             is => 'ro', isa => Bool,
40             lazy => 1,
41             default => sub { shift->_prereq_type eq 'requires' ? 1 : 0 },
42             );
43              
44             has default => (
45             is => 'ro', isa => Bool,
46             predicate => '_has_default',
47             # NO DEFAULT
48             );
49              
50             has load_prereqs => (
51             is => 'ro', isa => Bool,
52             default => 1,
53             );
54              
55             has _prereq_phase => (
56             is => 'ro', isa => NonEmptySimpleStr,
57             lazy => 1,
58             default => 'runtime',
59             );
60              
61             has _prereq_type => (
62             is => 'ro', isa => NonEmptySimpleStr,
63             lazy => 1,
64             default => 'requires',
65             );
66              
67             has _prereqs => (
68             is => 'ro', isa => HashRef[NonEmptySimpleStr],
69             lazy => 1,
70             default => sub { {} },
71             traits => ['Hash'],
72             handles => { _prereq_modules => 'keys', _prereq_version => 'get' },
73             );
74              
75 22     22 0 1025941 sub mvp_aliases { +{ -relationship => '-type' } }
76              
77             around BUILDARGS => sub
78             {
79             my $orig = shift;
80             my $class = shift;
81              
82             my $args = $class->$orig(@_);
83              
84             my @private = grep { /^_/ } keys %$args;
85             confess "Invalid options: @private" if @private;
86              
87             # pull these out so they don't become part of our prereq list
88             my ($zilla, $plugin_name) = delete @{$args}{qw(zilla plugin_name)};
89              
90             my %opts = (
91             map { exists $args->{$_} ? ( substr($_, 1) => delete($args->{$_}) ) : () }
92             qw(-name -description -always_recommend -require_develop -prompt -default -load_prereqs -phase -type));
93             $opts{type} //= delete $args->{'-relationship'} if defined $args->{'-relationship'};
94              
95             my @other_options = grep { /^-/ } keys %$args;
96             delete @{$args}{@other_options};
97             warn "[OptionalFeature] warning: unrecognized option(s): @other_options" if @other_options;
98              
99             # handle magic plugin names
100             if ((not $opts{name} or not $opts{phase} or not $opts{type})
101             # plugin comes from a bundle
102             and $plugin_name !~ m! (?: \A | / ) OptionalFeature \z !x)
103             {
104             $opts{name} ||= $plugin_name;
105              
106             if ($opts{name} =~ / -
107             (Build|Test|Runtime|Configure|Develop)
108             (Requires|Recommends|Suggests|Conflicts)?
109             \z/xp)
110             {
111             $opts{name} = ${^PREMATCH};
112             $opts{phase} ||= lc($1) if $1;
113             $opts{type} = lc($2) if $2;
114             }
115             }
116              
117             confess 'optional features may not use the configure phase'
118             if $opts{phase} and $opts{phase} eq 'configure';
119              
120             $opts{_prereq_phase} = delete $opts{phase} if exists $opts{phase};
121             $opts{_prereq_type} = delete $opts{type} if exists $opts{type};
122              
123             return {
124             zilla => $zilla,
125             plugin_name => $plugin_name,
126             %opts,
127             _prereqs => $args,
128             };
129             };
130              
131             has _dynamicprereqs_prompt => (
132             is => 'ro', isa => 'ArrayRef[Str]',
133             lazy => 1,
134             default => sub {
135             my $self = shift;
136              
137             my $phase = $self->_prereq_phase;
138             my $mm_key = $phase eq 'runtime' ? 'PREREQ_PM'
139             : $phase eq 'test' ? 'TEST_REQUIRES'
140             : $phase eq 'build' ? 'BUILD_REQUIRES'
141             : $self->log_fatal("illegal phase $phase");
142             $self->log_fatal('prompts are only used for the \'requires\' type')
143             if $self->_prereq_type ne 'requires';
144              
145             (my $description = $self->description) =~ s/'/\\'/g;
146              
147             my $prompt = "prompt('install $description? "
148             . ($self->default ? "[Y/n]', 'Y'" : "[y/N]', 'N'" )
149             . ') =~ /^y/i';
150             my @assignments = map {
151             qq!\$WriteMakefileArgs{$mm_key}{'$_'} = \$FallbackPrereqs{'$_'} = '${ \$self->_prereq_version($_) }'!
152             } sort $self->_prereq_modules;
153             my $require_clause = !$self->load_prereqs ? ''
154             : ('eval "' . join('', map {
155             my $version = $self->_prereq_version($_);
156             "require $_; " . ($version ? "$_->VERSION('$version'); " : '')
157             } sort $self->_prereq_modules) . '1"' . "\n || ");
158              
159             # TODO: in the future, [DynamicPrereqs] will have more sophisticated
160             # options, so we would just need to pass the prompt text, default
161             # answer, and list of prereqs and phases.
162             [
163             @assignments > 1
164             ? (
165             'if (' . $require_clause . $prompt . ') {', # to mollify vim
166             (map { ' ' . $_ . ';' } @assignments),
167             '}',
168             )
169             : ( @assignments, ' if ' . $require_clause . $prompt . ';' )
170             ];
171             },
172             );
173              
174             # package-scoped singleton to track the OptionalFeature instance that manages all the dynamic prereqs
175             my $master_plugin;
176 6     6   631505 sub __clear_master_plugin { undef $master_plugin } # for testing
177              
178             sub before_build
179             {
180 20     20 0 447846 my $self = shift;
181              
182 20 100 100     822 if ($self->prompt and not $master_plugin)
183             {
184             # because [DynamicPrereqs] inserts Makefile.PL content in reverse
185             # order to when it was called, we make just one [OptionalFeature]
186             # plugin will create and add all DynamicPrereqs plugins, so the order
187             # of the prompts is in the same order as the dist.ini declarations and
188             # the corresponding metadata.
189              
190 10         21 $master_plugin = $self;
191              
192 11         464 my $plugin = use_module('Dist::Zilla::Plugin::DynamicPrereqs')->new(
193             zilla => $self->zilla,
194             plugin_name => 'via OptionalFeature',
195             raw => [
196             join("\n",
197             map {
198 11 50       428 join("\n", $_->prompt ? @{ $_->_dynamicprereqs_prompt } : ())
  140         880  
199 10         62 } grep { $_->isa(__PACKAGE__) } @{ $self->zilla->plugins },
  10         813801  
200             )
201             ],
202             );
203              
204 8         1361 push @{ $self->zilla->plugins }, $plugin;
  8         291  
205             }
206             }
207              
208             around dump_config => sub
209             {
210             my ($orig, $self) = @_;
211             my $config = $self->$orig;
212              
213             $config->{+__PACKAGE__} = {
214             (map { $_ => $self->$_ }
215             qw(name description always_recommend require_develop prompt)),
216             $self->prompt ? ( load_prereqs => $self->load_prereqs ) : (),
217             # FIXME: YAML::Tiny does not handle leading - properly yet
218             # (map { defined $self->$_ ? ( '-' . $_ => $self->$_ ) : () }
219             (map { defined $self->$_ ? ( $_ => $self->$_ ) : () } qw(default)),
220             phase => $self->_prereq_phase,
221             type => $self->_prereq_type,
222             prereqs => $self->_prereqs,
223             };
224              
225             return $config;
226             };
227              
228             sub register_prereqs
229             {
230 18     18 0 265248 my $self = shift;
231              
232 17         808 $self->zilla->register_prereqs(
233             {
234             type => 'requires',
235             phase => 'develop',
236             },
237 18 100       837 %{ $self->_prereqs },
238             ) if $self->require_develop;
239              
240 18 100       4874 return if not $self->always_recommend;
241 2         64 $self->zilla->register_prereqs(
242             {
243             type => 'recommends',
244             phase => $self->_prereq_phase,
245             },
246 2         53 %{ $self->_prereqs },
247             );
248             }
249              
250             sub metadata
251             {
252 18     18 0 416784 my $self = shift;
253              
254             # this might be relaxed in the future -- see
255             # https://github.com/Perl-Toolchain-Gang/cpan-meta/issues/28
256             # but this is the current v2.0 spec - regexp lifted from Test::CPAN::Meta::JSON::Version
257 18 100       1143 $self->log_fatal('invalid syntax for optional feature name \'' . $self->name . '\'')
258             if $self->name !~ /^([a-z][_a-z]+)$/i;
259              
260             return {
261             # dynamic_config is NOT set, on purpose -- normally the CPAN client
262             # does the user interrogation and merging of prereqs, not Makefile.PL/Build.PL
263 17 100       600 optional_features => {
264             $self->name => {
265             description => $self->description,
266             # we don't know which way this will/should default in the spec if omitted,
267             # so we only include it if the user explicitly sets it
268             $self->_has_default ? ( x_default => $self->default ) : (),
269             prereqs => { $self->_prereq_phase => { $self->_prereq_type => $self->_prereqs } },
270             },
271             },
272             };
273             }
274              
275             __PACKAGE__->meta->make_immutable;
276              
277             __END__
278              
279             =pod
280              
281             =encoding UTF-8
282              
283             =head1 NAME
284              
285             Dist::Zilla::Plugin::OptionalFeature - Specify prerequisites for optional features in your distribution
286              
287             =head1 VERSION
288              
289             version 0.021
290              
291             =head1 SYNOPSIS
292              
293             In your F<dist.ini>:
294              
295             [OptionalFeature / XS_Support]
296             -description = XS implementation (faster, requires a compiler)
297             -prompt = 1
298             Foo::Bar::XS = 1.002
299              
300             =head1 DESCRIPTION
301              
302             This plugin provides a mechanism for specifying prerequisites for optional
303             features in metadata, which should cause CPAN clients to interactively prompt
304             you regarding these features at install time (assuming interactivity is turned
305             on: e.g. C<< cpanm --interactive Foo::Bar >>).
306              
307             The feature I<name> and I<description> are required. The name can be extracted
308             from the plugin name.
309              
310             You can specify requirements for different phases and relationships with:
311              
312             [OptionalFeature / Feature_name]
313             -description = description
314             -phase = test
315             -relationship = requires
316             Fitz::Fotz = 1.23
317             Text::SoundEx = 3
318              
319             If not provided, C<-phase> defaults to C<runtime>, and C<-relationship> to
320             C<requires>.
321              
322             To specify feature requirements for multiple phases, provide them as separate
323             plugin configurations (keeping the feature name and description constant):
324              
325             [OptionalFeature / Feature_name]
326             -description = description
327             -phase = runtime
328             Foo::Bar = 0
329              
330             [OptionalFeature / Feature_name]
331             -description = description
332             -phase = test
333             Foo::Baz = 0
334              
335             B<NOTE>: this doesn't seem to work properly with L<CPAN::Meta::Merge> (used in L<Dist::Zilla> since version 5.022).
336              
337             It is possible that future versions of this plugin may allow a more compact
338             way of providing sophisticated prerequisite specifications.
339              
340             If the plugin name is the CamelCase concatenation of a phase and relationship
341             (or just a relationship), it will set those parameters implicitly. If you use
342             a custom name, but it does not specify the relationship, and you didn't
343             specify either or both of C<-phase> or C<-relationship>, these values default
344             to C<runtime> and C<requires> respectively.
345              
346             The example below is equivalent to the synopsis example above, except for the
347             name of the resulting plugin:
348              
349             [OptionalFeature]
350             -name = XS_Support
351             -description = XS implementation (faster, requires a compiler)
352             -phase = runtime
353             -relationship = requires
354             Foo::Bar::XS = 1.002
355              
356             B<NOTE>: It is advised that you only specify I<one> prerequisite for a given
357             feature -- and if necessary, create a separate distribution to encapsulate the
358             code needed to make that feature work (along with all of its dependencies).
359             This allows external projects to declare a prerequisite not just on your
360             distribution, but also a particular feature of that distribution.
361              
362             =for Pod::Coverage mvp_aliases BUILD before_build metadata register_prereqs
363              
364             =head1 PROMPTING
365              
366             At the moment it doesn't appear that any CPAN clients properly support
367             C<optional_feature> metadata and interactively prompt the user with the
368             information therein. Therefore, prompting is added directly to F<Makefile.PL>
369             when the C<-relationship> is C<requires>. (It doesn't make much sense to
370             prompt for C<recommends> or C<suggests> features, so prompting is omitted
371             here.) You can also enable or disable this explicitly with the C<-prompt> option.
372             The prompt feature can only be used with F<Makefile.PL>. If a F<Build.PL> is
373             detected in the build and C<=prompt> is set, the build will fail.
374              
375             As with any other interactive features, the installing user can bypass the
376             prompts with C<PERL_MM_USE_DEFAULT=1>. You may want to set this when running
377             C<dzil build>.
378              
379             =head1 CONFIGURATION OPTIONS
380              
381             This is mostly a restating of the information above.
382              
383             =over 4
384              
385             =item * C<-name>
386              
387             The name of the optional feature, to be presented to the user. Can also be
388             extracted from the plugin name.
389              
390             =item * C<-description>
391              
392             The description of the optional feature, to be presented to the user.
393             Defaults to the feature name, if not provided.
394              
395             =item * C<-always_recommend>
396              
397             If set with a true value, the prerequisites are added to the distribution's
398             metadata as recommended prerequisites (e.g. L<cpanminus> will install
399             recommendations with C<--with-recommends>, even when running
400             non-interactively).
401              
402             Defaults to C<false>, but I recommend you turn this on.
403              
404             =item * C<-require_develop>
405              
406             If set with a true value, the prerequisites are added to the distribution's
407             metadata as develop requires prerequisites (e.g. L<cpanminus> will install
408             recommendations with C<--with-develop>, even when running
409             non-interactively).
410              
411             Defaults to C<true>.
412              
413             =item * C<-prompt>
414              
415             (Available since version 0.017)
416              
417             If set with a true value, F<Makefile.PL> is modified to include interactive
418             prompts.
419              
420             Default is C<true> if C<-relationship> is C<requires>.
421             C<false> otherwise.
422              
423             =item * C<-load_prereqs>
424              
425             (Available since version 0.021)
426              
427             If set, and C<-prompt> is also set, the prerequisites to be added by the feature
428             are checked for in the Perl installation; if the requirements are already met,
429             then the feature is automatically added.
430              
431             Default is C<true>.
432              
433             =item * C<-default>
434              
435             (Available since version 0.006)
436              
437             If set with a true value, non-interactive installs will automatically
438             fold the feature's prerequisites into the regular prerequisites.
439              
440             =for stopwords miyagawa
441              
442             Note that at the time of this feature's creation (September 2013), there is no
443             compliant CPAN client yet, as it invents a new C<x_default> field in metadata
444             under C<optional_feature> (thanks, miyagawa!)
445              
446             =item * C<-phase>
447              
448             The phase of the prequisite(s). Should be one of: build, test, runtime,
449             or develop.
450              
451             Default: C<runtime>
452              
453             =item * C<-relationship> (or C<-type>)
454              
455             The relationship of the prequisite(s). Should be one of: requires, recommends,
456             suggests, or conflicts.
457              
458             Default: C<requires>
459              
460             =back
461              
462             =head1 SUPPORT
463              
464             =for stopwords irc
465              
466             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-OptionalFeature>
467             (or L<bug-Dist-Zilla-Plugin-OptionalFeature@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-OptionalFeature@rt.cpan.org>).
468             I am also usually active on irc, as 'ether' at C<irc.perl.org>.
469              
470             =head1 SEE ALSO
471              
472             =over 4
473              
474             =item *
475              
476             L<CPAN::Meta::Spec/optional_features>
477              
478             =item *
479              
480             L<Module::Install::API/features, feature (Module::Install::Metadata)>
481              
482             =back
483              
484             =head1 AUTHOR
485              
486             Karen Etheridge <ether@cpan.org>
487              
488             =head1 COPYRIGHT AND LICENSE
489              
490             This software is copyright (c) 2013 by Karen Etheridge.
491              
492             This is free software; you can redistribute it and/or modify it under
493             the same terms as the Perl 5 programming language system itself.
494              
495             =head1 CONTRIBUTOR
496              
497             =for stopwords Kent Fredric
498              
499             Kent Fredric <kentnl@cpan.org>
500              
501             =cut