#!/usr/bin/env perl use warnings; use strict; use Capture::Tiny qw(:all); use Cwd qw(getcwd); use Data::Dumper; use Dist::Mgr qw(:private); use File::Copy qw(move); use File::Path qw(rmtree); use Getopt::Long qw(:config no_ignore_case); my $cmd = _init(); # shifts @ARGV my %args; my $git = _validate_git(); GetOptions( "h|help" => \$args{help}, "m|module=s@" => \$args{module}, "a|author=s" => \$args{author}, "e|email=s" => \$args{email}, "r|repo=s" => \$args{repo}, "u|user=s" => \$args{user}, "i|cpanid=s" => \$args{cpan_id}, "p|cpanpw=s" => \$args{cpan_pw}, "d|dryrun" => \$args{cpan_dryrun}, "v|version=s" => \$args{version}, "V|verbose" => \$args{verbose}, "w|wait!" => \$args{wait}, "g|gitignore" => \$args{install_gitignore}, "c|ci" => \$args{install_ci}, "B|badges" => \$args{add_badges}, "b|bugtracker" => \$args{add_bugtracker}, "R|repository" => \$args{add_repository}, "A|all" => \$args{install_all}, # destroy will erase the repository before running # Used for testing purposes only "destroy" => \$args{destroy}, ); # Make sure we don't upload stuff to the CPAN when in testing if ($args{destroy}) { $args{cpan_dryrun} = 1; } $args{cpan_id} //= $ENV{CPAN_USERNAME}; $args{cpan_pw} //= $ENV{CPAN_PASSWORD}; commands()->{$cmd}(%args); # Commands sub create { my (%args) = @_; if ($args{help}) { help('create'); } for (qw(module author email)) { if (! $args{$_}) { warn "create: requires --module --author --email\n"; exit; } } for (qw(repo user)) { if (! exists $args{$_} || ! defined $args{$_}) { warn "Git functionality disabled... need both '--repo' and '--user'\n\n"; $git = 0; last; } } my $module = $args{module}->[0]; my $dir; # Clone the repository, or create the directory if no VCS if ($git) { git_clone($args{user}, $args{repo}, $args{verbose}); $dir = $args{repo}; } else { $dir = $module; $dir =~ s/::/-/g; mkdir $dir or die "ERROR: Can't create the '$dir' directory: $!"; } # Change into module directory chdir $dir or die "ERROR: Can't change into the '$dir' directory: $!"; # If in testing mode ('destroy'), clean some things up _destroy(); # Create the distribution $args{modules} = $args{module}; init(%args); # Move the files to the module dir move_distribution_files($module); # Remove our unwanted files remove_unwanted_files(); # Changes changes($module); # MANIFEST.SKIP manifest_skip(); # manifest.t manifest_t(); # Git actions if ($git) { # .gitignore git_ignore(); # CI config ci_github(); # CI/Coverage badges ci_badges($args{user}, $args{repo}); # Add bugtracker to Makefile.PL add_bugtracker($args{user}, $args{repo}); # Add repository to Makefile.PL add_repository($args{user}, $args{repo}); # git add git_add($args{verbose}); # git commit git_commit("Initial import of $module", $args{verbose}); # git push git_push($args{verbose}); } print "\nNew distribution created successfully.\n"; } sub cycle { my (%args) = @_; if ($args{help}) { help('cycle'); } my $module = _get_module(); _check_repo(); # Version (updated) my $module_file = $module; $module_file =~ s/::/\//g; $module_file = 'lib/' . $module_file . '.pm'; my $ver; if (! eval { $ver = version_info($module_file)->{$module_file}; 1 }) { die "ERROR: The $module_file file for $module can't be found.\n"; } my $ver_incr = version_incr($ver); version_bump($ver_incr); changes_bump($ver_incr); if ($git) { git_commit("Prep for $ver_incr"); git_push(); } print "\nSuccessfully cycled to the next development version\n"; } sub dist { my (%args) = @_; if ($args{help}) { help('dist'); } for (qw(module author email)) { if (! $args{$_}) { warn "dist: requires --module --author --email\n"; exit; #help(); } } my $module = $args{module}->[0]; my $dir = $module; $dir =~ s/::/-/g; mkdir $dir or die "ERROR: Can't create the '$dir' directory: $!"; # Change into module directory chdir $dir or die "ERROR: Can't change into the '$dir' directory: $!"; # Create the distribution $args{modules} = $args{module}; init(%args); # Move the files to the module dir move_distribution_files($module); # Remove our unwanted files remove_unwanted_files(); # Changes changes($module); # MANIFEST.SKIP manifest_skip(); # manifest.t manifest_t(); } sub install { my (%args) = @_; if ($args{help}) { help('install'); } if (!$args{user} || !$args{repo}) { if ($args{install_ci}) { die "ERROR: --badges requires --user and --repo\n"; } if ($args{add_badges}) { die "ERROR: --badges requires --user and --repo\n"; } if ($args{add_bugtracker}) { die "ERROR: --bugtracker requires --user and --repo\n"; } if ($args{add_repository}) { die "ERROR: --repository requires --user and --repo\n"; } if ($args{install_all}) { die "ERROR: badges, bugtracker and repository require --user and --repo\n"; } } if ($args{install_gitignore} || $args{install_all}) { print "\nInstalling .gitignore file...\n"; git_ignore() if $args{install_gitignore} || $args{install_all}; exit; } if ($args{install_ci} || $args{install_all}) { print "\nInstalling Git Actions CI configuration file\n"; ci_github(); make_manifest(); if ($git) { git_add($args{verbose}); } exit; } if ($args{add_badges} || $args{install_all}) { print "\nInstalling CI badges to POD...\n"; ci_badges($args{user}, $args{repo}) if $args{add_badges} || $args{install_all}; exit; } if ($args{add_bugtracker} || $args{install_all}) { print "\nInstalling bugtracker information to Makefile.PL...\n"; add_bugtracker($args{user}, $args{repo}) if $args{add_bugtracker} || $args{install_all}; exit; } if ($args{add_repository} || $args{install_all}) { print "\nInstalling repository information to Makefile.PL...\n"; add_repository($args{user}, $args{repo}) if $args{add_repository} || $args{install_all}; exit; } help('install'); } sub release { my (%args) = @_; config(\%args); if ($args{help}) { help('release'); } # Set RELEASE_TESTING... we are creating a release after all $ENV{RELEASE_TESTING} = 1; my $module = _get_module(); _check_repo(); # Changes (Add release date) changes_date(); # Convert module name to file name my $module_file = $module; $module_file =~ s/::/\//g; $module_file = 'lib/' . $module_file . '.pm'; # Version (current) my $ver; if (! eval { $ver = version_info($module_file)->{$module_file}; 1 }) { die "ERROR: The $module_file file for $module can't be found\n"; } # Copyright bump copyright_bump('lib/'); # make manifest make_manifest($args{verbose}); # make test if (! eval { make_test($args{verbose}); 1 }) { die "\nERROR: Local 'make test' failed. Intervention required. Halting the release procedure\n"; } # CI testing if ($git && git_status_differs()) { if (! git_release($ver, $args{wait})) { die "\nERROR: User cancelled the release process due to failing CI testing\n"; } } else { print "\nRepository unchanged, skipping Git and CI operations\n"; } # Bundle distribution make_dist($args{verbose}); # CPAN upload print "\nAre you sure you're ready to upload to the CPAN?: [y/n]: "; my $uploaded_to_cpan = 0; my $upload_to_cpan_confirmed = $args{cpan_dryrun} ? 'No' : <>; if ($upload_to_cpan_confirmed =~ /[Yy]/) { if ($args{cpan_id} && $args{cpan_pw}) { (my $dist = $module) =~ s/::/-/g; my $dist_file = (glob("${dist}-*.tar.gz"))[-1]; my $cpan_msg = capture_merged { cpan_upload( $dist_file, user => $args{cpan_id}, password => $args{cpan_pw}, dry_run => $args{cpan_dryrun} ); }; if ($cpan_msg =~ /Message:\s+(.*)/) { die "ERROR: CPAN failed with error '$1'. Couldn't upload.\n"; } $uploaded_to_cpan = 1; } else { print "\nCPAN ID nor password are set, not uploading\n"; } } # Clean the working directory make_distclean($args{verbose}); # Git tag && push git_tag($ver, $args{verbose}); git_push($args{verbose}); $ENV{RELEASE_TESTING} = 0; $uploaded_to_cpan ? print "\nRelease process completed, and build uploaded to the CPAN\n" : print "\nRelease process completed, but we didn't upload to the CPAN\n"; } # Functional sub generate_config { config({}); my $file = config_file(); if (-e $file) { print "\nDefault configuration file $file created ok\n"; } else { print "\nUnable to create the $file configuration file\n"; } } sub commands { return { config => \&generate_config, create => \&create, cycle => \&cycle, dist => \&dist, install => \&install, release => \&release, }; } # Private sub _init { help() if ! @ARGV; my $command = shift @ARGV; help() if ! defined commands()->{$command}; return $command; } sub _check_repo { if ($git) { if (git_repo() =~ /^\d+$/) { warn "Disabling Git as it doesn't appear as though you're in a " . "repository directory\n"; $git = 0; } } } sub _get_module { if ($args{modules}) { return $args{modules}->[0]; } else { open my $fh, '<', 'Makefile.PL' or die "Can't open Makefile.PL to find distribution name. Send in --module\n"; while (<$fh>) { if (/^\s+NAME.*'(.*)'/) { return $1; } } die "Can't automatically find module name. Send in --module\n"; } } sub _check_repo_info { my (%args) = @_; } sub _destroy { return if ! $args{destroy}; my $cwd = getcwd(); if ($cwd !~ /test-module$/) { die "We're not in the test repository. No way we're destroying stuff!\n"; } my $dir = 'test-module-temporary'; chdir '..' or die "Can't escalate to the parent directory\n"; mkdir $dir or die "Can't create a temporary repo dir\n"; move 'test-module/.git', "$dir/.git" or die "Can't move the .git directory to the temp dir\n"; rmtree 'test-module' or die "Can't delete the original repo dir\n"; move $dir, 'test-module' or die "Can't rename the temp dir $dir to 'test-module'\n"; chdir $cwd or die "Can't enter the repo dir\n"; git_commit("Dist::Mgr 'distmgr' test run", $args{verbose}); git_push($args{verbose}); print "\nDone testing cleanup...\n"; } sub help { my ($command) = @_; my $help = { create => [ qq{}, qq{Usage: distmgr create [OPTIONS]}, qq{}, qq{create - Creates a full blown distribution with everything included}, qq{}, qq{Options:}, qq{}, qq{ -m | --module Mandatory: The module name (eg. Test::Module)}, qq{ -a | --author Mandatory: The name of the author (eg. "Steve Bertrand")}, qq{ -e | --email Mandatory: The email address of the author}, qq{ -u | --user Optional: The Github username (eg. stevieb9)}, qq{ -r | --repo Optional: The Github repository name (eg. test-module)}, qq{ -V | --verbose Optional: (Flag) Display verbose output for each process}, qq{ -h | --help Optional: (Flag) Display this help message}, qq{}, qq{Notes: We will skip adding repository information, skip adding CI badge info,}, qq{and skip adding repository and bugtracker information to Makefile.PL file}, qq{if --user or --repo are not present in the parameter list.}, qq{}, ], dist => [ qq{}, qq{Usage: distmgr dist [OPTIONS]}, qq{}, qq{dist - Creates a bare-bones distribution with just the basics}, qq{}, qq{Options:}, qq{}, qq{ -m | --module Mandatory: The module name (eg. Test::Module)}, qq{ -a | --author Mandatory: The name of the author (eg. "Steve Bertrand")}, qq{ -e | --email Mandatory: The email address of the author}, qq{ -V | --verbose Optional: (Flag) Display verbose output for each process}, qq{ -h | --help Optional: (Flag) Display this help message}, qq{}, ], release => [ qq{}, qq{Usage: distmgr release [OPTIONS]}, qq{}, qq{release - Test then release a distribution to the CPAN}, qq{}, qq{Options:}, qq{}, qq{ -i | --cpanid Optional: Your PAUSE userid}, qq{ -p | --cpanpw Optional: Your PAUSE userid's password}, qq{ -d | --dryrun Optional: (Flag) Don't actually upload to the CPAN}, qq{ -V | --verbose Optional: (Flag) Display verbose output for each process}, qq{ -h | --help Optional: (Flag) Display this help message}, qq{}, qq{Notes: No Git operations will be performed without --repo and --user. If}, qq{the --cpanid or --cpanpw aren't available or the CPAN_USERNAME or}, qq{CPAN_PASSWORD environment variables aren't set, we won't upload to the CPAN.}, ], cycle => [ qq{}, qq{Usage: distmgr cycle [OPTIONS]}, qq{}, qq{cycle - Prepare a distribution for next development cycle after release}, qq{}, qq{Options:}, qq{ -V | --verbose Optional: (Flag) Display verbose output for each process}, qq{ -h | --help Optional: (Flag) Display this help message}, qq{}, ], install => [ qq{}, qq{Usage: distmgr install [OPTIONS]}, qq{}, qq{install - Installs/adds various files or file sections (all flags)}, qq{}, qq{ -g | --gitignore Install .gitignore file}, qq{ -c | --ci Install Github Actions CI configuration file}, qq{ -B | --badges Insert CI/Coverage badges links into the module's POD}, qq{ -b | --bugtracker Insert bugtracker information into Makefile.PL}, qq{ -R | --repository Insert repository information into Makefile.PL}, qq{ -A | --all Insert/Install all above options}, qq{ -h | --help Optional: (Flag) Display this help message}, qq{}, qq{Notes: For badges, bugtracker and repository, the --user and --repo arguments}, qq{must be supplied.}, qq{}, ] }; if (! defined $command || ! exists commands()->{$command}) { my @data = <DATA>; print $_ for @data; } else { print "$_\n" for @{ $help->{$command} }; } exit; } __DATA__ Usage: distmgr <command> [OPTIONS] Commands: create - Creates a full blown distribution with everything included -m | --module Mandatory: The module name (eg. Test::Module) -a | --author Mandatory: The name of the author (eg. "Steve Bertrand") -e | --email Mandatory: The email address of the author -u | --user Optional: The Github username (eg. stevieb9) -r | --repo Optional: The Github repository name (eg. test-module) -w | --wait Optional: Don't wait for CI test results (--nowait to disable) -V | --verbose Optional: Display verbose output for each process Note: For Git integration, create an empty Github repository, send in its short name with --repo and your Github username with --user. cycle - Bumps version numbers and prepares for the next development cycle -V | --verbose Optional: Display verbose output for each process dist - Creates a bare-bones distribution with just the basics -m | --module Mandatory: The module name (eg. Test::Module) -a | --author Mandatory: The name of the author (eg. "Steve Bertrand") -e | --email Mandatory: The email address of the author -V | --verbose Optional: (Flag) Display verbose output for each process install - Installs/adds various files or file sections (all flags) -g | --gitignore Install .gitignore file -c | --ci Install Github Actions CI configuration file -B | --badges Insert CI/Coverage badges links into the module's POD -b | --bugtracker Insert bugtracker information into Makefile.PL -R | --repository Insert repository information into Makefile.PL -A | --all Insert/Install all above options release - Tests, runs CI and releases to the CPAN your distribution. -i | --cpanid Optional: Your PAUSE userid -p | --cpanpw Optional: Your PAUSE userid's password -d | --dryrun Optional: (Flag) Don't actually upload to the CPAN Notes: No Git operations will be performed without C<--repo> and C<--user>. If the --cpanid or --cpanpw aren't available or the CPAN_USERNAME or CPAN_PASSWORD environment variables aren't set, we won't upload to the CPAN. config - Creates an initial default configuration file. This file will be named dist-mgr.json and will be placed in your HOME directory on Unix systems, and in your USERPROFILE directory on Windows.