root/lang/perl/Module-Setup/trunk/lib/Module/Setup.pm @ 20961

Revision 20961, 15.6 kB (checked in by yappo, 5 years ago)

tidy

Line 
1package Module::Setup;
2
3use strict;
4use warnings;
5our $VERSION = '0.03';
6
7use Carp ();
8use Class::Trigger;
9use ExtUtils::MakeMaker qw(prompt);
10use Fcntl qw( :mode );
11use File::Basename;
12use File::Find::Rule;
13use File::Path;
14use File::Temp;
15use Getopt::Long;
16use Module::Collect;
17use Path::Class;
18use Pod::Usage;
19use YAML ();
20
21our $HAS_TERM;
22
23sub new {
24    my $class = shift;
25    bless {}, $class;
26}
27
28sub log {
29    my($self, $msg) = @_;
30    print STDERR "$msg\n" if $HAS_TERM;
31}
32sub dialog {
33    my($self, $msg, $default) = @_;
34    return $default unless $HAS_TERM;
35    prompt($msg, $default);
36}
37
38sub _clear_triggers {
39    my $self = shift;
40    # reset triggers # this is bad hack
41    delete $self->{__triggers};
42    delete $self->{_class_trigger_results};
43}
44
45sub run {
46    my($self, $options, $argv) = @_;
47    $self->_clear_triggers;
48
49    $options ||= $self->setup_options;
50    my @argv = defined $argv && ref($argv) eq 'ARRAY' ? @{ $argv } : @ARGV;
51
52    $options->{flavor_class} ||= 'Default';
53
54    # create flavor
55    if ($options->{init}) {
56        $options->{flavor}   = shift @argv if @argv;
57        $options->{flavor} ||= 'default';
58        return $self->create_flavor($options);
59    }
60
61    # create module
62    $options->{module} = shift @argv;
63    $options->{flavor} = shift @argv if @argv;
64    $options->{flavor} ||= $self->select_flavor;
65
66    if ($options->{pack}) {
67        #pack flavor template
68        return $self->pack_flavor($options);
69    }
70
71
72    $self->{module_setup_dir} ||= $options->{module_setup_dir};
73    $self->{module_setup_dir}   = File::Temp->newdir if $options->{direct};
74
75    unless ( -d $self->module_setup_dir('flavors') && -d $self->module_setup_dir('flavors', $options->{flavor}) ) {
76        # setup the module-setup directory
77        $self->create_flavor($options);
78        $self->_create_directory( dir => $self->module_setup_dir('plugins') );
79    }
80
81    my $config = $self->load_config($options);
82    $self->load_plugins($config);
83
84    # create skeleton
85    my $attributes = $self->create_skeleton($config);
86    return unless $attributes;
87    $self->call_trigger( after_create_skeleton => $attributes );
88
89    # test
90    chdir Path::Class::Dir->new( @{ $attributes->{module_attribute}->{dist_path} } );
91    $self->call_trigger( check_skeleton_directory => $attributes );
92
93    $self->call_trigger( finalize_create_skeleton => $attributes );
94}
95
96sub setup_options {
97    my $self = shift;
98
99    pod2usage(2) unless @ARGV;
100
101    my $options = {};
102    GetOptions(
103        'init'           => \($options->{init}),
104        'pack'           => \($options->{pack}),
105        'direct'         => \($options->{direct}),
106        'flavor=s'       => \($options->{flavor}),
107        'flavor-class=s' => \($options->{flavor_class}),
108        'plugin=s@'      => \($options->{plugins}),
109        'target'         => \($options->{target}),
110        module_setup_dir => \($options->{module_setup_dir}),
111        version          => sub {
112            print "module-setup v$VERSION\n";
113            exit 1;
114        },
115        help             => sub { pod2usage(1); },
116    ) or pod2usage(2);
117
118    $options;
119}
120
121sub load_config {
122    my($self, $options) = @_;
123
124    $options->{plugins} ||= [];
125    my @option_plugins = @{ delete $options->{plugins} };
126
127    my $config = YAML::LoadFile( $self->module_setup_dir('flavors', $options->{flavor}, 'config.yaml') );
128        $config = +{
129        %{ $config },
130        %{ $options },
131    };
132
133    $config->{plugins} ||= [];
134    push @{ $config->{plugins} }, @option_plugins;
135
136    $config;
137}
138
139sub plugin_collect {
140    my($self, $config) = @_;
141
142    my @local_plugins;
143    push @local_plugins, @{ Module::Collect->new( path => $self->module_setup_dir('plugins') )->modules };
144    push @local_plugins, @{ Module::Collect->new( path => $self->module_setup_dir('flavors', $config->{flavor}, 'plugins') )->modules };
145    my %loaded_local_plugin;
146    for my $local_plugin (@local_plugins) {
147        $local_plugin->require;
148        if ($local_plugin->package->isa('Module::Setup::Plugin')) {
149            $loaded_local_plugin{$local_plugin->package} = $local_plugin;
150        }
151    }
152    %loaded_local_plugin;
153}
154
155sub load_plugins {
156    my($self, $config) = @_;
157
158    my %loaded_local_plugin = $self->plugin_collect($config);
159
160    my %loaded_plugin;
161    for my $plugin (@{ $config->{plugins} }) {
162        my $pkg;
163        my $config = +{};
164        if (ref($plugin)) {
165            if (ref($plugin) eq 'HASH') {
166                $pkg    = $plugin->{module};
167                $config = $plugin->{config};
168            } else {
169                next;
170            }
171        } else {
172            $pkg = $plugin;
173        }
174        $pkg = "Module::Setup::Plugin::$pkg" unless $pkg =~ s/^\+//;
175
176        unless ($loaded_local_plugin{$pkg}) {
177            eval "require $pkg"; ## no critic
178            Carp::croak $@ if $@;
179        }
180        $loaded_plugin{$pkg} = $pkg->new( context => $self, config => $config );
181    }
182}
183
184sub module_setup_dir {
185    my($self, @path) = @_;
186    my $base = $self->{module_setup_dir} || $ENV{MODULE_SETUP_DIR} || do {
187        eval { require File::HomeDir };
188        my $home = $@ ? $ENV{HOME} : File::HomeDir->my_home;
189        Path::Class::Dir->new( $home, '.module-setup' );
190    };
191
192    if (@path) {
193        my $new_base = Path::Class::Dir->new( $base, @path );
194        $new_base = Path::Class::File->new( $base, @path ) unless -d $base;
195        $base = $new_base;
196    }
197    $base;
198}
199
200sub create_directory {
201    my $self = shift;
202    $self->_create_directory(@_);
203}
204sub _create_directory {
205    my($self, %opts) = @_;
206    my $dir = $opts{dir} || File::Basename::dirname($opts{file});
207    unless (-e $dir) {
208        $self->log("Creating directory $dir");
209        $dir = $dir->stringify if ref($dir) && $dir->can('stringify');
210        File::Path::mkpath($dir, 0, 0777); ## no critic
211    }
212}
213
214sub write_file {
215    my($self, $opts) = @_;
216    my $path = $opts->{dist_path};
217
218    if (-e $path) {
219        my $ans = $self->dialog("$path exists. Override? [yN] ", 'n');
220        return if $ans !~ /[Yy]/;
221    }
222
223    $self->_create_directory( file => $path );
224
225    $self->log("Creating $path");
226    open my $out, ">", $path or die "$path: $!";
227    print $out $opts->{template};
228    close $out;
229
230    chmod oct($opts->{chmod}), $path if $opts->{chmod};
231}
232
233sub install_flavor {
234    my($self, $name, $tmpl) = @_;
235
236    my $path = (exists $tmpl->{plugin} && $tmpl->{plugin}) ?
237        $self->module_setup_dir( 'flavors', $name, 'plugins', $tmpl->{plugin} ) :
238            $self->module_setup_dir( 'flavors', $name, 'template', $tmpl->{file} );
239    $self->write_file(+{
240        dist_path => $path,
241        %{ $tmpl },
242    });
243}
244
245sub write_template {
246    my($self, $options) = @_;
247
248    $self->call_trigger( template_process => $options );
249    $options->{template} = delete $options->{content} unless $options->{template};
250    $options->{dist_path} =~ s/____var-(.+)-var____/$options->{vars}->{$1} || $options->{vars}->{config}->{$1}/eg;
251
252    push @{ $self->{install_files} }, $options->{dist_path};
253    $self->write_file($options);
254}
255
256sub install_template {
257    my($self, $base, $path, $vars, $module_attribute) = @_;
258
259    my $src  = Path::Class::File->new($base, $path);
260    my $dist = Path::Class::File->new(@{ $module_attribute->{dist_path} }, $path);
261
262    my $mode = ( stat $src )[2];
263    $mode = sprintf "%03o", S_IMODE($mode);
264
265    open my $fh, '<', $src or die "$src: $!";
266    my $template = do { local $/; <$fh> };
267    close $fh;
268
269    my $options = {
270        dist_path => $dist,
271        template  => $template,
272        chmod     => $mode,
273        vars      => $vars,
274        content   => undef,
275    };
276    $self->write_template($options);
277}
278
279sub create_flavor {
280    my($self, $options) = @_;
281    $options ||= +{};
282    my $name  = $options->{flavor};
283    my $class = $options->{flavor_class};
284
285    $class = "Module::Setup::Flavor::$class" unless $class =~ s/^\+//;
286
287    Carp::croak "create flavor: $name exists " if -d $self->module_setup_dir('flavors', $name);
288    eval " require $class "; Carp::croak $@ if $@; ## no critic
289
290    my @template = $class->loader;
291    my $config = +{};
292    for my $tmpl (@template) {
293        if (exists $tmpl->{config} && ref($tmpl->{config}) eq 'HASH') {
294            $config = $tmpl->{config};
295        } else {
296            $self->install_flavor($name, $tmpl);
297        }
298    }
299
300    # plugins
301    $self->_create_directory( dir => $self->module_setup_dir('flavors', $name, 'plugins') );
302
303    if (exists $options->{plugins} && $options->{plugins} && @{ $options->{plugins} }) {
304        $config->{plugins} ||= [];
305        push @{ $config->{plugins} }, @{ $options->{plugins} };
306    }
307    $config->{plugins} ||= [];
308
309    # load plugins
310    $self->load_plugins(+{
311        %{ $config },
312        %{ $options },
313        plugins => $config->{plugins},
314    });
315
316    $self->call_trigger( befor_dump_config => $config );
317
318    $self->_clear_triggers;
319
320    # save config
321    YAML::DumpFile($self->module_setup_dir('flavors', $name, 'config.yaml'), $config);
322}
323
324sub _find_flavor_template {
325    my($self, $config) = @_;
326    my $module = $config->{module};
327    my $flavor = $config->{flavor};
328
329    Carp::croak "module name is required" unless $module;
330
331    my $flavor_path = $self->module_setup_dir('flavors', $flavor);
332    Carp::croak "No such flavor: $flavor" unless -d $flavor_path;
333
334    my @files = File::Find::Rule->new->file->relative->in( $self->module_setup_dir('flavors', $flavor, 'template') );
335    Carp::croak "No such flavor template files: $flavor" unless @files;
336    @files;
337}
338
339sub create_skeleton {
340    my($self, $config) = @_;
341    $config ||= +{};
342    $self->{install_files} = [];
343
344    my @files = $self->_find_flavor_template($config);
345
346    my $module = $config->{module};
347    my $flavor = $config->{flavor};
348
349    my @pkg  = split /::/, $module;
350    my $module_attribute = +{
351        module    => $module,
352        package   => \@pkg,
353        dist_name => join('-', @pkg),
354        dist_path => [ join('-', @pkg) ],
355    };
356    if (exists $config->{target} && $config->{target}) {
357        unshift @{ $module_attribute->{dist_path} }, $config->{target};
358    }
359    $self->call_trigger( after_setup_module_attribute => $module_attribute);
360
361    $self->create_directory( dir => Path::Class::Dir->new(@{ $module_attribute->{dist_path} }) );
362
363    my $template_vars = {
364        module      => $module_attribute->{module},
365        dist        => $module_attribute->{dist_name},
366        module_path => join('/', @{ $module_attribute->{package} }),
367        config      => $config,
368        localtime   => scalar localtime,
369    };
370    $self->call_trigger( after_setup_template_vars => $template_vars);
371
372    my $base = $self->module_setup_dir('flavors', $flavor, 'template');
373    for my $path (@files) {
374        $self->install_template($base, $path, $template_vars, $module_attribute);
375    }
376    $self->call_trigger( append_template_file => $template_vars, $module_attribute);
377
378    return +{
379        module_attribute => $module_attribute,
380        template_vars    => $template_vars,
381        install_files    => $self->{install_files},
382    };
383}
384
385sub pack_flavor {
386    my($self, $config) = @_;
387    my $module = $config->{module};
388    my $flavor = $config->{flavor};
389
390    my @template_files = $self->_find_flavor_template($config);
391
392    my @plugin_files = File::Find::Rule->new->file->relative->in( $self->module_setup_dir('flavors', $flavor, 'plugins') );
393
394    my @template;
395    for my $conf (
396        { type => 'template', files => \@template_files },
397        { type => 'plugins' , files => \@plugin_files },
398        { type => 'config'  , files =>['config.yaml'] },
399    ) {
400        my $base_path;
401        if ($conf->{type} eq 'config') {
402            $base_path = $self->module_setup_dir('flavors', $flavor);
403        } else {
404            $base_path = $self->module_setup_dir('flavors', $flavor, $conf->{type});
405        }
406
407        for my $file (@{ $conf->{files} }) {
408            my $path = Path::Class::File->new($base_path, $file);
409
410            if ($conf->{type} eq 'config') {
411                my $data = YAML::LoadFile($path);
412                push @template, +{
413                    config => $data
414                };
415            } else {
416                open my $fh, '<', $path or die "$path: $!";
417                my $data = do { local $/; <$fh> };
418                close $fh;
419                my $path_name = $conf->{type} eq 'template' ? 'file' : 'plugin';
420                push @template, +{
421                    $path_name => $file,
422                    template   => $data,
423                };
424            }
425        }
426    }
427
428    my $eq = '=';
429    my $yaml = YAML::Dump(@template);
430    print <<END;
431package $module;
432use strict;
433use warnings;
434use base 'Module::Setup::Flavor';
4351;
436
437${eq}head1
438
439$module - pack from $flavor
440
441${eq}head1 SYNOPSIS
442
443  $ module-setup --init --flavor-class=+$module new_flavor
444
445${eq}cut
446
447__DATA__
448
449$yaml
450END
451}
452
453sub select_flavor {
454    my $self = shift;
455    return 'default' unless -d $self->module_setup_dir('flavors');
456
457    my $num = 1;
458    my @flavors;
459    for my $flavor ( $self->module_setup_dir('flavors')->children ) {
460        next unless $flavor->is_dir;
461
462        my $name = $flavor->dir_list(-1);
463        push @flavors, $name;
464
465        $self->log( sprintf "[%d]: %s", $num++, $name );
466    }
467
468    my $selected = $self->dialog( 'Select flavor:', 1 ) || 1;
469    $flavors[ $selected - 1 ] || 'default';
470}
471
4721;
473__END__
474
475=head1 NAME
476
477Module::Setup - a simple module maker "yet another Module::Start(?:er)?"
478
479=head1 SYNOPSIS
480
481simply use
482
483  $ module-setup Foo::Bar
484
485make flavor
486
487  $ module-setup --init catalyst-action # create a "catalyst actions" flavor
488
489edit for flavor
490
491  $ cd ~/.module-setup/flavor/catalyst-action/template && some files edit for catalyst action templates
492
493use flavor
494
495  $ module-setup Foo catalyst-action # create to Catalyst::Action::Foo module
496
497redistribute pack for flavor
498
499  $ module-setup --pack MyFlavorCatalystAction catalyst-action > MyFlavorCatalystAction.pm
500
501using redistributed flavor
502
503  $ module-setup --direct --flavor-class=+MyFlavorCatalystAction New::Class
504
505importing redistributed flavor
506
507  $ module-setup --init --flavor-class=+MyFlavorCatalystAction new_flavor
508
509for git
510
511  $ module-setup --plugin=VC::Git Foo::Bar # or edit your ~/.module-setup/flavor/foo/config.yaml
512
513
514=head1 DESCRIPTION
515
516Module::Setup is very simply module start kit.
517
518When the module-setup command is executed first, a necessary template for ~/.module-setup directory is copied.
519
520=head1 What's difference Module::Setup and Module::Starter?
521
522L<Module::Starter> is very useful module. However customize of module template is complex.
523
524If L<Module::Sterter::PBP> is used, do you solve it?
525
526Yes, but switch of two or more templates is complex.
527
528If Module::Setup is used, switch of template flavor is easy.
529
530flavor customized uniquely becomes the form which can be redistributed by "module-setup --pack".
531
532if incorporating Module::Setup in your application, you can make Helper which is well alike of Catalyst::Helper.
533
534=head1 Example For Incorporating
535
536  use Module::Setup;
537  my $pmsetup = Module::Setup->new;
538  local $ENV{MODULE_SETUP_DIR} = '/tmp/module-setup'; # dont use  ~/.module-setup directory
539  my $options = {
540      # see setup_options method
541  };
542  $pmsetup->run($options, [qw/ New::Module foo_flavor /]); # create New::Module module with foo_flavor flavor
543
544=head1 AUTHOR
545
546Kazuhiro Osawa E<lt>ko@yappo.ne.jpE<gt>
547
548=head1 SEE ALSO
549
550L<Module::Setup::Plugin>, L<module-setup>
551
552this module's base code is pmsetup written by Tatsuhiko Miyagawa.
553
554some pmsetup scripts are in a L<http://svn.coderepos.org/share/lang/perl/misc/pmsetup>
555
556=head1 REPOSITORY
557
558  svn co http://svn.coderepos.org/share/lang/perl/Module-Setup/trunk Module-Setup
559
560Module::Setup is Subversion repository is hosted at L<http://coderepos.org/share/>.
561patches and collaborators are welcome.
562
563=head1 LICENSE
564
565This library is free software; you can redistribute it and/or modify
566it under the same terms as Perl itself.
567
568=cut
Note: See TracBrowser for help on using the browser.