File Coverage

File:blib/lib/App/GHGen/PerlCustomizer.pm
Coverage:97.8%

linestmtbrancondsubtimecode
1package 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';
10our @EXPORT_OK = qw(
11        detect_perl_requirements
12        generate_custom_perl_workflow
13);
14
15our $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
408VERSION_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
488LINT_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
497UNUSED_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"
566YAML
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
6401;