X-Git-Url: https://mattmccutchen.net/utils/utils.git/blobdiff_plain/40ea9b7868f2b7746e7cbabfba6aba982096392a..273c390351c42303171c25215304d1cfd6ca02d4:/stow diff --git a/stow b/stow new file mode 100755 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 <