Skip to content
This repository
Fetching contributors…

Cannot retrieve contributors at this time

executable file 384 lines (289 sloc) 15.439 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
#!/usr/bin/perl -w
#
my $versionStr = '$Id: buildDMG.pl,v 1.9 2004/03/29 21:20:32 westheide Exp $';
#
# Created by Jšrg Westheide on Fri Feb 13 2003.
# Copyright (c) 2003, 2004 Jšrg Westheide. All rights reserved.
#
# Permission to use, copy, modify and distribute this software and its documentation
# is hereby granted, provided that both the copyright notice and this permission
# notice appear in all copies of the software, derivative works or modified versions,
# and any portions thereof, and that both notices appear in supporting documentation,
# and that credit is given to Jšrg Westheide in all documents and publicity
# pertaining to direct or indirect use of this code or its derivatives.
#
# THIS IS EXPERIMENTAL SOFTWARE AND IT IS KNOWN TO HAVE BUGS, SOME OF WHICH MAY HAVE
# SERIOUS CONSEQUENCES. THE COPYRIGHT HOLDER ALLOWS FREE USE OF THIS SOFTWARE IN ITS
# "AS IS" CONDITION. THE COPYRIGHT HOLDER DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY
# DAMAGES WHATSOEVER RESULTING DIRECTLY OR INDIRECTLY FROM THE USE OF THIS SOFTWARE
# OR OF ANY DERIVATIVE WORK.
#
# For the most recent version see <http://www.objectpark.org>
#
use strict;
use diagnostics;
use Getopt::Long;
use Cwd;

my $version;
my $debug;
my $help;
my $output;
my $err;
my $minVolSize = 5; # minimum size of a dmg volume in MB

# determine the build directory, compression level, the list of files to copy, and the size of the dmg volume
# from the environment unless set from the command line
my $buildDir = $ENV{BUILT_PRODUCTS_DIR};
my $compressionLevel = $ENV{DMG_COMPRESSIONLEVEL};
my $volSize = $ENV{DMG_VOLSIZE};
my $volName = $ENV{DMG_VOLNAME};
my $dmgName = $ENV{DMG_NAME};
my $internetEnabled = $ENV{DMG_INTERNETENABLED};
my $slaRsrcFile = $ENV{DMG_SLA_RSRCFILE};
my $deleteHeaders = ($ENV{DMG_DELETEHEADERS} && ($ENV{DMG_DELETEHEADERS} =~ /^\s*yes\s*$/i));
my $volIcon = $ENV{DMG_VOLICON};
my $files;
my $FlatCarbon;

# override them with command line options
GetOptions('help' => \$help,
           'version' => \$version,
           'buildDir=s' => \$buildDir,
           'compressionLevel=i' => \$compressionLevel,
           'debug' => \$debug,
           'deleteHeaders!' => \$deleteHeaders,
           'dmgName=s' => \$dmgName,
           'internetEnabled!' => \$internetEnabled,
           'slaRsrcFile=s' => \$slaRsrcFile,
           'volSize=i' => \$volSize,
           'volName=s' => \$volName,
           'volIcon=s' => \$volIcon
          );

if ($help) {
    print `perldoc $0`;
    exit 0;
}

if ($version) {
    my ($prog, $version) = ($versionStr =~ /:\s*(\w+).pl\S*\s+(\d+\.?\d*)/);
    print "$prog v$version\n";
    exit 0;
}

my $firstFile = $ARGV[0]; # save an unescaped version, we may need it for the dmg's name
for (my $i = @ARGV-1; $i >= 0; $i--) {
    $ARGV[$i] =~ s/ /\\ /g; # escape spaces (we pass the files on the command line)
}

die "FATAL: No files to copy specified\n" unless @ARGV or $ENV{DMG_FILESLIST};

$files = join(' ', @ARGV);
$files .= " $ENV{DMG_FILESLIST}" if $ENV{DMG_FILESLIST};

$buildDir = cwd() unless $buildDir;

# determine dmg and volume name
if (my $settings = readSettings()) {
    my ($name) = ($settings =~ /<key>CFBundleName<\/key>.*?<string>(.*?)<\/string>/is);
    my ($version) = ($settings =~ /<key>CFBundleVersion<\/key>.*?<string>(.*?)<\/string>/is);
    $volName = "$name $version" unless $volName;
    unless ($dmgName) {
        $dmgName = "$name $version";
        $dmgName =~ tr/ ./__/;
    }
}

unless ($ENV{SETTINGS_FILE}) {
    $dmgName = $firstFile unless $dmgName;
    $dmgName =~ s#.*/([^/]+)$#$1#; # we have to cut off the path
    $dmgName =~ s/(.*?)(\.[^.]*)?$/$1/; # cut off the extension
    $volName = $dmgName unless $volName;
}

# if ProjectBuilder asks us to "clean" we remove the dmg. if we determined the name ourself, we cannot determine
# it now since PB has already deleted the settings file :-(. So we delete all dmgs in the build directory
if ($ENV{ACTION} && $ENV{ACTION} =~ /clean/i) {
    $dmgName = '*' unless $dmgName;
    print glob "$buildDir/$dmgName.dmg";
    unlink glob "$buildDir/$dmgName.dmg";
    exit 0;
}

# if requested determine required size for the dmg
unless ($volSize && ($volSize > 0)) {
    eval { $output = `du -csk $files`};
    die "Couldn't determine the required space for the dmg: $@\n" if $@;
    
    ($volSize) = ($output =~ /\s*(\d+)\s+total\s*$/si);
    $volSize = int $volSize * 1.5 / 1024 + 1;
    $volSize = $minVolSize if $volSize < $minVolSize;
}

# OK, we have determined all out parameters.

# print them for debugging
if ($debug) {
    print STDERR "buildDir: ", $buildDir ? $buildDir : "", "\n";
    print STDERR "compressionLevel: ", $compressionLevel ? $compressionLevel : "", "\n";
    print STDERR "volSize: ", $volSize ? $volSize : "", "\n";
    print STDERR "volName: ", $volName ? $volName : "", "\n";
    print STDERR "VolIcon: ", $volIcon ? $volIcon : "", "\n";
    print STDERR "dmgName: ", $dmgName ? $dmgName : "", "\n";
    print STDERR "internetEnabled: ", $internetEnabled ? $internetEnabled : "", "\n";
    print STDERR "slaRsrcFile: ", $slaRsrcFile ? $slaRsrcFile : "", "\n";
    print STDERR "deleteHeaders: ", $deleteHeaders ? $deleteHeaders : "", "\n";
    print STDERR "files: ", $files ? $files : "", "\n";
}

# Now we start our work...

# create the dmg
print "> hdiutil create \"$buildDir/$dmgName\" -ov -megabytes $volSize -fs HFS+ -volname \"$volName\"\n" if $debug;
$output = `hdiutil create \"$buildDir/$dmgName\" -ov -megabytes $volSize -fs HFS+ -volname \"$volName\"`;
die "FATAL: Couldn't create dmg $dmgName (Error: $?)\nIs it possibly mounted?\n" if $?;

($dmgName) = ($output =~ /created\s*:\s*(?:.*?$buildDir\/)?(.+?)\s*$/m);
die "FATAL: Couldn't read created dmg name\n" unless $dmgName;

print "Changed dmgName to \"$dmgName\"\n" if $debug;

# mount the dmg
print "> hdiutil attach -nobrowse \"$buildDir/$dmgName\"\n" if $debug;
$output = `hdiutil attach -nobrowse \"$buildDir/$dmgName\"`;
die "FATAL: Couldn't mount DMG $dmgName (Error: $?)\n" if $?;

my ($dev) = ($output =~ /(\/dev\/.+?)\s*GUID_partition_scheme/im);
my ($dest) = ($output =~ /Apple_HFS\s+(.+?)\s*$/im);

# copy the files onto the dmg
print "Copying files to $dest...\n";
print "> CpMac -r $files \"$dest\"\n" if $debug;
$output = `CpMac -r $files \"$dest\"`;

die "FATAL: Error while copying files (Error: $err)\n" if $?;

# set custom volume icon
if ($volIcon) {
    print "Setting custom volume icon...\n";
    print "> cp $volIcon $dest/.VolumeIcon.icns\n" if $debug;
    $output = `cp $volIcon \"$dest/.VolumeIcon.icns\"`;
    print "FATAL: Failed to copy custom icon file\n" if $?;

    print "> SetFile -c icnC $dest/.VolumeIcon.icns\n" if $debug;
    $output = `SetFile -c icnC $dest/.VolumeIcon.icns`;
    print "FATAL: Failed to set custom icon flags on file\n" if $?;

    print "> SetFile -a C $dest\n" if $debug;
    $output = `SetFile -a C $dest`;
    print "FATAL: Failed to set custom icon flags on volume\n" if $?;
}

# delete headers
if ($deleteHeaders) {
    print "Deleting header files and directories...\n";
    print "> find -E -d \"$dest\" -regex \".*/(Private)?Headers\" -exec rm -rf {} \";\"\n" if $debug;
    $output = `find -E -d "$dest" -regex ".*/(Private)?Headers" -exec rm -rf {} ";"`;
}

# unmount the dmg
print "> hdiutil detach $dev\n" if $debug;
$output = `hdiutil detach $dev`;
die "FATAL: Couldn't unmount device $dev: $?\n" if $?;

# compress the dmg
my $tmpDmgName = "$dmgName~";

if ($compressionLevel) {
    print "Compressing $dmgName...\n";
    print "> mv -f \"$buildDir/$dmgName\" \"$buildDir/$tmpDmgName\"\n" if $debug;
    $output = `mv -f "$buildDir/$dmgName" "$buildDir/$tmpDmgName"`;
    
    print "> hdiutil convert \"$buildDir/$tmpDmgName\" -format UDZO -imagekey zlib-level=$compressionLevel -o \"$buildDir/$dmgName\"\n" if $debug;
    $output = `hdiutil convert "$buildDir/$tmpDmgName" -format UDZO -imagekey zlib-level=$compressionLevel -o "$buildDir/$dmgName"`;
    die "Error: Couldn't compress the dmg $dmgName: $?\n" if $?;
    
    unlink "$buildDir/$tmpDmgName";
}

# Adding the SLA
if ($slaRsrcFile) {
    print "Adding SLA...\n";
    print "> hdiutil unflatten \"$buildDir/$dmgName\"\n" if $debug;
    $output = `hdiutil unflatten \"$buildDir/$dmgName"`;
    die "Couldn't unflatten dmg (Error:$?)\n" if $?;
    
    unless ($?) {
        if (-e "/Applications/Xcode.app/Contents/Developer/Headers/FlatCarbon") {
            ## Xcode 4.3
            $FlatCarbon = "/Applications/Xcode.app/Contents/Developer/Headers/FlatCarbon";
        } else {
            $FlatCarbon = "/Developer/Headers/FlatCarbon";
        }
        
        print "> Rez $FlatCarbon/*.r \"$slaRsrcFile\" -a -o \"$buildDir/$dmgName\"\n" if $debug;
        $output = `Rez $FlatCarbon/*.r "$slaRsrcFile" -a -o "$buildDir/$dmgName"`;
        print STDERR "Couldn't add SLA (Error: $?)\n" if $?;
        
        print "> hdiutil flatten \"$buildDir/$dmgName\"\n" if $debug;
        $output = `hdiutil flatten "$buildDir/$dmgName"`;
        die "Couldn't flatten dmg (Error: $?)\n" if $?;
    }
}

# Enabling internet access
if ($internetEnabled) {
    print "> hdiutil internet-enable -yes \"$buildDir/$dmgName\"\n" if $debug;
    $output = `hdiutil internet-enable -yes "$buildDir/$dmgName"`;
    print STDERR "Couldn't enable internet access for $dmgName (Error: $?)\n" if $?;
}

print "Done.\n";

exit 0;



sub readSettings {
    return undef unless $ENV{SETTINGS_FILE};
    return undef if ($ENV{ACTION} =~ /clean/i) && !(-s $ENV{SETTINGS_FILE});
    
    my $settings;
    my $oldSep = $/;
    undef $/;
    
    open FH, "<$ENV{SETTINGS_FILE}" or die "Couldn't read file $ENV{SETTINGS_FILE}\n";
    $settings = <FH>;
    close FH;
    
    $/ = $oldSep;
    
    return $settings;
}


=head1 NAME

B<buildDMG> - build a DMG from the commandline or from inside ProjectBuilder

=head1 SYNOPSIS

buildDMG.pl [-help] [-version] [-debug] [-buildDir dir] [-compressionLevel n] [-deleteHeaders] [-dmgName name] [-slaRsrcFile file] [-volName name]
[-volSize n] files...

=head1 DESCRIPTION

buildDMG can be used to create a dmg either from command line or within ProjectBuilder. The special support for ProjectBuilder consist
of evaluating environment variables and creating volume and dmg names from the project's settings file.

The following options are available (and override the mentioned environment variables):

=over 4

=item B<-buildDir> I<directory>

