Import the remaining utilities.
[utils/utils.git] / stow
diff --git a/stow b/stow
new file mode 100755 (executable)
index 0000000..9cb9ae2
--- /dev/null
+++ b/stow
@@ -0,0 +1,614 @@
+#!/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 <<EOT;
+  -n, --no              Do not actually make changes
+  -c, --conflicts       Scan for conflicts, implies -n
+  -d DIR, --dir=DIR     Set stow dir to DIR (default is current dir)
+  -t DIR, --target=DIR  Set target to DIR (default is parent of stow dir)
+  -v, --verbose[=N]     Increase verboseness (levels are 0,1,2,3;
+                          -v or --verbose adds 1; --verbose=N sets level)
+  -D, --delete          Unstow instead of stow
+  -R, --restow          Restow (like stow -D followed by stow)
+  --poststow-only       Run poststow scripts without (un)stowing any packages
+  -V, --version         Show Stow version number
+  -h, --help            Show this help
+EOT
+  exit($msg ? 1 : 0);
+}
+
+sub version {
+  print "$ProgramName (GNU Stow) version $Version\n";
+  exit(0);
+}
+
+# Local variables:
+# mode: perl
+# End: