Merge pull request #40 from jwillemsen/master
[MPC.git] / clone_build_tree.pl
blob19f40cb37e454cf281ad83cb75134bb82736c789
1 #! /usr/bin/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 defined $exclude &&
53 /^$exclude\z/s && ($File::Find::prune = 1) ||
54 /^\.cvsignore\z/s && ($File::Find::prune = 1) ||
55 /^\..*obj\z/s && ($File::Find::prune = 1) ||
56 /^Templates\.DB\z/s && ($File::Find::prune = 1) ||
57 /^Debug\z/s && ($File::Find::prune = 1) ||
58 /^Release\z/s && ($File::Find::prune = 1) ||
59 /^Static_Debug\z/s && ($File::Find::prune = 1) ||
60 /^Static_Release\z/s && ($File::Find::prune = 1)
63 if ($matches) {
64 if(!$lnonbuildf) {
65 $matches &&= (! -l $_ &&
66 ! ( -f $_ && /^core\z/s) &&
67 ! /^.*\.rej\z/s &&
68 ! /^.*\.state\z/s &&
69 ! /^.*\.so\z/s &&
70 ! /^.*\.[oa]\z/s &&
71 ! /^.*\.dll\z/s &&
72 ! /^.*\.lib\z/s &&
73 ! /^.*\.obj\z/s &&
74 ! /^.*~\z/s &&
75 ! /^\.\z/s &&
76 ! /^\.#.*\z/s &&
77 ! /^.*\.ncb\z/s &&
78 ! /^.*\.opt\z/s &&
79 ! /^.*\.bak\z/s &&
80 ! /^.*\.suo\z/s &&
81 ! /^.*\.ilk\z/s &&
82 ! /^.*\.pdb\z/s &&
83 ! /^.*\.pch\z/s &&
84 ! /^.*\.log\z/s &&
85 ! ( -f $_ && /^.*\.d\z/s )
89 if ($matches) {
90 if (!$lbuildf) {
91 $matches = (! /^.*\.dsp\z/s &&
92 ! /^.*\.dsw\z/s &&
93 ! /^.*\.vcproj\z/s &&
94 ! /^.*\.sln\z/s &&
95 ! /^Makefile.*\z/s &&
96 ! /^GNUmakefile.*\z/s &&
97 ! /^.*\.am\z/s &&
98 ! /^\.depend\..*\z/s &&
99 ! /^.*\.vcn\z/s &&
100 ! /^.*\.vcp\z/s &&
101 ! /^.*\.vcw\z/s &&
102 ! /^.*\.vpj\z/s &&
103 ! /^.*\.vpw\z/s &&
104 ! /^.*\.cbx\z/s &&
105 ! /^.*\.bpgr\z/s &&
106 ! /^.*\.bmak\z/s &&
107 ! /^.*\.bmake\z/s &&
108 ! /^.*\.mak\z/s &&
109 ! /^.*\.nmake\z/s &&
110 ! /^.*\.bld\z/s &&
111 ! /^.*\.icc\z/s &&
112 ! /^.*\.icp\z/s &&
113 ! /^.*\.classpath\z/s &&
114 ! /^.*\.project\z/s &&
115 ! /^.*\.wrproject\z/s &&
116 ! /^.*\.wrmakefile\z/s &&
117 ! /^.*\.vxtest\z/s
121 if ($matches) {
122 ## Remove the beginning dot slash as we save the file
123 push(@foundFiles, $File::Find::name);
124 $foundFiles[$#foundFiles] =~ s/^\.[\\\/]+//;
131 sub getFileList {
132 File::Find::find({wanted => \&findCallback}, '.');
133 return \@foundFiles;
137 sub backupAndMoveModified {
138 my($realpath, $linkpath) = @_;
139 my $mltime = -M $linkpath;
140 my $mrtime = -M $realpath;
141 my $status = 1;
143 ## -M returns the number of days since modification. Therefore,
144 ## a smaller time means that it has been modified more recently.
145 ## This is different than what stat() returns.
147 ## If the hard linked file is newer than the original file, that means
148 ## the link has been broken by something and needs to be "fixed". We
149 ## will back up the original file and move the modified file into it's
150 ## place.
151 if ($mltime < $mrtime) {
152 $status = 0;
154 ## Move the real file to a backup
155 unlink("$realpath.bak");
156 if (rename($realpath, "$realpath.bak")) {
157 ## Move the linked file to the real file name
158 if (move($linkpath, $realpath)) {
159 $status = 1;
161 else {
162 ## The move failed, so we will attempt to put
163 ## the original file back.
164 unlink($realpath);
165 rename("$realpath.bak", $realpath);
169 elsif ($mltime != $mrtime || -s $linkpath != -s $realpath) {
170 ## The two files are different in some way, we need to make a backup
171 ## so that we don't cause a loss of data/work.
172 $status = 0;
175 if (!$status) {
176 ## We were not able to properly deal with this file. We will
177 ## attempt to preserve the modified file.
178 unlink("$linkpath.bak");
179 rename($linkpath, "$linkpath.bak");
184 sub hardlink {
185 my($realpath, $linkpath) = @_;
187 if ($^O eq 'MSWin32' && ! -e $realpath) {
188 ## If the real file "doesn't exist", then we need to
189 ## look up the short file name.
190 my $short = Win32::GetShortPathName($realpath);
192 ## If we were able to find the short file name, then we need to
193 ## try again.
194 if (defined $short) {
195 $realpath = $short;
197 else {
198 ## This should never happen, but there appears to be a bug
199 ## with the underlying Win32 APIs on Windows Server 2003.
200 ## Long paths will cause an error which perl will ignore.
201 ## Unicode versions of the APIs seem to work fine.
202 ## To experiment try Win32 _fullpath() and CreateHardLink with
203 ## long paths.
204 print "WARNING: Skipping $realpath.\n";
205 return 1;
209 return link($realpath, $linkpath);
213 sub symlinkFiles {
214 my($files, $fullbuild, $dmode, $startdir, $absolute) = @_;
215 my $sdlength = length($startdir) + 1;
216 my $partial = ($absolute ? undef :
217 substr($fullbuild, $sdlength,
218 length($fullbuild) - $sdlength));
220 foreach my $file (@$files) {
221 my $fullpath = "$fullbuild/$file";
222 if (-e $fullpath) {
223 ## We need to make sure that we're not attempting to mix hardlinks
224 ## and softlinks.
225 if (! -d $fullpath && ! -l $fullpath) {
226 my $stat = stat($fullpath);
227 if ($stat->nlink() > 1) {
228 print STDERR "ERROR: Attempting to mix softlinks ",
229 "with a hardlink build.\n",
230 "$fullpath has ", $stat->nlink(), " links.\n";
231 return 1;
235 else {
236 if (-d $file) {
237 if ($verbose) {
238 print "Creating $fullpath\n";
240 if (!mkpath($fullpath, 0, $dmode)) {
241 print STDERR "ERROR: Unable to create $fullpath\n";
242 return 1;
245 else {
246 if ($absolute) {
247 if ($verbose) {
248 print "symlink $startdir/$file $fullpath\n";
250 if (!symlink("$startdir/$file", $fullpath)) {
251 print STDERR "ERROR: Unable to symlink $fullpath\n";
252 return 1;
255 else {
256 my $buildfile = "$partial/$file";
257 my $slashcount = ($buildfile =~ tr/\///);
258 my $real = ($slashcount == 0 ? './' : ('../' x $slashcount)) .
259 $file;
261 print "symlink $real $fullpath\n" if ($verbose);
262 if (!symlink($real, $fullpath)) {
263 print STDERR "ERROR: Unable to symlink $fullpath\n";
264 return 1;
271 ## Remove links that point to non-existant files. The subroutine is
272 ## now anonymous to avoid the "will not stay shared" warning for %dirs.
273 my %dirs;
274 File::Find::find({wanted => sub {
275 if (-l $_ && ! -e $_) {
276 unlink($_);
277 $dirs{$File::Find::dir} = 1;
278 if ($verbose) {
279 print "Removing $File::Find::dir/$_\n";
283 }, $fullbuild);
284 foreach my $key (keys %dirs) {
285 rmdir($key);
287 return 0;
291 sub hardlinkFiles {
292 my($files, $fullbuild, $dmode, $startdir) = @_;
293 my @hardlinks;
295 foreach my $file (@$files) {
296 my $fullpath = "$fullbuild/$file";
297 if (-d $file) {
298 if (! -e $fullpath) {
299 if ($verbose) {
300 print "Creating $fullpath\n";
302 if (!mkpath($fullpath, 0, $dmode)) {
303 print STDERR "ERROR: Unable to create $fullpath\n";
304 return 1;
308 else {
309 if (-e $fullpath) {
310 ## We need to make sure that we're not attempting to mix hardlinks
311 ## and softlinks.
312 if (-l $fullpath) {
313 print STDERR "ERROR: Attempting to mix hardlinks ",
314 "with a softlink build.\n",
315 "$fullpath is a softlink.\n";
316 return 1;
318 backupAndMoveModified($file, $fullpath);
320 if (! -e $fullpath) {
321 if ($verbose) {
322 print "hardlink $file $fullpath\n";
324 if (!hardlink($file, $fullpath)) {
325 print STDERR "ERROR: Unable to link $fullpath\n";
326 return 1;
330 ## If we successfully linked the file or it already exists,
331 ## we need to keep track of it.
332 push(@hardlinks, $file);
336 ## Remove links that point to non-existant files
337 my $lfh = new FileHandle();
338 my $txt = "$fullbuild/clone_build_tree.links";
339 if (open($lfh, $txt)) {
340 my %dirs;
341 while(<$lfh>) {
342 my $line = $_;
343 $line =~ s/\s+$//;
344 if (! -e $line) {
345 my $full = "$fullbuild/$line";
346 unlink($full);
347 $dirs{dirname($full)} = 1;
348 print "Removing $full\n" if ($verbose);
351 close($lfh);
352 foreach my $key (keys %dirs) {
353 rmdir($key);
357 ## Rewrite the link file.
358 unlink($txt);
359 if (open($lfh, ">$txt")) {
360 foreach my $file (@hardlinks) {
361 print $lfh "$file\n";
363 close($lfh);
366 return 0;
370 sub linkFiles {
371 my($absolute, $dmode, $hardlink, $builddir, $builds) = @_;
372 my $status = 0;
373 my $starttime = time();
374 my $startdir = getcwd();
376 ## Ensure that the build directory exists and is writable
377 mkpath($builddir, 0, $dmode);
378 if (! -d $builddir || ! -w $builddir) {
379 print STDERR "ERROR: Unable to create or write to $builddir\n";
380 return 1;
383 ## Search for the clonable files
384 print "Searching $startdir for files...\n";
385 my $files = getFileList();
386 my $findtime = time() - $starttime;
387 print 'Found ', scalar(@$files), ' files and directories in ',
388 $findtime, ' second', ($findtime == 1 ? '' : 's'), ".\n";
390 foreach my $build (@$builds) {
391 my $fullbuild = "$builddir/$build";
393 ## Create all of the links for this build
394 if (-d $fullbuild) {
395 print "Updating $fullbuild\n";
397 else {
398 print "Creating $fullbuild\n";
399 mkpath($fullbuild, 0, $dmode);
402 if ($hardlink) {
403 $status += hardlinkFiles($files, $fullbuild, $dmode, $startdir);
405 else {
406 $status += symlinkFiles($files, $fullbuild,
407 $dmode, $startdir, $absolute);
409 print "Finished in $fullbuild\n";
412 print 'Total time: ', time() - $starttime, " seconds.\n" if ($status == 0);
414 return $status;
418 sub usageAndExit {
419 my $msg = shift;
421 print STDERR "$msg\n" if (defined $msg);
423 my $base = basename($0);
424 my $spc = ' ' x (length($base) + 8);
426 print STDERR "$base v$version\n\n",
427 "Create a tree identical in layout to the current directory\n",
428 "with the use of ", ($hasSymlink ? "symbolic links or " : ''),
429 "hard links.\n\n",
430 "Usage: $base [-b <builddir>] [-d <dmode>] [-f] [-n]",
431 ($hasSymlink ? "[-a] [-l] " : ''),
432 "[-v]\n",
433 $spc, "[build names...]\n\n",
434 ($hasSymlink ?
435 "-a Use absolute paths when creating soft links.\n" .
436 "-l Use hard links instead of soft links.\n" : ''),
437 "-b Set the build directory. It defaults to the ",
438 "<current directory>/build.\n",
439 "-d Set the directory permissions mode.\n",
440 "-f Link build files (Makefile, .dsw, .sln, .etc).\n",
441 "-n Link non-build files normally avoided (.o,.so, etc.).\n",
442 "-s Set the start directory. It defaults to the ",
443 "<current directory>.\n",
444 "-v Enable verbose mode.\n";
446 exit(0);
450 # ******************************************************************
451 # Main Section
452 # ******************************************************************
454 my $dmode = 0777;
455 my $absolute = 0;
456 my $hardlink = !$hasSymlink;
457 my $builddir;
458 my @builds;
459 my $startdir;
461 for(my $i = 0; $i <= $#ARGV; ++$i) {
462 if ($ARGV[$i] eq '-a') {
463 $absolute = 1;
465 elsif ($ARGV[$i] eq '-b') {
466 ++$i;
467 if (defined $ARGV[$i]) {
468 $builddir = $ARGV[$i];
470 ## Convert backslashes to slashes
471 $builddir =~ s/\\/\//g;
473 ## Remove trailing slashes
474 $builddir =~ s/\/+$//;
476 ## Remove duplicate slashes
477 while($builddir =~ s/\/\//\//g) {
480 else {
481 usageAndExit('-b requires an argument');
484 elsif ($ARGV[$i] eq '-d') {
485 ++$i;
486 if (defined $ARGV[$i]) {
487 $dmode = $ARGV[$i];
489 else {
490 usageAndExit('-d requires an argument');
493 elsif ($ARGV[$i] eq '-f') {
494 $lbuildf = 1;
496 elsif ($ARGV[$i] eq '-l') {
497 $hardlink = 1;
499 elsif ($ARGV[$i] eq '-n') {
500 $lnonbuildf = 1;
502 elsif ($ARGV[$i] eq '-v') {
503 $verbose = 1;
505 elsif ($ARGV[$i] eq '-s') {
506 ++$i;
507 if (defined $ARGV[$i]) {
508 $startdir = $ARGV[$i];
510 else {
511 usageAndExit('-s requires an argument');
514 elsif ($ARGV[$i] =~ /^-/) {
515 usageAndExit('Unknown option: ' . $ARGV[$i]);
517 else {
518 push(@builds, $ARGV[$i]);
522 if (defined $startdir && !chdir($startdir)) {
523 print "ERROR: Unable to change directory to $startdir\n";
524 exit(1);
527 $builddir = getcwd() . '/build' if (!defined $builddir);
529 if (index($builddir, getcwd()) == 0) {
530 $exclude = substr($builddir, length(getcwd()) + 1);
531 $exclude =~ s/([\+\-\\\$\[\]\(\)\.])/\\$1/g;
532 $exclude =~ s/.*?([^\/]+)$/$1/;
534 else {
535 $absolute = 1;
538 if (!defined $builds[0]) {
539 my $cwd = getcwd();
540 if (chdir($builddir)) {
541 @builds = glob('*');
542 chdir($cwd);
544 else {
545 usageAndExit('There are no builds to update.');
549 exit(linkFiles($absolute, $dmode, $hardlink, $builddir, \@builds));