File Coverage

blib/lib/App/eachperl.pm
Criterion Covered Total %
statement 72 274 26.2
branch 0 88 0.0
condition 0 39 0.0
subroutine 25 50 50.0
pod 0 17 0.0
total 97 468 20.7


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2020-2024 -- leonerd@leonerd.org.uk
5              
6 1     1   535475 use v5.26;
  1         4  
7 1     1   12 use warnings;
  1         2  
  1         87  
8 1     1   1023 use Object::Pad 0.800;
  1         16556  
  1         59  
9              
10             package App::eachperl 0.11;
11             class App::eachperl;
12              
13 1     1   2201 use Object::Pad::FieldAttr::Checked 0.04;
  1         5777  
  1         5  
14 1     1   66 use Data::Checks 0.08 qw( Str Maybe );
  1         11  
  1         5  
15              
16 1     1   918 use Config::Tiny;
  1         3666  
  1         69  
17 1     1   700 use Syntax::Keyword::Dynamically;
  1         2915  
  1         7  
18              
19 1     1   2053 use Commandable::Finder::MethodAttributes 0.13 ':attrs';
  1         106058  
  1         5  
20 1     1   14452 use Commandable::Invocation;
  1         1995  
  1         72  
21              
22 1     1   605 use IO::Term::Status;
  1         14247  
  1         91  
23 1     1   1185 use IPC::Run ();
  1         69892  
  1         150  
24 1     1   14 use String::Tagged 0.17;
  1         30  
  1         54  
25 1     1   3335 use Convert::Color::XTerm 0.06;
  1         5760  
  1         278  
26              
27             my $RESET = "\e[m";
28             my $BOLD = "\e[1m";
29              
30             my %COL = (
31             ( map { $_ => Convert::Color->new( "vga:$_" ) } qw( red blue green ) ),
32             grey => Convert::Color->new( "xterm:grey(70%)" ),
33             );
34              
35             # Allow conversion of signal numbers into names
36 1     1   12 use Config;
  1         3  
  1         280  
37             my @SIGNAMES = split m/\s+/, $Config{sig_name};
38              
39             =head1 NAME
40              
41             C - a wrapper script for iterating multiple F binaries
42              
43             =head1 SYNOPSIS
44              
45             $ eachperl exec -E 'say "Hello"'
46              
47             --- perl5.30.0 ---
48             Hello
49              
50             --- bleadperl ---
51             Hello
52              
53             ----------
54             perl5.30.0 : 0
55             bleadperl : 0
56              
57             =head1 DESCRIPTION
58              
59             For more detail see the manpage for the eachperl(1) script.
60              
61             =cut
62              
63             my $VersionString_re;
64             my $VersionString;
65             BEGIN {
66 1     1   14 $VersionString = Data::Checks::StrMatch
67             $VersionString_re = qr/^v?\d+(?:\.\d+)*$/;
68             }
69              
70             field $_finder;
71              
72             field $_perls;
73             field $_install_no_system :param = undef;
74             field $_no_system_perl = !!$ENV{NO_SYSTEM_PERL};
75             field $_no_test;
76             field $_since_version;
77             field $_until_version;
78             field $_use_devel;
79             field $_only_if;
80             field $_reverse;
81             field $_stop_on_fail;
82              
83             field $_io_term = IO::Term::Status->new_for_stdout;
84              
85             class App::eachperl::_Perl {
86 0     0     field $name :param :reader :Checked(Str);
  0            
87 0     0     field $fullpath :param :reader :Checked(Str);
  0            
88 0     0     field $version :param :reader :Checked($VersionString);
  0            
89 0     0     field $is_threads :param :reader;
  0            
90 0     0     field $is_debugging :param :reader;
  0            
91 0     0     field $is_devel :param :reader;
  0            
92 0     0     field $selected :mutator;
  0            
93             }
94              
95             field @_perlobjs;
96              
97             ADJUST
98             {
99             $_finder = Commandable::Finder::MethodAttributes->new( object => $self );
100              
101             $_finder->add_global_options(
102             { name => "no-system-perl", into => \$_no_system_perl,
103             description => "Deselects the system perl version" },
104             { name => "no-test", into => \$_no_test,
105             description => "Skips the 'test' step when building a local distribution" },
106             { name => "since=", into => \$_since_version,
107             matches => $VersionString_re, match_msg => "a version string",
108             description => "Selects only perl versions that are at least as new as the requested version" },
109             { name => "until=", into => \$_until_version,
110             matches => $VersionString_re, match_msg => "a version string",
111             description => "Selects only perl versions that are at least as old as the requested version" },
112             { name => "version|v=", into => sub { $_since_version = $_until_version = $_[1] },
113             matches => $VersionString_re, match_msg => "a version string",
114             description => "Selects only the given perl version" },
115             { name => "devel", into => \$_use_devel, mode => "bool",
116             description => "Select only perl versions that are (or are not) development versions" },
117             { name => "only-if=", into => \$_only_if,
118             description => "Select only perl versions where this expression returns true" },
119             { name => "reverse|r", into => \$_reverse,
120             description => "Reverses the order in which perl versions are invoked" },
121             { name => "stop-on-fail|s", into => \$_stop_on_fail,
122             description => "Stops running after the first failure" },
123             );
124              
125             $self->maybe_apply_config( "./.eachperlrc" );
126             $self->maybe_apply_config( "$ENV{HOME}/.eachperlrc" );
127             }
128              
129 0     0 0   method maybe_apply_config ( $path )
  0            
  0            
  0            
