local::lib and File::Spec

local::lib is a great way of providing a contained set of Cpan modules for an application. But it can be awkward if you wish to use it to provide an updated File::Spec.

I wished to use Path::Tiny, which I installed using cpanm to a local::lib directory. That requires a newer version of File::Spec than came with the Perl we're using, so cpanm downloaded and installed that to the local::lib directory too, just as you'd want.

Our programs specify their local::lib directory, so they can be run by users without them having to fiddle around setting up environment variables (on Windows). Unfortunately, however, using it in our programs like this didn't work for Path::Tiny:

use local::lib qw<Cpan>;
use Path::Tiny;

That complains “File::Spec version 3.4 required--this is only version 3.33”.

I'm grateful to Haarg on #perl-help for pointing out that local::lib itself uses File::Spec (which isn't at all surprising, when you think about it), so will have loaded the system-installed version of File::Spec before it gets round to adding the directory where the new one is found to the search path. When Path::Tiny then uses File::Spec, the older one is already loaded, which means perl doesn't go looking for a newer one.

Apparently what I should've been doing is this instead:

use lib qw<Cpan/lib/perl5/>;
use Path::Tiny;

That adds the relevant local::lib directories first, without actually loading the local::lib module or any of its dependencies such as File::Spec.

This works even though File::Spec is an XS module, so is actually found in a subdirectory called something like Cpan/lib/perl5/MSWin32-x86-multi-thread: the use lib automatically adds the architecture-specific directory as well, which is handy.

(The above was my fourth attempt at a workaround. Not knowing about that lib feature, my third attempt involved messing around with $Config{archname} to build the subdirectory path.

My second attempt was to unload the File::Spec that local::lib had loaded:

use local::lib 'Cpan';
BEGIN
{
  delete @INC{glob 'File/Spec{,/Unix,/Win32}.pm'};
  no strict qw<refs>;
  %{'File::Spec::'} = ();
}
use Path::Tiny;

It, erm, works, but doesn't exactly strike me as code we should have in production.

My first attempt tried to use Class::Unload. I now think that would've worked, but I was copying the example in its synopsis, which uses Class::Inspector to report whether the class has been unloaded — which itself uses File::Spec!)

Updated Tweaked after MST explained a little more about local::lib. And another workaround attempt added (and the others renumbered) after Ribasushi reminded me I'd tried Class::Unload.

2 Comments

Personally I don't like to use 'lib' or 'local::lib' in my application scripts as I find they lead to unnecessary structural binding between your application and the directory structure.

Ideally you application would be decoupled from its dependency paths (or at least I think so). So I tend to do like:

perl -I $pathtolib bin/myapp.pl

or, if using local::lib

perl -Mlocal::lib=$pathtolib bin/mayapp.pl

I also wrote a tool App::local::lib::helper that may or may not be useful in encapsulating this idea.

If you hard code the path to your local::lib or otherwise into your application, sooner or later its going to cause you trouble. Just my thoughts, good luck!

jnap

Leave a comment

About Smylers

user-pic Perl developer based in Leeds. Half-time for Webfusion, t'other half freelance, and a sideline in writing questions for BBC4's ‘Only Connect’. Previously worked for ‘The Register’ and run Perl training courses for Yahoo!, Microsoft, Sony, and the Nationwide building society among others. Keep being allowed back to speak at Yapc Europe conferences, despite a habit of presenting non-Perl topics.