App-git-ship
view release on metacpan or search on metacpan
lib/App/git/ship/perl.pm view on Meta::CPAN
sub build {
my $self = shift;
$self->clean(0);
$self->system(prove => split /\s/, $self->config('build_test_options'))
if $self->config('build_test_options');
$self->clean(0);
$self->run_hook('before_build');
$self->_render_makefile_pl if -e 'cpanfile';
$self->_timestamp_to_changes;
$self->_update_version_info;
$self->_render_readme;
$self->_make('manifest');
$self->_make('dist', '-e');
$self->run_hook('after_build');
$self;
}
sub can_handle_project {
my ($class, $file) = @_;
return $file =~ /\.pm$/ ? 1 : 0 if $file;
return path('lib')->list_tree->grep(sub {/\.pm$/})->size;
}
sub clean {
my $self = shift;
my $all = shift // 1;
my @files = qw(Makefile Makefile.old MANIFEST MYMETA.json MYMETA.yml);
unlink 'Makefile' and $self->_make('clean') if -e 'Makefile';
push @files, qw(Changes.bak META.json META.yml) if $all;
push @files, $self->_dist_files->each;
for my $file (@files) {
next unless -e $file;
unlink $file or warn "!! rm $file: $!" and next;
say "\$ rm $file" unless $self->SILENT;
}
return $self;
}
sub ship {
my $self = shift;
my $dist_file = $self->_dist_files->[0];
my $changelog = $self->config('changelog_filename');
my $uploader;
require CPAN::Uploader;
$uploader = CPAN::Uploader->new(CPAN::Uploader->read_config_file);
unless ($dist_file) {
$self->build;
$self->abort(
"Project built. Run 'git ship' again to post dist to CPAN and remote repostitory.");
}
unless ($self->config('next_version')) {
close ARGV;
local @ARGV = $changelog;
while (<>) {
/^$VERSION_RE\s*/ or next;
$self->config(next_version => $1);
last;
}
}
$self->run_hook('before_ship');
$self->system(qw(git add Makefile.PL), $changelog);
$self->system(qw(git add README.md)) if -e 'README.md';
$self->system(qw(git commit -a -m), $self->_changes_to_commit_message);
$self->SUPER::ship(@_); # after all the changes
$uploader->upload_file($dist_file);
$self->run_hook('after_ship');
}
sub start {
my $self = shift;
my $changelog = $self->config('changelog_filename');
if (my $file = $_[0]) {
$file = $file =~ m!^.?lib! ? path($file) : path(lib => $file);
$self->config(main_module_path => $file);
unless (-e $file) {
my $work_dir = lc($self->config('project_name')) =~ s!::!-!gr;
mkdir $work_dir;
chdir $work_dir or $self->abort("Could not chdir to $work_dir");
$self->config('main_module_path')->dirname->make_path;
open my $MAINMODULE, '>>', $self->config('main_module_path')
or $self->abort("Could not create %s", $self->config('main_module_path'));
}
}
$self->SUPER::start(@_);
$self->render_template('.travis.yml');
$self->render_template('.perltidyrc', {template_from_home => 1});
$self->render_template('cpanfile');
$self->render_template('Changes') if $changelog eq 'Changes';
$self->render_template('MANIFEST.SKIP');
$self->render_template('t/00-basic.t');
$self->system(qw(git add .perltidyrc .travis.yml cpanfile MANIFEST.SKIP t), $changelog);
$self->system(qw(git commit --amend -C HEAD --allow-empty)) if @_;
$self;
}
sub test_coverage {
my $self = shift;
unless (eval 'require Devel::Cover; 1') {
$self->abort(
'Devel::Cover is not installed. Install it with curl -L http://cpanmin.us | perl - Devel::Cover'
);
}
local $ENV{DEVEL_COVER_OPTIONS} = $ENV{DEVEL_COVER_OPTIONS} || '+ignore,^t\b';
local $ENV{HARNESS_PERL_SWITCHES} = '-MDevel::Cover';
$self->system(qw(cover -delete));
$self->system(qw(prove -l));
$self->system(qw(cover));
}
lib/App/git/ship/perl.pm view on Meta::CPAN
if ($line =~ $CONTRIB_START_RE) {
$contrib_block = 1;
next;
}
$contrib_block = 0 if $line =~ $CONTRIB_END_RE;
next unless $contrib_block;
if ($line =~ $CONTRIB_NAME_EMAIL_RE) {
push @contributors, "$1 <$2>";
}
elsif ($line =~ $CONTRIB_NAME_RE) {
push @contributors, $1;
}
}
return join ',', @contributors;
}
sub _build_config_param_new_version_format {
return $ENV{GIT_SHIP_NEW_VERSION_FORMAT} || '%v %Y-%m-%dT%H:%M:%S%z';
}
sub _build_config_param_main_module_path {
my $self = shift;
return path($ENV{GIT_SHIP_MAIN_MODULE_PATH}) if $ENV{GIT_SHIP_MAIN_MODULE_PATH};
my @project_name = split /-/, path->basename;
my $path = path 'lib';
PATH_PART:
for my $p (@project_name) {
opendir my $DH, $path or $self->abort("Cannot find project name from $path: $!");
for (sort { length $b <=> length $a } readdir $DH) {
my $f = "$_";
s!\.pm$!!;
next unless lc eq lc $p;
$path = path $path, $f;
last PATH_PART unless -d $path;
next PATH_PART;
}
}
return $path;
}
sub _build_config_param_project_name {
my $self = shift;
my @name = @{path($self->config('main_module_path'))};
shift @name if $name[0] eq 'lib';
$name[-1] =~ s!\.pm$!!;
return join '::', @name;
}
sub _changes_to_commit_message {
my $self = shift;
my $changelog = $self->config('changelog_filename');
my ($version, @message);
close ARGV; # reset <> iterator
local @ARGV = $changelog;
while (<>) {
last if @message and /^$VERSION_RE\s+/;
push @message, $_ if @message;
push @message, $_ and $version = $1 if /^$VERSION_RE\s+/;
}
$self->abort("Could not find any changes in $changelog") unless @message;
$message[0] =~ s!.*?\n!Released version $version\n\n!s;
local $" = '';
return "@message";
}
sub _c_objects {
my $self = shift;
my @files;
for my $d (qw(.)) {
push @files,
path($d)->list->grep(sub {/\.c|\.xs/})->map(sub { $_->basename('.c', '.xs') . '.o' })->each;
}
return @files;
}
sub _dist_files {
my $self = shift;
my $name = $self->config('project_name') =~ s!::!-!gr;
return path->list->grep(sub {m!\b$name.*\.tar!i});
}
sub _exe_files {
my $self = shift;
my @files;
for my $d (qw(bin script)) {
push @files, path($d)->list->grep(sub {-x})->each;
}
return @files;
}
sub _include_mskip_file {
my ($self, $file) = @_;
my @lines;
$file ||= do { require ExtUtils::Manifest; $ExtUtils::Manifest::DEFAULT_MSKIP; };
unless (-r $file) {
warn "MANIFEST.SKIP included file '$file' not found - skipping\n";
return '';
}
@lines = ("#!start included $file\n");
local @ARGV = ($file);
push @lines, $_ while <>;
return join "", @lines, "#!end included $file\n";
}
sub _make {
my ($self, @args) = @_;
$self->_render_makefile_pl unless -e 'Makefile.PL';
$self->system(perl => 'Makefile.PL') unless -e 'Makefile';
$self->system(make => @args);
}
sub _render_makefile_pl {
my $self = shift;
my $prereqs = Module::CPANfile->load->prereqs;
my $args = {force => 1};
my $r;
$args->{PREREQ_PM} = $prereqs->requirements_for(qw(runtime requires))->as_string_hash;
$r = $prereqs->requirements_for(qw(build requires))->as_string_hash;
$args->{BUILD_REQUIRES} = $r;
$r = $prereqs->requirements_for(qw(test requires))->as_string_hash;
$args->{TEST_REQUIRES} = $r;
$args->{RECOMMENDS} = $prereqs->requirements_for(qw(runtime recommends))->as_string_hash;
$args->{CONTRIBUTORS} = [split /,\s*/, $self->config('contributors')];
$self->render_template('Makefile.PL', $args);
$self->system(qw(perl -c Makefile.PL)); # test Makefile.PL
}
sub _render_readme {
my $self = shift;
my $skip;
if (-e 'README.md') {
my $re = "# NAME[\\n\\r\\s]+@{[$self->config('project_name')]}\\s-\\s";
$skip = path('README.md')->slurp =~ m!$re! ? undef : 'Custom README.md is in place';
}
elsif (my @alternative = path->list->grep(sub {/^README/i})->each) {
$skip = "@alternative exists.";
}
if ($skip) {
say "# Will not generate README.md: $skip" unless $self->SILENT;
return;
}
open my $README, '>:encoding(UTF-8)', 'README.md' or die "Write README.md: $!";
my $parser = Pod::Markdown->new;
$parser->output_fh($README);
$parser->parse_string_document(path($self->config('main_module_path'))->slurp);
say '# Generated README.md' unless $self->SILENT;
}
sub _timestamp_to_changes {
my $self = shift;
my $changelog = $self->config('changelog_filename');
my $loc = setlocale(LC_TIME);
my $release_line;
$release_line = sub {
my $v = shift;
my $str = $self->config('new_version_format');
$str =~ s!(%-?\d*)v!{ sprintf "${1}s", $v }!e;
setlocale LC_TIME, 'C';
$str = strftime $str, localtime;
setlocale LC_TIME, $loc;
return $str;
};
local @ARGV = $changelog;
local $^I = '';
while (<>) {
$self->config(next_version => $1)
if s/^$VERSION_RE\x20*(?:Not Released)?\x20*([\r\n]+)/{ $release_line->($1) . $2 }/e;
print; # print back to same file
}
say '# Building version ', $self->config('next_version') unless $self->SILENT;
$self->abort('Unable to add timestamp to ./%s', $changelog) unless $self->config('next_version');
}
sub _update_changes {
my $self = shift;
unless (eval "require CPAN::Changes; 1") {
say "# Cannot update './Changes' without CPAN::Changes. Install using 'cpanm CPAN::Changes'."
unless $self->SILENT;
return;
}
my $changes = CPAN::Changes->load('Changes');
$changes->preamble(
'Revision history for perl distribution ' . ($self->config('project_name') =~ s!::!-!gr));
path('Changes')->spurt($changes->serialize);
say "# Generated Changes" unless $self->SILENT;
}
sub _update_version_info {
my $self = shift;
my $version = $self->config('next_version')
or $self->abort('Internal error: Are you sure Changes has a timestamp?');
local @ARGV = ($self->config('main_module_path'));
local $^I = '';
my %r;
while (<>) {
$r{pod} ||= s/$VERSION_RE/$version/ if /^=head1 VERSION/ .. $r{pod} && /^=(cut|head1)/ || eof;
$r{var} ||= s/((?:our)?\s*\$VERSION)\s*=.*/$1 = '$version';/;
print; # print back to same file
}
$self->abort('Could not update VERSION in %s', $self->config('main_module_path')) unless $r{var};
}
1;
=encoding utf8
=head1 NAME
App::git::ship::perl - Ship your Perl module
=head1 SYNOPSIS
# Set up basic files for a Perl repo
# (Not needed if you already have an existing repo)
$ git ship start lib/My/Project.pm
$ git ship start
# Make changes
$ $EDITOR lib/My/Project.pm
# Build first if you want to investigate the changes
$ git ship build
# Ship the project to git (and CPAN)
$ git ship ship
=head1 DESCRIPTION
L<App::git::ship::perl> is a module that can ship your Perl module. This tool
differs from other tools like dzil by *NOT* requiring any configuration, except
for a file containing the credentials for uploading to CPAN.
See also L<App::git::ship/DESCRIPTION>.
Example structure and how L<App::git::ship> works on your files:
=over 4
=item * my-app/cpanfile and my-app/Makefile.PL
The C<cpanfile> is used to build the "PREREQ_PM" and "BUILD_REQUIRES"
structures in the L<ExtUtils::MakeMaker> based C<Makefile.PL> build file.
The reason for this is that C<cpanfile> is a more powerful format that can
be used by L<Carton> and other tools, so generating C<cpanfile> from
Makefile.PL would simply not be possible. Other data used to generate
Makefile.PL are:
Note that the C<cpanfile> is optional and C<Makefile.PL> will be kept untouched
unless C<cpanfile> exists.
"NAME" and "LICENSE" will have values from L</GIT_SHIP_PROJECT_NAME> and
( run in 0.230 second using v1.01-cache-2.11-cpan-eab888a1d7d )