Skip to content

Instantly share code, notes, and snippets.

@x-yuri
Created October 19, 2023 03:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save x-yuri/f87c509b9437e39fe2d4e90b38cb38bd to your computer and use it in GitHub Desktop.
Save x-yuri/f87c509b9437e39fe2d4e90b38cb38bd to your computer and use it in GitHub Desktop.
perl: dependencies

perl: dependencies

use strict;
use warnings;
use List::Util qw/none uniq/;
use Module::CoreList;
use Data::Dumper;

use Getopt::Long;
use Pod::Usage;

use CHI;
use WWW::Mechanize::Cached;
use HTTP::Tiny::Mech;
use MetaCPAN::Client;

$Data::Dumper::Indent = 1;

my %opts;
GetOptions(\%opts,
    'recursive|r',
    'runtime', 'requires', 'recommends', 'suggests',
    'non-core', 'no-develop',
    'help') or pod2usage;
pod2usage(1) if $opts{'help'};
pod2usage "$0: command is missing" if @ARGV == 0;
my $cmd = $ARGV[0];
pod2usage "$0: unknown command ($cmd)"
    if none {$cmd eq $_} 'tree', 'list', 'ignored', 'show', 'prereqs', 'mod2dist';
pod2usage "$0: distribution is missing" if @ARGV == 1 && $cmd ne 'mod2dist';
my $dist = $ARGV[1];
pod2usage "$0: too many arguments" if @ARGV > 2;

my $mcpan = MetaCPAN::Client->new(
  ua => HTTP::Tiny::Mech->new(
    mechua => WWW::Mechanize::Cached->new(
      cache => CHI->new(
        driver   => 'File',
        root_dir => 'metacpan-cache',
      ),
    ),
  ),
);

if ($cmd eq 'show') {
    my $dist = $mcpan->release($dist)->data;
    print Dumper $dist;
    exit;
} elsif ($cmd eq 'prereqs') {
    my $prereqs = $mcpan->release($dist)->data->{'metadata'}{'prereqs'};
    delete $prereqs->{'develop'} if $opts{'no-develop'};
    $prereqs = $prereqs->{'runtime'} if $opts{'runtime'};
    sub process_prereqs {
        my $prereqs = shift;
        return $prereqs unless $opts{'non-core'};
        my %r;
        for my $k (keys %$prereqs) {
            if (ref $prereqs->{$k} eq 'HASH') {
                my $r = process_prereqs($prereqs->{$k});
                $r{$k} = $r if %$r;
            } else {
                next if Module::CoreList::is_core($k);
                $r{$k} = $prereqs->{$k};
            }
        }
        return \%r;
    }
    if ($opts{'requires'}) {
        $prereqs = $prereqs->{'requires'};
    } elsif ($opts{'recommends'}) {
        $prereqs = $prereqs->{'recommends'};
    } elsif ($opts{'suggests'}) {
        $prereqs = $prereqs->{'suggests'};
    }
    $prereqs = process_prereqs $prereqs;
    print Dumper $prereqs;
    exit;
} elsif ($cmd eq 'mod2dist') {
    if (defined $dist) {
        print $mcpan->module($dist)->distribution;
    } else {
        while (<STDIN>) {
            chomp;
            print $mcpan->module($_)->distribution, "\n";
        }
    }
    exit;
}

my %seen;
my @mod_deps;
my @ignored;
sub process_release {
    my ($release, $l, $module) = @_;
    $l ||= 1;
    return if $release eq 'perl';
    if ($cmd eq 'tree') {
        my $indent = ' ' x (($l - 1) * 2);
        my $prefix = $seen{$release} ? '// ' : '';
        my $postfix = defined($module) ? ' (' . $module . ')' : '';
        print $indent, $prefix, $release, $postfix, "\n";
    }
    return if $seen{$release};
    $seen{$release} = 1;
    if ($cmd eq 'list') {
        push @mod_deps, $module if defined($module);
    }
    return if $l > 1 && !$opts{'recursive'};
    $release = $mcpan->release($release);
    my @requires = grep { $_ ne 'perl' }
        keys %{$release->metadata->{'prereqs'}{'runtime'}{'requires'}};
    foreach my $r (@requires) {
        if (Module::CoreList::is_core($r)) {
            push @ignored, $r if $cmd eq 'ignored';
            next;
        }
        process_release($mcpan->module($r)->distribution, $l + 1, $r);
    }
}
process_release $dist;

