File Coverage

blib/lib/App/eachperl.pm
Criterion Covered Total %
statement 62 261 23.7
branch 0 86 0.0
condition 0 36 0.0
subroutine 21 46 45.6
pod 0 17 0.0
total 83 446 18.6


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