From 4b38ff1cb75d9b45d6620842ceb63b3ef6f62b4f Mon Sep 17 00:00:00 2001 From: Charles McGarvey Date: Sat, 9 Nov 2019 17:33:21 -0700 Subject: [PATCH] add fatpack script --- .gitignore | 2 + Makefile | 2 +- bin/git-codeowners | 33 +++++- dist.ini | 16 +++ maint/branch_solo.pl | 61 +++++++++++ maint/fatpack.pl | 240 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 348 insertions(+), 6 deletions(-) create mode 100755 maint/branch_solo.pl create mode 100755 maint/fatpack.pl diff --git a/.gitignore b/.gitignore index 2df827a..6b495ee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /.perl-version /App-Codeowners-* /cover_db +/fatlib +/git-codeowners /local diff --git a/Makefile b/Makefile index 1df821f..74425b9 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ dist: $(DZIL) build distclean: clean - rm -rf cover_db + rm -rf .build cover_db fatlib git-codeowners run: $(PERL) -Ilib bin/git-codeowners $(GIT_CODEOWNERS_FLAGS) diff --git a/bin/git-codeowners b/bin/git-codeowners index 60e79b2..231b16b 100755 --- a/bin/git-codeowners +++ b/bin/git-codeowners @@ -19,12 +19,35 @@ =head1 DESCRIPTION -F is yet another CLI tool for managing F files in -git repos. In particular, it can be used to quickly find out who owns -a particular file in a monorepo (or monolith). +F is yet another CLI tool for managing F files in git repos. In +particular, it can be used to quickly find out who owns a particular file in a monorepo (or +monolith). -B The interface of this tool and its modules will -probably change as I field test some things. Feedback welcome. +B The interface of this tool and its modules will probably change as I field +test some things. Feedback welcome. + +=head1 INSTALL + +There are several ways to install F to your system. + +=head2 from CPAN + +You can install F using L: + + cpanm App::Codeowners + +=head2 from GitHub + +You can also choose to download F as a self-contained executable: + + curl -OL https://raw.githubusercontent.com/chazmcgarvey/git-codeowners/solo/git-codeowners + chmod +x git-codeowners + +To hack on the code, clone the repo instead: + + git clone https://github.com/chazmcgarvey/git-codeowners.git + cd git-codeowners + make bootstrap # installs dependencies; requires cpanm =head1 OPTIONS diff --git a/dist.ini b/dist.ini index c7fe9e6..ca06b97 100644 --- a/dist.ini +++ b/dist.ini @@ -11,9 +11,13 @@ license = Perl_5 -remove = PodCoverageTests -remove = Test::CleanNamespaces max_target_perl = 5.10.1 +PruneFiles.filename = maint [ConsistentVersionTest] +[Run::Release] +run = %x maint%pbranch_solo.pl %v %d + [RemovePhasedPrereqs] remove_runtime = JSON::MaybeXS remove_runtime = Text::CSV @@ -28,4 +32,16 @@ JSON::MaybeXS = 0 Text::CSV = 0 Text::Table = 0 YAML = 0 +[Prereqs / DevelopRecommends] +; for fatpack.pl +App::FatPacker = 0 +CPAN::Meta = 0 +Capture::Tiny = 0 +Config = 0 +File::pushd = 0 +Getopt::Long = 0 +MetaCPAN::API = 0 +Module::CoreList = 0 +Path::Tiny = 0 +Perl::Strip = 0 diff --git a/maint/branch_solo.pl b/maint/branch_solo.pl new file mode 100755 index 0000000..aa44384 --- /dev/null +++ b/maint/branch_solo.pl @@ -0,0 +1,61 @@ +#!/usr/bin/env perl + +# This script prepares the git-codeowners script for standalone use and puts it in a new branch. + +use strict; +use warnings; +use autodie; + +use File::Copy; +use File::Path qw(make_path remove_tree); +use String::ShellQuote; + +my $version = shift or die 'Need version'; +my $distdir = shift or die 'Need distdir'; + +my $branch_name = 'solo'; +my $script_name = 'git-codeowners'; + +my $branch_oldref = ''; +my $branch_oldref_origin = ''; + +open(my $fh, '-|', qw{git show-ref}, $branch_name); +while (my $line = <$fh>) { + chomp $line; + my ($hash, $ref) = split(/\s+/, $line); + $branch_oldref = $hash if $ref eq "refs/heads/$branch_name"; + $branch_oldref_origin = $hash if $ref eq "refs/remotes/origin/$branch_name"; +} +if ($branch_oldref_origin && $branch_oldref ne $branch_oldref_origin) { + # reset local branch + system(qw{git branch -f}, $branch_name, "origin/$branch_name"); + $branch_oldref = $branch_oldref_origin +} + +my $commit_msg = shell_quote("Release $version"); + +my $solodir = "solo_branch.$$"; +make_path($solodir); + +use Config; +system($Config{'perlpath'}, qw{maint/fatpack.pl --clean --dist}, $distdir); +move($script_name, "$solodir/$script_name"); + +copy("$distdir/README", "$solodir/README"); + +system(qw{git update-index --add}, glob("$solodir/*")); +my $tree_ref = `git write-tree --prefix=$solodir/`; +chomp $tree_ref; + +system(qw{git reset}); +remove_tree($solodir); + +my $branch_oldref_safe = shell_quote($branch_oldref); +my $tree_ref_safe = shell_quote($tree_ref); +my $parent = $branch_oldref ? "-p $branch_oldref_safe" : ''; +my $commit_ref = `git commit-tree -m $commit_msg $parent $tree_ref_safe`; +chomp $commit_ref; + +system(qw(git branch -f), $branch_name, $commit_ref); +system(qw(git tag -a -m), "Version $version", "$branch_name-$version", $commit_ref); + diff --git a/maint/fatpack.pl b/maint/fatpack.pl new file mode 100755 index 0000000..921380c --- /dev/null +++ b/maint/fatpack.pl @@ -0,0 +1,240 @@ +#!/usr/bin/env perl + +=head1 NAME + + maint/fatpack.pl - Generate a fatpack version of git-codeowners + +=head1 SYNOPSIS + + maint/fatpack.pl --dist-dir DIRPATH [--clean] + +=cut + +use 5.010001; +use strict; +use warnings; + +use CPAN::Meta; +use Capture::Tiny qw(capture_stdout); +use Config; +use File::pushd; +use Getopt::Long; +use MetaCPAN::API; +use Module::CoreList; +use Path::Tiny; + +my $core_version = '5.010001'; +my $plenv_version = '5.10.1'; +my %blacklist_modules = map { $_ => 1 } ( + 'perl', + 'Text::Table::ASV', # brought in by Text::Table::Any but not actually required + 'Unicode::GCString', # optional XS module +); +my @extra_modules = ( + 'Proc::Find::Parents', # used by Term::Detect::Software on some platforms +); + +my $clean = 0; +my $distdir; +GetOptions( + 'clean!' => \$clean, + 'dist=s' => \$distdir, +) or die "Invalid options.\n"; +$distdir && -d $distdir or die "Use --dist to specify path to a distribution directory.\n"; + +my $mcpan = MetaCPAN::API->new; + +run($distdir, $clean); +exit; + +sub install_modules { + my $path = path(shift); + my @modules = @_; + run_command('cpanm', '-n', "-L$path", @modules); +} + +sub run { + my $distdir = path(shift); + my $clean = shift; + + my $builddir = path('.build'); + my $fatlibdir = path('fatlib'); + + if ($clean) { + print STDERR "Cleaning...\n"; + $builddir->remove_tree({safe => 0}); + $fatlibdir->remove_tree({safe => 0}); + } + + $builddir->mkpath; + + my @modules = required_modules($distdir, $builddir->child('deps.txt')); + install_modules($builddir->child('local'), @modules); + pack_modules($builddir->child('local'), @modules); + + clean_fatlib($fatlibdir); + + # consolidate all modules into a new directory for packing + my $moduledir = $builddir->child('modules'); + $moduledir->remove_tree({safe => 0}); + $moduledir->mkpath; + system(qw{cp -r}, $fatlibdir, $distdir->child('lib'), "$moduledir/"); + + $moduledir->child('lib/Test/File/Codeowners.pm')->remove; # don't need this + + my $fatpack = do { + my $cd_builddir = pushd($moduledir); + + system('perlstrip', '--cache', '-v', find_modules('.')); + `fatpack file`; + }; + + generate_script($distdir->child('bin/git-codeowners'), $fatpack, 'git-codeowners'); +} + +sub required_modules { + my $path = path(shift); + my $cache_filepath = shift; + + print STDERR "Determining required modules...\n"; + + my $cachefile = $cache_filepath && path($cache_filepath); + if (my $contents = eval { $cachefile->slurp_utf8 }) { + chomp $contents; + return split(/\n/, $contents); + } + + my $meta = CPAN::Meta->load_file($path->child('META.json')); + + my $requires = CPAN::Meta::Requirements->new; + + for my $type (qw{requires recommends suggests}) { + my $reqs = $meta->effective_prereqs->requirements_for('runtime', $type); + for my $module ($reqs->required_modules) { + next if $blacklist_modules{$module}; + + my $core = $Module::CoreList::version{$core_version}{$module}; + print STDERR "skipping core: $module $core\n" if $core; + next if $core && $reqs->accepts_module($module, $core); + + $requires->add_string_requirement($module => $reqs->requirements_for_module($module)); + dependencies_for_module($requires, $module); + } + } + $requires->clear_requirement($_) for qw(Module::CoreList ExtUtils::MakeMaker Carp); + my @deps = $requires->required_modules; + + push @deps, @extra_modules; + + $cachefile->spew_utf8([map { "$_\n" } @deps]) if $cachefile; + + return @deps; +} + +sub dependencies_for_dist { + my $requires = shift; + my $name = shift; + + state %dists; + return if $dists{$name}++; + print STDERR "Finding dependencies for dist $name\n"; + + my $dist = $mcpan->release(distribution => $name); + + my $reqs = CPAN::Meta::Requirements->new; + + foreach my $dep (@{$dist->{dependency}}) { + next if $dep->{phase} ne 'runtime'; + next if $dep->{relationship} ne 'requires'; # && $dep->{relationship} ne 'recommends'; + + my $module = $dep->{module}; + next if $blacklist_modules{$module}; + + $reqs->add_minimum($dep->{module} => $dep->{version}); + my $core = $Module::CoreList::version{$core_version}{$module}; + print STDERR "skipping core: $module $core\n" if $core; + next if $core && $reqs->accepts_module($module, $core); + + $requires->add_string_requirement($module => $reqs->requirements_for_module($module)); + dependencies_for_module($requires, $dep->{module}); + } +} + +sub dependencies_for_module { + my $requires = shift; + my $name = shift; + + state %modules; + return if $modules{$name}++; + print STDERR "Finding dependencies for module $name\n"; + + my $module = $mcpan->module($name); + dependencies_for_dist($requires, $module->{distribution}); +} + +sub clean_fatlib { + my $path = path(shift); + $path->child($Config{archname})->remove_tree({safe => 0}); + $path->child('POD2')->remove_tree({safe => 0}); + $path->visit(sub { + local $_ = shift; + if (/\.p(od|l)$/ || /\.sample$/) { + print "rm $_\n"; + $_->remove; + } + }, {recurse => 1}); +} + +sub find_modules { + my $path = path(shift); + my @pm_filepaths; + $path->visit(sub { + local $_ = shift; + push @pm_filepaths, $_ if /\.pm$/; + }, {recurse => 1}); + return @pm_filepaths; +} + +sub pack_modules { + my ($path, @modules) = @_; + + my @filepaths = map { my $s = $_; $s =~ s!::!/!g; "$s.pm" } @modules; + + my $stdout = capture_stdout { + local $ENV{PERL5LIB} = $path->child('lib/perl5')->absolute; + system('fatpack', 'packlists-for', @filepaths); + }; + + my @packlists = split(/\n/, $stdout); + for my $packlist (@packlists) { + warn "Packing $packlist\n"; + } + + system('fatpack', 'tree', map { path($_)->absolute } @packlists); +} + +sub generate_script { + my ($input_filepath, $fatpack, $output_filepath) = @_; + + open(my $in, '<', $input_filepath) or die "open failed: $!"; + open(my $out, '>', "$output_filepath.tmp") or die "open failed: $!"; + + while (<$in>) { + s|^#!\h*perl|#!/usr/bin/env perl|; + s|^# FATPACK.*|$fatpack|; + print $out $_; + } + + unlink($output_filepath); + rename("$output_filepath.tmp", $output_filepath); + + path($output_filepath)->chmod(0755); + + print STDERR "Wrote fatpacked script: $output_filepath\n"; +} + +sub run_command { + local $ENV{PLENV_VERSION} = $plenv_version; + system('plenv', 'exec', @_); +} + -- 2.43.0