130             {
131             # Only accept files readable and owned by UID
132 0 0         return unless -r $path;
133 0 0         return unless -o _;
134              
135 0           my $config = Config::Tiny->read( $path );
136              
137 0   0       $_perls //= $config->{_}{perls};
138 0   0       $_since_version //= $config->{_}{since_version};
139 0   0       $_until_version //= $config->{_}{until_version};
140 0   0       $_only_if //= $config->{_}{only_if};
141 0   0       $_install_no_system //= $config->{_}{install_no_system};
142             }
143              
144 0     0 0   method postprocess_config ()
  0            
  0            
145             {
146 0           foreach ( $_since_version, $_until_version ) {
147 0 0         defined $_ or next;
148 0 0         m/^v/ or $_ = "v$_";
149             # E.g. --until 5.14 means until the /end/ of the 5.14 series; so 5.14.999
150 0 0 0       $_ .= ".999" if \$_ == \$_until_version and $_ !~ m/\.\d+\./;
151 0           $_ = version->parse( $_ )->stringify;
152             }
153              
154 0 0         if( my $perlnames = $_perls ) {
155 0           foreach my $perl ( split m/\s+/, $perlnames ) {
156 0           chomp( my $fullpath = `which $perl` );
157 0 0         $? and warn( "Can't find perl at $perl" ), next;
158              
159 0           my ( $ver, $usethreads, $ccflags ) = split m/\n/,
160             scalar `$fullpath -MConfig -e 'print "\$]\\n\$Config{usethreads}\\n\$Config{ccflags}\\n"'`;
161              
162 0           $ver = version->parse( $ver )->normal;
163 0           my $threads = ( $usethreads eq "define" );
164 0           my $debug = $ccflags =~ m/-DDEBUGGING\b/;
165 0           my $devel = ( $ver =~ m/^v\d+\.(\d+)/ )[0] % 2;
166              
167 0           push @_perlobjs, App::eachperl::_Perl->new(
168             name => $perl,
169             fullpath => $fullpath,
170             version => $ver,
171             is_threads => $threads,
172             is_debugging => $debug,
173             is_devel => $devel,
174             );
175             }
176             }
177             }
178              
179 0     0 0   method perls ()
  0            
  0            
