better bootstrap
[p5sagit/local-lib.git] / lib / local / lib.pm
1 use strict;
2 use warnings;
3
4 package local::lib;
5
6 use 5.8.1; # probably works with earlier versions but I'm not supporting them
7            # (patches would, of course, be welcome)
8
9 use File::Spec ();
10 use File::Path ();
11 use Carp ();
12 use Config;
13
14 our $VERSION = '1.001000'; # 1.1.0
15
16 sub import {
17   my ($class, $path) = @_;
18   $path = $class->resolve_path($path);
19   $class->setup_local_lib_for($path);
20 }
21
22 sub pipeline;
23
24 sub pipeline {
25   my @methods = @_;
26   my $last = pop(@methods);
27   if (@methods) {
28     \sub {
29       my ($obj, @args) = @_;
30       $obj->${pipeline @methods}(
31         $obj->$last(@args)
32       );
33     };
34   } else {
35     \sub {
36       shift->$last(@_);
37     };
38   }
39 }
40
41 =begin testing
42
43 #:: test pipeline
44
45 package local::lib;
46
47 { package Foo; sub foo { -$_[1] } sub bar { $_[1]+2 } sub baz { $_[1]+3 } }
48 my $foo = bless({}, 'Foo');                                                 
49 Test::More::ok($foo->${pipeline qw(foo bar baz)}(10) == -15);
50
51 =end testing
52
53 =cut
54
55 sub resolve_path {
56   my ($class, $path) = @_;
57   $class->${pipeline qw(
58     resolve_relative_path
59     resolve_home_path
60     resolve_empty_path
61   )}($path);
62 }
63
64 sub resolve_empty_path {
65   my ($class, $path) = @_;
66   if (defined $path) {
67     $path;
68   } else {
69     '~/perl5';
70   }
71 }
72
73 =begin testing
74
75 #:: test classmethod setup
76
77 my $c = 'local::lib';
78
79 =end testing
80
81 =begin testing
82
83 #:: test classmethod
84
85 is($c->resolve_empty_path, '~/perl5');
86 is($c->resolve_empty_path('foo'), 'foo');
87
88 =end testing
89
90 =cut
91
92 sub resolve_home_path {
93   my ($class, $path) = @_;
94   return $path unless ($path =~ /^~/);
95   my ($user) = ($path =~ /^~([^\/]+)/); # can assume ^~ so undef for 'us'
96   my $tried_file_homedir;
97   my $homedir = do {
98     if (eval { require File::HomeDir } && $File::HomeDir::VERSION >= 0.65) {
99       $tried_file_homedir = 1;
100       if (defined $user) {
101         File::HomeDir->users_home($user);
102       } else {
103         File::HomeDir->my_home;
104       }
105     } else {
106       if (defined $user) {
107         (getpwnam $user)[7];
108       } else {
109         if (defined $ENV{HOME}) {
110           $ENV{HOME};
111         } else {
112           (getpwuid $<)[7];
113         }
114       }
115     }
116   };
117   unless (defined $homedir) {
118     Carp::croak(
119       "Couldn't resolve homedir for "
120       .(defined $user ? $user : 'current user')
121       .($tried_file_homedir ? '' : ' - consider installing File::HomeDir')
122     );
123   }
124   $path =~ s/^~[^\/]*/$homedir/;
125   $path;
126 }
127
128 sub resolve_relative_path {
129   my ($class, $path) = @_;
130   File::Spec->rel2abs($path);
131 }
132
133 =begin testing
134
135 #:: test classmethod
136
137 local *File::Spec::rel2abs = sub { shift; 'FOO'.shift; };
138 is($c->resolve_relative_path('bar'),'FOObar');
139
140 =end testing
141
142 =cut
143
144 sub setup_local_lib_for {
145   my ($class, $path) = @_;
146   $class->ensure_dir_structure_for($path);
147   if ($0 eq '-') {
148     $class->print_environment_vars_for($path);
149     exit 0;
150   } else {
151     $class->setup_env_hash_for($path);
152     unshift(@INC, split(':', $ENV{PERL5LIB}));
153   }
154 }
155
156 sub modulebuildrc_path {
157   my ($class, $path) = @_;
158   File::Spec->catfile($path, '.modulebuildrc');
159 }
160
161 sub install_base_bin_path {
162   my ($class, $path) = @_;
163   File::Spec->catdir($path, 'bin');
164 }
165
166 sub install_base_perl_path {
167   my ($class, $path) = @_;
168   File::Spec->catdir($path, 'lib', 'perl5');
169 }
170
171 sub install_base_arch_path {
172   my ($class, $path) = @_;
173   File::Spec->catdir($class->install_base_perl_path($path), $Config{archname});
174 }
175
176 sub ensure_dir_structure_for {
177   my ($class, $path) = @_;
178   unless (-d $path) {
179     warn "Attempting to create directory ${path}\n";
180   }
181   File::Path::mkpath($path);
182   my $modulebuildrc_path = $class->modulebuildrc_path($path);
183   if (-e $modulebuildrc_path) {
184     unless (-f _) {
185       Carp::croak("${modulebuildrc_path} exists but is not a plain file");
186     }
187   } else {
188     warn "Attempting to create file ${modulebuildrc_path}\n";
189     open MODULEBUILDRC, '>', $modulebuildrc_path
190       || Carp::croak("Couldn't open ${modulebuildrc_path} for writing: $!");
191     print MODULEBUILDRC qq{install  --install_base  ${path}\n}
192       || Carp::croak("Couldn't write line to ${modulebuildrc_path}: $!");
193     close MODULEBUILDRC
194       || Carp::croak("Couldn't close file ${modulebuildrc_path}: $@");
195   }
196 }
197
198 sub INTERPOLATE_PATH () { 1 }
199 sub LITERAL_PATH     () { 0 }
200
201 sub print_environment_vars_for {
202   my ($class, $path) = @_;
203   my @envs = $class->build_environment_vars_for($path, LITERAL_PATH);
204   my $out = '';
205
206   # rather basic csh detection, goes on the assumption that something won't
207   # call itself csh unless it really is. also, default to bourne in the
208   # pathological situation where a user doesn't have $ENV{SHELL} defined.
209   # note also that shells with funny names, like zoid, are assumed to be
210   # bourne.
211   my $shellbin = 'sh';
212   if(defined $ENV{'SHELL'}) {
213       my @shell_bin_path_parts = File::Spec->splitpath($ENV{'SHELL'});
214       $shellbin = $shell_bin_path_parts[-1];
215   }
216   my $shelltype = do {
217       local $_ = $shellbin;
218       if(/csh/) {
219           'csh'
220       } else {
221           'bourne'
222       }
223   };
224
225   while (@envs) {
226     my ($name, $value) = (shift(@envs), shift(@envs));
227     $value =~ s/(\\")/\\$1/g;
228     $out .= $class->${\"build_${shelltype}_env_declaration"}($name, $value);
229   }
230   print $out;
231 }
232
233 # simple routines that take two arguments: an %ENV key and a value. return
234 # strings that are suitable for passing directly to the relevant shell to set
235 # said key to said value.
236 sub build_bourne_env_declaration {
237   my $class = shift;
238   my($name, $value) = @_;
239   return qq{export ${name}="${value}"\n};
240 }
241
242 sub build_csh_env_declaration {
243   my $class = shift;
244   my($name, $value) = @_;
245   return qq{setenv ${name} "${value}"\n};
246 }
247
248 sub setup_env_hash_for {
249   my ($class, $path) = @_;
250   my %envs = $class->build_environment_vars_for($path, INTERPOLATE_PATH);
251   @ENV{keys %envs} = values %envs;
252 }
253
254 sub build_environment_vars_for {
255   my ($class, $path, $interpolate) = @_;
256   return (
257     MODULEBUILDRC => $class->modulebuildrc_path($path),
258     PERL_MM_OPT => "INSTALL_BASE=${path}",
259     PERL5LIB => join(':',
260                   $class->install_base_perl_path($path),
261                   $class->install_base_arch_path($path),
262                 ),
263     PATH => join(':',
264               $class->install_base_bin_path($path),
265               ($interpolate == INTERPOLATE_PATH
266                 ? $ENV{PATH}
267                 : '$PATH')
268              ),
269   )
270 }
271
272 =begin testing
273
274 #:: test classmethod
275
276 File::Path::rmtree('t/var/splat');
277
278 $c->ensure_dir_structure_for('t/var/splat');
279
280 ok(-d 't/var/splat');
281
282 ok(-f 't/var/splat/.modulebuildrc');
283
284 =end testing
285
286 =head1 NAME
287
288 local::lib - create and use a local lib/ for perl modules with PERL5LIB
289
290 =head1 SYNOPSIS
291
292 In code -
293
294   use local::lib; # sets up a local lib at ~/perl5
295
296   use local::lib '~/foo'; # same, but ~/foo
297
298   # Or...
299   use FindBin;
300   use local::lib "$FindBin::Bin/../support";  # app-local support library
301
302 From the shell -
303
304   $ perl -Mlocal::lib
305   export MODULEBUILDRC=/home/username/perl/.modulebuildrc
306   export PERL_MM_OPT='INSTALL_BASE=/home/username/perl'
307   export PERL5LIB='/home/username/perl/lib/perl5:/home/username/perl/lib/perl5/i386-linux'
308   export PATH="/home/username/perl/bin:$PATH"
309
310 To bootstrap if you don't have local::lib itself installed -
311
312   <download local::lib tarball from CPAN, unpack and cd into dir>
313
314   $ perl Makefile.PL --bootstrap
315   $ make test && make install
316
317   $ echo 'eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >>~/.bashrc
318
319   # Or for C shells...
320
321   $ /bin/csh
322   % echo $SHELL
323   /bin/csh
324   % perl -I$HOME/perl5/lib/perl5 -Mlocal::lib >> ~/.cshrc
325
326 You can also pass --boostrap=~/foo to get a different location (adjust the
327 bashrc / cshrc line appropriately)
328
329 =head1 DESCRIPTION
330
331 This module provides a quick, convenient way of bootstrapping a user-local Perl
332 module library located within the user's home directory. It also constructs and
333 prints out for the user the list of environment variables using the syntax
334 appropriate for the user's current shell (as specified by the C<SHELL>
335 environment variable), suitable for directly adding to one's shell configuration
336 file.
337
338 More generally, local::lib allows for the bootstrapping and usage of a directory
339 containing Perl modules outside of Perl's C<@INC>. This makes it easier to ship
340 an application with an app-specific copy of a Perl module, or collection of
341 modules. Useful in cases like when an upstream maintainer hasn't applied a patch
342 to a module of theirs that you need for your application.
343
344 On import, local::lib sets the following environment variables to appropriate
345 values:
346
347 =over 4
348
349 =item MODULEBUILDRC
350
351 =item PERL_MM_OPT
352
353 =item PERL5LIB
354
355 =item PATH
356
357 PATH is appended to, rather than clobbered.
358
359 =back
360
361 These values are then available for reference by any code after import.
362
363 =head1 LIMITATIONS
364
365 Rather basic shell detection. Right now anything with csh in its name is
366 assumed to be a C shell or something compatible, and everything else is assumed
367 to be Bourne. If the C<SHELL> environment variable is not set, a
368 Bourne-compatible shell is assumed.
369
370 Bootstrap is a hack and will use CPAN.pm for ExtUtils::MakeMaker even if you
371 have CPANPLUS installed.
372
373 Kills any existing PERL5LIB, PERL_MM_OPT or MODULEBUILDRC.
374
375 Should probably auto-fixup CPAN config if not already done.
376
377 Patches very much welcome for any of the above.
378
379 =head1 ENVIRONMENT
380
381 =over 4
382
383 =item SHELL
384
385 local::lib looks at the user's C<SHELL> environment variable when printing out
386 commands to add to the shell configuration file.
387
388 =back
389
390 =head1 AUTHOR
391
392 Matt S Trout <mst@shadowcat.co.uk> http://www.shadowcat.co.uk/
393
394 auto_install fixes kindly sponsored by http://www.takkle.com/
395
396 =head1 CONTRIBUTORS
397
398 Patches to correctly output commands for csh style shells, as well as some
399 documentation additions, contributed by Christopher Nehren <apeiron@cpan.org>.
400
401 =head1 LICENSE
402
403 This library is free software under the same license as perl itself
404
405 =cut
406
407 1;