diff options
author | Mike Crute <mike@crute.us> | 2020-06-01 17:29:10 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2020-06-01 17:29:10 -0700 |
commit | 7d51c7d60ef8b8f7fb717a418945e6116df83032 (patch) | |
tree | 12e4ddb6d505b500700723399267cd2388a8498f /bin | |
parent | 330ba93bf5a2465422ba1170f3c8aa5b8866393b (diff) | |
download | dotfiles-7d51c7d60ef8b8f7fb717a418945e6116df83032.tar.bz2 dotfiles-7d51c7d60ef8b8f7fb717a418945e6116df83032.tar.xz dotfiles-7d51c7d60ef8b8f7fb717a418945e6116df83032.zip |
Combine stow into one file
Diffstat (limited to 'bin')
-rw-r--r-- | bin/Stow.pm | 2299 | ||||
-rwxr-xr-x | bin/stow | 2311 |
2 files changed, 2307 insertions, 2303 deletions
diff --git a/bin/Stow.pm b/bin/Stow.pm deleted file mode 100644 index c63e164..0000000 --- a/bin/Stow.pm +++ /dev/null | |||
@@ -1,2299 +0,0 @@ | |||
1 | #!/usr/bin/perl | ||
2 | |||
3 | package Stow; | ||
4 | |||
5 | =head1 NAME | ||
6 | |||
7 | Stow - manage the installation of multiple software packages | ||
8 | |||
9 | =head1 SYNOPSIS | ||
10 | |||
11 | my $stow = new Stow(%$options); | ||
12 | |||
13 | $stow->plan_unstow(@pkgs_to_unstow); | ||
14 | $stow->plan_stow (@pkgs_to_stow); | ||
15 | |||
16 | my %conflicts = $stow->get_conflicts; | ||
17 | $stow->process_tasks() unless %conflicts; | ||
18 | |||
19 | =head1 DESCRIPTION | ||
20 | |||
21 | This is the backend Perl module for GNU Stow, a program for managing | ||
22 | the installation of software packages, keeping them separate | ||
23 | (C</usr/local/stow/emacs> vs. C</usr/local/stow/perl>, for example) | ||
24 | while making them appear to be installed in the same place | ||
25 | (C</usr/local>). | ||
26 | |||
27 | Stow doesn't store an extra state between runs, so there's no danger | ||
28 | of mangling directories when file hierarchies don't match the | ||
29 | database. Also, stow will never delete any files, directories, or | ||
30 | links that appear in a stow directory, so it is always possible to | ||
31 | rebuild the target tree. | ||
32 | |||
33 | =cut | ||
34 | |||
35 | use strict; | ||
36 | use warnings; | ||
37 | |||
38 | use Carp qw(carp cluck croak confess longmess); | ||
39 | use File::Copy qw(move); | ||
40 | use File::Spec; | ||
41 | use POSIX qw(getcwd); | ||
42 | |||
43 | =head1 NAME | ||
44 | |||
45 | Stow::Util - general utilities | ||
46 | |||
47 | =head1 SYNOPSIS | ||
48 | |||
49 | use Stow::Util qw(debug set_debug_level error ...); | ||
50 | |||
51 | =head1 DESCRIPTION | ||
52 | |||
53 | Supporting utility routines for L<Stow>. | ||
54 | |||
55 | =cut | ||
56 | |||
57 | use base qw(Exporter); | ||
58 | our @EXPORT_OK = qw( | ||
59 | error debug set_debug_level set_test_mode | ||
60 | join_paths parent canon_path restore_cwd | ||
61 | ); | ||
62 | |||
63 | our $ProgramName = 'stow'; | ||
64 | our $VERSION = '2.2.2'; | ||
65 | |||
66 | ############################################################################# | ||
67 | # | ||
68 | # General Utilities: nothing stow specific here. | ||
69 | # | ||
70 | ############################################################################# | ||
71 | |||
72 | =head1 IMPORTABLE SUBROUTINES | ||
73 | |||
74 | =head2 error($format, @args) | ||
75 | |||
76 | Outputs an error message in a consistent form and then dies. | ||
77 | |||
78 | =cut | ||
79 | |||
80 | sub error { | ||
81 | my ($format, @args) = @_; | ||
82 | die "$ProgramName: ERROR: " . sprintf($format, @args) . "\n"; | ||
83 | } | ||
84 | |||
85 | =head2 set_debug_level($level) | ||
86 | |||
87 | Sets verbosity level for C<debug()>. | ||
88 | |||
89 | =cut | ||
90 | |||
91 | our $debug_level = 0; | ||
92 | |||
93 | sub set_debug_level { | ||
94 | my ($level) = @_; | ||
95 | $debug_level = $level; | ||
96 | } | ||
97 | |||
98 | =head2 set_test_mode($on_or_off) | ||
99 | |||
100 | Sets testmode on or off. | ||
101 | |||
102 | =cut | ||
103 | |||
104 | our $test_mode = 0; | ||
105 | |||
106 | sub set_test_mode { | ||
107 | my ($on_or_off) = @_; | ||
108 | if ($on_or_off) { | ||
109 | $test_mode = 1; | ||
110 | } | ||
111 | else { | ||
112 | $test_mode = 0; | ||
113 | } | ||
114 | } | ||
115 | |||
116 | =head2 debug($level, $msg) | ||
117 | |||
118 | Logs to STDERR based on C<$debug_level> setting. C<$level> is the | ||
119 | minimum verbosity level required to output C<$msg>. All output is to | ||
120 | STDERR to preserve backward compatibility, except for in test mode, | ||
121 | when STDOUT is used instead. In test mode, the verbosity can be | ||
122 | overridden via the C<TEST_VERBOSE> environment variable. | ||
123 | |||
124 | Verbosity rules: | ||
125 | |||
126 | =over 4 | ||
127 | |||
128 | =item 0: errors only | ||
129 | |||
130 | =item >= 1: print operations: LINK/UNLINK/MKDIR/RMDIR/MV | ||
131 | |||
132 | =item >= 2: print operation exceptions | ||
133 | |||
134 | e.g. "_this_ already points to _that_", skipping, deferring, | ||
135 | overriding, fixing invalid links | ||
136 | |||
137 | =item >= 3: print trace detail: trace: stow/unstow/package/contents/node | ||
138 | |||
139 | =item >= 4: debug helper routines | ||
140 | |||
141 | =item >= 5: debug ignore lists | ||
142 | |||
143 | =back | ||
144 | |||
145 | =cut | ||
146 | |||
147 | sub debug { | ||
148 | my ($level, $msg) = @_; | ||
149 | if ($debug_level >= $level) { | ||
150 | if ($test_mode) { | ||
151 | print "# $msg\n"; | ||
152 | } | ||
153 | else { | ||
154 | warn "$msg\n"; | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | |||
159 | #===== METHOD =============================================================== | ||
160 | # Name : join_paths() | ||
161 | # Purpose : concatenates given paths | ||
162 | # Parameters: path1, path2, ... => paths | ||
163 | # Returns : concatenation of given paths | ||
164 | # Throws : n/a | ||
165 | # Comments : factors out redundant path elements: | ||
166 | # : '//' => '/' and 'a/b/../c' => 'a/c' | ||
167 | #============================================================================ | ||
168 | sub join_paths { | ||
169 | my @paths = @_; | ||
170 | |||
171 | # weed out empty components and concatenate | ||
172 | my $result = join '/', grep {! /\A\z/} @paths; | ||
173 | |||
174 | # factor out back references and remove redundant /'s) | ||
175 | my @result = (); | ||
176 | PART: | ||
177 | for my $part (split m{/+}, $result) { | ||
178 | next PART if $part eq '.'; | ||
179 | if (@result && $part eq '..' && $result[-1] ne '..') { | ||
180 | pop @result; | ||
181 | } | ||
182 | else { | ||
183 | push @result, $part; | ||
184 | } | ||
185 | } | ||
186 | |||
187 | return join '/', @result; | ||
188 | } | ||
189 | |||
190 | #===== METHOD =============================================================== | ||
191 | # Name : parent | ||
192 | # Purpose : find the parent of the given path | ||
193 | # Parameters: @path => components of the path | ||
194 | # Returns : returns a path string | ||
195 | # Throws : n/a | ||
196 | # Comments : allows you to send multiple chunks of the path | ||
197 | # : (this feature is currently not used) | ||
198 | #============================================================================ | ||
199 | sub parent { | ||
200 | my @path = @_; | ||
201 | my $path = join '/', @_; | ||
202 | my @elts = split m{/+}, $path; | ||
203 | pop @elts; | ||
204 | return join '/', @elts; | ||
205 | } | ||
206 | |||
207 | #===== METHOD =============================================================== | ||
208 | # Name : canon_path | ||
209 | # Purpose : find absolute canonical path of given path | ||
210 | # Parameters: $path | ||
211 | # Returns : absolute canonical path | ||
212 | # Throws : n/a | ||
213 | # Comments : is this significantly different from File::Spec->rel2abs? | ||
214 | #============================================================================ | ||
215 | sub canon_path { | ||
216 | my ($path) = @_; | ||
217 | |||
218 | my $cwd = getcwd(); | ||
219 | chdir($path) or error("canon_path: cannot chdir to $path from $cwd"); | ||
220 | my $canon_path = getcwd(); | ||
221 | restore_cwd($cwd); | ||
222 | |||
223 | return $canon_path; | ||
224 | } | ||
225 | |||
226 | sub restore_cwd { | ||
227 | my ($prev) = @_; | ||
228 | chdir($prev) or error("Your current directory $prev seems to have vanished"); | ||
229 | } | ||
230 | |||
231 | =head1 BUGS | ||
232 | |||
233 | =head1 SEE ALSO | ||
234 | |||
235 | =cut | ||
236 | |||
237 | |||
238 | our $LOCAL_IGNORE_FILE = '.stow-local-ignore'; | ||
239 | our $GLOBAL_IGNORE_FILE = '.stow-global-ignore'; | ||
240 | |||
241 | our @default_global_ignore_regexps = | ||
242 | __PACKAGE__->get_default_global_ignore_regexps(); | ||
243 | |||
244 | # These are the default options for each Stow instance. | ||
245 | our %DEFAULT_OPTIONS = ( | ||
246 | conflicts => 0, | ||
247 | simulate => 0, | ||
248 | verbose => 0, | ||
249 | paranoid => 0, | ||
250 | compat => 0, | ||
251 | test_mode => 0, | ||
252 | adopt => 0, | ||
253 | 'no-folding' => 0, | ||
254 | ignore => [], | ||
255 | override => [], | ||
256 | defer => [], | ||
257 | ); | ||
258 | |||
259 | =head1 CONSTRUCTORS | ||
260 | |||
261 | =head2 new(%options) | ||
262 | |||
263 | =head3 Required options | ||
264 | |||
265 | =over 4 | ||
266 | |||
267 | =item * dir - the stow directory | ||
268 | |||
269 | =item * target - the target directory | ||
270 | |||
271 | =back | ||
272 | |||
273 | =head3 Non-mandatory options | ||
274 | |||
275 | See the documentation for the F<stow> CLI front-end for information on these. | ||
276 | |||
277 | =over 4 | ||
278 | |||
279 | =item * conflicts | ||
280 | |||
281 | =item * simulate | ||
282 | |||
283 | =item * verbose | ||
284 | |||
285 | =item * paranoid | ||
286 | |||
287 | =item * compat | ||
288 | |||
289 | =item * test_mode | ||
290 | |||
291 | =item * adopt | ||
292 | |||
293 | =item * no-folding | ||
294 | |||
295 | =item * ignore | ||
296 | |||
297 | =item * override | ||
298 | |||
299 | =item * defer | ||
300 | |||
301 | =back | ||
302 | |||
303 | N.B. This sets the current working directory to the target directory. | ||
304 | |||
305 | =cut | ||
306 | |||
307 | sub new { | ||
308 | my $self = shift; | ||
309 | my $class = ref($self) || $self; | ||
310 | my %opts = @_; | ||
311 | |||
312 | my $new = bless { }, $class; | ||
313 | |||
314 | $new->{action_count} = 0; | ||
315 | |||
316 | for my $required_arg (qw(dir target)) { | ||
317 | croak "$class->new() called without '$required_arg' parameter\n" | ||
318 | unless exists $opts{$required_arg}; | ||
319 | $new->{$required_arg} = delete $opts{$required_arg}; | ||
320 | } | ||
321 | |||
322 | for my $opt (keys %DEFAULT_OPTIONS) { | ||
323 | $new->{$opt} = exists $opts{$opt} ? delete $opts{$opt} | ||
324 | : $DEFAULT_OPTIONS{$opt}; | ||
325 | } | ||
326 | |||
327 | if (%opts) { | ||
328 | croak "$class->new() called with unrecognised parameter(s): ", | ||
329 | join(", ", keys %opts), "\n"; | ||
330 | } | ||
331 | |||
332 | set_debug_level($new->get_verbosity()); | ||
333 | set_test_mode($new->{test_mode}); | ||
334 | $new->set_stow_dir(); | ||
335 | $new->init_state(); | ||
336 | |||
337 | return $new; | ||
338 | } | ||
339 | |||
340 | sub get_verbosity { | ||
341 | my $self = shift; | ||
342 | |||
343 | return $self->{verbose} unless $self->{test_mode}; | ||
344 | |||
345 | return 0 unless exists $ENV{TEST_VERBOSE}; | ||
346 | return 0 unless length $ENV{TEST_VERBOSE}; | ||
347 | |||
348 | # Convert TEST_VERBOSE=y into numeric value | ||
349 | $ENV{TEST_VERBOSE} = 3 if $ENV{TEST_VERBOSE} !~ /^\d+$/; | ||
350 | |||
351 | return $ENV{TEST_VERBOSE}; | ||
352 | } | ||
353 | |||
354 | =head2 set_stow_dir([$dir]) | ||
355 | |||
356 | Sets a new stow directory. This allows the use of multiple stow | ||
357 | directories within one Stow instance, e.g. | ||
358 | |||
359 | $stow->plan_stow('foo'); | ||
360 | $stow->set_stow_dir('/different/stow/dir'); | ||
361 | $stow->plan_stow('bar'); | ||
362 | $stow->process_tasks; | ||
363 | |||
364 | If C<$dir> is omitted, uses the value of the C<dir> parameter passed | ||
365 | to the L<new()> constructor. | ||
366 | |||
367 | =cut | ||
368 | |||
369 | sub set_stow_dir { | ||
370 | my $self = shift; | ||
371 | my ($dir) = @_; | ||
372 | if (defined $dir) { | ||
373 | $self->{dir} = $dir; | ||
374 | } | ||
375 | |||
376 | my $stow_dir = canon_path($self->{dir}); | ||
377 | my $target = canon_path($self->{target}); | ||
378 | $self->{stow_path} = File::Spec->abs2rel($stow_dir, $target); | ||
379 | |||
380 | debug(2, "stow dir is $stow_dir"); | ||
381 | debug(2, "stow dir path relative to target $target is $self->{stow_path}"); | ||
382 | } | ||
383 | |||
384 | sub init_state { | ||
385 | my $self = shift; | ||
386 | |||
387 | # Store conflicts during pre-processing | ||
388 | $self->{conflicts} = {}; | ||
389 | $self->{conflict_count} = 0; | ||
390 | |||
391 | # Store command line packages to stow (-S and -R) | ||
392 | $self->{pkgs_to_stow} = []; | ||
393 | |||
394 | # Store command line packages to unstow (-D and -R) | ||
395 | $self->{pkgs_to_delete} = []; | ||
396 | |||
397 | # The following structures are used by the abstractions that allow us to | ||
398 | # defer operating on the filesystem until after all potential conflicts have | ||
399 | # been assessed. | ||
400 | |||
401 | # $self->{tasks}: list of operations to be performed (in order) | ||
402 | # each element is a hash ref of the form | ||
403 | # { | ||
404 | # action => ... ('create' or 'remove' or 'move') | ||
405 | # type => ... ('link' or 'dir' or 'file') | ||
406 | # path => ... (unique) | ||
407 | # source => ... (only for links) | ||
408 | # dest => ... (only for moving files) | ||
409 | # } | ||
410 | $self->{tasks} = []; | ||
411 | |||
412 | # $self->{dir_task_for}: map a path to the corresponding directory task reference | ||
413 | # This structure allows us to quickly determine if a path has an existing | ||
414 | # directory task associated with it. | ||
415 | $self->{dir_task_for} = {}; | ||
416 | |||
417 | # $self->{link_task_for}: map a path to the corresponding directory task reference | ||
418 | # This structure allows us to quickly determine if a path has an existing | ||
419 | # directory task associated with it. | ||
420 | $self->{link_task_for} = {}; | ||
421 | |||
422 | # N.B.: directory tasks and link tasks are NOT mutually exclusive due | ||
423 | # to tree splitting (which involves a remove link task followed by | ||
424 | # a create directory task). | ||
425 | } | ||
426 | |||
427 | =head1 METHODS | ||
428 | |||
429 | =head2 plan_unstow(@packages) | ||
430 | |||
431 | Plan which symlink/directory creation/removal tasks need to be executed | ||
432 | in order to unstow the given packages. Any potential conflicts are then | ||
433 | accessible via L<get_conflicts()>. | ||
434 | |||
435 | =cut | ||
436 | |||
437 | sub plan_unstow { | ||
438 | my $self = shift; | ||
439 | my @packages = @_; | ||
440 | |||
441 | $self->within_target_do(sub { | ||
442 | for my $package (@packages) { | ||
443 | my $path = join_paths($self->{stow_path}, $package); | ||
444 | if (not -d $path) { | ||
445 | error("The stow directory $self->{stow_path} does not contain package $package"); | ||
446 | } | ||
447 | debug(2, "Planning unstow of package $package..."); | ||
448 | if ($self->{compat}) { | ||
449 | $self->unstow_contents_orig( | ||
450 | $self->{stow_path}, | ||
451 | $package, | ||
452 | '.', | ||
453 | ); | ||
454 | } | ||
455 | else { | ||
456 | $self->unstow_contents( | ||
457 | $self->{stow_path}, | ||
458 | $package, | ||
459 | '.', | ||
460 | ); | ||
461 | } | ||
462 | debug(2, "Planning unstow of package $package... done"); | ||
463 | $self->{action_count}++; | ||
464 | } | ||
465 | }); | ||
466 | } | ||
467 | |||
468 | =head2 plan_stow(@packages) | ||
469 | |||
470 | Plan which symlink/directory creation/removal tasks need to be executed | ||
471 | in order to stow the given packages. Any potential conflicts are then | ||
472 | accessible via L<get_conflicts()>. | ||
473 | |||
474 | =cut | ||
475 | |||
476 | sub plan_stow { | ||
477 | my $self = shift; | ||
478 | my @packages = @_; | ||
479 | |||
480 | $self->within_target_do(sub { | ||
481 | for my $package (@packages) { | ||
482 | my $path = join_paths($self->{stow_path}, $package); | ||
483 | if (not -d $path) { | ||
484 | error("The stow directory $self->{stow_path} does not contain package $package"); | ||
485 | } | ||
486 | debug(2, "Planning stow of package $package..."); | ||
487 | $self->stow_contents( | ||
488 | $self->{stow_path}, | ||
489 | $package, | ||
490 | '.', | ||
491 | $path, # source from target | ||
492 | ); | ||
493 | debug(2, "Planning stow of package $package... done"); | ||
494 | $self->{action_count}++; | ||
495 | } | ||
496 | }); | ||
497 | } | ||
498 | |||
499 | #===== METHOD =============================================================== | ||
500 | # Name : within_target_do() | ||
501 | # Purpose : execute code within target directory, preserving cwd | ||
502 | # Parameters: $code => anonymous subroutine to execute within target dir | ||
503 | # Returns : n/a | ||
504 | # Throws : n/a | ||
505 | # Comments : This is done to ensure that the consumer of the Stow interface | ||
506 | # : doesn't have to worry about (a) what their cwd is, and | ||
507 | # : (b) that their cwd might change. | ||
508 | #============================================================================ | ||
509 | sub within_target_do { | ||
510 | my $self = shift; | ||
511 | my ($code) = @_; | ||
512 | |||
513 | my $cwd = getcwd(); | ||
514 | chdir($self->{target}) | ||
515 | or error("Cannot chdir to target tree: $self->{target} ($!)"); | ||
516 | debug(3, "cwd now $self->{target}"); | ||
517 | |||
518 | $self->$code(); | ||
519 | |||
520 | restore_cwd($cwd); | ||
521 | debug(3, "cwd restored to $cwd"); | ||
522 | } | ||
523 | |||
524 | #===== METHOD =============================================================== | ||
525 | # Name : stow_contents() | ||
526 | # Purpose : stow the contents of the given directory | ||
527 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
528 | # : to the stow dir containing the package to be stowed | ||
529 | # : $package => the package whose contents are being stowed | ||
530 | # : $target => subpath relative to package and target directories | ||
531 | # : $source => relative path from the (sub)dir of target | ||
532 | # : to symlink source | ||
533 | # Returns : n/a | ||
534 | # Throws : a fatal error if directory cannot be read | ||
535 | # Comments : stow_node() and stow_contents() are mutually recursive. | ||
536 | # : $source and $target are used for creating the symlink | ||
537 | # : $path is used for folding/unfolding trees as necessary | ||
538 | #============================================================================ | ||
539 | sub stow_contents { | ||
540 | my $self = shift; | ||
541 | my ($stow_path, $package, $target, $source) = @_; | ||
542 | |||
543 | my $path = join_paths($stow_path, $package, $target); | ||
544 | |||
545 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
546 | |||
547 | my $cwd = getcwd(); | ||
548 | my $msg = "Stowing contents of $path (cwd=$cwd)"; | ||
549 | $msg =~ s!$ENV{HOME}(/|$)!~$1!g; | ||
550 | debug(3, $msg); | ||
551 | debug(4, " => $source"); | ||
552 | |||
553 | error("stow_contents() called with non-directory path: $path") | ||
554 | unless -d $path; | ||
555 | error("stow_contents() called with non-directory target: $target") | ||
556 | unless $self->is_a_node($target); | ||
557 | |||
558 | opendir my $DIR, $path | ||
559 | or error("cannot read directory: $path ($!)"); | ||
560 | my @listing = readdir $DIR; | ||
561 | closedir $DIR; | ||
562 | |||
563 | NODE: | ||
564 | for my $node (@listing) { | ||
565 | next NODE if $node eq '.'; | ||
566 | next NODE if $node eq '..'; | ||
567 | my $node_target = join_paths($target, $node); | ||
568 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
569 | $self->stow_node( | ||
570 | $stow_path, | ||
571 | $package, | ||
572 | $node_target, # target | ||
573 | join_paths($source, $node), # source | ||
574 | ); | ||
575 | } | ||
576 | } | ||
577 | |||
578 | #===== METHOD =============================================================== | ||
579 | # Name : stow_node() | ||
580 | # Purpose : stow the given node | ||
581 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
582 | # : to the stow dir containing the node to be stowed | ||
583 | # : $package => the package containing the node being stowed | ||
584 | # : $target => subpath relative to package and target directories | ||
585 | # : $source => relative path to symlink source from the dir of target | ||
586 | # Returns : n/a | ||
587 | # Throws : fatal exception if a conflict arises | ||
588 | # Comments : stow_node() and stow_contents() are mutually recursive | ||
589 | # : $source and $target are used for creating the symlink | ||
590 | # : $path is used for folding/unfolding trees as necessary | ||
591 | #============================================================================ | ||
592 | sub stow_node { | ||
593 | my $self = shift; | ||
594 | my ($stow_path, $package, $target, $source) = @_; | ||
595 | |||
596 | my $path = join_paths($stow_path, $package, $target); | ||
597 | |||
598 | debug(3, "Stowing $stow_path / $package / $target"); | ||
599 | debug(4, " => $source"); | ||
600 | |||
601 | # Don't try to stow absolute symlinks (they can't be unstowed) | ||
602 | if (-l $source) { | ||
603 | my $second_source = $self->read_a_link($source); | ||
604 | if ($second_source =~ m{\A/}) { | ||
605 | $self->conflict( | ||
606 | 'stow', | ||
607 | $package, | ||
608 | "source is an absolute symlink $source => $second_source" | ||
609 | ); | ||
610 | debug(3, "Absolute symlinks cannot be unstowed"); | ||
611 | return; | ||
612 | } | ||
613 | } | ||
614 | |||
615 | # Does the target already exist? | ||
616 | if ($self->is_a_link($target)) { | ||
617 | # Where is the link pointing? | ||
618 | my $existing_source = $self->read_a_link($target); | ||
619 | if (not $existing_source) { | ||
620 | error("Could not read link: $target"); | ||
621 | } | ||
622 | debug(4, " Evaluate existing link: $target => $existing_source"); | ||
623 | |||
624 | # Does it point to a node under any stow directory? | ||
625 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
626 | $self->find_stowed_path($target, $existing_source); | ||
627 | if (not $existing_path) { | ||
628 | $self->conflict( | ||
629 | 'stow', | ||
630 | $package, | ||
631 | "existing target is not owned by stow: $target" | ||
632 | ); | ||
633 | return; # XXX # | ||
634 | } | ||
635 | |||
636 | # Does the existing $target actually point to anything? | ||
637 | if ($self->is_a_node($existing_path)) { | ||
638 | if ($existing_source eq $source) { | ||
639 | debug(2, "--- Skipping $target as it already points to $source"); | ||
640 | } | ||
641 | elsif ($self->defer($target)) { | ||
642 | debug(2, "--- Deferring installation of: $target"); | ||
643 | } | ||
644 | elsif ($self->override($target)) { | ||
645 | debug(2, "--- Overriding installation of: $target"); | ||
646 | $self->do_unlink($target); | ||
647 | $self->do_link($source, $target); | ||
648 | } | ||
649 | elsif ($self->is_a_dir(join_paths(parent($target), $existing_source)) && | ||
650 | $self->is_a_dir(join_paths(parent($target), $source)) ) { | ||
651 | |||
652 | # If the existing link points to a directory, | ||
653 | # and the proposed new link points to a directory, | ||
654 | # then we can unfold (split open) the tree at that point | ||
655 | |||
656 | debug(2, "--- Unfolding $target which was already owned by $existing_package"); | ||
657 | $self->do_unlink($target); | ||
658 | $self->do_mkdir($target); | ||
659 | $self->stow_contents( | ||
660 | $existing_stow_path, | ||
661 | $existing_package, | ||
662 | $target, | ||
663 | join_paths('..', $existing_source), | ||
664 | ); | ||
665 | $self->stow_contents( | ||
666 | $self->{stow_path}, | ||
667 | $package, | ||
668 | $target, | ||
669 | join_paths('..', $source), | ||
670 | ); | ||
671 | } | ||
672 | else { | ||
673 | $self->conflict( | ||
674 | 'stow', | ||
675 | $package, | ||
676 | "existing target is stowed to a different package: " | ||
677 | . "$target => $existing_source" | ||
678 | ); | ||
679 | } | ||
680 | } | ||
681 | else { | ||
682 | # The existing link is invalid, so replace it with a good link | ||
683 | debug(2, "--- replacing invalid link: $path"); | ||
684 | $self->do_unlink($target); | ||
685 | $self->do_link($source, $target); | ||
686 | } | ||
687 | } | ||
688 | elsif ($self->is_a_node($target)) { | ||
689 | debug(4, " Evaluate existing node: $target"); | ||
690 | if ($self->is_a_dir($target)) { | ||
691 | $self->stow_contents( | ||
692 | $self->{stow_path}, | ||
693 | $package, | ||
694 | $target, | ||
695 | join_paths('..', $source), | ||
696 | ); | ||
697 | } | ||
698 | else { | ||
699 | if ($self->{adopt}) { | ||
700 | $self->do_mv($target, $path); | ||
701 | $self->do_link($source, $target); | ||
702 | } | ||
703 | else { | ||
704 | $self->conflict( | ||
705 | 'stow', | ||
706 | $package, | ||
707 | "existing target is neither a link nor a directory: $target" | ||
708 | ); | ||
709 | } | ||
710 | } | ||
711 | } | ||
712 | elsif ($self->{'no-folding'} && -d $path && ! -l $path) { | ||
713 | $self->do_mkdir($target); | ||
714 | $self->stow_contents( | ||
715 | $self->{stow_path}, | ||
716 | $package, | ||
717 | $target, | ||
718 | join_paths('..', $source), | ||
719 | ); | ||
720 | } | ||
721 | else { | ||
722 | $self->do_link($source, $target); | ||
723 | } | ||
724 | return; | ||
725 | } | ||
726 | |||
727 | #===== METHOD =============================================================== | ||
728 | # Name : should_skip_target_which_is_stow_dir() | ||
729 | # Purpose : determine whether target is a stow directory which should | ||
730 | # : not be stowed to or unstowed from | ||
731 | # Parameters: $target => relative path to symlink target from the current directory | ||
732 | # Returns : true iff target is a stow directory | ||
733 | # Throws : n/a | ||
734 | # Comments : none | ||
735 | #============================================================================ | ||
736 | sub should_skip_target_which_is_stow_dir { | ||
737 | my $self = shift; | ||
738 | my ($target) = @_; | ||
739 | |||
740 | # Don't try to remove anything under a stow directory | ||
741 | if ($target eq $self->{stow_path}) { | ||
742 | warn "WARNING: skipping target which was current stow directory $target\n"; | ||
743 | return 1; | ||
744 | } | ||
745 | |||
746 | if ($self->marked_stow_dir($target)) { | ||
747 | warn "WARNING: skipping protected directory $target\n"; | ||
748 | return 1; | ||
749 | } | ||
750 | |||
751 | debug (4, "$target not protected"); | ||
752 | return 0; | ||
753 | } | ||
754 | |||
755 | sub marked_stow_dir { | ||
756 | my $self = shift; | ||
757 | my ($target) = @_; | ||
758 | for my $f (".stow", ".nonstow") { | ||
759 | if (-e join_paths($target, $f)) { | ||
760 | debug(4, "$target contained $f"); | ||
761 | return 1; | ||
762 | } | ||
763 | } | ||
764 | return 0; | ||
765 | } | ||
766 | |||
767 | #===== METHOD =============================================================== | ||
768 | # Name : unstow_contents_orig() | ||
769 | # Purpose : unstow the contents of the given directory | ||
770 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
771 | # : to the stow dir containing the package to be unstowed | ||
772 | # : $package => the package whose contents are being unstowed | ||
773 | # : $target => relative path to symlink target from the current directory | ||
774 | # Returns : n/a | ||
775 | # Throws : a fatal error if directory cannot be read | ||
776 | # Comments : unstow_node_orig() and unstow_contents_orig() are mutually recursive | ||
777 | # : Here we traverse the target tree, rather than the source tree. | ||
778 | #============================================================================ | ||
779 | sub unstow_contents_orig { | ||
780 | my $self = shift; | ||
781 | my ($stow_path, $package, $target) = @_; | ||
782 | |||
783 | my $path = join_paths($stow_path, $package, $target); | ||
784 | |||
785 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
786 | |||
787 | my $cwd = getcwd(); | ||
788 | my $msg = "Unstowing from $target (compat mode, cwd=$cwd, stow dir=$self->{stow_path})"; | ||
789 | $msg =~ s!$ENV{HOME}(/|$)!~$1!g; | ||
790 | debug(3, $msg); | ||
791 | debug(4, " source path is $path"); | ||
792 | # In compat mode we traverse the target tree not the source tree, | ||
793 | # so we're unstowing the contents of /target/foo, there's no | ||
794 | # guarantee that the corresponding /stow/mypkg/foo exists. | ||
795 | error("unstow_contents_orig() called with non-directory target: $target") | ||
796 | unless -d $target; | ||
797 | |||
798 | opendir my $DIR, $target | ||
799 | or error("cannot read directory: $target ($!)"); | ||
800 | my @listing = readdir $DIR; | ||
801 | closedir $DIR; | ||
802 | |||
803 | NODE: | ||
804 | for my $node (@listing) { | ||
805 | next NODE if $node eq '.'; | ||
806 | next NODE if $node eq '..'; | ||
807 | my $node_target = join_paths($target, $node); | ||
808 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
809 | $self->unstow_node_orig($stow_path, $package, $node_target); | ||
810 | } | ||
811 | } | ||
812 | |||
813 | #===== METHOD =============================================================== | ||
814 | # Name : unstow_node_orig() | ||
815 | # Purpose : unstow the given node | ||
816 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
817 | # : to the stow dir containing the node to be stowed | ||
818 | # : $package => the package containing the node being stowed | ||
819 | # : $target => relative path to symlink target from the current directory | ||
820 | # Returns : n/a | ||
821 | # Throws : fatal error if a conflict arises | ||
822 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
823 | #============================================================================ | ||
824 | sub unstow_node_orig { | ||
825 | my $self = shift; | ||
826 | my ($stow_path, $package, $target) = @_; | ||
827 | |||
828 | my $path = join_paths($stow_path, $package, $target); | ||
829 | |||
830 | debug(3, "Unstowing $target (compat mode)"); | ||
831 | debug(4, " source path is $path"); | ||
832 | |||
833 | # Does the target exist? | ||
834 | if ($self->is_a_link($target)) { | ||
835 | debug(4, " Evaluate existing link: $target"); | ||
836 | |||
837 | # Where is the link pointing? | ||
838 | my $existing_source = $self->read_a_link($target); | ||
839 | if (not $existing_source) { | ||
840 | error("Could not read link: $target"); | ||
841 | } | ||
842 | |||
843 | # Does it point to a node under any stow directory? | ||
844 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
845 | $self->find_stowed_path($target, $existing_source); | ||
846 | if (not $existing_path) { | ||
847 | # We're traversing the target tree not the package tree, | ||
848 | # so we definitely expect to find stuff not owned by stow. | ||
849 | # Therefore we can't flag a conflict. | ||
850 | return; # XXX # | ||
851 | } | ||
852 | |||
853 | # Does the existing $target actually point to anything? | ||
854 | if (-e $existing_path) { | ||
855 | # Does link point to the right place? | ||
856 | if ($existing_path eq $path) { | ||
857 | $self->do_unlink($target); | ||
858 | } | ||
859 | elsif ($self->override($target)) { | ||
860 | debug(2, "--- overriding installation of: $target"); | ||
861 | $self->do_unlink($target); | ||
862 | } | ||
863 | # else leave it alone | ||
864 | } | ||
865 | else { | ||
866 | debug(2, "--- removing invalid link into a stow directory: $path"); | ||
867 | $self->do_unlink($target); | ||
868 | } | ||
869 | } | ||
870 | elsif (-d $target) { | ||
871 | $self->unstow_contents_orig($stow_path, $package, $target); | ||
872 | |||
873 | # This action may have made the parent directory foldable | ||
874 | if (my $parent = $self->foldable($target)) { | ||
875 | $self->fold_tree($target, $parent); | ||
876 | } | ||
877 | } | ||
878 | elsif (-e $target) { | ||
879 | $self->conflict( | ||
880 | 'unstow', | ||
881 | $package, | ||
882 | "existing target is neither a link nor a directory: $target", | ||
883 | ); | ||
884 | } | ||
885 | else { | ||
886 | debug(2, "$target did not exist to be unstowed"); | ||
887 | } | ||
888 | return; | ||
889 | } | ||
890 | |||
891 | #===== METHOD =============================================================== | ||
892 | # Name : unstow_contents() | ||
893 | # Purpose : unstow the contents of the given directory | ||
894 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
895 | # : to the stow dir containing the package to be unstowed | ||
896 | # : $package => the package whose contents are being unstowed | ||
897 | # : $target => relative path to symlink target from the current directory | ||
898 | # Returns : n/a | ||
899 | # Throws : a fatal error if directory cannot be read | ||
900 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
901 | # : Here we traverse the source tree, rather than the target tree. | ||
902 | #============================================================================ | ||
903 | sub unstow_contents { | ||
904 | my $self = shift; | ||
905 | my ($stow_path, $package, $target) = @_; | ||
906 | |||
907 | my $path = join_paths($stow_path, $package, $target); | ||
908 | |||
909 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
910 | |||
911 | my $cwd = getcwd(); | ||
912 | my $msg = "Unstowing from $target (cwd=$cwd, stow dir=$self->{stow_path})"; | ||
913 | $msg =~ s!$ENV{HOME}/!~/!g; | ||
914 | debug(3, $msg); | ||
915 | debug(4, " source path is $path"); | ||
916 | # We traverse the source tree not the target tree, so $path must exist. | ||
917 | error("unstow_contents() called with non-directory path: $path") | ||
918 | unless -d $path; | ||
919 | # When called at the top level, $target should exist. And | ||
920 | # unstow_node() should only call this via mutual recursion if | ||
921 | # $target exists. | ||
922 | error("unstow_contents() called with invalid target: $target") | ||
923 | unless $self->is_a_node($target); | ||
924 | |||
925 | opendir my $DIR, $path | ||
926 | or error("cannot read directory: $path ($!)"); | ||
927 | my @listing = readdir $DIR; | ||
928 | closedir $DIR; | ||
929 | |||
930 | NODE: | ||
931 | for my $node (@listing) { | ||
932 | next NODE if $node eq '.'; | ||
933 | next NODE if $node eq '..'; | ||
934 | my $node_target = join_paths($target, $node); | ||
935 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
936 | $self->unstow_node($stow_path, $package, $node_target); | ||
937 | } | ||
938 | if (-d $target) { | ||
939 | $self->cleanup_invalid_links($target); | ||
940 | } | ||
941 | } | ||
942 | |||
943 | #===== METHOD =============================================================== | ||
944 | # Name : unstow_node() | ||
945 | # Purpose : unstow the given node | ||
946 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
947 | # : to the stow dir containing the node to be stowed | ||
948 | # : $package => the package containing the node being unstowed | ||
949 | # : $target => relative path to symlink target from the current directory | ||
950 | # Returns : n/a | ||
951 | # Throws : fatal error if a conflict arises | ||
952 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
953 | #============================================================================ | ||
954 | sub unstow_node { | ||
955 | my $self = shift; | ||
956 | my ($stow_path, $package, $target) = @_; | ||
957 | |||
958 | my $path = join_paths($stow_path, $package, $target); | ||
959 | |||
960 | debug(3, "Unstowing $path"); | ||
961 | debug(4, " target is $target"); | ||
962 | |||
963 | # Does the target exist? | ||
964 | if ($self->is_a_link($target)) { | ||
965 | debug(4, " Evaluate existing link: $target"); | ||
966 | |||
967 | # Where is the link pointing? | ||
968 | my $existing_source = $self->read_a_link($target); | ||
969 | if (not $existing_source) { | ||
970 | error("Could not read link: $target"); | ||
971 | } | ||
972 | |||
973 | if ($existing_source =~ m{\A/}) { | ||
974 | warn "Ignoring an absolute symlink: $target => $existing_source\n"; | ||
975 | return; # XXX # | ||
976 | } | ||
977 | |||
978 | # Does it point to a node under any stow directory? | ||
979 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
980 | $self->find_stowed_path($target, $existing_source); | ||
981 | if (not $existing_path) { | ||
982 | $self->conflict( | ||
983 | 'unstow', | ||
984 | $package, | ||
985 | "existing target is not owned by stow: $target => $existing_source" | ||
986 | ); | ||
987 | return; # XXX # | ||
988 | } | ||
989 | |||
990 | # Does the existing $target actually point to anything? | ||
991 | if (-e $existing_path) { | ||
992 | # Does link points to the right place? | ||
993 | if ($existing_path eq $path) { | ||
994 | $self->do_unlink($target); | ||
995 | } | ||
996 | |||
997 | # XXX we quietly ignore links that are stowed to a different | ||
998 | # package. | ||
999 | |||
1000 | #elsif (defer($target)) { | ||
1001 | # debug(2, "--- deferring to installation of: $target"); | ||
1002 | #} | ||
1003 | #elsif ($self->override($target)) { | ||
1004 | # debug(2, "--- overriding installation of: $target"); | ||
1005 | # $self->do_unlink($target); | ||
1006 | #} | ||
1007 | #else { | ||
1008 | # $self->conflict( | ||
1009 | # 'unstow', | ||
1010 | # $package, | ||
1011 | # "existing target is stowed to a different package: " | ||
1012 | # . "$target => $existing_source" | ||
1013 | # ); | ||
1014 | #} | ||
1015 | } | ||
1016 | else { | ||
1017 | debug(2, "--- removing invalid link into a stow directory: $path"); | ||
1018 | $self->do_unlink($target); | ||
1019 | } | ||
1020 | } | ||
1021 | elsif (-e $target) { | ||
1022 | debug(4, " Evaluate existing node: $target"); | ||
1023 | if (-d $target) { | ||
1024 | $self->unstow_contents($stow_path, $package, $target); | ||
1025 | |||
1026 | # This action may have made the parent directory foldable | ||
1027 | if (my $parent = $self->foldable($target)) { | ||
1028 | $self->fold_tree($target, $parent); | ||
1029 | } | ||
1030 | } | ||
1031 | else { | ||
1032 | $self->conflict( | ||
1033 | 'unstow', | ||
1034 | $package, | ||
1035 | "existing target is neither a link nor a directory: $target", | ||
1036 | ); | ||
1037 | } | ||
1038 | } | ||
1039 | else { | ||
1040 | debug(2, "$target did not exist to be unstowed"); | ||
1041 | } | ||
1042 | return; | ||
1043 | } | ||
1044 | |||
1045 | #===== METHOD =============================================================== | ||
1046 | # Name : path_owned_by_package() | ||
1047 | # Purpose : determine whether the given link points to a member of a | ||
1048 | # : stowed package | ||
1049 | # Parameters: $target => path to a symbolic link under current directory | ||
1050 | # : $source => where that link points to | ||
1051 | # Returns : the package iff link is owned by stow, otherwise '' | ||
1052 | # Throws : n/a | ||
1053 | # Comments : lossy wrapper around find_stowed_path() | ||
1054 | #============================================================================ | ||
1055 | sub path_owned_by_package { | ||
1056 | my $self = shift; | ||
1057 | my ($target, $source) = @_; | ||
1058 | |||
1059 | my ($path, $stow_path, $package) = | ||
1060 | $self->find_stowed_path($target, $source); | ||
1061 | return $package; | ||
1062 | } | ||
1063 | |||
1064 | #===== METHOD =============================================================== | ||
1065 | # Name : find_stowed_path() | ||
1066 | # Purpose : determine whether the given link points to a member of a | ||
1067 | # : stowed package | ||
1068 | # Parameters: $target => path to a symbolic link under current directory | ||
1069 | # : $source => where that link points to (needed because link | ||
1070 | # : might not exist yet due to two-phase approach, | ||
1071 | # : so we can't just call readlink()) | ||
1072 | # Returns : ($path, $stow_path, $package) where $path and $stow_path are | ||
1073 | # : relative from the current (i.e. target) directory. $path | ||
1074 | # : is the full relative path, $stow_path is the relative path | ||
1075 | # : to the stow directory, and $package is the name of the package. | ||
1076 | # : or ('', '', '') if link is not owned by stow | ||
1077 | # Throws : n/a | ||
1078 | # Comments : Needs | ||
1079 | # : Allow for stow dir not being under target dir. | ||
1080 | # : We could put more logic under here for multiple stow dirs. | ||
1081 | #============================================================================ | ||
1082 | sub find_stowed_path { | ||
1083 | my $self = shift; | ||
1084 | my ($target, $source) = @_; | ||
1085 | |||
1086 | # Evaluate softlink relative to its target | ||
1087 | my $path = join_paths(parent($target), $source); | ||
1088 | debug(4, " is path $path owned by stow?"); | ||
1089 | |||
1090 | # Search for .stow files - this allows us to detect links | ||
1091 | # owned by stow directories other than the current one. | ||
1092 | my $dir = ''; | ||
1093 | my @path = split m{/+}, $path; | ||
1094 | for my $i (0 .. $#path) { | ||
1095 | my $part = $path[$i]; | ||
1096 | $dir = join_paths($dir, $part); | ||
1097 | if ($self->marked_stow_dir($dir)) { | ||
1098 | # FIXME - not sure if this can ever happen | ||
1099 | internal_error("find_stowed_path() called directly on stow dir") | ||
1100 | if $i == $#path; | ||
1101 | |||
1102 | debug(4, " yes - $dir was marked as a stow dir"); | ||
1103 | my $package = $path[$i + 1]; | ||
1104 | return ($path, $dir, $package); | ||
1105 | } | ||
1106 | } | ||
1107 | |||
1108 | # If no .stow file was found, we need to find out whether it's | ||
1109 | # owned by the current stow directory, in which case $path will be | ||
1110 | # a prefix of $self->{stow_path}. | ||
1111 | my @stow_path = split m{/+}, $self->{stow_path}; | ||
1112 | |||
1113 | # Strip off common prefixes until one is empty | ||
1114 | while (@path && @stow_path) { | ||
1115 | if ((shift @path) ne (shift @stow_path)) { | ||
1116 | debug(4, " no - either $path not under $self->{stow_path} or vice-versa"); | ||
1117 | return ('', '', ''); | ||
1118 | } | ||
1119 | } | ||
1120 | |||
1121 | if (@stow_path) { # @path must be empty | ||
1122 | debug(4, " no - $path is not under $self->{stow_path}"); | ||
1123 | return ('', '', ''); | ||
1124 | } | ||
1125 | |||
1126 | my $package = shift @path; | ||
1127 | |||
1128 | debug(4, " yes - by $package in " . join_paths(@path)); | ||
1129 | return ($path, $self->{stow_path}, $package); | ||
1130 | } | ||
1131 | |||
1132 | #===== METHOD ================================================================ | ||
1133 | # Name : cleanup_invalid_links() | ||
1134 | # Purpose : clean up invalid links that may block folding | ||
1135 | # Parameters: $dir => path to directory to check | ||
1136 | # Returns : n/a | ||
1137 | # Throws : no exceptions | ||
1138 | # Comments : removing files from a stowed package is probably a bad practice | ||
1139 | # : so this kind of clean up is not _really_ stow's responsibility; | ||
1140 | # : however, failing to clean up can block tree folding, so we'll do | ||
1141 | # : it anyway | ||
1142 | #============================================================================= | ||
1143 | sub cleanup_invalid_links { | ||
1144 | my $self = shift; | ||
1145 | my ($dir) = @_; | ||
1146 | |||
1147 | if (not -d $dir) { | ||
1148 | error("cleanup_invalid_links() called with a non-directory: $dir"); | ||
1149 | } | ||
1150 | |||
1151 | opendir my $DIR, $dir | ||
1152 | or error("cannot read directory: $dir ($!)"); | ||
1153 | my @listing = readdir $DIR; | ||
1154 | closedir $DIR; | ||
1155 | |||
1156 | NODE: | ||
1157 | for my $node (@listing) { | ||
1158 | next NODE if $node eq '.'; | ||
1159 | next NODE if $node eq '..'; | ||
1160 | |||
1161 | my $node_path = join_paths($dir, $node); | ||
1162 | |||
1163 | if (-l $node_path and not exists $self->{link_task_for}{$node_path}) { | ||
1164 | |||
1165 | # Where is the link pointing? | ||
1166 | # (don't use read_a_link() here) | ||
1167 | my $source = readlink($node_path); | ||
1168 | if (not $source) { | ||
1169 | error("Could not read link $node_path"); | ||
1170 | } | ||
1171 | |||
1172 | if ( | ||
1173 | not -e join_paths($dir, $source) and # bad link | ||
1174 | $self->path_owned_by_package($node_path, $source) # owned by stow | ||
1175 | ){ | ||
1176 | debug(2, "--- removing stale link: $node_path => " . | ||
1177 | join_paths($dir, $source)); | ||
1178 | $self->do_unlink($node_path); | ||
1179 | } | ||
1180 | } | ||
1181 | } | ||
1182 | return; | ||
1183 | } | ||
1184 | |||
1185 | |||
1186 | #===== METHOD =============================================================== | ||
1187 | # Name : foldable() | ||
1188 | # Purpose : determine whether a tree can be folded | ||
1189 | # Parameters: $target => path to a directory | ||
1190 | # Returns : path to the parent dir iff the tree can be safely folded | ||
1191 | # Throws : n/a | ||
1192 | # Comments : the path returned is relative to the parent of $target, | ||
1193 | # : that is, it can be used as the source for a replacement symlink | ||
1194 | #============================================================================ | ||
1195 | sub foldable { | ||
1196 | my $self = shift; | ||
1197 | my ($target) = @_; | ||
1198 | |||
1199 | debug(3, "--- Is $target foldable?"); | ||
1200 | if ($self->{'no-folding'}) { | ||
1201 | debug(3, "--- no because --no-folding enabled"); | ||
1202 | return ''; | ||
1203 | } | ||
1204 | |||
1205 | opendir my $DIR, $target | ||
1206 | or error(qq{Cannot read directory "$target" ($!)\n}); | ||
1207 | my @listing = readdir $DIR; | ||
1208 | closedir $DIR; | ||
1209 | |||
1210 | my $parent = ''; | ||
1211 | NODE: | ||
1212 | for my $node (@listing) { | ||
1213 | |||
1214 | next NODE if $node eq '.'; | ||
1215 | next NODE if $node eq '..'; | ||
1216 | |||
1217 | my $path = join_paths($target, $node); | ||
1218 | |||
1219 | # Skip nodes scheduled for removal | ||
1220 | next NODE if not $self->is_a_node($path); | ||
1221 | |||
1222 | # If it's not a link then we can't fold its parent | ||
1223 | return '' if not $self->is_a_link($path); | ||
1224 | |||
1225 | # Where is the link pointing? | ||
1226 | my $source = $self->read_a_link($path); | ||
1227 | if (not $source) { | ||
1228 | error("Could not read link $path"); | ||
1229 | } | ||
1230 | if ($parent eq '') { | ||
1231 | $parent = parent($source) | ||
1232 | } | ||
1233 | elsif ($parent ne parent($source)) { | ||
1234 | return ''; | ||
1235 | } | ||
1236 | } | ||
1237 | return '' if not $parent; | ||
1238 | |||
1239 | # If we get here then all nodes inside $target are links, and those links | ||
1240 | # point to nodes inside the same directory. | ||
1241 | |||
1242 | # chop of leading '..' to get the path to the common parent directory | ||
1243 | # relative to the parent of our $target | ||
1244 | $parent =~ s{\A\.\./}{}; | ||
1245 | |||
1246 | # If the resulting path is owned by stow, we can fold it | ||
1247 | if ($self->path_owned_by_package($target, $parent)) { | ||
1248 | debug(3, "--- $target is foldable"); | ||
1249 | return $parent; | ||
1250 | } | ||
1251 | else { | ||
1252 | return ''; | ||
1253 | } | ||
1254 | } | ||
1255 | |||
1256 | #===== METHOD =============================================================== | ||
1257 | # Name : fold_tree() | ||
1258 | # Purpose : fold the given tree | ||
1259 | # Parameters: $source => link to the folded tree source | ||
1260 | # : $target => directory that we will replace with a link to $source | ||
1261 | # Returns : n/a | ||
1262 | # Throws : none | ||
1263 | # Comments : only called iff foldable() is true so we can remove some checks | ||
1264 | #============================================================================ | ||
1265 | sub fold_tree { | ||
1266 | my $self = shift; | ||
1267 | my ($target, $source) = @_; | ||
1268 | |||
1269 | debug(3, "--- Folding tree: $target => $source"); | ||
1270 | |||
1271 | opendir my $DIR, $target | ||
1272 | or error(qq{Cannot read directory "$target" ($!)\n}); | ||
1273 | my @listing = readdir $DIR; | ||
1274 | closedir $DIR; | ||
1275 | |||
1276 | NODE: | ||
1277 | for my $node (@listing) { | ||
1278 | next NODE if $node eq '.'; | ||
1279 | next NODE if $node eq '..'; | ||
1280 | next NODE if not $self->is_a_node(join_paths($target, $node)); | ||
1281 | $self->do_unlink(join_paths($target, $node)); | ||
1282 | } | ||
1283 | $self->do_rmdir($target); | ||
1284 | $self->do_link($source, $target); | ||
1285 | return; | ||
1286 | } | ||
1287 | |||
1288 | |||
1289 | #===== METHOD =============================================================== | ||
1290 | # Name : conflict() | ||
1291 | # Purpose : handle conflicts in stow operations | ||
1292 | # Parameters: $package => the package involved with the conflicting operation | ||
1293 | # : $message => a description of the conflict | ||
1294 | # Returns : n/a | ||
1295 | # Throws : none | ||
1296 | # Comments : none | ||
1297 | #============================================================================ | ||
1298 | sub conflict { | ||
1299 | my $self = shift; | ||
1300 | my ($action, $package, $message) = @_; | ||
1301 | |||
1302 | debug(2, "CONFLICT when ${action}ing $package: $message"); | ||
1303 | $self->{conflicts}{$action}{$package} ||= []; | ||
1304 | push @{ $self->{conflicts}{$action}{$package} }, $message; | ||
1305 | $self->{conflict_count}++; | ||
1306 | |||
1307 | return; | ||
1308 | } | ||
1309 | |||
1310 | =head2 get_conflicts() | ||
1311 | |||
1312 | Returns a nested hash of all potential conflicts discovered: the keys | ||
1313 | are actions ('stow' or 'unstow'), and the values are hashrefs whose | ||
1314 | keys are stow package names and whose values are conflict | ||
1315 | descriptions, e.g.: | ||
1316 | |||
1317 | ( | ||
1318 | stow => { | ||
1319 | perl => [ | ||
1320 | "existing target is not owned by stow: bin/a2p" | ||
1321 | "existing target is neither a link nor a directory: bin/perl" | ||
1322 | ] | ||
1323 | } | ||
1324 | ) | ||
1325 | |||
1326 | =cut | ||
1327 | |||
1328 | sub get_conflicts { | ||
1329 | my $self = shift; | ||
1330 | return %{ $self->{conflicts} }; | ||
1331 | } | ||
1332 | |||
1333 | =head2 get_conflict_count() | ||
1334 | |||
1335 | Returns the number of conflicts found. | ||
1336 | |||
1337 | =cut | ||
1338 | |||
1339 | sub get_conflict_count { | ||
1340 | my $self = shift; | ||
1341 | return $self->{conflict_count}; | ||
1342 | } | ||
1343 | |||
1344 | =head2 get_tasks() | ||
1345 | |||
1346 | Returns a list of all symlink/directory creation/removal tasks. | ||
1347 | |||
1348 | =cut | ||
1349 | |||
1350 | sub get_tasks { | ||
1351 | my $self = shift; | ||
1352 | return @{ $self->{tasks} }; | ||
1353 | } | ||
1354 | |||
1355 | =head2 get_action_count() | ||
1356 | |||
1357 | Returns the number of actions planned for this Stow instance. | ||
1358 | |||
1359 | =cut | ||
1360 | |||
1361 | sub get_action_count { | ||
1362 | my $self = shift; | ||
1363 | return $self->{action_count}; | ||
1364 | } | ||
1365 | |||
1366 | #===== METHOD ================================================================ | ||
1367 | # Name : ignore | ||
1368 | # Purpose : determine if the given path matches a regex in our ignore list | ||
1369 | # Parameters: $stow_path => the stow directory containing the package | ||
1370 | # : $package => the package containing the path | ||
1371 | # : $target => the path to check against the ignore list | ||
1372 | # : relative to its package directory | ||
1373 | # Returns : true iff the path should be ignored | ||
1374 | # Throws : no exceptions | ||
1375 | # Comments : none | ||
1376 | #============================================================================= | ||
1377 | sub ignore { | ||
1378 | my $self = shift; | ||
1379 | my ($stow_path, $package, $target) = @_; | ||
1380 | |||
1381 | internal_error(__PACKAGE__ . "::ignore() called with empty target") | ||
1382 | unless length $target; | ||
1383 | |||
1384 | for my $suffix (@{ $self->{ignore} }) { | ||
1385 | if ($target =~ m/$suffix/) { | ||
1386 | debug(4, " Ignoring path $target due to --ignore=$suffix"); | ||
1387 | return 1; | ||
1388 | } | ||
1389 | } | ||
1390 | |||
1391 | my $package_dir = join_paths($stow_path, $package); | ||
1392 | my ($path_regexp, $segment_regexp) = | ||
1393 | $self->get_ignore_regexps($package_dir); | ||
1394 | debug(5, " Ignore list regexp for paths: " . | ||
1395 | (defined $path_regexp ? "/$path_regexp/" : "none")); | ||
1396 | debug(5, " Ignore list regexp for segments: " . | ||
1397 | (defined $segment_regexp ? "/$segment_regexp/" : "none")); | ||
1398 | |||
1399 | if (defined $path_regexp and "/$target" =~ $path_regexp) { | ||
1400 | debug(4, " Ignoring path /$target"); | ||
1401 | return 1; | ||
1402 | } | ||
1403 | |||
1404 | (my $basename = $target) =~ s!.+/!!; | ||
1405 | if (defined $segment_regexp and $basename =~ $segment_regexp) { | ||
1406 | debug(4, " Ignoring path segment $basename"); | ||
1407 | return 1; | ||
1408 | } | ||
1409 | |||
1410 | debug(5, " Not ignoring $target"); | ||
1411 | return 0; | ||
1412 | } | ||
1413 | |||
1414 | sub get_ignore_regexps { | ||
1415 | my $self = shift; | ||
1416 | my ($dir) = @_; | ||
1417 | |||
1418 | # N.B. the local and global stow ignore files have to have different | ||
1419 | # names so that: | ||
1420 | # 1. the global one can be a symlink to within a stow | ||
1421 | # package, managed by stow itself, and | ||
1422 | # 2. the local ones can be ignored via hardcoded logic in | ||
1423 | # GlobsToRegexp(), so that they always stay within their stow packages. | ||
1424 | |||
1425 | my $local_stow_ignore = join_paths($dir, $LOCAL_IGNORE_FILE); | ||
1426 | my $global_stow_ignore = join_paths($ENV{HOME}, $GLOBAL_IGNORE_FILE); | ||
1427 | |||
1428 | for my $file ($local_stow_ignore, $global_stow_ignore) { | ||
1429 | if (-e $file) { | ||
1430 | debug(5, " Using ignore file: $file"); | ||
1431 | return $self->get_ignore_regexps_from_file($file); | ||
1432 | } | ||
1433 | else { | ||
1434 | debug(5, " $file didn't exist"); | ||
1435 | } | ||
1436 | } | ||
1437 | |||
1438 | debug(4, " Using built-in ignore list"); | ||
1439 | return @default_global_ignore_regexps; | ||
1440 | } | ||
1441 | |||
1442 | my %ignore_file_regexps; | ||
1443 | |||
1444 | sub get_ignore_regexps_from_file { | ||
1445 | my $self = shift; | ||
1446 | my ($file) = @_; | ||
1447 | |||
1448 | if (exists $ignore_file_regexps{$file}) { | ||
1449 | debug(4, " Using memoized regexps from $file"); | ||
1450 | return @{ $ignore_file_regexps{$file} }; | ||
1451 | } | ||
1452 | |||
1453 | if (! open(REGEXPS, $file)) { | ||
1454 | debug(4, " Failed to open $file: $!"); | ||
1455 | return undef; | ||
1456 | } | ||
1457 | |||
1458 | my @regexps = $self->get_ignore_regexps_from_fh(\*REGEXPS); | ||
1459 | close(REGEXPS); | ||
1460 | |||
1461 | $ignore_file_regexps{$file} = [ @regexps ]; | ||
1462 | return @regexps; | ||
1463 | } | ||
1464 | |||
1465 | =head2 invalidate_memoized_regexp($file) | ||
1466 | |||
1467 | For efficiency of performance, regular expressions are compiled from | ||
1468 | each ignore list file the first time it is used by the Stow process, | ||
1469 | and then memoized for future use. If you expect the contents of these | ||
1470 | files to change during a single run, you will need to invalidate the | ||
1471 | memoized value from this cache. This method allows you to do that. | ||
1472 | |||
1473 | =cut | ||
1474 | |||
1475 | sub invalidate_memoized_regexp { | ||
1476 | my $self = shift; | ||
1477 | my ($file) = @_; | ||
1478 | if (exists $ignore_file_regexps{$file}) { | ||
1479 | debug(4, " Invalidated memoized regexp for $file"); | ||
1480 | delete $ignore_file_regexps{$file}; | ||
1481 | } | ||
1482 | else { | ||
1483 | debug(2, " WARNING: no memoized regexp for $file to invalidate"); | ||
1484 | } | ||
1485 | } | ||
1486 | |||
1487 | sub get_ignore_regexps_from_fh { | ||
1488 | my $self = shift; | ||
1489 | my ($fh) = @_; | ||
1490 | my %regexps; | ||
1491 | while (<$fh>) { | ||
1492 | chomp; | ||
1493 | s/^\s+//; | ||
1494 | s/\s+$//; | ||
1495 | next if /^#/ or length($_) == 0; | ||
1496 | s/\s+#.+//; # strip comments to right of pattern | ||
1497 | s/\\#/#/g; | ||
1498 | $regexps{$_}++; | ||
1499 | } | ||
1500 | |||
1501 | # Local ignore lists should *always* stay within the stow directory, | ||
1502 | # because this is the only place stow looks for them. | ||
1503 | $regexps{"^/\Q$LOCAL_IGNORE_FILE\E\$"}++; | ||
1504 | |||
1505 | return $self->compile_ignore_regexps(%regexps); | ||
1506 | } | ||
1507 | |||
1508 | sub compile_ignore_regexps { | ||
1509 | my $self = shift; | ||
1510 | my (%regexps) = @_; | ||
1511 | |||
1512 | my @segment_regexps; | ||
1513 | my @path_regexps; | ||
1514 | for my $regexp (keys %regexps) { | ||
1515 | if (index($regexp, '/') < 0) { | ||
1516 | # No / found in regexp, so use it for matching against basename | ||
1517 | push @segment_regexps, $regexp; | ||
1518 | } | ||
1519 | else { | ||
1520 | # / found in regexp, so use it for matching against full path | ||
1521 | push @path_regexps, $regexp; | ||
1522 | } | ||
1523 | } | ||
1524 | |||
1525 | my $segment_regexp = join '|', @segment_regexps; | ||
1526 | my $path_regexp = join '|', @path_regexps; | ||
1527 | $segment_regexp = @segment_regexps ? | ||
1528 | $self->compile_regexp("^($segment_regexp)\$") : undef; | ||
1529 | $path_regexp = @path_regexps ? | ||
1530 | $self->compile_regexp("(^|/)($path_regexp)(/|\$)") : undef; | ||
1531 | |||
1532 | return ($path_regexp, $segment_regexp); | ||
1533 | } | ||
1534 | |||
1535 | sub compile_regexp { | ||
1536 | my $self = shift; | ||
1537 | my ($regexp) = @_; | ||
1538 | my $compiled = eval { qr/$regexp/ }; | ||
1539 | die "Failed to compile regexp: $@\n" if $@; | ||
1540 | return $compiled; | ||
1541 | } | ||
1542 | |||
1543 | sub get_default_global_ignore_regexps { | ||
1544 | my $class = shift; | ||
1545 | # Bootstrap issue - first time we stow, we will be stowing | ||
1546 | # .cvsignore so it might not exist in ~ yet, or if it does, it could | ||
1547 | # be an old version missing the entries we need. So we make sure | ||
1548 | # they are there by hardcoding some crucial entries. | ||
1549 | return $class->get_ignore_regexps_from_fh(\*DATA); | ||
1550 | } | ||
1551 | |||
1552 | #===== METHOD ================================================================ | ||
1553 | # Name : defer | ||
1554 | # Purpose : determine if the given path matches a regex in our defer list | ||
1555 | # Parameters: $path | ||
1556 | # Returns : Boolean | ||
1557 | # Throws : no exceptions | ||
1558 | # Comments : none | ||
1559 | #============================================================================= | ||
1560 | sub defer { | ||
1561 | my $self = shift; | ||
1562 | my ($path) = @_; | ||
1563 | |||
1564 | for my $prefix (@{ $self->{defer} }) { | ||
1565 | return 1 if $path =~ m/$prefix/; | ||
1566 | } | ||
1567 | return 0; | ||
1568 | } | ||
1569 | |||
1570 | #===== METHOD ================================================================ | ||
1571 | # Name : override | ||
1572 | # Purpose : determine if the given path matches a regex in our override list | ||
1573 | # Parameters: $path | ||
1574 | # Returns : Boolean | ||
1575 | # Throws : no exceptions | ||
1576 | # Comments : none | ||
1577 | #============================================================================= | ||
1578 | sub override { | ||
1579 | my $self = shift; | ||
1580 | my ($path) = @_; | ||
1581 | |||
1582 | for my $regex (@{ $self->{override} }) { | ||
1583 | return 1 if $path =~ m/$regex/; | ||
1584 | } | ||
1585 | return 0; | ||
1586 | } | ||
1587 | |||
1588 | ############################################################################## | ||
1589 | # | ||
1590 | # The following code provides the abstractions that allow us to defer operating | ||
1591 | # on the filesystem until after all potential conflcits have been assessed. | ||
1592 | # | ||
1593 | ############################################################################## | ||
1594 | |||
1595 | #===== METHOD =============================================================== | ||
1596 | # Name : process_tasks() | ||
1597 | # Purpose : process each task in the tasks list | ||
1598 | # Parameters: none | ||
1599 | # Returns : n/a | ||
1600 | # Throws : fatal error if tasks list is corrupted or a task fails | ||
1601 | # Comments : none | ||
1602 | #============================================================================ | ||
1603 | sub process_tasks { | ||
1604 | my $self = shift; | ||
1605 | |||
1606 | debug(2, "Processing tasks..."); | ||
1607 | |||
1608 | # Strip out all tasks with a skip action | ||
1609 | $self->{tasks} = [ grep { $_->{action} ne 'skip' } @{ $self->{tasks} } ]; | ||
1610 | |||
1611 | if (not @{ $self->{tasks} }) { | ||
1612 | return; | ||
1613 | } | ||
1614 | |||
1615 | $self->within_target_do(sub { | ||
1616 | for my $task (@{ $self->{tasks} }) { | ||
1617 | $self->process_task($task); | ||
1618 | } | ||
1619 | }); | ||
1620 | |||
1621 | debug(2, "Processing tasks... done"); | ||
1622 | } | ||
1623 | |||
1624 | #===== METHOD =============================================================== | ||
1625 | # Name : process_task() | ||
1626 | # Purpose : process a single task | ||
1627 | # Parameters: $task => the task to process | ||
1628 | # Returns : n/a | ||
1629 | # Throws : fatal error if task fails | ||
1630 | # Comments : Must run from within target directory. | ||
1631 | # : Task involve either creating or deleting dirs and symlinks | ||
1632 | # : an action is set to 'skip' if it is found to be redundant | ||
1633 | #============================================================================ | ||
1634 | sub process_task { | ||
1635 | my $self = shift; | ||
1636 | my ($task) = @_; | ||
1637 | |||
1638 | if ($task->{action} eq 'create') { | ||
1639 | if ($task->{type} eq 'dir') { | ||
1640 | mkdir($task->{path}, 0777) | ||
1641 | or error("Could not create directory: $task->{path} ($!)"); | ||
1642 | return; | ||
1643 | } | ||
1644 | elsif ($task->{type} eq 'link') { | ||
1645 | symlink $task->{source}, $task->{path} | ||
1646 | or error( | ||
1647 | "Could not create symlink: %s => %s ($!)", | ||
1648 | $task->{path}, | ||
1649 | $task->{source} | ||
1650 | ); | ||
1651 | return; | ||
1652 | } | ||
1653 | } | ||
1654 | elsif ($task->{action} eq 'remove') { | ||
1655 | if ($task->{type} eq 'dir') { | ||
1656 | rmdir $task->{path} | ||
1657 | or error("Could not remove directory: $task->{path} ($!)"); | ||
1658 | return; | ||
1659 | } | ||
1660 | elsif ($task->{type} eq 'link') { | ||
1661 | unlink $task->{path} | ||
1662 | or error("Could not remove link: $task->{path} ($!)"); | ||
1663 | return; | ||
1664 | } | ||
1665 | } | ||
1666 | elsif ($task->{action} eq 'move') { | ||
1667 | if ($task->{type} eq 'file') { | ||
1668 | # rename() not good enough, since the stow directory | ||
1669 | # might be on a different filesystem to the target. | ||
1670 | move $task->{path}, $task->{dest} | ||
1671 | or error("Could not move $task->{path} -> $task->{dest} ($!)"); | ||
1672 | return; | ||
1673 | } | ||
1674 | } | ||
1675 | |||
1676 | # Should never happen. | ||
1677 | internal_error("bad task action: $task->{action}"); | ||
1678 | } | ||
1679 | |||
1680 | #===== METHOD =============================================================== | ||
1681 | # Name : link_task_action() | ||
1682 | # Purpose : finds the link task action for the given path, if there is one | ||
1683 | # Parameters: $path | ||
1684 | # Returns : 'remove', 'create', or '' if there is no action | ||
1685 | # Throws : a fatal exception if an invalid action is found | ||
1686 | # Comments : none | ||
1687 | #============================================================================ | ||
1688 | sub link_task_action { | ||
1689 | my $self = shift; | ||
1690 | my ($path) = @_; | ||
1691 | |||
1692 | if (! exists $self->{link_task_for}{$path}) { | ||
1693 | debug(4, " link_task_action($path): no task"); | ||
1694 | return ''; | ||
1695 | } | ||
1696 | |||
1697 | my $action = $self->{link_task_for}{$path}->{action}; | ||
1698 | internal_error("bad task action: $action") | ||
1699 | unless $action eq 'remove' or $action eq 'create'; | ||
1700 | |||
1701 | debug(4, " link_task_action($path): link task exists with action $action"); | ||
1702 | return $action; | ||
1703 | } | ||
1704 | |||
1705 | #===== METHOD =============================================================== | ||
1706 | # Name : dir_task_action() | ||
1707 | # Purpose : finds the dir task action for the given path, if there is one | ||
1708 | # Parameters: $path | ||
1709 | # Returns : 'remove', 'create', or '' if there is no action | ||
1710 | # Throws : a fatal exception if an invalid action is found | ||
1711 | # Comments : none | ||
1712 | #============================================================================ | ||
1713 | sub dir_task_action { | ||
1714 | my $self = shift; | ||
1715 | my ($path) = @_; | ||
1716 | |||
1717 | if (! exists $self->{dir_task_for}{$path}) { | ||
1718 | debug(4, " dir_task_action($path): no task"); | ||
1719 | return ''; | ||
1720 | } | ||
1721 | |||
1722 | my $action = $self->{dir_task_for}{$path}->{action}; | ||
1723 | internal_error("bad task action: $action") | ||
1724 | unless $action eq 'remove' or $action eq 'create'; | ||
1725 | |||
1726 | debug(4, " dir_task_action($path): dir task exists with action $action"); | ||
1727 | return $action; | ||
1728 | } | ||
1729 | |||
1730 | #===== METHOD =============================================================== | ||
1731 | # Name : parent_link_scheduled_for_removal() | ||
1732 | # Purpose : determine whether the given path or any parent thereof | ||
1733 | # : is a link scheduled for removal | ||
1734 | # Parameters: $path | ||
1735 | # Returns : Boolean | ||
1736 | # Throws : none | ||
1737 | # Comments : none | ||
1738 | #============================================================================ | ||
1739 | sub parent_link_scheduled_for_removal { | ||
1740 | my $self = shift; | ||
1741 | my ($path) = @_; | ||
1742 | |||
1743 | my $prefix = ''; | ||
1744 | for my $part (split m{/+}, $path) { | ||
1745 | $prefix = join_paths($prefix, $part); | ||
1746 | debug(4, " parent_link_scheduled_for_removal($path): prefix $prefix"); | ||
1747 | if (exists $self->{link_task_for}{$prefix} and | ||
1748 | $self->{link_task_for}{$prefix}->{action} eq 'remove') { | ||
1749 | debug(4, " parent_link_scheduled_for_removal($path): link scheduled for removal"); | ||
1750 | return 1; | ||
1751 | } | ||
1752 | } | ||
1753 | |||
1754 | debug(4, " parent_link_scheduled_for_removal($path): returning false"); | ||
1755 | return 0; | ||
1756 | } | ||
1757 | |||
1758 | #===== METHOD =============================================================== | ||
1759 | # Name : is_a_link() | ||
1760 | # Purpose : determine if the given path is a current or planned link | ||
1761 | # Parameters: $path | ||
1762 | # Returns : Boolean | ||
1763 | # Throws : none | ||
1764 | # Comments : returns false if an existing link is scheduled for removal | ||
1765 | # : and true if a non-existent link is scheduled for creation | ||
1766 | #============================================================================ | ||
1767 | sub is_a_link { | ||
1768 | my $self = shift; | ||
1769 | my ($path) = @_; | ||
1770 | debug(4, " is_a_link($path)"); | ||
1771 | |||
1772 | if (my $action = $self->link_task_action($path)) { | ||
1773 | if ($action eq 'remove') { | ||
1774 | debug(4, " is_a_link($path): returning 0 (remove action found)"); | ||
1775 | return 0; | ||
1776 | } | ||
1777 | elsif ($action eq 'create') { | ||
1778 | debug(4, " is_a_link($path): returning 1 (create action found)"); | ||
1779 | return 1; | ||
1780 | } | ||
1781 | } | ||
1782 | |||
1783 | if (-l $path) { | ||
1784 | # Check if any of its parent are links scheduled for removal | ||
1785 | # (need this for edge case during unfolding) | ||
1786 | debug(4, " is_a_link($path): is a real link"); | ||
1787 | return $self->parent_link_scheduled_for_removal($path) ? 0 : 1; | ||
1788 | } | ||
1789 | |||
1790 | debug(4, " is_a_link($path): returning 0"); | ||
1791 | return 0; | ||
1792 | } | ||
1793 | |||
1794 | #===== METHOD =============================================================== | ||
1795 | # Name : is_a_dir() | ||
1796 | # Purpose : determine if the given path is a current or planned directory | ||
1797 | # Parameters: $path | ||
1798 | # Returns : Boolean | ||
1799 | # Throws : none | ||
1800 | # Comments : returns false if an existing directory is scheduled for removal | ||
1801 | # : and true if a non-existent directory is scheduled for creation | ||
1802 | # : we also need to be sure we are not just following a link | ||
1803 | #============================================================================ | ||
1804 | sub is_a_dir { | ||
1805 | my $self = shift; | ||
1806 | my ($path) = @_; | ||
1807 | debug(4, " is_a_dir($path)"); | ||
1808 | |||
1809 | if (my $action = $self->dir_task_action($path)) { | ||
1810 | if ($action eq 'remove') { | ||
1811 | return 0; | ||
1812 | } | ||
1813 | elsif ($action eq 'create') { | ||
1814 | return 1; | ||
1815 | } | ||
1816 | } | ||
1817 | |||
1818 | return 0 if $self->parent_link_scheduled_for_removal($path); | ||
1819 | |||
1820 | if (-d $path) { | ||
1821 | debug(4, " is_a_dir($path): real dir"); | ||
1822 | return 1; | ||
1823 | } | ||
1824 | |||
1825 | debug(4, " is_a_dir($path): returning false"); | ||
1826 | return 0; | ||
1827 | } | ||
1828 | |||
1829 | #===== METHOD =============================================================== | ||
1830 | # Name : is_a_node() | ||
1831 | # Purpose : determine whether the given path is a current or planned node | ||
1832 | # Parameters: $path | ||
1833 | # Returns : Boolean | ||
1834 | # Throws : none | ||
1835 | # Comments : returns false if an existing node is scheduled for removal | ||
1836 | # : true if a non-existent node is scheduled for creation | ||
1837 | # : we also need to be sure we are not just following a link | ||
1838 | #============================================================================ | ||
1839 | sub is_a_node { | ||
1840 | my $self = shift; | ||
1841 | my ($path) = @_; | ||
1842 | debug(4, " is_a_node($path)"); | ||
1843 | |||
1844 | my $laction = $self->link_task_action($path); | ||
1845 | my $daction = $self->dir_task_action($path); | ||
1846 | |||
1847 | if ($laction eq 'remove') { | ||
1848 | if ($daction eq 'remove') { | ||
1849 | internal_error("removing link and dir: $path"); | ||
1850 | return 0; | ||
1851 | } | ||
1852 | elsif ($daction eq 'create') { | ||
1853 | # Assume that we're unfolding $path, and that the link | ||
1854 | # removal action is earlier than the dir creation action | ||
1855 | # in the task queue. FIXME: is this a safe assumption? | ||
1856 | return 1; | ||
1857 | } | ||
1858 | else { # no dir action | ||
1859 | return 0; | ||
1860 | } | ||
1861 | } | ||
1862 | elsif ($laction eq 'create') { | ||
1863 | if ($daction eq 'remove') { | ||
1864 | # Assume that we're folding $path, and that the dir | ||
1865 | # removal action is earlier than the link creation action | ||
1866 | # in the task queue. FIXME: is this a safe assumption? | ||
1867 | return 1; | ||
1868 | } | ||
1869 | elsif ($daction eq 'create') { | ||
1870 | internal_error("creating link and dir: $path"); | ||
1871 | return 1; | ||
1872 | } | ||
1873 | else { # no dir action | ||
1874 | return 1; | ||
1875 | } | ||
1876 | } | ||
1877 | else { | ||
1878 | # No link action | ||
1879 | if ($daction eq 'remove') { | ||
1880 | return 0; | ||
1881 | } | ||
1882 | elsif ($daction eq 'create') { | ||
1883 | return 1; | ||
1884 | } | ||
1885 | else { # no dir action | ||
1886 | # fall through to below | ||
1887 | } | ||
1888 | } | ||
1889 | |||
1890 | return 0 if $self->parent_link_scheduled_for_removal($path); | ||
1891 | |||
1892 | if (-e $path) { | ||
1893 | debug(4, " is_a_node($path): really exists"); | ||
1894 | return 1; | ||
1895 | } | ||
1896 | |||
1897 | debug(4, " is_a_node($path): returning false"); | ||
1898 | return 0; | ||
1899 | } | ||
1900 | |||
1901 | #===== METHOD =============================================================== | ||
1902 | # Name : read_a_link() | ||
1903 | # Purpose : return the source of a current or planned link | ||
1904 | # Parameters: $path => path to the link target | ||
1905 | # Returns : a string | ||
1906 | # Throws : fatal exception if the given path is not a current or planned | ||
1907 | # : link | ||
1908 | # Comments : none | ||
1909 | #============================================================================ | ||
1910 | sub read_a_link { | ||
1911 | my $self = shift; | ||
1912 | my ($path) = @_; | ||
1913 | |||
1914 | if (my $action = $self->link_task_action($path)) { | ||
1915 | debug(4, " read_a_link($path): task exists with action $action"); | ||
1916 | |||
1917 | if ($action eq 'create') { | ||
1918 | return $self->{link_task_for}{$path}->{source}; | ||
1919 | } | ||
1920 | elsif ($action eq 'remove') { | ||
1921 | internal_error( | ||
1922 | "read_a_link() passed a path that is scheduled for removal: $path" | ||
1923 | ); | ||
1924 | } | ||
1925 | } | ||
1926 | elsif (-l $path) { | ||
1927 | debug(4, " read_a_link($path): real link"); | ||
1928 | my $target = readlink $path or error("Could not read link: $path ($!)"); | ||
1929 | return $target; | ||
1930 | } | ||
1931 | internal_error("read_a_link() passed a non link path: $path\n"); | ||
1932 | } | ||
1933 | |||
1934 | #===== METHOD =============================================================== | ||
1935 | # Name : do_link() | ||
1936 | # Purpose : wrap 'link' operation for later processing | ||
1937 | # Parameters: $oldfile => the existing file to link to | ||
1938 | # : $newfile => the file to link | ||
1939 | # Returns : n/a | ||
1940 | # Throws : error if this clashes with an existing planned operation | ||
1941 | # Comments : cleans up operations that undo previous operations | ||
1942 | #============================================================================ | ||
1943 | sub do_link { | ||
1944 | my $self = shift; | ||
1945 | my ($oldfile, $newfile) = @_; | ||
1946 | |||
1947 | if (exists $self->{dir_task_for}{$newfile}) { | ||
1948 | my $task_ref = $self->{dir_task_for}{$newfile}; | ||
1949 | |||
1950 | if ($task_ref->{action} eq 'create') { | ||
1951 | if ($task_ref->{type} eq 'dir') { | ||
1952 | internal_error( | ||
1953 | "new link (%s => %s) clashes with planned new directory", | ||
1954 | $newfile, | ||
1955 | $oldfile, | ||
1956 | ); | ||
1957 | } | ||
1958 | } | ||
1959 | elsif ($task_ref->{action} eq 'remove') { | ||
1960 | # We may need to remove a directory before creating a link so continue. | ||
1961 | } | ||
1962 | else { | ||
1963 | internal_error("bad task action: $task_ref->{action}"); | ||
1964 | } | ||
1965 | } | ||
1966 | |||
1967 | if (exists $self->{link_task_for}{$newfile}) { | ||
1968 | my $task_ref = $self->{link_task_for}{$newfile}; | ||
1969 | |||
1970 | if ($task_ref->{action} eq 'create') { | ||
1971 | if ($task_ref->{source} ne $oldfile) { | ||
1972 | internal_error( | ||
1973 | "new link clashes with planned new link: %s => %s", | ||
1974 | $task_ref->{path}, | ||
1975 | $task_ref->{source}, | ||
1976 | ) | ||
1977 | } | ||
1978 | else { | ||
1979 | debug(1, "LINK: $newfile => $oldfile (duplicates previous action)"); | ||
1980 | return; | ||
1981 | } | ||
1982 | } | ||
1983 | elsif ($task_ref->{action} eq 'remove') { | ||
1984 | if ($task_ref->{source} eq $oldfile) { | ||
1985 | # No need to remove a link we are going to recreate | ||
1986 | debug(1, "LINK: $newfile => $oldfile (reverts previous action)"); | ||
1987 | $self->{link_task_for}{$newfile}->{action} = 'skip'; | ||
1988 | delete $self->{link_task_for}{$newfile}; | ||
1989 | return; | ||
1990 | } | ||
1991 | # We may need to remove a link to replace it so continue | ||
1992 | } | ||
1993 | else { | ||
1994 | internal_error("bad task action: $task_ref->{action}"); | ||
1995 | } | ||
1996 | } | ||
1997 | |||
1998 | # Creating a new link | ||
1999 | debug(1, "LINK: $newfile => $oldfile"); | ||
2000 | my $task = { | ||
2001 | action => 'create', | ||
2002 | type => 'link', | ||
2003 | path => $newfile, | ||
2004 | source => $oldfile, | ||
2005 | }; | ||
2006 | push @{ $self->{tasks} }, $task; | ||
2007 | $self->{link_task_for}{$newfile} = $task; | ||
2008 | |||
2009 | return; | ||
2010 | } | ||
2011 | |||
2012 | #===== METHOD =============================================================== | ||
2013 | # Name : do_unlink() | ||
2014 | # Purpose : wrap 'unlink' operation for later processing | ||
2015 | # Parameters: $file => the file to unlink | ||
2016 | # Returns : n/a | ||
2017 | # Throws : error if this clashes with an existing planned operation | ||
2018 | # Comments : will remove an existing planned link | ||
2019 | #============================================================================ | ||
2020 | sub do_unlink { | ||
2021 | my $self = shift; | ||
2022 | my ($file) = @_; | ||
2023 | |||
2024 | if (exists $self->{link_task_for}{$file}) { | ||
2025 | my $task_ref = $self->{link_task_for}{$file}; | ||
2026 | if ($task_ref->{action} eq 'remove') { | ||
2027 | debug(1, "UNLINK: $file (duplicates previous action)"); | ||
2028 | return; | ||
2029 | } | ||
2030 | elsif ($task_ref->{action} eq 'create') { | ||
2031 | # Do need to create a link then remove it | ||
2032 | debug(1, "UNLINK: $file (reverts previous action)"); | ||
2033 | $self->{link_task_for}{$file}->{action} = 'skip'; | ||
2034 | delete $self->{link_task_for}{$file}; | ||
2035 | return; | ||
2036 | } | ||
2037 | else { | ||
2038 | internal_error("bad task action: $task_ref->{action}"); | ||
2039 | } | ||
2040 | } | ||
2041 | |||
2042 | if (exists $self->{dir_task_for}{$file} and $self->{dir_task_for}{$file} eq 'create') { | ||
2043 | internal_error( | ||
2044 | "new unlink operation clashes with planned operation: %s dir %s", | ||
2045 | $self->{dir_task_for}{$file}->{action}, | ||
2046 | $file | ||
2047 | ); | ||
2048 | } | ||
2049 | |||
2050 | # Remove the link | ||
2051 | debug(1, "UNLINK: $file"); | ||
2052 | |||
2053 | my $source = readlink $file or error("could not readlink $file ($!)"); | ||
2054 | |||
2055 | my $task = { | ||
2056 | action => 'remove', | ||
2057 | type => 'link', | ||
2058 | path => $file, | ||
2059 | source => $source, | ||
2060 | }; | ||
2061 | push @{ $self->{tasks} }, $task; | ||
2062 | $self->{link_task_for}{$file} = $task; | ||
2063 | |||
2064 | return; | ||
2065 | } | ||
2066 | |||
2067 | #===== METHOD =============================================================== | ||
2068 | # Name : do_mkdir() | ||
2069 | # Purpose : wrap 'mkdir' operation | ||
2070 | # Parameters: $dir => the directory to remove | ||
2071 | # Returns : n/a | ||
2072 | # Throws : fatal exception if operation fails | ||
2073 | # Comments : outputs a message if 'verbose' option is set | ||
2074 | # : does not perform operation if 'simulate' option is set | ||
2075 | # Comments : cleans up operations that undo previous operations | ||
2076 | #============================================================================ | ||
2077 | sub do_mkdir { | ||
2078 | my $self = shift; | ||
2079 | my ($dir) = @_; | ||
2080 | |||
2081 | if (exists $self->{link_task_for}{$dir}) { | ||
2082 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2083 | |||
2084 | if ($task_ref->{action} eq 'create') { | ||
2085 | internal_error( | ||
2086 | "new dir clashes with planned new link (%s => %s)", | ||
2087 | $task_ref->{path}, | ||
2088 | $task_ref->{source}, | ||
2089 | ); | ||
2090 | } | ||
2091 | elsif ($task_ref->{action} eq 'remove') { | ||
2092 | # May need to remove a link before creating a directory so continue | ||
2093 | } | ||
2094 | else { | ||
2095 | internal_error("bad task action: $task_ref->{action}"); | ||
2096 | } | ||
2097 | } | ||
2098 | |||
2099 | if (exists $self->{dir_task_for}{$dir}) { | ||
2100 | my $task_ref = $self->{dir_task_for}{$dir}; | ||
2101 | |||
2102 | if ($task_ref->{action} eq 'create') { | ||
2103 | debug(1, "MKDIR: $dir (duplicates previous action)"); | ||
2104 | return; | ||
2105 | } | ||
2106 | elsif ($task_ref->{action} eq 'remove') { | ||
2107 | debug(1, "MKDIR: $dir (reverts previous action)"); | ||
2108 | $self->{dir_task_for}{$dir}->{action} = 'skip'; | ||
2109 | delete $self->{dir_task_for}{$dir}; | ||
2110 | return; | ||
2111 | } | ||
2112 | else { | ||
2113 | internal_error("bad task action: $task_ref->{action}"); | ||
2114 | } | ||
2115 | } | ||
2116 | |||
2117 | debug(1, "MKDIR: $dir"); | ||
2118 | my $task = { | ||
2119 | action => 'create', | ||
2120 | type => 'dir', | ||
2121 | path => $dir, | ||
2122 | source => undef, | ||
2123 | }; | ||
2124 | push @{ $self->{tasks} }, $task; | ||
2125 | $self->{dir_task_for}{$dir} = $task; | ||
2126 | |||
2127 | return; | ||
2128 | } | ||
2129 | |||
2130 | #===== METHOD =============================================================== | ||
2131 | # Name : do_rmdir() | ||
2132 | # Purpose : wrap 'rmdir' operation | ||
2133 | # Parameters: $dir => the directory to remove | ||
2134 | # Returns : n/a | ||
2135 | # Throws : fatal exception if operation fails | ||
2136 | # Comments : outputs a message if 'verbose' option is set | ||
2137 | # : does not perform operation if 'simulate' option is set | ||
2138 | #============================================================================ | ||
2139 | sub do_rmdir { | ||
2140 | my $self = shift; | ||
2141 | my ($dir) = @_; | ||
2142 | |||
2143 | if (exists $self->{link_task_for}{$dir}) { | ||
2144 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2145 | internal_error( | ||
2146 | "rmdir clashes with planned operation: %s link %s => %s", | ||
2147 | $task_ref->{action}, | ||
2148 | $task_ref->{path}, | ||
2149 | $task_ref->{source} | ||
2150 | ); | ||
2151 | } | ||
2152 | |||
2153 | if (exists $self->{dir_task_for}{$dir}) { | ||
2154 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2155 | |||
2156 | if ($task_ref->{action} eq 'remove') { | ||
2157 | debug(1, "RMDIR $dir (duplicates previous action)"); | ||
2158 | return; | ||
2159 | } | ||
2160 | elsif ($task_ref->{action} eq 'create') { | ||
2161 | debug(1, "MKDIR $dir (reverts previous action)"); | ||
2162 | $self->{link_task_for}{$dir}->{action} = 'skip'; | ||
2163 | delete $self->{link_task_for}{$dir}; | ||
2164 | return; | ||
2165 | } | ||
2166 | else { | ||
2167 | internal_error("bad task action: $task_ref->{action}"); | ||
2168 | } | ||
2169 | } | ||
2170 | |||
2171 | debug(1, "RMDIR $dir"); | ||
2172 | my $task = { | ||
2173 | action => 'remove', | ||
2174 | type => 'dir', | ||
2175 | path => $dir, | ||
2176 | source => '', | ||
2177 | }; | ||
2178 | push @{ $self->{tasks} }, $task; | ||
2179 | $self->{dir_task_for}{$dir} = $task; | ||
2180 | |||
2181 | return; | ||
2182 | } | ||
2183 | |||
2184 | #===== METHOD =============================================================== | ||
2185 | # Name : do_mv() | ||
2186 | # Purpose : wrap 'move' operation for later processing | ||
2187 | # Parameters: $src => the file to move | ||
2188 | # : $dst => the path to move it to | ||
2189 | # Returns : n/a | ||
2190 | # Throws : error if this clashes with an existing planned operation | ||
2191 | # Comments : alters contents of package installation image in stow dir | ||
2192 | #============================================================================ | ||
2193 | sub do_mv { | ||
2194 | my $self = shift; | ||
2195 | my ($src, $dst) = @_; | ||
2196 | |||
2197 | if (exists $self->{link_task_for}{$src}) { | ||
2198 | # I don't *think* this should ever happen, but I'm not | ||
2199 | # 100% sure. | ||
2200 | my $task_ref = $self->{link_task_for}{$src}; | ||
2201 | internal_error( | ||
2202 | "do_mv: pre-existing link task for $src; action: %s, source: %s", | ||
2203 | $task_ref->{action}, $task_ref->{source} | ||
2204 | ); | ||
2205 | } | ||
2206 | elsif (exists $self->{dir_task_for}{$src}) { | ||
2207 | my $task_ref = $self->{dir_task_for}{$src}; | ||
2208 | internal_error( | ||
2209 | "do_mv: pre-existing dir task for %s?! action: %s", | ||
2210 | $src, $task_ref->{action} | ||
2211 | ); | ||
2212 | } | ||
2213 | |||
2214 | # Remove the link | ||
2215 | debug(1, "MV: $src -> $dst"); | ||
2216 | |||
2217 | my $task = { | ||
2218 | action => 'move', | ||
2219 | type => 'file', | ||
2220 | path => $src, | ||
2221 | dest => $dst, | ||
2222 | }; | ||
2223 | push @{ $self->{tasks} }, $task; | ||
2224 | |||
2225 | # FIXME: do we need this for anything? | ||
2226 | #$self->{mv_task_for}{$file} = $task; | ||
2227 | |||
2228 | return; | ||
2229 | } | ||
2230 | |||
2231 | |||
2232 | ############################################################################# | ||
2233 | # | ||
2234 | # End of methods; subroutines follow. | ||
2235 | # FIXME: Ideally these should be in a separate module. | ||
2236 | |||
2237 | |||
2238 | #===== PRIVATE SUBROUTINE =================================================== | ||
2239 | # Name : internal_error() | ||
2240 | # Purpose : output internal error message in a consistent form and die | ||
2241 | # Parameters: $message => error message to output | ||
2242 | # Returns : n/a | ||
2243 | # Throws : n/a | ||
2244 | # Comments : none | ||
2245 | #============================================================================ | ||
2246 | sub internal_error { | ||
2247 | my ($format, @args) = @_; | ||
2248 | my $error = sprintf($format, @args); | ||
2249 | my $stacktrace = Carp::longmess(); | ||
2250 | die <<EOF; | ||
2251 | |||
2252 | $ProgramName: INTERNAL ERROR: $error$stacktrace | ||
2253 | |||
2254 | This _is_ a bug. Please submit a bug report so we can fix it! :-) | ||
2255 | See http://www.gnu.org/software/stow/ for how to do this. | ||
2256 | EOF | ||
2257 | } | ||
2258 | |||
2259 | =head1 BUGS | ||
2260 | |||
2261 | =head1 SEE ALSO | ||
2262 | |||
2263 | =cut | ||
2264 | |||
2265 | 1; | ||
2266 | |||
2267 | # Local variables: | ||
2268 | # mode: perl | ||
2269 | # cperl-indent-level: 4 | ||
2270 | # end: | ||
2271 | # vim: ft=perl | ||
2272 | |||
2273 | ############################################################################# | ||
2274 | # Default global list of ignore regexps follows | ||
2275 | # (automatically appended by the Makefile) | ||
2276 | |||
2277 | __DATA__ | ||
2278 | # Comments and blank lines are allowed. | ||
2279 | |||
2280 | RCS | ||
2281 | .+,v | ||
2282 | |||
2283 | CVS | ||
2284 | \.\#.+ # CVS conflict files / emacs lock files | ||
2285 | \.cvsignore | ||
2286 | |||
2287 | \.svn | ||
2288 | _darcs | ||
2289 | \.hg | ||
2290 | |||
2291 | \.git | ||
2292 | \.gitignore | ||
2293 | |||
2294 | .+~ # emacs backup files | ||
2295 | \#.*\# # emacs autosave files | ||
2296 | |||
2297 | ^/README.* | ||
2298 | ^/LICENSE.* | ||
2299 | ^/COPYING | ||
@@ -1,4 +1,2302 @@ | |||
1 | #!/usr/bin/perl | 1 | #!/usr/bin/perl |
2 | # | ||
3 | # GNU Stow in a single file | ||
4 | # | ||
5 | # Just a hacked up version of stow to make it live and work in a single file. | ||
6 | # Doesn't really change much except the way the module is constructed, | ||
7 | # duplicates the parent function, and removes the __DATA__ section of the Stow | ||
8 | # module. | ||
9 | # | ||
10 | |||
11 | { | ||
12 | package Stow; | ||
13 | |||
14 | =head1 NAME | ||
15 | |||
16 | Stow - manage the installation of multiple software packages | ||
17 | |||
18 | =head1 SYNOPSIS | ||
19 | |||
20 | my $stow = new Stow(%$options); | ||
21 | |||
22 | $stow->plan_unstow(@pkgs_to_unstow); | ||
23 | $stow->plan_stow (@pkgs_to_stow); | ||
24 | |||
25 | my %conflicts = $stow->get_conflicts; | ||
26 | $stow->process_tasks() unless %conflicts; | ||
27 | |||
28 | =head1 DESCRIPTION | ||
29 | |||
30 | This is the backend Perl module for GNU Stow, a program for managing | ||
31 | the installation of software packages, keeping them separate | ||
32 | (C</usr/local/stow/emacs> vs. C</usr/local/stow/perl>, for example) | ||
33 | while making them appear to be installed in the same place | ||
34 | (C</usr/local>). | ||
35 | |||
36 | Stow doesn't store an extra state between runs, so there's no danger | ||
37 | of mangling directories when file hierarchies don't match the | ||
38 | database. Also, stow will never delete any files, directories, or | ||
39 | links that appear in a stow directory, so it is always possible to | ||
40 | rebuild the target tree. | ||
41 | |||
42 | =cut | ||
43 | |||
44 | use strict; | ||
45 | use warnings; | ||
46 | |||
47 | use Carp qw(carp cluck croak confess longmess); | ||
48 | use File::Copy qw(move); | ||
49 | use File::Spec; | ||
50 | use POSIX qw(getcwd); | ||
51 | |||
52 | =head1 NAME | ||
53 | |||
54 | Stow::Util - general utilities | ||
55 | |||
56 | =head1 SYNOPSIS | ||
57 | |||
58 | use Stow::Util qw(debug set_debug_level error ...); | ||
59 | |||
60 | =head1 DESCRIPTION | ||
61 | |||
62 | Supporting utility routines for L<Stow>. | ||
63 | |||
64 | =cut | ||
65 | |||
66 | use base qw(Exporter); | ||
67 | our @EXPORT_OK = qw( | ||
68 | error debug set_debug_level set_test_mode | ||
69 | join_paths parent canon_path restore_cwd | ||
70 | ); | ||
71 | |||
72 | our $ProgramName = 'stow'; | ||
73 | our $VERSION = '2.2.2'; | ||
74 | |||
75 | ############################################################################# | ||
76 | # | ||
77 | # General Utilities: nothing stow specific here. | ||
78 | # | ||
79 | ############################################################################# | ||
80 | |||
81 | =head1 IMPORTABLE SUBROUTINES | ||
82 | |||
83 | =head2 error($format, @args) | ||
84 | |||
85 | Outputs an error message in a consistent form and then dies. | ||
86 | |||
87 | =cut | ||
88 | |||
89 | sub error { | ||
90 | my ($format, @args) = @_; | ||
91 | die "$ProgramName: ERROR: " . sprintf($format, @args) . "\n"; | ||
92 | } | ||
93 | |||
94 | =head2 set_debug_level($level) | ||
95 | |||
96 | Sets verbosity level for C<debug()>. | ||
97 | |||
98 | =cut | ||
99 | |||
100 | our $debug_level = 0; | ||
101 | |||
102 | sub set_debug_level { | ||
103 | my ($level) = @_; | ||
104 | $debug_level = $level; | ||
105 | } | ||
106 | |||
107 | =head2 set_test_mode($on_or_off) | ||
108 | |||
109 | Sets testmode on or off. | ||
110 | |||
111 | =cut | ||
112 | |||
113 | our $test_mode = 0; | ||
114 | |||
115 | sub set_test_mode { | ||
116 | my ($on_or_off) = @_; | ||
117 | if ($on_or_off) { | ||
118 | $test_mode = 1; | ||
119 | } | ||
120 | else { | ||
121 | $test_mode = 0; | ||
122 | } | ||
123 | } | ||
124 | |||
125 | =head2 debug($level, $msg) | ||
126 | |||
127 | Logs to STDERR based on C<$debug_level> setting. C<$level> is the | ||
128 | minimum verbosity level required to output C<$msg>. All output is to | ||
129 | STDERR to preserve backward compatibility, except for in test mode, | ||
130 | when STDOUT is used instead. In test mode, the verbosity can be | ||
131 | overridden via the C<TEST_VERBOSE> environment variable. | ||
132 | |||
133 | Verbosity rules: | ||
134 | |||
135 | =over 4 | ||
136 | |||
137 | =item 0: errors only | ||
138 | |||
139 | =item >= 1: print operations: LINK/UNLINK/MKDIR/RMDIR/MV | ||
140 | |||
141 | =item >= 2: print operation exceptions | ||
142 | |||
143 | e.g. "_this_ already points to _that_", skipping, deferring, | ||
144 | overriding, fixing invalid links | ||
145 | |||
146 | =item >= 3: print trace detail: trace: stow/unstow/package/contents/node | ||
147 | |||
148 | =item >= 4: debug helper routines | ||
149 | |||
150 | =item >= 5: debug ignore lists | ||
151 | |||
152 | =back | ||
153 | |||
154 | =cut | ||
155 | |||
156 | sub debug { | ||
157 | my ($level, $msg) = @_; | ||
158 | if ($debug_level >= $level) { | ||
159 | if ($test_mode) { | ||
160 | print "# $msg\n"; | ||
161 | } | ||
162 | else { | ||
163 | warn "$msg\n"; | ||
164 | } | ||
165 | } | ||
166 | } | ||
167 | |||
168 | #===== METHOD =============================================================== | ||
169 | # Name : join_paths() | ||
170 | # Purpose : concatenates given paths | ||
171 | # Parameters: path1, path2, ... => paths | ||
172 | # Returns : concatenation of given paths | ||
173 | # Throws : n/a | ||
174 | # Comments : factors out redundant path elements: | ||
175 | # : '//' => '/' and 'a/b/../c' => 'a/c' | ||
176 | #============================================================================ | ||
177 | sub join_paths { | ||
178 | my @paths = @_; | ||
179 | |||
180 | # weed out empty components and concatenate | ||
181 | my $result = join '/', grep {! /\A\z/} @paths; | ||
182 | |||
183 | # factor out back references and remove redundant /'s) | ||
184 | my @result = (); | ||
185 | PART: | ||
186 | for my $part (split m{/+}, $result) { | ||
187 | next PART if $part eq '.'; | ||
188 | if (@result && $part eq '..' && $result[-1] ne '..') { | ||
189 | pop @result; | ||
190 | } | ||
191 | else { | ||
192 | push @result, $part; | ||
193 | } | ||
194 | } | ||
195 | |||
196 | return join '/', @result; | ||
197 | } | ||
198 | |||
199 | #===== METHOD =============================================================== | ||
200 | # Name : parent | ||
201 | # Purpose : find the parent of the given path | ||
202 | # Parameters: @path => components of the path | ||
203 | # Returns : returns a path string | ||
204 | # Throws : n/a | ||
205 | # Comments : allows you to send multiple chunks of the path | ||
206 | # : (this feature is currently not used) | ||
207 | #============================================================================ | ||
208 | sub parent { | ||
209 | my @path = @_; | ||
210 | my $path = join '/', @_; | ||
211 | my @elts = split m{/+}, $path; | ||
212 | pop @elts; | ||
213 | return join '/', @elts; | ||
214 | } | ||
215 | |||
216 | #===== METHOD =============================================================== | ||
217 | # Name : canon_path | ||
218 | # Purpose : find absolute canonical path of given path | ||
219 | # Parameters: $path | ||
220 | # Returns : absolute canonical path | ||
221 | # Throws : n/a | ||
222 | # Comments : is this significantly different from File::Spec->rel2abs? | ||
223 | #============================================================================ | ||
224 | sub canon_path { | ||
225 | my ($path) = @_; | ||
226 | |||
227 | my $cwd = getcwd(); | ||
228 | chdir($path) or error("canon_path: cannot chdir to $path from $cwd"); | ||
229 | my $canon_path = getcwd(); | ||
230 | restore_cwd($cwd); | ||
231 | |||
232 | return $canon_path; | ||
233 | } | ||
234 | |||
235 | sub restore_cwd { | ||
236 | my ($prev) = @_; | ||
237 | chdir($prev) or error("Your current directory $prev seems to have vanished"); | ||
238 | } | ||
239 | |||
240 | =head1 BUGS | ||
241 | |||
242 | =head1 SEE ALSO | ||
243 | |||
244 | =cut | ||
245 | |||
246 | |||
247 | our $LOCAL_IGNORE_FILE = '.stow-local-ignore'; | ||
248 | our $GLOBAL_IGNORE_FILE = '.stow-global-ignore'; | ||
249 | |||
250 | our @default_global_ignore_regexps = | ||
251 | __PACKAGE__->get_default_global_ignore_regexps(); | ||
252 | |||
253 | # These are the default options for each Stow instance. | ||
254 | our %DEFAULT_OPTIONS = ( | ||
255 | conflicts => 0, | ||
256 | simulate => 0, | ||
257 | verbose => 0, | ||
258 | paranoid => 0, | ||
259 | compat => 0, | ||
260 | test_mode => 0, | ||
261 | adopt => 0, | ||
262 | 'no-folding' => 0, | ||
263 | ignore => [], | ||
264 | override => [], | ||
265 | defer => [], | ||
266 | ); | ||
267 | |||
268 | =head1 CONSTRUCTORS | ||
269 | |||
270 | =head2 new(%options) | ||
271 | |||
272 | =head3 Required options | ||
273 | |||
274 | =over 4 | ||
275 | |||
276 | =item * dir - the stow directory | ||
277 | |||
278 | =item * target - the target directory | ||
279 | |||
280 | =back | ||
281 | |||
282 | =head3 Non-mandatory options | ||
283 | |||
284 | See the documentation for the F<stow> CLI front-end for information on these. | ||
285 | |||
286 | =over 4 | ||
287 | |||
288 | =item * conflicts | ||
289 | |||
290 | =item * simulate | ||
291 | |||
292 | =item * verbose | ||
293 | |||
294 | =item * paranoid | ||
295 | |||
296 | =item * compat | ||
297 | |||
298 | =item * test_mode | ||
299 | |||
300 | =item * adopt | ||
301 | |||
302 | =item * no-folding | ||
303 | |||
304 | =item * ignore | ||
305 | |||
306 | =item * override | ||
307 | |||
308 | =item * defer | ||
309 | |||
310 | =back | ||
311 | |||
312 | N.B. This sets the current working directory to the target directory. | ||
313 | |||
314 | =cut | ||
315 | |||
316 | sub new { | ||
317 | my $self = shift; | ||
318 | my $class = ref($self) || $self; | ||
319 | my %opts = @_; | ||
320 | |||
321 | my $new = bless { }, $class; | ||
322 | |||
323 | $new->{action_count} = 0; | ||
324 | |||
325 | for my $required_arg (qw(dir target)) { | ||
326 | croak "$class->new() called without '$required_arg' parameter\n" | ||
327 | unless exists $opts{$required_arg}; | ||
328 | $new->{$required_arg} = delete $opts{$required_arg}; | ||
329 | } | ||
330 | |||
331 | for my $opt (keys %DEFAULT_OPTIONS) { | ||
332 | $new->{$opt} = exists $opts{$opt} ? delete $opts{$opt} | ||
333 | : $DEFAULT_OPTIONS{$opt}; | ||
334 | } | ||
335 | |||
336 | if (%opts) { | ||
337 | croak "$class->new() called with unrecognised parameter(s): ", | ||
338 | join(", ", keys %opts), "\n"; | ||
339 | } | ||
340 | |||
341 | set_debug_level($new->get_verbosity()); | ||
342 | set_test_mode($new->{test_mode}); | ||
343 | $new->set_stow_dir(); | ||
344 | $new->init_state(); | ||
345 | |||
346 | return $new; | ||
347 | } | ||
348 | |||
349 | sub get_verbosity { | ||
350 | my $self = shift; | ||
351 | |||
352 | return $self->{verbose} unless $self->{test_mode}; | ||
353 | |||
354 | return 0 unless exists $ENV{TEST_VERBOSE}; | ||
355 | return 0 unless length $ENV{TEST_VERBOSE}; | ||
356 | |||
357 | # Convert TEST_VERBOSE=y into numeric value | ||
358 | $ENV{TEST_VERBOSE} = 3 if $ENV{TEST_VERBOSE} !~ /^\d+$/; | ||
359 | |||
360 | return $ENV{TEST_VERBOSE}; | ||
361 | } | ||
362 | |||
363 | =head2 set_stow_dir([$dir]) | ||
364 | |||
365 | Sets a new stow directory. This allows the use of multiple stow | ||
366 | directories within one Stow instance, e.g. | ||
367 | |||
368 | $stow->plan_stow('foo'); | ||
369 | $stow->set_stow_dir('/different/stow/dir'); | ||
370 | $stow->plan_stow('bar'); | ||
371 | $stow->process_tasks; | ||
372 | |||
373 | If C<$dir> is omitted, uses the value of the C<dir> parameter passed | ||
374 | to the L<new()> constructor. | ||
375 | |||
376 | =cut | ||
377 | |||
378 | sub set_stow_dir { | ||
379 | my $self = shift; | ||
380 | my ($dir) = @_; | ||
381 | if (defined $dir) { | ||
382 | $self->{dir} = $dir; | ||
383 | } | ||
384 | |||
385 | my $stow_dir = canon_path($self->{dir}); | ||
386 | my $target = canon_path($self->{target}); | ||
387 | $self->{stow_path} = File::Spec->abs2rel($stow_dir, $target); | ||
388 | |||
389 | debug(2, "stow dir is $stow_dir"); | ||
390 | debug(2, "stow dir path relative to target $target is $self->{stow_path}"); | ||
391 | } | ||
392 | |||
393 | sub init_state { | ||
394 | my $self = shift; | ||
395 | |||
396 | # Store conflicts during pre-processing | ||
397 | $self->{conflicts} = {}; | ||
398 | $self->{conflict_count} = 0; | ||
399 | |||
400 | # Store command line packages to stow (-S and -R) | ||
401 | $self->{pkgs_to_stow} = []; | ||
402 | |||
403 | # Store command line packages to unstow (-D and -R) | ||
404 | $self->{pkgs_to_delete} = []; | ||
405 | |||
406 | # The following structures are used by the abstractions that allow us to | ||
407 | # defer operating on the filesystem until after all potential conflicts have | ||
408 | # been assessed. | ||
409 | |||
410 | # $self->{tasks}: list of operations to be performed (in order) | ||
411 | # each element is a hash ref of the form | ||
412 | # { | ||
413 | # action => ... ('create' or 'remove' or 'move') | ||
414 | # type => ... ('link' or 'dir' or 'file') | ||
415 | # path => ... (unique) | ||
416 | # source => ... (only for links) | ||
417 | # dest => ... (only for moving files) | ||
418 | # } | ||
419 | $self->{tasks} = []; | ||
420 | |||
421 | # $self->{dir_task_for}: map a path to the corresponding directory task reference | ||
422 | # This structure allows us to quickly determine if a path has an existing | ||
423 | # directory task associated with it. | ||
424 | $self->{dir_task_for} = {}; | ||
425 | |||
426 | # $self->{link_task_for}: map a path to the corresponding directory task reference | ||
427 | # This structure allows us to quickly determine if a path has an existing | ||
428 | # directory task associated with it. | ||
429 | $self->{link_task_for} = {}; | ||
430 | |||
431 | # N.B.: directory tasks and link tasks are NOT mutually exclusive due | ||
432 | # to tree splitting (which involves a remove link task followed by | ||
433 | # a create directory task). | ||
434 | } | ||
435 | |||
436 | =head1 METHODS | ||
437 | |||
438 | =head2 plan_unstow(@packages) | ||
439 | |||
440 | Plan which symlink/directory creation/removal tasks need to be executed | ||
441 | in order to unstow the given packages. Any potential conflicts are then | ||
442 | accessible via L<get_conflicts()>. | ||
443 | |||
444 | =cut | ||
445 | |||
446 | sub plan_unstow { | ||
447 | my $self = shift; | ||
448 | my @packages = @_; | ||
449 | |||
450 | $self->within_target_do(sub { | ||
451 | for my $package (@packages) { | ||
452 | my $path = join_paths($self->{stow_path}, $package); | ||
453 | if (not -d $path) { | ||
454 | error("The stow directory $self->{stow_path} does not contain package $package"); | ||
455 | } | ||
456 | debug(2, "Planning unstow of package $package..."); | ||
457 | if ($self->{compat}) { | ||
458 | $self->unstow_contents_orig( | ||
459 | $self->{stow_path}, | ||
460 | $package, | ||
461 | '.', | ||
462 | ); | ||
463 | } | ||
464 | else { | ||
465 | $self->unstow_contents( | ||
466 | $self->{stow_path}, | ||
467 | $package, | ||
468 | '.', | ||
469 | ); | ||
470 | } | ||
471 | debug(2, "Planning unstow of package $package... done"); | ||
472 | $self->{action_count}++; | ||
473 | } | ||
474 | }); | ||
475 | } | ||
476 | |||
477 | =head2 plan_stow(@packages) | ||
478 | |||
479 | Plan which symlink/directory creation/removal tasks need to be executed | ||
480 | in order to stow the given packages. Any potential conflicts are then | ||
481 | accessible via L<get_conflicts()>. | ||
482 | |||
483 | =cut | ||
484 | |||
485 | sub plan_stow { | ||
486 | my $self = shift; | ||
487 | my @packages = @_; | ||
488 | |||
489 | $self->within_target_do(sub { | ||
490 | for my $package (@packages) { | ||
491 | my $path = join_paths($self->{stow_path}, $package); | ||
492 | if (not -d $path) { | ||
493 | error("The stow directory $self->{stow_path} does not contain package $package"); | ||
494 | } | ||
495 | debug(2, "Planning stow of package $package..."); | ||
496 | $self->stow_contents( | ||
497 | $self->{stow_path}, | ||
498 | $package, | ||
499 | '.', | ||
500 | $path, # source from target | ||
501 | ); | ||
502 | debug(2, "Planning stow of package $package... done"); | ||
503 | $self->{action_count}++; | ||
504 | } | ||
505 | }); | ||
506 | } | ||
507 | |||
508 | #===== METHOD =============================================================== | ||
509 | # Name : within_target_do() | ||
510 | # Purpose : execute code within target directory, preserving cwd | ||
511 | # Parameters: $code => anonymous subroutine to execute within target dir | ||
512 | # Returns : n/a | ||
513 | # Throws : n/a | ||
514 | # Comments : This is done to ensure that the consumer of the Stow interface | ||
515 | # : doesn't have to worry about (a) what their cwd is, and | ||
516 | # : (b) that their cwd might change. | ||
517 | #============================================================================ | ||
518 | sub within_target_do { | ||
519 | my $self = shift; | ||
520 | my ($code) = @_; | ||
521 | |||
522 | my $cwd = getcwd(); | ||
523 | chdir($self->{target}) | ||
524 | or error("Cannot chdir to target tree: $self->{target} ($!)"); | ||
525 | debug(3, "cwd now $self->{target}"); | ||
526 | |||
527 | $self->$code(); | ||
528 | |||
529 | restore_cwd($cwd); | ||
530 | debug(3, "cwd restored to $cwd"); | ||
531 | } | ||
532 | |||
533 | #===== METHOD =============================================================== | ||
534 | # Name : stow_contents() | ||
535 | # Purpose : stow the contents of the given directory | ||
536 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
537 | # : to the stow dir containing the package to be stowed | ||
538 | # : $package => the package whose contents are being stowed | ||
539 | # : $target => subpath relative to package and target directories | ||
540 | # : $source => relative path from the (sub)dir of target | ||
541 | # : to symlink source | ||
542 | # Returns : n/a | ||
543 | # Throws : a fatal error if directory cannot be read | ||
544 | # Comments : stow_node() and stow_contents() are mutually recursive. | ||
545 | # : $source and $target are used for creating the symlink | ||
546 | # : $path is used for folding/unfolding trees as necessary | ||
547 | #============================================================================ | ||
548 | sub stow_contents { | ||
549 | my $self = shift; | ||
550 | my ($stow_path, $package, $target, $source) = @_; | ||
551 | |||
552 | my $path = join_paths($stow_path, $package, $target); | ||
553 | |||
554 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
555 | |||
556 | my $cwd = getcwd(); | ||
557 | my $msg = "Stowing contents of $path (cwd=$cwd)"; | ||
558 | $msg =~ s!$ENV{HOME}(/|$)!~$1!g; | ||
559 | debug(3, $msg); | ||
560 | debug(4, " => $source"); | ||
561 | |||
562 | error("stow_contents() called with non-directory path: $path") | ||
563 | unless -d $path; | ||
564 | error("stow_contents() called with non-directory target: $target") | ||
565 | unless $self->is_a_node($target); | ||
566 | |||
567 | opendir my $DIR, $path | ||
568 | or error("cannot read directory: $path ($!)"); | ||
569 | my @listing = readdir $DIR; | ||
570 | closedir $DIR; | ||
571 | |||
572 | NODE: | ||
573 | for my $node (@listing) { | ||
574 | next NODE if $node eq '.'; | ||
575 | next NODE if $node eq '..'; | ||
576 | my $node_target = join_paths($target, $node); | ||
577 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
578 | $self->stow_node( | ||
579 | $stow_path, | ||
580 | $package, | ||
581 | $node_target, # target | ||
582 | join_paths($source, $node), # source | ||
583 | ); | ||
584 | } | ||
585 | } | ||
586 | |||
587 | #===== METHOD =============================================================== | ||
588 | # Name : stow_node() | ||
589 | # Purpose : stow the given node | ||
590 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
591 | # : to the stow dir containing the node to be stowed | ||
592 | # : $package => the package containing the node being stowed | ||
593 | # : $target => subpath relative to package and target directories | ||
594 | # : $source => relative path to symlink source from the dir of target | ||
595 | # Returns : n/a | ||
596 | # Throws : fatal exception if a conflict arises | ||
597 | # Comments : stow_node() and stow_contents() are mutually recursive | ||
598 | # : $source and $target are used for creating the symlink | ||
599 | # : $path is used for folding/unfolding trees as necessary | ||
600 | #============================================================================ | ||
601 | sub stow_node { | ||
602 | my $self = shift; | ||
603 | my ($stow_path, $package, $target, $source) = @_; | ||
604 | |||
605 | my $path = join_paths($stow_path, $package, $target); | ||
606 | |||
607 | debug(3, "Stowing $stow_path / $package / $target"); | ||
608 | debug(4, " => $source"); | ||
609 | |||
610 | # Don't try to stow absolute symlinks (they can't be unstowed) | ||
611 | if (-l $source) { | ||
612 | my $second_source = $self->read_a_link($source); | ||
613 | if ($second_source =~ m{\A/}) { | ||
614 | $self->conflict( | ||
615 | 'stow', | ||
616 | $package, | ||
617 | "source is an absolute symlink $source => $second_source" | ||
618 | ); | ||
619 | debug(3, "Absolute symlinks cannot be unstowed"); | ||
620 | return; | ||
621 | } | ||
622 | } | ||
623 | |||
624 | # Does the target already exist? | ||
625 | if ($self->is_a_link($target)) { | ||
626 | # Where is the link pointing? | ||
627 | my $existing_source = $self->read_a_link($target); | ||
628 | if (not $existing_source) { | ||
629 | error("Could not read link: $target"); | ||
630 | } | ||
631 | debug(4, " Evaluate existing link: $target => $existing_source"); | ||
632 | |||
633 | # Does it point to a node under any stow directory? | ||
634 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
635 | $self->find_stowed_path($target, $existing_source); | ||
636 | if (not $existing_path) { | ||
637 | $self->conflict( | ||
638 | 'stow', | ||
639 | $package, | ||
640 | "existing target is not owned by stow: $target" | ||
641 | ); | ||
642 | return; # XXX # | ||
643 | } | ||
644 | |||
645 | # Does the existing $target actually point to anything? | ||
646 | if ($self->is_a_node($existing_path)) { | ||
647 | if ($existing_source eq $source) { | ||
648 | debug(2, "--- Skipping $target as it already points to $source"); | ||
649 | } | ||
650 | elsif ($self->defer($target)) { | ||
651 | debug(2, "--- Deferring installation of: $target"); | ||
652 | } | ||
653 | elsif ($self->override($target)) { | ||
654 | debug(2, "--- Overriding installation of: $target"); | ||
655 | $self->do_unlink($target); | ||
656 | $self->do_link($source, $target); | ||
657 | } | ||
658 | elsif ($self->is_a_dir(join_paths(parent($target), $existing_source)) && | ||
659 | $self->is_a_dir(join_paths(parent($target), $source)) ) { | ||
660 | |||
661 | # If the existing link points to a directory, | ||
662 | # and the proposed new link points to a directory, | ||
663 | # then we can unfold (split open) the tree at that point | ||
664 | |||
665 | debug(2, "--- Unfolding $target which was already owned by $existing_package"); | ||
666 | $self->do_unlink($target); | ||
667 | $self->do_mkdir($target); | ||
668 | $self->stow_contents( | ||
669 | $existing_stow_path, | ||
670 | $existing_package, | ||
671 | $target, | ||
672 | join_paths('..', $existing_source), | ||
673 | ); | ||
674 | $self->stow_contents( | ||
675 | $self->{stow_path}, | ||
676 | $package, | ||
677 | $target, | ||
678 | join_paths('..', $source), | ||
679 | ); | ||
680 | } | ||
681 | else { | ||
682 | $self->conflict( | ||
683 | 'stow', | ||
684 | $package, | ||
685 | "existing target is stowed to a different package: " | ||
686 | . "$target => $existing_source" | ||
687 | ); | ||
688 | } | ||
689 | } | ||
690 | else { | ||
691 | # The existing link is invalid, so replace it with a good link | ||
692 | debug(2, "--- replacing invalid link: $path"); | ||
693 | $self->do_unlink($target); | ||
694 | $self->do_link($source, $target); | ||
695 | } | ||
696 | } | ||
697 | elsif ($self->is_a_node($target)) { | ||
698 | debug(4, " Evaluate existing node: $target"); | ||
699 | if ($self->is_a_dir($target)) { | ||
700 | $self->stow_contents( | ||
701 | $self->{stow_path}, | ||
702 | $package, | ||
703 | $target, | ||
704 | join_paths('..', $source), | ||
705 | ); | ||
706 | } | ||
707 | else { | ||
708 | if ($self->{adopt}) { | ||
709 | $self->do_mv($target, $path); | ||
710 | $self->do_link($source, $target); | ||
711 | } | ||
712 | else { | ||
713 | $self->conflict( | ||
714 | 'stow', | ||
715 | $package, | ||
716 | "existing target is neither a link nor a directory: $target" | ||
717 | ); | ||
718 | } | ||
719 | } | ||
720 | } | ||
721 | elsif ($self->{'no-folding'} && -d $path && ! -l $path) { | ||
722 | $self->do_mkdir($target); | ||
723 | $self->stow_contents( | ||
724 | $self->{stow_path}, | ||
725 | $package, | ||
726 | $target, | ||
727 | join_paths('..', $source), | ||
728 | ); | ||
729 | } | ||
730 | else { | ||
731 | $self->do_link($source, $target); | ||
732 | } | ||
733 | return; | ||
734 | } | ||
735 | |||
736 | #===== METHOD =============================================================== | ||
737 | # Name : should_skip_target_which_is_stow_dir() | ||
738 | # Purpose : determine whether target is a stow directory which should | ||
739 | # : not be stowed to or unstowed from | ||
740 | # Parameters: $target => relative path to symlink target from the current directory | ||
741 | # Returns : true iff target is a stow directory | ||
742 | # Throws : n/a | ||
743 | # Comments : none | ||
744 | #============================================================================ | ||
745 | sub should_skip_target_which_is_stow_dir { | ||
746 | my $self = shift; | ||
747 | my ($target) = @_; | ||
748 | |||
749 | # Don't try to remove anything under a stow directory | ||
750 | if ($target eq $self->{stow_path}) { | ||
751 | warn "WARNING: skipping target which was current stow directory $target\n"; | ||
752 | return 1; | ||
753 | } | ||
754 | |||
755 | if ($self->marked_stow_dir($target)) { | ||
756 | warn "WARNING: skipping protected directory $target\n"; | ||
757 | return 1; | ||
758 | } | ||
759 | |||
760 | debug (4, "$target not protected"); | ||
761 | return 0; | ||
762 | } | ||
763 | |||
764 | sub marked_stow_dir { | ||
765 | my $self = shift; | ||
766 | my ($target) = @_; | ||
767 | for my $f (".stow", ".nonstow") { | ||
768 | if (-e join_paths($target, $f)) { | ||
769 | debug(4, "$target contained $f"); | ||
770 | return 1; | ||
771 | } | ||
772 | } | ||
773 | return 0; | ||
774 | } | ||
775 | |||
776 | #===== METHOD =============================================================== | ||
777 | # Name : unstow_contents_orig() | ||
778 | # Purpose : unstow the contents of the given directory | ||
779 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
780 | # : to the stow dir containing the package to be unstowed | ||
781 | # : $package => the package whose contents are being unstowed | ||
782 | # : $target => relative path to symlink target from the current directory | ||
783 | # Returns : n/a | ||
784 | # Throws : a fatal error if directory cannot be read | ||
785 | # Comments : unstow_node_orig() and unstow_contents_orig() are mutually recursive | ||
786 | # : Here we traverse the target tree, rather than the source tree. | ||
787 | #============================================================================ | ||
788 | sub unstow_contents_orig { | ||
789 | my $self = shift; | ||
790 | my ($stow_path, $package, $target) = @_; | ||
791 | |||
792 | my $path = join_paths($stow_path, $package, $target); | ||
793 | |||
794 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
795 | |||
796 | my $cwd = getcwd(); | ||
797 | my $msg = "Unstowing from $target (compat mode, cwd=$cwd, stow dir=$self->{stow_path})"; | ||
798 | $msg =~ s!$ENV{HOME}(/|$)!~$1!g; | ||
799 | debug(3, $msg); | ||
800 | debug(4, " source path is $path"); | ||
801 | # In compat mode we traverse the target tree not the source tree, | ||
802 | # so we're unstowing the contents of /target/foo, there's no | ||
803 | # guarantee that the corresponding /stow/mypkg/foo exists. | ||
804 | error("unstow_contents_orig() called with non-directory target: $target") | ||
805 | unless -d $target; | ||
806 | |||
807 | opendir my $DIR, $target | ||
808 | or error("cannot read directory: $target ($!)"); | ||
809 | my @listing = readdir $DIR; | ||
810 | closedir $DIR; | ||
811 | |||
812 | NODE: | ||
813 | for my $node (@listing) { | ||
814 | next NODE if $node eq '.'; | ||
815 | next NODE if $node eq '..'; | ||
816 | my $node_target = join_paths($target, $node); | ||
817 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
818 | $self->unstow_node_orig($stow_path, $package, $node_target); | ||
819 | } | ||
820 | } | ||
821 | |||
822 | #===== METHOD =============================================================== | ||
823 | # Name : unstow_node_orig() | ||
824 | # Purpose : unstow the given node | ||
825 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
826 | # : to the stow dir containing the node to be stowed | ||
827 | # : $package => the package containing the node being stowed | ||
828 | # : $target => relative path to symlink target from the current directory | ||
829 | # Returns : n/a | ||
830 | # Throws : fatal error if a conflict arises | ||
831 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
832 | #============================================================================ | ||
833 | sub unstow_node_orig { | ||
834 | my $self = shift; | ||
835 | my ($stow_path, $package, $target) = @_; | ||
836 | |||
837 | my $path = join_paths($stow_path, $package, $target); | ||
838 | |||
839 | debug(3, "Unstowing $target (compat mode)"); | ||
840 | debug(4, " source path is $path"); | ||
841 | |||
842 | # Does the target exist? | ||
843 | if ($self->is_a_link($target)) { | ||
844 | debug(4, " Evaluate existing link: $target"); | ||
845 | |||
846 | # Where is the link pointing? | ||
847 | my $existing_source = $self->read_a_link($target); | ||
848 | if (not $existing_source) { | ||
849 | error("Could not read link: $target"); | ||
850 | } | ||
851 | |||
852 | # Does it point to a node under any stow directory? | ||
853 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
854 | $self->find_stowed_path($target, $existing_source); | ||
855 | if (not $existing_path) { | ||
856 | # We're traversing the target tree not the package tree, | ||
857 | # so we definitely expect to find stuff not owned by stow. | ||
858 | # Therefore we can't flag a conflict. | ||
859 | return; # XXX # | ||
860 | } | ||
861 | |||
862 | # Does the existing $target actually point to anything? | ||
863 | if (-e $existing_path) { | ||
864 | # Does link point to the right place? | ||
865 | if ($existing_path eq $path) { | ||
866 | $self->do_unlink($target); | ||
867 | } | ||
868 | elsif ($self->override($target)) { | ||
869 | debug(2, "--- overriding installation of: $target"); | ||
870 | $self->do_unlink($target); | ||
871 | } | ||
872 | # else leave it alone | ||
873 | } | ||
874 | else { | ||
875 | debug(2, "--- removing invalid link into a stow directory: $path"); | ||
876 | $self->do_unlink($target); | ||
877 | } | ||
878 | } | ||
879 | elsif (-d $target) { | ||
880 | $self->unstow_contents_orig($stow_path, $package, $target); | ||
881 | |||
882 | # This action may have made the parent directory foldable | ||
883 | if (my $parent = $self->foldable($target)) { | ||
884 | $self->fold_tree($target, $parent); | ||
885 | } | ||
886 | } | ||
887 | elsif (-e $target) { | ||
888 | $self->conflict( | ||
889 | 'unstow', | ||
890 | $package, | ||
891 | "existing target is neither a link nor a directory: $target", | ||
892 | ); | ||
893 | } | ||
894 | else { | ||
895 | debug(2, "$target did not exist to be unstowed"); | ||
896 | } | ||
897 | return; | ||
898 | } | ||
899 | |||
900 | #===== METHOD =============================================================== | ||
901 | # Name : unstow_contents() | ||
902 | # Purpose : unstow the contents of the given directory | ||
903 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
904 | # : to the stow dir containing the package to be unstowed | ||
905 | # : $package => the package whose contents are being unstowed | ||
906 | # : $target => relative path to symlink target from the current directory | ||
907 | # Returns : n/a | ||
908 | # Throws : a fatal error if directory cannot be read | ||
909 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
910 | # : Here we traverse the source tree, rather than the target tree. | ||
911 | #============================================================================ | ||
912 | sub unstow_contents { | ||
913 | my $self = shift; | ||
914 | my ($stow_path, $package, $target) = @_; | ||
915 | |||
916 | my $path = join_paths($stow_path, $package, $target); | ||
917 | |||
918 | return if $self->should_skip_target_which_is_stow_dir($target); | ||
919 | |||
920 | my $cwd = getcwd(); | ||
921 | my $msg = "Unstowing from $target (cwd=$cwd, stow dir=$self->{stow_path})"; | ||
922 | $msg =~ s!$ENV{HOME}/!~/!g; | ||
923 | debug(3, $msg); | ||
924 | debug(4, " source path is $path"); | ||
925 | # We traverse the source tree not the target tree, so $path must exist. | ||
926 | error("unstow_contents() called with non-directory path: $path") | ||
927 | unless -d $path; | ||
928 | # When called at the top level, $target should exist. And | ||
929 | # unstow_node() should only call this via mutual recursion if | ||
930 | # $target exists. | ||
931 | error("unstow_contents() called with invalid target: $target") | ||
932 | unless $self->is_a_node($target); | ||
933 | |||
934 | opendir my $DIR, $path | ||
935 | or error("cannot read directory: $path ($!)"); | ||
936 | my @listing = readdir $DIR; | ||
937 | closedir $DIR; | ||
938 | |||
939 | NODE: | ||
940 | for my $node (@listing) { | ||
941 | next NODE if $node eq '.'; | ||
942 | next NODE if $node eq '..'; | ||
943 | my $node_target = join_paths($target, $node); | ||
944 | next NODE if $self->ignore($stow_path, $package, $node_target); | ||
945 | $self->unstow_node($stow_path, $package, $node_target); | ||
946 | } | ||
947 | if (-d $target) { | ||
948 | $self->cleanup_invalid_links($target); | ||
949 | } | ||
950 | } | ||
951 | |||
952 | #===== METHOD =============================================================== | ||
953 | # Name : unstow_node() | ||
954 | # Purpose : unstow the given node | ||
955 | # Parameters: $stow_path => relative path from current (i.e. target) directory | ||
956 | # : to the stow dir containing the node to be stowed | ||
957 | # : $package => the package containing the node being unstowed | ||
958 | # : $target => relative path to symlink target from the current directory | ||
959 | # Returns : n/a | ||
960 | # Throws : fatal error if a conflict arises | ||
961 | # Comments : unstow_node() and unstow_contents() are mutually recursive | ||
962 | #============================================================================ | ||
963 | sub unstow_node { | ||
964 | my $self = shift; | ||
965 | my ($stow_path, $package, $target) = @_; | ||
966 | |||
967 | my $path = join_paths($stow_path, $package, $target); | ||
968 | |||
969 | debug(3, "Unstowing $path"); | ||
970 | debug(4, " target is $target"); | ||
971 | |||
972 | # Does the target exist? | ||
973 | if ($self->is_a_link($target)) { | ||
974 | debug(4, " Evaluate existing link: $target"); | ||
975 | |||
976 | # Where is the link pointing? | ||
977 | my $existing_source = $self->read_a_link($target); | ||
978 | if (not $existing_source) { | ||
979 | error("Could not read link: $target"); | ||
980 | } | ||
981 | |||
982 | if ($existing_source =~ m{\A/}) { | ||
983 | warn "Ignoring an absolute symlink: $target => $existing_source\n"; | ||
984 | return; # XXX # | ||
985 | } | ||
986 | |||
987 | # Does it point to a node under any stow directory? | ||
988 | my ($existing_path, $existing_stow_path, $existing_package) = | ||
989 | $self->find_stowed_path($target, $existing_source); | ||
990 | if (not $existing_path) { | ||
991 | $self->conflict( | ||
992 | 'unstow', | ||
993 | $package, | ||
994 | "existing target is not owned by stow: $target => $existing_source" | ||
995 | ); | ||
996 | return; # XXX # | ||
997 | } | ||
998 | |||
999 | # Does the existing $target actually point to anything? | ||
1000 | if (-e $existing_path) { | ||
1001 | # Does link points to the right place? | ||
1002 | if ($existing_path eq $path) { | ||
1003 | $self->do_unlink($target); | ||
1004 | } | ||
1005 | |||
1006 | # XXX we quietly ignore links that are stowed to a different | ||
1007 | # package. | ||
1008 | |||
1009 | #elsif (defer($target)) { | ||
1010 | # debug(2, "--- deferring to installation of: $target"); | ||
1011 | #} | ||
1012 | #elsif ($self->override($target)) { | ||
1013 | # debug(2, "--- overriding installation of: $target"); | ||
1014 | # $self->do_unlink($target); | ||
1015 | #} | ||
1016 | #else { | ||
1017 | # $self->conflict( | ||
1018 | # 'unstow', | ||
1019 | # $package, | ||
1020 | # "existing target is stowed to a different package: " | ||
1021 | # . "$target => $existing_source" | ||
1022 | # ); | ||
1023 | #} | ||
1024 | } | ||
1025 | else { | ||
1026 | debug(2, "--- removing invalid link into a stow directory: $path"); | ||
1027 | $self->do_unlink($target); | ||
1028 | } | ||
1029 | } | ||
1030 | elsif (-e $target) { | ||
1031 | debug(4, " Evaluate existing node: $target"); | ||
1032 | if (-d $target) { | ||
1033 | $self->unstow_contents($stow_path, $package, $target); | ||
1034 | |||
1035 | # This action may have made the parent directory foldable | ||
1036 | if (my $parent = $self->foldable($target)) { | ||
1037 | $self->fold_tree($target, $parent); | ||
1038 | } | ||
1039 | } | ||
1040 | else { | ||
1041 | $self->conflict( | ||
1042 | 'unstow', | ||
1043 | $package, | ||
1044 | "existing target is neither a link nor a directory: $target", | ||
1045 | ); | ||
1046 | } | ||
1047 | } | ||
1048 | else { | ||
1049 | debug(2, "$target did not exist to be unstowed"); | ||
1050 | } | ||
1051 | return; | ||
1052 | } | ||
1053 | |||
1054 | #===== METHOD =============================================================== | ||
1055 | # Name : path_owned_by_package() | ||
1056 | # Purpose : determine whether the given link points to a member of a | ||
1057 | # : stowed package | ||
1058 | # Parameters: $target => path to a symbolic link under current directory | ||
1059 | # : $source => where that link points to | ||
1060 | # Returns : the package iff link is owned by stow, otherwise '' | ||
1061 | # Throws : n/a | ||
1062 | # Comments : lossy wrapper around find_stowed_path() | ||
1063 | #============================================================================ | ||
1064 | sub path_owned_by_package { | ||
1065 | my $self = shift; | ||
1066 | my ($target, $source) = @_; | ||
1067 | |||
1068 | my ($path, $stow_path, $package) = | ||
1069 | $self->find_stowed_path($target, $source); | ||
1070 | return $package; | ||
1071 | } | ||
1072 | |||
1073 | #===== METHOD =============================================================== | ||
1074 | # Name : find_stowed_path() | ||
1075 | # Purpose : determine whether the given link points to a member of a | ||
1076 | # : stowed package | ||
1077 | # Parameters: $target => path to a symbolic link under current directory | ||
1078 | # : $source => where that link points to (needed because link | ||
1079 | # : might not exist yet due to two-phase approach, | ||
1080 | # : so we can't just call readlink()) | ||
1081 | # Returns : ($path, $stow_path, $package) where $path and $stow_path are | ||
1082 | # : relative from the current (i.e. target) directory. $path | ||
1083 | # : is the full relative path, $stow_path is the relative path | ||
1084 | # : to the stow directory, and $package is the name of the package. | ||
1085 | # : or ('', '', '') if link is not owned by stow | ||
1086 | # Throws : n/a | ||
1087 | # Comments : Needs | ||
1088 | # : Allow for stow dir not being under target dir. | ||
1089 | # : We could put more logic under here for multiple stow dirs. | ||
1090 | #============================================================================ | ||
1091 | sub find_stowed_path { | ||
1092 | my $self = shift; | ||
1093 | my ($target, $source) = @_; | ||
1094 | |||
1095 | # Evaluate softlink relative to its target | ||
1096 | my $path = join_paths(parent($target), $source); | ||
1097 | debug(4, " is path $path owned by stow?"); | ||
1098 | |||
1099 | # Search for .stow files - this allows us to detect links | ||
1100 | # owned by stow directories other than the current one. | ||
1101 | my $dir = ''; | ||
1102 | my @path = split m{/+}, $path; | ||
1103 | for my $i (0 .. $#path) { | ||
1104 | my $part = $path[$i]; | ||
1105 | $dir = join_paths($dir, $part); | ||
1106 | if ($self->marked_stow_dir($dir)) { | ||
1107 | # FIXME - not sure if this can ever happen | ||
1108 | internal_error("find_stowed_path() called directly on stow dir") | ||
1109 | if $i == $#path; | ||
1110 | |||
1111 | debug(4, " yes - $dir was marked as a stow dir"); | ||
1112 | my $package = $path[$i + 1]; | ||
1113 | return ($path, $dir, $package); | ||
1114 | } | ||
1115 | } | ||
1116 | |||
1117 | # If no .stow file was found, we need to find out whether it's | ||
1118 | # owned by the current stow directory, in which case $path will be | ||
1119 | # a prefix of $self->{stow_path}. | ||
1120 | my @stow_path = split m{/+}, $self->{stow_path}; | ||
1121 | |||
1122 | # Strip off common prefixes until one is empty | ||
1123 | while (@path && @stow_path) { | ||
1124 | if ((shift @path) ne (shift @stow_path)) { | ||
1125 | debug(4, " no - either $path not under $self->{stow_path} or vice-versa"); | ||
1126 | return ('', '', ''); | ||
1127 | } | ||
1128 | } | ||
1129 | |||
1130 | if (@stow_path) { # @path must be empty | ||
1131 | debug(4, " no - $path is not under $self->{stow_path}"); | ||
1132 | return ('', '', ''); | ||
1133 | } | ||
1134 | |||
1135 | my $package = shift @path; | ||
1136 | |||
1137 | debug(4, " yes - by $package in " . join_paths(@path)); | ||
1138 | return ($path, $self->{stow_path}, $package); | ||
1139 | } | ||
1140 | |||
1141 | #===== METHOD ================================================================ | ||
1142 | # Name : cleanup_invalid_links() | ||
1143 | # Purpose : clean up invalid links that may block folding | ||
1144 | # Parameters: $dir => path to directory to check | ||
1145 | # Returns : n/a | ||
1146 | # Throws : no exceptions | ||
1147 | # Comments : removing files from a stowed package is probably a bad practice | ||
1148 | # : so this kind of clean up is not _really_ stow's responsibility; | ||
1149 | # : however, failing to clean up can block tree folding, so we'll do | ||
1150 | # : it anyway | ||
1151 | #============================================================================= | ||
1152 | sub cleanup_invalid_links { | ||
1153 | my $self = shift; | ||
1154 | my ($dir) = @_; | ||
1155 | |||
1156 | if (not -d $dir) { | ||
1157 | error("cleanup_invalid_links() called with a non-directory: $dir"); | ||
1158 | } | ||
1159 | |||
1160 | opendir my $DIR, $dir | ||
1161 | or error("cannot read directory: $dir ($!)"); | ||
1162 | my @listing = readdir $DIR; | ||
1163 | closedir $DIR; | ||
1164 | |||
1165 | NODE: | ||
1166 | for my $node (@listing) { | ||
1167 | next NODE if $node eq '.'; | ||
1168 | next NODE if $node eq '..'; | ||
1169 | |||
1170 | my $node_path = join_paths($dir, $node); | ||
1171 | |||
1172 | if (-l $node_path and not exists $self->{link_task_for}{$node_path}) { | ||
1173 | |||
1174 | # Where is the link pointing? | ||
1175 | # (don't use read_a_link() here) | ||
1176 | my $source = readlink($node_path); | ||
1177 | if (not $source) { | ||
1178 | error("Could not read link $node_path"); | ||
1179 | } | ||
1180 | |||
1181 | if ( | ||
1182 | not -e join_paths($dir, $source) and # bad link | ||
1183 | $self->path_owned_by_package($node_path, $source) # owned by stow | ||
1184 | ){ | ||
1185 | debug(2, "--- removing stale link: $node_path => " . | ||
1186 | join_paths($dir, $source)); | ||
1187 | $self->do_unlink($node_path); | ||
1188 | } | ||
1189 | } | ||
1190 | } | ||
1191 | return; | ||
1192 | } | ||
1193 | |||
1194 | |||
1195 | #===== METHOD =============================================================== | ||
1196 | # Name : foldable() | ||
1197 | # Purpose : determine whether a tree can be folded | ||
1198 | # Parameters: $target => path to a directory | ||
1199 | # Returns : path to the parent dir iff the tree can be safely folded | ||
1200 | # Throws : n/a | ||
1201 | # Comments : the path returned is relative to the parent of $target, | ||
1202 | # : that is, it can be used as the source for a replacement symlink | ||
1203 | #============================================================================ | ||
1204 | sub foldable { | ||
1205 | my $self = shift; | ||
1206 | my ($target) = @_; | ||
1207 | |||
1208 | debug(3, "--- Is $target foldable?"); | ||
1209 | if ($self->{'no-folding'}) { | ||
1210 | debug(3, "--- no because --no-folding enabled"); | ||
1211 | return ''; | ||
1212 | } | ||
1213 | |||
1214 | opendir my $DIR, $target | ||
1215 | or error(qq{Cannot read directory "$target" ($!)\n}); | ||
1216 | my @listing = readdir $DIR; | ||
1217 | closedir $DIR; | ||
1218 | |||
1219 | my $parent = ''; | ||
1220 | NODE: | ||
1221 | for my $node (@listing) { | ||
1222 | |||
1223 | next NODE if $node eq '.'; | ||
1224 | next NODE if $node eq '..'; | ||
1225 | |||
1226 | my $path = join_paths($target, $node); | ||
1227 | |||
1228 | # Skip nodes scheduled for removal | ||
1229 | next NODE if not $self->is_a_node($path); | ||
1230 | |||
1231 | # If it's not a link then we can't fold its parent | ||
1232 | return '' if not $self->is_a_link($path); | ||
1233 | |||
1234 | # Where is the link pointing? | ||
1235 | my $source = $self->read_a_link($path); | ||
1236 | if (not $source) { | ||
1237 | error("Could not read link $path"); | ||
1238 | } | ||
1239 | if ($parent eq '') { | ||
1240 | $parent = parent($source) | ||
1241 | } | ||
1242 | elsif ($parent ne parent($source)) { | ||
1243 | return ''; | ||
1244 | } | ||
1245 | } | ||
1246 | return '' if not $parent; | ||
1247 | |||
1248 | # If we get here then all nodes inside $target are links, and those links | ||
1249 | # point to nodes inside the same directory. | ||
1250 | |||
1251 | # chop of leading '..' to get the path to the common parent directory | ||
1252 | # relative to the parent of our $target | ||
1253 | $parent =~ s{\A\.\./}{}; | ||
1254 | |||
1255 | # If the resulting path is owned by stow, we can fold it | ||
1256 | if ($self->path_owned_by_package($target, $parent)) { | ||
1257 | debug(3, "--- $target is foldable"); | ||
1258 | return $parent; | ||
1259 | } | ||
1260 | else { | ||
1261 | return ''; | ||
1262 | } | ||
1263 | } | ||
1264 | |||
1265 | #===== METHOD =============================================================== | ||
1266 | # Name : fold_tree() | ||
1267 | # Purpose : fold the given tree | ||
1268 | # Parameters: $source => link to the folded tree source | ||
1269 | # : $target => directory that we will replace with a link to $source | ||
1270 | # Returns : n/a | ||
1271 | # Throws : none | ||
1272 | # Comments : only called iff foldable() is true so we can remove some checks | ||
1273 | #============================================================================ | ||
1274 | sub fold_tree { | ||
1275 | my $self = shift; | ||
1276 | my ($target, $source) = @_; | ||
1277 | |||
1278 | debug(3, "--- Folding tree: $target => $source"); | ||
1279 | |||
1280 | opendir my $DIR, $target | ||
1281 | or error(qq{Cannot read directory "$target" ($!)\n}); | ||
1282 | my @listing = readdir $DIR; | ||
1283 | closedir $DIR; | ||
1284 | |||
1285 | NODE: | ||
1286 | for my $node (@listing) { | ||
1287 | next NODE if $node eq '.'; | ||
1288 | next NODE if $node eq '..'; | ||
1289 | next NODE if not $self->is_a_node(join_paths($target, $node)); | ||
1290 | $self->do_unlink(join_paths($target, $node)); | ||
1291 | } | ||
1292 | $self->do_rmdir($target); | ||
1293 | $self->do_link($source, $target); | ||
1294 | return; | ||
1295 | } | ||
1296 | |||
1297 | |||
1298 | #===== METHOD =============================================================== | ||
1299 | # Name : conflict() | ||
1300 | # Purpose : handle conflicts in stow operations | ||
1301 | # Parameters: $package => the package involved with the conflicting operation | ||
1302 | # : $message => a description of the conflict | ||
1303 | # Returns : n/a | ||
1304 | # Throws : none | ||
1305 | # Comments : none | ||
1306 | #============================================================================ | ||
1307 | sub conflict { | ||
1308 | my $self = shift; | ||
1309 | my ($action, $package, $message) = @_; | ||
1310 | |||
1311 | debug(2, "CONFLICT when ${action}ing $package: $message"); | ||
1312 | $self->{conflicts}{$action}{$package} ||= []; | ||
1313 | push @{ $self->{conflicts}{$action}{$package} }, $message; | ||
1314 | $self->{conflict_count}++; | ||
1315 | |||
1316 | return; | ||
1317 | } | ||
1318 | |||
1319 | =head2 get_conflicts() | ||
1320 | |||
1321 | Returns a nested hash of all potential conflicts discovered: the keys | ||
1322 | are actions ('stow' or 'unstow'), and the values are hashrefs whose | ||
1323 | keys are stow package names and whose values are conflict | ||
1324 | descriptions, e.g.: | ||
1325 | |||
1326 | ( | ||
1327 | stow => { | ||
1328 | perl => [ | ||
1329 | "existing target is not owned by stow: bin/a2p" | ||
1330 | "existing target is neither a link nor a directory: bin/perl" | ||
1331 | ] | ||
1332 | } | ||
1333 | ) | ||
1334 | |||
1335 | =cut | ||
1336 | |||
1337 | sub get_conflicts { | ||
1338 | my $self = shift; | ||
1339 | return %{ $self->{conflicts} }; | ||
1340 | } | ||
1341 | |||
1342 | =head2 get_conflict_count() | ||
1343 | |||
1344 | Returns the number of conflicts found. | ||
1345 | |||
1346 | =cut | ||
1347 | |||
1348 | sub get_conflict_count { | ||
1349 | my $self = shift; | ||
1350 | return $self->{conflict_count}; | ||
1351 | } | ||
1352 | |||
1353 | =head2 get_tasks() | ||
1354 | |||
1355 | Returns a list of all symlink/directory creation/removal tasks. | ||
1356 | |||
1357 | =cut | ||
1358 | |||
1359 | sub get_tasks { | ||
1360 | my $self = shift; | ||
1361 | return @{ $self->{tasks} }; | ||
1362 | } | ||
1363 | |||
1364 | =head2 get_action_count() | ||
1365 | |||
1366 | Returns the number of actions planned for this Stow instance. | ||
1367 | |||
1368 | =cut | ||
1369 | |||
1370 | sub get_action_count { | ||
1371 | my $self = shift; | ||
1372 | return $self->{action_count}; | ||
1373 | } | ||
1374 | |||
1375 | #===== METHOD ================================================================ | ||
1376 | # Name : ignore | ||
1377 | # Purpose : determine if the given path matches a regex in our ignore list | ||
1378 | # Parameters: $stow_path => the stow directory containing the package | ||
1379 | # : $package => the package containing the path | ||
1380 | # : $target => the path to check against the ignore list | ||
1381 | # : relative to its package directory | ||
1382 | # Returns : true iff the path should be ignored | ||
1383 | # Throws : no exceptions | ||
1384 | # Comments : none | ||
1385 | #============================================================================= | ||
1386 | sub ignore { | ||
1387 | my $self = shift; | ||
1388 | my ($stow_path, $package, $target) = @_; | ||
1389 | |||
1390 | internal_error(__PACKAGE__ . "::ignore() called with empty target") | ||
1391 | unless length $target; | ||
1392 | |||
1393 | for my $suffix (@{ $self->{ignore} }) { | ||
1394 | if ($target =~ m/$suffix/) { | ||
1395 | debug(4, " Ignoring path $target due to --ignore=$suffix"); | ||
1396 | return 1; | ||
1397 | } | ||
1398 | } | ||
1399 | |||
1400 | my $package_dir = join_paths($stow_path, $package); | ||
1401 | my ($path_regexp, $segment_regexp) = | ||
1402 | $self->get_ignore_regexps($package_dir); | ||
1403 | debug(5, " Ignore list regexp for paths: " . | ||
1404 | (defined $path_regexp ? "/$path_regexp/" : "none")); | ||
1405 | debug(5, " Ignore list regexp for segments: " . | ||
1406 | (defined $segment_regexp ? "/$segment_regexp/" : "none")); | ||
1407 | |||
1408 | if (defined $path_regexp and "/$target" =~ $path_regexp) { | ||
1409 | debug(4, " Ignoring path /$target"); | ||
1410 | return 1; | ||
1411 | } | ||
1412 | |||
1413 | (my $basename = $target) =~ s!.+/!!; | ||
1414 | if (defined $segment_regexp and $basename =~ $segment_regexp) { | ||
1415 | debug(4, " Ignoring path segment $basename"); | ||
1416 | return 1; | ||
1417 | } | ||
1418 | |||
1419 | debug(5, " Not ignoring $target"); | ||
1420 | return 0; | ||
1421 | } | ||
1422 | |||
1423 | sub get_ignore_regexps { | ||
1424 | my $self = shift; | ||
1425 | my ($dir) = @_; | ||
1426 | |||
1427 | # N.B. the local and global stow ignore files have to have different | ||
1428 | # names so that: | ||
1429 | # 1. the global one can be a symlink to within a stow | ||
1430 | # package, managed by stow itself, and | ||
1431 | # 2. the local ones can be ignored via hardcoded logic in | ||
1432 | # GlobsToRegexp(), so that they always stay within their stow packages. | ||
1433 | |||
1434 | my $local_stow_ignore = join_paths($dir, $LOCAL_IGNORE_FILE); | ||
1435 | my $global_stow_ignore = join_paths($ENV{HOME}, $GLOBAL_IGNORE_FILE); | ||
1436 | |||
1437 | for my $file ($local_stow_ignore, $global_stow_ignore) { | ||
1438 | if (-e $file) { | ||
1439 | debug(5, " Using ignore file: $file"); | ||
1440 | return $self->get_ignore_regexps_from_file($file); | ||
1441 | } | ||
1442 | else { | ||
1443 | debug(5, " $file didn't exist"); | ||
1444 | } | ||
1445 | } | ||
1446 | |||
1447 | debug(4, " Using built-in ignore list"); | ||
1448 | return @default_global_ignore_regexps; | ||
1449 | } | ||
1450 | |||
1451 | my %ignore_file_regexps; | ||
1452 | |||
1453 | sub get_ignore_regexps_from_file { | ||
1454 | my $self = shift; | ||
1455 | my ($file) = @_; | ||
1456 | |||
1457 | if (exists $ignore_file_regexps{$file}) { | ||
1458 | debug(4, " Using memoized regexps from $file"); | ||
1459 | return @{ $ignore_file_regexps{$file} }; | ||
1460 | } | ||
1461 | |||
1462 | if (! open(REGEXPS, $file)) { | ||
1463 | debug(4, " Failed to open $file: $!"); | ||
1464 | return undef; | ||
1465 | } | ||
1466 | |||
1467 | my @regexps = $self->get_ignore_regexps_from_fh(\*REGEXPS); | ||
1468 | close(REGEXPS); | ||
1469 | |||
1470 | $ignore_file_regexps{$file} = [ @regexps ]; | ||
1471 | return @regexps; | ||
1472 | } | ||
1473 | |||
1474 | =head2 invalidate_memoized_regexp($file) | ||
1475 | |||
1476 | For efficiency of performance, regular expressions are compiled from | ||
1477 | each ignore list file the first time it is used by the Stow process, | ||
1478 | and then memoized for future use. If you expect the contents of these | ||
1479 | files to change during a single run, you will need to invalidate the | ||
1480 | memoized value from this cache. This method allows you to do that. | ||
1481 | |||
1482 | =cut | ||
1483 | |||
1484 | sub invalidate_memoized_regexp { | ||
1485 | my $self = shift; | ||
1486 | my ($file) = @_; | ||
1487 | if (exists $ignore_file_regexps{$file}) { | ||
1488 | debug(4, " Invalidated memoized regexp for $file"); | ||
1489 | delete $ignore_file_regexps{$file}; | ||
1490 | } | ||
1491 | else { | ||
1492 | debug(2, " WARNING: no memoized regexp for $file to invalidate"); | ||
1493 | } | ||
1494 | } | ||
1495 | |||
1496 | sub get_ignore_regexps_from_fh { | ||
1497 | my $self = shift; | ||
1498 | my ($fh) = @_; | ||
1499 | my %regexps; | ||
1500 | while (<$fh>) { | ||
1501 | chomp; | ||
1502 | s/^\s+//; | ||
1503 | s/\s+$//; | ||
1504 | next if /^#/ or length($_) == 0; | ||
1505 | s/\s+#.+//; # strip comments to right of pattern | ||
1506 | s/\\#/#/g; | ||
1507 | $regexps{$_}++; | ||
1508 | } | ||
1509 | |||
1510 | # Local ignore lists should *always* stay within the stow directory, | ||
1511 | # because this is the only place stow looks for them. | ||
1512 | $regexps{"^/\Q$LOCAL_IGNORE_FILE\E\$"}++; | ||
1513 | |||
1514 | return $self->compile_ignore_regexps(%regexps); | ||
1515 | } | ||
1516 | |||
1517 | sub compile_ignore_regexps { | ||
1518 | my $self = shift; | ||
1519 | my (%regexps) = @_; | ||
1520 | |||
1521 | my @segment_regexps; | ||
1522 | my @path_regexps; | ||
1523 | for my $regexp (keys %regexps) { | ||
1524 | if (index($regexp, '/') < 0) { | ||
1525 | # No / found in regexp, so use it for matching against basename | ||
1526 | push @segment_regexps, $regexp; | ||
1527 | } | ||
1528 | else { | ||
1529 | # / found in regexp, so use it for matching against full path | ||
1530 | push @path_regexps, $regexp; | ||
1531 | } | ||
1532 | } | ||
1533 | |||
1534 | my $segment_regexp = join '|', @segment_regexps; | ||
1535 | my $path_regexp = join '|', @path_regexps; | ||
1536 | $segment_regexp = @segment_regexps ? | ||
1537 | $self->compile_regexp("^($segment_regexp)\$") : undef; | ||
1538 | $path_regexp = @path_regexps ? | ||
1539 | $self->compile_regexp("(^|/)($path_regexp)(/|\$)") : undef; | ||
1540 | |||
1541 | return ($path_regexp, $segment_regexp); | ||
1542 | } | ||
1543 | |||
1544 | sub compile_regexp { | ||
1545 | my $self = shift; | ||
1546 | my ($regexp) = @_; | ||
1547 | my $compiled = eval { qr/$regexp/ }; | ||
1548 | die "Failed to compile regexp: $@\n" if $@; | ||
1549 | return $compiled; | ||
1550 | } | ||
1551 | |||
1552 | sub get_default_global_ignore_regexps { | ||
1553 | my $class = shift; | ||
1554 | # Bootstrap issue - first time we stow, we will be stowing | ||
1555 | # .cvsignore so it might not exist in ~ yet, or if it does, it could | ||
1556 | # be an old version missing the entries we need. So we make sure | ||
1557 | # they are there by hardcoding some crucial entries. | ||
1558 | |||
1559 | my %regexps; | ||
1560 | |||
1561 | $regexps{"RCS"}++; | ||
1562 | $regexps{".+,v"}++; | ||
1563 | $regexps{"CVS"}++; | ||
1564 | $regexps{"\.\#.+"}++; | ||
1565 | $regexps{"\.cvsignore"}++; | ||
1566 | $regexps{"\.svn"}++; | ||
1567 | $regexps{"_darcs"}++; | ||
1568 | $regexps{"\.hg"}++; | ||
1569 | $regexps{"\.git"}++; | ||
1570 | $regexps{"\.gitignore"}++; | ||
1571 | $regexps{".+~"}++; | ||
1572 | $regexps{"\#.*\#"}++; | ||
1573 | $regexps{"^/README.*"}++; | ||
1574 | $regexps{"^/LICENSE.*"}++; | ||
1575 | $regexps{"^/COPYING"}++; | ||
1576 | |||
1577 | # Local ignore lists should *always* stay within the stow directory, | ||
1578 | # because this is the only place stow looks for them. | ||
1579 | $regexps{"^/\Q$LOCAL_IGNORE_FILE\E\$"}++; | ||
1580 | |||
1581 | return $class->compile_ignore_regexps(%regexps); | ||
1582 | } | ||
1583 | |||
1584 | #===== METHOD ================================================================ | ||
1585 | # Name : defer | ||
1586 | # Purpose : determine if the given path matches a regex in our defer list | ||
1587 | # Parameters: $path | ||
1588 | # Returns : Boolean | ||
1589 | # Throws : no exceptions | ||
1590 | # Comments : none | ||
1591 | #============================================================================= | ||
1592 | sub defer { | ||
1593 | my $self = shift; | ||
1594 | my ($path) = @_; | ||
1595 | |||
1596 | for my $prefix (@{ $self->{defer} }) { | ||
1597 | return 1 if $path =~ m/$prefix/; | ||
1598 | } | ||
1599 | return 0; | ||
1600 | } | ||
1601 | |||
1602 | #===== METHOD ================================================================ | ||
1603 | # Name : override | ||
1604 | # Purpose : determine if the given path matches a regex in our override list | ||
1605 | # Parameters: $path | ||
1606 | # Returns : Boolean | ||
1607 | # Throws : no exceptions | ||
1608 | # Comments : none | ||
1609 | #============================================================================= | ||
1610 | sub override { | ||
1611 | my $self = shift; | ||
1612 | my ($path) = @_; | ||
1613 | |||
1614 | for my $regex (@{ $self->{override} }) { | ||
1615 | return 1 if $path =~ m/$regex/; | ||
1616 | } | ||
1617 | return 0; | ||
1618 | } | ||
1619 | |||
1620 | ############################################################################## | ||
1621 | # | ||
1622 | # The following code provides the abstractions that allow us to defer operating | ||
1623 | # on the filesystem until after all potential conflcits have been assessed. | ||
1624 | # | ||
1625 | ############################################################################## | ||
1626 | |||
1627 | #===== METHOD =============================================================== | ||
1628 | # Name : process_tasks() | ||
1629 | # Purpose : process each task in the tasks list | ||
1630 | # Parameters: none | ||
1631 | # Returns : n/a | ||
1632 | # Throws : fatal error if tasks list is corrupted or a task fails | ||
1633 | # Comments : none | ||
1634 | #============================================================================ | ||
1635 | sub process_tasks { | ||
1636 | my $self = shift; | ||
1637 | |||
1638 | debug(2, "Processing tasks..."); | ||
1639 | |||
1640 | # Strip out all tasks with a skip action | ||
1641 | $self->{tasks} = [ grep { $_->{action} ne 'skip' } @{ $self->{tasks} } ]; | ||
1642 | |||
1643 | if (not @{ $self->{tasks} }) { | ||
1644 | return; | ||
1645 | } | ||
1646 | |||
1647 | $self->within_target_do(sub { | ||
1648 | for my $task (@{ $self->{tasks} }) { | ||
1649 | $self->process_task($task); | ||
1650 | } | ||
1651 | }); | ||
1652 | |||
1653 | debug(2, "Processing tasks... done"); | ||
1654 | } | ||
1655 | |||
1656 | #===== METHOD =============================================================== | ||
1657 | # Name : process_task() | ||
1658 | # Purpose : process a single task | ||
1659 | # Parameters: $task => the task to process | ||
1660 | # Returns : n/a | ||
1661 | # Throws : fatal error if task fails | ||
1662 | # Comments : Must run from within target directory. | ||
1663 | # : Task involve either creating or deleting dirs and symlinks | ||
1664 | # : an action is set to 'skip' if it is found to be redundant | ||
1665 | #============================================================================ | ||
1666 | sub process_task { | ||
1667 | my $self = shift; | ||
1668 | my ($task) = @_; | ||
1669 | |||
1670 | if ($task->{action} eq 'create') { | ||
1671 | if ($task->{type} eq 'dir') { | ||
1672 | mkdir($task->{path}, 0777) | ||
1673 | or error("Could not create directory: $task->{path} ($!)"); | ||
1674 | return; | ||
1675 | } | ||
1676 | elsif ($task->{type} eq 'link') { | ||
1677 | symlink $task->{source}, $task->{path} | ||
1678 | or error( | ||
1679 | "Could not create symlink: %s => %s ($!)", | ||
1680 | $task->{path}, | ||
1681 | $task->{source} | ||
1682 | ); | ||
1683 | return; | ||
1684 | } | ||
1685 | } | ||
1686 | elsif ($task->{action} eq 'remove') { | ||
1687 | if ($task->{type} eq 'dir') { | ||
1688 | rmdir $task->{path} | ||
1689 | or error("Could not remove directory: $task->{path} ($!)"); | ||
1690 | return; | ||
1691 | } | ||
1692 | elsif ($task->{type} eq 'link') { | ||
1693 | unlink $task->{path} | ||
1694 | or error("Could not remove link: $task->{path} ($!)"); | ||
1695 | return; | ||
1696 | } | ||
1697 | } | ||
1698 | elsif ($task->{action} eq 'move') { | ||
1699 | if ($task->{type} eq 'file') { | ||
1700 | # rename() not good enough, since the stow directory | ||
1701 | # might be on a different filesystem to the target. | ||
1702 | move $task->{path}, $task->{dest} | ||
1703 | or error("Could not move $task->{path} -> $task->{dest} ($!)"); | ||
1704 | return; | ||
1705 | } | ||
1706 | } | ||
1707 | |||
1708 | # Should never happen. | ||
1709 | internal_error("bad task action: $task->{action}"); | ||
1710 | } | ||
1711 | |||
1712 | #===== METHOD =============================================================== | ||
1713 | # Name : link_task_action() | ||
1714 | # Purpose : finds the link task action for the given path, if there is one | ||
1715 | # Parameters: $path | ||
1716 | # Returns : 'remove', 'create', or '' if there is no action | ||
1717 | # Throws : a fatal exception if an invalid action is found | ||
1718 | # Comments : none | ||
1719 | #============================================================================ | ||
1720 | sub link_task_action { | ||
1721 | my $self = shift; | ||
1722 | my ($path) = @_; | ||
1723 | |||
1724 | if (! exists $self->{link_task_for}{$path}) { | ||
1725 | debug(4, " link_task_action($path): no task"); | ||
1726 | return ''; | ||
1727 | } | ||
1728 | |||
1729 | my $action = $self->{link_task_for}{$path}->{action}; | ||
1730 | internal_error("bad task action: $action") | ||
1731 | unless $action eq 'remove' or $action eq 'create'; | ||
1732 | |||
1733 | debug(4, " link_task_action($path): link task exists with action $action"); | ||
1734 | return $action; | ||
1735 | } | ||
1736 | |||
1737 | #===== METHOD =============================================================== | ||
1738 | # Name : dir_task_action() | ||
1739 | # Purpose : finds the dir task action for the given path, if there is one | ||
1740 | # Parameters: $path | ||
1741 | # Returns : 'remove', 'create', or '' if there is no action | ||
1742 | # Throws : a fatal exception if an invalid action is found | ||
1743 | # Comments : none | ||
1744 | #============================================================================ | ||
1745 | sub dir_task_action { | ||
1746 | my $self = shift; | ||
1747 | my ($path) = @_; | ||
1748 | |||
1749 | if (! exists $self->{dir_task_for}{$path}) { | ||
1750 | debug(4, " dir_task_action($path): no task"); | ||
1751 | return ''; | ||
1752 | } | ||
1753 | |||
1754 | my $action = $self->{dir_task_for}{$path}->{action}; | ||
1755 | internal_error("bad task action: $action") | ||
1756 | unless $action eq 'remove' or $action eq 'create'; | ||
1757 | |||
1758 | debug(4, " dir_task_action($path): dir task exists with action $action"); | ||
1759 | return $action; | ||
1760 | } | ||
1761 | |||
1762 | #===== METHOD =============================================================== | ||
1763 | # Name : parent_link_scheduled_for_removal() | ||
1764 | # Purpose : determine whether the given path or any parent thereof | ||
1765 | # : is a link scheduled for removal | ||
1766 | # Parameters: $path | ||
1767 | # Returns : Boolean | ||
1768 | # Throws : none | ||
1769 | # Comments : none | ||
1770 | #============================================================================ | ||
1771 | sub parent_link_scheduled_for_removal { | ||
1772 | my $self = shift; | ||
1773 | my ($path) = @_; | ||
1774 | |||
1775 | my $prefix = ''; | ||
1776 | for my $part (split m{/+}, $path) { | ||
1777 | $prefix = join_paths($prefix, $part); | ||
1778 | debug(4, " parent_link_scheduled_for_removal($path): prefix $prefix"); | ||
1779 | if (exists $self->{link_task_for}{$prefix} and | ||
1780 | $self->{link_task_for}{$prefix}->{action} eq 'remove') { | ||
1781 | debug(4, " parent_link_scheduled_for_removal($path): link scheduled for removal"); | ||
1782 | return 1; | ||
1783 | } | ||
1784 | } | ||
1785 | |||
1786 | debug(4, " parent_link_scheduled_for_removal($path): returning false"); | ||
1787 | return 0; | ||
1788 | } | ||
1789 | |||
1790 | #===== METHOD =============================================================== | ||
1791 | # Name : is_a_link() | ||
1792 | # Purpose : determine if the given path is a current or planned link | ||
1793 | # Parameters: $path | ||
1794 | # Returns : Boolean | ||
1795 | # Throws : none | ||
1796 | # Comments : returns false if an existing link is scheduled for removal | ||
1797 | # : and true if a non-existent link is scheduled for creation | ||
1798 | #============================================================================ | ||
1799 | sub is_a_link { | ||
1800 | my $self = shift; | ||
1801 | my ($path) = @_; | ||
1802 | debug(4, " is_a_link($path)"); | ||
1803 | |||
1804 | if (my $action = $self->link_task_action($path)) { | ||
1805 | if ($action eq 'remove') { | ||
1806 | debug(4, " is_a_link($path): returning 0 (remove action found)"); | ||
1807 | return 0; | ||
1808 | } | ||
1809 | elsif ($action eq 'create') { | ||
1810 | debug(4, " is_a_link($path): returning 1 (create action found)"); | ||
1811 | return 1; | ||
1812 | } | ||
1813 | } | ||
1814 | |||
1815 | if (-l $path) { | ||
1816 | # Check if any of its parent are links scheduled for removal | ||
1817 | # (need this for edge case during unfolding) | ||
1818 | debug(4, " is_a_link($path): is a real link"); | ||
1819 | return $self->parent_link_scheduled_for_removal($path) ? 0 : 1; | ||
1820 | } | ||
1821 | |||
1822 | debug(4, " is_a_link($path): returning 0"); | ||
1823 | return 0; | ||
1824 | } | ||
1825 | |||
1826 | #===== METHOD =============================================================== | ||
1827 | # Name : is_a_dir() | ||
1828 | # Purpose : determine if the given path is a current or planned directory | ||
1829 | # Parameters: $path | ||
1830 | # Returns : Boolean | ||
1831 | # Throws : none | ||
1832 | # Comments : returns false if an existing directory is scheduled for removal | ||
1833 | # : and true if a non-existent directory is scheduled for creation | ||
1834 | # : we also need to be sure we are not just following a link | ||
1835 | #============================================================================ | ||
1836 | sub is_a_dir { | ||
1837 | my $self = shift; | ||
1838 | my ($path) = @_; | ||
1839 | debug(4, " is_a_dir($path)"); | ||
1840 | |||
1841 | if (my $action = $self->dir_task_action($path)) { | ||
1842 | if ($action eq 'remove') { | ||
1843 | return 0; | ||
1844 | } | ||
1845 | elsif ($action eq 'create') { | ||
1846 | return 1; | ||
1847 | } | ||
1848 | } | ||
1849 | |||
1850 | return 0 if $self->parent_link_scheduled_for_removal($path); | ||
1851 | |||
1852 | if (-d $path) { | ||
1853 | debug(4, " is_a_dir($path): real dir"); | ||
1854 | return 1; | ||
1855 | } | ||
1856 | |||
1857 | debug(4, " is_a_dir($path): returning false"); | ||
1858 | return 0; | ||
1859 | } | ||
1860 | |||
1861 | #===== METHOD =============================================================== | ||
1862 | # Name : is_a_node() | ||
1863 | # Purpose : determine whether the given path is a current or planned node | ||
1864 | # Parameters: $path | ||
1865 | # Returns : Boolean | ||
1866 | # Throws : none | ||
1867 | # Comments : returns false if an existing node is scheduled for removal | ||
1868 | # : true if a non-existent node is scheduled for creation | ||
1869 | # : we also need to be sure we are not just following a link | ||
1870 | #============================================================================ | ||
1871 | sub is_a_node { | ||
1872 | my $self = shift; | ||
1873 | my ($path) = @_; | ||
1874 | debug(4, " is_a_node($path)"); | ||
1875 | |||
1876 | my $laction = $self->link_task_action($path); | ||
1877 | my $daction = $self->dir_task_action($path); | ||
1878 | |||
1879 | if ($laction eq 'remove') { | ||
1880 | if ($daction eq 'remove') { | ||
1881 | internal_error("removing link and dir: $path"); | ||
1882 | return 0; | ||
1883 | } | ||
1884 | elsif ($daction eq 'create') { | ||
1885 | # Assume that we're unfolding $path, and that the link | ||
1886 | # removal action is earlier than the dir creation action | ||
1887 | # in the task queue. FIXME: is this a safe assumption? | ||
1888 | return 1; | ||
1889 | } | ||
1890 | else { # no dir action | ||
1891 | return 0; | ||
1892 | } | ||
1893 | } | ||
1894 | elsif ($laction eq 'create') { | ||
1895 | if ($daction eq 'remove') { | ||
1896 | # Assume that we're folding $path, and that the dir | ||
1897 | # removal action is earlier than the link creation action | ||
1898 | # in the task queue. FIXME: is this a safe assumption? | ||
1899 | return 1; | ||
1900 | } | ||
1901 | elsif ($daction eq 'create') { | ||
1902 | internal_error("creating link and dir: $path"); | ||
1903 | return 1; | ||
1904 | } | ||
1905 | else { # no dir action | ||
1906 | return 1; | ||
1907 | } | ||
1908 | } | ||
1909 | else { | ||
1910 | # No link action | ||
1911 | if ($daction eq 'remove') { | ||
1912 | return 0; | ||
1913 | } | ||
1914 | elsif ($daction eq 'create') { | ||
1915 | return 1; | ||
1916 | } | ||
1917 | else { # no dir action | ||
1918 | # fall through to below | ||
1919 | } | ||
1920 | } | ||
1921 | |||
1922 | return 0 if $self->parent_link_scheduled_for_removal($path); | ||
1923 | |||
1924 | if (-e $path) { | ||
1925 | debug(4, " is_a_node($path): really exists"); | ||
1926 | return 1; | ||
1927 | } | ||
1928 | |||
1929 | debug(4, " is_a_node($path): returning false"); | ||
1930 | return 0; | ||
1931 | } | ||
1932 | |||
1933 | #===== METHOD =============================================================== | ||
1934 | # Name : read_a_link() | ||
1935 | # Purpose : return the source of a current or planned link | ||
1936 | # Parameters: $path => path to the link target | ||
1937 | # Returns : a string | ||
1938 | # Throws : fatal exception if the given path is not a current or planned | ||
1939 | # : link | ||
1940 | # Comments : none | ||
1941 | #============================================================================ | ||
1942 | sub read_a_link { | ||
1943 | my $self = shift; | ||
1944 | my ($path) = @_; | ||
1945 | |||
1946 | if (my $action = $self->link_task_action($path)) { | ||
1947 | debug(4, " read_a_link($path): task exists with action $action"); | ||
1948 | |||
1949 | if ($action eq 'create') { | ||
1950 | return $self->{link_task_for}{$path}->{source}; | ||
1951 | } | ||
1952 | elsif ($action eq 'remove') { | ||
1953 | internal_error( | ||
1954 | "read_a_link() passed a path that is scheduled for removal: $path" | ||
1955 | ); | ||
1956 | } | ||
1957 | } | ||
1958 | elsif (-l $path) { | ||
1959 | debug(4, " read_a_link($path): real link"); | ||
1960 | my $target = readlink $path or error("Could not read link: $path ($!)"); | ||
1961 | return $target; | ||
1962 | } | ||
1963 | internal_error("read_a_link() passed a non link path: $path\n"); | ||
1964 | } | ||
1965 | |||
1966 | #===== METHOD =============================================================== | ||
1967 | # Name : do_link() | ||
1968 | # Purpose : wrap 'link' operation for later processing | ||
1969 | # Parameters: $oldfile => the existing file to link to | ||
1970 | # : $newfile => the file to link | ||
1971 | # Returns : n/a | ||
1972 | # Throws : error if this clashes with an existing planned operation | ||
1973 | # Comments : cleans up operations that undo previous operations | ||
1974 | #============================================================================ | ||
1975 | sub do_link { | ||
1976 | my $self = shift; | ||
1977 | my ($oldfile, $newfile) = @_; | ||
1978 | |||
1979 | if (exists $self->{dir_task_for}{$newfile}) { | ||
1980 | my $task_ref = $self->{dir_task_for}{$newfile}; | ||
1981 | |||
1982 | if ($task_ref->{action} eq 'create') { | ||
1983 | if ($task_ref->{type} eq 'dir') { | ||
1984 | internal_error( | ||
1985 | "new link (%s => %s) clashes with planned new directory", | ||
1986 | $newfile, | ||
1987 | $oldfile, | ||
1988 | ); | ||
1989 | } | ||
1990 | } | ||
1991 | elsif ($task_ref->{action} eq 'remove') { | ||
1992 | # We may need to remove a directory before creating a link so continue. | ||
1993 | } | ||
1994 | else { | ||
1995 | internal_error("bad task action: $task_ref->{action}"); | ||
1996 | } | ||
1997 | } | ||
1998 | |||
1999 | if (exists $self->{link_task_for}{$newfile}) { | ||
2000 | my $task_ref = $self->{link_task_for}{$newfile}; | ||
2001 | |||
2002 | if ($task_ref->{action} eq 'create') { | ||
2003 | if ($task_ref->{source} ne $oldfile) { | ||
2004 | internal_error( | ||
2005 | "new link clashes with planned new link: %s => %s", | ||
2006 | $task_ref->{path}, | ||
2007 | $task_ref->{source}, | ||
2008 | ) | ||
2009 | } | ||
2010 | else { | ||
2011 | debug(1, "LINK: $newfile => $oldfile (duplicates previous action)"); | ||
2012 | return; | ||
2013 | } | ||
2014 | } | ||
2015 | elsif ($task_ref->{action} eq 'remove') { | ||
2016 | if ($task_ref->{source} eq $oldfile) { | ||
2017 | # No need to remove a link we are going to recreate | ||
2018 | debug(1, "LINK: $newfile => $oldfile (reverts previous action)"); | ||
2019 | $self->{link_task_for}{$newfile}->{action} = 'skip'; | ||
2020 | delete $self->{link_task_for}{$newfile}; | ||
2021 | return; | ||
2022 | } | ||
2023 | # We may need to remove a link to replace it so continue | ||
2024 | } | ||
2025 | else { | ||
2026 | internal_error("bad task action: $task_ref->{action}"); | ||
2027 | } | ||
2028 | } | ||
2029 | |||
2030 | # Creating a new link | ||
2031 | debug(1, "LINK: $newfile => $oldfile"); | ||
2032 | my $task = { | ||
2033 | action => 'create', | ||
2034 | type => 'link', | ||
2035 | path => $newfile, | ||
2036 | source => $oldfile, | ||
2037 | }; | ||
2038 | push @{ $self->{tasks} }, $task; | ||
2039 | $self->{link_task_for}{$newfile} = $task; | ||
2040 | |||
2041 | return; | ||
2042 | } | ||
2043 | |||
2044 | #===== METHOD =============================================================== | ||
2045 | # Name : do_unlink() | ||
2046 | # Purpose : wrap 'unlink' operation for later processing | ||
2047 | # Parameters: $file => the file to unlink | ||
2048 | # Returns : n/a | ||
2049 | # Throws : error if this clashes with an existing planned operation | ||
2050 | # Comments : will remove an existing planned link | ||
2051 | #============================================================================ | ||
2052 | sub do_unlink { | ||
2053 | my $self = shift; | ||
2054 | my ($file) = @_; | ||
2055 | |||
2056 | if (exists $self->{link_task_for}{$file}) { | ||
2057 | my $task_ref = $self->{link_task_for}{$file}; | ||
2058 | if ($task_ref->{action} eq 'remove') { | ||
2059 | debug(1, "UNLINK: $file (duplicates previous action)"); | ||
2060 | return; | ||
2061 | } | ||
2062 | elsif ($task_ref->{action} eq 'create') { | ||
2063 | # Do need to create a link then remove it | ||
2064 | debug(1, "UNLINK: $file (reverts previous action)"); | ||
2065 | $self->{link_task_for}{$file}->{action} = 'skip'; | ||
2066 | delete $self->{link_task_for}{$file}; | ||
2067 | return; | ||
2068 | } | ||
2069 | else { | ||
2070 | internal_error("bad task action: $task_ref->{action}"); | ||
2071 | } | ||
2072 | } | ||
2073 | |||
2074 | if (exists $self->{dir_task_for}{$file} and $self->{dir_task_for}{$file} eq 'create') { | ||
2075 | internal_error( | ||
2076 | "new unlink operation clashes with planned operation: %s dir %s", | ||
2077 | $self->{dir_task_for}{$file}->{action}, | ||
2078 | $file | ||
2079 | ); | ||
2080 | } | ||
2081 | |||
2082 | # Remove the link | ||
2083 | debug(1, "UNLINK: $file"); | ||
2084 | |||
2085 | my $source = readlink $file or error("could not readlink $file ($!)"); | ||
2086 | |||
2087 | my $task = { | ||
2088 | action => 'remove', | ||
2089 | type => 'link', | ||
2090 | path => $file, | ||
2091 | source => $source, | ||
2092 | }; | ||
2093 | push @{ $self->{tasks} }, $task; | ||
2094 | $self->{link_task_for}{$file} = $task; | ||
2095 | |||
2096 | return; | ||
2097 | } | ||
2098 | |||
2099 | #===== METHOD =============================================================== | ||
2100 | # Name : do_mkdir() | ||
2101 | # Purpose : wrap 'mkdir' operation | ||
2102 | # Parameters: $dir => the directory to remove | ||
2103 | # Returns : n/a | ||
2104 | # Throws : fatal exception if operation fails | ||
2105 | # Comments : outputs a message if 'verbose' option is set | ||
2106 | # : does not perform operation if 'simulate' option is set | ||
2107 | # Comments : cleans up operations that undo previous operations | ||
2108 | #============================================================================ | ||
2109 | sub do_mkdir { | ||
2110 | my $self = shift; | ||
2111 | my ($dir) = @_; | ||
2112 | |||
2113 | if (exists $self->{link_task_for}{$dir}) { | ||
2114 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2115 | |||
2116 | if ($task_ref->{action} eq 'create') { | ||
2117 | internal_error( | ||
2118 | "new dir clashes with planned new link (%s => %s)", | ||
2119 | $task_ref->{path}, | ||
2120 | $task_ref->{source}, | ||
2121 | ); | ||
2122 | } | ||
2123 | elsif ($task_ref->{action} eq 'remove') { | ||
2124 | # May need to remove a link before creating a directory so continue | ||
2125 | } | ||
2126 | else { | ||
2127 | internal_error("bad task action: $task_ref->{action}"); | ||
2128 | } | ||
2129 | } | ||
2130 | |||
2131 | if (exists $self->{dir_task_for}{$dir}) { | ||
2132 | my $task_ref = $self->{dir_task_for}{$dir}; | ||
2133 | |||
2134 | if ($task_ref->{action} eq 'create') { | ||
2135 | debug(1, "MKDIR: $dir (duplicates previous action)"); | ||
2136 | return; | ||
2137 | } | ||
2138 | elsif ($task_ref->{action} eq 'remove') { | ||
2139 | debug(1, "MKDIR: $dir (reverts previous action)"); | ||
2140 | $self->{dir_task_for}{$dir}->{action} = 'skip'; | ||
2141 | delete $self->{dir_task_for}{$dir}; | ||
2142 | return; | ||
2143 | } | ||
2144 | else { | ||
2145 | internal_error("bad task action: $task_ref->{action}"); | ||
2146 | } | ||
2147 | } | ||
2148 | |||
2149 | debug(1, "MKDIR: $dir"); | ||
2150 | my $task = { | ||
2151 | action => 'create', | ||
2152 | type => 'dir', | ||
2153 | path => $dir, | ||
2154 | source => undef, | ||
2155 | }; | ||
2156 | push @{ $self->{tasks} }, $task; | ||
2157 | $self->{dir_task_for}{$dir} = $task; | ||
2158 | |||
2159 | return; | ||
2160 | } | ||
2161 | |||
2162 | #===== METHOD =============================================================== | ||
2163 | # Name : do_rmdir() | ||
2164 | # Purpose : wrap 'rmdir' operation | ||
2165 | # Parameters: $dir => the directory to remove | ||
2166 | # Returns : n/a | ||
2167 | # Throws : fatal exception if operation fails | ||
2168 | # Comments : outputs a message if 'verbose' option is set | ||
2169 | # : does not perform operation if 'simulate' option is set | ||
2170 | #============================================================================ | ||
2171 | sub do_rmdir { | ||
2172 | my $self = shift; | ||
2173 | my ($dir) = @_; | ||
2174 | |||
2175 | if (exists $self->{link_task_for}{$dir}) { | ||
2176 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2177 | internal_error( | ||
2178 | "rmdir clashes with planned operation: %s link %s => %s", | ||
2179 | $task_ref->{action}, | ||
2180 | $task_ref->{path}, | ||
2181 | $task_ref->{source} | ||
2182 | ); | ||
2183 | } | ||
2184 | |||
2185 | if (exists $self->{dir_task_for}{$dir}) { | ||
2186 | my $task_ref = $self->{link_task_for}{$dir}; | ||
2187 | |||
2188 | if ($task_ref->{action} eq 'remove') { | ||
2189 | debug(1, "RMDIR $dir (duplicates previous action)"); | ||
2190 | return; | ||
2191 | } | ||
2192 | elsif ($task_ref->{action} eq 'create') { | ||
2193 | debug(1, "MKDIR $dir (reverts previous action)"); | ||
2194 | $self->{link_task_for}{$dir}->{action} = 'skip'; | ||
2195 | delete $self->{link_task_for}{$dir}; | ||
2196 | return; | ||
2197 | } | ||
2198 | else { | ||
2199 | internal_error("bad task action: $task_ref->{action}"); | ||
2200 | } | ||
2201 | } | ||
2202 | |||
2203 | debug(1, "RMDIR $dir"); | ||
2204 | my $task = { | ||
2205 | action => 'remove', | ||
2206 | type => 'dir', | ||
2207 | path => $dir, | ||
2208 | source => '', | ||
2209 | }; | ||
2210 | push @{ $self->{tasks} }, $task; | ||
2211 | $self->{dir_task_for}{$dir} = $task; | ||
2212 | |||
2213 | return; | ||
2214 | } | ||
2215 | |||
2216 | #===== METHOD =============================================================== | ||
2217 | # Name : do_mv() | ||
2218 | # Purpose : wrap 'move' operation for later processing | ||
2219 | # Parameters: $src => the file to move | ||
2220 | # : $dst => the path to move it to | ||
2221 | # Returns : n/a | ||
2222 | # Throws : error if this clashes with an existing planned operation | ||
2223 | # Comments : alters contents of package installation image in stow dir | ||
2224 | #============================================================================ | ||
2225 | sub do_mv { | ||
2226 | my $self = shift; | ||
2227 | my ($src, $dst) = @_; | ||
2228 | |||
2229 | if (exists $self->{link_task_for}{$src}) { | ||
2230 | # I don't *think* this should ever happen, but I'm not | ||
2231 | # 100% sure. | ||
2232 | my $task_ref = $self->{link_task_for}{$src}; | ||
2233 | internal_error( | ||
2234 | "do_mv: pre-existing link task for $src; action: %s, source: %s", | ||
2235 | $task_ref->{action}, $task_ref->{source} | ||
2236 | ); | ||
2237 | } | ||
2238 | elsif (exists $self->{dir_task_for}{$src}) { | ||
2239 | my $task_ref = $self->{dir_task_for}{$src}; | ||
2240 | internal_error( | ||
2241 | "do_mv: pre-existing dir task for %s?! action: %s", | ||
2242 | $src, $task_ref->{action} | ||
2243 | ); | ||
2244 | } | ||
2245 | |||
2246 | # Remove the link | ||
2247 | debug(1, "MV: $src -> $dst"); | ||
2248 | |||
2249 | my $task = { | ||
2250 | action => 'move', | ||
2251 | type => 'file', | ||
2252 | path => $src, | ||
2253 | dest => $dst, | ||
2254 | }; | ||
2255 | push @{ $self->{tasks} }, $task; | ||
2256 | |||
2257 | # FIXME: do we need this for anything? | ||
2258 | #$self->{mv_task_for}{$file} = $task; | ||
2259 | |||
2260 | return; | ||
2261 | } | ||
2262 | |||
2263 | |||
2264 | ############################################################################# | ||
2265 | # | ||
2266 | # End of methods; subroutines follow. | ||
2267 | # FIXME: Ideally these should be in a separate module. | ||
2268 | |||
2269 | |||
2270 | #===== PRIVATE SUBROUTINE =================================================== | ||
2271 | # Name : internal_error() | ||
2272 | # Purpose : output internal error message in a consistent form and die | ||
2273 | # Parameters: $message => error message to output | ||
2274 | # Returns : n/a | ||
2275 | # Throws : n/a | ||
2276 | # Comments : none | ||
2277 | #============================================================================ | ||
2278 | sub internal_error { | ||
2279 | my ($format, @args) = @_; | ||
2280 | my $error = sprintf($format, @args); | ||
2281 | my $stacktrace = Carp::longmess(); | ||
2282 | die <<EOF; | ||
2283 | |||
2284 | $ProgramName: INTERNAL ERROR: $error$stacktrace | ||
2285 | |||
2286 | This _is_ a bug. Please submit a bug report so we can fix it! :-) | ||
2287 | See http://www.gnu.org/software/stow/ for how to do this. | ||
2288 | EOF | ||
2289 | } | ||
2290 | |||
2291 | =head1 BUGS | ||
2292 | |||
2293 | =head1 SEE ALSO | ||
2294 | |||
2295 | =cut | ||
2296 | |||
2297 | 1; | ||
2298 | |||
2299 | } | ||
2 | 2300 | ||
3 | use FindBin; | 2301 | use FindBin; |
4 | use lib "$FindBin::Bin"; | 2302 | use lib "$FindBin::Bin"; |
@@ -418,14 +2716,19 @@ use POSIX qw(getcwd); | |||
418 | use Getopt::Long; | 2716 | use Getopt::Long; |
419 | 2717 | ||
420 | 2718 | ||
421 | use Stow; | ||
422 | use Stow qw(parent error); | ||
423 | |||
424 | my $ProgramName = $0; | 2719 | my $ProgramName = $0; |
425 | $ProgramName =~ s{.*/}{}; | 2720 | $ProgramName =~ s{.*/}{}; |
426 | 2721 | ||
427 | main() unless caller(); | 2722 | main() unless caller(); |
428 | 2723 | ||
2724 | sub parent { | ||
2725 | my @path = @_; | ||
2726 | my $path = join '/', @_; | ||
2727 | my @elts = split m{/+}, $path; | ||
2728 | pop @elts; | ||
2729 | return join '/', @elts; | ||
2730 | } | ||
2731 | |||
429 | sub main { | 2732 | sub main { |
430 | my ($options, $pkgs_to_unstow, $pkgs_to_stow) = process_options(); | 2733 | my ($options, $pkgs_to_unstow, $pkgs_to_stow) = process_options(); |
431 | 2734 | ||
@@ -568,7 +2871,7 @@ sub check_packages { | |||
568 | for my $package (@$pkgs_to_stow, @$pkgs_to_unstow) { | 2871 | for my $package (@$pkgs_to_stow, @$pkgs_to_unstow) { |
569 | $package =~ s{/+$}{}; # delete trailing slashes | 2872 | $package =~ s{/+$}{}; # delete trailing slashes |
570 | if ($package =~ m{/}) { | 2873 | if ($package =~ m{/}) { |
571 | error("Slashes are not permitted in package names"); | 2874 | Stow->error("Slashes are not permitted in package names"); |
572 | } | 2875 | } |
573 | } | 2876 | } |
574 | } | 2877 | } |