File Coverage

blib/lib/App/eachperl.pm
Criterion Covered Total %
statement 32 200 16.0
branch 0 82 0.0
condition 0 33 0.0
subroutine 11 27 40.7
pod 0 14 0.0
total 43 356 12.0


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 -- leonerd@leonerd.org.uk
5              
6 1     1   611 use v5.26;
  1         3  
7 1     1   501 use Object::Pad 0.54; # slot init BLOCK
  1         8890  
  1         4  
8              
9             package App::eachperl 0.06;
10             class App::eachperl;
11              
12 1     1   337 use Object::Pad qw( :experimental(init_expr) );
  1         2  
  1         3  
13              
14 1     1   506 use Config::Tiny;
  1         962  
  1         32  
15 1     1   394 use Syntax::Keyword::Dynamically;
  1         612  
  1         4  
16              
17 1     1   433 use IO::Term::Status;
  1         14983  
  1         49  
18 1     1   896 use IPC::Run ();
  1         30268  
  1         38  
19 1     1   20 use String::Tagged 0.17;
  1         28  
  1         32  
20 1     1   413 use Convert::Color::XTerm 0.06;
  1         22059  
  1         123  
21              
22             my $RESET = "\e[m";
23             my $BOLD = "\e[1m";
24              
25             my %COL = (
26             ( map { $_ => Convert::Color->new( "vga:$_" ) } qw( red blue green ) ),
27             grey => Convert::Color->new( "xterm:grey(70%)" ),
28             );
29              
30             # Allow conversion of signal numbers into names
31 1     1   7 use Config;
  1         2  
  1         88  
32             my @SIGNAMES = split m/\s+/, $Config{sig_name};
33              
34 1     1   472 use Struct::Dumb qw( struct );
  1         1873  
  1         4  
35             struct Perl => [qw( name fullpath version is_threads selected )];
36              
37             =head1 NAME
38              
39             C - a wrapper script for iterating multiple F binaries
40              
41             =head1 SYNOPSIS
42              
43             $ eachperl exec -E 'say "Hello"'
44              
45             --- perl5.30.0 ---
46             Hello
47              
48             --- bleadperl ---
49             Hello
50              
51             ----------
52             perl5.30.0 : 0
53             bleadperl : 0
54              
55             =head1 DESCRIPTION
56              
57             For more detail see the manpage for the eachperl(1) script.
58              
59             =cut
60              
61             has $_perls;
62             has $_no_system_perl :param;
63             has $_no_test :param;
64             has $_since_version :param;
65             has $_until_version :param;
66             has $_only_if :param;
67             has $_reverse :param;
68             has $_stop_on_fail :param;
69              
70             has $_io_term { IO::Term::Status->new_for_stdout };
71              
72             ADJUST
73             {
74             $self->maybe_apply_config( "./.eachperlrc" );
75             $self->maybe_apply_config( "$ENV{HOME}/.eachperlrc" );
76             $self->postprocess_config;
77             }
78              
79 0           method maybe_apply_config ( $path )
  0            
  0            
80 0     0 0   {
81             # Only accept files readable and owned by UID
82 0 0         return unless -r $path;
83 0 0         return unless -o _;
84              
85 0           my $config = Config::Tiny->read( $path );
86              
87 0   0       $_perls //= $config->{_}{perls};
88 0   0       $_since_version //= $config->{_}{since_version};
89 0   0       $_until_version //= $config->{_}{until_version};
90 0   0       $_only_if //= $config->{_}{only_if};
91             }
92              
93 0           method postprocess_config ()
  0            
94 0     0 0   {
95 0           foreach ( $_since_version, $_until_version ) {
96 0 0         defined $_ or next;
97 0 0         m/^v/ or $_ = "v$_";
98             # E.g. --until 5.14 means until the /end/ of the 5.14 series; so 5.14.999
99 0 0 0       $_ .= ".999" if \$_ == \$_until_version and $_ !~ m/\.\d+\./;
100 0           $_ = version->parse( $_ );
101             }
102              
103 0 0         if( my $perlnames = $_perls ) {
104 0           $_perls = \my @perls;
105 0           foreach my $perl ( split m/\s+/, $perlnames ) {
106 0           chomp( my $fullpath = `which $perl` );
107 0 0         $? and warn( "Can't find perl at $perl" ), next;
108              
109 0           my ( $ver, $threads ) = split m/\n/,
110             scalar `$fullpath -MConfig -e 'print "\$]\\n\$Config{usethreads}\\n"'`;
111              
112 0           $ver = version->parse( $ver )->normal;
113 0           $threads = ( $threads eq "define" );
114              
115 0           push @perls, Perl( $perl, $fullpath, $ver, $threads, undef );
116             }
117             }
118             }
119              
120 0           method perls ()
  0            