specifies the I<directory> in which the dmg should be created. If this option is not specified the value of the environment variable
B<BUILT_PRODUCTS_DIR> (which is automatically provided by ProjectBuilder). If no value is provided the default will be the current
directory

=item B<-compressionLevel> I<n>

specifies the compression level for zlib compression. Legal values for I<n> are 1-9 with 1 being fastet, 9 best compression. 0 turns
compression off. The corresponding environment variable is B<DMG_COMPRESSIONLEVEL>. The default is 0 (no compression)

=item B<-debug>

enables output of debug information

=item B<-[no]deleteHeaders>

specifies whether all the folders "Headers" and "PrivateHeaders" on the dmg should be deleted or not. The environment variable is
B<DMG_DELETEHEADERS>, the default is not to delete

=item B<-dmgName> I<name>

specifies the I<name> of the dmg to produce (without extension). The corresponding environment variable is B<DMG_NAME>. If neither
the option, nor the environment variable contains a I<name>, nor a settings file is specified (see environment variable
B<SETTINGS_FILE> in the Project Builder Support section below) the name of the first file will be used

=item B<-help>

displays this documentation

=item B<-[no]internetEnabled>

specifies whether the dmg should be enabled for internet access or not (default). Seems this works only works with compressed dmgs,
but since that is a "feature" of B<hdiutil> this is not enforced by buildDMG

=item B<-slaRsrcFile> I<file>

specifies the .r I<file> containing the source of the resources for the software license agreement to display when the dmg is mounted.
The corresponding environment variable is B<DMG_SLA_RSRCFILE>. The source will be compiled with the Rez command and the result
attached to the dmg

=item B<-version>

displays the version number

=item B<-volName> I<name>

specifies the I<name> of the volume inside the dmg. The corresponding environment variable is B<DMG_VOLNAME>. If neither the option,
nor the environment variable contains a I<name>, nor a settings file is specified (see environment variable B<SETTINGS_FILE> in the
Project Builder Support section below) the name of the first file will be used

=item B<-volSize> I<n>

specifies the size of the volume to create in megabytes. The environment variable is B<DMG_VOLSIZE>. If no value or 0 is specified
B<buildDmg> will try to determine the size by looking at the files to copy

=item B<-volIcon> I<file>

specifies the custom icon to use as the volume icon. The environment variable is B<DMG_VOLICO>. If no value is specified, no custom
icon will be used.

=back

The B<files> specified as parameters AND the files specified in the environment variable B<DMG_FILESLIST> are copied onto the dmg
(before the headers are deleted), starting with the files from the command line

=head1 PROJECT BUILDER SUPPORT

Due to the possibility to use environment variables instead of the above mentioned command line options you can use this script from
a "Legacy Makefile" target. Therefore you have to set the build tool to "/usr/bin/perl", the arguments to "<pathToScript>/buildDMG.pl",
and check the "Pass build settings in environment" checkbox. You then can control everything with the build settings. If you make this
target depending on your "application target" you can build you app and put it in a dmg with a single click

The B<SETTINGS_FILE> environment variable is only used if the dmg or volume name is not specified. If B<SETTINGS_FILE> is set it
should point to the "Info.plist" of the project to copy onto the dmg. buildDMG is then able to automatically generate the dmg and
volume name from the B<CFBundleName> and B<CFBundleVersion> entries. For the dmg name some characters which may be problematic
are then replaced by an underscore ('_')

When cleaning the target there is a problem with Project Builder cleaning the dependent target first, so chances are good that the file
specified in B<SETTINGS_FILE> is not existing anymore. If so buildDMG deletes all dmg files in B<buildDir>

=head1 EXAMPLES

C</usr/bin/perl buildDMG.pl>

This is the way buildDMG can be called when all required environment variables are set (e.g. from ProjectBuilder)

C<./buildDMG.pl -dmgName Name -buildDir build -volSize 10 -volName Volume -compressionLevel 9 -slaRsrcFile SLA.r Example.app
-deleteHeaders>

This creates a dmg called "Name.dmg" in the directory "build". It contains a 10 MB volume named "Volume" and is compressed with the
highest compression level. The source for the SLA is obtained from the file SLA.r and the file (or file tree) "Example.app" is copied
onto the dmg, with header directories removed (after copying!)

=head1 AUTHOR

Joerg Westheide (joerg@objectpark.org)

=head1 SEE ALSO

Rez(1), hdiutil(1)
Something went wrong with that request. Please try again.