mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 06:05:44 +00:00
1526 lines
46 KiB
Perl
Executable File
1526 lines
46 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
B<unify> - Mac OS X universal binary packager
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
B<unify>
|
|
I<ppc-path>
|
|
I<x86-path>
|
|
I<universal-path>
|
|
[B<--dry-run>]
|
|
[B<--only-one> I<action>]
|
|
[B<--verbosity> I<level>]
|
|
[B<--unify-with-sort> I<regex>]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
I<unify> merges any two architecture-specific files or directory trees
|
|
into a single file or tree suitable for use on either architecture as a
|
|
"fat" or "universal binary."
|
|
|
|
Architecture-specific Mach-O files will be merged into fat Mach-O files
|
|
using L<lipo(1)>. Non-Mach-O files in the architecture-specific trees
|
|
are compared to ensure that they are equivalent before copying. Symbolic
|
|
links are permitted in the architecture-specific trees and will cause
|
|
identical links to be created in the merged tree, provided that the source
|
|
links have identical targets. Directories are processed recursively.
|
|
|
|
If the architecture-specific source trees contain zip archives (including
|
|
jar files) that are not identical according to a byte-for-byte check, they
|
|
are still assumed to be equivalent if both archives contain exactly the
|
|
same members with identical checksums and sizes.
|
|
|
|
Behavior when one architecture-specific tree contains files that the other
|
|
does not is controlled by the B<--only-one> option.
|
|
|
|
If Mach-O files cannot be merged using L<lipo(1)>, zip archives are not
|
|
equivalent, regular files are not identical, or any other error occurs,
|
|
B<unify> will fail with an exit status of 1. Diagnostic messages are
|
|
typically printed to stderr; this behavior can be controlled with the
|
|
B<--verbosity> option.
|
|
|
|
=head1 OPTIONS
|
|
|
|
=over 5
|
|
|
|
=item I<ppc-path>
|
|
|
|
=item I<x86-path>
|
|
|
|
The paths to directory trees containing PowerPC and x86 builds,
|
|
respectively. I<ppc-path> and I<x86-path> are permitted to contain files
|
|
that are already "fat," and only the appropriate architecture's images will
|
|
be used.
|
|
|
|
I<ppc-path> and I<x86-path> are also permitted to both be files, in which
|
|
case B<unify> operates solely on those files, and produces an appropriate
|
|
merged file at I<target-path>.
|
|
|
|
=item I<target-path>
|
|
|
|
The path to the merged file or directory tree. This path will be created,
|
|
and it must not exist prior to running B<unify>.
|
|
|
|
=item B<--dry-run>
|
|
|
|
When specified, the commands that would be executed are printed, without
|
|
actually executing them. Note that B<--dry-run> and the equivalent
|
|
B<--verbosity> level during "wet" runs may print equivalent commands when
|
|
no commands are in fact executed: certain operations are handled internally
|
|
within B<unify>, and an approximation of a command that performs a similar
|
|
task is printed.
|
|
|
|
=item B<--only-one> I<action>
|
|
|
|
Controls handling of files that are only present in one of the two source
|
|
trees. I<action> may be:
|
|
skip - These files are skipped.
|
|
copy - These files are copied from the tree in which they exist.
|
|
fail - When this condition occurs, it is treated as an error.
|
|
|
|
The default I<action> is copy.
|
|
|
|
=item B<--verbosity> I<level>
|
|
|
|
Adjusts the level of loudness of B<unify>. The possible values for
|
|
I<level> are:
|
|
0 - B<unify> never prints anything.
|
|
(Other programs that B<unify> calls may still print messages.)
|
|
1 - Fatal error messages are printed to stderr.
|
|
2 - Nonfatal warnings are printed to stderr.
|
|
3 - Commands are printed to stdout as they are executed.
|
|
|
|
The default I<level> is 2.
|
|
|
|
=item B<--unify-with-sort> I<regex>
|
|
|
|
Allows merging files matching I<regex> that differ only by the ordering
|
|
of the lines contained within them. The unified file will have its contents
|
|
sorted. This option may be given multiple times to specify multiple
|
|
regexes for matching files.
|
|
|
|
=back
|
|
|
|
=head1 EXAMPLES
|
|
|
|
=over 5
|
|
|
|
=item Create a universal .app bundle from two architecture-specific .app
|
|
bundles:
|
|
|
|
unify --only-one copy ppc/dist/firefox/Firefox.app
|
|
x86/dist/firefox/Firefox.app universal/Firefox.app
|
|
--verbosity 3
|
|
|
|
=item Merge two identical architecture-specific trees:
|
|
|
|
unify --only-one fail /usr/local /nfs/x86/usr/local
|
|
/tmp/usrlocal.fat
|
|
|
|
=back
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
The only esoteric requirement of B<unify> is that the L<lipo(1)> command
|
|
be available. It is present on Mac OS X systems at least as early as
|
|
10.3.9, and probably earlier. Mac OS X 10.4 ("Tiger") or later are
|
|
recommended.
|
|
|
|
=head1 LICENSE
|
|
|
|
MPL 1.1/GPL 2.0/LGPL 2.1. Your choice
|
|
|
|
=head1 AUTHOR
|
|
|
|
The software was initially written by Mark Mentovai; copyright 2006
|
|
Google Inc.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<cmp(1)>, L<ditto(1)>, L<lipo(1)>
|
|
|
|
=cut
|
|
|
|
use Archive::Zip(':ERROR_CODES');
|
|
use Errno;
|
|
use Fcntl;
|
|
use File::Compare;
|
|
use File::Copy;
|
|
use Getopt::Long;
|
|
|
|
my (%gConfig, $gDryRun, $gOnlyOne, $gVerbosity, @gSortMatches);
|
|
|
|
sub argumentEscape(@);
|
|
sub command(@);
|
|
sub compareZipArchives($$);
|
|
sub complain($$@);
|
|
sub copyIfIdentical($$$);
|
|
sub slurp($);
|
|
sub get_sorted($);
|
|
sub compare_sorted($$);
|
|
sub copyIfIdenticalWhenSorted($$$);
|
|
sub createUniqueFile($$);
|
|
sub makeUniversal($$$);
|
|
sub makeUniversalDirectory($$$);
|
|
sub makeUniversalInternal($$$$);
|
|
sub makeUniversalFile($$$);
|
|
sub usage();
|
|
sub readZipCRCs($);
|
|
|
|
{
|
|
package FileAttrCache;
|
|
|
|
sub new($$);
|
|
|
|
sub isFat($);
|
|
sub isMachO($);
|
|
sub isZip($);
|
|
sub lIsDir($);
|
|
sub lIsExecutable($);
|
|
sub lIsRegularFile($);
|
|
sub lIsSymLink($);
|
|
sub lstat($);
|
|
sub lstatMode($);
|
|
sub lstatType($);
|
|
sub magic($);
|
|
sub magic2($);
|
|
sub path($);
|
|
sub stat($);
|
|
sub statSize($);
|
|
}
|
|
|
|
%gConfig = (
|
|
'cmd_lipo' => 'lipo',
|
|
'cmd_rm' => 'rm',
|
|
);
|
|
|
|
$gDryRun = 0;
|
|
$gOnlyOne = 'copy';
|
|
$gVerbosity = 2;
|
|
@gSortMatches = ();
|
|
|
|
Getopt::Long::Configure('pass_through');
|
|
GetOptions('dry-run' => \$gDryRun,
|
|
'only-one=s' => \$gOnlyOne,
|
|
'verbosity=i' => \$gVerbosity,
|
|
'unify-with-sort=s' => \@gSortMatches,
|
|
'config=s' => \%gConfig); # "hidden" option not in usage()
|
|
|
|
if (scalar(@ARGV) != 3 || $gVerbosity < 0 || $gVerbosity > 3 ||
|
|
($gOnlyOne ne 'skip' && $gOnlyOne ne 'copy' && $gOnlyOne ne 'fail')) {
|
|
usage();
|
|
exit(1);
|
|
}
|
|
|
|
if (!makeUniversal($ARGV[0],$ARGV[1],$ARGV[2])) {
|
|
# makeUniversal or something it called will have printed an error.
|
|
exit(1);
|
|
}
|
|
|
|
exit(0);
|
|
|
|
# argumentEscape(@arguments)
|
|
#
|
|
# Takes a list of @arguments and makes them shell-safe.
|
|
sub argumentEscape(@) {
|
|
my (@arguments);
|
|
@arguments = @_;
|
|
|
|
my ($argument, @argumentsOut);
|
|
foreach $argument (@arguments) {
|
|
$argument =~ s%([^A-Za-z0-9_\-/.=+,])%\\$1%g;
|
|
push(@argumentsOut, $argument);
|
|
}
|
|
|
|
return @argumentsOut;
|
|
}
|
|
|
|
# command(@arguments)
|
|
#
|
|
# Runs the specified command by calling system(@arguments). If $gDryRun
|
|
# is true, the command is printed but not executed, and 0 is returned.
|
|
# if $gVerbosity is greater than 1, the command is printed before being
|
|
# executed. When the command is executed, the system() return value will
|
|
# be returned. stdout and stderr are left connected for command output.
|
|
sub command(@) {
|
|
my (@arguments);
|
|
@arguments = @_;
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print(join(' ', argumentEscape(@arguments))."\n");
|
|
}
|
|
if ($gDryRun) {
|
|
return 0;
|
|
}
|
|
return system(@arguments);
|
|
}
|
|
|
|
# compareZipArchives($zip1, $zip2)
|
|
#
|
|
# Given two pathnames to zip archives, determines whether or not they are
|
|
# functionally identical. Returns true if they are, false if they differ in
|
|
# some substantial way, and undef if an error occurs. If the zip files
|
|
# differ, diagnostic messages are printed indicating how they differ.
|
|
#
|
|
# Zip files will differ if any of the members are different as defined by
|
|
# readZipCRCs, which consider CRCs, sizes, and file types as stored in the
|
|
# file header. Timestamps are not considered. Zip files also differ if one
|
|
# file contains members that the other one does not. $gOnlyOne has no
|
|
# effect on this behavior.
|
|
sub compareZipArchives($$) {
|
|
my ($zip1, $zip2);
|
|
($zip1, $zip2) = @_;
|
|
|
|
my ($CRCHash1, $CRCHash2);
|
|
if (!defined($CRCHash1 = readZipCRCs($zip1))) {
|
|
# readZipCRCs printed an error.
|
|
return undef;
|
|
}
|
|
if (!defined($CRCHash2 = readZipCRCs($zip2))) {
|
|
# readZipCRCs printed an error.
|
|
return undef;
|
|
}
|
|
|
|
my (@diffCRCs, @onlyInZip1);
|
|
@diffCRCs = ();
|
|
@onlyInZip1 = ();
|
|
|
|
my ($memberName);
|
|
foreach $memberName (keys(%$CRCHash1)) {
|
|
if (!exists($$CRCHash2{$memberName})) {
|
|
# The member is present in $zip1 but not $zip2.
|
|
push(@onlyInZip1, $memberName);
|
|
}
|
|
elsif ($$CRCHash1{$memberName} ne $$CRCHash2{$memberName}) {
|
|
# The member is present in both archives but its CRC or some other
|
|
# other critical attribute isn't identical.
|
|
push(@diffCRCs, $memberName);
|
|
}
|
|
delete($$CRCHash2{$memberName});
|
|
}
|
|
|
|
# If any members remain in %CRCHash2, it's because they're not present
|
|
# in $zip1.
|
|
my (@onlyInZip2);
|
|
@onlyInZip2 = keys(%$CRCHash2);
|
|
|
|
if (scalar(@onlyInZip1) + scalar(@onlyInZip2) + scalar(@diffCRCs)) {
|
|
complain(1, 'compareZipArchives: zip archives differ:',
|
|
$zip1,
|
|
$zip2);
|
|
if (scalar(@onlyInZip1)) {
|
|
complain(1, 'compareZipArchives: members only in former:',
|
|
@onlyInZip1);
|
|
}
|
|
if (scalar(@onlyInZip2)) {
|
|
complain(1, 'compareZipArchives: members only in latter:',
|
|
@onlyInZip2);
|
|
}
|
|
if (scalar(@diffCRCs)) {
|
|
complain(1, 'compareZipArchives: members differ:',
|
|
@diffCRCs);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# complain($severity, $message, @list)
|
|
#
|
|
# Prints $message to stderr if $gVerbosity allows it for severity level
|
|
# $severity. @list is a list of words that will be shell-escaped and printed
|
|
# after $message, one per line, intended to be used, for example, to list
|
|
# arguments to a call that failed.
|
|
#
|
|
# Expected severity levels are 1 for hard errors and 2 for non-fatal warnings.
|
|
#
|
|
# Always returns false as a convenience, so callers can return complain's
|
|
# return value when it is used to signal errors.
|
|
sub complain($$@) {
|
|
my ($severity, $message, @list);
|
|
($severity, $message, @list) = @_;
|
|
|
|
if ($gVerbosity >= $severity) {
|
|
print STDERR ($0.': '.$message."\n");
|
|
|
|
my ($item);
|
|
while ($item = shift(@list)) {
|
|
print STDERR (' '.(argumentEscape($item))[0].
|
|
(scalar(@list)?',':'')."\n");
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
# copyIfIdentical($source1, $source2, $target)
|
|
#
|
|
# $source1 and $source2 are FileAttrCache objects that are compared, and if
|
|
# identical, copied to path string $target. The comparison is initially
|
|
# done as a byte-for-byte comparison, but if the files differ and appear to
|
|
# be zip archives, compareZipArchives is called to determine whether
|
|
# files that are not byte-for-byte identical are equivalent archives.
|
|
#
|
|
# Returns true on success, false for files that are not identical or
|
|
# equivalent archives, and undef if an error occurs.
|
|
#
|
|
# One of $source1 and $source2 is permitted to be undef. In this event,
|
|
# whichever source is defined is copied directly to $target without performing
|
|
# any comparisons. This enables the $gOnlyOne = 'copy' mode, which is
|
|
# driven by makeUniversalDirectory and makeUniversalInternal.
|
|
sub copyIfIdentical($$$) {
|
|
my ($source1, $source2, $target);
|
|
($source1, $source2, $target) = @_;
|
|
|
|
if (!defined($source1)) {
|
|
# If there's only one source file, make it the first file. Order
|
|
# isn't important here, and this makes it possible to use
|
|
# defined($source2) as the switch, and to always copy from $source1.
|
|
$source1 = $source2;
|
|
$source2 = undef;
|
|
}
|
|
|
|
if (defined($source2)) {
|
|
# Only do the comparisons if there are two source files. If there's
|
|
# only one source file, skip the comparisons and go straight to the
|
|
# copy operation.
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('cmp -s '.
|
|
join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
|
|
}
|
|
my ($comparison);
|
|
if (!defined($comparison = compare($source1->path(), $source2->path())) ||
|
|
$comparison == -1) {
|
|
return complain(1, 'copyIfIdentical: compare: '.$!.' while comparing:',
|
|
$source1->path(),
|
|
$source2->path());
|
|
}
|
|
elsif ($comparison != 0) {
|
|
my ($zip1, $zip2);
|
|
if (defined($zip1 = $source1->isZip()) &&
|
|
defined($zip2 = $source2->isZip()) &&
|
|
$zip1 && $zip2) {
|
|
my ($zipComparison);
|
|
if (!defined($zipComparison = compareZipArchives($source1->path(),
|
|
$source2->path)) ||
|
|
!$zipComparison) {
|
|
# An error occurred or the zip files aren't sufficiently identical.
|
|
# compareZipArchives will have printed an error message.
|
|
return 0;
|
|
}
|
|
# The zip files were compared successfully, and they both contain
|
|
# all of the same members, and all of their members' CRCs are
|
|
# identical. For the purposes of this script, the zip files can be
|
|
# treated as identical, so reset $comparison.
|
|
$comparison = 0;
|
|
}
|
|
}
|
|
if ($comparison != 0) {
|
|
return complain(1, 'copyIfIdentical: files differ:',
|
|
$source1->path(),
|
|
$source2->path());
|
|
}
|
|
}
|
|
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('cp '.
|
|
join(' ',argumentEscape($source1->path(), $target))."\n");
|
|
}
|
|
|
|
if (!$gDryRun) {
|
|
my ($isExecutable);
|
|
|
|
# Set the execute bits (as allowed by the umask) on the new file if any
|
|
# execute bit is set on either old file.
|
|
$isExecutable = $source1->lIsExecutable() ||
|
|
(defined($source2) && $source2->lIsExecutable());
|
|
|
|
if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
|
|
# createUniqueFile printed an error.
|
|
return 0;
|
|
}
|
|
|
|
if (!copy($source1->path(), $target)) {
|
|
complain(1, 'copyIfIdentical: copy: '.$!.' while copying',
|
|
$source1->path(),
|
|
$target);
|
|
unlink($target);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# slurp($file)
|
|
#
|
|
# Read the contents of $file into an array and return it.
|
|
# Returns undef on error.
|
|
sub slurp($) {
|
|
my $file = $_[0];
|
|
open FILE, $file or return undef;
|
|
my @lines = <FILE>;
|
|
close FILE;
|
|
return @lines;
|
|
}
|
|
|
|
# get_sorted($file)
|
|
# Get the sorted lines of a file as a list, normalizing a newline on the last line if necessary.
|
|
sub get_sorted($) {
|
|
my ($file) = @_;
|
|
my @lines = slurp($file);
|
|
my $lastline = $lines[-1];
|
|
if (!($lastline =~ /\n/)) {
|
|
$lines[-1] = $lastline . "\n";
|
|
}
|
|
return sort(@lines);
|
|
}
|
|
|
|
# compare_sorted($file1, $file2)
|
|
#
|
|
# Read the contents of both files into arrays, sort the arrays,
|
|
# and then compare the two arrays for equality.
|
|
#
|
|
# Returns 0 if the sorted array contents are equal, or 1 if not.
|
|
# Returns undef on error.
|
|
sub compare_sorted($$) {
|
|
my ($file1, $file2) = @_;
|
|
my @lines1 = get_sorted($file1);
|
|
my @lines2 = get_sorted($file2);
|
|
|
|
return undef if !@lines1 || !@lines2;
|
|
return 1 unless scalar @lines1 == scalar @lines2;
|
|
|
|
for (my $i = 0; $i < scalar @lines1; $i++) {
|
|
return 1 if $lines1[$i] ne $lines2[$i];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
# copyIfIdenticalWhenSorted($source1, $source2, $target)
|
|
#
|
|
# $source1 and $source2 are FileAttrCache objects that are compared, and if
|
|
# identical, copied to path string $target. The comparison is done by
|
|
# sorting the individual lines within the two files and comparing the results.
|
|
#
|
|
# Returns true on success, false for files that are not equivalent,
|
|
# and undef if an error occurs.
|
|
sub copyIfIdenticalWhenSorted($$$) {
|
|
my ($source1, $source2, $target);
|
|
($source1, $source2, $target) = @_;
|
|
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('cmp -s '.
|
|
join(' ',argumentEscape($source1->path(), $source2->path()))."\n");
|
|
}
|
|
my ($comparison);
|
|
if (!defined($comparison = compare_sorted($source1->path(),
|
|
$source2->path())) ||
|
|
$comparison == -1) {
|
|
return complain(1, 'copyIfIdenticalWhenSorted: compare: '.$!
|
|
.' while comparing:',
|
|
$source1->path(),
|
|
$source2->path());
|
|
}
|
|
if ($comparison != 0) {
|
|
return complain(1, 'copyIfIdenticalWhenSorted: files differ:',
|
|
$source1->path(),
|
|
$source2->path());
|
|
}
|
|
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('cp '.
|
|
join(' ',argumentEscape($source1->path(), $target))."\n");
|
|
}
|
|
|
|
if (!$gDryRun) {
|
|
my ($isExecutable);
|
|
|
|
# Set the execute bits (as allowed by the umask) on the new file if any
|
|
# execute bit is set on either old file.
|
|
$isExecutable = $source1->lIsExecutable() ||
|
|
(defined($source2) && $source2->lIsExecutable());
|
|
|
|
if (!createUniqueFile($target, $isExecutable ? 0777 : 0666)) {
|
|
# createUniqueFile printed an error.
|
|
return 0;
|
|
}
|
|
|
|
if (!copy($source1->path(), $target)) {
|
|
complain(1, 'copyIfIdenticalWhenSorted: copy: '.$!
|
|
.' while copying',
|
|
$source1->path(),
|
|
$target);
|
|
unlink($target);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# createUniqueFile($path, $mode)
|
|
#
|
|
# Creates a new plain empty file at pathname $path, provided it does not
|
|
# yet exist. $mode is used as the file mode. The actual file's mode will
|
|
# be modified by the effective umask. Returns false if the file could
|
|
# not be created, setting $! to the error. An error message is printed
|
|
# in the event of failure.
|
|
sub createUniqueFile($$) {
|
|
my ($path, $mode);
|
|
($path, $mode) = @_;
|
|
|
|
my ($fh);
|
|
if (!sysopen($fh, $path, O_WRONLY | O_CREAT | O_EXCL, $mode)) {
|
|
return complain(1, 'createUniqueFile: open: '.$!.' for:',
|
|
$path);
|
|
}
|
|
close($fh);
|
|
|
|
return 1;
|
|
}
|
|
|
|
# makeUniversal($pathPPC, $pathX86, $pathTarget)
|
|
#
|
|
# The top-level call. $pathPPC, $pathX86, and $pathTarget are strings
|
|
# identifying the ppc and x86 files or directories to merge and the location
|
|
# to merge them to. Returns false on failure and true on success.
|
|
sub makeUniversal($$$) {
|
|
my ($pathTarget, $pathPPC, $pathX86);
|
|
($pathPPC, $pathX86, $pathTarget) = @_;
|
|
|
|
my ($filePPC, $fileX86);
|
|
$filePPC = FileAttrCache->new($pathPPC);
|
|
$fileX86 = FileAttrCache->new($pathX86);
|
|
|
|
return makeUniversalInternal(1, $filePPC, $fileX86, $pathTarget);
|
|
}
|
|
|
|
# makeUniversalDirectory($dirPPC, $dirX86, $dirTarget)
|
|
#
|
|
# This is part of the heart of recursion. $dirPPC and $dirX86 are
|
|
# FileAttrCache objects designating the source ppc and x86 directories to
|
|
# merge into a universal directory at $dirTarget, a string. For each file
|
|
# in $dirPPC and $dirX86, makeUniversalInternal is called.
|
|
# makeUniversalInternal will call back into makeUniversalDirectory for
|
|
# directories, thus completing the recursion. If a failure is encountered
|
|
# in ths function or in makeUniversalInternal or anything that it calls,
|
|
# false is returned, otherwise, true is returned.
|
|
#
|
|
# If there are files present in one source directory but not both, the
|
|
# value of $gOnlyOne controls the behavior. If $gOnlyOne is 'copy', the
|
|
# single source file is copied into $pathTarget. If it is 'skip', it is
|
|
# skipped. If it is 'fail', such files will trigger makeUniversalDirectory
|
|
# to fail.
|
|
#
|
|
# If either source directory is undef, it is treated as having no files.
|
|
# This facilitates deep recursion when entire directories are only present
|
|
# in one source when $gOnlyOne = 'copy'.
|
|
sub makeUniversalDirectory($$$) {
|
|
my ($dirPPC, $dirX86, $dirTarget);
|
|
($dirPPC, $dirX86, $dirTarget) = @_;
|
|
|
|
my ($dh, @filesPPC, @filesX86);
|
|
|
|
@filesPPC = ();
|
|
if (defined($dirPPC)) {
|
|
if (!opendir($dh, $dirPPC->path())) {
|
|
return complain(1, 'makeUniversalDirectory: opendir ppc: '.$!.' for:',
|
|
$dirPPC->path());
|
|
}
|
|
@filesPPC = readdir($dh);
|
|
closedir($dh);
|
|
}
|
|
|
|
@filesX86 = ();
|
|
if (defined($dirX86)) {
|
|
if (!opendir($dh, $dirX86->path())) {
|
|
return complain(1, 'makeUniversalDirectory: opendir x86: '.$!.' for:',
|
|
$dirX86->path());
|
|
}
|
|
@filesX86 = readdir($dh);
|
|
closedir($dh);
|
|
}
|
|
|
|
my (%common, $file, %onlyPPC, %onlyX86);
|
|
|
|
%onlyPPC = ();
|
|
foreach $file (@filesPPC) {
|
|
if ($file eq '.' || $file eq '..') {
|
|
next;
|
|
}
|
|
$onlyPPC{$file}=1;
|
|
}
|
|
|
|
%common = ();
|
|
%onlyX86 = ();
|
|
foreach $file (@filesX86) {
|
|
if ($file eq '.' || $file eq '..') {
|
|
next;
|
|
}
|
|
if ($onlyPPC{$file}) {
|
|
delete $onlyPPC{$file};
|
|
$common{$file}=1;
|
|
}
|
|
else {
|
|
$onlyX86{$file}=1;
|
|
}
|
|
}
|
|
|
|
# First, handle files common to both.
|
|
foreach $file (sort(keys(%common))) {
|
|
if (!makeUniversalInternal(0,
|
|
FileAttrCache->new($dirPPC->path().'/'.$file),
|
|
FileAttrCache->new($dirX86->path().'/'.$file),
|
|
$dirTarget.'/'.$file)) {
|
|
# makeUniversalInternal will have printed an error.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# Handle files found only in a single directory here. There are three
|
|
# options, dictated by $gOnlyOne: fail if files are only present in
|
|
# one directory, skip any files only present in one directory, or copy
|
|
# these files straight over to the target directory. In any event,
|
|
# a message will be printed indicating that the file trees don't match
|
|
# exactly.
|
|
if (keys(%onlyPPC)) {
|
|
complain(($gOnlyOne eq 'fail' ? 1 : 2),
|
|
($gOnlyOne ne 'fail' ? 'warning: ' : '').
|
|
'makeUniversalDirectory: only in ppc '.
|
|
(argumentEscape($dirPPC->path()))[0].':',
|
|
argumentEscape(keys(%onlyPPC)));
|
|
}
|
|
|
|
if (keys(%onlyX86)) {
|
|
complain(($gOnlyOne eq 'fail' ? 1 : 2),
|
|
($gOnlyOne ne 'fail' ? 'warning: ' : '').
|
|
'makeUniversalDirectory: only in x86 '.
|
|
(argumentEscape($dirX86->path()))[0].':',
|
|
argumentEscape(keys(%onlyX86)));
|
|
}
|
|
|
|
if ($gOnlyOne eq 'fail' && (keys(%onlyPPC) || keys(%onlyX86))) {
|
|
# Error message(s) printed above.
|
|
return 0;
|
|
}
|
|
|
|
if ($gOnlyOne eq 'copy') {
|
|
foreach $file (sort(keys(%onlyPPC))) {
|
|
if (!makeUniversalInternal(0,
|
|
FileAttrCache->new($dirPPC->path().'/'.$file),
|
|
undef,
|
|
$dirTarget.'/'.$file)) {
|
|
# makeUniversalInternal will have printed an error.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
foreach $file (sort(keys(%onlyX86))) {
|
|
if (!makeUniversalInternal(0,
|
|
undef,
|
|
FileAttrCache->new($dirX86->path().'/'.$file),
|
|
$dirTarget.'/'.$file)) {
|
|
# makeUniversalInternal will have printed an error.
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# makeUniversalFile($sourcePPC, $sourceX86, $targetPath)
|
|
#
|
|
# Creates a universal file at pathname $targetPath based on a ppc image at
|
|
# $sourcePPC and an x86 image at $sourceX86. $sourcePPC and $sourceX86 are
|
|
# both FileAttrCache objects. Returns true on success and false on failure.
|
|
# On failure, diagnostics will be printed to stderr.
|
|
#
|
|
# The source files may be either thin Mach-O images of the appropriate
|
|
# architecture, or fat Mach-O files that contain images of the appropriate
|
|
# architecture.
|
|
#
|
|
# This function wraps the lipo utility, see lipo(1).
|
|
sub makeUniversalFile($$$) {
|
|
my ($sourcePPC, $sourceX86, $targetPath, @tempThinFiles, $thinPPC, $thinX86);
|
|
($sourcePPC, $sourceX86, $targetPath) = @_;
|
|
$thinPPC = $sourcePPC;
|
|
$thinX86 = $sourceX86;
|
|
|
|
@tempThinFiles = ();
|
|
|
|
# The source files might already be fat. They should be thinned out to only
|
|
# contain a single architecture.
|
|
|
|
my ($isFatPPC, $isFatX86);
|
|
|
|
if(!defined($isFatPPC = $sourcePPC->isFat())) {
|
|
# isFat printed its own error
|
|
return 0;
|
|
}
|
|
elsif($isFatPPC) {
|
|
$thinPPC = FileAttrCache->new($targetPath.'.ppc');
|
|
push(@tempThinFiles, $thinPPC->path());
|
|
if (command($gConfig{'cmd_lipo'}, '-thin', 'ppc',
|
|
$sourcePPC->path(), '-output', $thinPPC->path()) != 0) {
|
|
unlink(@tempThinFiles);
|
|
return complain(1, 'lipo thin ppc failed for:',
|
|
$sourcePPC->path(),
|
|
$thinPPC->path());
|
|
}
|
|
}
|
|
|
|
if(!defined($isFatX86 = $sourceX86->isFat())) {
|
|
# isFat printed its own error
|
|
unlink(@tempThinFiles);
|
|
return 0;
|
|
}
|
|
elsif($isFatX86) {
|
|
$thinX86 = FileAttrCache->new($targetPath.'.x86');
|
|
push(@tempThinFiles, $thinX86->path());
|
|
if (command($gConfig{'cmd_lipo'}, '-thin', 'i386',
|
|
$sourceX86->path(), '-output', $thinX86->path()) != 0) {
|
|
unlink(@tempThinFiles);
|
|
return complain(1, 'lipo thin x86 failed for:',
|
|
$sourceX86->path(),
|
|
$thinX86->path());
|
|
}
|
|
}
|
|
|
|
# The image for each architecture in the fat file will be aligned on
|
|
# a specific boundary, default 4096 bytes, see lipo(1) -segalign.
|
|
# Since there's no tail-padding, the fat file will consume the least
|
|
# space on disk if the image that comes last exceeds the segment size
|
|
# by the smallest amount.
|
|
#
|
|
# This saves an average of 1kB per fat file over the naive approach of
|
|
# always putting one architecture first: average savings is 2kB per
|
|
# file, but the naive approach would have gotten it right half of the
|
|
# time.
|
|
|
|
my ($sizePPC, $sizeX86, $thinPPCForStat, $thinX86ForStat);
|
|
|
|
if (!$gDryRun) {
|
|
$thinPPCForStat = $thinPPC;
|
|
$thinX86ForStat = $thinX86;
|
|
}
|
|
else {
|
|
# Normally, fat source files will have been converted into temporary
|
|
# thin files. During a dry run, that doesn't happen, so fake it up
|
|
# a little bit by always using the source file, fat or thin, for the
|
|
# stat.
|
|
$thinPPCForStat = $sourcePPC;
|
|
$thinX86ForStat = $sourceX86;
|
|
}
|
|
|
|
if (!defined($sizePPC = $thinPPCForStat->statSize())) {
|
|
unlink(@tempThinFiles);
|
|
return complain(1, 'stat ppc: '.$!.' for:',
|
|
$thinPPCForStat->path());
|
|
}
|
|
if (!defined($sizeX86 = $thinX86ForStat->statSize())) {
|
|
unlink(@tempThinFiles);
|
|
return complain(1, 'stat x86: '.$!.' for:',
|
|
$thinX86ForStat->path());
|
|
}
|
|
|
|
$sizePPC = $sizePPC % 4096;
|
|
$sizeX86 = $sizeX86 % 4096;
|
|
|
|
my (@thinFiles);
|
|
|
|
if ($sizePPC == 0) {
|
|
# PPC image ends on an alignment boundary, there will be no padding before
|
|
# starting the x86 image.
|
|
@thinFiles = ($thinPPC->path(), $thinX86->path());
|
|
}
|
|
elsif ($sizeX86 == 0 || $sizeX86 > $sizePPC) {
|
|
# x86 image ends on an alignment boundary, there will be no padding before
|
|
# starting the PPC image, or the x86 image exceeds its alignment boundary
|
|
# by more than the PPC image, so there will be less padding if the x86
|
|
# comes first.
|
|
@thinFiles = ($thinX86->path(), $thinPPC->path());
|
|
}
|
|
else {
|
|
# PPC image exceeds its alignment boundary by more than the x86 image, so
|
|
# there will be less padding if the PPC comes first.
|
|
@thinFiles = ($thinPPC->path(), $thinX86->path());
|
|
}
|
|
|
|
my ($isExecutable);
|
|
$isExecutable = $sourcePPC->lIsExecutable() ||
|
|
$sourceX86->lIsExecutable();
|
|
|
|
if (!$gDryRun) {
|
|
# Ensure that the file does not yet exist.
|
|
|
|
# Set the execute bits (as allowed by the umask) on the new file if any
|
|
# execute bit is set on either old file. Yes, it is possible to have
|
|
# proper Mach-O files without x-bits: think object files (.o) and static
|
|
# archives (.a).
|
|
if (!createUniqueFile($targetPath, $isExecutable ? 0777 : 0666)) {
|
|
# createUniqueFile printed an error.
|
|
unlink(@tempThinFiles);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# Create the fat file.
|
|
if (command($gConfig{'cmd_lipo'}, '-create', @thinFiles,
|
|
'-output', $targetPath) != 0) {
|
|
unlink(@tempThinFiles, $targetPath);
|
|
return complain(1, 'lipo create fat failed for:',
|
|
@thinFiles,
|
|
$targetPath);
|
|
}
|
|
|
|
unlink(@tempThinFiles);
|
|
|
|
if (!$gDryRun) {
|
|
# lipo seems to think that it's free to set its own file modes that
|
|
# ignore the umask, which is bogus when the rest of this script
|
|
# respects the umask.
|
|
if (!chmod(($isExecutable ? 0777 : 0666) & ~umask(), $targetPath)) {
|
|
complain(1, 'makeUniversalFile: chmod: '.$!.' for',
|
|
$targetPath);
|
|
unlink($targetPath);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# makeUniversalInternal($isToplevel, $filePPC, $fileX86, $fileTargetPath)
|
|
#
|
|
# Given FileAttrCache objects $filePPC and $fileX86, compares filetypes
|
|
# and performs the appropriate action to produce a universal file at
|
|
# path string $fileTargetPath. $isToplevel should be true if this is
|
|
# the recursive base and false otherwise; this controls cleanup behavior
|
|
# (cleanup is only performed at the base, because cleanup itself is
|
|
# recursive).
|
|
#
|
|
# This handles regular files by determining whether they are Mach-O files
|
|
# and calling makeUniversalFile if so and copyIfIdentical otherwise. Symbolic
|
|
# links are handled directly in this function by ensuring that the source link
|
|
# targets are identical and creating a new link with the same target
|
|
# at $fileTargetPath. Directories are handled by calling
|
|
# makeUniversalDirectory.
|
|
#
|
|
# One of $filePPC and $fileX86 is permitted to be undef. In that case,
|
|
# the defined source file is copied directly to the target if a regular
|
|
# file, and symlinked appropriately if a symbolic link. This facilitates
|
|
# use of $gOnlyOne = 'copy', although no $gOnlyOne checks are made in this
|
|
# function, they are all handled in makeUniversalDirectory.
|
|
#
|
|
# Returns true on success. Returns false on failure, including failures
|
|
# in other functions called.
|
|
sub makeUniversalInternal($$$$) {
|
|
my ($filePPC, $fileTargetPath, $fileX86, $isToplevel);
|
|
($isToplevel, $filePPC, $fileX86, $fileTargetPath) = @_;
|
|
|
|
my ($typePPC, $typeX86);
|
|
if (defined($filePPC) && !defined($typePPC = $filePPC->lstatType())) {
|
|
return complain(1, 'makeUniversal: lstat ppc: '.$!.' for:',
|
|
$filePPC->path());
|
|
}
|
|
if (defined($fileX86) && !defined($typeX86 = $fileX86->lstatType())) {
|
|
return complain(1, 'makeUniversal: lstat x86: '.$!.' for:',
|
|
$fileX86->path());
|
|
}
|
|
|
|
if (defined($filePPC) && defined($fileX86) && $typePPC != $typeX86) {
|
|
return complain(1, 'makeUniversal: incompatible types:',
|
|
$filePPC->path(),
|
|
$fileX86->path());
|
|
}
|
|
|
|
# $aSourceFile will contain a FileAttrCache object that will return
|
|
# the correct type data. It's used because it's possible for one of
|
|
# the two source files to be undefined (indicating a straight copy).
|
|
my ($aSourceFile);
|
|
if (defined($filePPC)) {
|
|
$aSourceFile = $filePPC;
|
|
}
|
|
else {
|
|
$aSourceFile = $fileX86;
|
|
}
|
|
|
|
if ($aSourceFile->lIsDir()) {
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('mkdir '.(argumentEscape($fileTargetPath))[0]."\n");
|
|
}
|
|
if (!$gDryRun && !mkdir($fileTargetPath)) {
|
|
return complain(1, 'makeUniversal: mkdir: '.$!.' for:',
|
|
$fileTargetPath);
|
|
}
|
|
|
|
my ($rv);
|
|
|
|
if (!($rv = makeUniversalDirectory($filePPC, $fileX86, $fileTargetPath))) {
|
|
# makeUniversalDirectory printed an error.
|
|
if ($isToplevel) {
|
|
command($gConfig{'cmd_rm'},'-rf','--',$fileTargetPath);
|
|
}
|
|
}
|
|
else {
|
|
# Touch the directory when leaving it. If unify is being run on an
|
|
# .app bundle, the .app might show up without an icon because the
|
|
# system might have found the .app before it was completely built.
|
|
# Touching it dirties it in LaunchServices' mind.
|
|
if ($gVerbosity >= 3) {
|
|
print('touch '.(argumentEscape($fileTargetPath))[0]."\n");
|
|
}
|
|
utime(undef, undef, $fileTargetPath);
|
|
}
|
|
|
|
return $rv;
|
|
}
|
|
elsif ($aSourceFile->lIsSymLink()) {
|
|
my ($linkPPC, $linkX86);
|
|
if (defined($filePPC) && !defined($linkPPC=readlink($filePPC->path()))) {
|
|
return complain(1, 'makeUniversal: readlink ppc: '.$!.' for:',
|
|
$filePPC->path());
|
|
}
|
|
if (defined($fileX86) && !defined($linkX86=readlink($fileX86->path()))) {
|
|
return complain(1, 'makeUniversal: readlink x86: '.$!.' for:',
|
|
$fileX86->path());
|
|
}
|
|
if (defined($filePPC) && defined($fileX86) && $linkPPC ne $linkX86) {
|
|
return complain(1, 'makeUniversal: symbolic links differ:',
|
|
$filePPC->path(),
|
|
$fileX86->path());
|
|
}
|
|
|
|
# $aLink here serves the same purpose as $aSourceFile in the enclosing
|
|
# block: it refers to the target of the symbolic link, whether there
|
|
# is one valid source or two.
|
|
my ($aLink);
|
|
if (defined($linkPPC)) {
|
|
$aLink = $linkPPC;
|
|
}
|
|
else {
|
|
$aLink = $linkX86;
|
|
}
|
|
|
|
if ($gVerbosity >= 3 || $gDryRun) {
|
|
print('ln -s '.
|
|
join(' ',argumentEscape($aLink, $fileTargetPath))."\n");
|
|
}
|
|
if (!$gDryRun && !symlink($aLink, $fileTargetPath)) {
|
|
return complain(1, 'makeUniversal: symlink: '.$!.' for:',
|
|
$aLink,
|
|
$fileTargetPath);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
elsif($aSourceFile->lIsRegularFile()) {
|
|
my ($machPPC, $machX86, $fileName);
|
|
if (!defined($filePPC) || !defined($fileX86)) {
|
|
# One of the source files isn't present. The right thing to do is
|
|
# to just copy what does exist straight over, so skip Mach-O checks.
|
|
$machPPC = 0;
|
|
$machX86 = 0;
|
|
if (defined($filePPC)) {
|
|
$fileName = $filePPC;
|
|
} elsif (defined($fileX86)) {
|
|
$fileName = $fileX86;
|
|
} else {
|
|
complain(1, "The file must exist in at least one directory");
|
|
exit(1);
|
|
}
|
|
}
|
|
else {
|
|
# both files exist, pick the name of one.
|
|
$fileName = $fileX86;
|
|
if (!defined($machPPC=$filePPC->isMachO())) {
|
|
return complain(1, 'makeUniversal: isFileMachO ppc failed for:',
|
|
$filePPC->path());
|
|
}
|
|
if (!defined($machX86=$fileX86->isMachO())) {
|
|
return complain(1, 'makeUniversal: isFileMachO x86 failed for:',
|
|
$fileX86->path());
|
|
}
|
|
}
|
|
|
|
if ($machPPC != $machX86) {
|
|
return complain(1, 'makeUniversal: variant Mach-O attributes:',
|
|
$filePPC->path(),
|
|
$fileX86->path());
|
|
}
|
|
|
|
if ($machPPC) {
|
|
# makeUniversalFile will print an error if it fails.
|
|
return makeUniversalFile($filePPC, $fileX86, $fileTargetPath);
|
|
}
|
|
|
|
if (grep { $fileName->path() =~ m/$_/; } @gSortMatches) {
|
|
# Regular files, but should be compared with sorting first.
|
|
# copyIfIdenticalWhenSorted will print an error if it fails.
|
|
return copyIfIdenticalWhenSorted($filePPC, $fileX86, $fileTargetPath);
|
|
}
|
|
|
|
# Regular file. copyIfIdentical will print an error if it fails.
|
|
return copyIfIdentical($filePPC, $fileX86, $fileTargetPath);
|
|
}
|
|
|
|
# Special file, don't know how to handle.
|
|
return complain(1, 'makeUniversal: cannot handle special file:',
|
|
$filePPC->path(),
|
|
$fileX86->path());
|
|
}
|
|
|
|
# usage()
|
|
#
|
|
# Give the user a hand.
|
|
sub usage() {
|
|
print STDERR (
|
|
"usage: unify <ppc-path> <x86-path> <universal-path>\n".
|
|
" [--dry-run] (print what would be done)\n".
|
|
" [--only-one <action>] (skip, copy, fail; default=copy)\n".
|
|
" [--verbosity <level>] (0, 1, 2, 3; default=2)\n");
|
|
return;
|
|
}
|
|
|
|
# readZipCRCs($zipFile)
|
|
#
|
|
# $zipFile is the pathname to a zip file whose directory will be read.
|
|
# A reference to a hash is returned, with the member pathnames from the
|
|
# zip file as keys, and reasonably unique identifiers as values. The
|
|
# format of the values is not specified exactly, but does include the
|
|
# member CRCs and sizes and differentiates between files and directories.
|
|
# It specifically does not distinguish between modification times. On
|
|
# failure, prints a message and returns undef.
|
|
sub readZipCRCs($) {
|
|
my ($zipFile);
|
|
($zipFile) = @_;
|
|
|
|
my ($ze, $zip);
|
|
$zip = Archive::Zip->new();
|
|
|
|
if (($ze = $zip->read($zipFile)) != AZ_OK) {
|
|
complain(1, 'readZipCRCs: read error '.$ze.' for:',
|
|
$zipFile);
|
|
return undef;
|
|
}
|
|
|
|
my ($member, %memberCRCs, @memberList);
|
|
%memberCRCs = ();
|
|
@memberList = $zip->members();
|
|
|
|
foreach $member (@memberList) {
|
|
# Take a few of the attributes that identify the file and stuff them into
|
|
# the members hash. Directories will show up with size 0 and crc32 0,
|
|
# so isDirectory() is used to distinguish them from empty files.
|
|
$memberCRCs{$member->fileName()} = join(',', $member->isDirectory() ? 1 : 0,
|
|
$member->uncompressedSize(),
|
|
$member->crc32String());
|
|
}
|
|
|
|
return {%memberCRCs};
|
|
}
|
|
|
|
{
|
|
# FileAttrCache allows various attributes about a file to be cached
|
|
# so that if they are needed again after first use, no system calls
|
|
# will be made and the program won't need to hit the disk.
|
|
|
|
package FileAttrCache;
|
|
|
|
# from /usr/include/mach-o/loader.h
|
|
use constant MH_MAGIC => 0xfeedface;
|
|
use constant MH_CIGAM => 0xcefaedfe;
|
|
use constant MH_MAGIC_64 => 0xfeedfacf;
|
|
use constant MH_CIGAM_64 => 0xcffaedfe;
|
|
|
|
use Fcntl(':DEFAULT', ':mode');
|
|
|
|
# FileAttrCache->new($path)
|
|
#
|
|
# Creates a new FileAttrCache object for the file at path $path and
|
|
# returns it. The cache is not primed at creation time, values are
|
|
# fetched lazily as they are needed.
|
|
sub new($$) {
|
|
my ($class, $path, $proto, $this);
|
|
($proto, $path) = @_;
|
|
if (!($class = ref($proto))) {
|
|
$class = $proto;
|
|
}
|
|
$this = {
|
|
'path' => $path,
|
|
'lstat' => undef,
|
|
'lstatErrno' => 0,
|
|
'lstatInit' => 0,
|
|
'magic' => undef,
|
|
'magic2' => undef,
|
|
'magicErrno' => 0,
|
|
'magicErrMsg' => undef,
|
|
'magicInit' => 0,
|
|
'stat' => undef,
|
|
'statErrno' => 0,
|
|
'statInit' => 0,
|
|
};
|
|
bless($this, $class);
|
|
return($this);
|
|
}
|
|
|
|
# $FileAttrCache->isFat()
|
|
#
|
|
# Returns true if the file is a fat Mach-O file, false if it's not, and
|
|
# undef if an error occurs. See /usr/include/mach-o/fat.h.
|
|
sub isFat($) {
|
|
my ($magic, $magic2, $this);
|
|
($this) = @_;
|
|
|
|
# magic() caches, there's no separate cache because isFat() doesn't hit
|
|
# the disk other than by calling magic().
|
|
|
|
if (!defined($magic = $this->magic())) {
|
|
return undef;
|
|
}
|
|
$magic2 = $this->magic2();
|
|
|
|
# We have to sanity check the second four bytes, because Java class
|
|
# files use the same magic number as Mach-O fat binaries.
|
|
# This logic is adapted from file(1), which says that Mach-O uses
|
|
# these bytes to count the number of architectures within, while
|
|
# Java uses it for a version number. Conveniently, there are only
|
|
# 18 labelled Mach-O architectures, and Java's first released
|
|
# class format used the version 43.0.
|
|
if ($magic == 0xcafebabe && $magic2 < 20) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
# $FileAttrCache->isMachO()
|
|
#
|
|
# Returns true if the file is a Mach-O image (including a fat file), false
|
|
# if it's not, and undef if an error occurs. See
|
|
# /usr/include/mach-o/loader.h and /usr/include/mach-o/fat.h.
|
|
sub isMachO($) {
|
|
my ($magic, $this);
|
|
($this) = @_;
|
|
|
|
# magic() caches, there's no separate cache because isMachO() doesn't hit
|
|
# the disk other than by calling magic().
|
|
|
|
if (!defined($magic = $this->magic())) {
|
|
return undef;
|
|
}
|
|
|
|
# Accept Mach-O fat files or Mach-O thin files of either endianness.
|
|
if ($magic == MH_MAGIC ||
|
|
$magic == MH_CIGAM ||
|
|
$magic == MH_MAGIC_64 ||
|
|
$magic == MH_CIGAM_64 ||
|
|
$this->isFat()) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
# $FileAttrCache->isZip()
|
|
#
|
|
# Returns true if the file is a zip file, false if it's not, and undef if
|
|
# an error occurs. See http://www.pkware.com/business_and_developers/developer/popups/appnote.txt .
|
|
sub isZip($) {
|
|
my ($magic, $this);
|
|
($this) = @_;
|
|
|
|
# magic() caches, there's no separate cache because isFat() doesn't hit
|
|
# the disk other than by calling magic().
|
|
|
|
if (!defined($magic = $this->magic())) {
|
|
return undef;
|
|
}
|
|
|
|
if ($magic == 0x504b0304) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
# $FileAttrCache->lIsExecutable()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning true if the file is has any,
|
|
# execute bit set, false if none are set, or undef if an error occurs.
|
|
# On error, $! is set to lstat's errno.
|
|
sub lIsExecutable($) {
|
|
my ($mode, $this);
|
|
($this) = @_;
|
|
|
|
if (!defined($mode = $this->lstatMode())) {
|
|
return undef;
|
|
}
|
|
|
|
return $mode & (S_IXUSR | S_IXGRP | S_IXOTH);
|
|
}
|
|
|
|
# $FileAttrCache->lIsDir()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning true if the file is a directory,
|
|
# false if it isn't, or undef if an error occurs. Because lstat is used,
|
|
# this will return false even if the file is a symlink pointing to a
|
|
# directory. On error, $! is set to lstat's errno.
|
|
sub lIsDir($) {
|
|
my ($type, $this);
|
|
($this) = @_;
|
|
|
|
if (!defined($type = $this->lstatType())) {
|
|
return undef;
|
|
}
|
|
|
|
return S_ISDIR($type);
|
|
}
|
|
|
|
# $FileAttrCache->lIsRegularFile()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning true if the file is a regular,
|
|
# file, false if it isn't, or undef if an error occurs. Because lstat is
|
|
# used, this will return false even if the file is a symlink pointing to a
|
|
# regular file. On error, $! is set to lstat's errno.
|
|
sub lIsRegularFile($) {
|
|
my ($type, $this);
|
|
($this) = @_;
|
|
|
|
if (!defined($type = $this->lstatType())) {
|
|
return undef;
|
|
}
|
|
|
|
return S_ISREG($type);
|
|
}
|
|
|
|
# $FileAttrCache->lIsSymLink()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning true if the file is a symbolic,
|
|
# link, false if it isn't, or undef if an error occurs. On error, $! is
|
|
# set to lstat's errno.
|
|
sub lIsSymLink($) {
|
|
my ($type, $this);
|
|
($this) = @_;
|
|
|
|
if (!defined($type = $this->lstatType())) {
|
|
return undef;
|
|
}
|
|
|
|
return S_ISLNK($type);
|
|
}
|
|
|
|
# $FileAttrCache->lstat()
|
|
#
|
|
# Wraps the lstat system call, providing a cache to speed up multiple
|
|
# lstat calls for the same file. See lstat(2) and lstat in perlfunc(1).
|
|
sub lstat($) {
|
|
my (@stat, $this);
|
|
($this) = @_;
|
|
|
|
# Use the cached lstat result.
|
|
if ($$this{'lstatInit'}) {
|
|
if (defined($$this{'lstatErrno'})) {
|
|
$! = $$this{'lstatErrno'};
|
|
}
|
|
return @{$$this{'lstat'}};
|
|
}
|
|
$$this{'lstatInit'} = 1;
|
|
|
|
if (!(@stat = CORE::lstat($$this{'path'}))) {
|
|
$$this{'lstatErrno'} = $!;
|
|
}
|
|
|
|
$$this{'lstat'} = [@stat];
|
|
return @stat;
|
|
}
|
|
|
|
# $FileAttrCache->lstatMode()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning the mode bits from the st_mode
|
|
# field, or undef if an error occurs. On error, $! is set to lstat's
|
|
# errno.
|
|
sub lstatMode($) {
|
|
my (@stat, $this);
|
|
($this) = @_;
|
|
|
|
if (!(@stat = $this->lstat())) {
|
|
return undef;
|
|
}
|
|
|
|
return S_IMODE($stat[2]);
|
|
}
|
|
|
|
# $FileAttrCache->lstatType()
|
|
#
|
|
# Wraps $FileAttrCache->lstat(), returning the type bits from the st_mode
|
|
# field, or undef if an error occurs. On error, $! is set to lstat's
|
|
# errno.
|
|
sub lstatType($) {
|
|
my (@stat, $this);
|
|
($this) = @_;
|
|
|
|
if (!(@stat = $this->lstat())) {
|
|
return undef;
|
|
}
|
|
|
|
return S_IFMT($stat[2]);
|
|
}
|
|
|
|
# $FileAttrCache->magic()
|
|
#
|
|
# Returns the "magic number" for the file by reading its first four bytes
|
|
# as a big-endian unsigned 32-bit integer and returning the result. If an
|
|
# error occurs, returns undef and prints diagnostic messages to stderr. If
|
|
# the file is shorter than 32 bits, returns -1. A cache is provided to
|
|
# speed multiple magic calls for the same file.
|
|
sub magic($) {
|
|
my ($this);
|
|
($this) = @_;
|
|
|
|
# Use the cached magic result.
|
|
if ($$this{'magicInit'}) {
|
|
if (defined($$this{'magicErrno'})) {
|
|
if (defined($$this{'magicErrMsg'})) {
|
|
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
|
|
$$this{'path'});
|
|
}
|
|
$! = $$this{'magicErrno'};
|
|
}
|
|
return $$this{'magic'};
|
|
}
|
|
|
|
$$this{'magicInit'} = 1;
|
|
|
|
my ($fh);
|
|
if (!sysopen($fh, $$this{'path'}, O_RDONLY)) {
|
|
$$this{'magicErrno'} = $!;
|
|
$$this{'magicErrMsg'} = 'open "'.$$this{'path'}.'": '.$!;
|
|
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
|
|
$$this{'path'});
|
|
return undef;
|
|
}
|
|
|
|
$! = 0;
|
|
my ($bytes, $magic, $bytes2, $magic2);
|
|
if (!defined($bytes = sysread($fh, $magic, 4))) {
|
|
$$this{'magicErrno'} = $!;
|
|
$$this{'magicErrMsg'} = 'read "'.$$this{'path'}.'": '.$!;
|
|
complain(1, 'FileAttrCache::magic: '.$$this{'magicErrMsg'}.' for:',
|
|
$$this{'path'});
|
|
close($fh);
|
|
return undef;
|
|
}
|
|
else {
|
|
$bytes2 = sysread($fh, $magic2, 4);
|
|
}
|
|
|
|
close($fh);
|
|
|
|
if ($bytes != 4) {
|
|
# The file is too short, didn't read a magic number. This isn't really
|
|
# an error. Return an unlikely value.
|
|
$$this{'magic'} = -1;
|
|
$$this{'magic2'} = -1;
|
|
return -1;
|
|
}
|
|
if ($bytes2 != 4) {
|
|
# File is too short to read a second 4 bytes.
|
|
$magic2 = -1;
|
|
}
|
|
|
|
$$this{'magic'} = unpack('N', $magic);
|
|
$$this{'magic2'} = unpack('N', $magic2);
|
|
return $$this{'magic'};
|
|
}
|
|
|
|
# $FileAttrCache->magic2()
|
|
#
|
|
# Returns the second four bytes of the file as a 32-bit little endian number.
|
|
# See magic(), above for more info.
|
|
sub magic2($) {
|
|
my ($this);
|
|
($this) = @_;
|
|
|
|
# we do the actual work (and cache it) in magic().
|
|
if (!$$this{'magicInit'}) {
|
|
my $magic = $$this->magic();
|
|
}
|
|
|
|
return $$this{'magic2'};
|
|
}
|
|
|
|
# $FileAttrCache->path()
|
|
#
|
|
# Returns the file's pathname.
|
|
sub path($) {
|
|
my ($this);
|
|
($this) = @_;
|
|
return $$this{'path'};
|
|
}
|
|
|
|
# $FileAttrCache->stat()
|
|
#
|
|
# Wraps the stat system call, providing a cache to speed up multiple
|
|
# stat calls for the same file. If lstat() has already been called and
|
|
# the file is not a symbolic link, the cached lstat() result will be used.
|
|
# See stat(2) and lstat in perlfunc(1).
|
|
sub stat($) {
|
|
my (@stat, $this);
|
|
($this) = @_;
|
|
|
|
# Use the cached stat result.
|
|
if ($$this{'statInit'}) {
|
|
if (defined($$this{'statErrno'})) {
|
|
$! = $$this{'statErrno'};
|
|
}
|
|
return @{$$this{'stat'}};
|
|
}
|
|
|
|
$$this{'statInit'} = 1;
|
|
|
|
# If lstat has already been called, and the file isn't a symbolic link,
|
|
# use the cached lstat result.
|
|
if ($$this{'lstatInit'} && !$$this{'lstatErrno'} &&
|
|
!S_ISLNK(${$$this{'lstat'}}[2])) {
|
|
$$this{'stat'} = $$this{'lstat'};
|
|
return @{$$this{'stat'}};
|
|
}
|
|
|
|
if (!(@stat = CORE::stat($$this{'path'}))) {
|
|
$$this{'statErrno'} = $!;
|
|
}
|
|
|
|
$$this{'stat'} = [@stat];
|
|
return @stat;
|
|
}
|
|
|
|
# $FileAttrCache->statSize()
|
|
#
|
|
# Wraps $FileAttrCache->stat(), returning the st_size field, or undef
|
|
# undef if an error occurs. On error, $! is set to stat's errno.
|
|
sub statSize($) {
|
|
my (@stat, $this);
|
|
($this) = @_;
|
|
|
|
if (!(@stat = $this->lstat())) {
|
|
return undef;
|
|
}
|
|
|
|
return $stat[7];
|
|
}
|
|
}
|