121 0     0 0   {
122 0           my @perls = @$_perls;
123 0 0         @perls = reverse @perls if $_reverse;
124              
125             return map {
126 0           my $perl = $_;
  0            
127 0           my $ver = $perl->version;
128              
129 0           my $selected = 1;
130 0 0 0       $selected = 0 if $_since_version and $ver lt $_since_version;
131 0 0 0       $selected = 0 if $_until_version and $ver gt $_until_version;
132 0 0 0       $selected = 0 if $_no_system_perl and $perl->fullpath eq $^X;
133              
134 0 0 0       if( $selected and defined $_only_if ) {
135 0 0         IPC::Run::run(
136             [ $perl->fullpath, "-Mstrict", "-Mwarnings", "-MConfig",
137             "-e", "exit !do {$_only_if}" ]
138             ) == 0 and $selected = 0;
139             }
140              
141 0           $perl->selected = $selected;
142              
143 0           $perl;
144             } @perls;
145             }
146              
147 0           method run ( @argv )
  0            
  0            
148 0     0 0   {
149 0 0         if( $argv[0] =~ m/^-/ ) {
150 0           unshift @argv, "exec";
151             }
152              
153 0           ( my $cmd = shift @argv ) =~ s/-/_/g;
154 0 0         my $code = $self->can( "run_$cmd" ) or
155             die "Unrecognised eachperl command $cmd\n";
156              
157 0           return $self->$code( @argv );
158             }
159              
160 0           method run_list ()
  0            
161 0     0 0   {
162 0           foreach my $perl ( $self->perls ) {
163 0 0         printf "%s%s: %s (%s%s)\n",
    0          
164             ( $perl->selected ? "* " : " " ),
165             $perl->name, $perl->fullpath, $perl->version,
166             $perl->is_threads ? ",threads" : "";
167             }
168 0           return 0;
169             }
170              
171 0           method run_exec ( @argv )
  0            
  0            
172 0     0 0   {
173 0 0 0       my %opts = %{ shift @argv } if @argv and ref $argv[0] eq "HASH";
  0            
174              
175 0           my @results;
176 0           my $ok = 1;
177              
178 0           my $signal;
179              
180 0           my @perls = $self->perls;
181 0           my $idx = 0;
182 0           foreach ( @perls ) {
183 0           $idx++;
184 0 0         next unless $_->selected;
185              
186 0           my $perl = $_->name;
187 0           my $path = $_->fullpath;
188              
189             my @status = (
190             ( $ok
191             ? String::Tagged->new_tagged( "-OK-", fg => $COL{grey} )
192             : String::Tagged->new_tagged( "FAIL", fg => $COL{red} ) ),
193              
194             String::Tagged->new
195             ->append( "Running " )
196             ->append_tagged( $perl, bold => 1 ),
197              
198             ( $idx < @perls
199             ? String::Tagged->new_tagged( sprintf( "(%d more)", @perls - $idx ), fg => $COL{grey} )
200 0 0         : () ),
    0          
201             );
202              
203 0           $_io_term->set_status(
204             String::Tagged->join( " | ", @status )
205             ->apply_tag( 0, -1, bg => Convert::Color->new( "vga:blue" ) )
206             );
207              
208             $opts{oneline}
209 0 0         ? $_io_term->more_partial( "$BOLD$perl:$RESET " )
210             : $_io_term->print_line( "\n$BOLD --- $perl --- $RESET" );
211              
212 0           my $has_partial = $opts{oneline};
213             IPC::Run::run [ $path, @argv ], ">pty>", sub {
214 0     0     my @lines = split m/\r?\n/, $_[0], -1;
215              
216 0 0         if( $has_partial ) {
217 0           my $line = shift @lines;
218              
219 0 0         if( $line =~ s/^\r// ) {
220 0           $_io_term->replace_partial( $line );
221             }
222             else {
223 0           $_io_term->more_partial( $line );
224             }
225              
226 0 0         if( @lines ) {
227 0           $_io_term->finish_partial;
228 0           $has_partial = 0;
229             }
230             }
231              
232             # Final element will be empty string if it ended in a newline
233 0           my $partial = pop @lines;
234              
235 0           $_io_term->print_line( $_ ) for @lines;
236              
237 0 0         if( length $partial ) {
238 0           $_io_term->more_partial( $partial );
239 0           $has_partial = 1;
240             }
241 0           };
242              
243 0 0         if( $has_partial ) {
244 0           $_io_term->finish_partial;
245             }
246              
247 0 0         if( $? & 127 ) {
248             # Exited via signal
249 0           $signal = $?;
250 0           push @results, [ $perl => "aborted on SIG$SIGNAMES[ $? ]" ];
251 0           last;
252             }
253             else {
254 0           push @results, [ $perl => $? >> 8 ];
255 0 0 0       last if $? and $_stop_on_fail;
256             }
257              
258 0 0         $ok = 0 if $?;
259             }
260              
261 0           $_io_term->set_status( "" );
262              
263 0 0         unless( $opts{no_summary} ) {
264 0           $_io_term->print_line( "\n----------" );
265 0           $_io_term->print_line( sprintf "%-20s: %s", @$_ ) for @results;
266             }
267              
268 0 0         kill $signal, $$ if $signal;
269 0           return 0;
270             }
271              
272 0           method run_cpan ( @argv )
  0            
  0            
