File Coverage

blib/lib/Dist/Zilla/Plugin/OptionalFeature.pm
Criterion Covered Total %
statement 47 47 100.0
branch 11 12 91.6
condition 3 3 100.0
subroutine 13 13 100.0
pod 0 4 0.0
total 74 79 93.6


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