1795 lines
56 KiB
Text
1795 lines
56 KiB
Text
|
#!@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
|
||
|
# Copyright (C) 2007 Kahlil Hodgson
|
||
|
#
|
||
|
# 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.
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
|
||
|
require 5.005;
|
||
|
use POSIX qw(getcwd);
|
||
|
use Getopt::Long;
|
||
|
|
||
|
my $Version = '@VERSION@';
|
||
|
my $ProgramName = $0;
|
||
|
$ProgramName =~ s{.*/}{};
|
||
|
|
||
|
# Verbosity rules:
|
||
|
#
|
||
|
# 0: errors only
|
||
|
# > 0: print operations: LINK/UNLINK/MKDIR/RMDIR
|
||
|
# > 1: print trace: stow/unstow package/contents/node
|
||
|
# > 2: print trace detail: "_this_ already points to _that_"
|
||
|
#
|
||
|
# All output (except for version() and usage()) is to stderr to preserve
|
||
|
# backward compatibility.
|
||
|
|
||
|
# These are the defaults for command line options
|
||
|
our %Option = (
|
||
|
help => 0,
|
||
|
conflicts => 0,
|
||
|
action => 'stow',
|
||
|
simulate => 0,
|
||
|
verbose => 0,
|
||
|
paranoid => 0,
|
||
|
dir => undef,
|
||
|
target => undef,
|
||
|
ignore => [],
|
||
|
override => [],
|
||
|
defer => [],
|
||
|
);
|
||
|
|
||
|
# This becomes static after option processing
|
||
|
our $Stow_Path; # only use in main loop and find_stowed_path()
|
||
|
|
||
|
# Store conflicts during pre-processing
|
||
|
our @Conflicts = ();
|
||
|
|
||
|
# Store command line packges to stow (-S and -R)
|
||
|
our @Pkgs_To_Stow = ();
|
||
|
|
||
|
# Store command line packages to unstow (-D and -R)
|
||
|
our @Pkgs_To_Delete = ();
|
||
|
|
||
|
# The following structures are used by the abstractions that allow us to
|
||
|
# defer operating on the filesystem until after all potential conflcits have
|
||
|
# been assessed.
|
||
|
|
||
|
# our @Tasks: list of operations to be performed (in order)
|
||
|
# each element is a hash ref of the form
|
||
|
# {
|
||
|
# action => ...
|
||
|
# type => ...
|
||
|
# path => ... (unique)
|
||
|
# source => ... (only for links)
|
||
|
# }
|
||
|
our @Tasks = ();
|
||
|
|
||
|
# my %Dir_Task_For: map a path to the corresponding directory task reference
|
||
|
# This structurew allows us to quickly determine if a path has an existing
|
||
|
# directory task associated with it.
|
||
|
our %Dir_Task_For = ();
|
||
|
|
||
|
# my %Link_Task_For: map a path to the corresponding directory task reference
|
||
|
# This structurew allows us to quickly determine if a path has an existing
|
||
|
# directory task associated with it.
|
||
|
our %Link_Task_For = ();
|
||
|
|
||
|
# NB: directory tasks and link tasks are NOT mutually exclusive
|
||
|
|
||
|
# put the main loop in this block so we can load the
|
||
|
# rest of the code as a module for testing
|
||
|
if ( not caller() ) {
|
||
|
|
||
|
process_options();
|
||
|
set_stow_path();
|
||
|
|
||
|
# current dir is now the target directory
|
||
|
|
||
|
for my $package (@Pkgs_To_Delete) {
|
||
|
if (not -d join_paths($Stow_Path,$package)) {
|
||
|
error("The given package name ($package) is not in your stow path");
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Unstowing package $package...\n";
|
||
|
}
|
||
|
if ($Option{'compat'}) {
|
||
|
unstow_contents_orig(
|
||
|
join_paths($Stow_Path,$package), # path to package
|
||
|
'', # target is current_dir
|
||
|
);
|
||
|
}
|
||
|
else {
|
||
|
unstow_contents(
|
||
|
join_paths($Stow_Path,$package), # path to package
|
||
|
'', # target is current_dir
|
||
|
);
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Unstowing package $package...done\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for my $package (@Pkgs_To_Stow) {
|
||
|
if (not -d join_paths($Stow_Path,$package)) {
|
||
|
error("The given package name ($package) is not in your stow path");
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Stowing package $package...\n";
|
||
|
}
|
||
|
stow_contents(
|
||
|
join_paths($Stow_Path,$package), # path package
|
||
|
'', # target is current dir
|
||
|
join_paths($Stow_Path,$package), # source from target
|
||
|
);
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Stowing package $package...done\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# --verbose: tell me what you are planning to do
|
||
|
# --simulate: don't execute planned operations
|
||
|
# --conflicts: just list any detected conflicts
|
||
|
|
||
|
if (scalar @Conflicts) {
|
||
|
warn "WARNING: conflicts detected.\n";
|
||
|
if ($Option{'conflicts'}) {
|
||
|
map { warn $_ } @Conflicts;
|
||
|
}
|
||
|
warn "WARNING: all operations aborted.\n";
|
||
|
}
|
||
|
else {
|
||
|
process_tasks();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : process_options()
|
||
|
# Purpose : parse command line options and update the %Option hash
|
||
|
# Parameters: none
|
||
|
# Returns : n/a
|
||
|
# Throws : a fatal error if a bad command line option is given
|
||
|
# Comments : checks @ARGV for valid package names
|
||
|
#============================================================================
|
||
|
sub process_options {
|
||
|
|
||
|
get_defaults();
|
||
|
#$,="\n"; print @ARGV,"\n"; # for debugging rc file
|
||
|
|
||
|
Getopt::Long::config('no_ignore_case', 'bundling', 'permute');
|
||
|
GetOptions(
|
||
|
'v' => sub { $Option{'verbose'}++ },
|
||
|
'verbose=s' => sub { $Option{'verbose'} = $_[1] },
|
||
|
'h|help' => sub { $Option{'help'} = '1' },
|
||
|
'n|no|simulate' => sub { $Option{'simulate'} = '1' },
|
||
|
'c|conflicts' => sub { $Option{'conflicts'} = '1' },
|
||
|
'V|version' => sub { $Option{'version'} = '1' },
|
||
|
'p|compat' => sub { $Option{'compat'} = '1' },
|
||
|
'd|dir=s' => sub { $Option{'dir'} = $_[1] },
|
||
|
't|target=s' => sub { $Option{'target'} = $_[1] },
|
||
|
|
||
|
# clean and pre-compile any regex's at parse time
|
||
|
'ignore=s' =>
|
||
|
sub {
|
||
|
my $regex = strip_quotes($_[1]);
|
||
|
push @{$Option{'ignore'}}, qr($regex\z)
|
||
|
},
|
||
|
|
||
|
'override=s' =>
|
||
|
sub {
|
||
|
my $regex = strip_quotes($_[1]);
|
||
|
push @{$Option{'override'}}, qr(\A$regex)
|
||
|
},
|
||
|
|
||
|
'defer=s' =>
|
||
|
sub {
|
||
|
my $regex = strip_quotes($_[1]);
|
||
|
push @{$Option{'defer'}}, qr(\A$regex) ;
|
||
|
},
|
||
|
|
||
|
# a little craziness so we can do different actions on the same line:
|
||
|
# a -D, -S, or -R changes the action that will be performed on the
|
||
|
# package arguments that follow it.
|
||
|
'D|delete' => sub { $Option{'action'} = 'delete' },
|
||
|
'S|stow' => sub { $Option{'action'} = 'stow' },
|
||
|
'R|restow' => sub { $Option{'action'} = 'restow' },
|
||
|
'<>' =>
|
||
|
sub {
|
||
|
if ($Option{'action'} eq 'restow') {
|
||
|
push @Pkgs_To_Delete, $_[0];
|
||
|
push @Pkgs_To_Stow, $_[0];
|
||
|
}
|
||
|
elsif ($Option{'action'} eq 'delete') {
|
||
|
push @Pkgs_To_Delete, $_[0];
|
||
|
}
|
||
|
else {
|
||
|
push @Pkgs_To_Stow, $_[0];
|
||
|
}
|
||
|
},
|
||
|
) or usage();
|
||
|
|
||
|
#print "$Option{'dir'}\n"; print "$Option{'target'}\n"; exit;
|
||
|
|
||
|
# clean any leading and trailing whitespace in paths
|
||
|
if ($Option{'dir'}) {
|
||
|
$Option{'dir'} =~ s/\A +//;
|
||
|
$Option{'dir'} =~ s/ +\z//;
|
||
|
}
|
||
|
if ($Option{'target'}) {
|
||
|
$Option{'target'} =~ s/\A +//;
|
||
|
$Option{'target'} =~ s/ +\z//;
|
||
|
}
|
||
|
|
||
|
if ($Option{'help'}) {
|
||
|
usage();
|
||
|
}
|
||
|
if ($Option{'version'}) {
|
||
|
version();
|
||
|
}
|
||
|
if ($Option{'conflicts'}) {
|
||
|
$Option{'simulate'} = 1;
|
||
|
}
|
||
|
|
||
|
if (not scalar @Pkgs_To_Stow and not scalar @Pkgs_To_Delete ) {
|
||
|
usage("No packages named");
|
||
|
}
|
||
|
|
||
|
# check package arguments
|
||
|
for my $package ( (@Pkgs_To_Stow, @Pkgs_To_Delete) ) {
|
||
|
$package =~ s{/+$}{}; # delete trailing slashes
|
||
|
if ( $package =~ m{/} ) {
|
||
|
error("Slashes are not permitted in package names");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : get_defaults()
|
||
|
# Purpose : search for default settings in any .stow files
|
||
|
# Parameters: none
|
||
|
# Returns : n/a
|
||
|
# Throws : no exceptions
|
||
|
# Comments : prepends the contents '~/.stowrc' and '.stowrc' to the command
|
||
|
# : line so they get parsed just like noremal arguments. (This was
|
||
|
# : hacked in so that Emil and I could set different preferences).
|
||
|
#=============================================================================
|
||
|
sub get_defaults {
|
||
|
|
||
|
my @defaults = ();
|
||
|
for my $file ($ENV{'HOME'}.'/.stowrc','.stowrc') {
|
||
|
if (-r $file ) {
|
||
|
warn "Loading defaults from $file\n";
|
||
|
open my $FILE, '<', $file
|
||
|
or die "Could not open $file for reading\n";
|
||
|
while (my $line = <$FILE> ){
|
||
|
chomp $line;
|
||
|
push @defaults, split " ", $line;
|
||
|
}
|
||
|
close $FILE or die "Could not close open file: $file\n";
|
||
|
}
|
||
|
}
|
||
|
# doing this inline does not seem to work
|
||
|
unshift @ARGV, @defaults;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : usage()
|
||
|
# Purpose : print program usage message and exit
|
||
|
# Parameters: msg => string to prepend to the usage message
|
||
|
# Returns : n/a
|
||
|
# Throws : n/a
|
||
|
# Comments : if 'msg' is given, then exit with non-zero status
|
||
|
#============================================================================
|
||
|
sub usage {
|
||
|
my ($msg) = @_;
|
||
|
|
||
|
if ($msg) {
|
||
|
print "$ProgramName: $msg\n\n";
|
||
|
}
|
||
|
|
||
|
print <<"EOT";
|
||
|
$ProgramName (GNU Stow) version $Version
|
||
|
|
||
|
SYNOPSIS:
|
||
|
|
||
|
$ProgramName [OPTION ...] [-D|-S|-R] PACKAGE ... [-D|-S|-R] PACKAGE ...
|
||
|
|
||
|
OPTIONS:
|
||
|
|
||
|
-n, --no Do not actually make any filesystem changes
|
||
|
-c, --conflicts Scan for and print any 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 verbosity (levels are 0,1,2,3;
|
||
|
-v or --verbose adds 1; --verbose=N sets level)
|
||
|
|
||
|
-S, --stow Stow the package names that follow this option
|
||
|
-D, --delete Unstow the package names that follow this option
|
||
|
-R, --restow Restow (like stow -D followed by stow -S)
|
||
|
-p, --compat use legacy algorithm for unstowing
|
||
|
|
||
|
--ignore=REGEX ignore files ending in this perl regex
|
||
|
--defer=REGEX defer stowing files begining with this perl regex
|
||
|
if the file is already stowed to another package
|
||
|
--override=REGEX force stowing files begining with this perl regex
|
||
|
if the file is already stowed to another package
|
||
|
-V, --version Show stow version number
|
||
|
-h, --help Show this help
|
||
|
EOT
|
||
|
exit( $msg ? 1 : 0 );
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : set_stow_path()
|
||
|
# Purpose : find the relative path to the stow directory
|
||
|
# Parameters: none
|
||
|
# Returns : a relative path
|
||
|
# Throws : fatal error if either default directories or those set by the
|
||
|
# : the command line flags are not valid.
|
||
|
# Comments : This sets the current working directory to $Option{target}
|
||
|
#============================================================================
|
||
|
sub set_stow_path {
|
||
|
|
||
|
# Changing dirs helps a lot when soft links are used
|
||
|
# Also prevents problems when 'stow_dir' or 'target' are
|
||
|
# supplied as relative paths (FIXME: examples?)
|
||
|
|
||
|
my $current_dir = getcwd();
|
||
|
|
||
|
# default stow dir is the current directory
|
||
|
if (not $Option{'dir'} ) {
|
||
|
$Option{'dir'} = getcwd();
|
||
|
}
|
||
|
if (not chdir($Option{'dir'})) {
|
||
|
error("Cannot chdir to target tree: '$Option{'dir'}'");
|
||
|
}
|
||
|
my $stow_dir = getcwd();
|
||
|
|
||
|
# back to start in case target is relative
|
||
|
if (not chdir($current_dir)) {
|
||
|
error("Your directory does not seem to exist anymore");
|
||
|
}
|
||
|
|
||
|
# default target is the parent of the stow directory
|
||
|
if (not $Option{'target'}) {
|
||
|
$Option{'target'} = parent($Option{'dir'});
|
||
|
}
|
||
|
if (not chdir($Option{'target'})) {
|
||
|
error("Cannot chdir to target tree: $Option{'target'}");
|
||
|
}
|
||
|
|
||
|
# set our one global
|
||
|
$Stow_Path = relative_path(getcwd(),$stow_dir);
|
||
|
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "current dir is ".getcwd()."\n";
|
||
|
warn "stow dir path is $Stow_Path\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : stow_contents()
|
||
|
# Purpose : stow the contents of the given directory
|
||
|
# Parameters: $path => relative path to source dir from current directory
|
||
|
# : $source => relative path to symlink source from the dir of target
|
||
|
# : $target => relative path to symlink target from the current directory
|
||
|
# Returns : n/a
|
||
|
# Throws : a fatal error if directory cannot be read
|
||
|
# Comments : stow_node() and stow_contents() are mutually recursive
|
||
|
# : $source and $target are used for creating the symlink
|
||
|
# : $path is used for folding/unfolding trees as necessary
|
||
|
#============================================================================
|
||
|
sub stow_contents {
|
||
|
|
||
|
my ($path, $target, $source) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 1){
|
||
|
warn "Stowing contents of $path\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- $target => $source\n";
|
||
|
}
|
||
|
|
||
|
if (not -d $path) {
|
||
|
error("stow_contents() called on a non-directory: $path");
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $path
|
||
|
or error("cannot read directory: $path");
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
next NODE if ignore($node);
|
||
|
stow_node(
|
||
|
join_paths($path, $node), # path
|
||
|
join_paths($target,$node), # target
|
||
|
join_paths($source,$node), # source
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : stow_node()
|
||
|
# Purpose : stow the given node
|
||
|
# Parameters: $path => realtive path to source node from the current directory
|
||
|
# : $target => realtive path to symlink target from the current directory
|
||
|
# : $source => realtive path to symlink source from the dir of target
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal exception if a conflict arises
|
||
|
# Comments : stow_node() and stow_contents() are mutually recursive
|
||
|
# : $source and $target are used for creating the symlink
|
||
|
# : $path is used for folding/unfolding trees as necessary
|
||
|
#============================================================================
|
||
|
sub stow_node {
|
||
|
|
||
|
my ($path, $target, $source) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Stowing $path\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- $target => $source\n";
|
||
|
}
|
||
|
|
||
|
# don't try to stow absolute symlinks (they cant be unstowed)
|
||
|
if (-l $source) {
|
||
|
my $second_source = read_a_link($source);
|
||
|
if ($second_source =~ m{\A/} ) {
|
||
|
conflict("source is an absolute symlink $source => $second_source");
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "absolute symlinks cannot be unstowed";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# does the target already exist?
|
||
|
if (is_a_link($target)) {
|
||
|
|
||
|
# where is the link pointing?
|
||
|
my $old_source = read_a_link($target);
|
||
|
if (not $old_source) {
|
||
|
error("Could not read link: $target");
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- Evaluate existing link: $target => $old_source\n";
|
||
|
}
|
||
|
|
||
|
# does it point to a node under our stow directory?
|
||
|
my $old_path = find_stowed_path($target, $old_source);
|
||
|
if (not $old_path) {
|
||
|
conflict("existing target is not owned by stow: $target");
|
||
|
return; # XXX #
|
||
|
}
|
||
|
|
||
|
# does the existing $target actually point to anything?
|
||
|
if (is_a_node($old_path)) {
|
||
|
if ($old_source eq $source) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- Skipping $target as it already points to $source\n";
|
||
|
}
|
||
|
}
|
||
|
elsif (defer($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- deferring installation of: $target\n";
|
||
|
}
|
||
|
}
|
||
|
elsif (override($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- overriding installation of: $target\n";
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
do_link($source,$target);
|
||
|
}
|
||
|
elsif (is_a_dir(join_paths(parent($target),$old_source)) &&
|
||
|
is_a_dir(join_paths(parent($target),$source)) ) {
|
||
|
|
||
|
# if the existing link points to a directory,
|
||
|
# and the proposed new link points to a directory,
|
||
|
# then we can unfold the tree at that point
|
||
|
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- Unfolding $target\n";
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
do_mkdir($target);
|
||
|
stow_contents($old_path, $target, join_paths('..',$old_source));
|
||
|
stow_contents($path, $target, join_paths('..',$source));
|
||
|
}
|
||
|
else {
|
||
|
conflict(
|
||
|
q{existing target is stowed to a different package: %s => %s},
|
||
|
$target,
|
||
|
$old_source,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
# the existing link is invalid, so replace it with a good link
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- replacing invalid link: $path\n";
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
do_link($source, $target);
|
||
|
}
|
||
|
}
|
||
|
elsif (is_a_node($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn("--- Evaluate existing node: $target\n");
|
||
|
}
|
||
|
if (is_a_dir($target)) {
|
||
|
stow_contents($path, $target, join_paths('..',$source));
|
||
|
}
|
||
|
else {
|
||
|
conflict(
|
||
|
qq{existing target is neither a link nor a directory: $target}
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
do_link($source, $target);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : unstow_contents_orig()
|
||
|
# Purpose : unstow the contents of the given directory
|
||
|
# Parameters: $path => relative path to source dir from current directory
|
||
|
# : $target => relative path to symlink target from the current directory
|
||
|
# Returns : n/a
|
||
|
# Throws : a fatal error if directory cannot be read
|
||
|
# Comments : unstow_node() and unstow_contents() are mutually recursive
|
||
|
# : Here we traverse the target tree, rather than the source tree.
|
||
|
#============================================================================
|
||
|
sub unstow_contents_orig {
|
||
|
|
||
|
my ($path, $target) = @_;
|
||
|
|
||
|
# don't try to remove anything under a stow directory
|
||
|
if ($target eq $Stow_Path or -e "$target/.stow" or -e "$target/.nonstow") {
|
||
|
return;
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1){
|
||
|
warn "Unstowing in $target\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- path is $path\n";
|
||
|
}
|
||
|
if (not -d $target) {
|
||
|
error("unstow_contents() called on a non-directory: $target");
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $target
|
||
|
or error("cannot read directory: $target");
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
next NODE if ignore($node);
|
||
|
unstow_node_orig(
|
||
|
join_paths($path, $node), # path
|
||
|
join_paths($target, $node), # target
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : unstow_node_orig()
|
||
|
# Purpose : unstow the given node
|
||
|
# Parameters: $path => relative path to source node from the current directory
|
||
|
# : $target => relative path to symlink target from the current directory
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal error if a conflict arises
|
||
|
# Comments : unstow_node() and unstow_contents() are mutually recursive
|
||
|
#============================================================================
|
||
|
sub unstow_node_orig {
|
||
|
|
||
|
my ($path, $target) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Unstowing $target\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- path is $path\n";
|
||
|
}
|
||
|
|
||
|
# does the target exist
|
||
|
if (is_a_link($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn("Evaluate existing link: $target\n");
|
||
|
}
|
||
|
|
||
|
# where is the link pointing?
|
||
|
my $old_source = read_a_link($target);
|
||
|
if (not $old_source) {
|
||
|
error("Could not read link: $target");
|
||
|
}
|
||
|
|
||
|
# does it point to a node under our stow directory?
|
||
|
my $old_path = find_stowed_path($target, $old_source);
|
||
|
if (not $old_path) {
|
||
|
# skip links not owned by stow
|
||
|
return; # XXX #
|
||
|
}
|
||
|
|
||
|
# does the existing $target actually point to anything
|
||
|
if (-e $old_path) {
|
||
|
# does link points to the right place
|
||
|
if ($old_path eq $path) {
|
||
|
do_unlink($target);
|
||
|
}
|
||
|
elsif (override($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn("--- overriding installation of: $target\n");
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
}
|
||
|
# else leave it alone
|
||
|
}
|
||
|
else {
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- removing invalid link into a stow directory: $path\n";
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
}
|
||
|
}
|
||
|
elsif (-d $target) {
|
||
|
unstow_contents_orig($path, $target);
|
||
|
|
||
|
# this action may have made the parent directory foldable
|
||
|
if (my $parent = foldable($target)) {
|
||
|
fold_tree($target,$parent);
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : unstow_contents()
|
||
|
# Purpose : unstow the contents of the given directory
|
||
|
# Parameters: $path => relative path to source dir from current directory
|
||
|
# : $target => relative path to symlink target from the current directory
|
||
|
# Returns : n/a
|
||
|
# Throws : a fatal error if directory cannot be read
|
||
|
# Comments : unstow_node() and unstow_contents() are mutually recursive
|
||
|
# : Here we traverse the target tree, rather than the source tree.
|
||
|
#============================================================================
|
||
|
sub unstow_contents {
|
||
|
|
||
|
my ($path, $target) = @_;
|
||
|
|
||
|
# don't try to remove anything under a stow directory
|
||
|
if ($target eq $Stow_Path or -e "$target/.stow") {
|
||
|
return;
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1){
|
||
|
warn "Unstowing in $target\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- path is $path\n";
|
||
|
}
|
||
|
if (not -d $path) {
|
||
|
error("unstow_contents() called on a non-directory: $path");
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $path
|
||
|
or error("cannot read directory: $path");
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
next NODE if ignore($node);
|
||
|
unstow_node(
|
||
|
join_paths($path, $node), # path
|
||
|
join_paths($target, $node), # target
|
||
|
);
|
||
|
}
|
||
|
if (-d $target) {
|
||
|
cleanup_invalid_links($target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : unstow_node()
|
||
|
# Purpose : unstow the given node
|
||
|
# Parameters: $path => relative path to source node from the current directory
|
||
|
# : $target => relative path to symlink target from the current directory
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal error if a conflict arises
|
||
|
# Comments : unstow_node() and unstow_contents() are mutually recursive
|
||
|
#============================================================================
|
||
|
sub unstow_node {
|
||
|
|
||
|
my ($path, $target) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Unstowing $path\n";
|
||
|
}
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- target is $target\n";
|
||
|
}
|
||
|
|
||
|
# does the target exist
|
||
|
if (is_a_link($target)) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn("Evaluate existing link: $target\n");
|
||
|
}
|
||
|
|
||
|
# where is the link pointing?
|
||
|
my $old_source = read_a_link($target);
|
||
|
if (not $old_source) {
|
||
|
error("Could not read link: $target");
|
||
|
}
|
||
|
|
||
|
if ($old_source =~ m{\A/}) {
|
||
|
warn "ignoring a absolute symlink: $target => $old_source\n";
|
||
|
return; # XXX #
|
||
|
}
|
||
|
|
||
|
# does it point to a node under our stow directory?
|
||
|
my $old_path = find_stowed_path($target, $old_source);
|
||
|
if (not $old_path) {
|
||
|
conflict(
|
||
|
qq{existing target is not owned by stow: $target => $old_source}
|
||
|
);
|
||
|
return; # XXX #
|
||
|
}
|
||
|
|
||
|
# does the existing $target actually point to anything
|
||
|
if (-e $old_path) {
|
||
|
# does link points to the right place
|
||
|
if ($old_path eq $path) {
|
||
|
do_unlink($target);
|
||
|
}
|
||
|
|
||
|
# XXX we quietly ignore links that are stowed to a different
|
||
|
# package.
|
||
|
|
||
|
#elsif (defer($target)) {
|
||
|
# if ($Option{'verbose'} > 2) {
|
||
|
# warn("--- deferring to installation of: $target\n");
|
||
|
# }
|
||
|
#}
|
||
|
#elsif (override($target)) {
|
||
|
# if ($Option{'verbose'} > 2) {
|
||
|
# warn("--- overriding installation of: $target\n");
|
||
|
# }
|
||
|
# do_unlink($target);
|
||
|
#}
|
||
|
#else {
|
||
|
# conflict(
|
||
|
# q{existing target is stowed to a different package: %s => %s},
|
||
|
# $target,
|
||
|
# $old_source
|
||
|
# );
|
||
|
#}
|
||
|
}
|
||
|
else {
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- removing invalid link into a stow directory: $path\n";
|
||
|
}
|
||
|
do_unlink($target);
|
||
|
}
|
||
|
}
|
||
|
elsif (-e $target) {
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn("Evaluate existing node: $target\n");
|
||
|
}
|
||
|
if (-d $target) {
|
||
|
unstow_contents($path, $target);
|
||
|
|
||
|
# this action may have made the parent directory foldable
|
||
|
if (my $parent = foldable($target)) {
|
||
|
fold_tree($target,$parent);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
conflict(
|
||
|
qq{existing target is neither a link nor a directory: $target},
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : find_stowed_path()
|
||
|
# Purpose : determine if the given link points to a member of a
|
||
|
# : stowed package
|
||
|
# Parameters: $target => path to a symbolic link under current directory
|
||
|
# : $source => where that link points to
|
||
|
# Returns : relative path to stowed node (from the current directory)
|
||
|
# : or '' if link is not owned by stow
|
||
|
# Throws : fatal exception if link is unreadable
|
||
|
# Comments : allow for stow dir not being under target dir
|
||
|
# : we could put more logic under here for multiple stow dirs
|
||
|
#============================================================================
|
||
|
sub find_stowed_path {
|
||
|
|
||
|
my ($target, $source) = @_;
|
||
|
|
||
|
# evaluate softlink relative to its target
|
||
|
my $path = join_paths(parent($target), $source);
|
||
|
|
||
|
# search for .stow files
|
||
|
my $dir = '';
|
||
|
for my $part (split m{/+}, $path) {
|
||
|
$dir = join_paths($dir,$part);
|
||
|
if (-f "$dir/.stow") {
|
||
|
return $path;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# compare with $Stow_Path
|
||
|
my @path = split m{/+}, $path;
|
||
|
my @stow_path = split m{/+}, $Stow_Path;
|
||
|
|
||
|
# strip off common prefixes
|
||
|
while ( @path && @stow_path ) {
|
||
|
if ( (shift @path) ne (shift @stow_path) ) {
|
||
|
return '';
|
||
|
}
|
||
|
}
|
||
|
if (@stow_path) {
|
||
|
# @path is not under @stow_dir
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
return $path
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : cleanup_invalid_links()
|
||
|
# Purpose : clean up invalid links that may block folding
|
||
|
# Parameters: $dir => path to directory to check
|
||
|
# Returns : n/a
|
||
|
# Throws : no exceptions
|
||
|
# Comments : removing files from a stowed package is probably a bad practice
|
||
|
# : so this kind of clean up is not _really_ stow's responsibility;
|
||
|
# : however, failing to clean up can block tree folding, so we'll do
|
||
|
# : it anyway
|
||
|
#=============================================================================
|
||
|
sub cleanup_invalid_links {
|
||
|
|
||
|
my ($dir) = @_;
|
||
|
|
||
|
if (not -d $dir) {
|
||
|
error("cleanup_invalid_links() called with a non-directory: $dir");
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $dir
|
||
|
or error("cannot read directory: $dir");
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
|
||
|
my $node_path = join_paths($dir,$node);
|
||
|
|
||
|
if (-l $node_path and not exists $Link_Task_For{$node_path}) {
|
||
|
|
||
|
# where is the link pointing?
|
||
|
# (dont use read_a_link here)
|
||
|
my $source = readlink($node_path);
|
||
|
if (not $source) {
|
||
|
error("Could not read link $node_path");
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
not -e join_paths($dir,$source) and # bad link
|
||
|
find_stowed_path($node_path,$source) # owned by stow
|
||
|
){
|
||
|
if ($Option{'verbose'} > 2) {
|
||
|
warn "--- removing stale link: $node_path => ",
|
||
|
join_paths($dir,$source), "\n";
|
||
|
}
|
||
|
do_unlink($node_path);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : foldable()
|
||
|
# Purpose : determine if a tree can be folded
|
||
|
# Parameters: target => path to a directory
|
||
|
# Returns : path to the parent dir iff the tree can be safely folded
|
||
|
# Throws : n/a
|
||
|
# Comments : the path returned is relative to the parent of $target,
|
||
|
# : that is, it can be used as the source for a replacement symlink
|
||
|
#============================================================================
|
||
|
sub foldable {
|
||
|
|
||
|
my ($target) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- Is $target foldable?\n";
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $target
|
||
|
or error(qq{Cannot read directory "$target" ($!)\n});
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
my $parent = '';
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
|
||
|
my $path = join_paths($target,$node);
|
||
|
|
||
|
# skip nodes scheduled for removal
|
||
|
next NODE if not is_a_node($path);
|
||
|
|
||
|
# if its not a link then we can't fold its parent
|
||
|
return '' if not is_a_link($path);
|
||
|
|
||
|
# where is the link pointing?
|
||
|
my $source = read_a_link($path);
|
||
|
if (not $source) {
|
||
|
error("Could not read link $path");
|
||
|
}
|
||
|
if ($parent eq '') {
|
||
|
$parent = parent($source)
|
||
|
}
|
||
|
elsif ($parent ne parent($source)) {
|
||
|
return '';
|
||
|
}
|
||
|
}
|
||
|
return '' if not $parent;
|
||
|
|
||
|
# if we get here then all nodes inside $target are links, and those links
|
||
|
# point to nodes inside the same directory.
|
||
|
|
||
|
# chop of leading '..' to get the path to the common parent directory
|
||
|
# relative to the parent of our $target
|
||
|
$parent =~ s{\A\.\./}{};
|
||
|
|
||
|
# if the resulting path is owned by stow, we can fold it
|
||
|
if (find_stowed_path($target,$parent)) {
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- $target is foldable\n";
|
||
|
}
|
||
|
return $parent;
|
||
|
}
|
||
|
else {
|
||
|
return '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : fold_tree()
|
||
|
# Purpose : fold the given tree
|
||
|
# Parameters: $source => link to the folded tree source
|
||
|
# : $target => directory that we will replace with a link to $source
|
||
|
# Returns : n/a
|
||
|
# Throws : none
|
||
|
# Comments : only called iff foldable() is true so we can remove some checks
|
||
|
#============================================================================
|
||
|
sub fold_tree {
|
||
|
|
||
|
my ($target,$source) = @_;
|
||
|
|
||
|
if ($Option{'verbose'} > 2){
|
||
|
warn "--- Folding tree: $target => $source\n";
|
||
|
}
|
||
|
|
||
|
opendir my $DIR, $target
|
||
|
or error(qq{Cannot read directory "$target" ($!)\n});
|
||
|
my @listing = readdir $DIR;
|
||
|
closedir $DIR;
|
||
|
|
||
|
NODE:
|
||
|
for my $node (@listing) {
|
||
|
next NODE if $node eq '.';
|
||
|
next NODE if $node eq '..';
|
||
|
next NODE if not is_a_node(join_paths($target,$node));
|
||
|
do_unlink(join_paths($target,$node));
|
||
|
}
|
||
|
do_rmdir($target);
|
||
|
do_link($source, $target);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : conflict()
|
||
|
# Purpose : handle conflicts in stow operations
|
||
|
# Parameters: paths that conflict
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal exception unless 'conflicts' option is set
|
||
|
# Comments : indicates what type of conflict it is
|
||
|
#============================================================================
|
||
|
sub conflict {
|
||
|
my ( $format, @args ) = @_;
|
||
|
|
||
|
my $message = sprintf($format, @args);
|
||
|
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn qq{CONFLICT: $message\n};
|
||
|
}
|
||
|
push @Conflicts, qq{CONFLICT: $message\n};
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : ignore
|
||
|
# Purpose : determine if the given path matches a regex in our ignore list
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : no exceptions
|
||
|
# Comments : none
|
||
|
#=============================================================================
|
||
|
sub ignore {
|
||
|
|
||
|
my ($path) = @_;
|
||
|
|
||
|
for my $suffix (@{$Option{'ignore'}}) {
|
||
|
return 1 if $path =~ m/$suffix/;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : defer
|
||
|
# Purpose : determine if the given path matches a regex in our defer list
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : no exceptions
|
||
|
# Comments : none
|
||
|
#=============================================================================
|
||
|
sub defer {
|
||
|
|
||
|
my ($path) = @_;
|
||
|
|
||
|
for my $prefix (@{$Option{'defer'}}) {
|
||
|
return 1 if $path =~ m/$prefix/;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : overide
|
||
|
# Purpose : determine if the given path matches a regex in our override list
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : no exceptions
|
||
|
# Comments : none
|
||
|
#=============================================================================
|
||
|
sub override {
|
||
|
|
||
|
my ($path) = @_;
|
||
|
|
||
|
for my $regex (@{$Option{'override'}}) {
|
||
|
return 1 if $path =~ m/$regex/;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
##############################################################################
|
||
|
#
|
||
|
# The following code provides the abstractions that allow us to defer operating
|
||
|
# on the filesystem until after all potential conflcits have been assessed.
|
||
|
#
|
||
|
##############################################################################
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : process_tasks()
|
||
|
# Purpose : process each task in the @Tasks list
|
||
|
# Parameters: none
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal error if @Tasks is corrupted or a task fails
|
||
|
# Comments : task involve either creating or deleting dirs and symlinks
|
||
|
# : an action is set to 'skip' if it is found to be redundant
|
||
|
#============================================================================
|
||
|
sub process_tasks {
|
||
|
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Processing tasks...\n"
|
||
|
}
|
||
|
|
||
|
# strip out all tasks with a skip action
|
||
|
@Tasks = grep { $_->{'action'} ne 'skip' } @Tasks;
|
||
|
|
||
|
if (not scalar @Tasks) {
|
||
|
warn "There are no outstanding operations to perform.\n";
|
||
|
return;
|
||
|
}
|
||
|
if ($Option{'simulate'}) {
|
||
|
warn "WARNING: simulating so all operations are deferred.\n";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
for my $task (@Tasks) {
|
||
|
|
||
|
if ( $task->{'action'} eq 'create' ) {
|
||
|
if ( $task->{'type'} eq 'dir' ) {
|
||
|
mkdir($task->{'path'}, 0777)
|
||
|
or error(qq(Could not create directory: $task->{'path'}));
|
||
|
}
|
||
|
elsif ( $task->{'type'} eq 'link' ) {
|
||
|
symlink $task->{'source'}, $task->{'path'}
|
||
|
or error(
|
||
|
q(Could not create symlink: %s => %s),
|
||
|
$task->{'path'},
|
||
|
$task->{'source'}
|
||
|
);
|
||
|
}
|
||
|
else {
|
||
|
internal_error(qq(bad task type: $task->{'type'}));
|
||
|
}
|
||
|
}
|
||
|
elsif ( $task->{'action'} eq 'remove' ) {
|
||
|
if ( $task->{'type'} eq 'dir' ) {
|
||
|
rmdir $task->{'path'}
|
||
|
or error(qq(Could not remove directory: $task->{'path'}));
|
||
|
}
|
||
|
elsif ( $task->{'type'} eq 'link' ) {
|
||
|
unlink $task->{'path'}
|
||
|
or error(qq(Could not remove link: $task->{'path'}));
|
||
|
}
|
||
|
else {
|
||
|
internal_error(qq(bad task type: $task->{'type'}));
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
internal_error(qq(bad task action: $task->{'action'}));
|
||
|
}
|
||
|
}
|
||
|
if ($Option{'verbose'} > 1) {
|
||
|
warn "Processing tasks...done\n"
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : is_a_link()
|
||
|
# Purpose : is the given path a current or planned link
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : none
|
||
|
# Comments : returns false if an existing link is scheduled for removal
|
||
|
# : and true if a non-exsitent link is scheduled for creation
|
||
|
#============================================================================
|
||
|
sub is_a_link {
|
||
|
my ($path) = @_;
|
||
|
|
||
|
|
||
|
if ( exists $Link_Task_For{$path} ) {
|
||
|
|
||
|
my $action = $Link_Task_For{$path}->{'action'};
|
||
|
|
||
|
if ($action eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
elsif ($action eq 'create') {
|
||
|
return 1;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $action");
|
||
|
}
|
||
|
}
|
||
|
elsif (-l $path) {
|
||
|
# check if any of its parent are links scheduled for removal
|
||
|
# (need this for edge case during unfolding)
|
||
|
my $parent = '';
|
||
|
for my $part (split m{/+}, $path ) {
|
||
|
$parent = join_paths($parent,$part);
|
||
|
if ( exists $Link_Task_For{$parent} ) {
|
||
|
if ($Link_Task_For{$parent}->{'action'} eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return 1;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : is_a_dir()
|
||
|
# Purpose : is the given path a current or planned directory
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : none
|
||
|
# Comments : returns false if an existing directory is scheduled for removal
|
||
|
# : and true if a non-existent directory is scheduled for creation
|
||
|
# : we also need to be sure we are not just following a link
|
||
|
#============================================================================
|
||
|
sub is_a_dir {
|
||
|
my ($path) = @_;
|
||
|
|
||
|
if ( exists $Dir_Task_For{$path} ) {
|
||
|
my $action = $Dir_Task_For{$path}->{'action'};
|
||
|
if ($action eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
elsif ($action eq 'create') {
|
||
|
return 1;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $action");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# are we really following a link that is scheduled for removal
|
||
|
my $prefix = '';
|
||
|
for my $part (split m{/+}, $path) {
|
||
|
$prefix = join_paths($prefix,$part);
|
||
|
if (exists $Link_Task_For{$prefix} and
|
||
|
$Link_Task_For{$prefix}->{'action'} eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (-d $path) {
|
||
|
return 1;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : is_a_node()
|
||
|
# Purpose : is the given path a current or planned node
|
||
|
# Parameters: none
|
||
|
# Returns : Boolean
|
||
|
# Throws : none
|
||
|
# Comments : returns false if an existing node is scheduled for removal
|
||
|
# : true if a non-existent node is scheduled for creation
|
||
|
# : we also need to be sure we are not just following a link
|
||
|
#============================================================================
|
||
|
sub is_a_node {
|
||
|
my ($path) = @_;
|
||
|
|
||
|
if ( exists $Link_Task_For{$path} ) {
|
||
|
|
||
|
my $action = $Link_Task_For{$path}->{'action'};
|
||
|
|
||
|
if ($action eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
elsif ($action eq 'create') {
|
||
|
return 1;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $action");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( exists $Dir_Task_For{$path} ) {
|
||
|
|
||
|
my $action = $Dir_Task_For{$path}->{'action'};
|
||
|
|
||
|
if ($action eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
elsif ($action eq 'create') {
|
||
|
return 1;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $action");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# are we really following a link that is scheduled for removal
|
||
|
my $prefix = '';
|
||
|
for my $part (split m{/+}, $path) {
|
||
|
$prefix = join_paths($prefix,$part);
|
||
|
if ( exists $Link_Task_For{$prefix} and
|
||
|
$Link_Task_For{$prefix}->{'action'} eq 'remove') {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (-e $path) {
|
||
|
return 1;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : read_a_link()
|
||
|
# Purpose : return the source of a current or planned link
|
||
|
# Parameters: $path => path to the link target
|
||
|
# Returns : a string
|
||
|
# Throws : fatal exception if the given path is not a current or planned
|
||
|
# : link
|
||
|
# Comments : none
|
||
|
#============================================================================
|
||
|
sub read_a_link {
|
||
|
|
||
|
my ($path) = @_;
|
||
|
|
||
|
if ( exists $Link_Task_For{$path} ) {
|
||
|
my $action = $Link_Task_For{$path}->{'action'};
|
||
|
|
||
|
if ($action eq 'create') {
|
||
|
return $Link_Task_For{$path}->{'source'};
|
||
|
}
|
||
|
elsif ($action eq 'remove') {
|
||
|
internal_error(
|
||
|
"read_a_link() passed a path that is scheduled for removal: $path"
|
||
|
);
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $action");
|
||
|
}
|
||
|
}
|
||
|
elsif (-l $path) {
|
||
|
return readlink $path
|
||
|
or error("Could not read link: $path");
|
||
|
}
|
||
|
internal_error("read_a_link() passed a non link path: $path\n");
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : do_link()
|
||
|
# Purpose : wrap 'link' operation for later processing
|
||
|
# Parameters: file => the file to link
|
||
|
# Returns : n/a
|
||
|
# Throws : error if this clashes with an existing planned operation
|
||
|
# Comments : cleans up operations that undo previous operations
|
||
|
#============================================================================
|
||
|
sub do_link {
|
||
|
|
||
|
my ( $oldfile, $newfile ) = @_;
|
||
|
|
||
|
if ( exists $Dir_Task_For{$newfile} ) {
|
||
|
|
||
|
my $task_ref = $Dir_Task_For{$newfile};
|
||
|
|
||
|
if ( $task_ref->{'action'} eq 'create' ) {
|
||
|
if ($task_ref->{'type'} eq 'dir') {
|
||
|
internal_error(
|
||
|
"new link (%s => %s ) clashes with planned new directory",
|
||
|
$newfile,
|
||
|
$oldfile,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
elsif ( $task_ref->{'action'} eq 'remove' ) {
|
||
|
# we may need to remove a directory before creating a link so continue;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( exists $Link_Task_For{$newfile} ) {
|
||
|
|
||
|
my $task_ref = $Link_Task_For{$newfile};
|
||
|
|
||
|
if ( $task_ref->{'action'} eq 'create' ) {
|
||
|
if ( $task_ref->{'source'} ne $oldfile ) {
|
||
|
internal_error(
|
||
|
"new link clashes with planned new link: %s => %s",
|
||
|
$task_ref->{'path'},
|
||
|
$task_ref->{'source'},
|
||
|
)
|
||
|
}
|
||
|
else {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "LINK: $newfile => $oldfile (duplicates previous action)\n";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
elsif ( $task_ref->{'action'} eq 'remove' ) {
|
||
|
if ( $task_ref->{'source'} eq $oldfile ) {
|
||
|
# no need to remove a link we are going to recreate
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "LINK: $newfile => $oldfile (reverts previous action)\n";
|
||
|
}
|
||
|
$Link_Task_For{$newfile}->{'action'} = 'skip';
|
||
|
delete $Link_Task_For{$newfile};
|
||
|
return;
|
||
|
}
|
||
|
# we may need to remove a link to replace it so continue
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# creating a new link
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "LINK: $newfile => $oldfile\n";
|
||
|
}
|
||
|
my $task = {
|
||
|
action => 'create',
|
||
|
type => 'link',
|
||
|
path => $newfile,
|
||
|
source => $oldfile,
|
||
|
};
|
||
|
push @Tasks, $task;
|
||
|
$Link_Task_For{$newfile} = $task;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : do_unlink()
|
||
|
# Purpose : wrap 'unlink' operation for later processing
|
||
|
# Parameters: $file => the file to unlink
|
||
|
# Returns : n/a
|
||
|
# Throws : error if this clashes with an existing planned operation
|
||
|
# Comments : will remove an existing planned link
|
||
|
#============================================================================
|
||
|
sub do_unlink {
|
||
|
|
||
|
my ($file) = @_;
|
||
|
|
||
|
if (exists $Link_Task_For{$file} ) {
|
||
|
my $task_ref = $Link_Task_For{$file};
|
||
|
if ( $task_ref->{'action'} eq 'remove' ) {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "UNLINK: $file (duplicates previous action)\n";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
elsif ( $task_ref->{'action'} eq 'create' ) {
|
||
|
# do need to create a link then remove it
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "UNLINK: $file (reverts previous action)\n";
|
||
|
}
|
||
|
$Link_Task_For{$file}->{'action'} = 'skip';
|
||
|
delete $Link_Task_For{$file};
|
||
|
return;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( exists $Dir_Task_For{$file} and $Dir_Task_For{$file} eq 'create' ) {
|
||
|
internal_error(
|
||
|
"new unlink operation clashes with planned operation: %s dir %s",
|
||
|
$Dir_Task_For{$file}->{'action'},
|
||
|
$file
|
||
|
);
|
||
|
}
|
||
|
|
||
|
# remove the link
|
||
|
if ($Option{'verbose'}) {
|
||
|
#warn "UNLINK: $file (".(caller())[2].")\n";
|
||
|
warn "UNLINK: $file\n";
|
||
|
}
|
||
|
|
||
|
my $source = readlink $file or error("could not readlink $file");
|
||
|
|
||
|
my $task = {
|
||
|
action => 'remove',
|
||
|
type => 'link',
|
||
|
path => $file,
|
||
|
source => $source,
|
||
|
};
|
||
|
push @Tasks, $task;
|
||
|
$Link_Task_For{$file} = $task;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : do_mkdir()
|
||
|
# Purpose : wrap 'mkdir' operation
|
||
|
# Parameters: $dir => the directory to remove
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal exception if operation fails
|
||
|
# Comments : outputs a message if 'verbose' option is set
|
||
|
# : does not perform operation if 'simulate' option is set
|
||
|
# Comments : cleans up operations that undo previous operations
|
||
|
#============================================================================
|
||
|
sub do_mkdir {
|
||
|
my ($dir) = @_;
|
||
|
|
||
|
if ( exists $Link_Task_For{$dir} ) {
|
||
|
|
||
|
my $task_ref = $Link_Task_For{$dir};
|
||
|
|
||
|
if ($task_ref->{'action'} eq 'create') {
|
||
|
internal_error(
|
||
|
"new dir clashes with planned new link (%s => %s)",
|
||
|
$task_ref->{'path'},
|
||
|
$task_ref->{'source'},
|
||
|
);
|
||
|
}
|
||
|
elsif ($task_ref->{'action'} eq 'remove') {
|
||
|
# may need to remove a link before creating a directory so continue
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( exists $Dir_Task_For{$dir} ) {
|
||
|
|
||
|
my $task_ref = $Dir_Task_For{$dir};
|
||
|
|
||
|
if ($task_ref->{'action'} eq 'create') {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "MKDIR: $dir (duplicates previous action)\n";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
elsif ($task_ref->{'action'} eq 'remove') {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "MKDIR: $dir (reverts previous action)\n";
|
||
|
}
|
||
|
$Dir_Task_For{$dir}->{'action'} = 'skip';
|
||
|
delete $Dir_Task_For{$dir};
|
||
|
return;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "MKDIR: $dir\n";
|
||
|
}
|
||
|
my $task = {
|
||
|
action => 'create',
|
||
|
type => 'dir',
|
||
|
path => $dir,
|
||
|
source => undef,
|
||
|
};
|
||
|
push @Tasks, $task;
|
||
|
$Dir_Task_For{$dir} = $task;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : do_rmdir()
|
||
|
# Purpose : wrap 'rmdir' operation
|
||
|
# Parameters: $dir => the directory to remove
|
||
|
# Returns : n/a
|
||
|
# Throws : fatal exception if operation fails
|
||
|
# Comments : outputs a message if 'verbose' option is set
|
||
|
# : does not perform operation if 'simulate' option is set
|
||
|
#============================================================================
|
||
|
sub do_rmdir {
|
||
|
my ($dir) = @_;
|
||
|
|
||
|
if (exists $Link_Task_For{$dir} ) {
|
||
|
my $task_ref = $Link_Task_For{$dir};
|
||
|
internal_error(
|
||
|
"rmdir clashes with planned operation: %s link %s => %s",
|
||
|
$task_ref->{'action'},
|
||
|
$task_ref->{'path'},
|
||
|
$task_ref->{'source'}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (exists $Dir_Task_For{$dir} ) {
|
||
|
my $task_ref = $Link_Task_For{$dir};
|
||
|
|
||
|
if ($task_ref->{'action'} eq 'remove' ) {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "RMDIR $dir (duplicates previous action)\n";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
elsif ($task_ref->{'action'} eq 'create' ) {
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "MKDIR $dir (reverts previous action)\n";
|
||
|
}
|
||
|
$Link_Task_For{$dir}->{'action'} = 'skip';
|
||
|
delete $Link_Task_For{$dir};
|
||
|
return;
|
||
|
}
|
||
|
else {
|
||
|
internal_error("bad task action: $task_ref->{'action'}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($Option{'verbose'}) {
|
||
|
warn "RMDIR $dir\n";
|
||
|
}
|
||
|
my $task = {
|
||
|
action => 'remove',
|
||
|
type => 'dir',
|
||
|
path => $dir,
|
||
|
source => '',
|
||
|
};
|
||
|
push @Tasks, $task;
|
||
|
$Dir_Task_For{$dir} = $task;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
#
|
||
|
# General Utilities: nothing stow specific here.
|
||
|
#
|
||
|
#############################################################################
|
||
|
|
||
|
#===== SUBROUTINE ============================================================
|
||
|
# Name : strip_quotes
|
||
|
# Purpose : remove matching outer quotes from the given string
|
||
|
# Parameters: none
|
||
|
# Returns : n/a
|
||
|
# Throws : no exceptions
|
||
|
# Comments : none
|
||
|
#=============================================================================
|
||
|
sub strip_quotes {
|
||
|
|
||
|
my ($string) = @_;
|
||
|
|
||
|
if ($string =~ m{\A\s*'(.*)'\s*\z}) {
|
||
|
return $1;
|
||
|
}
|
||
|
elsif ($string =~ m{\A\s*"(.*)"\s*\z}) {
|
||
|
return $1
|
||
|
}
|
||
|
return $string;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : relative_path()
|
||
|
# Purpose : find the relative path between two given paths
|
||
|
# Parameters: path1 => a directory path
|
||
|
# : path2 => a directory path
|
||
|
# Returns : path2 relative to path1
|
||
|
# Throws : n/a
|
||
|
# Comments : only used once by main interactive routine
|
||
|
# : factored out for testing
|
||
|
#============================================================================
|
||
|
sub relative_path {
|
||
|
|
||
|
my ($path1, $path2) = @_;
|
||
|
|
||
|
my (@path1) = split m{/+}, $path1;
|
||
|
my (@path2) = split m{/+}, $path2;
|
||
|
|
||
|
# drop common prefixes until we find a difference
|
||
|
PREFIX:
|
||
|
while ( @path1 && @path2 ) {
|
||
|
last PREFIX if $path1[0] ne $path2[0];
|
||
|
shift @path1;
|
||
|
shift @path2;
|
||
|
}
|
||
|
|
||
|
# prepend one '..' to $path2 for each component of $path1
|
||
|
while ( shift @path1 ) {
|
||
|
unshift @path2, '..';
|
||
|
}
|
||
|
|
||
|
return join_paths(@path2);
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : join_path()
|
||
|
# Purpose : concatenates given paths
|
||
|
# Parameters: path1, path2, ... => paths
|
||
|
# Returns : concatenation of given paths
|
||
|
# Throws : n/a
|
||
|
# Comments : factors out redundant path elements:
|
||
|
# : '//' => '/' and 'a/b/../c' => 'a/c'
|
||
|
#============================================================================
|
||
|
sub join_paths {
|
||
|
|
||
|
my @paths = @_;
|
||
|
|
||
|
# weed out empty components and concatenate
|
||
|
my $result = join '/', grep {!/\A\z/} @paths;
|
||
|
|
||
|
# factor out back references and remove redundant /'s)
|
||
|
my @result = ();
|
||
|
PART:
|
||
|
for my $part ( split m{/+}, $result) {
|
||
|
next PART if $part eq '.';
|
||
|
if (@result && $part eq '..' && $result[-1] ne '..') {
|
||
|
pop @result;
|
||
|
}
|
||
|
else {
|
||
|
push @result, $part;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return join '/', @result;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : parent
|
||
|
# Purpose : find the parent of the given path
|
||
|
# Parameters: @path => components of the path
|
||
|
# Returns : returns a path string
|
||
|
# Throws : n/a
|
||
|
# Comments : allows you to send multiple chunks of the path
|
||
|
# : (this feature is currently not used)
|
||
|
#============================================================================
|
||
|
sub parent {
|
||
|
my @path = @_;
|
||
|
my $path = join '/', @_;
|
||
|
my @elts = split m{/+}, $path;
|
||
|
pop @elts;
|
||
|
return join '/', @elts;
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : internal_error()
|
||
|
# Purpose : output internal error message in a consistent form and die
|
||
|
# Parameters: $message => error message to output
|
||
|
# Returns : n/a
|
||
|
# Throws : n/a
|
||
|
# Comments : none
|
||
|
#============================================================================
|
||
|
sub internal_error {
|
||
|
my ($format,@args) = @_;
|
||
|
die "$ProgramName: INTERNAL ERROR: ".sprintf($format,@args)."\n",
|
||
|
"This _is_ a bug. Please submit a bug report so we can fix it:-)\n";
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : error()
|
||
|
# Purpose : output error message in a consistent form and die
|
||
|
# Parameters: $message => error message to output
|
||
|
# Returns : n/a
|
||
|
# Throws : n/a
|
||
|
# Comments : none
|
||
|
#============================================================================
|
||
|
sub error {
|
||
|
my ($format,@args) = @_;
|
||
|
die "$ProgramName: ERROR: ".sprintf($format,@args)." ($!)\n";
|
||
|
}
|
||
|
|
||
|
#===== SUBROUTINE ===========================================================
|
||
|
# Name : version()
|
||
|
# Purpose : print this programs verison and exit
|
||
|
# Parameters: none
|
||
|
# Returns : n/a
|
||
|
# Throws : n/a
|
||
|
# Comments : none
|
||
|
#============================================================================
|
||
|
sub version {
|
||
|
print "$ProgramName (GNU Stow) version $Version\n";
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
1; # return true so we can load this script as a module during unit testing
|
||
|
|
||
|
# Local variables:
|
||
|
# mode: perl
|
||
|
# End:
|
||
|
# vim: ft=perl
|