Another simplification
[MPC.git] / clone_build_tree.pl
blob8a64a44cf215d1d54540b6b03ca85aa4be459c63
1 #!/usr/bin/env perl
2 eval '(exit $?0)' && eval 'exec perl -w -S $0 ${1+"$@"}'
3 & eval 'exec perl -w -S $0 $argv:q'
4 if 0;
6 # ******************************************************************
7 # Author: Chad Elliott
8 # Date: 4/8/2004
9 # Description: Clone a build tree into an alternate location.
10 # This script is a rewrite of create_ace_build.pl and
11 # does not restrict the user to place the build
12 # in any particular location or that it be used with
13 # ACE_wrappers. Some of the functions were borrowed
14 # from create_ace_build.pl, but were modified quite a
15 # bit.
16 # ******************************************************************
18 # ******************************************************************
19 # Pragma Section
20 # ******************************************************************
22 use strict;
23 use Cwd;
24 use FileHandle;
25 use File::Copy;
26 use File::Find;
27 use File::Path;
28 use File::stat;
29 use File::Basename;
31 # ******************************************************************
32 # Data Section
33 # ******************************************************************
35 my $exclude;
36 my @foundFiles;
37 my $verbose = 0;
38 my $lbuildf = 0;
39 my $lnonbuildf = 0;
40 my $version = '1.16';
42 eval 'symlink("", "");';
43 my $hasSymlink = ($@ eq '');
45 # ******************************************************************
46 # Subroutine Section
47 # ******************************************************************
49 sub findCallback {
50 my $matches = !(/^CVS\z/s && ($File::Find::prune = 1) ||
51 /^\.svn\z/s && ($File::Find::prune = 1) ||
52 /^\.git\z/s && ($File::Find::prune = 1) ||
53 defined $exclude &&
54 /^$exclude\z/s && ($File::Find::prune = 1) ||
55 /^\.cvsignore\z/s && ($File::Find::prune = 1) ||
56 /^\..*obj\z/s && ($File::Find::prune = 1) ||
57 /^Templates\.DB\z/s && ($File::Find::prune = 1) ||
58 /^Debug\z/s && ($File::Find::prune = 1) ||
59 /^Release\z/s && ($File::Find::prune = 1) ||
60 /^Static_Debug\z/s && ($File::Find::prune = 1) ||
61 /^Static_Release\z/s && ($File::Find::prune = 1)
64 if ($matches) {
65 if(!$lnonbuildf) {
66 $matches &&= (! -l $_ &&
67 ! ( -f $_ && /^core\z/s) &&
68 ! /^.*\.rej\z/s &&
69 ! /^.*\.state\z/s &&
70 ! /^.*\.so\z/s &&
71 ! /^.*\.[oa]\z/s &&
72 ! /^.*\.dll\z/s &&
73 ! /^.*\.lib\z/s &&
74 ! /^.*\.obj\z/s &&
75 ! /^.*~\z/s &&
76 ! /^\.\z/s &&
77 ! /^\.#.*\z/s &&
78 ! /^.*\.ncb\z/s &&
79 ! /^.*\.opt\z/s &&
80 ! /^.*\.bak\z/s &&
81 ! /^.*\.suo\z/s &&
82 ! /^.*\.ilk\z/s &&
83 ! /^.*\.pdb\z/s &&
84 ! /^.*\.pch\z/s &&
85 ! /^.*\.log\z/s &&
86 ! ( -f $_ && /^.*\.d\z/s )
90 if ($matches) {
91 if (!$lbuildf) {
92 $matches = (! /^.*\.dsp\z/s &&
93 ! /^.*\.dsw\z/s &&
94 ! /^.*\.vcproj\z/s &&
95 ! /^.*\.sln\z/s &&
96 ! /^Makefile.*\z/s &&
97 ! /^GNUmakefile.*\z/s &&
98 ! /^.*\.am\z/s &&
99 ! /^\.depend\..*\z/s &&
100 ! /^.*\.vcn\z/s &&
101 ! /^.*\.vcp\z/s &&
102 ! /^.*\.vcw\z/s &&
103 ! /^.*\.vpj\z/s &&
104 ! /^.*\.vpw\z/s &&
105 ! /^.*\.cbx\z/s &&
106 ! /^.*\.bpgr\z/s &&
107 ! /^.*\.bmak\z/s &&
108 ! /^.*\.bmake\z/s &&
109 ! /^.*\.mak\z/s &&
110 ! /^.*\.nmake\z/s &&
111 ! /^.*\.bld\z/s &&
112 ! /^.*\.icc\z/s &&
113 ! /^.*\.icp\z/s &&
114 ! /^.*\.classpath\z/s &&
115 ! /^.*\.project\z/s &&
116 ! /^.*\.wrproject\z/s &&
117 ! /^.*\.wrmakefile\z/s &&
118 ! /^.*\.vxtest\z/s
122 if ($matches) {
123 ## Remove the beginning dot slash as we save the file
124 push(@foundFiles, $File::Find::name);
125 $foundFiles[$#foundFiles] =~ s/^\.[\\\/]+//;
132 sub getFileList {
133 File::Find::find({wanted => \&findCallback}, '.');
134 return \@foundFiles;
138 sub backupAndMoveModified {
139 my($realpath, $linkpath) = @_;
140 my $mltime = -M $linkpath;
141 my $mrtime = -M $realpath;
142 my $status = 1;
144 ## -M returns the number of days since modification. Therefore,
145 ## a smaller time means that it has been modified more recently.
146 ## This is different than what stat() returns.
148 ## If the hard linked file is newer than the original file, that means
149 ## the link has been broken by something and needs to be "fixed". We
150 ## will back up the original file and move the modified file into it's
151 ## place.
152 if ($mltime < $mrtime) {
153 $status = 0;
155 ## Move the real file to a backup
156 unlink("$realpath.bak");
157 if (rename($realpath, "$realpath.bak")) {
158 ## Move the linked file to the real file name
159 if (move($linkpath, $realpath)) {
160 $status = 1;
162 else {
163 ## The move failed, so we will attempt to put
164 ## the original file back.
165 unlink($realpath);
166 rename("$realpath.bak", $realpath);
170 elsif ($mltime != $mrtime || -s $linkpath != -s $realpath) {
171 ## The two files are different in some way, we need to make a backup
172 ## so that we don't cause a loss of data/work.
173 $status = 0;
176 if (!$status) {
177 ## We were not able to properly deal with this file. We will
178 ## attempt to preserve the modified file.
179 unlink("$linkpath.bak");
180 rename($linkpath, "$linkpath.bak");
185 sub hardlink {
186 my($realpath, $linkpath) = @_;
188 if ($^O eq 'MSWin32' && ! -e $realpath) {
189 ## If the real file "doesn't exist", then we need to
190 ## look up the short file name.
191 my $short = Win32::GetShortPathName($realpath);
193 ## If we were able to find the short file name, then we need to
194 ## try again.
195 if (defined $short) {
196 $realpath = $short;
198 else {
199 ## This should never happen, but there appears to be a bug
200 ## with the underlying Win32 APIs on Windows Server 2003.
201 ## Long paths will cause an error which perl will ignore.
202 ## Unicode versions of the APIs seem to work fine.
203 ## To experiment try Win32 _fullpath() and CreateHardLink with
204 ## long paths.
205 print "WARNING: Skipping $realpath.\n";
206 return 1;
210 return link($realpath, $linkpath);
214 sub symlinkFiles {
215 my($files, $fullbuild, $dmode, $startdir, $absolute) = @_;
216 my $sdlength = length($startdir) + 1;
217 my $partial = ($absolute ? undef :
218 substr($fullbuild, $sdlength,
219 length($fullbuild) - $sdlength));
221 foreach my $file (@$files) {
222 my $fullpath = "$fullbuild/$file";
223 if (-e $fullpath) {
224 ## We need to make sure that we're not attempting to mix hardlinks
225 ## and softlinks.
226 if (! -d $fullpath && ! -l $fullpath) {
227 my $stat = stat($fullpath);
228 if ($stat->nlink() > 1) {
229 print STDERR "ERROR: Attempting to mix softlinks ",
230 "with a hardlink build.\n",
231 "$fullpath has ", $stat->nlink(), " links.\n";
232 return 1;
236 else {
237 if (-d $file) {
238 if ($verbose) {
239 print "Creating $fullpath\n";
241 if (!mkpath($fullpath, 0, $dmode)) {
242 print STDERR "ERROR: Unable to create $fullpath\n";
243 return 1;
246 else {
247 if ($absolute) {
248 if ($verbose) {
249 print "symlink $startdir/$file $fullpath\n";
251 if (!symlink("$startdir/$file", $fullpath)) {
252 print STDERR "ERROR: Unable to symlink $fullpath\n";
253 return 1;
256 else {
257 my $buildfile = "$partial/$file";
258 my $slashcount = ($buildfile =~ tr/\///);
259 my $real = ($slashcount == 0 ? './' : ('../' x $slashcount)) .
260 $file;
262 print "symlink $real $fullpath\n" if ($verbose);
263 if (!symlink($real, $fullpath)) {
264 print STDERR "ERROR: Unable to symlink $fullpath\n";
265 return 1;
272 ## Remove links that point to non-existent files. The subroutine is
273 ## now anonymous to avoid the "will not stay shared" warning for %dirs.
274 my %dirs;
275 File::Find::find({wanted => sub {
276 if (-l $_ && ! -e $_) {
277 unlink($_);
278 $dirs{$File::Find::dir} = 1;
279 if ($verbose) {
280 print "Removing $File::Find::dir/$_\n";
284 }, $fullbuild);
285 foreach my $key (keys %dirs) {
286 rmdir($key);
288 return 0;
292 sub hardlinkFiles {
293 my($files, $fullbuild, $dmode, $startdir) = @_;
294 my @hardlinks;
296 foreach my $file (@$files) {
297 my $fullpath = "$fullbuild/$file";
298 if (-d $file) {
299 if (! -e $fullpath) {
300 if ($verbose) {
301 print "Creating $fullpath\n";
303 if (!mkpath($fullpath, 0, $dmode)) {
304 print STDERR "ERROR: Unable to create $fullpath\n";
305 return 1;
309 else {
310 if (-e $fullpath) {
311 ## We need to make sure that we're not attempting to mix hardlinks
312 ## and softlinks.
313 if (-l $fullpath) {
314 print STDERR "ERROR: Attempting to mix hardlinks ",
315 "with a softlink build.\n",
316 "$fullpath is a softlink.\n";
317 return 1;
319 backupAndMoveModified($file, $fullpath);
321 if (! -e $fullpath) {
322 if ($verbose) {
323 print "hardlink $file $fullpath\n";
325 if (!hardlink($file, $fullpath)) {
326 print STDERR "ERROR: Unable to link $fullpath\n";
327 return 1;
331 ## If we successfully linked the file or it already exists,
332 ## we need to keep track of it.
333 push(@hardlinks, $file);
337 ## Remove links that point to non-existent files
338 my $lfh = new FileHandle();
339 my $txt = "$fullbuild/clone_build_tree.links";
340 if (open($lfh, $txt)) {
341 my %dirs;
342 while(<$lfh>) {
343 my $line = $_;
344 $line =~ s/\s+$//;
345 if (! -e $line) {
346 my $full = "$fullbuild/$line";
347 unlink($full);
348 $dirs{dirname($full)} = 1;
349 print "Removing $full\n" if ($verbose);
352 close($lfh);
353 foreach my $key (keys %dirs) {
354 rmdir($key);
358 ## Rewrite the link file.
359 unlink($txt);
360 if (open($lfh, ">$txt")) {
361 foreach my $file (@hardlinks) {
362 print $lfh "$file\n";
364 close($lfh);
367 return 0;
371 sub linkFiles {
372 my($absolute, $dmode, $hardlink, $builddir, $builds) = @_;
373 my $status = 0;
374 my $starttime = time();
375 my $startdir = getcwd();
377 ## Ensure that the build directory exists and is writable
378 mkpath($builddir, 0, $dmode);
379 if (! -d $builddir || ! -w $builddir) {
380 print STDERR "ERROR: Unable to create or write to $builddir\n";
381 return 1;
384 ## Search for the clonable files
385 print "Searching $startdir for files...\n";
386 my $files = getFileList();
387 my $findtime = time() - $starttime;
388 print 'Found ', scalar(@$files), ' files and directories in ',
389 $findtime, ' second', ($findtime == 1 ? '' : 's'), ".\n";
391 foreach my $build (@$builds) {
392 my $fullbuild = "$builddir/$build";
394 ## Create all of the links for this build
395 if (-d $fullbuild) {
396 print "Updating $fullbuild\n";
398 else {
399 print "Creating $fullbuild\n";
400 mkpath($fullbuild, 0, $dmode);
403 if ($hardlink) {
404 $status += hardlinkFiles($files, $fullbuild, $dmode, $startdir);
406 else {
407 $status += symlinkFiles($files, $fullbuild,
408 $dmode, $startdir, $absolute);
410 print "Finished in $fullbuild\n";
413 print 'Total time: ', time() - $starttime, " seconds.\n" if ($status == 0);
415 return $status;
419 sub usageAndExit {
420 my $msg = shift;
422 print STDERR "$msg\n" if (defined $msg);
424 my $base = basename($0);
425 my $spc = ' ' x (length($base) + 8);
427 print STDERR "$base v$version\n\n",
428 "Create a tree identical in layout to the current directory\n",
429 "with the use of ", ($hasSymlink ? "symbolic links or " : ''),
430 "hard links.\n\n",
431 "Usage: $base [-b <builddir>] [-d <dmode>] [-f] [-n]",
432 ($hasSymlink ? "[-a] [-l] " : ''),
433 "[-v]\n",
434 $spc, "[build names...]\n\n",
435 ($hasSymlink ?
436 "-a Use absolute paths when creating soft links.\n" .
437 "-l Use hard links instead of soft links.\n" : ''),
438 "-b Set the build directory. It defaults to the ",
439 "<current directory>/build.\n",
440 "-d Set the directory permissions mode.\n",
441 "-f Link build files (Makefile, .dsw, .sln, .etc).\n",
442 "-n Link non-build files normally avoided (.o,.so, etc.).\n",
443 "-s Set the start directory. It defaults to the ",
444 "<current directory>.\n",
445 "-v Enable verbose mode.\n";
447 exit(0);
451 # ******************************************************************
452 # Main Section
453 # ******************************************************************
455 my $dmode = 0777;
456 my $absolute = 0;
457 my $hardlink = !$hasSymlink;
458 my $builddir;
459 my @builds;
460 my $startdir;
462 for(my $i = 0; $i <= $#ARGV; ++$i) {
463 if ($ARGV[$i] eq '-a') {
464 $absolute = 1;
466 elsif ($ARGV[$i] eq '-b') {
467 ++$i;
468 if (defined $ARGV[$i]) {
469 $builddir = $ARGV[$i];
471 ## Convert backslashes to slashes
472 $builddir =~ s/\\/\//g;
474 ## Remove trailing slashes
475 $builddir =~ s/\/+$//;
477 ## Remove duplicate slashes
478 while($builddir =~ s/\/\//\//g) {
481 else {
482 usageAndExit('-b requires an argument');
485 elsif ($ARGV[$i] eq '-d') {
486 ++$i;
487 if (defined $ARGV[$i]) {
488 $dmode = $ARGV[$i];
490 else {
491 usageAndExit('-d requires an argument');
494 elsif ($ARGV[$i] eq '-f') {
495 $lbuildf = 1;
497 elsif ($ARGV[$i] eq '-l') {
498 $hardlink = 1;
500 elsif ($ARGV[$i] eq '-n') {
501 $lnonbuildf = 1;
503 elsif ($ARGV[$i] eq '-v') {
504 $verbose = 1;
506 elsif ($ARGV[$i] eq '-s') {
507 ++$i;
508 if (defined $ARGV[$i]) {
509 $startdir = $ARGV[$i];
511 else {
512 usageAndExit('-s requires an argument');
515 elsif ($ARGV[$i] =~ /^-/) {
516 usageAndExit('Unknown option: ' . $ARGV[$i]);
518 else {
519 push(@builds, $ARGV[$i]);
523 if (defined $startdir && !chdir($startdir)) {
524 print "ERROR: Unable to change directory to $startdir\n";
525 exit(1);
528 $builddir = getcwd() . '/build' if (!defined $builddir);
530 if (index($builddir, getcwd()) == 0) {
531 $exclude = substr($builddir, length(getcwd()) + 1);
532 $exclude =~ s/([\+\-\\\$\[\]\(\)\.])/\\$1/g;
533 $exclude =~ s/.*?([^\/]+)$/$1/;
535 else {
536 $absolute = 1;
539 if (!defined $builds[0]) {
540 my $cwd = getcwd();
541 if (chdir($builddir)) {
542 @builds = glob('*');
543 chdir($cwd);
545 else {
546 usageAndExit('There are no builds to update.');
550 exit(linkFiles($absolute, $dmode, $hardlink, $builddir, \@builds));