1#!/usr/bin/perl -w 2# 3my $versionStr = '$Id: buildDMG.pl,v 1.1.1.1 2004-12-17 21:52:17 russw Exp $'; 4# 5# Created by J�rg Westheide on Fri Feb 13 2003. 6# Copyright (c) 2003, 2004 J�rg Westheide. All rights reserved. 7# 8# Permission to use, copy, modify and distribute this software and its documentation 9# is hereby granted, provided that both the copyright notice and this permission 10# notice appear in all copies of the software, derivative works or modified versions, 11# and any portions thereof, and that both notices appear in supporting documentation, 12# and that credit is given to J�rg Westheide in all documents and publicity 13# pertaining to direct or indirect use of this code or its derivatives. 14# 15# THIS IS EXPERIMENTAL SOFTWARE AND IT IS KNOWN TO HAVE BUGS, SOME OF WHICH MAY HAVE 16# SERIOUS CONSEQUENCES. THE COPYRIGHT HOLDER ALLOWS FREE USE OF THIS SOFTWARE IN ITS 17# "AS IS" CONDITION. THE COPYRIGHT HOLDER DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY 18# DAMAGES WHATSOEVER RESULTING DIRECTLY OR INDIRECTLY FROM THE USE OF THIS SOFTWARE 19# OR OF ANY DERIVATIVE WORK. 20# 21# For the most recent version see <http://www.objectpark.org> 22# 23use strict; 24use diagnostics; 25use Getopt::Long; 26use Cwd; 27 28my $version; 29my $debug; 30my $help; 31my $output; 32my $err; 33my $minVolSize = 5; # minimum size of a dmg volume in MB 34 35# determine the build directory, compression level, the list of files to copy, and the size of the dmg volume 36# from the environment unless set from the command line 37my $buildDir = $ENV{BUILT_PRODUCTS_DIR}; 38my $compressionLevel = $ENV{DMG_COMPRESSIONLEVEL}; 39my $volSize = $ENV{DMG_VOLSIZE}; 40my $volName = $ENV{DMG_VOLNAME}; 41my $dmgName = $ENV{DMG_NAME}; 42my $internetEnabled = $ENV{DMG_INTERNETENABLED}; 43my $slaRsrcFile = $ENV{DMG_SLA_RSRCFILE}; 44my $deleteHeaders = ($ENV{DMG_DELETEHEADERS} && ($ENV{DMG_DELETEHEADERS} =~ /^\s*yes\s*$/i)); 45my $files; 46 47# override them with command line options 48GetOptions('help' => \$help, 49 'version' => \$version, 50 'buildDir=s' => \$buildDir, 51 'compressionLevel=i' => \$compressionLevel, 52 'debug' => \$debug, 53 'deleteHeaders!' => \$deleteHeaders, 54 'dmgName=s' => \$dmgName, 55 'internetEnabled!' => \$internetEnabled, 56 'slaRsrcFile=s' => \$slaRsrcFile, 57 'volSize=i' => \$volSize, 58 'volName=s' => \$volName 59 ); 60 61if ($help) { 62 print `perldoc $0`; 63 exit 0; 64} 65 66if ($version) { 67 my ($prog, $version) = ($versionStr =~ /:\s*(\w+).pl\S*\s+(\d+\.?\d*)/); 68 print "$prog v$version\n"; 69 exit 0; 70} 71 72my $firstFile = $ARGV[0]; # save an unescaped version, we may need it for the dmg's name 73for (my $i = @ARGV-1; $i >= 0; $i--) { 74 $ARGV[$i] =~ s/ /\\ /g; # escape spaces (we pass the files on the command line) 75} 76 77die "FATAL: No files to copy specified\n" unless @ARGV or $ENV{DMG_FILESLIST}; 78 79$files = join(' ', @ARGV); 80$files .= " $ENV{DMG_FILESLIST}" if $ENV{DMG_FILESLIST}; 81 82$buildDir = cwd() unless $buildDir; 83 84# determine dmg and volume name 85if (my $settings = readSettings()) { 86 my ($name) = ($settings =~ /<key>CFBundleName<\/key>.*?<string>(.*?)<\/string>/is); 87 my ($version) = ($settings =~ /<key>CFBundleVersion<\/key>.*?<string>(.*?)<\/string>/is); 88 $volName = "$name $version" unless $volName; 89 unless ($dmgName) { 90 $dmgName = "$name $version"; 91 $dmgName =~ tr/ ./__/; 92 } 93} 94 95unless ($ENV{SETTINGS_FILE}) { 96 $dmgName = $firstFile unless $dmgName; 97 $dmgName =~ s#.*/([^/]+)$#$1#; # we have to cut off the path 98 $dmgName =~ s/(.*?)(\.[^.]*)?$/$1/; # cut off the extension 99 $volName = $dmgName unless $volName; 100} 101 102# if ProjectBuilder asks us to "clean" we remove the dmg. if we determined the name ourself, we cannot determine 103# it now since PB has already deleted the settings file :-(. So we delete all dmgs in the build directory 104if ($ENV{ACTION} && $ENV{ACTION} =~ /clean/i) { 105 $dmgName = '*' unless $dmgName; 106 print glob "$buildDir/$dmgName.dmg"; 107 unlink glob "$buildDir/$dmgName.dmg"; 108 exit 0; 109} 110 111# if requested determine required size for the dmg 112unless ($volSize && ($volSize > 0)) { 113 eval { $output = `du -csk $files`}; 114 die "Couldn't determine the required space for the dmg: $@\n" if $@; 115 116 ($volSize) = ($output =~ /\s*(\d+)\s+total\s*$/si); 117 $volSize = int $volSize * 1.5 / 1024 + 1; 118 $volSize = $minVolSize if $volSize < $minVolSize; 119} 120 121# OK, we have determined all out parameters. 122 123# print them for debugging 124if ($debug) { 125 print STDERR "buildDir: ", $buildDir ? $buildDir : "", "\n"; 126 print STDERR "compressionLevel: ", $compressionLevel ? $compressionLevel : "", "\n"; 127 print STDERR "volSize: ", $volSize ? $volSize : "", "\n"; 128 print STDERR "volName: ", $volName ? $volName : "", "\n"; 129 print STDERR "dmgName: ", $dmgName ? $dmgName : "", "\n"; 130 print STDERR "internetEnabled: ", $internetEnabled ? $internetEnabled : "", "\n"; 131 print STDERR "slaRsrcFile: ", $slaRsrcFile ? $slaRsrcFile : "", "\n"; 132 print STDERR "deleteHeaders: ", $deleteHeaders ? $deleteHeaders : "", "\n"; 133 print STDERR "files: ", $files ? $files : "", "\n"; 134} 135 136# Now we start our work... 137 138# create the dmg 139print "> hdiutil create \"$buildDir/$dmgName\" -ov -megabytes $volSize -fs HFS+ -volname \"$volName\"\n" if $debug; 140$output = `hdiutil create \"$buildDir/$dmgName\" -ov -megabytes $volSize -fs HFS+ -volname \"$volName\"`; 141die "FATAL: Couldn't create dmg $dmgName (Error: $?)\nIs it possibly mounted?\n" if $?; 142 143($dmgName) = ($output =~ /created\s*:\s*(?:.*?$buildDir\/)?(.+?)\s*$/m); 144die "FATAL: Couldn't read created dmg name\n" unless $dmgName; 145 146print "Changed dmgName to \"$dmgName\"\n" if $debug; 147 148# mount the dmg 149print "> hdiutil attach \"$buildDir/$dmgName\"\n" if $debug; 150$output = `hdiutil attach \"$buildDir/$dmgName\"`; 151die "FATAL: Couldn't mount DMG $dmgName (Error: $?)\n" if $?; 152 153my ($dev) = ($output =~ /(\/dev\/.+?)\s*Apple_partition_scheme/im); 154my ($dest) = ($output =~ /Apple_HFS\s+(.+?)\s*$/im); 155 156# copy the files onto the dmg 157print "Copying files to $dest...\n"; 158print "> /Developer/Tools/CpMac -r $files \"$dest\"\n" if $debug; 159$output = `/Developer/Tools/CpMac -r $files \"$dest\"`; 160$err = $?; 161 162# delete headers 163if ($deleteHeaders) { 164 print "Deleting header files and directories...\n"; 165 print "> find -E -d \"$dest\" -regex \".*/(Private)?Headers\" -exec rm -rf {} \";\"\n" if $debug; 166 $output = `find -E -d "$dest" -regex ".*/(Private)?Headers" -exec rm -rf {} ";"`; 167} 168 169# unmount the dmg 170print "> hdiutil detach $dev\n" if $debug; 171$output = `hdiutil detach $dev`; 172die "FATAL: Error while copying files (Error: $err)\n" if $err; 173die "FATAL: Couldn't unmount device $dev: $?\n" if $?; 174 175# compress the dmg 176my $tmpDmgName = "$dmgName~"; 177 178if ($compressionLevel) { 179 print "Compressing $dmgName...\n"; 180 print "> mv -f \"$buildDir/$dmgName\" \"$buildDir/$tmpDmgName\"\n" if $debug; 181 $output = `mv -f "$buildDir/$dmgName" "$buildDir/$tmpDmgName"`; 182 183 print "> hdiutil convert \"$buildDir/$tmpDmgName\" -format UDZO -imagekey zlib-level=$compressionLevel -o \"$buildDir/$dmgName\"\n" if $debug; 184 $output = `hdiutil convert "$buildDir/$tmpDmgName" -format UDZO -imagekey zlib-level=$compressionLevel -o "$buildDir/$dmgName"`; 185 die "Error: Couldn't compress the dmg $dmgName: $?\n" if $?; 186 187 unlink "$buildDir/$tmpDmgName"; 188} 189 190# Adding the SLA 191if ($slaRsrcFile) { 192 print "Adding SLA...\n"; 193 print "> hdiutil unflatten \"$buildDir/$dmgName\"\n" if $debug; 194 $output = `hdiutil unflatten \"$buildDir/$dmgName"`; 195 die "Couldn't unflatten dmg (Error:$?)\n" if $?; 196 197 unless ($?) { 198 print "> /Developer/Tools/Rez /Developer/Headers/FlatCarbon/*.r \"$slaRsrcFile\" -a -o \"$buildDir/$dmgName\"\n" if $debug; 199 $output = `/Developer/Tools/Rez /Developer/Headers/FlatCarbon/*.r "$slaRsrcFile" -a -o "$buildDir/$dmgName"`; 200 print STDERR "Couldn't add SLA (Error: $?)\n" if $?; 201 202 print "> hdiutil flatten \"$buildDir/$dmgName\"\n" if $debug; 203 $output = `hdiutil flatten "$buildDir/$dmgName"`; 204 die "Couldn't flatten dmg (Error: $?)\n" if $?; 205 } 206} 207 208# Enabling internet access 209if ($internetEnabled) { 210 print "> hdiutil internet-enable -yes \"$buildDir/$dmgName\"\n" if $debug; 211 $output = `hdiutil internet-enable -yes "$buildDir/$dmgName"`; 212 print STDERR "Couldn't enable internet access for $dmgName (Error: $?)\n" if $?; 213} 214 215print "Done.\n"; 216 217exit 0; 218 219 220 221sub readSettings { 222 return undef unless $ENV{SETTINGS_FILE}; 223 return undef if ($ENV{ACTION} =~ /clean/i) && !(-s $ENV{SETTINGS_FILE}); 224 225 my $settings; 226 my $oldSep = $/; 227 undef $/; 228 229 open FH, "<$ENV{SETTINGS_FILE}" or die "Couldn't read file $ENV{SETTINGS_FILE}\n"; 230 $settings = <FH>; 231 close FH; 232 233 $/ = $oldSep; 234 235 return $settings; 236} 237 238 239=head1 NAME 240 241B<buildDMG> - build a DMG from the commandline or from inside ProjectBuilder 242 243=head1 SYNOPSIS 244 245buildDMG.pl [-help] [-version] [-debug] [-buildDir dir] [-compressionLevel n] [-deleteHeaders] [-dmgName name] [-slaRsrcFile file] [-volName name] 246[-volSize n] files... 247 248=head1 DESCRIPTION 249 250buildDMG can be used to create a dmg either from command line or within ProjectBuilder. The special support for ProjectBuilder consist 251of evaluating environment variables and creating volume and dmg names from the project's settings file. 252 253The following options are available (and override the mentioned environment variables): 254 255=over 4 256 257=item B<-buildDir> I<directory> 258 259specifies the I<directory> in which the dmg should be created. If this option is not specified the value of the environment variable 260B<BUILT_PRODUCTS_DIR> (which is automatically provided by ProjectBuilder). If no value is provided the default will be the current 261directory 262 263=item B<-compressionLevel> I<n> 264 265specifies the compression level for zlib compression. Legal values for I<n> are 1-9 with 1 being fastet, 9 best compression. 0 turns 266compression off. The corresponding environment variable is B<DMG_COMPRESSIONLEVEL>. The default is 0 (no compression) 267 268=item B<-debug> 269 270enables output of debug information 271 272=item B<-[no]deleteHeaders> 273 274specifies whether all the folders "Headers" and "PrivateHeaders" on the dmg should be deleted or not. The environment variable is 275B<DMG_DELETEHEADERS>, the default is not to delete 276 277=item B<-dmgName> I<name> 278 279specifies the I<name> of the dmg to produce (without extension). The corresponding environment variable is B<DMG_NAME>. If neither 280the option, nor the environment variable contains a I<name>, nor a settings file is specified (see environment variable 281B<SETTINGS_FILE> in the Project Builder Support section below) the name of the first file will be used 282 283=item B<-help> 284 285displays this documentation 286 287=item B<-[no]internetEnabled> 288 289specifies whether the dmg should be enabled for internet access or not (default). Seems this works only works with compressed dmgs, 290but since that is a "feature" of B<hdiutil> this is not enforced by buildDMG 291 292=item B<-slaRsrcFile> I<file> 293 294specifies the .r I<file> containing the source of the resources for the software license agreement to display when the dmg is mounted. 295The corresponding environment variable is B<DMG_SLA_RSRCFILE>. The source will be compiled with the Rez command and the result 296attached to the dmg 297 298=item B<-version> 299 300displays the version number 301 302=item B<-volName> I<name> 303 304specifies the I<name> of the volume inside the dmg. The corresponding environment variable is B<DMG_VOLNAME>. If neither the option, 305nor the environment variable contains a I<name>, nor a settings file is specified (see environment variable B<SETTINGS_FILE> in the 306Project Builder Support section below) the name of the first file will be used 307 308=item B<-volSize> I<n> 309 310specifies the size of the volume to create in megabytes. The environment variable is B<DMG_VOLSIZE>. If no value or 0 is specified 311B<buildDmg> will try to determine the size by looking at the files to copy 312 313=back 314 315The B<files> specified as parameters AND the files specified in the environment variable B<DMG_FILESLIST> are copied onto the dmg 316(before the headers are deleted), starting with the files from the command line 317 318=head1 PROJECT BUILDER SUPPORT 319 320Due to the possibility to use environment variables instead of the above mentioned command line options you can use this script from 321a "Legacy Makefile" target. Therefore you have to set the build tool to "/usr/bin/perl", the arguments to "<pathToScript>/buildDMG.pl", 322and check the "Pass build settings in environment" checkbox. You then can control everything with the build settings. If you make this 323target depending on your "application target" you can build you app and put it in a dmg with a single click 324 325The B<SETTINGS_FILE> environment variable is only used if the dmg or volume name is not specified. If B<SETTINGS_FILE> is set it 326should point to the "Info.plist" of the project to copy onto the dmg. buildDMG is then able to automatically generate the dmg and 327volume name from the B<CFBundleName> and B<CFBundleVersion> entries. For the dmg name some characters which may be problematic 328are then replaced by an underscore ('_') 329 330When cleaning the target there is a problem with Project Builder cleaning the dependent target first, so chances are good that the file 331specified in B<SETTINGS_FILE> is not existing anymore. If so buildDMG deletes all dmg files in B<buildDir> 332 333=head1 EXAMPLES 334 335C</usr/bin/perl buildDMG.pl> 336 337This is the way buildDMG can be called when all required environment variables are set (e.g. from ProjectBuilder) 338 339C<./buildDMG.pl -dmgName Name -buildDir build -volSize 10 -volName Volume -compressionLevel 9 -slaRsrcFile SLA.r Example.app 340-deleteHeaders> 341 342This creates a dmg called "Name.dmg" in the directory "build". It contains a 10 MB volume named "Volume" and is compressed with the 343highest compression level. The source for the SLA is obtained from the file SLA.r and the file (or file tree) "Example.app" is copied 344onto the dmg, with header directories removed (after copying!) 345 346=head1 AUTHOR 347 348Joerg Westheide (joerg@objectpark.org) 349 350=head1 SEE ALSO 351 352Rez(1), hdiutil(1) 353 354