if ($cmd eq 'list') {
    @mod_deps = sort @mod_deps;
    print join("\n", @mod_deps), "\n";
} elsif ($cmd eq 'ignored') {
    @ignored = uniq sort @ignored;
    print join("\n", @ignored), "\n";
}

__END__

=head1 SYNOPSIS

deps.pl [OPTIONS] tree|list|ignored|show|prereqs DISTRIBUTION

 Options:
   --recursive   (tree, list, ignored)
   --runtime     (prereqs)
   --requires    (prereqs)
   --recommends  (prereqs)
   --suggests    (prereqs)
   --non-core    (prereqs)
   --no-develop  (prereqs)
   --help

=cut

How to determine which dependencies can be preinstalled on Alpine Linux? Let's take Carton for example. It's dependency tree is:

$ perl deps.pl tree -r Carton
Carton
  Class-Tiny (Class::Tiny)
  Module-CPANfile (Module::CPANfile)
  Menlo-Legacy (Menlo::CLI::Compat)
    Menlo (Menlo)
      File-Which (File::Which)
      CPAN-DistnameInfo (CPAN::DistnameInfo)
      String-ShellQuote (String::ShellQuote)
      ExtUtils-Helpers (ExtUtils::Helpers)
      CPAN-Meta-Check (CPAN::Meta::Check)
      Parse-PMFile (Parse::PMFile)
      Win32-ShellQuote (Win32::ShellQuote)
      URI (URI)
      Capture-Tiny (Capture::Tiny)
      CPAN-Common-Index (CPAN::Common::Index)
        // URI (URI)
        // Class-Tiny (Class::Tiny)
        Tie-Handle-Offset (Tie::Handle::SkipHeader)
        // CPAN-DistnameInfo (CPAN::DistnameInfo)
      HTTP-Tinyish (HTTP::Tinyish)
        // File-Which (File::Which)
        IPC-Run3 (IPC::Run3)
      // Module-CPANfile (Module::CPANfile)
      local-lib (local::lib)
        Module-Build (Module::Build)
      // Class-Tiny (Class::Tiny)
      ExtUtils-InstallPaths (ExtUtils::InstallPaths)
        ExtUtils-Config (ExtUtils::Config)
      File-pushd (File::pushd)
      // ExtUtils-Config (ExtUtils::Config)
  Try-Tiny (Try::Tiny)
  Path-Tiny (Path::Tiny)

This shows only the runtime dependencies. There are also configure, build and test dependencies.

Now, there's no perl-carton package, as such we need to know its other dependencies:

$ perl deps.pl --non-core --no-develop prereqs Carton
$VAR1 = {
  'runtime' => {
    'recommends' => {
      'App::FatPacker' => '0.009018',
      'File::pushd' => '0'
    },
    'requires' => {
      'Try::Tiny' => '0.09',
      'Module::CPANfile' => '0.9031',
      'Class::Tiny' => '1.001',
      'Path::Tiny' => '0.033',
      'Menlo::CLI::Compat' => '1.9018',
      'perl' => 'v5.8.5'
    }
  }
};

Convert it into distribution names:

$ echo 'App::FatPacker
File::pushd
Try::Tiny
Module::CPANfile
Class::Tiny
Path::Tiny
Menlo::CLI::Compat' | perl deps.pl mod2dist
App-FatPacker
File-pushd
Try-Tiny
Module-CPANfile
Class-Tiny
Path-Tiny
Menlo-Legacy

There are the following Alpine packages:

  • perl-file-pushd
  • perl-try-tiny
  • perl-class-tiny
  • perl-path-tiny

