The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

package Module::Setup;
use strict;
use 5.008001;
our $VERSION = '0.09';
use Carp ();
use Cwd ();
use ExtUtils::MakeMaker qw(prompt);
our $HAS_TERM;
sub argv { shift->{argv} }
sub config { shift->{config} }
sub options { shift->{options} }
sub base_dir { shift->{base_dir} }
sub distribute { shift->{distribute} }
sub plugins_stash { shift->{plugins_stash} }
sub new {
my($class, %args) = @_;
$args{options} ||= +{};
$args{argv} ||= +[];
my $self = bless { %args }, $class;
$self->{_current_dir} = Cwd::getcwd;
$self->{plugins_stash} = +{};
$self;
}
sub DESTROY {
my $self = shift;
chdir $self->{_current_dir} unless $self->{_current_dir} eq Cwd::getcwd;
}
sub _setup_options_pod2usage {
pod2usage(1);
}
sub _setup_options_version {
print "module-setup v$VERSION\n";
exit 1;
}
sub setup_options {
my($self, %args) = @_;
$Module::Setup::HAS_TERM = 1;
_setup_options_pod2usage unless @ARGV;
my $options = {};
GetOptions(
'init' => \($options->{init}),
'pack' => \($options->{pack}),
'direct' => \($options->{direct}),
'flavor|flavour=s' => \($options->{flavor}),
'flavor-class|flavour-class=s' => \($options->{flavor_class}),
'additional=s' => \($options->{additional}),
'without-additional' => \($options->{without_additional}),
'executable' => \($options->{executable}),
'plugin=s@' => \($options->{plugins}),
'target' => \($options->{target}),
'module-setup-dir' => \($options->{module_setup_dir}),
'devel' => \($options->{devel}),
'test' => \($options->{test}),
version => \&_setup_options_version,
help => \&_setup_options_pod2usage,
) or _setup_options_pod2usage;
$self->{options} = $options;
$self->{argv} = [ @ARGV ];
$self;
}
sub _clear_triggers {
my $self = shift;
# reset triggers # this is bad hack
delete $self->{__triggers};
delete $self->{_class_trigger_results};
}
sub _load_argv {
my($self, $name, $default) = @_;
$self->options->{$name} = @{ $self->argv } ? shift @{ $self->argv } : undef;
if (!$self->options->{$name} && defined $default) {
$self->options->{$name} = ref($default) eq 'CODE' ? $default->() : $default;
}
$self->options->{$name};
}
sub _load_argv_module {
my $self = shift;
$self->_load_argv( module => '' );
Carp::croak "module name is required" unless $self->options->{module};
$self->options->{module};
}
sub _load_argv_flavor {
my $self = shift;
$self->_load_argv( flavor => sub { $self->select_flavor } );
Carp::croak "flavor name is required" unless $self->options->{flavor};
$self->options->{flavor};
}
sub setup_base_dir {
my $self = shift;
my $path;
if ($self->options->{direct}) {
$path = File::Temp->newdir;
} else {
$path = $self->options->{module_setup_dir} || $ENV{MODULE_SETUP_DIR} || Module::Setup::Path::Dir->new(File::HomeDir->my_home, '.module-setup');
}
die 'module_setup directory was not able to be discovered.' unless $path;
$self->{base_dir} = Module::Setup::Path->new($path);
$self->base_dir->init_directories unless $self->base_dir->is_initialized;
}
sub run {
my $self = shift;
my $options = $self->options;
$self->_clear_triggers;
$options->{flavor_class} ||= 'Default';
return Module::Setup::Devel->new($self)->run if $options->{devel};
$self->setup_base_dir;
if ($options->{init} || (!$options->{pack} && $options->{additional})) {
$self->_load_argv( flavor => 'default' );
return $self->create_flavor;
}
$self->_load_argv_module;
$self->_load_argv_flavor;
$self->base_dir->set_flavor($options->{flavor});
if ($options->{additional} && !-d $self->base_dir->flavor->additional->path_to($options->{additional})) {
Carp::croak "additional template is no exist: $options->{additional}";
}
return $self->pack_flavor if $options->{pack};
unless ($self->base_dir->flavor->is_dir) {
return unless $self->create_flavor;
}
$self->load_config;
$self->load_plugins;
# create skeleton
$self->create_skeleton;
$self->call_trigger( 'after_create_skeleton' );
# test
chdir $self->distribute->dist_path;
$self->call_trigger( 'check_skeleton_directory' );
$self->call_trigger( 'finalize_create_skeleton' );
chdir $self->{_current_dir};
$self->call_trigger( 'finish_of_run' );
$self;
}
sub load_config {
my $self = shift;
my $options = $self->options;
my $option_plugins = delete $options->{plugins} || [];
my $config = $self->base_dir->flavor->config->load;
$config = +{
plugins => [],
%{ $config },
%{ $options },
};
push @{ $config->{plugins} }, @{ $option_plugins };
$self->{config} = $config;
}
sub plugin_collect {
my $self = shift;
my %loaded_local_plugin;
for my $local_plugin ( $self->base_dir->global_plugins->collect, $self->base_dir->flavor->plugins->collect ) {
$local_plugin->require;
if ($local_plugin->package->isa('Module::Setup::Plugin')) {
$loaded_local_plugin{$local_plugin->package} = $local_plugin;
}
}
%loaded_local_plugin;
}
sub load_plugins {
my $self = shift;
my %loaded_local_plugin = $self->plugin_collect;
$self->{loaded_plugin} ||= +{};
for my $plugin (@{ $self->config->{plugins} }) {
my $pkg;
my $config = +{};
if (ref($plugin)) {
if (ref($plugin) eq 'HASH') {
$pkg = $plugin->{module};
$config = $plugin->{config};
} else {
next;
}
} else {
$pkg = $plugin;
}
$pkg = "Module::Setup::Plugin::$pkg" unless $pkg =~ s/^\+//;
unless ($loaded_local_plugin{$pkg}) {
eval "require $pkg"; ## no critic
Carp::croak $@ if $@;
}
$self->{loaded_plugin}->{$pkg} = $pkg->new( context => $self, config => $config );
}
}
sub write_file {
my($self, $opts) = @_;
my $path = $opts->{dist_path};
if (-e $path) {
my $ans = $self->dialog("$path exists. Override? [yN] ", 'n');
return if $ans !~ /[Yy]/;
} else {
$path->dir->mkpath;
}
my $template;
if ($opts->{is_binary}) {
$template = pack 'H*', $opts->{template};
} else {
$template = $opts->{template};
}
$self->log("Creating $path");
my $out = $path->openw;
$out->print($template);
$out->close;
chmod oct($opts->{chmod}), $path if $opts->{chmod};
}
sub install_flavor {
my($self, $tmpl) = @_;
my $flavor = $self->base_dir->flavor;
my $template_path = $flavor->template;
if (exists $tmpl->{additional}) {
$template_path = Module::Setup::Path::Template->new($flavor->additional->path, $tmpl->{additional});
$template_path->path->mkpath;
}
my $path;
if (exists $tmpl->{file} && $tmpl->{file}) {
$path = $template_path->path_to(split '/', $tmpl->{file});
} elsif (exists $tmpl->{dir} && $tmpl->{dir}) {
return Module::Setup::Path::Dir->new( $template_path->path, split('/', $tmpl->{dir}) )->mkpath;
} elsif (exists $tmpl->{plugin} && $tmpl->{plugin} && !exists $tmpl->{additional}) {
$path = $flavor->plugins->path_to(split '/', $tmpl->{plugin});
} else {
return;
}
$self->write_file(+{
dist_path => $path,
%{ $tmpl },
});
}
sub _load_flavor_class {
my($self, $class) = @_;
$class = "Module::Setup::Flavor::$class" unless $class =~ s/^\+//;
eval " require $class "; Carp::croak $@ if $@; ## no critic
$class;
}
sub create_flavor {
my $self = shift;
my $options = $self->options;
my $name = $options->{flavor};
my $flavor_class = $self->_load_flavor_class($options->{flavor_class});
$self->base_dir->set_flavor($name);
Carp::croak "create flavor: $name exists " if $self->base_dir->flavor->is_exists && !exists $options->{additional};
my $flavor = $flavor_class->new;
return unless $flavor->setup_flavor($self);
my @template = $flavor->loader;
my $config = +{};
my $additional_config = +{};
if ($options->{additional}) {
$additional_config = $self->base_dir->flavor->additional->config->load;
}
for my $tmpl (@template) {
if (exists $tmpl->{config} && ref($tmpl->{config}) eq 'HASH') {
$config = $tmpl->{config};
} else {
my $additional;
if (exists $tmpl->{additional}) {
$additional = $tmpl->{additional};
} elsif ($options->{additional}) {
$additional = $options->{additional};
}
local $tmpl->{additional} = $additional if $additional; ## no critic;
if ($additional) {
$additional_config->{$additional} = +{
class => $flavor_class,
};
}
$self->install_flavor($tmpl);
}
}
$config->{class} = $flavor_class;
$self->base_dir->flavor->additional->path->mkpath;
$self->base_dir->flavor->additional->config->dump($additional_config);
if ($options->{additional}) {
$flavor->setup_additional($self, $config);
return 1;
}
$self->base_dir->flavor->plugins->path->mkpath;
$self->base_dir->flavor->template->path->mkpath;
if (exists $options->{plugins} && $options->{plugins} && @{ $options->{plugins} }) {
$config->{plugins} ||= [];
push @{ $config->{plugins} }, @{ delete $options->{plugins} };
}
$config->{plugins} ||= [];
$flavor->setup_config($self, $config);
# load plugins
local $self->{config} = +{
%{ $config },
%{ $options },
plugins => $config->{plugins},
};
$self->load_plugins;
$self->call_trigger( before_dump_config => $config );
$self->_clear_triggers;
$self->base_dir->flavor->config->dump($config);
}
sub create_skeleton {
my $self = shift;
my $config = $self->config;
$self->{distribute} = Module::Setup::Distribute->new(
$config->{module},
target => $config->{target},
);
$self->call_trigger( 'after_setup_module_attribute' );
$self->distribute->dist_path->mkpath;
my $template_vars = {
module => $self->distribute->module,
dist => $self->distribute->dist_name,
module_path => $self->distribute->module_path,
module_unix_path => join('/', @{ $self->distribute->package }),
config => $config,
distribute => $self->distribute,
localtime => scalar localtime,
moniker => $self->distribute->package->[ scalar(@{ $self->distribute->package })-1 ],
};
$self->call_trigger( after_setup_template_vars => $template_vars);
$self->{distribute}->set_template_vars($template_vars);
for my $path ($self->base_dir->flavor->template->find_files) {
$self->{distribute}->install_template($self, $path);
}
$self->call_trigger( 'append_template_file' );
return $template_vars;
}
sub _collect_flavor_files {
my($self, $template, $path_name, $type) = @_;
my $base_path = $type->path;
for my $file ($type->find_files) {
my @path = $file->is_dir ? $file->dir_list : ($file->dir->dir_list, $file->basename);
while ($path[0] eq '.') { shift @path };
if ($file->is_dir) {
push @{ $template }, +{
dir => join('/', @path),
};
} else {
my $body = $type->path_to($file)->slurp;
my $tmpl = +{
$path_name => join('/', @path),
template => $body,
};
if (-B $type->path_to($file)) {
$tmpl->{template} = unpack 'H*', $body;
$tmpl->{is_binary} = 1;
}
push @{ $template }, $tmpl;
}
}
}
sub pack_flavor {
my $self = shift;
my $config = $self->options;
my $module = $config->{module};
my $flavor = $config->{flavor};
my $template = [];
my $flavor_dir = exists $config->{flavor_dir} ? Module::Setup::Path::Flavor->new( $config->{flavor_dir} ) : $self->base_dir->flavor;
$self->_collect_flavor_files($template, file => $flavor_dir->template);
$self->_collect_flavor_files($template, plugin => $flavor_dir->plugins);
push @{ $template }, +{
config => YAML::LoadFile($flavor_dir->config->path),
};
unless ($config->{without_additional}) {
$template = [] if $config->{additional};
for my $additional ( $flavor_dir->additional->path->children ) {
next unless $additional->is_dir;
my $name = $additional->dir_list(-1);
next if $config->{additional} && $name ne $config->{additional};
my $base_path = Module::Setup::Path::Template->new($flavor_dir->additional->path, $name);
my $templates = [];
$self->_collect_flavor_files($templates, file => $base_path);
if ($config->{additional}) {
push @{ $template }, @{ $templates };
} else {
push @{ $template }, map { $_->{additional} = $name; $_ } @{ $templates };
}
}
}
my $executable_code = '';
if ($config->{executable}) {
$executable_code = <<EXECUTABLE__;
#!/bin/env perl
package main;
use strict;
use warnings;
use Module::Setup;
my \$msetup = Module::Setup->new;
\$msetup->setup_options;
\$msetup->options->{direct} = 1;
\$msetup->options->{flavor_class} = '+$config->{module}';
{
no warnings 'redefine';
*Module::Setup::_load_flavor_class = sub { '$config->{module}' };
\$msetup->run;
}
EXECUTABLE__
}
my $eq = '=';
my $yaml = YAML::Dump(@{ $template });
$self->stdout(<<FLAVOR__);
$executable_code
package $module;
use strict;
use warnings;
use base 'Module::Setup::Flavor';
1;
${eq}head1
$module - pack from $flavor
${eq}head1 SYNOPSIS
$ module-setup --init --flavor-class=+$module new_flavor
${eq}cut
\__DATA__
$yaml
FLAVOR__
}
sub select_flavor {
my $self = shift;
return 'default' if $self->options->{direct};
return 'default' if $self->base_dir->flavors->path->children == 0;
my @flavors;
for my $flavor ( $self->base_dir->flavors->path->children ) {
next unless $flavor->is_dir;
my $name = $flavor->dir_list(-1);
($name eq 'default') ? unshift @flavors, $name : push @flavors, $name;
}
return $flavors[0] if @flavors == 1;
my $num = 1;
my $message;
for my $flavor (@flavors) {
$message .= sprintf "[%d]: %s\n", $num++, $flavor;
}
my $selected;
$self->dialog( "${message}Select flavor:", 1, sub {
my($self, $ret) = @_;
return unless $ret =~ /^[0-9]+$/;
$selected = $flavors[ $ret - 1 ];
} );
$self->log("You chose flavor: $selected");
return $selected;
}
sub stdout {
my($self, $msg) = @_;
print STDOUT "$msg\n" if $HAS_TERM;
}
sub log {
my($self, $msg) = @_;
print STDERR "$msg\n" if $HAS_TERM;
}
sub dialog {
my($self, $msg, $default, $validator_callback) = @_;
return $default unless $HAS_TERM;
while (1) {
my $ret = prompt($msg, $default);
return $ret unless $validator_callback && ref($validator_callback) eq 'CODE';
return $ret if $validator_callback->($self, $ret);
}
}
sub system {
my($self, @args) = @_;
CORE::system(@args);
}
sub shell {
my($self, $cmd) = @_;
`$cmd`;
}
1;
__END__
=head1 NAME
Module::Setup - a simple module maker "yet another Module::Start(?:er)?"
=head1 SYNOPSIS
simply use
$ module-setup Foo::Bar
make flavor
$ module-setup --init catalyst-action # create a "catalyst actions" flavor
edit for flavor
$ cd ~/.module-setup/flavor/catalyst-action/template && some files edit for catalyst action templates
use flavor
$ module-setup Foo catalyst-action # create to Catalyst::Action::Foo module
redistribute pack for flavor
$ module-setup --pack MyFlavorCatalystAction catalyst-action > MyFlavorCatalystAction.pm
using redistributed flavor
$ module-setup --direct --flavor-class=+MyFlavorCatalystAction New::Class
importing redistributed flavor
$ module-setup --init --flavor-class=+MyFlavorCatalystAction new_flavor
install additional template
$ module-setup --flavor-class=+MyFlavorCatalystDBIC --additional=DBIC catalyst
redistribute pack for additional template
$ module-setup --pack --additional=DBIC MyFlavorCatalystDBIC catalyst > MyFlavorCatalystDBIC.pm
redistribute pack without additional template
$ module-setup --pack --without-additional MyFlavorCatalyst catalyst > MyFlavorCatalyst.pm
for git
$ module-setup --plugin=VC::Git Foo::Bar # or edit your ~/.module-setup/flavor/foo/config.yaml
=head1 DESCRIPTION
Module::Setup is very simply module start kit.
When the module-setup command is executed first, a necessary template for ~/.module-setup directory is copied.
=head1 What's difference Module::Setup and Module::Starter?
L<Module::Starter> is very useful module. However customize of module template is complex.
If L<Module::Starter::PBP> is used, do you solve it?
Yes, but switch of two or more templates is complex.
If Module::Setup is used, switch of template flavor is easy.
flavor customized uniquely becomes the form which can be redistributed by "module-setup --pack".
if incorporating Module::Setup in your application, you can make Helper which is well alike of Catalyst::Helper.
=head1 Example For Incorporating
use Module::Setup;
local $ENV{MODULE_SETUP_DIR} = '/tmp/module-setup'; # dont use ~/.module-setup directory
my $options = {
# see setup_options method
};
my $pmsetup = Module::Setup->new(
options => $options,
argv => [qw/ New::Module foo_flavor /],
);
$pmsetup->run; # create New::Module module with foo_flavor flavor
=head2 Module::Setup::Core
Module::Setup considers as possible that it can use as Helper-toolkit for other applications.
but Module::Setup has many dependence modules.
I hear "Module::Setup is very not usefl for my application helper tool, because many dependence." well.
The Core function of Module::Setup is split to another distribution in conclusion.
The name considered now is Module::Setup::Core.
It leaves Flavors, Plugins and C<module-setup> script to Module::Setup.
Really required dependence for Core is lessened by this thing.
It develops in L<http://github.com/yappo/p5-Module-Setup-Core> and Module::Setup's core-branch.
=head1 AUTHOR
Kazuhiro Osawa E<lt>yappo <at> shibuya <döt> plE<gt>
walf443
hidek
tokuhirom
typester
gfx
charsbar
=head1 SEE ALSO
L<Module::Setup::Plugin>, L<module-setup>
this module's base code is pmsetup written by Tatsuhiko Miyagawa.
=head1 REPOSITORY
Module::Setup is git repository is hosted at L<http://github.com/yappo/p5-Module-Setup>.
patches and collaborators are welcome.
=head1 LICENSE
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut