#!/usr/bin/perl # GNU Stow - manage the installation of multiple software packages # Copyright (C) 1993, 1994, 1995, 1996 by Bob Glickstein # Copyright (C) 2000,2001 Guillaume Morin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # $Id: stow.in,v 1.8 2002/01/05 11:27:01 gmorin Exp $ # $Source: /cvsroot/stow/stow/stow.in,v $ # $Date: 2002/01/05 11:27:01 $ # $Author: gmorin $ # Add ".dontfold" support. A ".dontfold" file inside a package prevents its # containing directory from being folded even if no other package contributes # to that directory. Note that the ancestor directories up to the package root # should have ".dontfold" files too or they might get folded. One must ensure # that each directory in the target gets a ".dontfold" from at most one package, # or stow will report conflicts. # - Matt 2009-10-19 # Add support for poststow scripts. - Matt 2009-10-20 require 5.005; use POSIX; use File::Basename; $ProgramName = $0; $ProgramName =~ s,.*/,,; $Version = '1.3.3'; $Conflicts = 0; $Delete = 0; $NotReally = 0; $Verbose = 0; $ReportHelp = 0; $Stow = undef; $Target = undef; $Restow = 0; $PoststowOnly = 0; # FIXME: use Getopt::Long while (@ARGV && ($_ = $ARGV[0]) && /^-/) { $opt = $'; shift; last if /^--$/; if ($opt =~ /^-/) { $opt = $'; if ($opt =~ /^no?$/i) { $NotReally = 1; } elsif ($opt =~ /^c(o(n(f(l(i(c(ts?)?)?)?)?)?)?)?$/i) { $Conflicts = 1; $NotReally = 1; } elsif ($opt =~ /^dir?/i) { $remainder = $'; if ($remainder =~ /^=/) { $Stow = $'; # the stuff after the = } else { $Stow = shift; } } elsif ($opt =~ /^t(a(r(g(et?)?)?)?)?/i) { $remainder = $'; if ($remainder =~ /^=/) { $Target = $'; # the stuff after the = } else { $Target = shift; } } elsif ($opt =~ /^verb(o(se?)?)?/i) { $remainder = $'; if ($remainder =~ /^=(\d+)/) { $Verbose = $1; } else { ++$Verbose; } } elsif ($opt =~ /^de(l(e(te?)?)?)?$/i) { $Delete = 1; } elsif ($opt =~ /^r(e(s(t(ow?)?)?)?)?$/i) { $Restow = 1; } elsif ($opt =~ /^p(o(s(t(s(t(o(w(-(o(n(ly?)?)?)?)?)?)?)?)?)?)?)?$/i) { $PoststowOnly = 1; } elsif ($opt =~ /^vers(i(on?)?)?$/i) { &version(); } else { &usage(($opt =~ /^h(e(lp?)?)?$/) ? undef : "unknown or ambiguous option: $opt"); } } else { @opts = split(//, $opt); while ($_ = shift(@opts)) { if ($_ eq 'n') { $NotReally = 1; } elsif ($_ eq 'c') { $Conflicts = 1; $NotReally = 1; } elsif ($_ eq 'd') { $Stow = (join('', @opts) || shift); @opts = (); } elsif ($_ eq 't') { $Target = (join('', @opts) || shift); @opts = (); } elsif ($_ eq 'v') { ++$Verbose; } elsif ($_ eq 'D') { $Delete = 1; } elsif ($_ eq 'R') { $Restow = 1; } elsif ($_ eq 'V') { &version(); } else { &usage(($_ eq 'h') ? undef : "unknown option: $_"); } } } } if ($PoststowOnly) { &usage("--poststow-only should be used by itself") if @ARGV; } else { &usage("No packages named") unless @ARGV; } # Changing dirs helps a lot when soft links are used $current_dir = &getcwd; if ($Stow) { chdir($Stow) || die "Cannot chdir to target tree $Stow ($!)\n"; } # This prevents problems if $Target was supplied as a relative path $Stow = &getcwd; chdir($current_dir) || die "Your directory does not seem to exist anymore ($!)\n"; unless ($Target) { if (basename($Stow) eq 'stow') { $Target = &parent($Stow); } else { die "Not using the parent of the stow directory as the target because the basename of\n" . "the stow directory is not \`stow'. This is a safety feature in case you\n" . "mistakenly run this program in the target directory, to avoid trashing its\n" . "parent. If you are sure you have the right stow directory, specify the target\n" . "with the --target option to bypass this check.\n"; } } chdir($Target) || die "Cannot chdir to target tree $Target ($!)\n"; $Target = &getcwd; foreach $package (@ARGV) { $package =~ s,/+$,,; # delete trailing slashes if ($package =~ m,/,) { die "$ProgramName: slashes not permitted in package names\n"; } } if (!$PoststowOnly && ($Delete || $Restow)) { @Collections = @ARGV; &Unstow('', &RelativePath($Target, $Stow)); } if (!$PoststowOnly && (!$Delete || $Restow)) { foreach $Collection (@ARGV) { warn "Stowing package $Collection...\n" if $Verbose; &StowContents($Collection, &RelativePath($Target, $Stow)); } } if (!$NotReally) { &RunPoststowScripts(); } sub CommonParent { local($dir1, $dir2) = @_; local($result, $x); local(@d1) = split(/\/+/, $dir1); local(@d2) = split(/\/+/, $dir2); while (@d1 && @d2 && (($x = shift(@d1)) eq shift(@d2))) { $result .= "$x/"; } chop($result); $result; } # Find the relative patch between # two paths given as arguments. sub RelativePath { local($a, $b) = @_; local($c) = &CommonParent($a, $b); local(@a) = split(/\/+/, $a); local(@b) = split(/\/+/, $b); local(@c) = split(/\/+/, $c); # if $c == "/something", scalar(@c) >= 2 # but if $c == "/", scalar(@c) == 0 # but we want 1 my $length = scalar(@c) ? scalar(@c) : 1; splice(@a, 0, $length); splice(@b, 0, $length); unshift(@b, (('..') x (@a + 0))); &JoinPaths(@b); } # Basically concatenates the paths given # as arguments sub JoinPaths { local(@paths, @parts); local ($x, $y); local($result) = ''; $result = '/' if ($_[0] =~ /^\//); foreach $x (@_) { @parts = split(/\/+/, $x); foreach $y (@parts) { push(@paths, $y) if ($y ne ""); } } $result .= join('/', @paths); } sub Unstow { local($targetdir, $stow) = @_; local(@contents); local($content); local($linktarget, $stowmember, $collection); local(@stowmember); local($pure, $othercollection) = (1, ''); local($subpure, $subother); local($empty) = (1); local(@puresubdirs); return (0, '') if (&JoinPaths($Target, $targetdir) eq $Stow); return (0, '') if (-e &JoinPaths($Target, $targetdir, '.stow')); warn sprintf("Unstowing in %s\n", &JoinPaths($Target, $targetdir)) if ($Verbose > 1); if (!opendir(DIR, &JoinPaths($Target, $targetdir))) { warn "Warning: $ProgramName: Cannot read directory \"$dir\" ($!). Stow might leave some links. If you think, it does. Rerun Stow with appropriate rights.\n"; } @contents = readdir(DIR); closedir(DIR); foreach $content (@contents) { next if (($content eq '.') || ($content eq '..')); $empty = 0; if (-l &JoinPaths($Target, $targetdir, $content)) { ($linktarget = readlink(&JoinPaths($Target, $targetdir, $content))) || die sprintf("%s: Cannot read link %s (%s)\n", $ProgramName, &JoinPaths($Target, $targetdir, $content), $!); if ($stowmember = &FindStowMember(&JoinPaths($Target, $targetdir), $linktarget)) { @stowmember = split(/\/+/, $stowmember); $collection = shift(@stowmember); if (grep(($collection eq $_), @Collections)) { &DoUnlink(&JoinPaths($Target, $targetdir, $content)); } elsif ($pure) { if ($content eq '.dontfold') { $pure = 0; } elsif ($othercollection) { $pure = 0 if ($collection ne $othercollection); } else { $othercollection = $collection; } } } else { $pure = 0; } } elsif (-d &JoinPaths($Target, $targetdir, $content)) { ($subpure, $subother) = &Unstow(&JoinPaths($targetdir, $content), &JoinPaths('..', $stow)); if ($subpure) { push(@puresubdirs, "$content/$subother"); } if ($pure) { if ($subpure) { if ($othercollection) { if ($subother) { if ($othercollection ne $subother) { $pure = 0; } } } elsif ($subother) { $othercollection = $subother; } } else { $pure = 0; } } } else { $pure = 0; } } # This directory was an initially empty directory therefore # We do not remove it. $pure = 0 if $empty; if ((!$pure || !$targetdir) && @puresubdirs) { &CoalesceTrees($targetdir, $stow, @puresubdirs); } ($pure, $othercollection); } sub CoalesceTrees { local($parent, $stow, @trees) = @_; local($tree, $collection, $x); foreach $x (@trees) { ($tree, $collection) = ($x =~ /^(.*)\/(.*)/); &EmptyTree(&JoinPaths($Target, $parent, $tree)); &DoRmdir(&JoinPaths($Target, $parent, $tree)); if ($collection) { &DoLink(&JoinPaths($stow, $collection, $parent, $tree), &JoinPaths($Target, $parent, $tree)); } } } sub EmptyTree { local($dir) = @_; local(@contents); local($content); opendir(DIR, $dir) || die "$ProgramName: Cannot read directory \"$dir\" ($!)\n"; @contents = readdir(DIR); closedir(DIR); foreach $content (@contents) { next if (($content eq '.') || ($content eq '..')); if (-l &JoinPaths($dir, $content)) { &DoUnlink(&JoinPaths($dir, $content)); } elsif (-d &JoinPaths($dir, $content)) { &EmptyTree(&JoinPaths($dir, $content)); &DoRmdir(&JoinPaths($dir, $content)); } else { &DoUnlink(&JoinPaths($dir, $content)); } } } sub StowContents { local($dir, $stow) = @_; local(@contents); local($content); warn "Stowing contents of $dir\n" if ($Verbose > 1); opendir(DIR, &JoinPaths($Stow, $dir)) || die "$ProgramName: Cannot read directory \"$dir\" ($!)\n"; @contents = readdir(DIR); closedir(DIR); foreach $content (@contents) { next if (($content eq '.') || ($content eq '..')); if (-d &JoinPaths($Stow, $dir, $content)) { &StowDir(&JoinPaths($dir, $content), $stow); } else { &StowNondir(&JoinPaths($dir, $content), $stow); } } } sub StowDir { local($dir, $stow) = @_; local(@dir) = split(/\/+/, $dir); local($collection) = shift(@dir); local($subdir) = join('/', @dir); local($linktarget, $stowsubdir); warn "Stowing directory $dir\n" if ($Verbose > 1); if (-l &JoinPaths($Target, $subdir)) { ($linktarget = readlink(&JoinPaths($Target, $subdir))) || die sprintf("%s: Could not read link %s (%s)\n", $ProgramName, &JoinPaths($Target, $subdir), $!); ($stowsubdir = &FindStowMember(sprintf('%s/%s', $Target, join('/', @dir[0..($#dir - 1)])), $linktarget)) || (&Conflict($dir, $subdir), return); if (-e &JoinPaths($Stow, $stowsubdir)) { if ($stowsubdir eq $dir) { warn sprintf("%s already points to %s\n", &JoinPaths($Target, $subdir), &JoinPaths($Stow, $dir)) if ($Verbose > 2); return; } if (-d &JoinPaths($Stow, $stowsubdir)) { &DoUnlink(&JoinPaths($Target, $subdir)); &DoMkdir(&JoinPaths($Target, $subdir)); &StowContents($stowsubdir, &JoinPaths('..', $stow)); &StowContents($dir, &JoinPaths('..', $stow)); } else { (&Conflict($dir, $subdir), return); } } else { &DoUnlink(&JoinPaths($Target, $subdir)); &StowNewDir($dir, $stow); } } elsif (-e &JoinPaths($Target, $subdir)) { if (-d &JoinPaths($Target, $subdir)) { &StowContents($dir, &JoinPaths('..', $stow)); } else { &Conflict($dir, $subdir); } } else { &StowNewDir($dir, $stow); } } sub StowNewDir { local($dir, $stow) = @_; if (-e &JoinPaths($Stow, $dir, '.dontfold')) { &DoMkdir(&JoinPaths($Target, $subdir)); &StowContents($dir, &JoinPaths('..', $stow)); } else { &DoLink(&JoinPaths($stow, $dir), &JoinPaths($Target, $subdir)); } } sub StowNondir { local($file, $stow) = @_; local(@file) = split(/\/+/, $file); local($collection) = shift(@file); local($subfile) = join('/', @file); local($linktarget, $stowsubfile); if (-l &JoinPaths($Target, $subfile)) { ($linktarget = readlink(&JoinPaths($Target, $subfile))) || die sprintf("%s: Could not read link %s (%s)\n", $ProgramName, &JoinPaths($Target, $subfile), $!); ($stowsubfile = &FindStowMember(sprintf('%s/%s', $Target, join('/', @file[0..($#file - 1)])), $linktarget)) || (&Conflict($file, $subfile), return); if (-e &JoinPaths($Stow, $stowsubfile)) { (&Conflict($file, $subfile), return) unless ($stowsubfile eq $file); warn sprintf("%s already points to %s\n", &JoinPaths($Target, $subfile), &JoinPaths($Stow, $file)) if ($Verbose > 2); } else { &DoUnlink(&JoinPaths($Target, $subfile)); &DoLink(&JoinPaths($stow, $file), &JoinPaths($Target, $subfile)); } } elsif (-e &JoinPaths($Target, $subfile)) { &Conflict($file, $subfile); } else { &DoLink(&JoinPaths($stow, $file), &JoinPaths($Target, $subfile)); } } sub DoUnlink { local($file) = @_; warn "UNLINK $file\n" if $Verbose; (unlink($file) || die "$ProgramName: Could not unlink $file ($!)\n") unless $NotReally; } sub DoRmdir { local($dir) = @_; warn "RMDIR $dir\n" if $Verbose; (rmdir($dir) || die "$ProgramName: Could not rmdir $dir ($!)\n") unless $NotReally; } sub DoLink { local($target, $name) = @_; warn "LINK $name to $target\n" if $Verbose; (symlink($target, $name) || die "$ProgramName: Could not symlink $name to $target ($!)\n") unless $NotReally; } sub DoMkdir { local($dir) = @_; warn "MKDIR $dir\n" if $Verbose; (mkdir($dir, 0777) || die "$ProgramName: Could not make directory $dir ($!)\n") unless $NotReally; } sub Conflict { local($a, $b) = @_; if ($Conflicts) { warn sprintf("CONFLICT: %s vs. %s\n", &JoinPaths($Stow, $a), &JoinPaths($Target, $b)); } else { die sprintf("%s: CONFLICT: %s vs. %s\n", $ProgramName, &JoinPaths($Stow, $a), &JoinPaths($Target, $b)); } } sub FindStowMember { local($start, $path) = @_; local(@x) = split(/\/+/, $start); local(@path) = split(/\/+/, $path); local($x); local(@d) = split(/\/+/, $Stow); while (@path) { $x = shift(@path); if ($x eq '..') { pop(@x); return '' unless @x; } elsif ($x) { push(@x, $x); } } while (@x && @d) { if (($x = shift(@x)) ne shift(@d)) { return ''; } } return '' if @d; join('/', @x); } sub RunPoststowScripts { local(@scripts); local($script); local($psd) = &JoinPaths($Target, 'poststow.d'); if (opendir(PSD, $psd)) { # good } elsif ($! eq 'No such file or directory') { # it doesn't exist, skip this step warn "Poststow script directory does not exist\n" if $Verbose; return; } else { die "$ProgramName: Cannot read poststow script directory ($!)\n"; } warn "Running poststow scripts...\n" if $Verbose; @scripts = readdir(PSD); # XXX Should we define the order? closedir(PSD); foreach $script (@scripts) { next if ($script =~ m/^\./); # catches '.', '..', '.dontfold', and maybe others warn "Running poststow script '$script'...\n" if $Verbose; system(&JoinPaths($psd, $script), $Target) == 0 or warn "Poststow script '$script' failed\n"; } } sub parent { local($path) = join('/', @_); local(@elts) = split(/\/+/, $path); pop(@elts); join('/', @elts); } sub usage { local($msg) = shift; if ($msg) { print "$ProgramName: $msg\n"; } print "$ProgramName (GNU Stow) version $Version\n\n"; print "Usage: $ProgramName [OPTION ...] PACKAGE ...\n"; print <