| File: | blib/lib/App/GHGen/PerlCustomizer.pm |
| Coverage: | 97.8% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::GHGen::PerlCustomizer; | |||||
| 2 | ||||||
| 3 | 6 6 | 109737 7 | use v5.36; | |||
| 4 | 6 6 6 | 7 4 37 | use strict; | |||
| 5 | 6 6 6 | 8 4 80 | use warnings; | |||
| 6 | ||||||
| 7 | 6 6 6 | 645 8922 109 | use Path::Tiny; | |||
| 8 | ||||||
| 9 | 6 6 6 | 10 5 4277 | use Exporter 'import'; | |||
| 10 | our @EXPORT_OK = qw( | |||||
| 11 | detect_perl_requirements | |||||
| 12 | generate_custom_perl_workflow | |||||
| 13 | ); | |||||
| 14 | ||||||
| 15 | our $VERSION = '0.10'; | |||||
| 16 | ||||||
| 17 | =encoding utf-8 | |||||
| 18 | ||||||
| 19 - 104 | =head1 NAME
App::GHGen::PerlCustomizer - Customize Perl workflows based on project requirements
=head1 SYNOPSIS
use App::GHGen::PerlCustomizer qw(detect_perl_requirements);
my $requirements = detect_perl_requirements();
# Returns: { min_version => '5.036', has_cpanfile => 1, ... }
=head1 FUNCTIONS
=head2 detect_perl_requirements()
Detect Perl version requirements and dependency-file presence in the current directory.
=head3 Purpose
Inspect the current working directory for common Perl distribution files
(C<cpanfile>, C<Makefile.PL>, C<dist.ini>, C<Build.PL>) and extract the
minimum Perl version declared in any of them.
=head3 Arguments
None.
=head3 Returns
A hash reference with the following keys, all of which are always present:
{
min_version => Str | undef, # e.g. '5.036'; undef when undetected
has_cpanfile => Bool,
has_makefile_pl => Bool,
has_dist_ini => Bool,
has_build_pl => Bool,
}
=head3 Side Effects
Reads files from the current working directory.
=head3 Usage Example
use App::GHGen::PerlCustomizer qw(detect_perl_requirements);
my $reqs = detect_perl_requirements();
say $reqs->{min_version} // 'not specified';
=head3 API SPECIFICATION
=head4 Input
# No parameters.
=head4 Output
{
type => 'hashref',
keys => {
min_version => { type => 'scalar', optional => 1 },
has_cpanfile => { type => 'scalar' },
has_makefile_pl => { type => 'scalar' },
has_dist_ini => { type => 'scalar' },
has_build_pl => { type => 'scalar' },
},
}
=head3 FORMAL SPECIFICATION
detect_perl_requirements : â Requirements
Requirements â {
min_version: â¤* ⪠{ ⥠},
has_cpanfile: ð¹,
has_makefile_pl: ð¹,
has_dist_ini: ð¹,
has_build_pl: ð¹,
}
min_version â
cpanfile exists â§ version parseable from cpanfile â version
Makefile.PL exists â§ MIN_PERL_VERSION parseable â version
otherwise â â¥
=cut | |||||
| 105 | ||||||
| 106 | 13 13 | 12922 11 | sub detect_perl_requirements() { | |||
| 107 | 13 | 40 | my %reqs = ( | |||
| 108 | min_version => undef, | |||||
| 109 | has_cpanfile => 0, | |||||
| 110 | has_makefile_pl => 0, | |||||
| 111 | has_dist_ini => 0, | |||||
| 112 | has_build_pl => 0, | |||||
| 113 | ); | |||||
| 114 | ||||||
| 115 | # Check for dependency files | |||||
| 116 | 13 | 20 | $reqs{has_cpanfile} = path('cpanfile')->exists; | |||
| 117 | 13 | 304 | $reqs{has_makefile_pl} = path('Makefile.PL')->exists; | |||
| 118 | 13 | 221 | $reqs{has_dist_ini} = path('dist.ini')->exists; | |||
| 119 | 13 | 177 | $reqs{has_build_pl} = path('Build.PL')->exists; | |||
| 120 | ||||||
| 121 | # Try to detect minimum Perl version | |||||
| 122 | 13 | 160 | if ($reqs{has_cpanfile}) { | |||
| 123 | 9 | 13 | my $content = path('cpanfile')->slurp_utf8; | |||
| 124 | 9 | 1694 | if ($content =~ /requires\s+['"]perl['"],?\s+['"]([0-9.]+)['"]/) { | |||
| 125 | 9 | 15 | $reqs{min_version} = $1; | |||
| 126 | } | |||||
| 127 | } | |||||
| 128 | ||||||
| 129 | 13 | 35 | if (!$reqs{min_version} && $reqs{has_makefile_pl}) { | |||
| 130 | 1 | 1 | my $content = path('Makefile.PL')->slurp_utf8; | |||
| 131 | 1 | 85 | if ($content =~ /MIN_PERL_VERSION\s*=>\s*['"]([0-9.]+)['"]/) { | |||
| 132 | 1 | 2 | $reqs{min_version} = $1; | |||
| 133 | } | |||||
| 134 | } | |||||
| 135 | ||||||
| 136 | 13 | 14 | return \%reqs; | |||
| 137 | } | |||||
| 138 | ||||||
| 139 - 300 | =head2 generate_custom_perl_workflow($options)
Generate a customized Perl CI workflow and return it as a YAML string.
B<Options:>
=over 4
=item C<perl_versions> (array ref)
Explicit list of Perl versions to include in the test matrix, e.g.
C<['5.40', '5.38', '5.36']>. Takes precedence over C<min_perl_version>
and C<max_perl_version> when supplied.
=item C<min_perl_version> (string, default C<'5.36'>)
Lowest Perl version to include in the matrix when C<perl_versions> is not
given. Accepts both C<'5.036'> and C<'5.36'> notation.
=item C<max_perl_version> (string, default C<'5.42'>)
Highest Perl version to include in the default matrix when C<perl_versions>
is not given. The built-in version table currently spans C<5.22> through
C<5.44>; the default produces the matrix C<5.36, 5.38, 5.40, 5.42>.
To test against B<Perl 5.44> (opt-in, not tested by default), pass
C<< max_perl_version => '5.44' >> or set C<perl_versions> explicitly:
generate_custom_perl_workflow({ max_perl_version => '5.44' });
# matrix: 5.36, 5.38, 5.40, 5.42, 5.44
generate_custom_perl_workflow({ perl_versions => ['5.42', '5.44'] });
# matrix: 5.42, 5.44
=item C<os> (array ref)
Operating systems for the build matrix.
Default: C<['ubuntu-latest', 'macos-latest', 'windows-latest']>.
=item C<timeout> (integer, default C<30>)
Value written to C<timeout-minutes> on every matrix job.
=item C<enable_linter> (boolean, default C<1>)
When true, inserts a B<"Lint and syntax check"> step after all dependency
installation steps and before the test run. The step runs on B<every>
matrix cell (all OS and Perl version combinations) so that compile-time
errors are caught across the full version range being tested.
The step uses C<shell: perl {0}>, which works identically on Linux, macOS,
and Windows without any OS-specific branching. It searches C<lib/> and
C<bin/> (falling back to C<.> when neither exists), loads each C<.pm> file
via C<do $file> with a stub C<@INC> handler that silently satisfies any
missing C<use> statements, and exits non-zero if any file fails. Using
C<do $file> avoids spawning C<perl -c> subprocesses (which have
Windows command-line quoting issues) and avoids false failures caused by
missing CPAN dependencies. B<No additional CPAN modules are required.>
=item C<enable_linter_unused> (boolean, default C<1>)
When true, appends an unused-variable check to the B<end of the lint step>
(i.e. it runs before the test run, not after). The check installs
L<warnings::unused> from CPAN, then runs
C<PERL5OPT=-Mwarnings::unused prove -lr t/> so that variable lifetimes are
exercised at runtime (C<perl -c> is compile-only and cannot detect unused
variables). It is gated on C<RUNNER_OS == Linux> and marked
C<continue-on-error: true> because unused-variable warnings can be
legitimately noisy on some codebases.
=item C<enable_critic> (boolean, default C<1>)
When true, adds a Perl::Critic step on the latest matrix Perl version and
C<ubuntu-latest>.
=item C<enable_coverage> (boolean, default C<1>)
When true, adds a Devel::Cover test-coverage step on the latest matrix Perl
version and C<ubuntu-latest>.
=item C<enable_perlimports> (boolean, default C<1>)
When true, adds a B<"Check imports with perlimports"> step on the latest matrix
Perl version and C<ubuntu-latest>. The step installs L<App::perlimports> from
CPAN and runs C<perlimports --lint> across all C<.pm> files under C<lib/>. It
is marked C<continue-on-error: true> because import hygiene warnings are
advisory; they should not block a CI run in established codebases.
=back
B<Generated step order:>
=over 4
=item 1. C<actions/checkout>
=item 2. Setup Perl via C<shogo82148/actions-setup-perl>
=item 3. Cache CPAN modules via C<actions/cache>
=item 4. Install cpanm and C<local::lib>
=item 5. Install project dependencies
=item 6. B<Lint and syntax check> â all matrix cells (when C<enable_linter> is true); the unused-variable check via C<PERL5OPT=-Mwarnings::unused> is embedded at the end of this step when C<enable_linter_unused> is true
=item 7. Run tests
=item 8. Run Perl::Critic â latest Perl + Ubuntu only (when C<enable_critic> is true)
=item 9. B<Check imports with perlimports> â latest Perl + Ubuntu only (when C<enable_perlimports> is true)
=item 10. Test coverage â latest Perl + Ubuntu only (when C<enable_coverage> is true)
=item 11. Show cpanm build log on failure
=back
=head3 API SPECIFICATION
=head4 Input
{
opts => {
type => 'hashref',
default => {},
keys => {
perl_versions => { type => 'arrayref', optional => 1 },
min_perl_version => { type => 'scalar', default => '5.36' },
max_perl_version => { type => 'scalar', default => '5.42' },
os => { type => 'arrayref', optional => 1 },
timeout => { type => 'scalar', default => 30 },
enable_linter => { type => 'scalar', default => 1 },
enable_linter_unused => { type => 'scalar', default => 1 },
enable_critic => { type => 'scalar', default => 1 },
enable_coverage => { type => 'scalar', default => 1 },
enable_perlimports => { type => 'scalar', default => 1 },
},
},
}
=head4 Output
{ type => 'scalar' } # multi-line YAML string starting with '---'
=head3 FORMAL SPECIFICATION
generate_custom_perl_workflow : Opts â YAML
versions â opts.perl_versions ?? range(min, max)
latest â versions[|versions|â1]
yaml contains "Lint and syntax check" step â opts.enable_linter = 1
yaml contains "PERL5OPT=-Mwarnings::unused" â opts.enable_linter_unused = 1
yaml contains "Run Perl::Critic" â opts.enable_critic = 1
yaml contains "Test coverage" â opts.enable_coverage = 1
yaml contains "Check imports with perlimports" â opts.enable_perlimports = 1
step ordering invariant:
pos(lint) < pos(unused) < pos(tests) < pos(critic) < pos(perlimports) < pos(coverage)
=cut | |||||
| 301 | ||||||
| 302 | 39 39 39 | 41879 29 21 | sub generate_custom_perl_workflow($opts = {}) { | |||
| 303 | 39 | 63 | my $min_version = $opts->{min_perl_version} // '5.36'; | |||
| 304 | 39 | 47 | my $max_version = $opts->{max_perl_version} // '5.42'; | |||
| 305 | 39 | 53 | my $timeout = $opts->{timeout} // 30; | |||
| 306 | 39 39 | 25 75 | my @os = @{$opts->{os} // ['ubuntu-latest', 'macos-latest', 'windows-latest']}; | |||
| 307 | 39 | 54 | my $enable_linter = $opts->{enable_linter} // 1; | |||
| 308 | 39 | 47 | my $enable_linter_unused = $opts->{enable_linter_unused} // 1; | |||
| 309 | 39 | 38 | my $enable_critic = $opts->{enable_critic} // 1; | |||
| 310 | 39 | 39 | my $enable_coverage = $opts->{enable_coverage} // 1; | |||
| 311 | 39 | 39 | my $enable_perlimports = $opts->{enable_perlimports} // 1; | |||
| 312 | ||||||
| 313 | # Generate Perl version list - use explicit list if provided, otherwise min/max | |||||
| 314 | 39 | 26 | my @perl_versions; | |||
| 315 | 39 2 | 52 4 | if ($opts->{perl_versions} && @{$opts->{perl_versions}}) { | |||
| 316 | 2 2 | 2 2 | @perl_versions = @{$opts->{perl_versions}}; | |||
| 317 | } else { | |||||
| 318 | 37 | 38 | @perl_versions = _get_perl_versions($min_version, $max_version); | |||
| 319 | } | |||||
| 320 | ||||||
| 321 | 39 | 29 | my $yaml = "---\n"; | |||
| 322 | 39 | 29 | $yaml .= '# Created by ' . __PACKAGE__ . "\n"; | |||
| 323 | ||||||
| 324 | 39 | 21 | $yaml .= "name: Perl CI\n\n"; | |||
| 325 | 39 | 20 | $yaml .= "'on':\n"; | |||
| 326 | 39 | 23 | $yaml .= " push:\n"; | |||
| 327 | 39 | 23 | $yaml .= " branches:\n"; | |||
| 328 | 39 | 16 | $yaml .= " - main\n"; | |||
| 329 | 39 | 22 | $yaml .= " - master\n"; | |||
| 330 | 39 | 22 | $yaml .= " pull_request:\n"; | |||
| 331 | 39 | 17 | $yaml .= " branches:\n"; | |||
| 332 | 39 | 21 | $yaml .= " - main\n"; | |||
| 333 | 39 | 22 | $yaml .= " - master\n\n"; | |||
| 334 | ||||||
| 335 | 39 | 19 | $yaml .= "concurrency:\n"; | |||
| 336 | 39 | 21 | $yaml .= " group: \${{ github.workflow }}-\${{ github.ref }}\n"; | |||
| 337 | 39 | 20 | $yaml .= " cancel-in-progress: true\n\n"; | |||
| 338 | ||||||
| 339 | 39 | 20 | $yaml .= "permissions:\n"; | |||
| 340 | 39 | 19 | $yaml .= " contents: read\n\n"; | |||
| 341 | ||||||
| 342 | 39 | 26 | $yaml .= "jobs:\n"; | |||
| 343 | 39 | 18 | $yaml .= " test:\n"; | |||
| 344 | 39 | 23 | $yaml .= " runs-on: \${{ matrix.os }}\n"; | |||
| 345 | 39 | 27 | $yaml .= " timeout-minutes: $timeout\n"; | |||
| 346 | 39 | 24 | $yaml .= " strategy:\n"; | |||
| 347 | 39 | 20 | $yaml .= " fail-fast: false\n"; | |||
| 348 | 39 | 26 | $yaml .= " matrix:\n"; | |||
| 349 | 39 | 19 | $yaml .= " os:\n"; | |||
| 350 | 39 | 25 | for my $os (@os) { | |||
| 351 | 113 | 67 | $yaml .= " - $os\n"; | |||
| 352 | } | |||||
| 353 | 39 | 18 | $yaml .= " perl:\n"; | |||
| 354 | 39 | 26 | for my $version (@perl_versions) { | |||
| 355 | 146 | 76 | $yaml .= " - '$version'\n"; | |||
| 356 | } | |||||
| 357 | # shogo82148/build-perl perl-5.40.4-thr-win32-x64.zip ships a perl.exe and a | |||||
| 358 | # libperl540.a from two different builds. Any XS module compiled against that | |||||
| 359 | # libperl540.a (key 0x12c00080) mismatches the running perl.exe (needs | |||||
| 360 | # 0x12d00080). Even Win32::Process (a core Windows XS module bundled in the | |||||
| 361 | # zip) fails to load, so no XS-using test can pass on this combination. | |||||
| 362 | # Exclude until the upstream distribution is fixed. | |||||
| 363 | 39 146 107 | 31 116 116 | if ((grep { $_ eq '5.40' } @perl_versions) && (grep { $_ eq 'windows-latest' } @os)) { | |||
| 364 | 35 | 20 | $yaml .= " exclude:\n"; | |||
| 365 | 35 | 19 | $yaml .= " - os: windows-latest\n"; | |||
| 366 | 35 | 22 | $yaml .= " perl: '5.40'\n"; | |||
| 367 | } | |||||
| 368 | 39 | 16 | $yaml .= " name: Perl \${{ matrix.perl }} on \${{ matrix.os }}\n"; | |||
| 369 | 39 | 21 | $yaml .= " env:\n"; | |||
| 370 | 39 | 33 | $yaml .= " AUTOMATED_TESTING: 1\n"; | |||
| 371 | 39 | 22 | $yaml .= " NO_NETWORK_TESTING: 1\n"; | |||
| 372 | 39 | 18 | $yaml .= " NONINTERACTIVE_TESTING: 1\n"; | |||
| 373 | 39 | 23 | $yaml .= " steps:\n"; | |||
| 374 | 39 | 19 | $yaml .= " - uses: actions/checkout\@v7\n\n"; | |||
| 375 | ||||||
| 376 | 39 | 18 | $yaml .= " - name: Setup Perl\n"; | |||
| 377 | 39 | 19 | $yaml .= " id: setup-perl\n"; | |||
| 378 | 39 | 21 | $yaml .= " uses: shogo82148/actions-setup-perl\@v1.43.1\n"; | |||
| 379 | 39 | 20 | $yaml .= " with:\n"; | |||
| 380 | 39 | 17 | $yaml .= " perl-version: \${{ matrix.perl }}\n\n"; | |||
| 381 | ||||||
| 382 | # Hash the actual Perl binary files so the cache key changes whenever the | |||||
| 383 | # binary changes, even across silent Strawberry Perl re-releases that keep the | |||||
| 384 | # same $Config{version} string but change the DLL ABI and handshake key. | |||||
| 385 | # On Windows we hash both perl.exe and the Perl runtime DLL (e.g. perl540.dll) | |||||
| 386 | # because perl.exe is a thin stub â only the DLL contains the real runtime. | |||||
| 387 | # shell: perl {0} runs identically on Linux, macOS, and Windows. | |||||
| 388 | 39 | 21 | $yaml .= " - name: Get exact Perl binary version for cache key\n"; | |||
| 389 | 39 | 23 | $yaml .= " id: perl-version\n"; | |||
| 390 | 39 | 15 | $yaml .= " shell: perl {0}\n"; | |||
| 391 | 39 | 23 | $yaml .= " run: |\n"; | |||
| 392 | 39 | 19 | $yaml .= <<'VERSION_STEP'; | |||
| 393 | use Config; | |||||
| 394 | my @paths = ($^X); | |||||
| 395 | if ($^O eq 'MSWin32' && $Config{libperl}) { | |||||
| 396 | (my $dir = $^X) =~ s{[\\/][^\\/]+$}{}; | |||||
| 397 | my $dll = "$dir/$Config{libperl}"; | |||||
| 398 | push @paths, $dll if -f $dll; | |||||
| 399 | } | |||||
| 400 | my $sum = 0; | |||||
| 401 | for my $path (@paths) { | |||||
| 402 | open(my $fh, '<:raw', $path) or next; | |||||
| 403 | $sum += unpack('%32C*', $_) while read $fh, $_, 65536; | |||||
| 404 | } | |||||
| 405 | open(my $out, '>>', $ENV{GITHUB_OUTPUT}) or die $!; | |||||
| 406 | print $out "version=$Config{version}-$Config{archname}-$sum\n"; | |||||
| 407 | ||||||
| 408 | VERSION_STEP | |||||
| 409 | ||||||
| 410 | 39 | 23 | $yaml .= " - name: Cache CPAN modules\n"; | |||
| 411 | 39 | 17 | $yaml .= " uses: actions/cache\@v6\n"; | |||
| 412 | 39 | 16 | $yaml .= " with:\n"; | |||
| 413 | 39 | 23 | $yaml .= " path: ~/perl5\n"; | |||
| 414 | 39 | 19 | $yaml .= " key: \${{ runner.os }}-perl-\${{ steps.perl-version.outputs.version }}-\${{ hashFiles('cpanfile') }}\n"; | |||
| 415 | 39 | 19 | $yaml .= " restore-keys: |\n"; | |||
| 416 | 39 | 19 | $yaml .= " \${{ runner.os }}-perl-\${{ steps.perl-version.outputs.version }}-\n\n"; | |||
| 417 | ||||||
| 418 | 39 | 16 | $yaml .= " - name: Install cpanm and local::lib\n"; | |||
| 419 | 39 | 20 | $yaml .= " if: runner.os != 'Windows'\n"; | |||
| 420 | 39 | 20 | $yaml .= " run: cpanm --notest --local-lib=~/perl5 local::lib\n\n"; | |||
| 421 | ||||||
| 422 | 39 | 22 | $yaml .= " - name: Install cpanm and local::lib (Windows)\n"; | |||
| 423 | 39 | 15 | $yaml .= " if: runner.os == 'Windows'\n"; | |||
| 424 | 39 | 18 | $yaml .= " run: cpanm --notest App::cpanminus local::lib\n\n"; | |||
| 425 | ||||||
| 426 | # TODO: should be configurable | |||||
| 427 | 39 | 22 | $yaml .= " - name: Install DB_File, needed for a lot of CPAN modules\n"; | |||
| 428 | 39 | 16 | $yaml .= " if: runner.os == 'Linux'\n"; | |||
| 429 | 39 | 22 | $yaml .= " run: |\n"; | |||
| 430 | 39 | 15 | $yaml .= " sudo apt update\n"; | |||
| 431 | 39 | 25 | $yaml .= " sudo apt install libdb-dev libperl-dev\n"; | |||
| 432 | ||||||
| 433 | 39 | 19 | $yaml .= " - name: Install dependencies\n"; | |||
| 434 | 39 | 18 | $yaml .= " if: runner.os != 'Windows'\n"; | |||
| 435 | 39 | 21 | $yaml .= " shell: bash\n"; | |||
| 436 | 39 | 13 | $yaml .= " run: |\n"; | |||
| 437 | 39 | 27 | $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; | |||
| 438 | # Add the --verbose to stop the no activity timeout in GitHub actions whene there are a lot of prequisites | |||||
| 439 | # TODO: should be configurable | |||||
| 440 | 39 | 18 | $yaml .= " cpanm --notest --installdeps --verbose .\n\n"; | |||
| 441 | ||||||
| 442 | 39 | 21 | $yaml .= " - name: Install dependencies (Windows)\n"; | |||
| 443 | 39 | 16 | $yaml .= " if: runner.os == 'Windows'\n"; | |||
| 444 | 39 | 19 | $yaml .= " shell: cmd\n"; | |||
| 445 | 39 | 23 | $yaml .= " run: |\n"; | |||
| 446 | 39 | 16 | $yaml .= " \@echo off\n"; | |||
| 447 | 39 | 21 | $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n"; | |||
| 448 | 39 | 16 | $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n"; | |||
| 449 | 39 | 20 | $yaml .= " cpanm --notest --installdeps --verbose .\n\n"; | |||
| 450 | ||||||
| 451 | # The shogo82148 Perl distributions bundle pre-compiled XS modules (e.g. | |||||
| 452 | # YAML::XS) in the Perl zip. cpanm sees them as "already installed" and | |||||
| 453 | # skips recompilation, leaving a DLL that may have the wrong handshake key. | |||||
| 454 | # Force-reinstall YAML::XS into site/lib (which precedes the bundled lib in | |||||
| 455 | # @INC) to ensure a freshly compiled copy is always used. | |||||
| 456 | # NOTE: this step cannot rescue a fundamentally broken distribution where | |||||
| 457 | # libperl540.a and perl.exe themselves are from different builds (as in the | |||||
| 458 | # shogo82148 perl-5.40.4-thr-win32-x64.zip build) â that case is handled | |||||
| 459 | # by the matrix exclusion above. | |||||
| 460 | 39 | 21 | $yaml .= " - name: Reinstall YAML::XS against current Perl (Windows)\n"; | |||
| 461 | 39 | 12 | $yaml .= " if: runner.os == 'Windows'\n"; | |||
| 462 | 39 | 23 | $yaml .= " shell: cmd\n"; | |||
| 463 | 39 | 13 | $yaml .= " run: cpanm --notest --reinstall YAML::XS\n\n"; | |||
| 464 | ||||||
| 465 | 39 | 36 | if ($enable_linter) { | |||
| 466 | 34 | 10 | $yaml .= " - name: Lint and syntax check\n"; | |||
| 467 | 34 | 21 | $yaml .= " shell: perl {0}\n"; | |||
| 468 | 34 | 13 | $yaml .= " run: |\n"; | |||
| 469 | 34 | 30 | $yaml .= <<'LINT_STEP'; | |||
| 470 | use strict; | |||||
| 471 | use warnings; | |||||
| 472 | use File::Find; | |||||
| 473 | use lib 'lib'; | |||||
| 474 | my @failed; | |||||
| 475 | push @INC, sub { open my $h, '<', \qq{1;\n}; $h }; | |||||
| 476 | my @dirs = grep { -d } qw(lib bin); | |||||
| 477 | @dirs = ('.') unless @dirs; | |||||
| 478 | find({ wanted => sub { | |||||
| 479 | return unless -f && /\.pm$/; | |||||
| 480 | my $file = $File::Find::name; | |||||
| 481 | do $file; | |||||
| 482 | if ($@) { | |||||
| 483 | warn "Syntax check failed: $file\n"; | |||||
| 484 | push @failed, $file; | |||||
| 485 | } | |||||
| 486 | }, no_chdir => 1 }, @dirs); | |||||
| 487 | ||||||
| 488 | LINT_STEP | |||||
| 489 | ||||||
| 490 | 34 | 23 | if ($enable_linter_unused) { | |||
| 491 | 31 | 26 | $yaml .= <<'UNUSED_CODE'; | |||
| 492 | if (($ENV{RUNNER_OS} // '') eq 'Linux') { | |||||
| 493 | system('cpanm --notest --quiet warnings::unused 2>/dev/null'); | |||||
| 494 | system('PERL5OPT=-Mwarnings::unused prove -lr t/ 2>&1 | grep -i unused && true || echo "warnings::unused: no unused variables detected"'); | |||||
| 495 | } | |||||
| 496 | ||||||
| 497 | UNUSED_CODE | |||||
| 498 | } | |||||
| 499 | ||||||
| 500 | 34 | 17 | $yaml .= " exit(\@failed ? 1 : 0);\n\n"; | |||
| 501 | } | |||||
| 502 | ||||||
| 503 | 39 | 21 | $yaml .= " - name: Run tests\n"; | |||
| 504 | 39 | 22 | $yaml .= " if: runner.os != 'Windows'\n"; | |||
| 505 | 39 | 15 | $yaml .= " shell: bash\n"; | |||
| 506 | 39 | 19 | $yaml .= " run: |\n"; | |||
| 507 | 39 | 17 | $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; | |||
| 508 | 39 | 20 | $yaml .= " prove -lr t/\n\n"; | |||
| 509 | ||||||
| 510 | 39 | 16 | $yaml .= " - name: Run tests (Windows)\n"; | |||
| 511 | 39 | 18 | $yaml .= " if: runner.os == 'Windows'\n"; | |||
| 512 | 39 | 18 | $yaml .= " shell: cmd\n"; | |||
| 513 | 39 | 15 | $yaml .= " run: |\n"; | |||
| 514 | 39 | 20 | $yaml .= " \@echo off\n"; | |||
| 515 | 39 | 16 | $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n"; | |||
| 516 | 39 | 32 | $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n"; | |||
| 517 | 39 | 19 | $yaml .= " prove -lr t/\n\n"; | |||
| 518 | ||||||
| 519 | 39 | 30 | if ($enable_critic) { | |||
| 520 | 32 | 22 | my $latest = $perl_versions[-1]; | |||
| 521 | 32 | 16 | $yaml .= " - name: Run Perl::Critic\n"; | |||
| 522 | 32 | 16 | $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; | |||
| 523 | 32 | 17 | $yaml .= " continue-on-error: true\n"; | |||
| 524 | 32 | 14 | $yaml .= " run: |\n"; | |||
| 525 | 32 | 19 | $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; | |||
| 526 | 32 | 31 | $yaml .= " cpanm --notest Perl::Critic\n"; | |||
| 527 | 32 | 12 | $yaml .= " perlcritic --severity 3 lib/ || true\n"; | |||
| 528 | 32 | 26 | $yaml .= " shell: bash\n\n"; | |||
| 529 | } | |||||
| 530 | ||||||
| 531 | 39 | 26 | if ($enable_perlimports) { | |||
| 532 | 35 | 21 | my $latest = $perl_versions[-1]; | |||
| 533 | 35 | 13 | $yaml .= " - name: Check imports with perlimports\n"; | |||
| 534 | 35 | 25 | $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; | |||
| 535 | 35 | 11 | $yaml .= " continue-on-error: true\n"; | |||
| 536 | 35 | 23 | $yaml .= " run: |\n"; | |||
| 537 | 35 | 14 | $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; | |||
| 538 | 35 | 19 | $yaml .= " cpanm --notest App::perlimports\n"; | |||
| 539 | 35 | 16 | $yaml .= " find lib -name '*.pm' | xargs perlimports --lint\n"; | |||
| 540 | 35 | 22 | $yaml .= " shell: bash\n\n"; | |||
| 541 | } | |||||
| 542 | ||||||
| 543 | 39 | 28 | if ($enable_coverage) { | |||
| 544 | 32 | 21 | my $latest = $perl_versions[-1]; | |||
| 545 | 32 | 13 | $yaml .= " - name: Test coverage\n"; | |||
| 546 | 32 | 18 | $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; | |||
| 547 | 32 | 16 | $yaml .= " run: |\n"; | |||
| 548 | 32 | 19 | $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; | |||
| 549 | 32 | 13 | $yaml .= " cpanm --notest Devel::Cover\n"; | |||
| 550 | 32 | 23 | $yaml .= " cover -delete\n"; | |||
| 551 | 32 | 18 | $yaml .= " HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t/\n"; | |||
| 552 | 32 | 11 | $yaml .= " cover\n"; | |||
| 553 | 32 | 26 | $yaml .= " shell: bash\n"; | |||
| 554 | } | |||||
| 555 | ||||||
| 556 | 39 | 16 | $yaml .= <<'YAML'; | |||
| 557 | ||||||
| 558 | - name: Show cpanm build log on failure (Windows) | |||||
| 559 | if: runner.os == 'Windows' && failure() | |||||
| 560 | shell: pwsh | |||||
| 561 | run: Get-Content "$env:USERPROFILE\.cpanm\work\*\build.log" -Tail 100 | |||||
| 562 | ||||||
| 563 | - name: Show cpanm build log on failure (non-Windows) | |||||
| 564 | if: runner.os != 'Windows' && failure() | |||||
| 565 | run: tail -100 "$HOME/.cpanm/work/*/build.log" | |||||
| 566 | YAML | |||||
| 567 | ||||||
| 568 | 39 | 142 | return $yaml; | |||
| 569 | } | |||||
| 570 | ||||||
| 571 | 41 41 41 41 | 5476 23 24 21 | sub _get_perl_versions($min, $max) { | |||
| 572 | # All available Perl versions in descending order. | |||||
| 573 | # 5.44 is listed so projects can opt in via max_perl_version => '5.44', | |||||
| 574 | # but it is not included in the default matrix (max defaults to 5.42). | |||||
| 575 | 41 | 82 | my @all_versions = qw(5.44 5.42 5.40 5.38 5.36 5.34 5.32 5.30 5.28 5.26 5.24 5.22); | |||
| 576 | ||||||
| 577 | # Normalize version strings for comparison | |||||
| 578 | 41 | 34 | my $min_normalized = _normalize_version($min); | |||
| 579 | 41 | 35 | my $max_normalized = _normalize_version($max); | |||
| 580 | ||||||
| 581 | 41 | 23 | my @selected; | |||
| 582 | 41 | 36 | for my $version (@all_versions) { | |||
| 583 | 492 | 297 | my $v_normalized = _normalize_version($version); | |||
| 584 | 492 | 600 | if ($v_normalized >= $min_normalized && $v_normalized <= $max_normalized) { | |||
| 585 | 149 | 100 | push @selected, $version; | |||
| 586 | } | |||||
| 587 | } | |||||
| 588 | ||||||
| 589 | 41 | 72 | return reverse @selected; # Return in ascending order | |||
| 590 | } | |||||
| 591 | ||||||
| 592 | 580 580 580 | 4390 263 271 | sub _normalize_version($version) { | |||
| 593 | # Convert "5.036" or "5.36" to comparable number | |||||
| 594 | 580 | 422 | $version =~ s/^v?//; | |||
| 595 | 580 | 365 | my @parts = split /\./, $version; | |||
| 596 | 580 | 777 | return sprintf('%d.%03d', $parts[0] // 5, $parts[1] // 0); | |||
| 597 | } | |||||
| 598 | ||||||
| 599 - 638 | =head1 LIMITATIONS
=head2 Windows + Perl 5.40 excluded from the generated matrix
The shogo82148 C<perl-5.40.4-thr-win32-x64.zip> distribution ships a
C<perl.exe> and a C<libperl540.a> from two different builds. Any XS module
compiled against that C<libperl540.a> receives handshake key
C<0x12c00080>, but C<perl.exe> needs C<0x12d00080>. Even
C<Win32::Process> â a core Windows XS module bundled in the zip and loaded
transitively by C<IPC::System::Simple> â fails to load with this mismatch,
so no XS-using test can pass on this combination.
Recompiling XS modules (e.g. via C<cpanm --reinstall YAML::XS>) does not
help: the compilation itself links against the broken C<libperl540.a> and
inherits the wrong key.
The generated workflow therefore includes a C<matrix.exclude> entry for
C<{os: windows-latest, perl: '5.40'}>. This entry is only emitted when
both C<windows-latest> and C<5.40> are present in the matrix, so workflows
that restrict their OS or Perl lists are unaffected.
Remove the exclusion once the upstream shogo82148/build-perl distribution
is fixed and the C<perl.exe> and C<libperl540.a> in the 5.40.x zip are
built from the same source tree.
See L<https://github.com/shogo82148/actions-setup-perl/issues/2310> for further information.
=head1 AUTHOR
Nigel Horne E<lt>njh@nigelhorne.comE<gt>
L<https://github.com/nigelhorne>
=head1 LICENSE
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 639 | ||||||
| 640 | 1; | |||||