Hello all,

I try to write a program which checks a Perl script for all modules it
needs at runtime.

This step is necessary for a standalone builder where all modules are
put into the TEXT resource of a standalone program. The current
RuntimeBuilder, originally written by Michael Ziege and greatly enhanced
by Keitarou Miyazaki, lacks this ability. Now standalone MacPerl
programs may therefore stop with messages about missing modules.

The task looked not that difficult but as you can see from the code
below it got larger and larger.

The idea is to go through the source file, find its "use" and "require"
lines, eval them and fill %INC. In a second loop I go through every
entry of %INC search for further "require" statements, check if the then
required files containes another "require" and so on and on until %INC
doesn't grow anymore.

You can run the script on Unix too.

I'd like to hear your comments. Corrections are very much welcomed.


Good night,
Axel.

###########################################################################
#!perl -w
### use strict;
### $| = 1;

sub eval_require_line {
  my $file = shift;
  local *FILE;
  open FILE, $file or die "cannot open file '$file'";
  my $skip_pod_section = 0;
  while( <FILE> ) {
    last if /^__END__/;

    if( ! $skip_pod_section and /^=/ )    { $skip_pod_section = 1; next; }
    if(   $skip_pod_section and /^=cut/ ) { $skip_pod_section = 0; next; }
    next if $skip_pod_section;

    next unless /\brequire\b.+;/;
    next if not_understandable();
    # tranlsate Unix path
    if( /require\s+'*"*\w+\/\w+/ ) { tr/\//:/ }

    my $result = m/\brequire\s+([\w:]+)\b(.*)/;
    my $new_module;
    if( $result ) {
      $new_module = $1;
    }
    else {
      warn "regexp for require statement failed while checking '$file'\nline '$_'";
      next;
    }
    my $new_module_key = $new_module.".pm";
    $new_module_key =~ s/::/\//g;
    next if exists $INC{ $new_module_key };
    # fill %INC
    my $eval_line = "use $new_module";
    eval( $eval_line );
    warn "$@ in line '$eval_line'" if $@;
  }
  close FILE;
}

sub eval_use_line {
  my $file = shift;
  local *FILE;
  open FILE, $file or die "cannot open file '$file'";
  my $skip_pod_section = 0;
  while( <FILE> ) {
    last if /^__END__/; # don't look into pod files...

    if( ! $skip_pod_section and /^=/ )    { $skip_pod_section = 1; next; }
    if(   $skip_pod_section and /^=cut/ ) { $skip_pod_section = 0; next; }
    next if $skip_pod_section;

    next unless /^\s*use.+;/o; # only "use" statements are interesting
    next if /^\s*use\s+constant/o; # avoid annoying messages on some module...
    # fill %INC
    eval( $_ );
    warn "$@ in line '$_'" if $@;
    # TODO: what, if "use" statement spans multiple lines ...
  }
  close FILE;
}

sub recurse_required {
  my $file = shift;
  my $count_includes = scalar keys %INC;
  local *FILE;
  open FILE, $file or die "cannot open file '$file'";
  my $skip_pod_section = 0;
  while( <FILE> ) {
    last if /^__END__/;

    if( ! $skip_pod_section and /^=/ )    { $skip_pod_section = 1; next; }
    if(   $skip_pod_section and /^=cut/ ) { $skip_pod_section = 0; next; }
    next if $skip_pod_section;

    next unless /\brequire\b.+;/;
    next if not_understandable();
    # tranlsate Unix path
    if( /require\s+'*"*\w+\/\w+/ ) { tr/\//:/ }

    my $result = m/\brequire\s+'*"*([\w:]+)'*"*\b(.*)/;
    my $new_module;
    if( $result ) {
      $new_module = $1;
    }
    else {
      warn "regexp for require statement failed while checking '$file'\nline '$_'";
      next;
    }
    my $new_module_key = $new_module.".pm";
    $new_module_key =~ s/::/\//g;
    next if exists $INC{ $new_module_key };
    # fill %INC
    my $eval_line = "use $new_module";
    eval( $eval_line );
    warn "$@ in line '$eval_line'" if $@;
=for debug
    print "INFO: found \"require\" line in '$file' - evaluated '$eval_line', now ",
      scalar keys %INC, " entries in %INC (was: $count_includes)\n";
=cut
    if( (scalar keys %INC) > $count_includes ) {
      print "INFO: new dependencies found in '$file' - '$new_module'\n";
      my $new_module_path = find_module( $new_module );
      recurse_required( $new_module_path );
      $count_includes = scalar keys %INC;
    }
  }
  close FILE;
}

sub not_understandable {
  return 1 if /require\s+\d/; # skip "require <perl-version>"
  return 1 if /#\s*require/; # skip comments
  return 1 if /require\s+'*"*(\$|.+\/\$)'*"*/; # we cannot know the content of a file 
name variable ...
  return 1 if /eval.+require/; # ignore modules if exception handling is already 
included
  return 1 if /\$require/; # keep regexp understandable
  # some particular difficult sample lines:
  #    require IO::Socket::UNIX if ($^O ne 'epoc');
  #    { local $@; require Carp::Heavy; } # XXX fix require to not clear $@?
  #    do { require Carp; Carp::croak("Can't export symbol: $type$sym") };
  #    Carp::croak("$pkg $wanted required--this is only version $version$file")
  #    ($pack,$file,$line,$sub,$hargs,undef,$eval,$require) = @a;
  #    eval { local $SIG{__DIE__}; require $filename };
  #    require '$optionRequire{$option}'
  #    eval "require Term::Rendezvous;" or die;
  #    require "File/Spec/$module.pm";

  return 0;
}

sub find_module {
  # try to find module in source script directories or subdirectories of @INC

  # a complete different strategy could be to extract the hash value in %INC
  # by finding the newly added entry

  my $DIRSEP;
  $DIRSEP = ":"  if $^O =~ /Mac/;
  $DIRSEP = "/"  if $^O =~ /unix|linux/i;
  $DIRSEP = "\\" if $^O =~ /win/i;
  my $pwd = `pwd`; chop $pwd; $pwd .= $DIRSEP unless $pwd =~ /$DIRSEP$/;

  my $module = shift;
  $module =~ s/::/$DIRSEP/g;
  $module .= ".pm";
  if( -r $pwd.$module ) { return $pwd.$module }
  my $libdir;
  foreach $libdir ( @INC ) {
=for debug
    print "searching for '$module' in '$libdir' ...\n";
=cut
    $libdir .= $DIRSEP unless $libdir =~ /$DIRSEP$/;
    if( -r $libdir.$module ) { return $libdir.$module }
  }
  warn "NOT FOUND: '$module' neither in '$pwd' or @INC paths";
}

sub print_INC {
  my $key; # don't assume newer Perl
  foreach $key ( sort keys %INC ) { print "$key => $INC{ $key }\n" };
  print scalar keys %INC, " key(s) found.\n";
}

# BEGIN {
  die "no input, no output\nplease supply argument (MacPerlers drop a file)" unless 
$ARGV[0];

  my $sourcefile = $ARGV[0];
  open S, $sourcefile or die "cannot open '$sourcefile'";
  my $pwd = `pwd`; chop $pwd;
  my $sourcedir;
  if(    $^O =~ /mac/i )        { ( $sourcedir ) = ( $sourcefile =~ m/(.*:).+$/  ) }
  elsif( $^O =~ /unix|linux/i ) { ( $sourcedir ) = ( $sourcefile =~ m/(.*\/).+$/ ) }
  elsif( $^O =~ /win/i )        { ( $sourcedir ) = ( $sourcefile =~ m/(.*\\).+$/ ) }
  else { die "unknown OS" }
  $sourcedir ||= $pwd;
  chdir( $sourcedir ) or die $!;
  eval_use_line( $sourcefile );
  eval_require_line( $sourcefile );
  print_INC();
  close S;

  foreach my $incs ( values %INC ) {
    recurse_required( $incs );
  }
  print "=" x 40, "\n";
  print_INC();
# }

__END__

=head1 NAME

module-finder

=head1 SYNOPSIS

  Mac:
  drop a file onto the droplet

  rest:
  perl module-finder.pl <your-perl-script>

=head1 OPTIONS

none in the moment


=head1 DESCRIPTION

The script tries to find all modules a script would need at runtime and fills
%INC accordingly.

The driving idea is to use those values in compiler like programs,
more specifically in the RuntimeBuilder utility for MacPerl.

First the source script itself is scanned. Lines containing a "use" statement
are evaluated. Lines containing a "require" statement are changed so that
they become a "use" statement and then evaluated. The global hash %INC will
be filled that way.

Second the %INC hash will be scanned. For each entry, i.e. each module,
we search for further "require" statements. If such a statement will be
found "require" is replaced by "use" and another evaluation proceeds.

If now %INC is still growing the newly added module in %INC must be checked
for further "require" statements. This goes on recursively until %INC stops
increasing, thereby ensuring that %INC contains all modules ever needed
at runtime of the source script.

=head1 AUTHOR

Axel Rose, [EMAIL PROTECTED]

idea and first revision October 2001

=head1 BUGS

My POD skip code relies on the hope that each pod section ends with "\n=cut".
I'm not sure whether this is true.

please report found bugs to [EMAIL PROTECTED]

=head1 EXAMPLE

analyzing the simple script:

  use Socket;

module-finder reports:

  Carp.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Carp.pm
  Exporter.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Exporter.pm
  Socket.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Socket.pm
  XSLoader.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:XSLoader.pm
  strict.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:strict.pm
  warnings.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:warnings.pm
  warnings/register.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:warnings:register.pm
  7 key(s) found.
  INFO: new dependencies found in 'Festplatte:Projekte:MacPerl5.6.1b1:lib:Exporter.pm' 
- 'Exporter::Heavy'
  INFO: new dependencies found in 'Festplatte:Projekte:MacPerl5.6.1b1:lib:Carp.pm' - 
'Carp::Heavy'
  INFO: new dependencies found in 'Festplatte:Projekte:MacPerl5.6.1b1:lib:XSLoader.pm' 
- 'DynaLoader'
  ========================================
  AutoLoader.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:AutoLoader.pm
  Carp.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Carp.pm
  Carp/Heavy.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Carp:Heavy.pm
  Config.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Config.pm
  DynaLoader.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:DynaLoader.pm
  Exporter.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Exporter.pm
  Exporter/Heavy.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Exporter:Heavy.pm
  Socket.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:Socket.pm
  XSLoader.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:XSLoader.pm
  strict.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:strict.pm
  vars.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:vars.pm
  warnings.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:warnings.pm
  warnings/register.pm => Festplatte:Projekte:MacPerl5.6.1b1:lib:warnings:register.pm
  13 key(s) found.

Explanation:

  "Exporter" requires "Exporter::Heavy" which then requires "Carp" which requires 
"Carp::Heavy"
  "Exporter/Heavy.pm" is added to %INC by executing 'eval( "use Exporter::Heavy" )'
  "Exporter/Heavy.pm" does "require Carp" a few times which has no effect because
  "Carp.pm" is already in %INC
  when checking "Carp.pm" one would find "require Carp::Heavy"
  "XSLoader" requires "DynaLoader" which requires other already included modules plus 
"AutoLoader"
  but "AutoLoader" doesn't add new modules either

=head1 SEE ALSO

=head1 COPYRIGHT

This program is free software. You may copy or
redistribute it under the same terms as Perl itself.

=head1 VERSION

in development

=cut



--
----------------------------------------------------------------------
Axel Rose, Springer & Jacoby Digital GmbH & Co. KG, mailto:[EMAIL PROTECTED]
  pub PGP key 1024/A21CB825 E0E4 BC69 E001 96E9  2EFD 86CA 9CA1 AAC5
  "... denn alles, was entsteht, ist wert, daß es zugrunde geht ..."

Reply via email to