1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149 |
- #!/usr/bin/perl
- #
- # This file is part of GNU Stow.
- #
- # GNU Stow 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 3 of the License, or
- # (at your option) any later version.
- #
- # GNU Stow 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, see https://www.gnu.org/licenses/.
- package Stow;
- =head1 NAME
- Stow - manage farms of symbolic links
- =head1 SYNOPSIS
- my $stow = new Stow(%$options);
- $stow->plan_unstow(@pkgs_to_unstow);
- $stow->plan_stow (@pkgs_to_stow);
- my %conflicts = $stow->get_conflicts;
- $stow->process_tasks() unless %conflicts;
- =head1 DESCRIPTION
- This is the backend Perl module for GNU Stow, a program for managing
- the installation of software packages, keeping them separate
- (C</usr/local/stow/emacs> vs. C</usr/local/stow/perl>, for example)
- while making them appear to be installed in the same place
- (C</usr/local>).
- Stow doesn't store an extra state between runs, so there's no danger
- of mangling directories when file hierarchies don't match the
- database. Also, stow will never delete any files, directories, or
- links that appear in a stow directory, so it is always possible to
- rebuild the target tree.
- =cut
- use strict;
- use warnings;
- use Carp qw(carp cluck croak confess longmess);
- use File::Copy qw(move);
- use File::Spec;
- use POSIX qw(getcwd);
- use Stow::Util qw(set_debug_level debug error set_test_mode
- join_paths restore_cwd canon_path parent adjust_dotfile);
- our $ProgramName = 'stow';
- our $VERSION = '@VERSION@';
- our $LOCAL_IGNORE_FILE = '.stow-local-ignore';
- our $GLOBAL_IGNORE_FILE = '.stow-global-ignore';
- our @default_global_ignore_regexps =
- __PACKAGE__->get_default_global_ignore_regexps();
- # These are the default options for each Stow instance.
- our %DEFAULT_OPTIONS = (
- conflicts => 0,
- simulate => 0,
- verbose => 0,
- paranoid => 0,
- compat => 0,
- test_mode => 0,
- dotfiles => 0,
- adopt => 0,
- 'no-folding' => 0,
- absolute => 0,
- ignore => [],
- override => [],
- defer => [],
- );
- =head1 CONSTRUCTORS
- =head2 new(%options)
- =head3 Required options
- =over 4
- =item * dir - the stow directory
- =item * target - the target directory
- =back
- =head3 Non-mandatory options
- See the documentation for the F<stow> CLI front-end for information on these.
- =over 4
- =item * conflicts
- =item * simulate
- =item * verbose
- =item * paranoid
- =item * compat
- =item * test_mode
- =item * adopt
- =item * no-folding
- =item * absolute
- =item * ignore
- =item * override
- =item * defer
- =back
- N.B. This sets the current working directory to the target directory.
- =cut
- sub new {
- my $self = shift;
- my $class = ref($self) || $self;
- my %opts = @_;
- my $new = bless { }, $class;
- $new->{action_count} = 0;
- for my $required_arg (qw(dir target)) {
- croak "$class->new() called without '$required_arg' parameter\n"
- unless exists $opts{$required_arg};
- $new->{$required_arg} = delete $opts{$required_arg};
- }
- for my $opt (keys %DEFAULT_OPTIONS) {
- $new->{$opt} = exists $opts{$opt} ? delete $opts{$opt}
- : $DEFAULT_OPTIONS{$opt};
- }
- if (%opts) {
- croak "$class->new() called with unrecognised parameter(s): ",
- join(", ", keys %opts), "\n";
- }
- set_debug_level($new->get_verbosity());
- set_test_mode($new->{test_mode});
- $new->set_stow_dir();
- $new->init_state();
- return $new;
- }
- sub get_verbosity {
- my $self = shift;
- return $self->{verbose} unless $self->{test_mode};
- return 0 unless exists $ENV{TEST_VERBOSE};
- return 0 unless length $ENV{TEST_VERBOSE};
- # Convert TEST_VERBOSE=y into numeric value
- $ENV{TEST_VERBOSE} = 3 if $ENV{TEST_VERBOSE} !~ /^\d+$/;
- return $ENV{TEST_VERBOSE};
- }
- =head2 set_stow_dir([$dir])
- Sets a new stow directory. This allows the use of multiple stow
- directories within one Stow instance, e.g.
- $stow->plan_stow('foo');
- $stow->set_stow_dir('/different/stow/dir');
- $stow->plan_stow('bar');
- $stow->process_tasks;
- If C<$dir> is omitted, uses the value of the C<dir> parameter passed
- to the L<new()> constructor.
- =cut
- sub set_stow_dir {
- my $self = shift;
- my ($dir) = @_;
- if (defined $dir) {
- $self->{dir} = $dir;
- }
- my $stow_dir = canon_path($self->{dir});
- my $target = canon_path($self->{target});
- $self->{stow_path} = $self->{absolute} ? $stow_dir :
- File::Spec->abs2rel($stow_dir, $target);
- debug(2, "stow dir is $stow_dir");
- if (!$self->{absolute}) {
- debug(2, "stow dir path relative to target $target is $self->{stow_path}");
- } else {
- debug(2, "stow dir path for target $target is $self->{stow_path}");
- }
- }
- sub init_state {
- my $self = shift;
- # Store conflicts during pre-processing
- $self->{conflicts} = {};
- $self->{conflict_count} = 0;
- # Store command line packages to stow (-S and -R)
- $self->{pkgs_to_stow} = [];
- # Store command line packages to unstow (-D and -R)
- $self->{pkgs_to_delete} = [];
- # The following structures are used by the abstractions that allow us to
- # defer operating on the filesystem until after all potential conflicts have
- # been assessed.
- # $self->{tasks}: list of operations to be performed (in order)
- # each element is a hash ref of the form
- # {
- # action => ... ('create' or 'remove' or 'move')
- # type => ... ('link' or 'dir' or 'file')
- # path => ... (unique)
- # source => ... (only for links)
- # dest => ... (only for moving files)
- # }
- $self->{tasks} = [];
- # $self->{dir_task_for}: map a path to the corresponding directory task reference
- # This structure allows us to quickly determine if a path has an existing
- # directory task associated with it.
- $self->{dir_task_for} = {};
- # $self->{link_task_for}: map a path to the corresponding directory task reference
- # This structure allows us to quickly determine if a path has an existing
- # directory task associated with it.
- $self->{link_task_for} = {};
- # N.B.: directory tasks and link tasks are NOT mutually exclusive due
- # to tree splitting (which involves a remove link task followed by
- # a create directory task).
- }
- =head1 METHODS
- =head2 plan_unstow(@packages)
- Plan which symlink/directory creation/removal tasks need to be executed
- in order to unstow the given packages. Any potential conflicts are then
- accessible via L<get_conflicts()>.
- =cut
- sub plan_unstow {
- my $self = shift;
- my @packages = @_;
- $self->within_target_do(sub {
- for my $package (@packages) {
- my $path = join_paths($self->{stow_path}, $package);
- if (not -d $path) {
- error("The stow directory $self->{stow_path} does not contain package $package");
- }
- debug(2, "Planning unstow of package $package...");
- if ($self->{compat}) {
- $self->unstow_contents_orig(
- $self->{stow_path},
- $package,
- '.',
- );
- }
- else {
- $self->unstow_contents(
- $self->{stow_path},
- $package,
- '.',
- );
- }
- debug(2, "Planning unstow of package $package... done");
- $self->{action_count}++;
- }
- });
- }
- =head2 plan_stow(@packages)
- Plan which symlink/directory creation/removal tasks need to be executed
- in order to stow the given packages. Any potential conflicts are then
- accessible via L<get_conflicts()>.
- =cut
- sub plan_stow {
- my $self = shift;
- my @packages = @_;
- $self->within_target_do(sub {
- for my $package (@packages) {
- my $path = join_paths($self->{stow_path}, $package);
- if (not -d $path) {
- error("The stow directory $self->{stow_path} does not contain package $package");
- }
- debug(2, "Planning stow of package $package...");
- $self->stow_contents(
- $self->{stow_path},
- $package,
- '.',
- $path, # source from target
- );
- debug(2, "Planning stow of package $package... done");
- $self->{action_count}++;
- }
- });
- }
- #===== METHOD ===============================================================
- # Name : within_target_do()
- # Purpose : execute code within target directory, preserving cwd
- # Parameters: $code => anonymous subroutine to execute within target dir
- # Returns : n/a
- # Throws : n/a
- # Comments : This is done to ensure that the consumer of the Stow interface
- # : doesn't have to worry about (a) what their cwd is, and
- # : (b) that their cwd might change.
- #============================================================================
- sub within_target_do {
- my $self = shift;
- my ($code) = @_;
- my $cwd = getcwd();
- chdir($self->{target})
- or error("Cannot chdir to target tree: $self->{target} ($!)");
- debug(3, "cwd now $self->{target}");
- $self->$code();
- restore_cwd($cwd);
- debug(3, "cwd restored to $cwd");
- }
- #===== METHOD ===============================================================
- # Name : stow_contents()
- # Purpose : stow the contents of the given directory
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the package to be stowed
- # : $package => the package whose contents are being stowed
- # : $target => subpath relative to package directory which needs
- # : stowing as a symlink at subpath relative to target
- # : directory.
- # : $source => relative path from the (sub)dir of target
- # : to symlink source
- # 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 $self = shift;
- my ($stow_path, $package, $target, $source) = @_;
- my $path = join_paths($stow_path, $package, $target);
- return if $self->should_skip_target_which_is_stow_dir($target);
- my $cwd = getcwd();
- my $msg = "Stowing contents of $path (cwd=$cwd)";
- $msg =~ s!$ENV{HOME}(/|$)!~$1!g;
- debug(3, $msg);
- debug(4, " => $source");
- error("stow_contents() called with non-directory path: $path")
- unless -d $path;
- error("stow_contents() called with non-directory target: $target")
- unless $self->is_a_node($target);
- 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 '..';
- my $node_target = join_paths($target, $node);
- next NODE if $self->ignore($stow_path, $package, $node_target);
- if ($self->{dotfiles}) {
- my $adj_node_target = adjust_dotfile($node_target);
- debug(4, " Adjusting: $node_target => $adj_node_target");
- $node_target = $adj_node_target;
- }
- $self->stow_node(
- $stow_path,
- $package,
- $node_target, # target
- join_paths($source, $node), # source
- );
- }
- }
- #===== METHOD ===============================================================
- # Name : stow_node()
- # Purpose : stow the given node
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the node to be stowed
- # : $package => the package containing the node being stowed
- # : $target => subpath relative to package directory of node which
- # : needs stowing as a symlink at subpath relative to
- # : target directory.
- # : $source => relative 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 $self = shift;
- my ($stow_path, $package, $target, $source) = @_;
- my $path = join_paths($stow_path, $package, $target);
- debug(3, "Stowing $stow_path / $package / $target");
- debug(4, " => $source");
- # Don't try to stow absolute symlinks (they can't be unstowed)
- if ((not $self->{absolute}) && -l $source) {
- my $second_source = $self->read_a_link($source);
- if ($second_source =~ m{\A/}) {
- $self->conflict(
- 'stow',
- $package,
- "source is an absolute symlink $source => $second_source"
- );
- debug(3, "Absolute symlinks cannot be unstowed");
- return;
- }
- }
- if ($self->{absolute}) {
- debug(3, "Absolute symlinks cannot be unstowed, proceeding due to command-line option");
- }
- # Does the target already exist?
- if ($self->is_a_link($target)) {
- # Where is the link pointing?
- my $existing_source = $self->read_a_link($target);
- if (not $existing_source) {
- error("Could not read link: $target");
- }
- debug(4, " Evaluate existing link: $target => $existing_source");
- # Does it point to a node under any stow directory?
- my ($existing_path, $existing_stow_path, $existing_package) =
- $self->find_stowed_path($target, $existing_source);
- if (not $existing_path) {
- $self->conflict(
- 'stow',
- $package,
- "existing target is not owned by stow: $target"
- );
- return; # XXX #
- }
- # Does the existing $target actually point to anything?
- if ($self->is_a_node($existing_path)) {
- if ($existing_source eq $source) {
- debug(2, "--- Skipping $target as it already points to $source");
- }
- elsif ($self->defer($target)) {
- debug(2, "--- Deferring installation of: $target");
- }
- elsif ($self->override($target)) {
- debug(2, "--- Overriding installation of: $target");
- $self->do_unlink($target);
- $self->do_link($source, $target);
- }
- elsif ($self->is_a_dir(join_paths(parent($target), $existing_source)) &&
- $self->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 (split open) the tree at that point
- debug(2, "--- Unfolding $target which was already owned by $existing_package");
- $self->do_unlink($target);
- $self->do_mkdir($target);
- $self->stow_contents(
- $existing_stow_path,
- $existing_package,
- $target,
- join_paths('..', $existing_source),
- );
- $self->stow_contents(
- $self->{stow_path},
- $package,
- $target,
- join_paths('..', $source),
- );
- }
- else {
- $self->conflict(
- 'stow',
- $package,
- "existing target is stowed to a different package: "
- . "$target => $existing_source"
- );
- }
- }
- else {
- # The existing link is invalid, so replace it with a good link
- debug(2, "--- replacing invalid link: $path");
- $self->do_unlink($target);
- $self->do_link($source, $target);
- }
- }
- elsif ($self->is_a_node($target)) {
- debug(4, " Evaluate existing node: $target");
- if ($self->is_a_dir($target)) {
- $self->stow_contents(
- $self->{stow_path},
- $package,
- $target,
- join_paths('..', $source),
- );
- }
- else {
- if ($self->{adopt}) {
- $self->do_mv($target, $path);
- $self->do_link($source, $target);
- }
- else {
- $self->conflict(
- 'stow',
- $package,
- "existing target is neither a link nor a directory: $target"
- );
- }
- }
- }
- elsif ($self->{'no-folding'} && -d $path && ! -l $path) {
- $self->do_mkdir($target);
- $self->stow_contents(
- $self->{stow_path},
- $package,
- $target,
- join_paths('..', $source),
- );
- }
- else {
- $self->do_link($source, $target);
- }
- return;
- }
- #===== METHOD ===============================================================
- # Name : should_skip_target_which_is_stow_dir()
- # Purpose : determine whether target is a stow directory which should
- # : not be stowed to or unstowed from
- # Parameters: $target => relative path to symlink target from the current directory
- # Returns : true iff target is a stow directory
- # Throws : n/a
- # Comments : none
- #============================================================================
- sub should_skip_target_which_is_stow_dir {
- my $self = shift;
- my ($target) = @_;
- # Don't try to remove anything under a stow directory
- if ($target eq $self->{stow_path}) {
- warn "WARNING: skipping target which was current stow directory $target\n";
- return 1;
- }
- if ($self->marked_stow_dir($target)) {
- warn "WARNING: skipping protected directory $target\n";
- return 1;
- }
- debug(4, "$target not protected");
- return 0;
- }
- sub marked_stow_dir {
- my $self = shift;
- my ($target) = @_;
- for my $f (".stow", ".nonstow") {
- if (-e join_paths($target, $f)) {
- debug(4, "$target contained $f");
- return 1;
- }
- }
- return 0;
- }
- #===== METHOD ===============================================================
- # Name : unstow_contents_orig()
- # Purpose : unstow the contents of the given directory
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the package to be unstowed
- # : $package => the package whose contents are being unstowed
- # : $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_orig() and unstow_contents_orig() are mutually recursive
- # : Here we traverse the target tree, rather than the source tree.
- #============================================================================
- sub unstow_contents_orig {
- my $self = shift;
- my ($stow_path, $package, $target) = @_;
- my $path = join_paths($stow_path, $package, $target);
- return if $self->should_skip_target_which_is_stow_dir($target);
- my $cwd = getcwd();
- my $msg = "Unstowing from $target (compat mode, cwd=$cwd, stow dir=$self->{stow_path})";
- $msg =~ s!$ENV{HOME}(/|$)!~$1!g;
- debug(3, $msg);
- debug(4, " source path is $path");
- # In compat mode we traverse the target tree not the source tree,
- # so we're unstowing the contents of /target/foo, there's no
- # guarantee that the corresponding /stow/mypkg/foo exists.
- error("unstow_contents_orig() called with non-directory target: $target")
- unless -d $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 '..';
- my $node_target = join_paths($target, $node);
- next NODE if $self->ignore($stow_path, $package, $node_target);
- $self->unstow_node_orig($stow_path, $package, $node_target);
- }
- }
- #===== METHOD ===============================================================
- # Name : unstow_node_orig()
- # Purpose : unstow the given node
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the node to be stowed
- # : $package => the package containing the node being stowed
- # : $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 $self = shift;
- my ($stow_path, $package, $target) = @_;
- my $path = join_paths($stow_path, $package, $target);
- debug(3, "Unstowing $target (compat mode)");
- debug(4, " source path is $path");
- # Does the target exist?
- if ($self->is_a_link($target)) {
- debug(4, " Evaluate existing link: $target");
- # Where is the link pointing?
- my $existing_source = $self->read_a_link($target);
- if (not $existing_source) {
- error("Could not read link: $target");
- }
- # Does it point to a node under any stow directory?
- my ($existing_path, $existing_stow_path, $existing_package) =
- $self->find_stowed_path($target, $existing_source);
- if (not $existing_path) {
- # We're traversing the target tree not the package tree,
- # so we definitely expect to find stuff not owned by stow.
- # Therefore we can't flag a conflict.
- return; # XXX #
- }
- # Does the existing $target actually point to anything?
- if (-e $existing_path) {
- # Does link point to the right place?
- if ($existing_path eq $path) {
- $self->do_unlink($target);
- }
- elsif ($self->override($target)) {
- debug(2, "--- overriding installation of: $target");
- $self->do_unlink($target);
- }
- # else leave it alone
- }
- else {
- debug(2, "--- removing invalid link into a stow directory: $path");
- $self->do_unlink($target);
- }
- }
- elsif (-d $target) {
- $self->unstow_contents_orig($stow_path, $package, $target);
- # This action may have made the parent directory foldable
- if (my $parent = $self->foldable($target)) {
- $self->fold_tree($target, $parent);
- }
- }
- elsif (-e $target) {
- $self->conflict(
- 'unstow',
- $package,
- "existing target is neither a link nor a directory: $target",
- );
- }
- else {
- debug(2, "$target did not exist to be unstowed");
- }
- return;
- }
- #===== METHOD ===============================================================
- # Name : unstow_contents()
- # Purpose : unstow the contents of the given directory
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the package to be unstowed
- # : $package => the package whose contents are being unstowed
- # : $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 source tree, rather than the target tree.
- #============================================================================
- sub unstow_contents {
- my $self = shift;
- my ($stow_path, $package, $target) = @_;
- my $path = join_paths($stow_path, $package, $target);
- return if $self->should_skip_target_which_is_stow_dir($target);
- my $cwd = getcwd();
- my $msg = "Unstowing from $target (cwd=$cwd, stow dir=$self->{stow_path})";
- $msg =~ s!$ENV{HOME}/!~/!g;
- debug(3, $msg);
- debug(4, " source path is $path");
- # We traverse the source tree not the target tree, so $path must exist.
- error("unstow_contents() called with non-directory path: $path")
- unless -d $path;
- # When called at the top level, $target should exist. And
- # unstow_node() should only call this via mutual recursion if
- # $target exists.
- error("unstow_contents() called with invalid target: $target")
- unless $self->is_a_node($target);
- 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 '..';
- my $node_target = join_paths($target, $node);
- next NODE if $self->ignore($stow_path, $package, $node_target);
- if ($self->{dotfiles}) {
- my $adj_node_target = adjust_dotfile($node_target);
- debug(4, " Adjusting: $node_target => $adj_node_target");
- $node_target = $adj_node_target;
- }
- $self->unstow_node($stow_path, $package, $node_target);
- }
- if (-d $target) {
- $self->cleanup_invalid_links($target);
- }
- }
- #===== METHOD ===============================================================
- # Name : unstow_node()
- # Purpose : unstow the given node
- # Parameters: $stow_path => relative path from current (i.e. target) directory
- # : to the stow dir containing the node to be stowed
- # : $package => the package containing the node being unstowed
- # : $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 $self = shift;
- my ($stow_path, $package, $target) = @_;
- my $path = join_paths($stow_path, $package, $target);
- debug(3, "Unstowing $path");
- debug(4, " target is $target");
- # Does the target exist?
- if ($self->is_a_link($target)) {
- debug(4, " Evaluate existing link: $target");
- # Where is the link pointing?
- my $existing_source = $self->read_a_link($target);
- if (not $existing_source) {
- error("Could not read link: $target");
- }
- if (not $self->{absolute} and $existing_source =~ m{\A/}) {
- warn "Ignoring an absolute symlink: $target => $existing_source\n";
- return; # XXX #
- }
- # Does it point to a node under any stow directory?
- my ($existing_path, $existing_stow_path, $existing_package) =
- $self->find_stowed_path($target, $existing_source);
- if (not $existing_path) {
- $self->conflict(
- 'unstow',
- $package,
- "existing target is not owned by stow: $target => $existing_source"
- );
- return; # XXX #
- }
- # Does the existing $target actually point to anything?
- if (-e $existing_path) {
- # Does link points to the right place?
- # Adjust for dotfile if necessary.
- if ($self->{dotfiles}) {
- $existing_path = adjust_dotfile($existing_path);
- }
- if ($existing_path eq $path) {
- $self->do_unlink($target);
- }
- # XXX we quietly ignore links that are stowed to a different
- # package.
- #elsif (defer($target)) {
- # debug(2, "--- deferring to installation of: $target");
- #}
- #elsif ($self->override($target)) {
- # debug(2, "--- overriding installation of: $target");
- # $self->do_unlink($target);
- #}
- #else {
- # $self->conflict(
- # 'unstow',
- # $package,
- # "existing target is stowed to a different package: "
- # . "$target => $existing_source"
- # );
- #}
- }
- else {
- debug(2, "--- removing invalid link into a stow directory: $path");
- $self->do_unlink($target);
- }
- }
- elsif (-e $target) {
- debug(4, " Evaluate existing node: $target");
- if (-d $target) {
- $self->unstow_contents($stow_path, $package, $target);
- # This action may have made the parent directory foldable
- if (my $parent = $self->foldable($target)) {
- $self->fold_tree($target, $parent);
- }
- }
- else {
- $self->conflict(
- 'unstow',
- $package,
- "existing target is neither a link nor a directory: $target",
- );
- }
- }
- else {
- debug(2, "$target did not exist to be unstowed");
- }
- return;
- }
- #===== METHOD ===============================================================
- # Name : path_owned_by_package()
- # Purpose : determine whether 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 : the package iff link is owned by stow, otherwise ''
- # Throws : n/a
- # Comments : lossy wrapper around find_stowed_path()
- #============================================================================
- sub path_owned_by_package {
- my $self = shift;
- my ($target, $source) = @_;
- my ($path, $stow_path, $package) =
- $self->find_stowed_path($target, $source);
- return $package;
- }
- #===== METHOD ===============================================================
- # Name : find_stowed_path()
- # Purpose : determine whether the given link points to a member of a
- # : stowed package
- # Parameters: $target => path to a symbolic link under current directory.
- # : Must share a common prefix with $self->{stow_path}
- # : $source => where that link points to (needed because link
- # : might not exist yet due to two-phase approach,
- # : so we can't just call readlink()). This must be
- # : expressed relative to (the directory containing)
- # : $target.
- # Returns : ($path, $stow_path, $package) where $path and $stow_path are
- # : relative from the current (i.e. target) directory. $path
- # : is the full relative path, $stow_path is the relative path
- # : to the stow directory, and $package is the name of the package.
- # : or ('', '', '') if link is not owned by stow
- # Throws : n/a
- # 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 $self = shift;
- my ($target, $source) = @_;
- # Evaluate softlink relative to its target
- my $path = join_paths(parent($target), $source);
- debug(4, " is path $path owned by stow?");
- # Search for .stow files - this allows us to detect links
- # owned by stow directories other than the current one.
- my $dir = '';
- my @path = split m{/+}, $path;
- for my $i (0 .. $#path) {
- my $part = $path[$i];
- $dir = join_paths($dir, $part);
- if ($self->marked_stow_dir($dir)) {
- # FIXME - not sure if this can ever happen
- internal_error("find_stowed_path() called directly on stow dir")
- if $i == $#path;
- debug(4, " yes - $dir was marked as a stow dir");
- my $package = $path[$i + 1];
- return ($path, $dir, $package);
- }
- }
- # If no .stow file was found, we need to find out whether it's
- # owned by the current stow directory, in which case $path will be
- # a prefix of $self->{stow_path}.
- if (substr($path, 0, 1) eq '/' xor substr($self->{stow_path}, 0, 1) eq '/')
- {
- warn "BUG in find_stowed_path? Absolute/relative mismatch between " .
- "Stow dir $self->{stow_path} and path $path";
- }
- my @stow_path = split m{/+}, $self->{stow_path};
- # Strip off common prefixes until one is empty
- while (@path && @stow_path) {
- if ((shift @path) ne (shift @stow_path)) {
- debug(4, " no - either $path not under $self->{stow_path} or vice-versa");
- return ('', '', '');
- }
- }
- if (@stow_path) { # @path must be empty
- debug(4, " no - $path is not under $self->{stow_path}");
- return ('', '', '');
- }
- my $package = shift @path;
- debug(4, " yes - by $package in " . join_paths(@path));
- return ($path, $self->{stow_path}, $package);
- }
- #===== METHOD ================================================================
- # 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 $self = shift;
- 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 $self->{link_task_for}{$node_path}) {
- # Where is the link pointing?
- # (don't 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
- $self->path_owned_by_package($node_path, $source) # owned by stow
- ){
- debug(2, "--- removing stale link: $node_path => " .
- join_paths($dir, $source));
- $self->do_unlink($node_path);
- }
- }
- }
- return;
- }
- #===== METHOD ===============================================================
- # Name : foldable()
- # Purpose : determine whether 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 $self = shift;
- my ($target) = @_;
- debug(3, "--- Is $target foldable?");
- if ($self->{'no-folding'}) {
- debug(3, "--- no because --no-folding enabled");
- return '';
- }
- 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 $self->is_a_node($path);
- # If it's not a link then we can't fold its parent
- return '' if not $self->is_a_link($path);
- # Where is the link pointing?
- my $source = $self->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 ($self->path_owned_by_package($target, $parent)) {
- debug(3, "--- $target is foldable");
- return $parent;
- }
- else {
- return '';
- }
- }
- #===== METHOD ===============================================================
- # 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 $self = shift;
- my ($target, $source) = @_;
- debug(3, "--- Folding tree: $target => $source");
- 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 $self->is_a_node(join_paths($target, $node));
- $self->do_unlink(join_paths($target, $node));
- }
- $self->do_rmdir($target);
- $self->do_link($source, $target);
- return;
- }
- #===== METHOD ===============================================================
- # Name : conflict()
- # Purpose : handle conflicts in stow operations
- # Parameters: $package => the package involved with the conflicting operation
- # : $message => a description of the conflict
- # Returns : n/a
- # Throws : none
- # Comments : none
- #============================================================================
- sub conflict {
- my $self = shift;
- my ($action, $package, $message) = @_;
- debug(2, "CONFLICT when ${action}ing $package: $message");
- $self->{conflicts}{$action}{$package} ||= [];
- push @{ $self->{conflicts}{$action}{$package} }, $message;
- $self->{conflict_count}++;
- return;
- }
- =head2 get_conflicts()
- Returns a nested hash of all potential conflicts discovered: the keys
- are actions ('stow' or 'unstow'), and the values are hashrefs whose
- keys are stow package names and whose values are conflict
- descriptions, e.g.:
- (
- stow => {
- perl => [
- "existing target is not owned by stow: bin/a2p"
- "existing target is neither a link nor a directory: bin/perl"
- ]
- }
- )
- =cut
- sub get_conflicts {
- my $self = shift;
- return %{ $self->{conflicts} };
- }
- =head2 get_conflict_count()
- Returns the number of conflicts found.
- =cut
- sub get_conflict_count {
- my $self = shift;
- return $self->{conflict_count};
- }
- =head2 get_tasks()
- Returns a list of all symlink/directory creation/removal tasks.
- =cut
- sub get_tasks {
- my $self = shift;
- return @{ $self->{tasks} };
- }
- =head2 get_action_count()
- Returns the number of actions planned for this Stow instance.
- =cut
- sub get_action_count {
- my $self = shift;
- return $self->{action_count};
- }
- #===== METHOD ================================================================
- # Name : ignore
- # Purpose : determine if the given path matches a regex in our ignore list
- # Parameters: $stow_path => the stow directory containing the package
- # : $package => the package containing the path
- # : $target => the path to check against the ignore list
- # : relative to its package directory
- # Returns : true iff the path should be ignored
- # Throws : no exceptions
- # Comments : none
- #=============================================================================
- sub ignore {
- my $self = shift;
- my ($stow_path, $package, $target) = @_;
- internal_error(__PACKAGE__ . "::ignore() called with empty target")
- unless length $target;
- for my $suffix (@{ $self->{ignore} }) {
- if ($target =~ m/$suffix/) {
- debug(4, " Ignoring path $target due to --ignore=$suffix");
- return 1;
- }
- }
- my $package_dir = join_paths($stow_path, $package);
- my ($path_regexp, $segment_regexp) =
- $self->get_ignore_regexps($package_dir);
- debug(5, " Ignore list regexp for paths: " .
- (defined $path_regexp ? "/$path_regexp/" : "none"));
- debug(5, " Ignore list regexp for segments: " .
- (defined $segment_regexp ? "/$segment_regexp/" : "none"));
- if (defined $path_regexp and "/$target" =~ $path_regexp) {
- debug(4, " Ignoring path /$target");
- return 1;
- }
- (my $basename = $target) =~ s!.+/!!;
- if (defined $segment_regexp and $basename =~ $segment_regexp) {
- debug(4, " Ignoring path segment $basename");
- return 1;
- }
- debug(5, " Not ignoring $target");
- return 0;
- }
- sub get_ignore_regexps {
- my $self = shift;
- my ($dir) = @_;
- # N.B. the local and global stow ignore files have to have different
- # names so that:
- # 1. the global one can be a symlink to within a stow
- # package, managed by stow itself, and
- # 2. the local ones can be ignored via hardcoded logic in
- # GlobsToRegexp(), so that they always stay within their stow packages.
- my $local_stow_ignore = join_paths($dir, $LOCAL_IGNORE_FILE);
- my $global_stow_ignore = join_paths($ENV{HOME}, $GLOBAL_IGNORE_FILE);
- for my $file ($local_stow_ignore, $global_stow_ignore) {
- if (-e $file) {
- debug(5, " Using ignore file: $file");
- return $self->get_ignore_regexps_from_file($file);
- }
- else {
- debug(5, " $file didn't exist");
- }
- }
- debug(4, " Using built-in ignore list");
- return @default_global_ignore_regexps;
- }
- my %ignore_file_regexps;
- sub get_ignore_regexps_from_file {
- my $self = shift;
- my ($file) = @_;
- if (exists $ignore_file_regexps{$file}) {
- debug(4, " Using memoized regexps from $file");
- return @{ $ignore_file_regexps{$file} };
- }
- if (! open(REGEXPS, $file)) {
- debug(4, " Failed to open $file: $!");
- return undef;
- }
- my @regexps = $self->get_ignore_regexps_from_fh(\*REGEXPS);
- close(REGEXPS);
- $ignore_file_regexps{$file} = [ @regexps ];
- return @regexps;
- }
- =head2 invalidate_memoized_regexp($file)
- For efficiency of performance, regular expressions are compiled from
- each ignore list file the first time it is used by the Stow process,
- and then memoized for future use. If you expect the contents of these
- files to change during a single run, you will need to invalidate the
- memoized value from this cache. This method allows you to do that.
- =cut
- sub invalidate_memoized_regexp {
- my $self = shift;
- my ($file) = @_;
- if (exists $ignore_file_regexps{$file}) {
- debug(4, " Invalidated memoized regexp for $file");
- delete $ignore_file_regexps{$file};
- }
- else {
- debug(2, " WARNING: no memoized regexp for $file to invalidate");
- }
- }
- sub get_ignore_regexps_from_fh {
- my $self = shift;
- my ($fh) = @_;
- my %regexps;
- while (<$fh>) {
- chomp;
- s/^\s+//;
- s/\s+$//;
- next if /^#/ or length($_) == 0;
- s/\s+#.+//; # strip comments to right of pattern
- s/\\#/#/g;
- $regexps{$_}++;
- }
- # Local ignore lists should *always* stay within the stow directory,
- # because this is the only place stow looks for them.
- $regexps{"^/\Q$LOCAL_IGNORE_FILE\E\$"}++;
- return $self->compile_ignore_regexps(%regexps);
- }
- sub compile_ignore_regexps {
- my $self = shift;
- my (%regexps) = @_;
- my @segment_regexps;
- my @path_regexps;
- for my $regexp (keys %regexps) {
- if (index($regexp, '/') < 0) {
- # No / found in regexp, so use it for matching against basename
- push @segment_regexps, $regexp;
- }
- else {
- # / found in regexp, so use it for matching against full path
- push @path_regexps, $regexp;
- }
- }
- my $segment_regexp = join '|', @segment_regexps;
- my $path_regexp = join '|', @path_regexps;
- $segment_regexp = @segment_regexps ?
- $self->compile_regexp("^($segment_regexp)\$") : undef;
- $path_regexp = @path_regexps ?
- $self->compile_regexp("(^|/)($path_regexp)(/|\$)") : undef;
- return ($path_regexp, $segment_regexp);
- }
- sub compile_regexp {
- my $self = shift;
- my ($regexp) = @_;
- my $compiled = eval { qr/$regexp/ };
- die "Failed to compile regexp: $@\n" if $@;
- return $compiled;
- }
- sub get_default_global_ignore_regexps {
- my $class = shift;
- # Bootstrap issue - first time we stow, we will be stowing
- # .cvsignore so it might not exist in ~ yet, or if it does, it could
- # be an old version missing the entries we need. So we make sure
- # they are there by hardcoding some crucial entries.
- return $class->get_ignore_regexps_from_fh(\*DATA);
- }
- #===== METHOD ================================================================
- # Name : defer
- # Purpose : determine if the given path matches a regex in our defer list
- # Parameters: $path
- # Returns : Boolean
- # Throws : no exceptions
- # Comments : none
- #=============================================================================
- sub defer {
- my $self = shift;
- my ($path) = @_;
- for my $prefix (@{ $self->{defer} }) {
- return 1 if $path =~ m/$prefix/;
- }
- return 0;
- }
- #===== METHOD ================================================================
- # Name : override
- # Purpose : determine if the given path matches a regex in our override list
- # Parameters: $path
- # Returns : Boolean
- # Throws : no exceptions
- # Comments : none
- #=============================================================================
- sub override {
- my $self = shift;
- my ($path) = @_;
- for my $regex (@{ $self->{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.
- #
- ##############################################################################
- #===== METHOD ===============================================================
- # Name : process_tasks()
- # Purpose : process each task in the tasks list
- # Parameters: none
- # Returns : n/a
- # Throws : fatal error if tasks list is corrupted or a task fails
- # Comments : none
- #============================================================================
- sub process_tasks {
- my $self = shift;
- debug(2, "Processing tasks...");
- # Strip out all tasks with a skip action
- $self->{tasks} = [ grep { $_->{action} ne 'skip' } @{ $self->{tasks} } ];
- if (not @{ $self->{tasks} }) {
- return;
- }
- $self->within_target_do(sub {
- for my $task (@{ $self->{tasks} }) {
- $self->process_task($task);
- }
- });
- debug(2, "Processing tasks... done");
- }
- #===== METHOD ===============================================================
- # Name : process_task()
- # Purpose : process a single task
- # Parameters: $task => the task to process
- # Returns : n/a
- # Throws : fatal error if task fails
- # Comments : Must run from within target directory.
- # : Task involve either creating or deleting dirs and symlinks
- # : an action is set to 'skip' if it is found to be redundant
- #============================================================================
- sub process_task {
- my $self = shift;
- my ($task) = @_;
- if ($task->{action} eq 'create') {
- if ($task->{type} eq 'dir') {
- mkdir($task->{path}, 0777)
- or error("Could not create directory: $task->{path} ($!)");
- return;
- }
- elsif ($task->{type} eq 'link') {
- symlink $task->{source}, $task->{path}
- or error(
- "Could not create symlink: %s => %s ($!)",
- $task->{path},
- $task->{source}
- );
- return;
- }
- }
- elsif ($task->{action} eq 'remove') {
- if ($task->{type} eq 'dir') {
- rmdir $task->{path}
- or error("Could not remove directory: $task->{path} ($!)");
- return;
- }
- elsif ($task->{type} eq 'link') {
- unlink $task->{path}
- or error("Could not remove link: $task->{path} ($!)");
- return;
- }
- }
- elsif ($task->{action} eq 'move') {
- if ($task->{type} eq 'file') {
- # rename() not good enough, since the stow directory
- # might be on a different filesystem to the target.
- move $task->{path}, $task->{dest}
- or error("Could not move $task->{path} -> $task->{dest} ($!)");
- return;
- }
- }
- # Should never happen.
- internal_error("bad task action: $task->{action}");
- }
- #===== METHOD ===============================================================
- # Name : link_task_action()
- # Purpose : finds the link task action for the given path, if there is one
- # Parameters: $path
- # Returns : 'remove', 'create', or '' if there is no action
- # Throws : a fatal exception if an invalid action is found
- # Comments : none
- #============================================================================
- sub link_task_action {
- my $self = shift;
- my ($path) = @_;
- if (! exists $self->{link_task_for}{$path}) {
- debug(4, " link_task_action($path): no task");
- return '';
- }
- my $action = $self->{link_task_for}{$path}->{action};
- internal_error("bad task action: $action")
- unless $action eq 'remove' or $action eq 'create';
- debug(4, " link_task_action($path): link task exists with action $action");
- return $action;
- }
- #===== METHOD ===============================================================
- # Name : dir_task_action()
- # Purpose : finds the dir task action for the given path, if there is one
- # Parameters: $path
- # Returns : 'remove', 'create', or '' if there is no action
- # Throws : a fatal exception if an invalid action is found
- # Comments : none
- #============================================================================
- sub dir_task_action {
- my $self = shift;
- my ($path) = @_;
- if (! exists $self->{dir_task_for}{$path}) {
- debug(4, " dir_task_action($path): no task");
- return '';
- }
- my $action = $self->{dir_task_for}{$path}->{action};
- internal_error("bad task action: $action")
- unless $action eq 'remove' or $action eq 'create';
- debug(4, " dir_task_action($path): dir task exists with action $action");
- return $action;
- }
- #===== METHOD ===============================================================
- # Name : parent_link_scheduled_for_removal()
- # Purpose : determine whether the given path or any parent thereof
- # : is a link scheduled for removal
- # Parameters: $path
- # Returns : Boolean
- # Throws : none
- # Comments : none
- #============================================================================
- sub parent_link_scheduled_for_removal {
- my $self = shift;
- my ($path) = @_;
- my $prefix = '';
- for my $part (split m{/+}, $path) {
- $prefix = join_paths($prefix, $part);
- debug(4, " parent_link_scheduled_for_removal($path): prefix $prefix");
- if (exists $self->{link_task_for}{$prefix} and
- $self->{link_task_for}{$prefix}->{action} eq 'remove') {
- debug(4, " parent_link_scheduled_for_removal($path): link scheduled for removal");
- return 1;
- }
- }
- debug(4, " parent_link_scheduled_for_removal($path): returning false");
- return 0;
- }
- #===== METHOD ===============================================================
- # Name : is_a_link()
- # Purpose : determine if the given path is a current or planned link
- # Parameters: $path
- # Returns : Boolean
- # Throws : none
- # Comments : returns false if an existing link is scheduled for removal
- # : and true if a non-existent link is scheduled for creation
- #============================================================================
- sub is_a_link {
- my $self = shift;
- my ($path) = @_;
- debug(4, " is_a_link($path)");
- if (my $action = $self->link_task_action($path)) {
- if ($action eq 'remove') {
- debug(4, " is_a_link($path): returning 0 (remove action found)");
- return 0;
- }
- elsif ($action eq 'create') {
- debug(4, " is_a_link($path): returning 1 (create action found)");
- return 1;
- }
- }
- if (-l $path) {
- # Check if any of its parent are links scheduled for removal
- # (need this for edge case during unfolding)
- debug(4, " is_a_link($path): is a real link");
- return $self->parent_link_scheduled_for_removal($path) ? 0 : 1;
- }
- debug(4, " is_a_link($path): returning 0");
- return 0;
- }
- #===== METHOD ===============================================================
- # Name : is_a_dir()
- # Purpose : determine if the given path is a current or planned directory
- # Parameters: $path
- # 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 $self = shift;
- my ($path) = @_;
- debug(4, " is_a_dir($path)");
- if (my $action = $self->dir_task_action($path)) {
- if ($action eq 'remove') {
- return 0;
- }
- elsif ($action eq 'create') {
- return 1;
- }
- }
- return 0 if $self->parent_link_scheduled_for_removal($path);
- if (-d $path) {
- debug(4, " is_a_dir($path): real dir");
- return 1;
- }
- debug(4, " is_a_dir($path): returning false");
- return 0;
- }
- #===== METHOD ===============================================================
- # Name : is_a_node()
- # Purpose : determine whether the given path is a current or planned node
- # Parameters: $path
- # 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 $self = shift;
- my ($path) = @_;
- debug(4, " is_a_node($path)");
- my $laction = $self->link_task_action($path);
- my $daction = $self->dir_task_action($path);
- if ($laction eq 'remove') {
- if ($daction eq 'remove') {
- internal_error("removing link and dir: $path");
- return 0;
- }
- elsif ($daction eq 'create') {
- # Assume that we're unfolding $path, and that the link
- # removal action is earlier than the dir creation action
- # in the task queue. FIXME: is this a safe assumption?
- return 1;
- }
- else { # no dir action
- return 0;
- }
- }
- elsif ($laction eq 'create') {
- if ($daction eq 'remove') {
- # Assume that we're folding $path, and that the dir
- # removal action is earlier than the link creation action
- # in the task queue. FIXME: is this a safe assumption?
- return 1;
- }
- elsif ($daction eq 'create') {
- internal_error("creating link and dir: $path");
- return 1;
- }
- else { # no dir action
- return 1;
- }
- }
- else {
- # No link action
- if ($daction eq 'remove') {
- return 0;
- }
- elsif ($daction eq 'create') {
- return 1;
- }
- else { # no dir action
- # fall through to below
- }
- }
- return 0 if $self->parent_link_scheduled_for_removal($path);
- if (-e $path) {
- debug(4, " is_a_node($path): really exists");
- return 1;
- }
- debug(4, " is_a_node($path): returning false");
- return 0;
- }
- #===== METHOD ===============================================================
- # 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 $self = shift;
- my ($path) = @_;
- if (my $action = $self->link_task_action($path)) {
- debug(4, " read_a_link($path): task exists with action $action");
- if ($action eq 'create') {
- return $self->{link_task_for}{$path}->{source};
- }
- elsif ($action eq 'remove') {
- internal_error(
- "read_a_link() passed a path that is scheduled for removal: $path"
- );
- }
- }
- elsif (-l $path) {
- debug(4, " read_a_link($path): real link");
- my $target = readlink $path or error("Could not read link: $path ($!)");
- return $target;
- }
- internal_error("read_a_link() passed a non link path: $path\n");
- }
- #===== METHOD ===============================================================
- # Name : do_link()
- # Purpose : wrap 'link' operation for later processing
- # Parameters: $oldfile => the existing file to link to
- # : $newfile => 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 $self = shift;
- my ($oldfile, $newfile) = @_;
- if (exists $self->{dir_task_for}{$newfile}) {
- my $task_ref = $self->{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 $self->{link_task_for}{$newfile}) {
- my $task_ref = $self->{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 {
- debug(1, "LINK: $newfile => $oldfile (duplicates previous action)");
- return;
- }
- }
- elsif ($task_ref->{action} eq 'remove') {
- if ($task_ref->{source} eq $oldfile) {
- # No need to remove a link we are going to recreate
- debug(1, "LINK: $newfile => $oldfile (reverts previous action)");
- $self->{link_task_for}{$newfile}->{action} = 'skip';
- delete $self->{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
- debug(1, "LINK: $newfile => $oldfile");
- my $task = {
- action => 'create',
- type => 'link',
- path => $newfile,
- source => $oldfile,
- };
- push @{ $self->{tasks} }, $task;
- $self->{link_task_for}{$newfile} = $task;
- return;
- }
- #===== METHOD ===============================================================
- # 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 $self = shift;
- my ($file) = @_;
- if (exists $self->{link_task_for}{$file}) {
- my $task_ref = $self->{link_task_for}{$file};
- if ($task_ref->{action} eq 'remove') {
- debug(1, "UNLINK: $file (duplicates previous action)");
- return;
- }
- elsif ($task_ref->{action} eq 'create') {
- # Do need to create a link then remove it
- debug(1, "UNLINK: $file (reverts previous action)");
- $self->{link_task_for}{$file}->{action} = 'skip';
- delete $self->{link_task_for}{$file};
- return;
- }
- else {
- internal_error("bad task action: $task_ref->{action}");
- }
- }
- if (exists $self->{dir_task_for}{$file} and $self->{dir_task_for}{$file} eq 'create') {
- internal_error(
- "new unlink operation clashes with planned operation: %s dir %s",
- $self->{dir_task_for}{$file}->{action},
- $file
- );
- }
- # Remove the link
- debug(1, "UNLINK: $file");
- my $source = readlink $file or error("could not readlink $file ($!)");
- my $task = {
- action => 'remove',
- type => 'link',
- path => $file,
- source => $source,
- };
- push @{ $self->{tasks} }, $task;
- $self->{link_task_for}{$file} = $task;
- return;
- }
- #===== METHOD ===============================================================
- # 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 $self = shift;
- my ($dir) = @_;
- if (exists $self->{link_task_for}{$dir}) {
- my $task_ref = $self->{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 $self->{dir_task_for}{$dir}) {
- my $task_ref = $self->{dir_task_for}{$dir};
- if ($task_ref->{action} eq 'create') {
- debug(1, "MKDIR: $dir (duplicates previous action)");
- return;
- }
- elsif ($task_ref->{action} eq 'remove') {
- debug(1, "MKDIR: $dir (reverts previous action)");
- $self->{dir_task_for}{$dir}->{action} = 'skip';
- delete $self->{dir_task_for}{$dir};
- return;
- }
- else {
- internal_error("bad task action: $task_ref->{action}");
- }
- }
- debug(1, "MKDIR: $dir");
- my $task = {
- action => 'create',
- type => 'dir',
- path => $dir,
- source => undef,
- };
- push @{ $self->{tasks} }, $task;
- $self->{dir_task_for}{$dir} = $task;
- return;
- }
- #===== METHOD ===============================================================
- # 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 $self = shift;
- my ($dir) = @_;
- if (exists $self->{link_task_for}{$dir}) {
- my $task_ref = $self->{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 $self->{dir_task_for}{$dir}) {
- my $task_ref = $self->{link_task_for}{$dir};
- if ($task_ref->{action} eq 'remove') {
- debug(1, "RMDIR $dir (duplicates previous action)");
- return;
- }
- elsif ($task_ref->{action} eq 'create') {
- debug(1, "MKDIR $dir (reverts previous action)");
- $self->{link_task_for}{$dir}->{action} = 'skip';
- delete $self->{link_task_for}{$dir};
- return;
- }
- else {
- internal_error("bad task action: $task_ref->{action}");
- }
- }
- debug(1, "RMDIR $dir");
- my $task = {
- action => 'remove',
- type => 'dir',
- path => $dir,
- source => '',
- };
- push @{ $self->{tasks} }, $task;
- $self->{dir_task_for}{$dir} = $task;
- return;
- }
- #===== METHOD ===============================================================
- # Name : do_mv()
- # Purpose : wrap 'move' operation for later processing
- # Parameters: $src => the file to move
- # : $dst => the path to move it to
- # Returns : n/a
- # Throws : error if this clashes with an existing planned operation
- # Comments : alters contents of package installation image in stow dir
- #============================================================================
- sub do_mv {
- my $self = shift;
- my ($src, $dst) = @_;
- if (exists $self->{link_task_for}{$src}) {
- # I don't *think* this should ever happen, but I'm not
- # 100% sure.
- my $task_ref = $self->{link_task_for}{$src};
- internal_error(
- "do_mv: pre-existing link task for $src; action: %s, source: %s",
- $task_ref->{action}, $task_ref->{source}
- );
- }
- elsif (exists $self->{dir_task_for}{$src}) {
- my $task_ref = $self->{dir_task_for}{$src};
- internal_error(
- "do_mv: pre-existing dir task for %s?! action: %s",
- $src, $task_ref->{action}
- );
- }
- # Remove the link
- debug(1, "MV: $src -> $dst");
- my $task = {
- action => 'move',
- type => 'file',
- path => $src,
- dest => $dst,
- };
- push @{ $self->{tasks} }, $task;
- # FIXME: do we need this for anything?
- #$self->{mv_task_for}{$file} = $task;
- return;
- }
- #############################################################################
- #
- # End of methods; subroutines follow.
- # FIXME: Ideally these should be in a separate module.
- #===== PRIVATE 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) = @_;
- my $error = sprintf($format, @args);
- my $stacktrace = Carp::longmess();
- die <<EOF;
- $ProgramName: INTERNAL ERROR: $error$stacktrace
- This _is_ a bug. Please submit a bug report so we can fix it! :-)
- See http://www.gnu.org/software/stow/ for how to do this.
- EOF
- }
- =head1 BUGS
- =head1 SEE ALSO
- =cut
- 1;
- # Local variables:
- # mode: perl
- # cperl-indent-level: 4
- # end:
- # vim: ft=perl
- #############################################################################
- # Default global list of ignore regexps follows
- # (automatically appended by the Makefile)
- __DATA__
|