#!/usr/bin/perl # $File: //member/autrijus/PAR/contrib/procedural_pp/pp_old $ $Author: autrijus $ # $Revision: #1 $ $Change: 10675 $ $DateTime: 2004/05/24 13:22:13 $ vim: expandtab shiftwidth=4 use 5.006; use strict; use warnings; use Config; our ($PARL, $OrigPARL); # bootstrap ourselves on a binary-only install. unless (eval { require PAR; 1 }) { $PARL ||= _can_run("parl$Config{_exe}") or die("Can't find par loader"); exec($PARL, $0, @ARGV); } use Archive::Zip; use Cwd; use ExtUtils::MakeMaker; # just for maybe_command() use File::Basename; use File::Spec; use File::Temp qw(tempfile); use Getopt::Long; use Module::ScanDeps 0.10; use PAR::Filter; our $VERSION = 0.05; $| = 1; $SIG{INT} = sub { exit() }; # exit gracefully and clean up after ourselves. sub opt(*); # imal quoting sub is_win32(); sub vprint($@); our ($Options); our (@Input, $Output); our ($logfh); our ($par_file); our (@SharedLibs); my $dynperl = $Config{useshrplib} && ($Config{useshrplib} ne 'false'); main(); sub main { parse_argv(); check_write($Output); generate_code(); run_code(); _die("XXX: Not reached?"); } ####################################################################### sub compile_par { my ($cfh, $lose); my $root = ''; $root = "$Config{archname}/" if opt('m'); if (opt(S) || opt(p)) { # We need to keep it. if (opt(e) or !@Input) { $par_file = "a.par"; } else { $par_file = $Input[0]; # File off extension if present # hold on: plx is executable; also, careful of ordering! $par_file =~ s/\.(?:p(?:lx|l|h)|m)\z//i; $par_file .= ".par"; } $par_file = $Output if opt(p) && $Output =~ /\.par\z/i; $Output = $par_file if opt(p); check_write($par_file); } else { # Don't need to keep it, be safe with a tempfile. $lose = 1; ($cfh, $par_file) = tempfile("ppXXXXX", SUFFIX => ".par"); close $cfh; # See comment just below } vprint 1, "Writing PAR on $par_file"; my (@modules, @data, @exclude); foreach my $name (@{opt(M) || []}) { _name2moddata($name, \@modules, \@data); } foreach my $name ('PAR', @{opt(X) || []}) { _name2moddata($name, \@exclude, \@exclude); } my %map; unshift @INC, @{opt(I) || []}; unshift @SharedLibs, map _find_shlib($_), @{opt(l) || []}; my %skip = map { $_, 1 } map Module::ScanDeps::_find_in_inc($_), @exclude; my @files = ((map Module::ScanDeps::_find_in_inc($_), @modules), @Input); my $scan_dispatch = ( opt(n) ? \&Module::ScanDeps::scan_deps_runtime : \&Module::ScanDeps::scan_deps ); $scan_dispatch->( rv => \%map, files => \@files, execute => opt(x), compile => opt(c), skip => \%skip, (opt(n)) ? () : ( recurse => 1, first => 1, ), ); # Reset the exclusion list so we won't implicitly ignore files in -M %skip = map { $_, 1 } map Module::ScanDeps::_find_in_inc($_), @exclude; Module::ScanDeps::add_deps( rv => \%map, modules => \@modules, skip => \%skip, ); my %text; $text{$_} = ($map{$_}{type} =~ /^(?:module|autoload)$/) for keys %map; $map{$_} = $map{$_}{file} for keys %map; my %manifest = map { $_ => 1 } ('MANIFEST', 'META.yml'); my $size = 0; my $zip = Archive::Zip->new; my $old_member; if (opt('m') and -e $par_file) { $zip->read($par_file); if ($old_member = $zip->memberNamed( 'MANIFEST' )) { $manifest{$_}++ for grep /^\S/, split(/\n/, $old_member->contents); } else { $old_member = 1; } } my %zip_args = ( 'desiredCompressionMethod' => Archive::Zip::COMPRESSION_DEFLATED(), 'desiredCompressionLevel' => Archive::Zip::COMPRESSION_LEVEL_BEST_COMPRESSION(), ); $zip->addDirectory('', substr($root, 0, -1)) if $root and %map and $] >= 5.008; $zip->addDirectory('', $root.'lib') if %map and $] >= 5.008; my $verbatim = ($ENV{PAR_VERBATIM} || 0); my $mod_filter = PAR::Filter->new( 'PatchContent', @{ opt(F) || ($verbatim ? [] : ['PodStrip']) }, ); foreach my $pfile (sort grep length $map{$_}, keys %map) { next if !opt(B) and ($map{$pfile} eq "$Config{privlib}/$pfile" or $map{$pfile} eq "$Config{archlib}/$pfile"); next unless $zip; vprint 2, "... adding $map{$pfile} as ${root}lib/$pfile"; if ($text{$pfile} or $pfile =~ /utf8_heavy\.pl$/i) { my $content_ref = $mod_filter->apply($map{$pfile}, $pfile); $size += length( $$content_ref ); $zip->addString( $content_ref => $root."lib/$pfile", %zip_args ); } elsif ( basename($map{$pfile}) =~ /^Tk\.dll$/i and opt(i) and eval { require Win32::Exe; 1 } and eval { require Win32::Exe::IconFile; 1 } and $] < 5.008 # XXX - broken on ActivePerl 5.8+ ) { my $tkdll = Win32::Exe->new($map{$pfile}); my $ico = Win32::Exe::IconFile->new(opt(i)); $tkdll->set_icons(scalar $ico->icons); $zip->addString($tkdll->dump => $root."lib/$pfile", %zip_args); } else { $zip->addFile($map{$pfile} => $root."lib/$pfile"); $size += -s $map{$pfile}; } $manifest{$root."lib/$pfile"}++; } @Input = grep !/\.pm\z/i, @Input; $zip->addDirectory('', 'script') if @Input and $] >= 5.008; my $script_filter = PAR::Filter->new( @{ opt(f) } ) if opt(f); foreach my $input (@Input) { my $name = basename($input); $size += -s $input; if ($script_filter) { $zip->addString( $script_filter->apply($input, $name) => "script/$name", %zip_args, ); } else { $zip->addFile($input => "script/$name"); } $manifest{"script/$name"}++; } my $shlib = "shlib/$Config{archname}"; $zip->addDirectory('', $shlib) if @SharedLibs and $] >= 5.008; foreach my $input (@SharedLibs) { next unless -e $input; $size += -s $input; my $name = basename($input); vprint 2, "... adding $input as $shlib/$name"; $zip->addFile($input => "$shlib/$name"); $manifest{"$shlib/$name"}++; } foreach my $input (@data) { unless (-r $input and !-d $input) { warn "'$input' does not exist or is not readable; skipping\n"; next; } my $name = basename($input); $size += -s $input; $zip->addFile($input => $name); $manifest{$name}++; } # Add a script/main.pl except when building PAR file with multiple scripts if (@Input and (@Input == 1 or !opt(p))) { $zip->addString( ((@Input == 1) ? _main_pl_single("script/" . basename($Input[0])) : _main_pl_multi()) => "script/main.pl", %zip_args ); $manifest{"script/main.pl"}++; } my $clean = (opt(C) ? 1 : 0); my $dist_name = (opt(p) ? $par_file : $Output); my $manifest = join("\n", ' ', (sort keys %manifest), q( #
)); my $meta_yaml = << "YAML"; build_requires: {} conflicts: {} dist_name: $dist_name distribution_type: par dynamic_config: 0 generated_by: 'Perl Packager version $VERSION' license: unknown par: clean: $clean signature: '' verbatim: $verbatim version: $PAR::VERSION YAML $size += length($_) for ($manifest, $meta_yaml); vprint 2, "... making $_" for qw(MANIFEST META.yml); $zip->addString($manifest => 'MANIFEST', %zip_args); $zip->addString($meta_yaml => 'META.yml', %zip_args); if ($old_member) { $zip->overwrite; } else { $zip->writeToFileNamed($par_file); } my $newsize = -s $par_file; vprint 2, sprintf( "*** %s: %d bytes read, %d compressed, %2.2d%% saved.\n", $par_file, $size, $newsize, (100 - ($newsize / $size * 100)) ); if ( opt('s') ) { if (eval { require PAR::Dist; require Module::Signature; Module::Signature->VERSION >= 0.25 }) { vprint 0, "Signing $par_file"; PAR::Dist::sign_par($par_file); } else { vprint -1, "*** Signing requires PAR::Dist with Module::Signature 0.25 or later. Skipping"; } } par_to_exe() unless opt(p); if ($lose) { vprint 2, "Unlinking $par_file"; unlink $par_file or _die("Can't unlink $par_file: $!"); } } sub _name2moddata { my ($name, $mod, $dat) = @_; if ($name =~ /^[\w:]+$/) { $name =~ s/::/\//g; push @$mod, "$name.pm"; } elsif ($name =~ /\.(?:pm|ix|al)$/i) { push @$mod, $name; } else { push @$dat, $name; } } sub par_to_exe { my $parl = 'parl'; $parl = 'parldyn' if (opt(d) and $dynperl); $parl .= $Config{_exe}; $parl = 'par.pl' if opt(P); $PARL ||= _can_run($parl, opt(P)) or _die("Can't find par loader"); if ($^O ne 'MSWin32' or opt(p) or opt(P)) { generate_output(); } elsif (!opt(N) and !opt(i)) { generate_output(); fix_console() if opt(g); } elsif (eval { require Win32::Exe; 1 }) { move_parl(); Win32::Exe->new($PARL)->update( icon => opt(i), info => opt(N), ); append_parl(); generate_output(); Win32::Exe->new($Output)->update( icon => opt(i), info => opt(N), ); fix_console(); unlink($PARL); unlink("$PARL.bak"); return; } else { die "--icon and --info support needs Win32::Exe"; } } sub fix_console { return unless opt(g); vprint 1, "Fixing $Output to remove its console window"; strip_console($Output); if ($dynperl and !opt(d)) { # we have a static.exe that needs taking care of. my $buf; open _FH, $OrigPARL || $PARL or die $!; binmode _FH; seek _FH, -8, 2; read _FH, $buf, 8; die unless $buf eq "\nPAR.pm\n"; seek _FH, -12, 2; read _FH, $buf, 4; seek _FH, -12 - unpack("N", $buf) - 4, 2; read _FH, $buf, 4; strip_console($Output, unpack("N", $buf)); } } sub move_parl { $OrigPARL = $PARL; my $cfh; local $/; open _FH, $PARL or die $!; binmode(_FH); ($cfh, $PARL) = tempfile("parlXXXX", SUFFIX => ".exe", UNLINK => 1); binmode($cfh); print $cfh <_FH>; close $cfh; } sub append_parl { my $buf; seek _FH, -8, 2; read _FH, $buf, 8; die unless $buf eq "\nPAR.pm\n"; seek _FH, -12, 2; read _FH, $buf, 4; seek _FH, -12 - unpack("N", $buf), 2; open my $cfh, ">>", $PARL or die $!; binmode($cfh); print $cfh <_FH>; close $cfh; } sub generate_output { my @args = ('-B', "-O$Output", $par_file); unshift @args, '-q' unless opt(v); if (opt(L)) { unshift @args, "-L".opt(L); } if (opt(P)) { unshift @args, $PARL; $PARL = $^X; } vprint 0, "Running $PARL @args"; system($PARL, @args); } sub strip_console { my $file = shift; my $preoff = shift || 0; my ($record, $magic, $signature, $offset, $size); open my $exe, "+< $file" or die "Cannot open $file: $!\n"; binmode $exe; seek $exe, $preoff, 0; # read IMAGE_DOS_HEADER structure read $exe, $record, 64; ($magic, $offset) = unpack "Sx58L", $record; die "$ARGV[0] is not an MSDOS executable file.\n" unless $magic == 0x5a4d; # "MZ" # read signature, IMAGE_FILE_HEADER and first WORD of IMAGE_OPTIONAL_HEADER seek $exe, $preoff + $offset, 0; read $exe, $record, 4+20+2; ($signature,$size,$magic) = unpack "Lx16Sx2S", $record; die "PE header not found" unless $signature == 0x4550; # "PE\0\0" die "Optional header is neither in NT32 nor in NT64 format" unless ($size == 224 && $magic == 0x10b) || # IMAGE_NT_OPTIONAL_HDR32_MAGIC ($size == 240 && $magic == 0x20b); # IMAGE_NT_OPTIONAL_HDR64_MAGIC # Offset 68 in the IMAGE_OPTIONAL_HEADER(32|64) is the 16 bit subsystem code seek $exe, $preoff + $offset+4+20+68, 0; print $exe pack "S", 2; # IMAGE_WINDOWS close $exe; } sub generate_code { vprint 0, "Packing @Input"; if (check_par($Input[0])) { # invoked as "pp foo.par" - never unlink it $par_file = $Input[0]; $Options->{S} = 1; par_to_exe(); } else { compile_par(); } exit(0) if (!opt('r')); } sub run_code { $Output = File::Spec->catfile(".", $Output); my @Loader = (); push @Loader, $^X if opt('P'); push @Loader, $^X, "-MPAR" if opt('p'); vprint 0, "Running @Loader $Output @ARGV"; system(@Loader, $Output, @ARGV); exit(0); } sub vprint ($@) { my $level = shift; my $msg = "@_"; $msg .= "\n" unless substr($msg, -1) eq "\n"; if (opt(v) > $level) { print "$0: $msg" if !opt(L); print $logfh "$0: $msg" if opt(L); } } sub parse_argv { Getopt::Long::Configure("no_ignore_case"); # no difference in exists and defined for %ENV; also, a "0" # argument or a "" would not help pp, so skip unshift @ARGV, split ' ', $ENV{PP_OPTS} if $ENV{PP_OPTS}; $Options = {}; Getopt::Long::GetOptions( $Options, 'M|add:s@', # Include modules 'B|bundle', # Bundle core modules 'C|clean', # Clean up temporary files 'c|compile', # Compile code to get dependencies 'd|dependent', # Do not package libperl 'e|eval:s', # Packing one-liner 'x|execute', # Execute code to get dependencies 'X|exclude:s@', # Exclude modules 'f|filter:s@', # Input filters for scripts 'g|gui', # No console window 'h|help', # Help me 'i|icon:s', # Icon file 'N|info:s@', # Executable header info 'I|lib:s@', # Include directories (for perl) 'l|link:s@', # Include additional shared libraries 'L|log:s', # Where to log packaging process information 'F|modfilter:s@', # Input filter for perl modules 'm|multiarch', # Build multiarch PAR file 'n|noscan', # Skips static scanning 'o|output:s', # Output file 'p|par', # Generate PAR only 'P|perlscript', # Generate perl script 'r|run', # Run the resulting executable 'S|save', # Preserve intermediate PAR files 's|sign', # Sign PAR files 'v|verbose:i', # Verbosity level 'vv', # Verbosity level 2 'vvv', # Verbosity level 3 'V|version', # Show version ); $Options->{p} = 1 if opt('m'); $Options->{v} = 1 if exists $Options->{v} and !$Options->{v}; $Options->{v} = 2 if exists $Options->{vv}; $Options->{v} = 3 if exists $Options->{vvv}; $Options->{B} = 1 unless opt(p) or opt(P); helpme() if opt(h); # And exit show_version() if opt(V); # And exit $Output = opt(o) || a_out(); open $logfh, '>>', opt(L) or die ("XXX: Cannot open log: $!") if (opt(L)); if (opt(e)) { warn "$0: using -e 'code' as input file, ignoring @ARGV\n" if @ARGV and !opt(r); my ($fh, $fake_input) = tempfile("ppXXXXX", SUFFIX => ".pl", UNLINK => 1); print $fh $Options->{e}; close $fh; @Input = $fake_input; } else { @Input = shift @ARGV if @ARGV; _die("$0: No input files specified\n") unless @Input or opt(M); push @Input, @ARGV if @ARGV and !opt(r); check_read(@Input) if @Input; check_perl(@Input) if @Input; sanity_check(); } } sub a_out { return 'a' . ( opt(p) ? '.par' : opt(P) ? '.pl' : ($Config{_exe} || '.out') ); } sub opt(*) { my $opt = shift; return exists($Options->{$opt}) && ($Options->{$opt} || 0); } sub sanity_check { # Check the input and output files make sense, are read/writable. if ("@Input" eq $Output) { my $a_out = a_out(); if ("@Input" eq $a_out) { _die("$0: Packing $a_out to itself is probably not what you want to do.\n"); # You fully deserve what you get now. No you *don't*. typos happen. } else { warn "$0: Will not write output on top of input file, ", "packing to $a_out instead\n"; $Output = $a_out; } } } sub check_read { foreach my $file (@_) { unless (-r $file) { _die("$0: Input file $file is a directory, not a file\n") if -d _; unless (-e _) { _die("$0: Input file $file was not found\n"); } else { _die("$0: Cannot read input file $file: $!\n"); } } unless (-f _) { # XXX: die? don't try this on /dev/tty warn "$0: WARNING: input $file is not a plain file\n"; } } } sub check_write { foreach my $file (@_) { if (-d $file) { _die("$0: Cannot write on $file, is a directory\n"); } if (-e _) { _die("$0: Cannot write on $file: $!\n") unless -w _; } unless (-w cwd()) { _die("$0: Cannot write in this directory: $!\n"); } } } sub check_perl { my $file = shift; return if check_par($file); unless (-T $file) { warn "$0: Binary `$file' sure doesn't smell like perl source!\n"; if (my $file_checker = _can_run("file")) { print "Checking file type... "; system($file_checker, $file); } _die("Please try a perlier file!\n"); } open(my $handle, "<", $file) or _die("XXX: Can't open $file: $!"); local $_ = <$handle>; if (/^#!/ && !/perl/) { _die("$0: $file is a ", /^#!\s*(\S+)/, " script, not perl\n"); } } sub check_par { my $file = shift or return; open(my $handle, "<", $file) or _die("XXX: Can't open $file: $!"); binmode($handle); local $/ = \4; return (<$handle> eq "PK\x03\x04"); } sub helpme { print "Perl Packager, version $VERSION (PAR version $PAR::VERSION)\n\n"; { no warnings; exec "pod2usage $0"; exec "perldoc $0"; exec "pod2text $0"; } } sub show_version { print << "."; Perl Packager, version $VERSION (PAR version $PAR::VERSION) Copyright 2002, 2003, 2004 by Autrijus Tang