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