180             {
181 0           my @perls = @_perlobjs;
182 0 0         @perls = reverse @perls if $_reverse;
183              
184             return map {
185 0           my $perl = $_;
  0            
186 0           my $ver = $perl->version;
187              
188 0           my $selected = 1;
189 0 0 0       $selected = 0 if $_since_version and $ver lt $_since_version;
190 0 0 0       $selected = 0 if $_until_version and $ver gt $_until_version;
191 0 0 0       $selected = 0 if $_no_system_perl and $perl->fullpath eq $^X;
192 0 0 0       $selected = 0 if defined $_use_devel and $perl->is_devel ^ $_use_devel;
193              
194 0 0 0       if( $selected and defined $_only_if ) {
195 0 0         IPC::Run::run(
196             [ $perl->fullpath, "-Mstrict", "-Mwarnings", "-MConfig",
197             "-e", "exit !do {$_only_if}" ]
198             ) == 0 and $selected = 0;
199             }
200              
201 0           $perl->selected = $selected;
202              
203 0           $perl;
204             } @perls;
205             }
206              
207 0     0 0   method run ( @argv )
  0            
  0            
  0            
208             {
209 0           my $cinv = Commandable::Invocation->new_from_tokens( @argv );
210              
211 0           $_finder->handle_global_options( $cinv );
212              
213 0           $self->postprocess_config;
214              
215 0 0         if( $cinv->peek_remaining =~ m/^-/ ) {
216 0           $cinv->putback_tokens( "exec" );
217             }
218              
219 0           return $_finder->find_and_invoke( $cinv );
220             }
221              
222             method command_list
223             :Command_description("List the available perls")
224 0     0 0   ()
  0            
  0            
225             {
226 0           foreach my $perl ( $self->perls ) {
227 0           my @flags;
228 0           push @flags, $perl->version;
229 0 0         push @flags, "threads" if $perl->is_threads;
230 0 0         push @flags, "DEBUGGING" if $perl->is_debugging;
231 0 0         push @flags, "devel" if $perl->is_devel;
232              
233 0 0         printf "%s%s: %s (%s)\n",
234             ( $perl->selected ? "* " : " " ),
235             $perl->name, $perl->fullpath, join( ",", @flags ),
236             ;
237             }
238 0           return 0;
239 1     1   11 }
  1         3  
  1         10  
240              
241 0     0 0   method exec ( @argv )
  0            
  0            
  0            
242             {
243 0 0 0       my %opts = %{ shift @argv } if @argv and ref $argv[0] eq "HASH";
  0            
244              
245 0           my @results;
246 0           my $ok = 1;
247              
248 0           my $signal;
249              
250 0           my @perls = $self->perls;
251 0           my $idx = 0;
252 0           foreach ( @perls ) {
253 0           $idx++;
254 0 0         next unless $_->selected;
255              
256 0           my $perl = $_->name;
257 0           my $path = $_->fullpath;
258              
259             my @status = (
260             ( $ok
261             ? String::Tagged->new_tagged( "-OK-", fg => $COL{grey} )
262             : String::Tagged->new_tagged( "FAIL", fg => $COL{red} ) ),
263              
264             String::Tagged->new
265             ->append( "Running " )
266             ->append_tagged( $perl, bold => 1 ),
267              
268             ( $idx < @perls
269             ? String::Tagged->new_tagged( sprintf( "(%d more)", @perls - $idx ), fg => $COL{grey} )
270 0 0         : () ),
    0          
271             );
272              
273 0           $_io_term->set_status(
274             String::Tagged->join( " | ", @status )
275             ->apply_tag( 0, -1, bg => Convert::Color->new( "vga:blue" ) )
276             );
277              
278             $opts{oneline}
279 0 0         ? $_io_term->more_partial( "$BOLD$perl:$RESET " )
280             : $_io_term->print_line( "\n$BOLD --- $perl --- $RESET" );
281              
282 0           my $has_partial = $opts{oneline};
283             IPC::Run::run [ $path, @argv ], ">pty>", sub {
284 0     0     my @lines = split m/\r?\n/, $_[0], -1;
285              
286 0 0         if( $has_partial ) {
287 0           my $line = shift @lines;
288              
289 0 0         if( $line =~ s/^\r// ) {
290 0           $_io_term->replace_partial( $line );
291             }
292             else {
293 0           $_io_term->more_partial( $line );
294             }
295              
296 0 0         if( @lines ) {
297 0           $_io_term->finish_partial;
298 0           $has_partial = 0;
299             }
300             }
301              
302             # Final element will be empty string if it ended in a newline
303 0           my $partial = pop @lines;
304              
305 0           $_io_term->print_line( $_ ) for @lines;
306              
307 0 0         if( length $partial ) {
308 0           $_io_term->more_partial( $partial );
309 0           $has_partial = 1;
310             }
311 0           };
312              
313 0 0         if( $has_partial ) {
314 0           $_io_term->finish_partial;
315             }
316              
317 0 0         if( $? & 127 ) {
318             # Exited via signal
319 0           $signal = $?;
320 0           push @results, [ $perl => "aborted on SIG$SIGNAMES[ $? ]" ];
321 0           last;
322             }
323             else {
324 0           push @results, [ $perl => $? >> 8 ];
325 0 0 0       last if $? and $_stop_on_fail;
326             }
327              
328 0 0         $ok = 0 if $?;
329             }
330              
331 0           $_io_term->set_status( "" );
332              
333 0 0         unless( $opts{no_summary} ) {
334 0           $_io_term->print_line( "\n----------" );
335 0           $_io_term->print_line( sprintf "%-20s: %s", @$_ ) for @results;
336             }
337              
338 0 0         kill $signal, $$ if $signal;
339 0           return 0;
340             }
341              
342             method command_exec
343             :Command_description("Execute a given command on each selected perl")
344             :Command_arg("argv...", "commandline arguments")
345 0     0 0   ( $argv )
  0            
  0            
  0            
