2
# This Source Code Form is subject to the terms of the Mozilla Public
3
# License, v. 2.0. If a copy of the MPL was not distributed with this
4
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
13
B<unify> - Mac OS X universal binary packager
22
[B<--only-one> I<action>]
23
[B<--verbosity> I<level>]
24
[B<--unify-with-sort> I<regex>]
28
I<unify> merges any two architecture-specific files or directory trees
29
into a single file or tree suitable for use on either architecture as a
30
"fat" or "universal binary."
32
Architecture-specific Mach-O files will be merged into fat Mach-O files
33
using L<lipo(1)>. Non-Mach-O files in the architecture-specific trees
34
are compared to ensure that they are equivalent before copying. Symbolic
35
links are permitted in the architecture-specific trees and will cause
36
identical links to be created in the merged tree, provided that the source
37
links have identical targets. Directories are processed recursively.
39
If the architecture-specific source trees contain zip archives (including
40
jar files) that are not identical according to a byte-for-byte check, they
41
are still assumed to be equivalent if both archives contain exactly the
42
same members with identical checksums and sizes.
44
Behavior when one architecture-specific tree contains files that the other
45
does not is controlled by the B<--only-one> option.
47
If Mach-O files cannot be merged using L<lipo(1)>, zip archives are not
48
equivalent, regular files are not identical, or any other error occurs,
49
B<unify> will fail with an exit status of 1. Diagnostic messages are
50
typically printed to stderr; this behavior can be controlled with the
51
B<--verbosity> option.
61
The paths to directory trees containing PowerPC and x86 builds,
62
respectively. I<ppc-path> and I<x86-path> are permitted to contain files
63
that are already "fat," and only the appropriate architecture's images will
66
I<ppc-path> and I<x86-path> are also permitted to both be files, in which
67
case B<unify> operates solely on those files, and produces an appropriate
68
merged file at I<target-path>.
72
The path to the merged file or directory tree. This path will be created,
73
and it must not exist prior to running B<unify>.
77
When specified, the commands that would be executed are printed, without
78
actually executing them. Note that B<--dry-run> and the equivalent
79
B<--verbosity> level during "wet" runs may print equivalent commands when
80
no commands are in fact executed: certain operations are handled internally
81
within B<unify>, and an approximation of a command that performs a similar
84
=item B<--only-one> I<action>
86
Controls handling of files that are only present in one of the two source
87
trees. I<action> may be:
88
skip - These files are skipped.
89
copy - These files are copied from the tree in which they exist.
90
fail - When this condition occurs, it is treated as an error.
92
The default I<action> is copy.
94
=item B<--verbosity> I<level>
96
Adjusts the level of loudness of B<unify>. The possible values for
98
0 - B<unify> never prints anything.
99
(Other programs that B<unify> calls may still print messages.)
100
1 - Fatal error messages are printed to stderr.
101
2 - Nonfatal warnings are printed to stderr.
102
3 - Commands are printed to stdout as they are executed.
104
The default I<level> is 2.
106
=item B<--unify-with-sort> I<regex>
108
Allows merging files matching I<regex> that differ only by the ordering
109
of the lines contained within them. The unified file will have its contents
110
sorted. This option may be given multiple times to specify multiple
111
regexes for matching files.
119
=item Create a universal .app bundle from two architecture-specific .app
122
unify --only-one copy ppc/dist/firefox/Firefox.app
123
x86/dist/firefox/Firefox.app universal/Firefox.app
126
=item Merge two identical architecture-specific trees:
128
unify --only-one fail /usr/local /nfs/x86/usr/local
135
The only esoteric requirement of B<unify> is that the L<lipo(1)> command
136
be available. It is present on Mac OS X systems at least as early as
137
10.3.9, and probably earlier. Mac OS X 10.4 ("Tiger") or later are
142
MPL 1.1/GPL 2.0/LGPL 2.1. Your choice
146
The software was initially written by Mark Mentovai; copyright 2006
151
L<cmp(1)>, L<ditto(1)>, L<lipo(1)>
155
use Archive::Zip(':ERROR_CODES');
162
my (%gConfig, $gDryRun, $gOnlyOne, $gVerbosity, @gSortMatches);
164
sub argumentEscape(@);
166
sub compareZipArchives($$);
168
sub copyIfIdentical($$$);
171
sub compare_sorted($$);
172
sub copyIfIdenticalWhenSorted($$$);
173
sub createUniqueFile($$);
174
sub makeUniversal($$$);
175
sub makeUniversalDirectory($$$);
176
sub makeUniversalInternal($$$$);
177
sub makeUniversalFile($$$);
182
package FileAttrCache;
190
sub lIsExecutable($);
191
sub lIsRegularFile($);
204
'cmd_lipo' => 'lipo',
213
Getopt::Long::Configure('pass_through');
214
GetOptions('dry-run' => \$gDryRun,
215
'only-one=s' => \$gOnlyOne,
216
'verbosity=i' => \$gVerbosity,
217
'unify-with-sort=s' => \@gSortMatches,
218
'config=s' => \%gConfig); # "hidden" option not in usage()
220
if (scalar(@ARGV) != 3 || $gVerbosity < 0 || $gVerbosity > 3 ||
221
($gOnlyOne ne 'skip' && $gOnlyOne ne 'copy' && $gOnlyOne ne 'fail')) {
226
if (!makeUniversal($ARGV[0],$ARGV[1],$ARGV[2])) {
227
# makeUniversal or something it called will have printed an error.
233
# argumentEscape(@arguments)
235
# Takes a list of @arguments and makes them shell-safe.
236
sub argumentEscape(@) {
240
my ($argument, @argumentsOut);
241
foreach $argument (@arguments) {
242
$argument =~ s%([^A-Za-z0-9_\-/.=+,])%\\$1%g;
243
push(@argumentsOut, $argument);
246
return @argumentsOut;
249
# command(@arguments)
251
# Runs the specified command by calling system(@arguments). If $gDryRun
252
# is true, the command is printed but not executed, and 0 is returned.
253
# if $gVerbosity is greater than 1, the command is printed before being
254
# executed. When the command is executed, the system() return value will
255
# be returned. stdout and stderr are left connected for command output.
259
if ($gVerbosity >= 3 || $gDryRun) {
260
print(join(' ', argumentEscape(@arguments))."\n");
265
return system(@arguments);
268
# compareZipArchives($zip1, $zip2)
270
# Given two pathnames to zip archives, determines whether or not they are
271
# functionally identical. Returns true if they are, false if they differ in
272
# some substantial way, and undef if an error occurs. If the zip files
273
# differ, diagnostic messages are printed indicating how they differ.
275
# Zip files will differ if any of the members are different as defined by
276
# readZipCRCs, which consider CRCs, sizes, and file types as stored in the
277
# file header. Timestamps are not considered. Zip files also differ if one
278
# file contains members that the other one does not. $gOnlyOne has no
279
# effect on this behavior.
280
sub compareZipArchives($$) {
284
my ($CRCHash1, $CRCHash2);
285
if (!defined($CRCHash1 = readZipCRCs($zip1))) {
286
# readZipCRCs printed an error.
289
if (!defined($CRCHash2 = readZipCRCs($zip2))) {
290
# readZipCRCs printed an error.
294
my (@diffCRCs, @onlyInZip1);
299
foreach $memberName (keys(%$CRCHash1)) {
300
if (!exists($$CRCHash2{$memberName})) {
301
# The member is present in $zip1 but not $zip2.
302
push(@onlyInZip1, $memberName);
304
elsif ($$CRCHash1{$memberName} ne $$CRCHash2{$memberName}) {
305
# The member is present in both archives but its CRC or some other
306
# other critical attribute isn't identical.
307
push(@diffCRCs, $memberName);
309
delete($$CRCHash2{$memberName});
312
# If any members remain in %CRCHash2, it's because they're not present
315
@onlyInZip2 = keys(%$CRCHash2);
317
if (scalar(@onlyInZip1) + scalar(@onlyInZip2) + scalar(@diffCRCs)) {
318
complain(1, 'compareZipArchives: zip archives differ:',
321
if (scalar(@onlyInZip1)) {
322
complain(1, 'compareZipArchives: members only in former:',
325
if (scalar(@onlyInZip2)) {
326
complain(1, 'compareZipArchives: members only in latter:',
329
if (scalar(@diffCRCs)) {
330
complain(1, 'compareZipArchives: members differ:',
339
# complain($severity, $message, @list)
341
# Prints $message to stderr if $gVerbosity allows it for severity level
342
# $severity. @list is a list of words that will be shell-escaped and printed
343
# after $message, one per line, intended to be used, for example, to list
344
# arguments to a call that failed.
346
# Expected severity levels are 1 for hard errors and 2 for non-fatal warnings.
348
# Always returns false as a convenience, so callers can return complain's
349
# return value when it is used to signal errors.
351
my ($severity, $message, @list);
352
($severity, $message, @list) = @_;
354
if ($gVerbosity >= $severity) {
355
print STDERR ($0.': '.$message."\n");
358
while ($item = shift(@list)) {
359
print STDERR (' '.(argumentEscape($item))[0].
360
(scalar(@list)?',':'')."\n");
367
# copyIfIdentical($source1, $source2, $target)
369
# $source1 and $source2 are FileAttrCache objects that are compared, and if
370
# identical, copied to path string $target. The comparison is initially
371
# done as a byte-for-byte comparison, but if the files differ and appear to
372
# be zip archives, compareZipArchives is called to determine whether
373
# files that are not byte-for-byte identical are equivalent archives.
375
# Returns true on success, false for files that are not identical or
376
# equivalent archives, and undef if an error occurs.
378
# One of $source1 and $source2 is permitted to be undef. In this event,
379
# whichever source is defined is copied directly to $target without performing
380
# any comparisons. This enables the $gOnlyOne = 'copy' mode, which is
381
# driven by makeUniversalDirectory and makeUniversalInternal.
382
sub copyIfIdentical($$$) {
383
my ($source1, $source2, $target);
384
($source1, $source2, $target) = @_;
386
if (!defined($source1)) {
387
# If there's only one source file, make it the first file. Order
388
# isn't important here, and this makes it possible to use
389
# defined($source2) as the switch, and to always copy from $source1.
394
if (defined($source2)) {
395
# Only do the comparisons if there are two source files. If there's
396
# only one source file, skip the comparisons and go straight to the
398
if ($gVerbosity >= 3 || $gDryRun) {
400
join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
403
if (!defined($comparison = compare($source1->path(), $source2->path())) ||
405
return complain(1, 'copyIfIdentical: compare: '.$!.' while comparing:',
409
elsif ($comparison != 0) {
411
if (defined($zip1 = $source1->isZip()) &&
412
defined($zip2 = $source2->isZip()) &&
415
if (!defined($zipComparison = compareZipArchives($source1->path(),
418
# An error occurred or the zip files aren't sufficiently identical.
419
# compareZipArchives will have printed an error message.
422
# The zip files were compared successfully, and they both contain
423
# all of the same members, and all of their members' CRCs are
424
# identical. For the purposes of this script, the zip files can be
425
# treated as identical, so reset $comparison.
429
if ($comparison != 0) {
430
return complain(1, 'copyIfIdentical: files differ:',
436
if ($gVerbosity >= 3 || $gDryRun) {
438
join(' ',argumentEscape($source1->path(), $target))."\n");
444
# Set the execute bits (as allowed by the umask) on the new file if any
445
# execute bit is set on either old file.
446
$isExecutable = $source1->lIsExecutable() ||
447
(defined($source2) && $source2->lIsExecutable());
449
if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
450
# createUniqueFile printed an error.
454
if (!copy($source1->path(), $target)) {
455
complain(1, 'copyIfIdentical: copy: '.$!.' while copying',
468
# Read the contents of $file into an array and return it.
469
# Returns undef on error.
472
open FILE, $file or return undef;
479
# Get the sorted lines of a file as a list, normalizing a newline on the last line if necessary.
482
my @lines = slurp($file);
483
my $lastline = $lines[-1];
484
if (!($lastline =~ /\n/)) {
485
$lines[-1] = $lastline . "\n";
490
# compare_sorted($file1, $file2)
492
# Read the contents of both files into arrays, sort the arrays,
493
# and then compare the two arrays for equality.
495
# Returns 0 if the sorted array contents are equal, or 1 if not.
496
# Returns undef on error.
497
sub compare_sorted($$) {
498
my ($file1, $file2) = @_;
499
my @lines1 = get_sorted($file1);
500
my @lines2 = get_sorted($file2);
502
return undef if !@lines1 || !@lines2;
503
return 1 unless scalar @lines1 == scalar @lines2;
505
for (my $i = 0; $i < scalar @lines1; $i++) {
506
return 1 if $lines1[$i] ne $lines2[$i];
511
# copyIfIdenticalWhenSorted($source1, $source2, $target)
513
# $source1 and $source2 are FileAttrCache objects that are compared, and if
514
# identical, copied to path string $target. The comparison is done by
515
# sorting the individual lines within the two files and comparing the results.
517
# Returns true on success, false for files that are not equivalent,
518
# and undef if an error occurs.
519
sub copyIfIdenticalWhenSorted($$$) {
520
my ($source1, $source2, $target);
521
($source1, $source2, $target) = @_;
523
if ($gVerbosity >= 3 || $gDryRun) {
525
join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
528
if (!defined($comparison = compare_sorted($source1->path(),
529
$source2->path())) ||
531
return complain(1, 'copyIfIdenticalWhenSorted: compare: '.$!
532
.' while comparing:',
536
if ($comparison != 0) {
537
return complain(1, 'copyIfIdenticalWhenSorted: files differ:',
542
if ($gVerbosity >= 3 || $gDryRun) {
544
join(' ',argumentEscape($source1->path(), $target))."\n");
550
# Set the execute bits (as allowed by the umask) on the new file if any
551
# execute bit is set on either old file.
552
$isExecutable = $source1->lIsExecutable() ||
553
(defined($source2) && $source2->lIsExecutable());
555
if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
556
# createUniqueFile printed an error.
560
if (!copy($source1->path(), $target)) {
561
complain(1, 'copyIfIdenticalWhenSorted: copy: '.$!
573
# createUniqueFile($path, $mode)
575
# Creates a new plain empty file at pathname $path, provided it does not
576
# yet exist. $mode is used as the file mode. The actual file's mode will
577
# be modified by the effective umask. Returns false if the file could
578
# not be created, setting $! to the error. An error message is printed
579
# in the event of failure.
580
sub createUniqueFile($$) {
585
if (!sysopen($fh, $path, O_WRONLY | O_CREAT | O_EXCL, $mode)) {
586
return complain(1, 'createUniqueFile: open: '.$!.' for:',
594
# makeUniversal($pathPPC, $pathX86, $pathTarget)
596
# The top-level call. $pathPPC, $pathX86, and $pathTarget are strings
597
# identifying the ppc and x86 files or directories to merge and the location
598
# to merge them to. Returns false on failure and true on success.
599
sub makeUniversal($$$) {
600
my ($pathTarget, $pathPPC, $pathX86);
601
($pathPPC, $pathX86, $pathTarget) = @_;
603
my ($filePPC, $fileX86);
604
$filePPC = FileAttrCache->new($pathPPC);
605
$fileX86 = FileAttrCache->new($pathX86);
607
return makeUniversalInternal(1, $filePPC, $fileX86, $pathTarget);
610
# makeUniversalDirectory($dirPPC, $dirX86, $dirTarget)
612
# This is part of the heart of recursion. $dirPPC and $dirX86 are
613
# FileAttrCache objects designating the source ppc and x86 directories to
614
# merge into a universal directory at $dirTarget, a string. For each file
615
# in $dirPPC and $dirX86, makeUniversalInternal is called.
616
# makeUniversalInternal will call back into makeUniversalDirectory for
617
# directories, thus completing the recursion. If a failure is encountered
618
# in ths function or in makeUniversalInternal or anything that it calls,
619
# false is returned, otherwise, true is returned.
621
# If there are files present in one source directory but not both, the
622
# value of $gOnlyOne controls the behavior. If $gOnlyOne is 'copy', the
623
# single source file is copied into $pathTarget. If it is 'skip', it is
624
# skipped. If it is 'fail', such files will trigger makeUniversalDirectory
627
# If either source directory is undef, it is treated as having no files.
628
# This facilitates deep recursion when entire directories are only present
629
# in one source when $gOnlyOne = 'copy'.
630
sub makeUniversalDirectory($$$) {
631
my ($dirPPC, $dirX86, $dirTarget);
632
($dirPPC, $dirX86, $dirTarget) = @_;
634
my ($dh, @filesPPC, @filesX86);
637
if (defined($dirPPC)) {
638
if (!opendir($dh, $dirPPC->path())) {
639
return complain(1, 'makeUniversalDirectory: opendir ppc: '.$!.' for:',
642
@filesPPC = readdir($dh);
647
if (defined($dirX86)) {
648
if (!opendir($dh, $dirX86->path())) {
649
return complain(1, 'makeUniversalDirectory: opendir x86: '.$!.' for:',
652
@filesX86 = readdir($dh);
656
my (%common, $file, %onlyPPC, %onlyX86);
659
foreach $file (@filesPPC) {
660
if ($file eq '.' || $file eq '..') {
668
foreach $file (@filesX86) {
669
if ($file eq '.' || $file eq '..') {
672
if ($onlyPPC{$file}) {
673
delete $onlyPPC{$file};
681
# First, handle files common to both.
682
foreach $file (sort(keys(%common))) {
683
if (!makeUniversalInternal(0,
684
FileAttrCache->new($dirPPC->path().'/'.$file),
685
FileAttrCache->new($dirX86->path().'/'.$file),
686
$dirTarget.'/'.$file)) {
687
# makeUniversalInternal will have printed an error.
692
# Handle files found only in a single directory here. There are three
693
# options, dictated by $gOnlyOne: fail if files are only present in
694
# one directory, skip any files only present in one directory, or copy
695
# these files straight over to the target directory. In any event,
696
# a message will be printed indicating that the file trees don't match
698
if (keys(%onlyPPC)) {
699
complain(($gOnlyOne eq 'fail' ? 1 : 2),
700
($gOnlyOne ne 'fail' ? 'warning: ' : '').
701
'makeUniversalDirectory: only in ppc '.
702
(argumentEscape($dirPPC->path()))[0].':',
703
argumentEscape(keys(%onlyPPC)));
706
if (keys(%onlyX86)) {
707
complain(($gOnlyOne eq 'fail' ? 1 : 2),
708
($gOnlyOne ne 'fail' ? 'warning: ' : '').
709
'makeUniversalDirectory: only in x86 '.
710
(argumentEscape($dirX86->path()))[0].':',
711
argumentEscape(keys(%onlyX86)));
714
if ($gOnlyOne eq 'fail' && (keys(%onlyPPC) || keys(%onlyX86))) {
715
# Error message(s) printed above.
719
if ($gOnlyOne eq 'copy') {
720
foreach $file (sort(keys(%onlyPPC))) {
721
if (!makeUniversalInternal(0,
722
FileAttrCache->new($dirPPC->path().'/'.$file),
724
$dirTarget.'/'.$file)) {
725
# makeUniversalInternal will have printed an error.
730
foreach $file (sort(keys(%onlyX86))) {
731
if (!makeUniversalInternal(0,
733
FileAttrCache->new($dirX86->path().'/'.$file),
734
$dirTarget.'/'.$file)) {
735
# makeUniversalInternal will have printed an error.
744
# makeUniversalFile($sourcePPC, $sourceX86, $targetPath)
746
# Creates a universal file at pathname $targetPath based on a ppc image at
747
# $sourcePPC and an x86 image at $sourceX86. $sourcePPC and $sourceX86 are
748
# both FileAttrCache objects. Returns true on success and false on failure.
749
# On failure, diagnostics will be printed to stderr.
751
# The source files may be either thin Mach-O images of the appropriate
752
# architecture, or fat Mach-O files that contain images of the appropriate
755
# This function wraps the lipo utility, see lipo(1).
756
sub makeUniversalFile($$$) {
757
my ($sourcePPC, $sourceX86, $targetPath, @tempThinFiles, $thinPPC, $thinX86);
758
($sourcePPC, $sourceX86, $targetPath) = @_;
759
$thinPPC = $sourcePPC;
760
$thinX86 = $sourceX86;
764
# The source files might already be fat. They should be thinned out to only
765
# contain a single architecture.
767
my ($isFatPPC, $isFatX86);
769
if(!defined($isFatPPC = $sourcePPC->isFat())) {
770
# isFat printed its own error
774
$thinPPC = FileAttrCache->new($targetPath.'.ppc');
775
push(@tempThinFiles, $thinPPC->path());
776
if (command($gConfig{'cmd_lipo'}, '-thin', 'ppc',
777
$sourcePPC->path(), '-output', $thinPPC->path()) != 0) {
778
unlink(@tempThinFiles);
779
return complain(1, 'lipo thin ppc failed for:',
785
if(!defined($isFatX86 = $sourceX86->isFat())) {
786
# isFat printed its own error
787
unlink(@tempThinFiles);
791
$thinX86 = FileAttrCache->new($targetPath.'.x86');
792
push(@tempThinFiles, $thinX86->path());
793
if (command($gConfig{'cmd_lipo'}, '-thin', 'i386',
794
$sourceX86->path(), '-output', $thinX86->path()) != 0) {
795
unlink(@tempThinFiles);
796
return complain(1, 'lipo thin x86 failed for:',
802
# The image for each architecture in the fat file will be aligned on
803
# a specific boundary, default 4096 bytes, see lipo(1) -segalign.
804
# Since there's no tail-padding, the fat file will consume the least
805
# space on disk if the image that comes last exceeds the segment size
806
# by the smallest amount.
808
# This saves an average of 1kB per fat file over the naive approach of
809
# always putting one architecture first: average savings is 2kB per
810
# file, but the naive approach would have gotten it right half of the
813
my ($sizePPC, $sizeX86, $thinPPCForStat, $thinX86ForStat);
816
$thinPPCForStat = $thinPPC;
817
$thinX86ForStat = $thinX86;
820
# Normally, fat source files will have been converted into temporary
821
# thin files. During a dry run, that doesn't happen, so fake it up
822
# a little bit by always using the source file, fat or thin, for the
824
$thinPPCForStat = $sourcePPC;
825
$thinX86ForStat = $sourceX86;
828
if (!defined($sizePPC = $thinPPCForStat->statSize())) {
829
unlink(@tempThinFiles);
830
return complain(1, 'stat ppc: '.$!.' for:',
831
$thinPPCForStat->path());
833
if (!defined($sizeX86 = $thinX86ForStat->statSize())) {
834
unlink(@tempThinFiles);
835
return complain(1, 'stat x86: '.$!.' for:',
836
$thinX86ForStat->path());
839
$sizePPC = $sizePPC % 4096;
840
$sizeX86 = $sizeX86 % 4096;
845
# PPC image ends on an alignment boundary, there will be no padding before
846
# starting the x86 image.
847
@thinFiles = ($thinPPC->path(), $thinX86->path());
849
elsif ($sizeX86 == 0 || $sizeX86 > $sizePPC) {
850
# x86 image ends on an alignment boundary, there will be no padding before
851
# starting the PPC image, or the x86 image exceeds its alignment boundary
852
# by more than the PPC image, so there will be less padding if the x86
854
@thinFiles = ($thinX86->path(), $thinPPC->path());
857
# PPC image exceeds its alignment boundary by more than the x86 image, so
858
# there will be less padding if the PPC comes first.
859
@thinFiles = ($thinPPC->path(), $thinX86->path());
863
$isExecutable = $sourcePPC->lIsExecutable() ||
864
$sourceX86->lIsExecutable();
867
# Ensure that the file does not yet exist.
869
# Set the execute bits (as allowed by the umask) on the new file if any
870
# execute bit is set on either old file. Yes, it is possible to have
871
# proper Mach-O files without x-bits: think object files (.o) and static
873
if (!createUniqueFile($targetPath, $isExecutable ? 0777 : 0666)) {
874
# createUniqueFile printed an error.
875
unlink(@tempThinFiles);
880
# Create the fat file.
881
if (command($gConfig{'cmd_lipo'}, '-create', @thinFiles,
882
'-output', $targetPath) != 0) {
883
unlink(@tempThinFiles, $targetPath);
884
return complain(1, 'lipo create fat failed for:',
889
unlink(@tempThinFiles);
892
# lipo seems to think that it's free to set its own file modes that
893
# ignore the umask, which is bogus when the rest of this script
894
# respects the umask.
895
if (!chmod(($isExecutable ? 0777 : 0666) & ~umask(), $targetPath)) {
896
complain(1, 'makeUniversalFile: chmod: '.$!.' for',
906
# makeUniversalInternal($isToplevel, $filePPC, $fileX86, $fileTargetPath)
908
# Given FileAttrCache objects $filePPC and $fileX86, compares filetypes
909
# and performs the appropriate action to produce a universal file at
910
# path string $fileTargetPath. $isToplevel should be true if this is
911
# the recursive base and false otherwise; this controls cleanup behavior
912
# (cleanup is only performed at the base, because cleanup itself is
915
# This handles regular files by determining whether they are Mach-O files
916
# and calling makeUniversalFile if so and copyIfIdentical otherwise. Symbolic
917
# links are handled directly in this function by ensuring that the source link
918
# targets are identical and creating a new link with the same target
919
# at $fileTargetPath. Directories are handled by calling
920
# makeUniversalDirectory.
922
# One of $filePPC and $fileX86 is permitted to be undef. In that case,
923
# the defined source file is copied directly to the target if a regular
924
# file, and symlinked appropriately if a symbolic link. This facilitates
925
# use of $gOnlyOne = 'copy', although no $gOnlyOne checks are made in this
926
# function, they are all handled in makeUniversalDirectory.
928
# Returns true on success. Returns false on failure, including failures
929
# in other functions called.
930
sub makeUniversalInternal($$$$) {
931
my ($filePPC, $fileTargetPath, $fileX86, $isToplevel);
932
($isToplevel, $filePPC, $fileX86, $fileTargetPath) = @_;
934
my ($typePPC, $typeX86);
935
if (defined($filePPC) && !defined($typePPC = $filePPC->lstatType())) {
936
return complain(1, 'makeUniversal: lstat ppc: '.$!.' for:',
939
if (defined($fileX86) && !defined($typeX86 = $fileX86->lstatType())) {
940
return complain(1, 'makeUniversal: lstat x86: '.$!.' for:',
944
if (defined($filePPC) && defined($fileX86) && $typePPC != $typeX86) {
945
return complain(1, 'makeUniversal: incompatible types:',
950
# $aSourceFile will contain a FileAttrCache object that will return
951
# the correct type data. It's used because it's possible for one of
952
# the two source files to be undefined (indicating a straight copy).
954
if (defined($filePPC)) {
955
$aSourceFile = $filePPC;
958
$aSourceFile = $fileX86;
961
if ($aSourceFile->lIsDir()) {
962
if ($gVerbosity >= 3 || $gDryRun) {
963
print('mkdir '.(argumentEscape($fileTargetPath))[0]."\n");
965
if (!$gDryRun && !mkdir($fileTargetPath)) {
966
return complain(1, 'makeUniversal: mkdir: '.$!.' for:',
972
if (!($rv = makeUniversalDirectory($filePPC, $fileX86, $fileTargetPath))) {
973
# makeUniversalDirectory printed an error.
975
command($gConfig{'cmd_rm'},'-rf','--',$fileTargetPath);
979
# Touch the directory when leaving it. If unify is being run on an
980
# .app bundle, the .app might show up without an icon because the
981
# system might have found the .app before it was completely built.
982
# Touching it dirties it in LaunchServices' mind.
983
if ($gVerbosity >= 3) {
984
print('touch '.(argumentEscape($fileTargetPath))[0]."\n");
986
utime(undef, undef, $fileTargetPath);
991
elsif ($aSourceFile->lIsSymLink()) {
992
my ($linkPPC, $linkX86);
993
if (defined($filePPC) && !defined($linkPPC=readlink($filePPC->path()))) {
994
return complain(1, 'makeUniversal: readlink ppc: '.$!.' for:',
997
if (defined($fileX86) && !defined($linkX86=readlink($fileX86->path()))) {
998
return complain(1, 'makeUniversal: readlink x86: '.$!.' for:',
1001
if (defined($filePPC) && defined($fileX86) && $linkPPC ne $linkX86) {
1002
return complain(1, 'makeUniversal: symbolic links differ:',
1007
# $aLink here serves the same purpose as $aSourceFile in the enclosing
1008
# block: it refers to the target of the symbolic link, whether there
1009
# is one valid source or two.
1011
if (defined($linkPPC)) {
1018
if ($gVerbosity >= 3 || $gDryRun) {
1020
join(' ',argumentEscape($aLink, $fileTargetPath))."\n");
1022
if (!$gDryRun && !symlink($aLink, $fileTargetPath)) {
1023
return complain(1, 'makeUniversal: symlink: '.$!.' for:',
1030
elsif($aSourceFile->lIsRegularFile()) {
1031
my ($machPPC, $machX86, $fileName);
1032
if (!defined($filePPC) || !defined($fileX86)) {
1033
# One of the source files isn't present. The right thing to do is
1034
# to just copy what does exist straight over, so skip Mach-O checks.
1037
if (defined($filePPC)) {
1038
$fileName = $filePPC;
1039
} elsif (defined($fileX86)) {
1040
$fileName = $fileX86;
1042
complain(1, "The file must exist in at least one directory");
1047
# both files exist, pick the name of one.
1048
$fileName = $fileX86;
1049
if (!defined($machPPC=$filePPC->isMachO())) {
1050
return complain(1, 'makeUniversal: isFileMachO ppc failed for:',
1053
if (!defined($machX86=$fileX86->isMachO())) {
1054
return complain(1, 'makeUniversal: isFileMachO x86 failed for:',
1059
if ($machPPC != $machX86) {
1060
return complain(1, 'makeUniversal: variant Mach-O attributes:',
1066
# makeUniversalFile will print an error if it fails.
1067
return makeUniversalFile($filePPC, $fileX86, $fileTargetPath);
1070
if (grep { $fileName->path() =~ m/$_/; } @gSortMatches) {
1071
# Regular files, but should be compared with sorting first.
1072
# copyIfIdenticalWhenSorted will print an error if it fails.
1073
return copyIfIdenticalWhenSorted($filePPC, $fileX86, $fileTargetPath);
1076
# Regular file. copyIfIdentical will print an error if it fails.
1077
return copyIfIdentical($filePPC, $fileX86, $fileTargetPath);
1080
# Special file, don't know how to handle.
1081
return complain(1, 'makeUniversal: cannot handle special file:',
1088
# Give the user a hand.
1091
"usage: unify <ppc-path> <x86-path> <universal-path>\n".
1092
" [--dry-run] (print what would be done)\n".
1093
" [--only-one <action>] (skip, copy, fail; default=copy)\n".
1094
" [--verbosity <level>] (0, 1, 2, 3; default=2)\n");
1098
# readZipCRCs($zipFile)
1100
# $zipFile is the pathname to a zip file whose directory will be read.
1101
# A reference to a hash is returned, with the member pathnames from the
1102
# zip file as keys, and reasonably unique identifiers as values. The
1103
# format of the values is not specified exactly, but does include the
1104
# member CRCs and sizes and differentiates between files and directories.
1105
# It specifically does not distinguish between modification times. On
1106
# failure, prints a message and returns undef.
1107
sub readZipCRCs($) {
1112
$zip = Archive::Zip->new();
1114
if (($ze = $zip->read($zipFile)) != AZ_OK) {
1115
complain(1, 'readZipCRCs: read error '.$ze.' for:',
1120
my ($member, %memberCRCs, @memberList);
1122
@memberList = $zip->members();
1124
foreach $member (@memberList) {
1125
# Take a few of the attributes that identify the file and stuff them into
1126
# the members hash. Directories will show up with size 0 and crc32 0,
1127
# so isDirectory() is used to distinguish them from empty files.
1128
$memberCRCs{$member->fileName()} = join(',', $member->isDirectory() ? 1 : 0,
1129
$member->uncompressedSize(),
1130
$member->crc32String());
1133
return {%memberCRCs};
1137
# FileAttrCache allows various attributes about a file to be cached
1138
# so that if they are needed again after first use, no system calls
1139
# will be made and the program won't need to hit the disk.
1141
package FileAttrCache;
1143
# from /usr/include/mach-o/loader.h
1144
use constant MH_MAGIC => 0xfeedface;
1145
use constant MH_CIGAM => 0xcefaedfe;
1146
use constant MH_MAGIC_64 => 0xfeedfacf;
1147
use constant MH_CIGAM_64 => 0xcffaedfe;
1149
use Fcntl(':DEFAULT', ':mode');
1151
# FileAttrCache->new($path)
1153
# Creates a new FileAttrCache object for the file at path $path and
1154
# returns it. The cache is not primed at creation time, values are
1155
# fetched lazily as they are needed.
1157
my ($class, $path, $proto, $this);
1158
($proto, $path) = @_;
1159
if (!($class = ref($proto))) {
1170
'magicErrMsg' => undef,
1176
bless($this, $class);
1180
# $FileAttrCache->isFat()
1182
# Returns true if the file is a fat Mach-O file, false if it's not, and
1183
# undef if an error occurs. See /usr/include/mach-o/fat.h.
1185
my ($magic, $magic2, $this);
1188
# magic() caches, there's no separate cache because isFat() doesn't hit
1189
# the disk other than by calling magic().
1191
if (!defined($magic = $this->magic())) {
1194
$magic2 = $this->magic2();
1196
# We have to sanity check the second four bytes, because Java class
1197
# files use the same magic number as Mach-O fat binaries.
1198
# This logic is adapted from file(1), which says that Mach-O uses
1199
# these bytes to count the number of architectures within, while
1200
# Java uses it for a version number. Conveniently, there are only
1201
# 18 labelled Mach-O architectures, and Java's first released
1202
# class format used the version 43.0.
1203
if ($magic == 0xcafebabe && $magic2 < 20) {
1210
# $FileAttrCache->isMachO()
1212
# Returns true if the file is a Mach-O image (including a fat file), false
1213
# if it's not, and undef if an error occurs. See
1214
# /usr/include/mach-o/loader.h and /usr/include/mach-o/fat.h.
1219
# magic() caches, there's no separate cache because isMachO() doesn't hit
1220
# the disk other than by calling magic().
1222
if (!defined($magic = $this->magic())) {
1226
# Accept Mach-O fat files or Mach-O thin files of either endianness.
1227
if ($magic == MH_MAGIC ||
1228
$magic == MH_CIGAM ||
1229
$magic == MH_MAGIC_64 ||
1230
$magic == MH_CIGAM_64 ||
1238
# $FileAttrCache->isZip()
1240
# Returns true if the file is a zip file, false if it's not, and undef if
1241
# an error occurs. See http://www.pkware.com/business_and_developers/developer/popups/appnote.txt .
1246
# magic() caches, there's no separate cache because isFat() doesn't hit
1247
# the disk other than by calling magic().
1249
if (!defined($magic = $this->magic())) {
1253
if ($magic == 0x504b0304) {
1260
# $FileAttrCache->lIsExecutable()
1262
# Wraps $FileAttrCache->lstat(), returning true if the file is has any,
1263
# execute bit set, false if none are set, or undef if an error occurs.
1264
# On error, $! is set to lstat's errno.
1265
sub lIsExecutable($) {
1269
if (!defined($mode = $this->lstatMode())) {
1273
return $mode & (S_IXUSR | S_IXGRP | S_IXOTH);
1276
# $FileAttrCache->lIsDir()
1278
# Wraps $FileAttrCache->lstat(), returning true if the file is a directory,
1279
# false if it isn't, or undef if an error occurs. Because lstat is used,
1280
# this will return false even if the file is a symlink pointing to a
1281
# directory. On error, $! is set to lstat's errno.
1286
if (!defined($type = $this->lstatType())) {
1290
return S_ISDIR($type);
1293
# $FileAttrCache->lIsRegularFile()
1295
# Wraps $FileAttrCache->lstat(), returning true if the file is a regular,
1296
# file, false if it isn't, or undef if an error occurs. Because lstat is
1297
# used, this will return false even if the file is a symlink pointing to a
1298
# regular file. On error, $! is set to lstat's errno.
1299
sub lIsRegularFile($) {
1303
if (!defined($type = $this->lstatType())) {
1307
return S_ISREG($type);
1310
# $FileAttrCache->lIsSymLink()
1312
# Wraps $FileAttrCache->lstat(), returning true if the file is a symbolic,
1313
# link, false if it isn't, or undef if an error occurs. On error, $! is
1314
# set to lstat's errno.
1319
if (!defined($type = $this->lstatType())) {
1323
return S_ISLNK($type);
1326
# $FileAttrCache->lstat()
1328
# Wraps the lstat system call, providing a cache to speed up multiple
1329
# lstat calls for the same file. See lstat(2) and lstat in perlfunc(1).
1334
# Use the cached lstat result.
1335
if ($$this{'lstatInit'}) {
1336
if (defined($$this{'lstatErrno'})) {
1337
$! = $$this{'lstatErrno'};
1339
return @{$$this{'lstat'}};
1341
$$this{'lstatInit'} = 1;
1343
if (!(@stat = CORE::lstat($$this{'path'}))) {
1344
$$this{'lstatErrno'} = $!;
1347
$$this{'lstat'} = [@stat];
1351
# $FileAttrCache->lstatMode()
1353
# Wraps $FileAttrCache->lstat(), returning the mode bits from the st_mode
1354
# field, or undef if an error occurs. On error, $! is set to lstat's
1360
if (!(@stat = $this->lstat())) {
1364
return S_IMODE($stat[2]);
1367
# $FileAttrCache->lstatType()
1369
# Wraps $FileAttrCache->lstat(), returning the type bits from the st_mode
1370
# field, or undef if an error occurs. On error, $! is set to lstat's
1376
if (!(@stat = $this->lstat())) {
1380
return S_IFMT($stat[2]);
1383
# $FileAttrCache->magic()
1385
# Returns the "magic number" for the file by reading its first four bytes
1386
# as a big-endian unsigned 32-bit integer and returning the result. If an
1387
# error occurs, returns undef and prints diagnostic messages to stderr. If
1388
# the file is shorter than 32 bits, returns -1. A cache is provided to
1389
# speed multiple magic calls for the same file.
1394
# Use the cached magic result.
1395
if ($$this{'magicInit'}) {
1396
if (defined($$this{'magicErrno'})) {
1397
if (defined($$this{'magicErrMsg'})) {
1398
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
1401
$! = $$this{'magicErrno'};
1403
return $$this{'magic'};
1406
$$this{'magicInit'} = 1;
1409
if (!sysopen($fh, $$this{'path'}, O_RDONLY)) {
1410
$$this{'magicErrno'} = $!;
1411
$$this{'magicErrMsg'} = 'open "'.$$this{'path'}.'": '.$!;
1412
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
1418
my ($bytes, $magic, $bytes2, $magic2);
1419
if (!defined($bytes = sysread($fh, $magic, 4))) {
1420
$$this{'magicErrno'} = $!;
1421
$$this{'magicErrMsg'} = 'read "'.$$this{'path'}.'": '.$!;
1422
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
1428
$bytes2 = sysread($fh, $magic2, 4);
1434
# The file is too short, didn't read a magic number. This isn't really
1435
# an error. Return an unlikely value.
1436
$$this{'magic'} = -1;
1437
$$this{'magic2'} = -1;
1441
# File is too short to read a second 4 bytes.
1445
$$this{'magic'} = unpack('N', $magic);
1446
$$this{'magic2'} = unpack('N', $magic2);
1447
return $$this{'magic'};
1450
# $FileAttrCache->magic2()
1452
# Returns the second four bytes of the file as a 32-bit little endian number.
1453
# See magic(), above for more info.
1458
# we do the actual work (and cache it) in magic().
1459
if (!$$this{'magicInit'}) {
1460
my $magic = $$this->magic();
1463
return $$this{'magic2'};
1466
# $FileAttrCache->path()
1468
# Returns the file's pathname.
1472
return $$this{'path'};
1475
# $FileAttrCache->stat()
1477
# Wraps the stat system call, providing a cache to speed up multiple
1478
# stat calls for the same file. If lstat() has already been called and
1479
# the file is not a symbolic link, the cached lstat() result will be used.
1480
# See stat(2) and lstat in perlfunc(1).
1485
# Use the cached stat result.
1486
if ($$this{'statInit'}) {
1487
if (defined($$this{'statErrno'})) {
1488
$! = $$this{'statErrno'};
1490
return @{$$this{'stat'}};
1493
$$this{'statInit'} = 1;
1495
# If lstat has already been called, and the file isn't a symbolic link,
1496
# use the cached lstat result.
1497
if ($$this{'lstatInit'} && !$$this{'lstatErrno'} &&
1498
!S_ISLNK(${$$this{'lstat'}}[2])) {
1499
$$this{'stat'} = $$this{'lstat'};
1500
return @{$$this{'stat'}};
1503
if (!(@stat = CORE::stat($$this{'path'}))) {
1504
$$this{'statErrno'} = $!;
1507
$$this{'stat'} = [@stat];
1511
# $FileAttrCache->statSize()
1513
# Wraps $FileAttrCache->stat(), returning the st_size field, or undef
1514
# undef if an error occurs. On error, $! is set to stat's errno.
1519
if (!(@stat = $this->lstat())) {