273 0     0 0   {
274 0           return $self->run_exec( "-MCPAN", "-e", join( " ", @argv ) );
275             }
276              
277 0           method _invoke_local ( %opts )
  0            
  0            
278 0     0     {
279 0           my $perl = "";
280 0           my @args;
281              
282 0 0         if( -r "Build.PL" ) {
    0          
283 0           $perl .= <<'EOPERL';
284             system( $^X, "Build.PL" ) == 0 and
285             system( $^X, "Build", "clean" ) == 0 and
286             system( $^X, "Build" ) == 0
287             EOPERL
288 0 0         $perl .= ' and system( $^X, "Build", "test" ) == 0' if $opts{test};
289 0 0         $perl .= ' and system( $^X, "Build", "install" ) == 0' if $opts{install};
290             }
291             elsif( -r "Makefile.PL" ) {
292 0           $perl .= <<'EOPERL';
293             system( $^X, "Makefile.PL" ) == 0 and
294             system( "make" ) == 0
295             EOPERL
296 0 0         $perl .= ' and system( "make", "test" ) == 0' if $opts{test};
297 0 0         $perl .= ' and system( "make", "install" ) == 0' if $opts{install};
298             }
299             else {
300 0           die "TODO: Work out how to locally control dist when lacking Build.PL or Makefile.PL";
301             }
302              
303 0 0         $perl .= ' and system( $^X, @ARGV ) == 0', push @args, "--", @{$opts{perl}} if $opts{perl};
  0            
304              
305 0           return $self->run_exec( "-e", $perl . <<'EOPERL', @args);
306             and print "-- PASS -\n" or print "-- FAIL --\n";
307             kill $?, $$ if $? & 127;
308             exit +($? >> 8);
309             EOPERL
310             }
311              
312 0           method run_install ( $module )
  0            
  0            
313 0     0 0   {
314 0           dynamically $_no_system_perl = 1;
315              
316 0 0         return $self->run_install_local if $module eq ".";
317 0           return $self->run_cpan( install => "\"$module\"" );
318             }
319              
320 0           method run_install_local ()
  0            
321 0     0 0   {
322 0           $self->_invoke_local( test => !$_no_test, install => 1 );
323             }
324              
325 0           method run_test ( $module )
  0            
  0            
326 0     0 0   {
327 0 0         return $self->run_test_local if $module eq ".";
328 0           return $self->run_cpan( test => "\"$module\"" );
329             }
330              
331 0           method run_test_local ()
  0            
332 0     0 0   {
333 0           $self->_invoke_local( test => 1 );
334             }
335              
336 0           method run_build_then_perl ( @argv )
  0            
  0            
337 0     0 0   {
338 0           $self->_invoke_local( test => !$_no_test, perl => \@argv );
339             }
340              
341 0           method run_modversion ( $module )
  0            
  0            
342 0     0 0   {
343 0           return $self->run_exec(
344             { oneline => 1, no_summary => 1 },
345             "-M$module", "-e", "print ${module}\->VERSION, qq(\\n);"
346             );
347             }
348              
349 0           method run_modpath ( $module )
  0            
  0            
350 0     0 0   {
351 0           ( my $filename = "$module.pm" ) =~ s{::}{/}g;
352              
353 0           return $self->run_exec(
354             { oneline => 1, no_summary => 1 },
355             "-M$module", "-e", "print \$INC{qq($filename)}, qq(\\n);"
356             );
357             }
358              
359             =head1 AUTHOR
360              
361             Paul Evans
362              
363             =cut
364              
365             0x55AA;