346             {
347 0           return $self->exec( @$argv );
348 1     1   3091 }
  1         3  
  1         6  
349              
350 0     0 0   method cpan ( $e, @argv )
  0            
  0            
  0            
  0            
351             {
352 0           return $self->exec( "-MCPAN", "-e", $e, @argv );
353             }
354              
355 0     0 0   method invoke_local ( %opts )
  0            
  0            
  0            
356             {
357 0           my $perl = "";
358 0           my @args;
359              
360 0 0         if( -r "Build.PL" ) {
    0          
361 0           $perl .= <<'EOPERL';
362             system( $^X, "Build.PL" ) == 0 and
363             system( $^X, "Build", "clean" ) == 0 and
364             system( $^X, "Build" ) == 0
365             EOPERL
366 0 0         $perl .= ' and system( $^X, "Build", "test" ) == 0' if $opts{test};
367 0 0         $perl .= ' and system( $^X, "Build", "install" ) == 0' if $opts{install};
368             }
369             elsif( -r "Makefile.PL" ) {
370 0           $perl .= <<'EOPERL';
371             system( $^X, "Makefile.PL" ) == 0 and
372             system( "make" ) == 0
373             EOPERL
374 0 0         $perl .= ' and system( "make", "test" ) == 0' if $opts{test};
375 0 0         $perl .= ' and system( "make", "install" ) == 0' if $opts{install};
376             }
377             else {
378 0           die "TODO: Work out how to locally control dist when lacking Build.PL or Makefile.PL";
379             }
380              
381 0 0         $perl .= ' and system( $^X, @ARGV ) == 0', push @args, "--", @{$opts{perl}} if $opts{perl};
  0            
382              
383 0           return $self->exec( "-e", $perl . <<'EOPERL', @args);
384             and print "-- PASS -\n" or print "-- FAIL --\n";
385             kill $?, $$ if $? & 127;
386             exit +($? >> 8);
387             EOPERL
388             }
389              
390             method command_install
391             :Command_description("Installs a given module")
392             :Command_arg("module", "name of the module (or \".\" for current directory)")
393 0     0 0   ( $module )
  0            
  0            
  0            
394             {
395 0 0         dynamically $_no_system_perl = 1 if $_install_no_system;
396              
397 0 0         return $self->command_install_local if $module eq ".";
398 0           return $self->cpan( 'CPAN::Shell->install($ARGV[0])', $module );
399 1     1   1909 }
  1         2  
  1         6  
