84778336f599a9d07e5ac976648797750c3a1d8a
[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.000000'; # 1.0.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   # rather basic csh detection, goes on the assumption that something won't
206   # call itself csh unless it really is. also, default to bourne in the
207   # pathological situation where a user doesn't have $ENV{SHELL} defined.
208   # note also that shells with funny names, like zoid, are assumed to be
209   # bourne.
210   my $shellbin = 'sh';
211   if(defined $ENV{'SHELL'}) {
212       my @shell_bin_path_parts = File::Spec->splitpath($ENV{'SHELL'});
213       $shellbin = $shell_bin_path_parts[-1];
214   }
215   while (@envs) {
216     my ($name, $value) = (shift(@envs), shift(@envs));
217     $value =~ s/(\\")/\\$1/g;
218
219     if($shellbin =~ /csh/) {
220       $out .= qq{setenv ${name} "${value}"\n};
221     }
222     else {
223       $out .= qq{export ${name}="${value}"\n};
224     }
225   }
226   print $out;
227 }
228
229 sub setup_env_hash_for {
230   my ($class, $path) = @_;
231   my %envs = $class->build_environment_vars_for($path, INTERPOLATE_PATH);
232   @ENV{keys %envs} = values %envs;
233 }
234
235 sub build_environment_vars_for {
236   my ($class, $path, $interpolate) = @_;
237   return (
238     MODULEBUILDRC => $class->modulebuildrc_path($path),
239     PERL_MM_OPT => "INSTALL_BASE=${path}",
240     PERL5LIB => join(':',
241                   $class->install_base_perl_path($path),
242                   $class->install_base_arch_path($path),
243                 ),
244     PATH => join(':',
245               $class->install_base_bin_path($path),
246               ($interpolate == INTERPOLATE_PATH
247                 ? $ENV{PATH}
248                 : '$PATH')
249              ),
250   )
251 }
252
253 =begin testing
254
255 #:: test classmethod
256
257 File::Path::rmtree('t/var/splat');
258
259 $c->ensure_dir_structure_for('t/var/splat');
260
261 ok(-d 't/var/splat');
262
263 ok(-f 't/var/splat/.modulebuildrc');
264
265 =end testing
266
267 =head1 NAME
268
269 local::lib - create and use a local lib/ for perl modules with PERL5LIB
270
271 =head1 SYNOPSIS
272
273 In code -
274
275   use local::lib; # sets up a local lib at ~/perl5
276
277   use local::lib '~/foo'; # same, but ~/foo
278
279 From the shell -
280
281   $ perl -Mlocal::lib
282   export MODULEBUILDRC=/home/username/perl/.modulebuildrc
283   export PERL_MM_OPT='INSTALL_BASE=/home/username/perl'
284   export PERL5LIB='/home/username/perl/lib/perl5:/home/username/perl/lib/perl5/i386-linux'
285   export PATH="/home/username/perl/bin:$PATH"
286
287 To bootstrap if you don't have local::lib itself installed -
288
289   $ perl -MCPAN -eshell # you only need to do this if you don't have a ~/.cpan
290   cpan> exit
291   <download local::lib tarball from CPAN, unpack and cd into dir>
292   $ perl Makefile.PL --bootstrap
293   $ make test && make install
294   $ echo 'eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >>~/.bashrc
295   # Or for C shells...
296   $ /bin/csh
297   % echo $SHELL
298   /bin/csh
299   % perl -I$HOME/perl5/lib/perl5 -Mlocal::lib >> ~/.cshrc
300
301 You can also pass --boostrap=~/foo to get a different location (adjust the
302 bashrc / cshrc line appropriately)
303
304 =head1 DESCRIPTION
305
306 This module provides a quick, convenient way of bootstrapping a user-local Perl
307 module library located within the user's home directory. It also constructs and
308 prints out for the user the list of environment variables using the syntax
309 appropriate for the user's current shell (as specified by the C<SHELL>
310 environment variable), suitable for directly adding to one's shell configuration
311 file.
312
313 =head1 LIMITATIONS
314
315 Rather basic shell detection. Right now anything with csh in its name is
316 assumed to be a C shell or something compatible, and everything else is assumed
317 to be Bourne.
318
319 Bootstrap is a hack and will use CPAN.pm for ExtUtils::MakeMaker even if you
320 have CPANPLUS installed.
321
322 Kills any existing PERL5LIB, PERL_MM_OPT or MODULEBUILDRC.
323
324 Should probably auto-fixup CPAN config if not already done.
325
326 Patches very much welcome for any of the above.
327
328 =head1 ENVIRONMENT
329
330 =over 4
331
332 =item SHELL
333
334 local::lib looks at the user's C<SHELL> environment variable when printing out
335 commands to add to the shell configuration file.
336
337 =back
338
339 =head1 AUTHOR
340
341 Matt S Trout <mst@shadowcat.co.uk> http://www.shadowcat.co.uk/
342
343 auto_install fixes kindly sponsored by http://www.takkle.com/
344
345 =head1 LICENSE
346
347 This library is free software under the same license as perl itself
348
349 =cut
350
351 1;