What pperl already has#
Several hundred modules are built into the pperl binary, in two kinds. About a hundred are compiled in: Storable, Socket, Digest::MD5, Data::Dumper, Time::HiRes, POSIX, Encode and friends are not files pperl loads - they are Rust code, and require is routed to it. The rest are the Perl modules of the core library, Test::More, File::Spec, Getopt::Long and so on, whose source the binary carries. For both kinds %INC records ./pperl-native/<name> (for example ./pperl-native/Storable.pm) - it reads like a path, but there is no file behind it.
That is invisible and harmless when you are writing Perl. It is a problem when an installer is trying to work out what you have.
The probe#
Every installer answers “is this module present, and at what version?” the same way: look for the .pm file on the search path, read $VERSION out of it. A built-in module has no file. So the probe walks past pperl entirely and finds the answer in the host perl’s tree - a different interpreter, with different versions.
On the reference machine, five of six modules checked disagreed:
module | pperl provides | host perl says |
|---|---|---|
| 2.58 | 2.59 |
| 3.41 | 3.37 |
| 2.038 | 2.040 |
| 1.9777 | 1.9778 |
| 2.183 | 2.192 |
Every consequence of that is wrong, and two of the three are silent.
A dependency on
Storable >= 3.40reads as unsatisfied, because the host says 3.37. The installer fetches Storable - an XS distribution - to replace a module pperl already has at 3.41. The build then fails, and it fails for a module you never needed.On a host with no perl at all - a minimal container, or the static build whose entire point is not needing one - every built-in module reads as missing and the same false build is attempted for each.
In the other direction, and this one is silent: a dependency on
Data::Dumper >= 2.190reads as satisfied by the host’s 2.192, while pperl actually runs 2.183.
Asking pperl instead#
pperl answers the question itself. Config::lib_file returns the path under which pperl sees a library file, a built-in one first:
use Config;
print Config::lib_file("Storable.pm"), "\n"; # ./pperl-native/Storable.pm
print Config::lib_file("Foo/Bar.pm"), "\n"; # /home/you/.pperl/lib/perl5/Foo/Bar.pm
The path can be opened, so the version can be read from it as from any installed file. A built-in Perl module reads as its source. A compiled-in module that has no Perl source reads as three lines, its package, its $VERSION and a true value:
use Config;
use ExtUtils::MakeMaker ();
print MM->parse_version(Config::lib_file("Storable.pm")), "\n"; # 3.41
The installer that is part of pperl asks exactly this. So do the ExtUtils::MakeMaker and Module::Metadata pperl carries, which is what a Makefile.PL and a Build.PL use to check their prerequisites. A built-in module is then simply skipped: no download, no build, no diagnostic to explain.
Asked for a module that is built in, --cpan says so and stops:
pperl: Storable 3.41 is built into pperl; nothing to install
Note
The built-in path opens, but it answers no file test: -e, -f and -r say no. Code that looks for a module by testing "$dir/Storable.pm" for each directory of @INC does not find a built-in one. Use Config::lib_file there.
When the built-in version is too old#
pperl reports the version it actually has. That does not make the version acceptable. If a distribution asks for Storable >= 3.50 and pperl carries 3.41, the dependency is genuinely unsatisfied, the installer goes after the real distribution, and - Storable being XS - the build fails.
That is the honest outcome rather than a bug: the module pperl carries is not the version being asked for. It does mean a built-in module falling behind CPAN can turn into an install failure some way downstream, so a version mismatch is worth checking early when an install fails for no visible reason:
pperl -MStorable -e 'print Storable->VERSION, "\n"'