400              
401             method command_install_local
402             :Command_description("Installs a module from the current directory")
403 0     0 0   ()
  0            
  0            
404             {
405 0           $self->invoke_local( test => !$_no_test, install => 1 );
406 1     1   837 }
  1         3  
  1         8  
407              
408             method command_test
409             :Command_description("Tests a given module")
410             :Command_arg("module", "name of the module (or \".\" for current directory)")
411 0     0 0   ( $module )
  0            
  0            
  0            
412             {
413 0 0         return $self->command_test_local if $module eq ".";
414 0           return $self->cpan( 'CPAN::Shell->test($ARGV[0])', $module );
415 1     1   721 }
  1         2  
  1         6  
416              
417             method command_test_local
418             :Command_description("Tests a module from the current directory")
419 0     0 0   ()
  0            
  0            
420             {
421 0           $self->invoke_local( test => 1 );
422 1     1   863 }
  1         3  
  1         5  
423              
424             method command_build_then_perl
425             :Command_description("Build the module in the current directory then run a perl command")
426             :Command_arg("argv...", "commandline arguments")
427 0     0 0   ( $argv )
  0            
  0            
  0            
428             {
429 0           $self->invoke_local( test => !$_no_test, perl => [ @$argv ] );
430 1     1   685 }
  1         2  
  1         6  
431              
432             method command_modversion
433             :Command_description("Print the installed module version")
434             :Command_arg("module", "name of the module")
435 0     0 0   ( $module )
  0            
  0            
  0            
436             {
437 0           return $self->exec(
438             { oneline => 1, no_summary => 1 },
439             "-M$module", "-e", "print ${module}\->VERSION, qq(\\n);"
440             );
441 1     1   961 }
  1         2  
  1         5  
442              
443             method command_modpath
444             :Command_description("Print the installed module path")
445             :Command_arg("module", "name of the module")
446 0     0 0   ( $module )
  0            
  0            
  0            
447             {
448 0           ( my $filename = "$module.pm" ) =~ s{::}{/}g;
449              
450 0           return $self->exec(
451             { oneline => 1, no_summary => 1 },
452             "-M$module", "-e", "print \$INC{qq($filename)}, qq(\\n);"
453             );
454 1     1   998 }
  1         3  
  1         5  
455              
456             method command_uninstall
457             :Command_description("Uninstalls a module")
458             :Command_arg("module", "name of the module")
459 0     0 0   ( $module )
  0            
  0            
  0            
460             {
461 0           return $self->exec(
462             "-e", <<'EOPERL' =~ s/^\s+//gr, $module
463             use Errno;
464             my $module = shift;
465             ( my $path = $module ) =~ s{::}{/}g;
466             my $packlist;
467             foreach ( @INC ) {
468             my $trial = "$_/auto/$path/.packlist";
469             next unless -f $trial;
470             $packlist = $trial; last;
471             }
472             defined $packlist or
473             die "Unable to find a .packlist file for $module\n";
474             open my $fh, "<", $packlist or
475             die "Unable to read $packlist - $!";
476             my $failed;
477             sub remove {
478             my ( $path ) = @_;
479             unlink $path or
480             $! == Errno::ENOENT or
481             $failed++, warn("Unable to unlink $path - $!"), return;
482             print "UNLINK $path\n";
483              
484             while( length $path ) {
485             $path =~ s{/[^/]+$}{};
486             rmdir $path and next;
487             $! == Errno::ENOENT and next;
488             $! == Errno::ENOTEMPTY and last;
489              
490             $failed++, warn("Unable to rmdir $path - $!"), return;
491             }
492             }
493             while( <$fh> ) {
494             chomp;
495             remove($_);
496             }
497             remove($packlist) if !$failed;
498             exit $failed;
499             EOPERL
500             );
501 1     1   928 }
  1         2  
  1         7  
502              
503             =head1 AUTHOR
504              
505             Paul Evans
506              
507             =cut
508              
509             0x55AA;