build/macosx/universal/unify

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/build/macosx/universal/unify	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1525 @@
     1.4 +#!/usr/bin/perl
     1.5 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.6 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.8 +
     1.9 +use strict;
    1.10 +use warnings;
    1.11 +
    1.12 +=pod
    1.13 +
    1.14 +=head1 NAME
    1.15 +
    1.16 +B<unify> - Mac OS X universal binary packager
    1.17 +
    1.18 +=head1 SYNOPSIS
    1.19 +
    1.20 +B<unify>
    1.21 +I<ppc-path>
    1.22 +I<x86-path>
    1.23 +I<universal-path>
    1.24 +[B<--dry-run>]
    1.25 +[B<--only-one> I<action>]
    1.26 +[B<--verbosity> I<level>]
    1.27 +[B<--unify-with-sort> I<regex>]
    1.28 +
    1.29 +=head1 DESCRIPTION
    1.30 +
    1.31 +I<unify> merges any two architecture-specific files or directory trees
    1.32 +into a single file or tree suitable for use on either architecture as a
    1.33 +"fat" or "universal binary."
    1.34 +
    1.35 +Architecture-specific Mach-O files will be merged into fat Mach-O files
    1.36 +using L<lipo(1)>.  Non-Mach-O files in the architecture-specific trees
    1.37 +are compared to ensure that they are equivalent before copying.  Symbolic
    1.38 +links are permitted in the architecture-specific trees and will cause
    1.39 +identical links to be created in the merged tree, provided that the source
    1.40 +links have identical targets.  Directories are processed recursively.
    1.41 +
    1.42 +If the architecture-specific source trees contain zip archives (including
    1.43 +jar files) that are not identical according to a byte-for-byte check, they
    1.44 +are still assumed to be equivalent if both archives contain exactly the
    1.45 +same members with identical checksums and sizes.
    1.46 +
    1.47 +Behavior when one architecture-specific tree contains files that the other
    1.48 +does not is controlled by the B<--only-one> option.
    1.49 +
    1.50 +If Mach-O files cannot be merged using L<lipo(1)>, zip archives are not
    1.51 +equivalent, regular files are not identical, or any other error occurs,
    1.52 +B<unify> will fail with an exit status of 1.  Diagnostic messages are
    1.53 +typically printed to stderr; this behavior can be controlled with the
    1.54 +B<--verbosity> option.
    1.55 +
    1.56 +=head1 OPTIONS
    1.57 +
    1.58 +=over 5
    1.59 +
    1.60 +=item I<ppc-path>
    1.61 +
    1.62 +=item I<x86-path>
    1.63 +
    1.64 +The paths to directory trees containing PowerPC and x86 builds,
    1.65 +respectively.  I<ppc-path> and I<x86-path> are permitted to contain files
    1.66 +that are already "fat," and only the appropriate architecture's images will
    1.67 +be used.
    1.68 +
    1.69 +I<ppc-path> and I<x86-path> are also permitted to both be files, in which
    1.70 +case B<unify> operates solely on those files, and produces an appropriate
    1.71 +merged file at I<target-path>.
    1.72 +
    1.73 +=item I<target-path>
    1.74 +
    1.75 +The path to the merged file or directory tree.  This path will be created,
    1.76 +and it must not exist prior to running B<unify>.
    1.77 +
    1.78 +=item B<--dry-run>
    1.79 +
    1.80 +When specified, the commands that would be executed are printed, without
    1.81 +actually executing them.  Note that B<--dry-run> and the equivalent
    1.82 +B<--verbosity> level during "wet" runs may print equivalent commands when
    1.83 +no commands are in fact executed: certain operations are handled internally
    1.84 +within B<unify>, and an approximation of a command that performs a similar
    1.85 +task is printed.
    1.86 +
    1.87 +=item B<--only-one> I<action>
    1.88 +
    1.89 +Controls handling of files that are only present in one of the two source
    1.90 +trees.  I<action> may be:
    1.91 +  skip - These files are skipped.
    1.92 +  copy - These files are copied from the tree in which they exist.
    1.93 +  fail - When this condition occurs, it is treated as an error.
    1.94 +
    1.95 +The default I<action> is copy.
    1.96 +
    1.97 +=item B<--verbosity> I<level>
    1.98 +
    1.99 +Adjusts the level of loudness of B<unify>.  The possible values for
   1.100 +I<level> are:
   1.101 +  0 - B<unify> never prints anything.
   1.102 +      (Other programs that B<unify> calls may still print messages.)
   1.103 +  1 - Fatal error messages are printed to stderr.
   1.104 +  2 - Nonfatal warnings are printed to stderr.
   1.105 +  3 - Commands are printed to stdout as they are executed.
   1.106 +
   1.107 +The default I<level> is 2.
   1.108 +
   1.109 +=item B<--unify-with-sort> I<regex>
   1.110 +
   1.111 +Allows merging files matching I<regex> that differ only by the ordering
   1.112 +of the lines contained within them. The unified file will have its contents
   1.113 +sorted. This option may be given multiple times to specify multiple
   1.114 +regexes for matching files.
   1.115 +
   1.116 +=back
   1.117 +
   1.118 +=head1 EXAMPLES
   1.119 +
   1.120 +=over 5
   1.121 +
   1.122 +=item Create a universal .app bundle from two architecture-specific .app
   1.123 +bundles:
   1.124 +
   1.125 +unify --only-one copy ppc/dist/firefox/Firefox.app
   1.126 +  x86/dist/firefox/Firefox.app universal/Firefox.app
   1.127 +  --verbosity 3
   1.128 +
   1.129 +=item Merge two identical architecture-specific trees:
   1.130 +
   1.131 +unify --only-one fail /usr/local /nfs/x86/usr/local
   1.132 +  /tmp/usrlocal.fat
   1.133 +
   1.134 +=back
   1.135 +
   1.136 +=head1 REQUIREMENTS
   1.137 +
   1.138 +The only esoteric requirement of B<unify> is that the L<lipo(1)> command
   1.139 +be available.  It is present on Mac OS X systems at least as early as
   1.140 +10.3.9, and probably earlier.  Mac OS X 10.4 ("Tiger") or later are
   1.141 +recommended.
   1.142 +
   1.143 +=head1 LICENSE
   1.144 +
   1.145 +MPL 2.
   1.146 +
   1.147 +=head1 AUTHOR
   1.148 +
   1.149 +The software was initially written by Mark Mentovai; copyright 2006
   1.150 +Google Inc.
   1.151 +
   1.152 +=head1 SEE ALSO
   1.153 +
   1.154 +L<cmp(1)>, L<ditto(1)>, L<lipo(1)>
   1.155 +
   1.156 +=cut
   1.157 +
   1.158 +use Archive::Zip(':ERROR_CODES');
   1.159 +use Errno;
   1.160 +use Fcntl;
   1.161 +use File::Compare;
   1.162 +use File::Copy;
   1.163 +use Getopt::Long;
   1.164 +
   1.165 +my (%gConfig, $gDryRun, $gOnlyOne, $gVerbosity, @gSortMatches);
   1.166 +
   1.167 +sub argumentEscape(@);
   1.168 +sub command(@);
   1.169 +sub compareZipArchives($$);
   1.170 +sub complain($$@);
   1.171 +sub copyIfIdentical($$$);
   1.172 +sub slurp($);
   1.173 +sub get_sorted($);
   1.174 +sub compare_sorted($$);
   1.175 +sub copyIfIdenticalWhenSorted($$$);
   1.176 +sub createUniqueFile($$);
   1.177 +sub makeUniversal($$$);
   1.178 +sub makeUniversalDirectory($$$);
   1.179 +sub makeUniversalInternal($$$$);
   1.180 +sub makeUniversalFile($$$);
   1.181 +sub usage();
   1.182 +sub readZipCRCs($);
   1.183 +
   1.184 +{
   1.185 +  package FileAttrCache;
   1.186 +
   1.187 +  sub new($$);
   1.188 +
   1.189 +  sub isFat($);
   1.190 +  sub isMachO($);
   1.191 +  sub isZip($);
   1.192 +  sub lIsDir($);
   1.193 +  sub lIsExecutable($);
   1.194 +  sub lIsRegularFile($);
   1.195 +  sub lIsSymLink($);
   1.196 +  sub lstat($);
   1.197 +  sub lstatMode($);
   1.198 +  sub lstatType($);
   1.199 +  sub magic($);
   1.200 +  sub magic2($);
   1.201 +  sub path($);
   1.202 +  sub stat($);
   1.203 +  sub statSize($);
   1.204 +}
   1.205 +
   1.206 +%gConfig = (
   1.207 +  'cmd_lipo' => 'lipo',
   1.208 +  'cmd_rm'   => 'rm',
   1.209 +);
   1.210 +
   1.211 +$gDryRun = 0;
   1.212 +$gOnlyOne = 'copy';
   1.213 +$gVerbosity = 2;
   1.214 +@gSortMatches = ();
   1.215 +
   1.216 +Getopt::Long::Configure('pass_through');
   1.217 +GetOptions('dry-run'           => \$gDryRun,
   1.218 +           'only-one=s'        => \$gOnlyOne,
   1.219 +           'verbosity=i'       => \$gVerbosity,
   1.220 +           'unify-with-sort=s' => \@gSortMatches,
   1.221 +           'config=s'          => \%gConfig); # "hidden" option not in usage()
   1.222 +
   1.223 +if (scalar(@ARGV) != 3 || $gVerbosity < 0 || $gVerbosity > 3 ||
   1.224 +    ($gOnlyOne ne 'skip' && $gOnlyOne ne 'copy' && $gOnlyOne ne 'fail')) {
   1.225 +  usage();
   1.226 +  exit(1);
   1.227 +}
   1.228 +
   1.229 +if (!makeUniversal($ARGV[0],$ARGV[1],$ARGV[2])) {
   1.230 +  # makeUniversal or something it called will have printed an error.
   1.231 +  exit(1);
   1.232 +}
   1.233 +
   1.234 +exit(0);
   1.235 +
   1.236 +# argumentEscape(@arguments)
   1.237 +#
   1.238 +# Takes a list of @arguments and makes them shell-safe.
   1.239 +sub argumentEscape(@) {
   1.240 +  my (@arguments);
   1.241 +  @arguments = @_;
   1.242 +
   1.243 +  my ($argument, @argumentsOut);
   1.244 +  foreach $argument (@arguments) {
   1.245 +    $argument =~ s%([^A-Za-z0-9_\-/.=+,])%\\$1%g;
   1.246 +    push(@argumentsOut, $argument);
   1.247 +  }
   1.248 +
   1.249 +  return @argumentsOut;
   1.250 +}
   1.251 +
   1.252 +# command(@arguments)
   1.253 +#
   1.254 +# Runs the specified command by calling system(@arguments).  If $gDryRun
   1.255 +# is true, the command is printed but not executed, and 0 is returned.
   1.256 +# if $gVerbosity is greater than 1, the command is printed before being
   1.257 +# executed.  When the command is executed, the system() return value will
   1.258 +# be returned.  stdout and stderr are left connected for command output.
   1.259 +sub command(@) {
   1.260 +  my (@arguments);
   1.261 +  @arguments = @_;
   1.262 +  if ($gVerbosity >= 3 || $gDryRun) {
   1.263 +    print(join(' ', argumentEscape(@arguments))."\n");
   1.264 +  }
   1.265 +  if ($gDryRun) {
   1.266 +    return 0;
   1.267 +  }
   1.268 +  return system(@arguments);
   1.269 +}
   1.270 +
   1.271 +# compareZipArchives($zip1, $zip2)
   1.272 +#
   1.273 +# Given two pathnames to zip archives, determines whether or not they are
   1.274 +# functionally identical.  Returns true if they are, false if they differ in
   1.275 +# some substantial way, and undef if an error occurs.  If the zip files
   1.276 +# differ, diagnostic messages are printed indicating how they differ.
   1.277 +#
   1.278 +# Zip files will differ if any of the members are different as defined by
   1.279 +# readZipCRCs, which consider CRCs, sizes, and file types as stored in the
   1.280 +# file header.  Timestamps are not considered.  Zip files also differ if one
   1.281 +# file contains members that the other one does not.  $gOnlyOne has no
   1.282 +# effect on this behavior.
   1.283 +sub compareZipArchives($$) {
   1.284 +  my ($zip1, $zip2);
   1.285 +  ($zip1, $zip2) = @_;
   1.286 +
   1.287 +  my ($CRCHash1, $CRCHash2);
   1.288 +  if (!defined($CRCHash1 = readZipCRCs($zip1))) {
   1.289 +    # readZipCRCs printed an error.
   1.290 +    return undef;
   1.291 +  }
   1.292 +  if (!defined($CRCHash2 = readZipCRCs($zip2))) {
   1.293 +    # readZipCRCs printed an error.
   1.294 +    return undef;
   1.295 +  }
   1.296 +
   1.297 +  my (@diffCRCs, @onlyInZip1);
   1.298 +  @diffCRCs = ();
   1.299 +  @onlyInZip1 = ();
   1.300 +
   1.301 +  my ($memberName);
   1.302 +  foreach $memberName (keys(%$CRCHash1)) {
   1.303 +    if (!exists($$CRCHash2{$memberName})) {
   1.304 +      # The member is present in $zip1 but not $zip2.
   1.305 +      push(@onlyInZip1, $memberName);
   1.306 +    }
   1.307 +    elsif ($$CRCHash1{$memberName} ne $$CRCHash2{$memberName}) {
   1.308 +      # The member is present in both archives but its CRC or some other
   1.309 +      # other critical attribute isn't identical.
   1.310 +      push(@diffCRCs, $memberName);
   1.311 +    }
   1.312 +    delete($$CRCHash2{$memberName});
   1.313 +  }
   1.314 +
   1.315 +  # If any members remain in %CRCHash2, it's because they're not present
   1.316 +  # in $zip1.
   1.317 +  my (@onlyInZip2);
   1.318 +  @onlyInZip2 = keys(%$CRCHash2);
   1.319 +
   1.320 +  if (scalar(@onlyInZip1) + scalar(@onlyInZip2) + scalar(@diffCRCs)) {
   1.321 +    complain(1, 'compareZipArchives: zip archives differ:',
   1.322 +             $zip1,
   1.323 +             $zip2);
   1.324 +    if (scalar(@onlyInZip1)) {
   1.325 +      complain(1, 'compareZipArchives: members only in former:',
   1.326 +               @onlyInZip1);
   1.327 +    }
   1.328 +    if (scalar(@onlyInZip2)) {
   1.329 +      complain(1, 'compareZipArchives: members only in latter:',
   1.330 +               @onlyInZip2);
   1.331 +    }
   1.332 +    if (scalar(@diffCRCs)) {
   1.333 +      complain(1, 'compareZipArchives: members differ:',
   1.334 +               @diffCRCs);
   1.335 +    }
   1.336 +    return 0;
   1.337 +  }
   1.338 +
   1.339 +  return 1;
   1.340 +}
   1.341 +
   1.342 +# complain($severity, $message, @list)
   1.343 +#
   1.344 +# Prints $message to stderr if $gVerbosity allows it for severity level
   1.345 +# $severity.  @list is a list of words that will be shell-escaped and printed
   1.346 +# after $message, one per line, intended to be used, for example, to list
   1.347 +# arguments to a call that failed.
   1.348 +#
   1.349 +# Expected severity levels are 1 for hard errors and 2 for non-fatal warnings.
   1.350 +#
   1.351 +# Always returns false as a convenience, so callers can return complain's
   1.352 +# return value when it is used to signal errors.
   1.353 +sub complain($$@) {
   1.354 +  my ($severity, $message, @list);
   1.355 +  ($severity, $message, @list) = @_;
   1.356 +
   1.357 +  if ($gVerbosity >= $severity) {
   1.358 +    print STDERR ($0.': '.$message."\n");
   1.359 +
   1.360 +    my ($item);
   1.361 +    while ($item = shift(@list)) {
   1.362 +      print STDERR ('  '.(argumentEscape($item))[0].
   1.363 +                    (scalar(@list)?',':'')."\n");
   1.364 +    }
   1.365 +  }
   1.366 +
   1.367 +  return 0;
   1.368 +}
   1.369 +
   1.370 +# copyIfIdentical($source1, $source2, $target)
   1.371 +#
   1.372 +# $source1 and $source2 are FileAttrCache objects that are compared, and if
   1.373 +# identical, copied to path string $target.  The comparison is initially
   1.374 +# done as a byte-for-byte comparison, but if the files differ and appear to
   1.375 +# be zip archives, compareZipArchives is called to determine whether
   1.376 +# files that are not byte-for-byte identical are equivalent archives.
   1.377 +#
   1.378 +# Returns true on success, false for files that are not identical or
   1.379 +# equivalent archives, and undef if an error occurs.
   1.380 +#
   1.381 +# One of $source1 and $source2 is permitted to be undef.  In this event,
   1.382 +# whichever source is defined is copied directly to $target without performing
   1.383 +# any comparisons.  This enables the $gOnlyOne = 'copy' mode, which is
   1.384 +# driven by makeUniversalDirectory and makeUniversalInternal.
   1.385 +sub copyIfIdentical($$$) {
   1.386 +  my ($source1, $source2, $target);
   1.387 +  ($source1, $source2, $target) = @_;
   1.388 +
   1.389 +  if (!defined($source1)) {
   1.390 +    # If there's only one source file, make it the first file.  Order
   1.391 +    # isn't important here, and this makes it possible to use
   1.392 +    # defined($source2) as the switch, and to always copy from $source1.
   1.393 +    $source1 = $source2;
   1.394 +    $source2 = undef;
   1.395 +  }
   1.396 +
   1.397 +  if (defined($source2)) {
   1.398 +    # Only do the comparisons if there are two source files.  If there's
   1.399 +    # only one source file, skip the comparisons and go straight to the
   1.400 +    # copy operation.
   1.401 +    if ($gVerbosity >= 3 || $gDryRun) {
   1.402 +      print('cmp -s '.
   1.403 +            join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
   1.404 +    }
   1.405 +    my ($comparison);
   1.406 +    if (!defined($comparison = compare($source1->path(), $source2->path())) ||
   1.407 +        $comparison == -1) {
   1.408 +      return complain(1, 'copyIfIdentical: compare: '.$!.' while comparing:',
   1.409 +                      $source1->path(),
   1.410 +                      $source2->path());
   1.411 +    }
   1.412 +    elsif ($comparison != 0) {
   1.413 +      my ($zip1, $zip2);
   1.414 +      if (defined($zip1 = $source1->isZip()) &&
   1.415 +          defined($zip2 = $source2->isZip()) &&
   1.416 +          $zip1 && $zip2) {
   1.417 +        my ($zipComparison);
   1.418 +        if (!defined($zipComparison = compareZipArchives($source1->path(),
   1.419 +                                                         $source2->path)) ||
   1.420 +            !$zipComparison) {
   1.421 +          # An error occurred or the zip files aren't sufficiently identical.
   1.422 +          # compareZipArchives will have printed an error message.
   1.423 +          return 0;
   1.424 +        }
   1.425 +        # The zip files were compared successfully, and they both contain
   1.426 +        # all of the same members, and all of their members' CRCs are
   1.427 +        # identical.  For the purposes of this script, the zip files can be
   1.428 +        # treated as identical, so reset $comparison.
   1.429 +        $comparison = 0;
   1.430 +      }
   1.431 +    }
   1.432 +    if ($comparison != 0) {
   1.433 +      return complain(1, 'copyIfIdentical: files differ:',
   1.434 +                      $source1->path(),
   1.435 +                      $source2->path());
   1.436 +    }
   1.437 +  }
   1.438 +
   1.439 +  if ($gVerbosity >= 3 || $gDryRun) {
   1.440 +    print('cp '.
   1.441 +          join(' ',argumentEscape($source1->path(), $target))."\n");
   1.442 +  }
   1.443 +
   1.444 +  if (!$gDryRun) {
   1.445 +    my ($isExecutable);
   1.446 +
   1.447 +    # Set the execute bits (as allowed by the umask) on the new file if any
   1.448 +    # execute bit is set on either old file.
   1.449 +    $isExecutable = $source1->lIsExecutable() ||
   1.450 +                    (defined($source2) && $source2->lIsExecutable());
   1.451 +
   1.452 +    if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
   1.453 +      # createUniqueFile printed an error.
   1.454 +      return 0;
   1.455 +    }
   1.456 +
   1.457 +    if (!copy($source1->path(), $target)) {
   1.458 +      complain(1, 'copyIfIdentical: copy: '.$!.' while copying',
   1.459 +               $source1->path(),
   1.460 +               $target);
   1.461 +      unlink($target);
   1.462 +      return 0;
   1.463 +    }
   1.464 +  }
   1.465 +
   1.466 +  return 1;
   1.467 +}
   1.468 +
   1.469 +# slurp($file)
   1.470 +#
   1.471 +# Read the contents of $file into an array and return it.
   1.472 +# Returns undef on error.
   1.473 +sub slurp($) {
   1.474 +  my $file = $_[0];
   1.475 +  open FILE, $file or return undef;
   1.476 +  my @lines = <FILE>;
   1.477 +  close FILE;
   1.478 +  return @lines;
   1.479 +}
   1.480 +
   1.481 +# get_sorted($file)
   1.482 +# Get the sorted lines of a file as a list, normalizing a newline on the last line if necessary.
   1.483 +sub get_sorted($) {
   1.484 +  my ($file) = @_;
   1.485 +  my @lines = slurp($file);
   1.486 +  my $lastline = $lines[-1];
   1.487 +  if (!($lastline =~ /\n/)) {
   1.488 +    $lines[-1] = $lastline . "\n";
   1.489 +  }
   1.490 +  return sort(@lines);
   1.491 +}
   1.492 +
   1.493 +# compare_sorted($file1, $file2)
   1.494 +#
   1.495 +# Read the contents of both files into arrays, sort the arrays,
   1.496 +# and then compare the two arrays for equality.
   1.497 +#
   1.498 +# Returns 0 if the sorted array contents are equal, or 1 if not.
   1.499 +# Returns undef on error.
   1.500 +sub compare_sorted($$) {
   1.501 +  my ($file1, $file2) = @_;
   1.502 +  my @lines1 = get_sorted($file1);
   1.503 +  my @lines2 = get_sorted($file2);
   1.504 +
   1.505 +  return undef if !@lines1 || !@lines2;
   1.506 +  return 1 unless scalar @lines1 == scalar @lines2;
   1.507 +
   1.508 +  for (my $i = 0; $i < scalar @lines1; $i++) {
   1.509 +    return 1 if $lines1[$i] ne $lines2[$i];
   1.510 +  }
   1.511 +  return 0;
   1.512 +}
   1.513 +
   1.514 +# copyIfIdenticalWhenSorted($source1, $source2, $target)
   1.515 +#
   1.516 +# $source1 and $source2 are FileAttrCache objects that are compared, and if
   1.517 +# identical, copied to path string $target.  The comparison is done by
   1.518 +# sorting the individual lines within the two files and comparing the results.
   1.519 +#
   1.520 +# Returns true on success, false for files that are not equivalent,
   1.521 +# and undef if an error occurs.
   1.522 +sub copyIfIdenticalWhenSorted($$$) {
   1.523 +  my ($source1, $source2, $target);
   1.524 +  ($source1, $source2, $target) = @_;
   1.525 +
   1.526 +  if ($gVerbosity >= 3 || $gDryRun) {
   1.527 +    print('cmp -s '.
   1.528 +          join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
   1.529 +  }
   1.530 +  my ($comparison);
   1.531 +  if (!defined($comparison = compare_sorted($source1->path(),
   1.532 +                                            $source2->path())) ||
   1.533 +      $comparison == -1) {
   1.534 +    return complain(1, 'copyIfIdenticalWhenSorted: compare: '.$!
   1.535 +                    .' while comparing:',
   1.536 +                      $source1->path(),
   1.537 +                      $source2->path());
   1.538 +  }
   1.539 +  if ($comparison != 0) {
   1.540 +    return complain(1, 'copyIfIdenticalWhenSorted: files differ:',
   1.541 +                    $source1->path(),
   1.542 +                    $source2->path());
   1.543 +  }
   1.544 +
   1.545 +  if ($gVerbosity >= 3 || $gDryRun) {
   1.546 +    print('cp '.
   1.547 +          join(' ',argumentEscape($source1->path(), $target))."\n");
   1.548 +  }
   1.549 +
   1.550 +  if (!$gDryRun) {
   1.551 +    my ($isExecutable);
   1.552 +
   1.553 +    # Set the execute bits (as allowed by the umask) on the new file if any
   1.554 +    # execute bit is set on either old file.
   1.555 +    $isExecutable = $source1->lIsExecutable() ||
   1.556 +                    (defined($source2) && $source2->lIsExecutable());
   1.557 +
   1.558 +    if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
   1.559 +      # createUniqueFile printed an error.
   1.560 +      return 0;
   1.561 +    }
   1.562 +
   1.563 +    if (!copy($source1->path(), $target)) {
   1.564 +      complain(1, 'copyIfIdenticalWhenSorted: copy: '.$!
   1.565 +               .' while copying',
   1.566 +               $source1->path(),
   1.567 +               $target);
   1.568 +      unlink($target);
   1.569 +      return 0;
   1.570 +    }
   1.571 +  }
   1.572 +
   1.573 +  return 1;
   1.574 +}
   1.575 +
   1.576 +# createUniqueFile($path, $mode)
   1.577 +#
   1.578 +# Creates a new plain empty file at pathname $path, provided it does not
   1.579 +# yet exist.  $mode is used as the file mode.  The actual file's mode will
   1.580 +# be modified by the effective umask.  Returns false if the file could
   1.581 +# not be created, setting $! to the error.  An error message is printed
   1.582 +# in the event of failure.
   1.583 +sub createUniqueFile($$) {
   1.584 +  my ($path, $mode);
   1.585 +  ($path, $mode) = @_;
   1.586 +
   1.587 +  my ($fh);
   1.588 +  if (!sysopen($fh, $path, O_WRONLY | O_CREAT | O_EXCL, $mode)) {
   1.589 +    return complain(1, 'createUniqueFile: open: '.$!.' for:',
   1.590 +                    $path);
   1.591 +  }
   1.592 +  close($fh);
   1.593 +
   1.594 +  return 1;
   1.595 +}
   1.596 +
   1.597 +# makeUniversal($pathPPC, $pathX86, $pathTarget)
   1.598 +#
   1.599 +# The top-level call.  $pathPPC, $pathX86, and $pathTarget are strings
   1.600 +# identifying the ppc and x86 files or directories to merge and the location
   1.601 +# to merge them to.  Returns false on failure and true on success.
   1.602 +sub makeUniversal($$$) {
   1.603 +  my ($pathTarget, $pathPPC, $pathX86);
   1.604 +  ($pathPPC, $pathX86, $pathTarget) = @_;
   1.605 +
   1.606 +  my ($filePPC, $fileX86);
   1.607 +  $filePPC = FileAttrCache->new($pathPPC);
   1.608 +  $fileX86 = FileAttrCache->new($pathX86);
   1.609 +
   1.610 +  return makeUniversalInternal(1, $filePPC, $fileX86, $pathTarget);
   1.611 +}
   1.612 +
   1.613 +# makeUniversalDirectory($dirPPC, $dirX86, $dirTarget)
   1.614 +#
   1.615 +# This is part of the heart of recursion.  $dirPPC and $dirX86 are
   1.616 +# FileAttrCache objects designating the source ppc and x86 directories to
   1.617 +# merge into a universal directory at $dirTarget, a string.  For each file
   1.618 +# in $dirPPC and $dirX86, makeUniversalInternal is called.
   1.619 +# makeUniversalInternal will call back into makeUniversalDirectory for
   1.620 +# directories, thus completing the recursion.  If a failure is encountered
   1.621 +# in ths function or in makeUniversalInternal or anything that it calls,
   1.622 +# false is returned, otherwise, true is returned.
   1.623 +#
   1.624 +# If there are files present in one source directory but not both, the
   1.625 +# value of $gOnlyOne controls the behavior.  If $gOnlyOne is 'copy', the
   1.626 +# single source file is copied into $pathTarget.  If it is 'skip', it is
   1.627 +# skipped.  If it is 'fail', such files will trigger makeUniversalDirectory
   1.628 +# to fail.
   1.629 +#
   1.630 +# If either source directory is undef, it is treated as having no files.
   1.631 +# This facilitates deep recursion when entire directories are only present
   1.632 +# in one source when $gOnlyOne = 'copy'.
   1.633 +sub makeUniversalDirectory($$$) {
   1.634 +  my ($dirPPC, $dirX86, $dirTarget);
   1.635 +  ($dirPPC, $dirX86, $dirTarget) = @_;
   1.636 +
   1.637 +  my ($dh, @filesPPC, @filesX86);
   1.638 +
   1.639 +  @filesPPC = ();
   1.640 +  if (defined($dirPPC)) {
   1.641 +    if (!opendir($dh, $dirPPC->path())) {
   1.642 +      return complain(1, 'makeUniversalDirectory: opendir ppc: '.$!.' for:',
   1.643 +                      $dirPPC->path());
   1.644 +    }
   1.645 +    @filesPPC = readdir($dh);
   1.646 +    closedir($dh);
   1.647 +  }
   1.648 +
   1.649 +  @filesX86 = ();
   1.650 +  if (defined($dirX86)) {
   1.651 +    if (!opendir($dh, $dirX86->path())) {
   1.652 +      return complain(1, 'makeUniversalDirectory: opendir x86: '.$!.' for:',
   1.653 +                      $dirX86->path());
   1.654 +     }
   1.655 +    @filesX86 = readdir($dh);
   1.656 +    closedir($dh);
   1.657 +  }
   1.658 +
   1.659 +  my (%common, $file, %onlyPPC, %onlyX86);
   1.660 +
   1.661 +  %onlyPPC = ();
   1.662 +  foreach $file (@filesPPC) {
   1.663 +    if ($file eq '.' || $file eq '..') {
   1.664 +      next;
   1.665 +    }
   1.666 +    $onlyPPC{$file}=1;
   1.667 +  }
   1.668 +
   1.669 +  %common = ();
   1.670 +  %onlyX86 = ();
   1.671 +  foreach $file (@filesX86) {
   1.672 +    if ($file eq '.' || $file eq '..') {
   1.673 +      next;
   1.674 +    }
   1.675 +    if ($onlyPPC{$file}) {
   1.676 +      delete $onlyPPC{$file};
   1.677 +      $common{$file}=1;
   1.678 +    }
   1.679 +    else {
   1.680 +      $onlyX86{$file}=1;
   1.681 +    }
   1.682 +  }
   1.683 +
   1.684 +  # First, handle files common to both.
   1.685 +  foreach $file (sort(keys(%common))) {
   1.686 +    if (!makeUniversalInternal(0,
   1.687 +                               FileAttrCache->new($dirPPC->path().'/'.$file),
   1.688 +                               FileAttrCache->new($dirX86->path().'/'.$file),
   1.689 +                               $dirTarget.'/'.$file)) {
   1.690 +      # makeUniversalInternal will have printed an error.
   1.691 +      return 0;
   1.692 +    }
   1.693 +  }
   1.694 +
   1.695 +  # Handle files found only in a single directory here.  There are three
   1.696 +  # options, dictated by $gOnlyOne: fail if files are only present in
   1.697 +  # one directory, skip any files only present in one directory, or copy
   1.698 +  # these files straight over to the target directory.  In any event,
   1.699 +  # a message will be printed indicating that the file trees don't match
   1.700 +  # exactly.
   1.701 +  if (keys(%onlyPPC)) {
   1.702 +    complain(($gOnlyOne eq 'fail' ? 1 : 2),
   1.703 +             ($gOnlyOne ne 'fail' ? 'warning: ' : '').
   1.704 +             'makeUniversalDirectory: only in ppc '.
   1.705 +             (argumentEscape($dirPPC->path()))[0].':',
   1.706 +             argumentEscape(keys(%onlyPPC)));
   1.707 +  }
   1.708 +
   1.709 +  if (keys(%onlyX86)) {
   1.710 +    complain(($gOnlyOne eq 'fail' ? 1 : 2),
   1.711 +             ($gOnlyOne ne 'fail' ? 'warning: ' : '').
   1.712 +             'makeUniversalDirectory: only in x86 '.
   1.713 +             (argumentEscape($dirX86->path()))[0].':',
   1.714 +             argumentEscape(keys(%onlyX86)));
   1.715 +  }
   1.716 +
   1.717 +  if ($gOnlyOne eq 'fail' && (keys(%onlyPPC) || keys(%onlyX86))) {
   1.718 +    # Error message(s) printed above.
   1.719 +    return 0;
   1.720 +  }
   1.721 +
   1.722 +  if ($gOnlyOne eq 'copy') {
   1.723 +    foreach $file (sort(keys(%onlyPPC))) {
   1.724 +      if (!makeUniversalInternal(0,
   1.725 +                                 FileAttrCache->new($dirPPC->path().'/'.$file),
   1.726 +                                 undef,
   1.727 +                                 $dirTarget.'/'.$file)) {
   1.728 +        # makeUniversalInternal will have printed an error.
   1.729 +        return 0;
   1.730 +      }
   1.731 +    }
   1.732 +
   1.733 +    foreach $file (sort(keys(%onlyX86))) {
   1.734 +      if (!makeUniversalInternal(0,
   1.735 +                                 undef,
   1.736 +                                 FileAttrCache->new($dirX86->path().'/'.$file),
   1.737 +                                 $dirTarget.'/'.$file)) {
   1.738 +        # makeUniversalInternal will have printed an error.
   1.739 +        return 0;
   1.740 +      }
   1.741 +    }
   1.742 +  }
   1.743 +
   1.744 +  return 1;
   1.745 +}
   1.746 +
   1.747 +# makeUniversalFile($sourcePPC, $sourceX86, $targetPath)
   1.748 +#
   1.749 +# Creates a universal file at pathname $targetPath based on a ppc image at
   1.750 +# $sourcePPC and an x86 image at $sourceX86.  $sourcePPC and $sourceX86 are
   1.751 +# both FileAttrCache objects.  Returns true on success and false on failure.
   1.752 +# On failure, diagnostics will be printed to stderr.
   1.753 +#
   1.754 +# The source files may be either thin Mach-O images of the appropriate
   1.755 +# architecture, or fat Mach-O files that contain images of the appropriate
   1.756 +# architecture.
   1.757 +#
   1.758 +# This function wraps the lipo utility, see lipo(1).
   1.759 +sub makeUniversalFile($$$) {
   1.760 +  my ($sourcePPC, $sourceX86, $targetPath, @tempThinFiles, $thinPPC, $thinX86);
   1.761 +  ($sourcePPC, $sourceX86, $targetPath) = @_;
   1.762 +  $thinPPC = $sourcePPC;
   1.763 +  $thinX86 = $sourceX86;
   1.764 +
   1.765 +  @tempThinFiles = ();
   1.766 +
   1.767 +  # The source files might already be fat.  They should be thinned out to only
   1.768 +  # contain a single architecture.
   1.769 + 
   1.770 +  my ($isFatPPC, $isFatX86);
   1.771 +
   1.772 +  if(!defined($isFatPPC = $sourcePPC->isFat())) {
   1.773 +    # isFat printed its own error
   1.774 +    return 0;
   1.775 +  }
   1.776 +  elsif($isFatPPC) {
   1.777 +    $thinPPC = FileAttrCache->new($targetPath.'.ppc');
   1.778 +    push(@tempThinFiles, $thinPPC->path());
   1.779 +    if (command($gConfig{'cmd_lipo'}, '-thin', 'ppc',
   1.780 +                $sourcePPC->path(), '-output', $thinPPC->path()) != 0) {
   1.781 +      unlink(@tempThinFiles);
   1.782 +      return complain(1, 'lipo thin ppc failed for:',
   1.783 +                      $sourcePPC->path(),
   1.784 +                      $thinPPC->path());
   1.785 +    }
   1.786 +  }
   1.787 +
   1.788 +  if(!defined($isFatX86 = $sourceX86->isFat())) {
   1.789 +    # isFat printed its own error
   1.790 +    unlink(@tempThinFiles);
   1.791 +    return 0;
   1.792 +  }
   1.793 +  elsif($isFatX86) {
   1.794 +    $thinX86 = FileAttrCache->new($targetPath.'.x86');
   1.795 +    push(@tempThinFiles, $thinX86->path());
   1.796 +    if (command($gConfig{'cmd_lipo'}, '-thin', 'i386',
   1.797 +                $sourceX86->path(), '-output', $thinX86->path()) != 0) {
   1.798 +      unlink(@tempThinFiles);
   1.799 +      return complain(1, 'lipo thin x86 failed for:',
   1.800 +                      $sourceX86->path(),
   1.801 +                      $thinX86->path());
   1.802 +    }
   1.803 +  }
   1.804 +
   1.805 +  # The image for each architecture in the fat file will be aligned on
   1.806 +  # a specific boundary, default 4096 bytes, see lipo(1) -segalign.
   1.807 +  # Since there's no tail-padding, the fat file will consume the least
   1.808 +  # space on disk if the image that comes last exceeds the segment size
   1.809 +  # by the smallest amount.
   1.810 +  #
   1.811 +  # This saves an average of 1kB per fat file over the naive approach of
   1.812 +  # always putting one architecture first: average savings is 2kB per
   1.813 +  # file, but the naive approach would have gotten it right half of the
   1.814 +  # time.
   1.815 +
   1.816 +  my ($sizePPC, $sizeX86, $thinPPCForStat, $thinX86ForStat);
   1.817 +
   1.818 +  if (!$gDryRun) {
   1.819 +    $thinPPCForStat = $thinPPC;
   1.820 +    $thinX86ForStat = $thinX86;
   1.821 +  }
   1.822 +  else {
   1.823 +    # Normally, fat source files will have been converted into temporary
   1.824 +    # thin files.  During a dry run, that doesn't happen, so fake it up
   1.825 +    # a little bit by always using the source file, fat or thin, for the
   1.826 +    # stat.
   1.827 +    $thinPPCForStat = $sourcePPC;
   1.828 +    $thinX86ForStat = $sourceX86;
   1.829 +  }
   1.830 +
   1.831 +  if (!defined($sizePPC = $thinPPCForStat->statSize())) {
   1.832 +    unlink(@tempThinFiles);
   1.833 +    return complain(1, 'stat ppc: '.$!.' for:',
   1.834 +                    $thinPPCForStat->path());
   1.835 +  }
   1.836 +  if (!defined($sizeX86 = $thinX86ForStat->statSize())) {
   1.837 +    unlink(@tempThinFiles);
   1.838 +    return complain(1, 'stat x86: '.$!.' for:',
   1.839 +                    $thinX86ForStat->path());
   1.840 +  }
   1.841 +
   1.842 +  $sizePPC = $sizePPC % 4096;
   1.843 +  $sizeX86 = $sizeX86 % 4096;
   1.844 +
   1.845 +  my (@thinFiles);
   1.846 +
   1.847 +  if ($sizePPC == 0) {
   1.848 +    # PPC image ends on an alignment boundary, there will be no padding before
   1.849 +    # starting the x86 image.
   1.850 +    @thinFiles = ($thinPPC->path(), $thinX86->path());
   1.851 +  }
   1.852 +  elsif ($sizeX86 == 0 || $sizeX86 > $sizePPC) {
   1.853 +    # x86 image ends on an alignment boundary, there will be no padding before
   1.854 +    # starting the PPC image, or the x86 image exceeds its alignment boundary
   1.855 +    # by more than the PPC image, so there will be less padding if the x86
   1.856 +    # comes first.
   1.857 +    @thinFiles = ($thinX86->path(), $thinPPC->path());
   1.858 +  }
   1.859 +  else {
   1.860 +    # PPC image exceeds its alignment boundary by more than the x86 image, so
   1.861 +    # there will be less padding if the PPC comes first.
   1.862 +    @thinFiles = ($thinPPC->path(), $thinX86->path());
   1.863 +  }
   1.864 +
   1.865 +  my ($isExecutable);
   1.866 +  $isExecutable = $sourcePPC->lIsExecutable() ||
   1.867 +                  $sourceX86->lIsExecutable();
   1.868 +
   1.869 +  if (!$gDryRun) {
   1.870 +    # Ensure that the file does not yet exist.
   1.871 +
   1.872 +    # Set the execute bits (as allowed by the umask) on the new file if any
   1.873 +    # execute bit is set on either old file.  Yes, it is possible to have
   1.874 +    # proper Mach-O files without x-bits: think object files (.o) and static
   1.875 +    # archives (.a).
   1.876 +    if (!createUniqueFile($targetPath, $isExecutable ? 0777 : 0666)) {
   1.877 +      # createUniqueFile printed an error.
   1.878 +      unlink(@tempThinFiles);
   1.879 +      return 0;
   1.880 +    }
   1.881 +  }
   1.882 +
   1.883 +  # Create the fat file.
   1.884 +  if (command($gConfig{'cmd_lipo'}, '-create', @thinFiles,
   1.885 +              '-output', $targetPath) != 0) {
   1.886 +    unlink(@tempThinFiles, $targetPath);
   1.887 +    return complain(1, 'lipo create fat failed for:',
   1.888 +                    @thinFiles,
   1.889 +                    $targetPath);
   1.890 +  }
   1.891 +
   1.892 +  unlink(@tempThinFiles);
   1.893 +
   1.894 +  if (!$gDryRun) {
   1.895 +    # lipo seems to think that it's free to set its own file modes that
   1.896 +    # ignore the umask, which is bogus when the rest of this script
   1.897 +    # respects the umask.
   1.898 +    if (!chmod(($isExecutable ? 0777 : 0666) & ~umask(), $targetPath)) {
   1.899 +      complain(1, 'makeUniversalFile: chmod: '.$!.' for',
   1.900 +               $targetPath);
   1.901 +      unlink($targetPath);
   1.902 +      return 0;
   1.903 +    }
   1.904 +  }
   1.905 +
   1.906 +  return 1;
   1.907 +}
   1.908 +
   1.909 +# makeUniversalInternal($isToplevel, $filePPC, $fileX86, $fileTargetPath)
   1.910 +#
   1.911 +# Given FileAttrCache objects $filePPC and $fileX86, compares filetypes
   1.912 +# and performs the appropriate action to produce a universal file at
   1.913 +# path string $fileTargetPath.  $isToplevel should be true if this is
   1.914 +# the recursive base and false otherwise; this controls cleanup behavior
   1.915 +# (cleanup is only performed at the base, because cleanup itself is
   1.916 +# recursive).
   1.917 +#
   1.918 +# This handles regular files by determining whether they are Mach-O files
   1.919 +# and calling makeUniversalFile if so and copyIfIdentical otherwise.  Symbolic
   1.920 +# links are handled directly in this function by ensuring that the source link
   1.921 +# targets are identical and creating a new link with the same target
   1.922 +# at $fileTargetPath.  Directories are handled by calling
   1.923 +# makeUniversalDirectory.
   1.924 +#
   1.925 +# One of $filePPC and $fileX86 is permitted to be undef.  In that case,
   1.926 +# the defined source file is copied directly to the target if a regular
   1.927 +# file, and symlinked appropriately if a symbolic link.  This facilitates
   1.928 +# use of $gOnlyOne = 'copy', although no $gOnlyOne checks are made in this
   1.929 +# function, they are all handled in makeUniversalDirectory.
   1.930 +#
   1.931 +# Returns true on success.  Returns false on failure, including failures
   1.932 +# in other functions called.
   1.933 +sub makeUniversalInternal($$$$) {
   1.934 +  my ($filePPC, $fileTargetPath, $fileX86, $isToplevel);
   1.935 +  ($isToplevel, $filePPC, $fileX86, $fileTargetPath) = @_;
   1.936 +
   1.937 +  my ($typePPC, $typeX86);
   1.938 +  if (defined($filePPC) && !defined($typePPC = $filePPC->lstatType())) {
   1.939 +    return complain(1, 'makeUniversal: lstat ppc: '.$!.' for:',
   1.940 +                    $filePPC->path());
   1.941 +  }
   1.942 +  if (defined($fileX86) && !defined($typeX86 = $fileX86->lstatType())) {
   1.943 +    return complain(1, 'makeUniversal: lstat x86: '.$!.' for:',
   1.944 +                    $fileX86->path());
   1.945 +  }
   1.946 +
   1.947 +  if (defined($filePPC) && defined($fileX86) && $typePPC != $typeX86) {
   1.948 +    return complain(1, 'makeUniversal: incompatible types:',
   1.949 +                    $filePPC->path(),
   1.950 +                    $fileX86->path());
   1.951 +  }
   1.952 +
   1.953 +  # $aSourceFile will contain a FileAttrCache object that will return
   1.954 +  # the correct type data.  It's used because it's possible for one of
   1.955 +  # the two source files to be undefined (indicating a straight copy).
   1.956 +  my ($aSourceFile);
   1.957 +  if (defined($filePPC)) { 
   1.958 +    $aSourceFile = $filePPC;
   1.959 +  }
   1.960 +  else {
   1.961 +    $aSourceFile = $fileX86;
   1.962 +  }
   1.963 +
   1.964 +  if ($aSourceFile->lIsDir()) {
   1.965 +    if ($gVerbosity >= 3 || $gDryRun) {
   1.966 +      print('mkdir '.(argumentEscape($fileTargetPath))[0]."\n");
   1.967 +    }
   1.968 +    if (!$gDryRun && !mkdir($fileTargetPath)) {
   1.969 +      return complain(1, 'makeUniversal: mkdir: '.$!.' for:',
   1.970 +                      $fileTargetPath);
   1.971 +    }
   1.972 +
   1.973 +    my ($rv);
   1.974 +
   1.975 +    if (!($rv = makeUniversalDirectory($filePPC, $fileX86, $fileTargetPath))) {
   1.976 +      # makeUniversalDirectory printed an error.
   1.977 +      if ($isToplevel) {
   1.978 +        command($gConfig{'cmd_rm'},'-rf','--',$fileTargetPath);
   1.979 +      }
   1.980 +    }
   1.981 +    else {
   1.982 +      # Touch the directory when leaving it.  If unify is being run on an
   1.983 +      # .app bundle, the .app might show up without an icon because the
   1.984 +      # system might have found the .app before it was completely built.
   1.985 +      # Touching it dirties it in LaunchServices' mind.
   1.986 +      if ($gVerbosity >= 3) {
   1.987 +        print('touch '.(argumentEscape($fileTargetPath))[0]."\n");
   1.988 +      }
   1.989 +      utime(undef, undef, $fileTargetPath);
   1.990 +    }
   1.991 +
   1.992 +    return $rv;
   1.993 +  }
   1.994 +  elsif ($aSourceFile->lIsSymLink()) {
   1.995 +    my ($linkPPC, $linkX86);
   1.996 +    if (defined($filePPC) && !defined($linkPPC=readlink($filePPC->path()))) {
   1.997 +      return complain(1, 'makeUniversal: readlink ppc: '.$!.' for:',
   1.998 +                      $filePPC->path());
   1.999 +    }
  1.1000 +    if (defined($fileX86) && !defined($linkX86=readlink($fileX86->path()))) {
  1.1001 +      return complain(1, 'makeUniversal: readlink x86: '.$!.' for:',
  1.1002 +                      $fileX86->path());
  1.1003 +    }
  1.1004 +    if (defined($filePPC) && defined($fileX86) && $linkPPC ne $linkX86) {
  1.1005 +      return complain(1, 'makeUniversal: symbolic links differ:',
  1.1006 +                      $filePPC->path(),
  1.1007 +                      $fileX86->path());
  1.1008 +    }
  1.1009 +
  1.1010 +    # $aLink here serves the same purpose as $aSourceFile in the enclosing
  1.1011 +    # block: it refers to the target of the symbolic link, whether there
  1.1012 +    # is one valid source or two.
  1.1013 +    my ($aLink);
  1.1014 +    if (defined($linkPPC)) {
  1.1015 +      $aLink = $linkPPC;
  1.1016 +    }
  1.1017 +    else {
  1.1018 +      $aLink = $linkX86;
  1.1019 +    }
  1.1020 +
  1.1021 +    if ($gVerbosity >= 3 || $gDryRun) {
  1.1022 +      print('ln -s '.
  1.1023 +            join(' ',argumentEscape($aLink, $fileTargetPath))."\n");
  1.1024 +    }
  1.1025 +    if (!$gDryRun && !symlink($aLink, $fileTargetPath)) {
  1.1026 +      return complain(1, 'makeUniversal: symlink: '.$!.' for:',
  1.1027 +                      $aLink,
  1.1028 +                      $fileTargetPath);
  1.1029 +    }
  1.1030 +
  1.1031 +    return 1;
  1.1032 +  }
  1.1033 +  elsif($aSourceFile->lIsRegularFile()) {
  1.1034 +    my ($machPPC, $machX86, $fileName);
  1.1035 +    if (!defined($filePPC) || !defined($fileX86)) {
  1.1036 +      # One of the source files isn't present.  The right thing to do is
  1.1037 +      # to just copy what does exist straight over, so skip Mach-O checks.
  1.1038 +      $machPPC = 0;
  1.1039 +      $machX86 = 0;
  1.1040 +      if (defined($filePPC)) {
  1.1041 +        $fileName = $filePPC;
  1.1042 +      } elsif (defined($fileX86)) {
  1.1043 +        $fileName = $fileX86;
  1.1044 +      } else {
  1.1045 +        complain(1, "The file must exist in at least one directory");
  1.1046 +        exit(1);
  1.1047 +      }
  1.1048 +    }
  1.1049 +    else {
  1.1050 +      # both files exist, pick the name of one.
  1.1051 +      $fileName = $fileX86;
  1.1052 +      if (!defined($machPPC=$filePPC->isMachO())) {
  1.1053 +        return complain(1, 'makeUniversal: isFileMachO ppc failed for:',
  1.1054 +                        $filePPC->path());
  1.1055 +      }
  1.1056 +      if (!defined($machX86=$fileX86->isMachO())) {
  1.1057 +        return complain(1, 'makeUniversal: isFileMachO x86 failed for:',
  1.1058 +                        $fileX86->path());
  1.1059 +      }
  1.1060 +    }
  1.1061 +
  1.1062 +    if ($machPPC != $machX86) {
  1.1063 +      return complain(1, 'makeUniversal: variant Mach-O attributes:',
  1.1064 +                      $filePPC->path(),
  1.1065 +                  $fileX86->path());
  1.1066 +    }
  1.1067 +
  1.1068 +    if ($machPPC) {
  1.1069 +      # makeUniversalFile will print an error if it fails.
  1.1070 +      return makeUniversalFile($filePPC, $fileX86, $fileTargetPath);
  1.1071 +    }
  1.1072 +
  1.1073 +    if (grep { $fileName->path() =~ m/$_/; } @gSortMatches) {
  1.1074 +      # Regular files, but should be compared with sorting first.
  1.1075 +      # copyIfIdenticalWhenSorted will print an error if it fails.
  1.1076 +      return copyIfIdenticalWhenSorted($filePPC, $fileX86, $fileTargetPath);
  1.1077 +    }
  1.1078 +
  1.1079 +    # Regular file.  copyIfIdentical will print an error if it fails.
  1.1080 +    return copyIfIdentical($filePPC, $fileX86, $fileTargetPath);
  1.1081 +  }
  1.1082 +
  1.1083 +  # Special file, don't know how to handle.
  1.1084 +  return complain(1, 'makeUniversal: cannot handle special file:',
  1.1085 +                  $filePPC->path(),
  1.1086 +                  $fileX86->path());
  1.1087 +}
  1.1088 +
  1.1089 +# usage()
  1.1090 +#
  1.1091 +# Give the user a hand.
  1.1092 +sub usage() {
  1.1093 +  print STDERR (
  1.1094 +"usage: unify <ppc-path> <x86-path> <universal-path>\n".
  1.1095 +"            [--dry-run]           (print what would be done)\n".
  1.1096 +"            [--only-one <action>] (skip, copy, fail; default=copy)\n".
  1.1097 +"            [--verbosity <level>] (0, 1, 2, 3; default=2)\n");
  1.1098 +  return;
  1.1099 +}
  1.1100 +
  1.1101 +# readZipCRCs($zipFile)
  1.1102 +#
  1.1103 +# $zipFile is the pathname to a zip file whose directory will be read.
  1.1104 +# A reference to a hash is returned, with the member pathnames from the
  1.1105 +# zip file as keys, and reasonably unique identifiers as values.  The
  1.1106 +# format of the values is not specified exactly, but does include the
  1.1107 +# member CRCs and sizes and differentiates between files and directories.
  1.1108 +# It specifically does not distinguish between modification times.  On
  1.1109 +# failure, prints a message and returns undef.
  1.1110 +sub readZipCRCs($) {
  1.1111 +  my ($zipFile);
  1.1112 +  ($zipFile) = @_;
  1.1113 +
  1.1114 +  my ($ze, $zip);
  1.1115 +  $zip = Archive::Zip->new();
  1.1116 +
  1.1117 +  if (($ze = $zip->read($zipFile)) != AZ_OK) {
  1.1118 +    complain(1, 'readZipCRCs: read error '.$ze.' for:',
  1.1119 +             $zipFile);
  1.1120 +    return undef;
  1.1121 +  }
  1.1122 +
  1.1123 +  my ($member, %memberCRCs, @memberList);
  1.1124 +  %memberCRCs = ();
  1.1125 +  @memberList = $zip->members();
  1.1126 +
  1.1127 +  foreach $member (@memberList) {
  1.1128 +    # Take a few of the attributes that identify the file and stuff them into
  1.1129 +    # the members hash.  Directories will show up with size 0 and crc32 0,
  1.1130 +    # so isDirectory() is used to distinguish them from empty files.
  1.1131 +    $memberCRCs{$member->fileName()} = join(',', $member->isDirectory() ? 1 : 0,
  1.1132 +                                                 $member->uncompressedSize(),
  1.1133 +                                                 $member->crc32String());
  1.1134 +  }
  1.1135 +
  1.1136 +  return {%memberCRCs};
  1.1137 +}
  1.1138 +
  1.1139 +{
  1.1140 +  # FileAttrCache allows various attributes about a file to be cached
  1.1141 +  # so that if they are needed again after first use, no system calls
  1.1142 +  # will be made and the program won't need to hit the disk.
  1.1143 +
  1.1144 +  package FileAttrCache;
  1.1145 +
  1.1146 +  # from /usr/include/mach-o/loader.h
  1.1147 +  use constant MH_MAGIC    => 0xfeedface;
  1.1148 +  use constant MH_CIGAM    => 0xcefaedfe;
  1.1149 +  use constant MH_MAGIC_64 => 0xfeedfacf;
  1.1150 +  use constant MH_CIGAM_64 => 0xcffaedfe;
  1.1151 +
  1.1152 +  use Fcntl(':DEFAULT', ':mode');
  1.1153 +
  1.1154 +  # FileAttrCache->new($path)
  1.1155 +  #
  1.1156 +  # Creates a new FileAttrCache object for the file at path $path and
  1.1157 +  # returns it.  The cache is not primed at creation time, values are
  1.1158 +  # fetched lazily as they are needed.
  1.1159 +  sub new($$) {
  1.1160 +    my ($class, $path, $proto, $this);
  1.1161 +    ($proto, $path) = @_;
  1.1162 +    if (!($class = ref($proto))) {
  1.1163 +      $class = $proto;
  1.1164 +    }
  1.1165 +    $this = {
  1.1166 +      'path'        => $path,
  1.1167 +      'lstat'       => undef,
  1.1168 +      'lstatErrno'  => 0,
  1.1169 +      'lstatInit'   => 0,
  1.1170 +      'magic'       => undef,
  1.1171 +      'magic2'       => undef,
  1.1172 +      'magicErrno'  => 0,
  1.1173 +      'magicErrMsg' => undef,
  1.1174 +      'magicInit'   => 0,
  1.1175 +      'stat'        => undef,
  1.1176 +      'statErrno'   => 0,
  1.1177 +      'statInit'    => 0,
  1.1178 +    };
  1.1179 +    bless($this, $class);
  1.1180 +    return($this);
  1.1181 +  }
  1.1182 +
  1.1183 +  # $FileAttrCache->isFat()
  1.1184 +  #
  1.1185 +  # Returns true if the file is a fat Mach-O file, false if it's not, and
  1.1186 +  # undef if an error occurs.  See /usr/include/mach-o/fat.h.
  1.1187 +  sub isFat($) {
  1.1188 +    my ($magic, $magic2, $this);
  1.1189 +    ($this) = @_;
  1.1190 +
  1.1191 +    # magic() caches, there's no separate cache because isFat() doesn't hit
  1.1192 +    # the disk other than by calling magic().
  1.1193 +
  1.1194 +    if (!defined($magic = $this->magic())) {
  1.1195 +      return undef;
  1.1196 +    }
  1.1197 +    $magic2 = $this->magic2();
  1.1198 +
  1.1199 +    # We have to sanity check the second four bytes, because Java class
  1.1200 +    # files use the same magic number as Mach-O fat binaries.
  1.1201 +    # This logic is adapted from file(1), which says that Mach-O uses
  1.1202 +    # these bytes to count the number of architectures within, while
  1.1203 +    # Java uses it for a version number. Conveniently, there are only
  1.1204 +    # 18 labelled Mach-O architectures, and Java's first released
  1.1205 +    # class format used the version 43.0.
  1.1206 +    if ($magic == 0xcafebabe && $magic2 < 20) {
  1.1207 +      return 1;
  1.1208 +    }
  1.1209 +
  1.1210 +    return 0;
  1.1211 +  }
  1.1212 +
  1.1213 +  # $FileAttrCache->isMachO()
  1.1214 +  #
  1.1215 +  # Returns true if the file is a Mach-O image (including a fat file), false
  1.1216 +  # if it's not, and undef if an error occurs.  See
  1.1217 +  # /usr/include/mach-o/loader.h and /usr/include/mach-o/fat.h.
  1.1218 +  sub isMachO($) {
  1.1219 +    my ($magic, $this);
  1.1220 +    ($this) = @_;
  1.1221 +
  1.1222 +    # magic() caches, there's no separate cache because isMachO() doesn't hit
  1.1223 +    # the disk other than by calling magic().
  1.1224 +
  1.1225 +    if (!defined($magic = $this->magic())) {
  1.1226 +      return undef;
  1.1227 +    }
  1.1228 +
  1.1229 +    # Accept Mach-O fat files or Mach-O thin files of either endianness.
  1.1230 +    if ($magic == MH_MAGIC ||
  1.1231 +        $magic == MH_CIGAM ||
  1.1232 +        $magic == MH_MAGIC_64 ||
  1.1233 +        $magic == MH_CIGAM_64 ||
  1.1234 +        $this->isFat()) {
  1.1235 +      return 1;
  1.1236 +    }
  1.1237 +
  1.1238 +    return 0;
  1.1239 +  }
  1.1240 +
  1.1241 +  # $FileAttrCache->isZip()
  1.1242 +  #
  1.1243 +  # Returns true if the file is a zip file, false if it's not, and undef if
  1.1244 +  # an error occurs.  See http://www.pkware.com/business_and_developers/developer/popups/appnote.txt .
  1.1245 +  sub isZip($) {
  1.1246 +    my ($magic, $this);
  1.1247 +    ($this) = @_;
  1.1248 +
  1.1249 +    # magic() caches, there's no separate cache because isFat() doesn't hit
  1.1250 +    # the disk other than by calling magic().
  1.1251 +
  1.1252 +    if (!defined($magic = $this->magic())) {
  1.1253 +      return undef;
  1.1254 +    }
  1.1255 +
  1.1256 +    if ($magic == 0x504b0304) {
  1.1257 +      return 1;
  1.1258 +    }
  1.1259 +
  1.1260 +    return 0;
  1.1261 +  }
  1.1262 +
  1.1263 +  # $FileAttrCache->lIsExecutable()
  1.1264 +  #
  1.1265 +  # Wraps $FileAttrCache->lstat(), returning true if the file is has any,
  1.1266 +  # execute bit set, false if none are set, or undef if an error occurs.
  1.1267 +  # On error, $! is set to lstat's errno.
  1.1268 +  sub lIsExecutable($) {
  1.1269 +    my ($mode, $this);
  1.1270 +    ($this) = @_;
  1.1271 +
  1.1272 +    if (!defined($mode = $this->lstatMode())) {
  1.1273 +      return undef;
  1.1274 +    }
  1.1275 +
  1.1276 +    return $mode & (S_IXUSR | S_IXGRP | S_IXOTH);
  1.1277 +  }
  1.1278 +
  1.1279 +  # $FileAttrCache->lIsDir()
  1.1280 +  #
  1.1281 +  # Wraps $FileAttrCache->lstat(), returning true if the file is a directory,
  1.1282 +  # false if it isn't, or undef if an error occurs.  Because lstat is used,
  1.1283 +  # this will return false even if the file is a symlink pointing to a
  1.1284 +  # directory.  On error, $! is set to lstat's errno.
  1.1285 +  sub lIsDir($) {
  1.1286 +    my ($type, $this);
  1.1287 +    ($this) = @_;
  1.1288 +
  1.1289 +    if (!defined($type = $this->lstatType())) {
  1.1290 +      return undef;
  1.1291 +    }
  1.1292 +
  1.1293 +    return S_ISDIR($type);
  1.1294 +  }
  1.1295 +
  1.1296 +  # $FileAttrCache->lIsRegularFile()
  1.1297 +  #
  1.1298 +  # Wraps $FileAttrCache->lstat(), returning true if the file is a regular,
  1.1299 +  # file, false if it isn't, or undef if an error occurs.  Because lstat is
  1.1300 +  # used, this will return false even if the file is a symlink pointing to a
  1.1301 +  # regular file.  On error, $! is set to lstat's errno.
  1.1302 +  sub lIsRegularFile($) {
  1.1303 +    my ($type, $this);
  1.1304 +    ($this) = @_;
  1.1305 +
  1.1306 +    if (!defined($type = $this->lstatType())) {
  1.1307 +      return undef;
  1.1308 +    }
  1.1309 +
  1.1310 +    return S_ISREG($type);
  1.1311 +  }
  1.1312 +
  1.1313 +  # $FileAttrCache->lIsSymLink()
  1.1314 +  #
  1.1315 +  # Wraps $FileAttrCache->lstat(), returning true if the file is a symbolic,
  1.1316 +  # link, false if it isn't, or undef if an error occurs.  On error, $! is
  1.1317 +  # set to lstat's errno.
  1.1318 +  sub lIsSymLink($) {
  1.1319 +    my ($type, $this);
  1.1320 +    ($this) = @_;
  1.1321 +   
  1.1322 +    if (!defined($type = $this->lstatType())) {
  1.1323 +      return undef;
  1.1324 +    }
  1.1325 +
  1.1326 +    return S_ISLNK($type);
  1.1327 +  }
  1.1328 +
  1.1329 +  # $FileAttrCache->lstat()
  1.1330 +  #
  1.1331 +  # Wraps the lstat system call, providing a cache to speed up multiple
  1.1332 +  # lstat calls for the same file.  See lstat(2) and lstat in perlfunc(1).
  1.1333 +  sub lstat($) {
  1.1334 +    my (@stat, $this);
  1.1335 +    ($this) = @_;
  1.1336 +
  1.1337 +    # Use the cached lstat result.
  1.1338 +    if ($$this{'lstatInit'}) {
  1.1339 +      if (defined($$this{'lstatErrno'})) {
  1.1340 +        $! = $$this{'lstatErrno'};
  1.1341 +      }
  1.1342 +      return @{$$this{'lstat'}};
  1.1343 +    }
  1.1344 +    $$this{'lstatInit'} = 1;
  1.1345 +
  1.1346 +    if (!(@stat = CORE::lstat($$this{'path'}))) {
  1.1347 +      $$this{'lstatErrno'} = $!;
  1.1348 +    }
  1.1349 +
  1.1350 +    $$this{'lstat'} = [@stat];
  1.1351 +    return @stat;
  1.1352 +  }
  1.1353 +
  1.1354 +  # $FileAttrCache->lstatMode()
  1.1355 +  #
  1.1356 +  # Wraps $FileAttrCache->lstat(), returning the mode bits from the st_mode
  1.1357 +  # field, or undef if an error occurs.  On error, $! is set to lstat's
  1.1358 +  # errno.
  1.1359 +  sub lstatMode($) {
  1.1360 +    my (@stat, $this);
  1.1361 +    ($this) = @_;
  1.1362 +
  1.1363 +    if (!(@stat = $this->lstat())) {
  1.1364 +      return undef;
  1.1365 +    }
  1.1366 +
  1.1367 +    return S_IMODE($stat[2]);
  1.1368 +  }
  1.1369 +
  1.1370 +  # $FileAttrCache->lstatType()
  1.1371 +  #
  1.1372 +  # Wraps $FileAttrCache->lstat(), returning the type bits from the st_mode
  1.1373 +  # field, or undef if an error occurs.  On error, $! is set to lstat's
  1.1374 +  # errno.
  1.1375 +  sub lstatType($) {
  1.1376 +    my (@stat, $this);
  1.1377 +    ($this) = @_;
  1.1378 +
  1.1379 +    if (!(@stat = $this->lstat())) {
  1.1380 +      return undef;
  1.1381 +    }
  1.1382 +
  1.1383 +    return S_IFMT($stat[2]);
  1.1384 +  }
  1.1385 +
  1.1386 +  # $FileAttrCache->magic()
  1.1387 +  #
  1.1388 +  # Returns the "magic number" for the file by reading its first four bytes
  1.1389 +  # as a big-endian unsigned 32-bit integer and returning the result.  If an
  1.1390 +  # error occurs, returns undef and prints diagnostic messages to stderr.  If
  1.1391 +  # the file is shorter than 32 bits, returns -1.  A cache is provided to
  1.1392 +  # speed multiple magic calls for the same file.
  1.1393 +  sub magic($) {
  1.1394 +    my ($this);
  1.1395 +    ($this) = @_;
  1.1396 +
  1.1397 +    # Use the cached magic result.
  1.1398 +    if ($$this{'magicInit'}) {
  1.1399 +      if (defined($$this{'magicErrno'})) {
  1.1400 +        if (defined($$this{'magicErrMsg'})) {
  1.1401 +          complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
  1.1402 +                   $$this{'path'});
  1.1403 +        }
  1.1404 +        $! = $$this{'magicErrno'};
  1.1405 +      }
  1.1406 +      return $$this{'magic'};
  1.1407 +    }
  1.1408 +
  1.1409 +    $$this{'magicInit'} = 1;
  1.1410 +
  1.1411 +    my ($fh);
  1.1412 +    if (!sysopen($fh, $$this{'path'}, O_RDONLY)) {
  1.1413 +      $$this{'magicErrno'} = $!;
  1.1414 +      $$this{'magicErrMsg'} = 'open "'.$$this{'path'}.'": '.$!;
  1.1415 +      complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
  1.1416 +               $$this{'path'});
  1.1417 +      return undef;
  1.1418 +    }
  1.1419 +
  1.1420 +    $! = 0;
  1.1421 +    my ($bytes, $magic, $bytes2, $magic2);
  1.1422 +    if (!defined($bytes = sysread($fh, $magic, 4))) {
  1.1423 +      $$this{'magicErrno'} = $!;
  1.1424 +      $$this{'magicErrMsg'} = 'read "'.$$this{'path'}.'": '.$!;
  1.1425 +      complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
  1.1426 +               $$this{'path'});
  1.1427 +      close($fh);
  1.1428 +      return undef;
  1.1429 +    }
  1.1430 +    else {
  1.1431 +      $bytes2 = sysread($fh, $magic2, 4);
  1.1432 +    }
  1.1433 +
  1.1434 +    close($fh);
  1.1435 +
  1.1436 +    if ($bytes != 4) {
  1.1437 +      # The file is too short, didn't read a magic number.  This isn't really
  1.1438 +      # an error.  Return an unlikely value.
  1.1439 +      $$this{'magic'} = -1;
  1.1440 +      $$this{'magic2'} = -1;
  1.1441 +      return -1;
  1.1442 +    }
  1.1443 +    if ($bytes2 != 4) {
  1.1444 +      # File is too short to read a second 4 bytes.
  1.1445 +      $magic2 = -1;
  1.1446 +    }
  1.1447 +
  1.1448 +    $$this{'magic'} = unpack('N', $magic);
  1.1449 +    $$this{'magic2'} = unpack('N', $magic2);
  1.1450 +    return $$this{'magic'};
  1.1451 +  }
  1.1452 +
  1.1453 +  # $FileAttrCache->magic2()
  1.1454 +  #
  1.1455 +  # Returns the second four bytes of the file as a 32-bit little endian number.
  1.1456 +  # See magic(), above for more info.
  1.1457 +  sub magic2($) {
  1.1458 +    my ($this);
  1.1459 +    ($this) = @_;
  1.1460 +
  1.1461 +    # we do the actual work (and cache it) in magic().
  1.1462 +    if (!$$this{'magicInit'}) {
  1.1463 +      my $magic = $$this->magic();
  1.1464 +    }
  1.1465 +
  1.1466 +    return $$this{'magic2'};
  1.1467 +  }
  1.1468 +
  1.1469 +  # $FileAttrCache->path()
  1.1470 +  #
  1.1471 +  # Returns the file's pathname.
  1.1472 +  sub path($) {
  1.1473 +    my ($this);
  1.1474 +    ($this) = @_;
  1.1475 +    return $$this{'path'};
  1.1476 +  }
  1.1477 +
  1.1478 +  # $FileAttrCache->stat()
  1.1479 +  #
  1.1480 +  # Wraps the stat system call, providing a cache to speed up multiple
  1.1481 +  # stat calls for the same file.  If lstat() has already been called and
  1.1482 +  # the file is not a symbolic link, the cached lstat() result will be used.
  1.1483 +  # See stat(2) and lstat in perlfunc(1).
  1.1484 +  sub stat($) {
  1.1485 +    my (@stat, $this);
  1.1486 +    ($this) = @_;
  1.1487 +
  1.1488 +    # Use the cached stat result.
  1.1489 +    if ($$this{'statInit'}) {
  1.1490 +      if (defined($$this{'statErrno'})) {
  1.1491 +        $! = $$this{'statErrno'};
  1.1492 +      }
  1.1493 +      return @{$$this{'stat'}};
  1.1494 +    }
  1.1495 +
  1.1496 +    $$this{'statInit'} = 1;
  1.1497 +
  1.1498 +    # If lstat has already been called, and the file isn't a symbolic link,
  1.1499 +    # use the cached lstat result.
  1.1500 +    if ($$this{'lstatInit'} && !$$this{'lstatErrno'} &&
  1.1501 +        !S_ISLNK(${$$this{'lstat'}}[2])) {
  1.1502 +      $$this{'stat'} = $$this{'lstat'};
  1.1503 +      return @{$$this{'stat'}};
  1.1504 +    }
  1.1505 +
  1.1506 +    if (!(@stat = CORE::stat($$this{'path'}))) {
  1.1507 +      $$this{'statErrno'} = $!;
  1.1508 +    }
  1.1509 +
  1.1510 +    $$this{'stat'} = [@stat];
  1.1511 +    return @stat;
  1.1512 +  }
  1.1513 +
  1.1514 +  # $FileAttrCache->statSize()
  1.1515 +  #
  1.1516 +  # Wraps $FileAttrCache->stat(), returning the st_size field, or undef
  1.1517 +  # undef if an error occurs.  On error, $! is set to stat's errno.
  1.1518 +  sub statSize($) {
  1.1519 +    my (@stat, $this);
  1.1520 +    ($this) = @_;
  1.1521 +
  1.1522 +    if (!(@stat = $this->lstat())) {
  1.1523 +      return undef;
  1.1524 +    }
  1.1525 +
  1.1526 +    return $stat[7];
  1.1527 +  }
  1.1528 +}

mercurial