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