This leaves:

  • App-FatPacker
  • Module-CPANfile
  • Menlo-Legacy

Repeat the procedure for them:

$ perl deps.pl --non-core --no-develop prereqs App-FatPacker
$VAR1 = {
  'runtime' => {
    'requires' => {
      'perl' => '5.008000'
    }
  }
};
$ perl deps.pl --non-core --no-develop prereqs Module-CPANfile
$VAR1 = {
  'test' => {
    'requires' => {
      'File::pushd' => '0'
    }
  }
};

$ perl deps.pl mod2dist File::pushd
File-pushd

Existing packages: perl-file-pushd.

$ perl deps.pl --non-core --no-develop prereqs Menlo-Legacy
$VAR1 = {
  'runtime' => {
    'requires' => {
      'perl' => '5.008001',
      'Menlo' => '1.9018'
    }
  }
};

$ perl deps.pl mod2dist Menlo
Menlo

Leftover dependencies: Menlo.

$ perl deps.pl --non-core --no-develop prereqs Menlo
$VAR1 = {
  'runtime' => {
    'suggests' => {
      'Archive::Zip' => '0',
      'File::HomeDir' => '0',
      'Module::Signature' => '0',
      'LWP::UserAgent' => '5.802'
    },
    'requires' => {
      'perl' => '5.008001',
      'local::lib' => '0',
      'Win32::ShellQuote' => '0',
      'Class::Tiny' => '1.001',
      'CPAN::Common::Index' => '0.006',
      'URI' => '0',
      'Capture::Tiny' => '0',
      'ExtUtils::Config' => '0.003',
      'File::Which' => '0',
      'File::pushd' => '0',
      'HTTP::Tinyish' => '0.04',
      'ExtUtils::InstallPaths' => '0.002',
      'ExtUtils::Helpers' => '0.020',
      'CPAN::Meta::Check' => '0',
      'Parse::PMFile' => '0.26',
      'String::ShellQuote' => '0',
      'CPAN::DistnameInfo' => '0',
      'Module::CPANfile' => '0'
    }
  }
};

Ignore suggests since cpan doesn't install them by default.

$ echo 'local::lib
Win32::ShellQuote
Class::Tiny
CPAN::Common::Index
URI
Capture::Tiny
ExtUtils::Config
File::Which
File::pushd
HTTP::Tinyish
ExtUtils::InstallPaths
ExtUtils::Helpers
CPAN::Meta::Check
Parse::PMFile
String::ShellQuote
CPAN::DistnameInfo
Module::CPANfile' | perl deps.pl mod2dist
local-lib
Win32-ShellQuote
Class-Tiny
CPAN-Common-Index
URI
Capture-Tiny
ExtUtils-Config
File-Which
File-pushd
HTTP-Tinyish
ExtUtils-InstallPaths
ExtUtils-Helpers
CPAN-Meta-Check
Parse-PMFile
String-ShellQuote
CPAN-DistnameInfo
Module-CPANfile

Existing packages:

  • perl-class-tiny
  • perl-uri
  • perl-capture-tiny
  • perl-extutils-config
  • perl-file-which
  • perl-file-pushd
  • perl-extutils-installpaths
  • perl-extutils-helpers
  • perl-cpan-meta-check
  • perl-string-shellquote

Leftover dependencies:

  • Module-CPANfile (already examined)
  • Win32-ShellQuote
  • CPAN-DistnameInfo
  • local-lib
  • HTTP-Tinyish
  • CPAN-Common-Index
  • Parse-PMFile
$ perl deps.pl --non-core --no-develop prereqs Win32-ShellQuote
$VAR1 = {
  'runtime' => {
    'requires' => {
      'perl' => '5.006'
    }
  }
};
$ perl deps.pl --non-core --no-develop prereqs CPAN-DistnameInfo
$VAR1 = {};
$ perl deps.pl --non-core --no-develop prereqs local-lib
$VAR1 = {
  'runtime' => {
    'requires' => {
      'Module::Build' => '0.36',
      'perl' => '5.006'
    }
  }
};

