package ActiveState::RelocateTree; require Exporter; use strict; use Config; use Cwd qw(abs_path getcwd); use File::Basename qw(dirname basename); use File::Copy (); use File::Find; use File::Path; use File::Spec; use vars qw(@ISA @EXPORT_OK $VERSION); @ISA = qw(Exporter); @EXPORT_OK = qw(relocate edit check move_tree spongedir rel2abs abs2rel); $VERSION = '1.04'; my $modifier = $^O eq 'MSWin32' ? '(?i)' : ''; # This variable has to be built up, or this package itself will be relocated. # That should never happen, since the unmodified path is needed by PPM. # Scripts like reloc_perl provide their own default, which will of course get # relocated as wanted. sub spongedir { my %sponges = ( ppm => '/tmp'.'/.ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZpErLZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZperl', thisperl => 'C:\WINNT\TEMP\perl-bivzutncmoaxcfbobsucozjxjjpjdwklrpubatlangukf'.'tmvsdfwpzkzefmytwtpgahnmbuppebbudxbbeyfntqeqcqbsomaoijipodpozapx', ); return $sponges{lc$_[0]}; } sub relocate { my %opt = ( # to => ??? -> you have to provide this one from => $Config{prefix}, quiet => 0, verbose => 0, filelist => undef, ranlib => 1, textonly => 0, savebaks => 0, bak => '.~1~', inplace => 0, killorig => 0, usenlink => 1, @_, ); $opt{search} = $opt{from} unless exists $opt{search}; $opt{replace} = $opt{to} unless exists $opt{replace}; $opt{inplace} = 1 if $opt{to} eq $opt{from}; $opt{verbose} = 0 if $opt{quiet}; unless ($opt{to}) { warn "No `to' path given to relocate(): cannot continue"; return; } # Substitute '/' or '\\' characters with [/\\] if this is Windows. This # allows matching either slashes or backslashes. my $regexp; if ($^O eq 'MSWin32') { my @parts = map { quotemeta } split m#[/\\]#, $opt{search}; $regexp = join '[/\\\\]', @parts; } else { $regexp = quotemeta($opt{search}); } move_tree(@opt{qw(from to killorig verbose usenlink)}) unless $opt{inplace}; my (@bin, @text); { # On HP-UX with pfs_mount, nlink is always 2. local $File::Find::dont_use_nlink = !$opt{usenlink}; find(sub { return if -l; if (-d) { chmod 0755, $_; return; } return unless -f && -s; if (-B) { return if $opt{textonly}; push @bin, $File::Find::name if check($_, $regexp, 1); } else { push @text, $File::Find::name if check($_, $regexp, 0) } }, resolve($opt{to})); } # show affected files print "Configuring Perl installation at $opt{to}\n" if !$opt{quiet} && (@bin || @text); if ($opt{filelist}) { open LOG, "> $opt{filelist}" or die "can't open $opt{filelist}: $!"; print LOG "B $_\n" for @bin; print LOG "T $_\n" for @text; close LOG or die "can't close $opt{filelist}: $!"; } if ($opt{verbose}) { print "Translating $opt{search} to $opt{replace}\n"; print "editing binary file $_\n" for @bin; print "editing text file $_\n" for @text; } # edit files edit($regexp, @opt{qw(search replace bak)}, 0, @text); edit($regexp, @opt{qw(search replace bak)}, 1, @bin); # clobber backups unless ($opt{savebaks}) { print "cleaning out backups\n" if $opt{verbose}; unlink "$_$opt{bak}" for (@text, @bin); } # run ranlib, where appropriate my $rl = $Config{ranlib}; $rl = '' if $rl =~ /^:?\s*$/; if ($rl and $opt{ranlib}) { for (@bin) { if (/\Q$Config{_a}\E$/o) { print "$rl $_\n" if $opt{verbose}; system("$rl $_") == 0 or die "`$rl $_' failed: $?"; } } } } sub binary_pad { my($suffix,$pad_length) = @_; my $pad_char = "\0"; $pad_char = ":" if $^O eq "aix" && index($suffix, ":") > 0; return $suffix . ($pad_char x $pad_length); } sub relocate2 { my $from = shift; my $to = shift; my $sponge = shift || $Config{prefix}; my $reloc_txt = shift || "support/reloc.txt"; my $inplace = $to eq $from; unless ($inplace) { # Find a suitable way to copy print "Copying files to $to..."; # XXX there is a bug somewhere in perl that unsets $@ if # move_tree() below croaks. Preloading Carp::Heavy avoids # this problem. This problem can be reproduced by trying to # install to a non-writable location. Without this hack the # installer will abort, but still report the install as # successful. require Carp::Heavy; move_tree($from, $to, 0, 0, 0); print "done\n"; } # Relocate if (open(my $reloc, $reloc_txt)) { die "Can't relocate to a path longer than " . length($sponge) . " chars" if length($to) > length($sponge); my $pad_length = length($sponge) - length($to); my $binary_pad = "\0" x $pad_length; print "Relocating..."; my $count = 0; local $_; while (<$reloc>) { chomp; my($type, $f) = split(' ', $_, 2); $f = "$to/$f"; #print "Relocating $f...\n"; # ensure that the file is writeable my $old_mode = (stat $f)[2] & 07777; my $mode = $old_mode | 0200; # make writable chmod($mode, $f) if $mode != $old_mode; # update the file open(my $fh, "+<", $f) || die "Can't open $f: $!"; binmode($fh); my $content = do { local $/; <$fh> }; if ($type eq "B") { if ($^O eq "aix") { $content =~ s|\Q$sponge\E([^\0]*)|$to . binary_pad($1, $pad_length)|ge; } else { $content =~ s,\Q$sponge\E([^\0]*),$to$1$binary_pad,go; } } else { $content =~ s,\Q$sponge\E,$to,go; truncate($fh, length($content)) || die "Can't truncate '$f': $!"; } seek($fh, 0, 0) || die "Can't reset file pos on '$f': $!"; print $fh $content; close($fh) || die "Can't write back content to '$f': $!"; # make file unwritable again if it was before the update chmod($old_mode, $f) if $mode != $old_mode; $count++; } print "done ($count files relocated)\n"; } else { die "Can't open '$reloc_txt': $!\n"; } } sub check { my $file = shift; my $re = shift; my $bin = shift; # On Windows we also need to look for escaped backslashes, like # foodir='c:\\perl\\foo'; my $re2 = $re; if ($^O eq "MSWin32") { my $from = '[/\\\\]'; $re2 =~ s[\Q$from\E][\\\\\\\\]g; } local (*F, $_); open F, "< $file" or die "Can't open `$file': $!"; binmode F if $bin; my $mod = $modifier; while () { return 1 if /$mod$re/; return 1 if $^O eq "MSWin32" && /$mod$re2/; } return 0; } sub edit { my $re = shift; my $from = shift; my $dest = shift; my $bak = shift; my $bin = shift; return unless @_; # prevent reading from STDIN my $mod = $modifier; my ($term, $pad); # On Windows we also need to look for escaped backslashes, like # foodir='c:\\perl\\foo'; my $re2 = $re; if ($^O eq "MSWin32") { my $from = '[/\\\\]'; $re2 =~ s[\Q$from\E][\\\\\\\\]g; } if ($bin) { my $d = length $dest; my $f = length $from; # If we're installing into a shorter path, we have to NUL pad the # replacement string. But if we're installing into a longer path, we # have to NUL-pad the search string. if ($d <= $f) { $term = '([^\0]*\0)'; $pad = "\0" x ($f - $d); } else { $term = "([^\0]*\\0)\\0{" . ($d - $f) . "}"; $pad = ''; } } else { $term = '()'; $pad = ''; } local ($_, *INPUT, *OUTPUT); my $old = select(STDOUT); for my $file (@_) { rename($file, "$file$bak") or do { warn "can't rename $file: $!" if $^W; next }; open(INPUT, "< $file$bak") or do { warn "can't open $file$bak: $!" if $^W; next }; open(OUTPUT, "> $file") or do { warn "can't write $file: $!" if $^W; next }; chmod((stat "$file$bak")[2] & 07777, $file); binmode(INPUT), binmode(OUTPUT) if $bin; select(OUTPUT); if ($^O eq 'MSWin32') { while () { if (m[($mod$re$term)]) { # if the string to be modified has backslashes anywhere # in it and has no forward slashes, make the replacement # string backslashed too my $match = $1; my $d = $dest; if ($match =~ m[\\] and $match !~ m[/]) { $d =~ s[/][\\]g; } s[$mod$re$term][$d$1$pad]g; } if (m[($mod$re2$term)]) { (my $d = $dest) =~ s[/][\\\\]g; s[$mod$re2$term][$d$1$pad]g; } print; } } else { while () { s[$mod$re$term][$dest$1$pad]g; print; } } close(OUTPUT); close(INPUT); } select($old); # Unfortunately, this doesn't work in 5.005_03. Oh, how I wish it would just # die once and for all! # local ($_, *ARGV, *ARGVOUT); # local $^I = $bak; # @ARGV = @_; # binmode(ARGV), binmode(ARGVOUT) if $bin; # while (<>) { # s[$mod\Q$from\E$term][$dest$1$pad]g; # print; # close ARGV if eof; # } } sub move_tree { my ($from, $to, $kill, $verbose, $usenlink) = @_; $from = rel2abs(resolve($from)); $to = rel2abs($to); # On HP-UX with pfs_mount, nlink is always 2. local $File::Find::dont_use_nlink = !$usenlink; my @dir_utime_stack; find({ bydepth => 1, wanted => sub { my $src = abs2rel($File::Find::name, $from); my $targ; if (-l) { # Resolve the source link. If it points inside the source tree, # build a similar one which points to the same relative location # in the destination tree. Else, copy it if it points to a file, # else die. my $resolved = resolve($File::Find::name); if ($resolved =~ /^$modifier\Q$from\E/) { # The symlink we create: my $link = File::Spec->catfile($to, $src); my $dir = dirname($link); # where unless (-d $dir) { mkpath($dir, 0, 0777) || die "Can't mkpath $dir: $!"; } # What should be the content of the link? my $rel = abs2rel($resolved, $from); my $dest = abs2rel(File::Spec->catfile($to, $rel), $dir); symlink($dest, $link) || die "Can't create symlink at '$link': $!"; return; } elsif (-d) { die "move_tree: symlink '$File::Find::name' points to a " . "directory '$resolved' outside of '$from'.\n"; } } if (-f) { $targ = File::Spec->catfile($to, $src); my $dir = dirname($targ); unless (-d $dir) { mkpath($dir, 0, 0777) || die "Can't mkpath $dir: $!"; } File::Copy::syscopy($File::Find::name, $targ) || die "Can't copy to '$targ': $!"; } elsif (-d) { $targ = File::Spec->catdir($to, $src); unless (-d $targ) { mkpath($targ, 0, 0777) || die "Can't mkpath $targ: $!"; } } else { die "Can't copy '$File::Find::name': unsupported file type.\n"; } # Preserve file/directory attributes. We expect chown() to fail if # the group is bad, so don't warn about it. This is the same # policy as GNU tar, which silently sets the user/group of # extracted files to the current user's primary uid/gid if the # initial chmod fails. my @stat = stat($File::Find::name) or die "Can't stat $File::Find::name: $!"; my $uid = $> ? -1 : $stat[4]; my $gid = $stat[5]; chown($uid, $gid, $targ); # ignore chmod failures. chmod($stat[2] & 07777, $targ) || die "Can't chmod $File::Find::name: $!"; # Windows does not allow the changing of file times if the file is readonly. utime($stat[8], $stat[9], $targ) || $^O eq "MSWin32" || die "Can't set atime/mtime of $File::Find::name: $!"; }, no_chdir => 1, }, $from); if ($kill) { print "deleting $from\n" if $verbose; rmtree($from, 0, 0); } } { my $rel2abs_test = UNIVERSAL::can("File::Spec", "rel2abs"); my $abs2rel_test = ( UNIVERSAL::can("File::Spec", "abs2rel") && $^O ne 'MSWin32' ); my $symlink_test = eval { symlink('', ''); 1 }; sub resolve { my $l = shift; return $l unless $symlink_test; return $l unless -l $l; my $d = dirname($l); my $v = readlink($l); return rel2abs($v, $d); } sub rel2abs { my ($rel, $relto) = @_; my ($base, $rest); if ($rel2abs_test) { $base = File::Spec->rel2abs(@_); $rest = ''; } else { # Support for 5.005: return $rel if File::Spec->file_name_is_absolute($rel); if (!defined $relto) { $relto = getcwd(); } elsif (!File::Spec->file_name_is_absolute($relto)) { $relto = rel2abs($relto); } ($base, $rest) = (File::Spec->catdir($relto, $rel), ''); } until (-d $base) { $rest = File::Spec->catdir(basename($base), $rest); $base = dirname($base); } return File::Spec->catdir(abs_path($base), $rest) if $base and $rest; return abs_path($base) if $base and not $rest; die "can't absolutize $rel against $relto\n"; } sub abs2rel { return File::Spec->abs2rel(@_) if $abs2rel_test; # Support for 5.005: my $abs = shift; my $from = shift; (my $rel = $abs) =~ s#$modifier^\Q$from\E[\\/]?##; return $rel; } } 1; __END__ =head1 NAME ActiveState::RelocateTree - copy tree substituting paths at the same time =head1 SYNOPSIS use ActiveState::RelocateTree qw(relocate); relocate(from => 'C:\Perl', to => 'D:\lang\perl'); =head1 DESCRIPTION When a perl installation is copied into a new location, some of its files need to be modified accordingly. The C module provide functions that helps you do this. The following functions are provided. None of them are exported by default. =over =item relocate( %options ) This is the main entry point that applications will use. The following options are recognized: =over 4 =item C The tree which must be transformed. Unless the C option is true, it will copy the tree at C to C before transforming it. This option is the only one required. The other options have reasonable defaults, so in most cases this is the only option you need to provide. =item C The path from which to copy the perl tree. Defaults to C<$Config{prefix}>, the home of the currently executing perl interpreter. =item C This is the path which will be searched for and replaced in C. This defaults to the value of C. =item C The replacement value for C. This defaults to the value of C. =item C If the tree at C already exists and you just want to transform it in-situ, use this option. It skips the copying step and just transforms the tree. If C equals C, it is set to true and cannot be unset. Otherwise it defaults to false. =item C If you're really moving the tree, this option will remove C after copying and transforming C. Use with care! Defaults to false. =item C While relocating the tree, relocate() creates a backup file for each file being edited. This option allows you to specify the extension of backup files. Defaults to C<.~1~>. =item C Normally relocate() deletes the backup files before returning. C skips that step, leaving the backup files alone. Defaults to false (backups are deleted). =item C Normally relocate() edits both text and binary files. Text files are replaced using a normal search-and-replace algorithm, but binary files are NULL-padded so that all offsets remain the same. By default, C is false, i.e. relocate() operates on both text and binary files. =item C If C is true, relocate() will call C on binary files which look like library files (have the C<$Config{_a}> extension). Defaults to true. =item C If C is true, relocate() emits warning messages as it performs certain operations. This may be useful for debugging, or for command-line tools, where user feedback is a good thing. =item C Normally, relocate() prints out some status messages even with C disabled. If C is true, all messages (except error messages) are temporarily silenced. This option overrides C, so there isn't much point calling relocate() with both C and C set. By default, C is false. =item C If specified, relocate() will write a list of the files modified to C, one filename per line. The lines are prefixed with "B " for binary files and "T " for text files. =back =item move_tree( $from, $to ) =item move_tree( $from, $to, $delete_after, $verbose ) This function will copy the directory tree at $from to the location $to. If $delete_after is TRUE, then tree at $from will be removed after the copy completes. If $verbose is TRUE, then print a message when deleting the $from tree. =item check( $file, $regexp, $is_binary ) Returns TRUE if there are occurrences of $regexp in $file. It is used by relocate() to search for files which should be edited. If $is_binary is TRUE, then read the file in binmode. =item edit( $regexp, $from, $dest, $bak, $are_binary, @files ) edit() is designed to rip though a set of files, efficiently replacing $from with $dest. It operates on the whole set of files, which all need to be of the same type (binary or text). It accepts the following parameters: =over 4 =item $regexp The regular expression to search for. Matching text will be replaced with $dest. =item $from The path to search for and replace. If $are_binary is true, this is used to calculate the amount of NUL-padding required to preserve the length of strings. It is not used otherwise. =item $dest The replacement string. If $are_binary is true and $dest is shorter than $from, then it inserts a NULL-pad to preserve the original length of the strings. =item $bak The extension to use when storing backup files. =item $are_binary A boolean: if true, the files are edited with binary semantics: the filehandles are set to binmode, and strings are NULL-padded. Otherwise a plain-old substitution occurs. =item @files A list of files to edit. =back =item spongedir( $name ) Returns the spongedir associated with a particular product. The $name is the spongedir you're interested in. It's case-insensitive. The following spongedirs are known: =over 10 =item ppm The sponge directory to be used in PPM packages. =item thisperl The original directory in which this copy of Perl was built. This allows relocate() to detect when a replacement path will not fit into the binary. =back =back =head1 SEE ALSO L =head1 COPYRIGHT Copyright 2002 ActiveState Software Inc. All Rights Reserved. =cut