#!/usr/bin/env perl use strict; use warnings; use 5.006000; use File::Spec::Functions qw< rel2abs splitpath splitdir catdir catpath file_name_is_absolute >; use Cwd qw< getcwd realpath >; use Pod::Usage qw< pod2usage >; my $VERSION = '0.03'; our $ME = absolute($0); our $PERL = $^X; our $PERLDOC = 'perldoc'; our $SUFFIX = '.pl'; my @alternatives = ( [qr{(?mxs: \A wrapperl \z)}, \&dispatch_wrapperl], [qr{(?mxs: \A perl \z)}, sub { action_exec($ME) }], [ qr{(?mxs: \A perldoc \z)}, sub { unshift @ARGV, $PERLDOC; action_sibling($ME); }, ], [ qr{.}, sub { unshift @ARGV, "$ME$SUFFIX"; action_exec($ME); }, ], ); my (undef, undef, $basename) = splitpath($ME); for my $candidate (@alternatives) { my ($match, $callback) = @$candidate; next unless $basename =~ m{$match}; $callback->(); } die "something went really wrong\n"; sub absolute { my ($path) = @_; return $path if file_name_is_absolute($path); return rel2abs($path); } sub find_envfile { my @starts = @_; push @starts, getcwd(), $ENV{HOME} unless @starts; for my $start (@starts) { next unless -e $start; my ($volume, $directories) = splitpath($start, -d $start); my @path = splitdir($directories); my $envfile; while (@path) { my $candidate = catpath($volume, catdir(@path), 'wrapperl.env'); return $candidate if -e $candidate; pop @path; } } ## end for my $start (@starts) die "could not find wrapperl.env\n"; return; # never reached } ## end sub find_envfile sub load_envfile { my $envfile = find_envfile(@_); my $retval = do $envfile or die "errors loading '$envfile'\n"; return $retval; } ## end sub load_envfile sub myexec { my @command = @_; exec {$command[0]} @command; die "failed execution of @command\n"; } sub action_env { my $e = find_envfile(@ARGV ? $ARGV[0] : ()); print {*STDOUT} "$e\n"; exit 0; } sub action_sibling { my $sibling = shift @ARGV; pod2usage( -verbose => 99, -sections => 'USAGE', message => 'no sibling program provided' ) unless defined $sibling; load_envfile(); my ($v, $d) = splitpath($PERL); my (undef, undef, $basename) = splitpath($sibling); my $path = catpath($v, $d, $basename); myexec($PERL, $path, @ARGV); } ## end sub action_sibling sub action_exec { load_envfile(@_); myexec($PERL, @ARGV); } sub dispatch_wrapperl { action_exec() unless @ARGV; my $first = $ARGV[0] || ''; if ($first =~ m{\A(?: -e | --env )\z}mxs) { shift @ARGV; action_env(); } elsif ($first =~ m{\A(?: -d | --doc )\z}mxs) { shift @ARGV; unshift @ARGV, $PERLDOC; action_sibling(); } elsif ($first =~ m{\A(?: -s | --sibling )\z}mxs) { shift @ARGV; action_sibling(); } elsif ($first =~ m{\A(?: -x | --exec )\z}mxs) { shift @ARGV; action_exec(); } # Last chance, we're to act as wrap-perl for hash-bang support action_exec(realpath($ARGV[0])); } ## end sub dispatch_wrapperl ### Handy functions for path management in wrapperl.env ### # legacy support sub set_PERL5LIB_from_siblings { @_ = map { if (ref $_) { my @dirs = @$_; my $file = pop @dirs; @dirs ? catpath('', catdir(@dirs), $file) : $file; } elsif (file_name_is_absolute($_)) { [ $_ ]; } else { $_; } } @_; goto \&PERL5LIB; } sub path_sibling { my ($reference, $child) = @_; my ($v, $d) = splitpath($reference); my (undef, $fd, $filename) = splitpath($child); $d = catdir(splitdir($d), splitdir($fd)) if defined($fd) && length($fd); return catpath($v, $d, $filename); } sub PERL5LIB_add { my @items; # initialize with current PERL5LIB contents, if any push @items, $ENV{PERL5LIB} if defined($ENV{PERL5LIB}) && length($ENV{PERL5LIB}); # add items related to this call my $caller_file = rel2abs((caller(1))[6]); push @items, map { ref($_) ? @$_ # plain paths, either relative to PWD or absolute : path_sibling($caller_file, $_); } @_; # return what's appropriate return unless @items; return $ENV{PERL5LIB} = join ':', @items; } sub PERL5LIB { delete $ENV{PERL5LIB}; goto \&PERL5LIB_add; } sub ME { our $ME = shift } sub PERL { our $PERL = shift } sub PERLDOC { our $PERLDOC = shift } sub SUFFIX { our $SUFFIX = shift } __END__ =pod =encoding utf-8 =head1 NAME wrapperl - wrapper for Perl customized invocation =head1 Hurry Up! # download in "visible" location cd /usr/bin # alias wget='curl -LO' # in case you need it wget https://raw.githubusercontent.com/polettix/wrapperl/master/wrapperl chmod +x wrapperl # set wrapperl.env for your project. Suppose you have a 'lib' directory # with your stuff inside, and a 'local/lib/perl5' directory with the # local installation of support modules, all inside /path/to/project cd /path/to/project cat > wrapperl.env <); PERL('/path/to/selected/bin/perl'); END # start using it, most straightforward way is from hash-bang cat > program.pl < from L and put somewhere in the environments where you need it. It is not necessary to put it in a directory in the C, although it is suggested in order to access all functionalities and it will also be assumed in the following of this example. Let's make a few assumptions: =over =item * you are in a sane environment where you managed to put C somewhere in your C and you have a working C (this assumption is not so strong, as you can understand, but will help us set a consistent hash-bang) =item * you will write your program and we will call it C. To get the gist of C you can start from this: #!/usr/bin/env wrapperl print "using perl '$^X', \@INC contains:\n"; print "- '$_'\n" for @INC; If you didn't manage to put C in C, or you don't have C, just put the path to C in the I, although you will then need to ensure that this choice will be true on all systems =item * you do your coding in a development environment where: =over =item * you develop C inside directory C =item * C is located at C =item * the modules you develop in association with C are located in sub-directory C. In addition, you keep a local library of support modules in sub-directory C and you also want to include modules from non-standard absolute location C and relative location C with respect to where you are calling the program for (you are generally advised against this, but the example shows that you can) =back =item * you deploy your program in a production environment with a different setup, namely: =over =item * your program C is deployed in directory C =item * C is located at C =item * you still keep the layout with the C and C sub-directories, but all system-wide modules you need are stored in C. =back =back In both environments, you create a C file inside the root directory of your project, which will hold configurations that are specific for the specific environment it is located into. In this example we will put it in the same directory as C. This is what you end up with in the development environment: me@devhost /home/me/program$ ls -l -rwxr-xr-x 1 me me 74 Apr 23 22:28 prg -rwxr-xr-x 1 me me 90 Apr 22 12:35 wrapperl.env me@devhost /home/me/program$ cat wrapperl.env PERL5LIB( qw< lib local/lib/perl5 >, # located as siblings of wrapperl.env [ qw< /path/to/some/lib another/lib > ], # non-siblings paths ); PERL('/home/me/perl/bin/perl'); This is what you have in the production environment: me@production /app/program$ ls -l -rwxr-xr-x 1 me me 74 Apr 25 20:51 prg -rwxr-xr-x 1 me me 66 Apr 25 20:51 wrapperl.env me@production /app/program$ cat wrapperl.env PERL5LIB( qw< lib local/lib/perl5 >, # located as siblings of wrapperl.env [ qw< /approved/lib > ], # non-siblings paths ); PERL('/approved/perl/bin/perl'); So yes, they two setups are mostly the same, except for the contents of the C files, each containing configurations that are environment-specific. You should be able to easily guess what the two functions C and C do. Now, you just execute your program. In the development environment: me@devhost /home/me/program$ ./prg using perl '/home/me/perl/bin/perl', @INC contains: - '/home/me/program/lib' - '/home/me/program/local/lib/perl5/i686-linux' - '/home/me/program/local/lib/perl5' - '/path/to/some/lib/i686-linux' - '/path/to/some/lib' - '/home/me/program/another/lib/i686-linux' - '/home/me/program/another/lib' - '/home/me/perl/lib/site_perl/5.18.1/i686-linux' - '/home/me/perl/lib/site_perl/5.18.1' - '/home/me/perl/lib/5.18.1/i686-linux' - '/home/me/perl/lib/5.18.1' - '.' In the production environment: me@production /app/program$ ./prg using perl '/approved/perl/bin/perl', @INC contains: - '/app/program/lib' - '/app/program/local/lib/perl5/i686-linux' - '/app/program/local/lib/perl5' - '/approved/lib/i686-linux' - '/approved/lib' - '/approved/perl/lib/site_perl/5.18.1/i686-linux' - '/approved/perl/lib/site_perl/5.18.1' - '/approved/perl/lib/5.18.1/i686-linux' - '/approved/perl/lib/5.18.1' - '.' One last hint! If you cannot manage to install C somewhere in the C in all the environments, you can either do some shell wrapping (but this would somehow make wrapperl slightly overkill probably) or use an approach based on symbolic links. If this is the case: =over =item * rename your program C as C, i.e. ending in suffix C<.pl> =item * in the same directory, create a symbolic link named C and pointing to the location of C (which could be in the very same directory if you plan to ship wrapperl as well) =back With this setup, when you run the symbolic link, it will just run the associated C<.pl> file with the settings in the C file. That's all folks! =head1 SYNOPSYS # Minimal setup: create a "wrapperl.env" file. # It is a Perl program to set up the right environment. # Two handy functions PERL5LIB() and PERL() are all you need usually shell$ cat wrapperl.env PERL5LIB(qw< lib llib > [qw< /path/to/lib /path/to/other/lib >]); PERL('/path/to/bin/perl'); # You can have a different "wrapperl.env" on each directory. # What is the one that we would see from here? "wrapperl" can # tell you this with -e | --env shell$ wrapperl -e /path/to/wrapperl.env # If you also provide a parameter to -e | --env, it will tell # you which environment file will be seen by the provided # parameter (that is of course expected to be a path) shell$ wrapperl -e /path/to/a/wrapperl/symlink /path/to/a/wrapper.env # Time to make it work! Option -x | --exec means calling it # as if it were the chosen perl with the configurations in # "wrapperl.env" shell$ wrapperl -x -le 'print $^X' /path/to/bin/perl # Option -s | --sibling allows to call programs that are # usually shipped with perl, e.g. perlthanks, podchecker, etc. shell$ wrapperl -s podchecker myprogram.pl # Another useful option is -d | --doc to call perldoc quickly, # so the following ones are equivalent but the latter is less typing shell$ wrapperl -s perldoc Module::Name shell$ wrapperl -d Module::Name # If the first parameter is not supported by "wrapperl" directly, # it will be considered a perl program to be executed along with # its own parameters. This makes it handy to use wrapperl in # hash-bang setups. The program's *realpath* is also used as the # starting point for searching wrapperl.env, so that symbolic # links to your program should work as expected shell$ wrapperl myprogram.pl --foo bar # You can symlink pointing to wrapperl and it will do some magic. # Call the real program a name ending with ".pl" (e.g. "prg.pl") # and symlink wrapperl with the same name withouth the extension # (e.g. "prg"). This is what will happen: shell$ ls -l -rw-r--r-- 1 me me 74 Apr 23 22:20 wrapperl.env lrwxrwxrwx 1 me me 8 Apr 23 22:51 prg -> /path/to/wrapperl -rwxr-xr-x 1 me me 74 Apr 23 22:28 prg.pl shell$ cat prg.pl #!/usr/bin/env perl print "using perl '$^X', \@INC contains:\n"; print "- '$_'\n" for @INC; shell$ cat wrapperl.env $ENV{PERL5LIB} = '/path/to/some/lib:/path/to/another/lib'; $PERL = '/path/to/bin/perl'; shell$ which perl /usr/bin/perl # If you call the program directly, wrapperl is not used of course shell$ ./prg.pl using perl '/usr/bin/perl', @INC contains: - '/etc/perl' - '/usr/local/lib/perl/5.14.2' - '/usr/local/share/perl/5.14.2' - '/usr/lib/perl5' - '/usr/share/perl5' - '/usr/lib/perl/5.14' - '/usr/share/perl/5.14' - '/usr/local/lib/site_perl' - '.' # On the other hand, if you call the "prg" symlink to wrapperl, # the same program above will be called, but with the perl and # options set in "wrapperl.env" shell$ ./prg using perl '/path/to/bin/perl', @INC contains: - '/path/to/another/lib/i686-linux' - '/path/to/another/lib' - '/path/to/some/lib/i686-linux' - '/path/to/some/lib' - '/path/to/lib/site_perl/5.18.1/i686-linux' - '/path/to/lib/site_perl/5.18.1' - '/path/to/lib/5.18.1/i686-linux' - '/path/to/lib/5.18.1' - '.' # There are two symlinks/names that trigger a special behaviour, # namely "perl" and "perldoc" that do what you think shell$ ls -l lrwxrwxrwx 1 me me 8 Apr 23 21:51 perl -> /path/to/wrapperl lrwxrwxrwx 1 me me 8 Apr 23 22:46 perldoc -> /path/to/wrapperl # The following two are therefore equivalent (and no, the # double "-x" is not an error, because the first is consumed by # "wrapperl" and the second one is for the invoked perl) shell$ ./perl -x /path/to/my/program.pl shell$ wrapperl -x -x /path/to/my/program.pl # These three are equivalent too shell$ ./perldoc My::Module shell$ wrapperl -d My::Module shell$ wrapperl -s perldoc My::Module # Last, if you manage to install wrapperl somewhere in the PATH # you can spare the symbolic link and use the hash bang directly! shell$ cat hashbanged-program #!/usr/bin/env wrapperl print "using perl $^X\n"; print "$_\n" for @INC; shell$ ./hashbanged-program using perl '/path/to/bin/perl', @INC contains: - '/path/to/another/lib/i686-linux' - '/path/to/another/lib' - '/path/to/some/lib/i686-linux' - '/path/to/some/lib' - '/path/to/lib/site_perl/5.18.1/i686-linux' - '/path/to/lib/site_perl/5.18.1' - '/path/to/lib/5.18.1/i686-linux' - '/path/to/lib/5.18.1' - '.' =head1 DESCRIPTION This program lets you wrap a perl program with some local-specific configurations. Why would you do this, e.g. as opposed to modifying the I line or setting C, or calling the perl executable directly? Well, lazyness of course, but also the fact that in different environments the same program might need different configurations, and changing those configurations possibly in many little Perl programs quickly becomes an error-prone hassle. C provides you with a consistent, minimal and easy to setup way to concentrate local-specific configurations in the L<< The C File >>, and be sure that you will call your Perl program(s) with the right setup every time. C's behaviour strongly depends on its name. That is, if you leave it as C it behaves in a specific way, while if you name it differently then it does something else. You have several options to do call C with a different name: =over =item * you just copy it with a different name. It works but it's also ugly and it will be a hassle every time you want to upgrade (but chances are you will not need. so don't worry too much) =item * you create a symbolic link. Works if your filesystem supports them, is robust and allows you to avoid touching the main program =item * if you can put C somewhere in the path in all your environments, and your system supports the I system (i.e. you're in some Unix-ish system), you can just set it inside the main program and avoid having anything more. Very clean and suggested if possible! =back The following sections start by describing the C file you should set up, then describe the behaviour in the different conditions; among them, most probably you will be interested into L. =head2 The C File The C file is at the heart of the localization of your configurations. =head3 Contents The file is a standard Perl program. It will be called using whatever I perl is found, that is not what you are looking for most probably (otherwise you would probably not be using C at all). You can do whatever setting inside it, while most probably you will be interested in setting the environment variable C to point towards the library directories you want to include in C<@INC>, and also set the right Perl executable to use. You can affect how C works by calling the following functions from within a C file (you should normally only need the first two anyway): =over =item B the path to the perl to use for invoking the other programs. By default it is set to the same perl that is executing C, namely C<$^X>, just in case you need to setup C only. =item B set the environment variable C according to your needs. You can pass a list of items, each of which can be: =over =item * a string, that is interpreted as a relative path starting from the same directory as where C is put. This allows e.g. to make sure you can point towards sub-directories C and C inside your project's root directory, provided you also put C in the same directory =item * a reference to an array of strings. These strings are passed unchanged in the environment variable, so that you can set either absolute paths or paths relative to the current directory. =back You should normally need to set paths relative to the root directory of your project, this is why it's slightly easier to set them instead of absolute paths or paths relative to the current directory. Any previous value of the environment variable C is wiped out, and this is considered a feature. If you really want to preserve it somewhere, just pass its value inside a reference to an array like this: PERL5LIB(..., [$ENV{PERL5LIB}], ...); =item B the location of the original program invoked. When calling C with a different name (see L), it is used together with C<$SUFFIX> described below to form the name of the program C<$ME$SUFFIX> that will be called with the new C<$PERL>. In general you should not need to fiddle with this. =item B The name of the C utility installed along with C<$PERL>. By default it is set to C, and you probably do not need to change it. =item B a suffix that is appended to the name of the invoked program when calling C with a different name (see L). Makes sense only if you are using the symbolic linking method and not the I approach. Assuming that C<$ME> holds the value set by C and C<$SUFFIX> the value set by C, the called program will be C<$ME$SUFFIX>, so if C<$SUFFIX> is C<.pl>, you are expected to call your I program the same as your symbolic link (or renamed C program) but with C<.pl> appended. Example: shell$ ls -l lrwxrwxrwx 1 me me 8 Apr 23 22:51 prg -> /path/to/wrapperl -rwxr-xr-x 1 me me 74 Apr 23 22:28 prg.pl If your system is picky about how files should be named (e.g. Windows might put some restrictions to what it considers as I), then you can do your transformations directly using C and set C to the empty string in order to select the I program to call. By default, it is set to C<.pl> and you should not need to change it. =back =head3 Loading The C file is loaded via a C, so you are warned about any possible security issue. The invocation is supposed to return a true value (in Perl terms), otherwise the execution will be stopped. =head3 Position Depending on how C is called, the C file is searched in different locations. One or more I will be considered, and used to perform a search from that position upwards in the filesystem. For example, if the starting point is C, then the following paths will be searched for C: /path/to/some/internal/sub /path/to/some/internal /path/to/some /path/to /path / An exception is thrown if no C file is found during the search in all the starting points. The I of the C file is performed starting from the current working directory, then from the user's home directory as read from the C environment variable. In some cases, the starting position will be some other specific location. For example, when C is L, the only starting location will be the path to the link to C, (i.e. what is used to initialize C<$ME>). =head2 Direct Invocation Direct invocation of C (i.e. without changing the name when calling it) is subject to the processing of some options (see L). Unless otherwise noted, the resolution of the C file is the I one as described in section L. If none of the options in L is recognized, the selected perl via C is invoked with whatever argument list is provided. This is equivalent to using the C<-x|--exec> option, except that the first option is not stripped away in this case and also that the first item in the command line list is assumed to be the path to a program and its path will be used as the starting position for C location resolution. Option C<-d|--doc> helps you call C, or whatever is set in C<$PERLDOC>. This will be useful in order to use the C that is shipped with the selected C<$PERL>, and more importantly with the same options (e.g. C) set in C, so that you will be able to find whatever module is installed in your personalized paths. Option C<-e|--env> helps you find out what will be the C used, so that you can double check that it is the one you are expecting and its contents. If you also pass a path in the command line, it will be used as the starting point for searching C, otherwise the standard resolution process is used. Option C<-s|--sibling> allows you to call one of the Perl programs that are present in the same directory as C<$PERL>, much in the same way as described for C above. For example, if you want to check the POD documentation in C using the C that is shipped with the perl you indicated in C: shell$ wrapperl -s podchecker Last, option C<-x|--exec> allows you to call C<$PERL> with the options set in C (where the resolution process starts from the current directory or from the C directory). =head2 Named C This name makes C transform into a call to what set as C, including any command line option provided. The resolution of the C file is performed according to the I process explained in section L, starting from the location of the symbolic link. =head2 Named This name calls the C set via C and located in the same directory as what set via C, including any command line option provided. The behaviour is the same as calling C with option C<-d|--doc>, with the exception of the resolution process. The resolution of the C file is performed according to the I process explained in section L, starting from the location of the symbolic link. =head2 Named Something Else If your system(s) have C and you can put C somewhere in the C, just set the I to: #!/usr/bin/env wrapperl and you're done. If not, read on. Assuming that you have set up your C file (see L<< /The C File >>), you are only two steps away from using C to automate calling your program with the right setup: =over =item * you can write your program without worrying about which perl will be used to call it or where the libraries are installed. Your only constraint is to name it ending with what is set for C or do some magic using C in C. By default, it suffices that you name your program ending with C<.pl>. For example, we will assume that your program is called C. =item * You set up a copy to C to be called the same as your program, but without the C. In the example, your copy would be called C. To make the copy you don't really have to make a copy! A symbolic link is sufficient, if your filesystem supports them. =back This is really it! Now, every time you need to run your program... don't do it, execute the C copy instead! That is, in the example you would call C, and it would in turn call your C but after reading all the configurations in C. See L for a complete and commented example. =head1 OPTIONS When invoked with name C, this program supports the following options. Note that you can provide one of them as the first option, and anyone not appearing here will actually be used for invoking the perl indicated in the C file. In all the options below, unless otherwise noted, the I for searching C is used (see L). =over =item B<< -d | --doc [arg1 arg2 ...] >> invoke whatever program is set in the C<$PERLDOC> variable in package C
(C by default), using C<$PERL> and the settings inside C. The C<$PERLDOC> program is expected to be placed in the same directory as the selected C<$PERL>. =item B<< -e | --env [path] >> print the path to the C file. If a C is provided after this option, it is used as a starting location for searching C, otherwise the I is used. See L for additional details. =item B<< -s | --sibling name [arg1 arg2 ...] >> invoke a I program, i.e. a program that is shipped along with C<$PERL> and is located in the same directory. The program is run with C<$PERL> and the configurations set inside C. Any argument is provided on the command line is passed along to the sibling program. This will thus work fine when the sibling is a Perl program, but not for binary executables. =item B<< -x | --exec program [arg1 arg2 ...] >> invoke C<$PERL> with the provided program and arguments, after loading the options in C. The C resolution is performed starting from the I of C (see L). =back =head1 DIAGNOSTICS =over =item C<< could not find wrapperl.env >> C tried to find C in the same directory as the symbolic link to it, or in any ancestor directory, but failed to find one. =item C<< errors loading '%s' >> loading the C was not successful, i.e. the invocation via C did not produce a true value. The placeholder provides the location of the offending file. =item C<< failed execution of %s >> C tried to execute the command (reported in the error message) but failed. The placeholder provides the offending command. =item C<< something went really wrong >> you shouldn't ever see this message, if you do it's a bug! =back =head1 CONFIGURATION AND ENVIRONMENT C does not have a configuration per-se, but is of course relying on the presence of a C file for proper functioning - see L. =head1 DEPENDENCIES C relies on modules that are part of any standard Perl distribution as of release 5.6.0. =head1 BUGS AND LIMITATIONS Please report bugs and hopefully solutions through the GitHub repository at L. =head1 AUTHOR Flavio Poletti =head1 LICENSE AND COPYRIGHT Copyright (c) 2015, Flavio Poletti C. This module is free software. You can redistribute it and/or modify it under the terms of the Artistic License 2.0. Please read the full license in the F file inside the distribution, as you can find at L. This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. =cut