$ echo Module::Build | perl deps.pl mod2dist
Module-Build

Existing packages: perl-module-build.

$ perl deps.pl --non-core --no-develop prereqs HTTP-Tinyish
$VAR1 = {
  'runtime' => {
    'requires' => {
      'perl' => '5.008001',
      'IPC::Run3' => '0',
      'File::Which' => '0'
    }
  }
};

$ echo 'IPC::Run3
File::Which' | perl deps.pl mod2dist
IPC-Run3
File-Which

Existing package:

  • perl-ipc-run3
  • perl-file-which
$ perl deps.pl --non-core --no-develop prereqs CPAN-Common-Index
$VAR1 = {
  'runtime' => {
    'requires' => {
      'Tie::Handle::SkipHeader' => '0',
      'perl' => '5.008001',
      'Class::Tiny' => '0',
      'URI' => '0',
      'CPAN::DistnameInfo' => '0'
    }
  },
  'configure' => {
    'requires' => {
      'perl' => '5.008001'
    }
  },
  'test' => {
    'requires' => {
      'perl' => '5.008001',
      'Test::Fatal' => '0',
      'Test::FailWarnings' => '0',
      'Test::Deep' => '0'
    }
  }
};

$ echo 'Tie::Handle::SkipHeader
Class::Tiny
URI
CPAN::DistnameInfo
Test::Fatal
Test::FailWarnings
Test::Deep' | perl deps.pl mod2dist
Tie-Handle-Offset
Class-Tiny
URI
CPAN-DistnameInfo
Test-Fatal
Test-FailWarnings
Test-Deep

Existing packages:

  • perl-class-tiny
  • perl-uri
  • perl-test-fatal
  • perl-test-failwarnings
  • perl-test-deep

Leftover dependencies:

  • CPAN-DistnameInfo (already examined)
  • Tie-Handle-Offset
$ perl deps.pl --non-core --no-develop prereqs Tie-Handle-Offset
$VAR1 = {
  'runtime' => {
    'requires' => {
      'perl' => '5.006'
    }
  },
  'configure' => {
    'requires' => {
      'perl' => '5.006'
    }
  },
  'test' => {
    'requires' => {
      'perl' => '5.006'
    }
  }
};
$ perl deps.pl --non-core --no-develop prereqs Parse-PMFile
$VAR1 = {
  'configure' => {
    'requires' => {
      'ExtUtils::MakeMaker::CPANfile' => '0.09'
    }
  }
};

$ echo ExtUtils::MakeMaker::CPANfile | perl deps.pl mod2dist
ExtUtils-MakeMaker-CPANfile

Leftover dependencies:

  • ExtUtils-MakeMaker-CPANfile
$ perl deps.pl --non-core --no-develop prereqs ExtUtils-MakeMaker-CPANfile
$VAR1 = {
  'runtime' => {
    'requires' => {
      'Module::CPANfile' => '0'
    }
  }
};

$ echo Module::CPANfile | perl deps.pl mod2dist
Module-CPANfile

Leftover dependencies:

  • Module-CPANfile (already examined)

This gives us the following list of Alpine packages that can be preinstalled:

  • perl-capture-tiny
  • perl-class-tiny
  • perl-cpan-meta-check
  • perl-extutils-config
  • perl-extutils-helpers
  • perl-extutils-installpaths
  • perl-file-pushd
  • perl-file-which
  • perl-ipc-run3
  • perl-module-build
  • perl-path-tiny
  • perl-string-shellquote
  • perl-test-deep
  • perl-test-failwarnings
  • perl-test-fatal
  • perl-try-tiny
  • perl-uri

Then cpan has to install only:

  • App-FatPacker
  • Carton
  • CPAN-Common-Index
  • CPAN-DistnameInfo
  • ExtUtils-MakeMaker-CPANfile
  • HTTP-Tinyish
  • local-lib
  • Menlo
  • Menlo-Legacy
  • Module-CPANfile
  • Parse-PMFile
  • Tie-Handle-Offset
  • Win32-ShellQuote
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment