1#!/usr/bin/perl
2# ***** BEGIN LICENSE BLOCK *****
3# Version: MPL 1.1/GPL 2.0/LGPL 2.1
4#
5# The contents of this file are subject to the Mozilla Public License Version
6# 1.1 (the "License"); you may not use this file except in compliance with
7# the License. You may obtain a copy of the License at
8# http://www.mozilla.org/MPL/
9#
10# Software distributed under the License is distributed on an "AS IS" basis,
11# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12# for the specific language governing rights and limitations under the
13# License.
14#
15# The Original Code is pkg-dmg, a Mac OS X disk image (.dmg) packager
16#
17# The Initial Developer of the Original Code is
18# Mark Mentovai <mark@moxienet.com>.
19# Portions created by the Initial Developer are Copyright (C) 2005
20# the Initial Developer. All Rights Reserved.
21#
22# Contributor(s):
23#
24# Alternatively, the contents of this file may be used under the terms of
25# either the GNU General Public License Version 2 or later (the "GPL"), or
26# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27# in which case the provisions of the GPL or the LGPL are applicable instead
28# of those above. If you wish to allow use of your version of this file only
29# under the terms of either the GPL or the LGPL, and not to allow others to
30# use your version of this file under the terms of the MPL, indicate your
31# decision by deleting the provisions above and replace them with the notice
32# and other provisions required by the GPL or the LGPL. If you do not delete
33# the provisions above, a recipient may use your version of this file under
34# the terms of any one of the MPL, the GPL or the LGPL.
35#
36# ***** END LICENSE BLOCK *****
37
38use strict;
39use warnings;
40
41=pod
42
43=head1 NAME
44
45B<pkg-dmg> - Mac OS X disk image (.dmg) packager
46
47=head1 SYNOPSIS
48
49B<pkg-dmg>
50B<--source> I<source-folder>
51B<--target> I<target-image>
52[B<--format> I<format>]
53[B<--volname> I<volume-name>]
54[B<--tempdir> I<temp-dir>]
55[B<--mkdir> I<directory>]
56[B<--copy> I<source>[:I<dest>]]
57[B<--symlink> I<source>[:I<dest>]]
58[B<--license> I<file>]
59[B<--resource> I<file>]
60[B<--icon> I<icns-file>]
61[B<--attribute> I<a>:I<file>[:I<file>...]
62[B<--idme>]
63[B<--sourcefile>]
64[B<--verbosity> I<level>]
65[B<--dry-run>]
66
67=head1 DESCRIPTION
68
69I<pkg-dmg> takes a directory identified by I<source-folder> and transforms
70it into a disk image stored as I<target-image>.  The disk image will
71occupy the least space possible for its format, or the least space that the
72authors have been able to figure out how to achieve.
73
74=head1 OPTIONS
75
76=over 5
77
78==item B<--source> I<source-folder>
79
80Identifies the directory that will be packaged up.  This directory is not
81touched, a copy will be made in a temporary directory for staging purposes.
82See B<--tempdir>.
83
84==item B<--target> I<target-image>
85
86The disk image to create.  If it exists and is not in use, it will be
87overwritten.  If I<target-image> already contains a suitable extension,
88it will be used unmodified.  If no extension is present, or the extension
89is incorrect for the selected format, the proper extension will be added.
90See B<--format>.
91
92==item B<--format> I<format>
93
94The format to create the disk image in.  Valid values for I<format> are:
95     - UDZO - zlib-compressed, read-only; extension I<.dmg>
96     - UDBZ - bzip2-compressed, read-only; extension I<.dmg>;
97              create and use on 10.4 ("Tiger") and later only
98     - UDRO - uncompressed, read-only; extension I<.dmg>
99     - UDRW - uncompressed, read-write; extension I<.dmg>
100     - UDSP - uncompressed, read-write, sparse; extension I<.sparseimage>
101
102UDZO is the default format.
103
104See L<hdiutil(1)> for a description of these formats.
105
106=item B<--volname> I<volume-name>
107
108The name of the volume in the disk image.  If not specified, I<volume-name>
109defaults to the name of the source directory from B<--source>.
110
111=item B<--tempdir> I<temp-dir>
112
113A temporary directory to stage intermediate files in.  I<temp-dir> must
114have enough space available to accommodate twice the size of the files
115being packaged.  If not specified, defaults to the same directory that
116the I<target-image> is to be placed in.  B<pkg-dmg> will remove any
117temporary files it places in I<temp-dir>.
118
119=item B<--mkdir> I<directory>
120
121Specifies a directory that should be created in the disk image.
122I<directory> and any ancestor directories will be created.  This is
123useful in conjunction with B<--copy>, when copying files to directories
124that may not exist in I<source-folder>.  B<--mkdir> may appear multiple
125times.
126
127=item B<--copy> I<source>[:I<dest>]
128
129Additional files to copy into the disk image.  If I<dest> is
130specified, I<source> is copied to the location I<dest> identifies,
131otherwise, I<source> is copied to the root of the new volume.  B<--copy>
132provides a way to package up a I<source-folder> by adding files to it
133without modifying the original I<source-folder>.  B<--copy> may appear
134multiple times.
135
136This option is useful for adding .DS_Store files and window backgrounds
137to disk images.
138
139=item B<--symlink> I<source>[:I<dest>]
140
141Like B<--copy>, but allows symlinks to point out of the volume. Empty symlink
142destinations are interpreted as "like the source path, but inside the dmg"
143
144This option is useful for adding symlinks to external resources,
145e.g. to /Applications.
146
147=item B<--license> I<file>
148
149A plain text file containing a license agreement to be displayed before
150the disk image is mounted.  English is the only supported language.  To
151include license agreements in other languages, in multiple languages,
152or to use formatted text, prepare a resource and use L<--resource>.
153
154=item B<--resource> I<file>
155
156A resource file to merge into I<target-image>.  If I<format> is UDZO, UDBZ,
157or UDRO, the disk image will be flattened to a single-fork file that contains
158the resource but may be freely transferred without any special encodings.
159I<file> must be in a format suitable for L<Rez(1)>.  See L<Rez(1)> for a
160description of the format, and L<hdiutil(1)> for a discussion on flattened
161disk images.  B<--resource> may appear multiple times.
162
163This option is useful for adding license agreements and other messages
164to disk images.
165
166=item B<--icon> I<icns-file>
167
168Specifies an I<icns> file that will be used as the icon for the root of
169the volume.  This file will be copied to the new volume and the custom
170icon attribute will be set on the root folder.
171
172=item B<--attribute> I<a>:I<file>[:I<file>...]
173
174Sets the attributes of I<file> to the attribute list in I<a>.  See
175L<SetFile(1)>
176
177=item B<--idme>
178
179Enable IDME to make the disk image "Internet-enabled."  The first time
180the image is mounted, if IDME processing is enabled on the system, the
181contents of the image will be copied out of the image and the image will
182be placed in the trash with IDME disabled.
183
184=item B<--sourcefile>
185
186If this option is present, I<source-folder> is treated as a file, and is
187placed as a file within the volume's root folder.  Without this option,
188I<source-folder> is treated as the volume root itself.
189
190=item B<--verbosity> I<level>
191
192Adjusts the level of loudness of B<pkg-dmg>.  The possible values for
193I<level> are:
194     0 - Only error messages are displayed.
195     1 - Print error messages and command invocations.
196     2 - Print everything, including command output.
197
198The default I<level> is 2.
199
200=item B<--dry-run>
201
202When specified, the commands that would be executed are printed, without
203actually executing them.  When commands depend on the output of previous
204commands, dummy values are displayed.
205
206=back
207
208=head1 NON-OPTIONS
209
210=over 5
211
212=item
213
214Resource forks aren't copied.
215
216=item
217
218The root folder of the created volume is designated as the folder
219to open when the volume is mounted.  See L<bless(8)>.
220
221=item
222
223All files in the volume are set to be world-readable, only writable
224by the owner, and world-executable when appropriate.  All other
225permissions bits are cleared.
226
227=item
228
229When possible, disk images are created without any partition tables.  This
230is what L<hdiutil(1)> refers to as I<-layout NONE>, and saves a handful of
231kilobytes.  The alternative, I<SPUD>, contains a partition table that
232is not terribly handy on disk images that are not intended to represent any
233physical disk.
234
235=item
236
237Read-write images are created with journaling off.  Any read-write image
238created by this tool is expected to be transient, and the goal of this tool
239is to create images which consume a minimum of space.
240
241=back
242
243=head1 EXAMPLE
244
245pkg-dmg --source /Applications/DeerPark.app --target ~/DeerPark.dmg
246  --sourcefile --volname DeerPark --icon ~/DeerPark.icns
247  --mkdir /.background
248  --copy DeerParkBackground.png:/.background/background.png
249  --copy DeerParkDSStore:/.DS_Store
250  --symlink /Applications:"/Drag to here"
251
252=head1 REQUIREMENTS
253
254I<pkg-dmg> has been tested with Mac OS X releases 10.2 ("Jaguar")
255through 10.4 ("Tiger").  Certain adjustments to behavior are made
256depending on the host system's release.  Mac OS X 10.3 ("Panther") or
257later are recommended.
258
259=head1 LICENSE
260
261MPL 1.1/GPL 2.0/LGPL 2.1.  Your choice.
262
263=head1 AUTHOR
264
265Mark Mentovai
266
267=head1 SEE ALSO
268
269L<bless(8)>, L<diskutil(8)>, L<hdid(8)>, L<hdiutil(1)>, L<Rez(1)>,
270L<rsync(1)>, L<SetFile(1)>
271
272=cut
273
274use Fcntl;
275use POSIX;
276use Getopt::Long;
277
278sub argumentEscape(@);
279sub cleanupDie($);
280sub command(@);
281sub commandInternal($@);
282sub commandInternalVerbosity($$@);
283sub commandOutput(@);
284sub commandOutputVerbosity($@);
285sub commandVerbosity($@);
286sub copyFiles($@);
287sub diskImageMaker($$$$$$$$);
288sub giveExtension($$);
289sub hdidMountImage($@);
290sub isFormatReadOnly($);
291sub licenseMaker($$);
292sub pathSplit($);
293sub setAttributes($@);
294sub trapSignal($);
295sub usage();
296
297# Variables used as globals
298my(@gCleanup, %gConfig, $gDarwinMajor, $gDryRun, $gVerbosity);
299
300# Use the commands by name if they're expected to be in the user's
301# $PATH (/bin:/sbin:/usr/bin:/usr/sbin).  Otherwise, go by absolute
302# path.  These may be overridden with --config.
303%gConfig = ('cmd_bless'          => 'bless',
304            'cmd_chmod'          => 'chmod',
305            'cmd_diskutil'       => 'diskutil',
306            'cmd_du'             => 'du',
307            'cmd_hdid'           => 'hdid',
308            'cmd_hdiutil'        => 'hdiutil',
309            'cmd_mkdir'          => 'mkdir',
310            'cmd_mktemp'         => 'mktemp',
311            'cmd_Rez'            => '/usr/bin/Rez',
312            'cmd_rm'             => 'rm',
313            'cmd_rsync'          => 'rsync',
314            'cmd_SetFile'        => '/usr/bin/SetFile',
315
316            # create_directly indicates whether hdiutil create supports
317            # -srcfolder and -srcdevice.  It does on >= 10.3 (Panther).
318            # This is fixed up for earlier systems below.  If false,
319            # hdiutil create is used to create empty disk images that
320            # are manually filled.
321            'create_directly'    => 1,
322
323            # If hdiutil attach -mountpoint exists, use it to avoid
324            # mounting disk images in the default /Volumes.  This reduces
325            # the likelihood that someone will notice a mounted image and
326            # interfere with it.  Only available on >= 10.3 (Panther),
327            # fixed up for earlier systems below.
328            #
329            # This is presently turned off for all systems, because there
330            # is an infrequent synchronization problem during ejection.
331            # diskutil eject might return before the image is actually
332            # unmounted.  If pkg-dmg then attempts to clean up its
333            # temporary directory, it could remove items from a read-write
334            # disk image or attempt to remove items from a read-only disk
335            # image (or a read-only item from a read-write image) and fail,
336            # causing pkg-dmg to abort.  This problem is experienced
337            # under Tiger, which appears to eject asynchronously where
338            # previous systems treated it as a synchronous operation.
339            # Using hdiutil attach -mountpoint didn't always keep images
340            # from showing up on the desktop anyway.
341            'hdiutil_mountpoint' => 0,
342
343            # hdiutil makehybrid results in optimized disk images that
344            # consume less space and mount more quickly.  Use it when
345            # it's available, but that's only on >= 10.3 (Panther).
346            # If false, hdiutil create is used instead.  Fixed up for
347            # earlier systems below.
348            'makehybrid'         => 1,
349
350            # hdiutil create doesn't allow specifying a folder to open
351            # at volume mount time, so those images are mounted and
352            # their root folders made holy with bless -openfolder.  But
353            # only on >= 10.3 (Panther).  Earlier systems are out of luck.
354            # Even on Panther, bless refuses to run unless root.
355            # Fixed up below.
356            'openfolder_bless'   => 1,
357
358            # It's possible to save a few more kilobytes by including the
359            # partition only without any partition table in the image.
360            # This is a good idea on any system, so turn this option off.
361            #
362            # Except it's buggy.  "-layout NONE" seems to be creating
363            # disk images with more data than just the partition table
364            # stripped out.  You might wind up losing the end of the
365            # filesystem - the last file (or several) might be incomplete.
366            'partition_table'    => 1,
367
368            # To create a partition table-less image from something
369            # created by makehybrid, the hybrid image needs to be
370            # mounted and a new image made from the device associated
371            # with the relevant partition.  This requires >= 10.4
372            # (Tiger), presumably because earlier systems have
373            # problems creating images from devices themselves attached
374            # to images.  If this is false, makehybrid images will
375            # have partition tables, regardless of the partition_table
376            # setting.  Fixed up for earlier systems below.
377            'recursive_access'   => 1);
378
379# --verbosity
380$gVerbosity = 2;
381
382# --dry-run
383$gDryRun = 0;
384
385# %gConfig fix-ups based on features and bugs present in certain releases.
386my($ignore, $uname_r, $uname_s);
387($uname_s, $ignore, $uname_r, $ignore, $ignore) = POSIX::uname();
388if($uname_s eq 'Darwin') {
389  ($gDarwinMajor, $ignore) = split(/\./, $uname_r, 2);
390
391  # $major is the Darwin major release, which for our purposes, is 4 higher
392  # than the interesting digit in a Mac OS X release.
393  if($gDarwinMajor <= 6) {
394    # <= 10.2 (Jaguar)
395    # hdiutil create does not support -srcfolder or -srcdevice
396    $gConfig{'create_directly'} = 0;
397    # hdiutil attach does not support -mountpoint
398    $gConfig{'hdiutil_mountpoint'} = 0;
399    # hdiutil mkhybrid does not exist
400    $gConfig{'makehybrid'} = 0;
401  }
402  if($gDarwinMajor <= 7) {
403    # <= 10.3 (Panther)
404    # Can't mount a disk image and then make a disk image from the device
405    $gConfig{'recursive_access'} = 0;
406    # bless does not support -openfolder on 10.2 (Jaguar) and must run
407    # as root under 10.3 (Panther)
408    $gConfig{'openfolder_bless'} = 0;
409  }
410}
411else {
412  # If it's not Mac OS X, just assume all of those good features are
413  # available.  They're not, but things will fail long before they
414  # have a chance to make a difference.
415  #
416  # Now, if someone wanted to document some of these private formats...
417  print STDERR ($0.": warning, not running on Mac OS X, ".
418   "this could be interesting.\n");
419}
420
421# Non-global variables used in Getopt
422my(@attributes, @copyFiles, @createSymlinks, $iconFile, $idme, $licenseFile,
423 @makeDirs, $outputFormat, @resourceFiles, $sourceFile, $sourceFolder,
424 $targetImage, $tempDir, $volumeName);
425
426# --format
427$outputFormat = 'UDZO';
428
429# --idme
430$idme = 0;
431
432# --sourcefile
433$sourceFile = 0;
434
435# Leaving this might screw up the Apple tools.
436delete $ENV{'NEXT_ROOT'};
437
438# This script can get pretty messy, so trap a few signals.
439$SIG{'INT'} = \&trapSignal;
440$SIG{'HUP'} = \&trapSignal;
441$SIG{'TERM'} = \&trapSignal;
442
443Getopt::Long::Configure('pass_through');
444GetOptions('source=s'    => \$sourceFolder,
445           'target=s'    => \$targetImage,
446           'volname=s'   => \$volumeName,
447           'format=s'    => \$outputFormat,
448           'tempdir=s'   => \$tempDir,
449           'mkdir=s'     => \@makeDirs,
450           'copy=s'      => \@copyFiles,
451           'symlink=s'   => \@createSymlinks,
452           'license=s'   => \$licenseFile,
453           'resource=s'  => \@resourceFiles,
454           'icon=s'      => \$iconFile,
455           'attribute=s' => \@attributes,
456           'idme'        => \$idme,
457           'sourcefile'  => \$sourceFile,
458           'verbosity=i' => \$gVerbosity,
459           'dry-run'     => \$gDryRun,
460           'config=s'    => \%gConfig); # "hidden" option not in usage()
461
462if(@ARGV) {
463  # All arguments are parsed by Getopt
464  usage();
465  exit(1);
466}
467
468if($gVerbosity<0 || $gVerbosity>2) {
469  usage();
470  exit(1);
471}
472
473if(!defined($sourceFolder) || $sourceFolder eq '' ||
474 !defined($targetImage) || $targetImage eq '') {
475  # --source and --target are required arguments
476  usage();
477  exit(1);
478}
479
480# Make sure $sourceFolder doesn't contain trailing slashes.  It messes with
481# rsync.
482while(substr($sourceFolder, -1) eq '/') {
483  chop($sourceFolder);
484}
485
486if(!defined($volumeName)) {
487  # Default volumeName is the name of the source directory.
488  my(@components);
489  @components = pathSplit($sourceFolder);
490  $volumeName = pop(@components);
491}
492
493my(@tempDirComponents, $targetImageFilename);
494@tempDirComponents = pathSplit($targetImage);
495$targetImageFilename = pop(@tempDirComponents);
496
497if(defined($tempDir)) {
498  @tempDirComponents = pathSplit($tempDir);
499}
500else {
501  # Default tempDir is the same directory as what is specified for
502  # targetImage
503  $tempDir = join('/', @tempDirComponents);
504}
505
506# Ensure that the path of the target image has a suitable extension.  If
507# it didn't, hdiutil would add one, and we wouldn't be able to find the
508# file.
509#
510# Note that $targetImageFilename is not being reset.  This is because it's
511# used to build other names below, and we don't need to be adding all sorts
512# of extra unnecessary extensions to the name.
513my($originalTargetImage, $requiredExtension);
514$originalTargetImage = $targetImage;
515if($outputFormat eq 'UDSP') {
516  $requiredExtension = '.sparseimage';
517}
518else {
519  $requiredExtension = '.dmg';
520}
521$targetImage = giveExtension($originalTargetImage, $requiredExtension);
522
523if($targetImage ne $originalTargetImage) {
524  print STDERR ($0.": warning: target image extension is being added\n");
525  print STDERR ('  The new filename is '.
526   giveExtension($targetImageFilename,$requiredExtension)."\n");
527}
528
529# Make a temporary directory in $tempDir for our own nefarious purposes.
530my(@output, $tempSubdir, $tempSubdirTemplate);
531$tempSubdirTemplate=join('/', @tempDirComponents,
532 'pkg-dmg.'.$$.'.XXXXXXXX');
533if(!(@output = commandOutput($gConfig{'cmd_mktemp'}, '-d',
534 $tempSubdirTemplate)) || $#output != 0) {
535  cleanupDie('mktemp failed');
536}
537
538if($gDryRun) {
539  (@output)=($tempSubdirTemplate);
540}
541
542($tempSubdir) = @output;
543
544push(@gCleanup,
545 sub {commandVerbosity(0, $gConfig{'cmd_rm'}, '-rf', $tempSubdir);});
546
547my($tempMount, $tempRoot, @tempsToMake);
548$tempRoot = $tempSubdir.'/stage';
549$tempMount = $tempSubdir.'/mount';
550push(@tempsToMake, $tempRoot);
551if($gConfig{'hdiutil_mountpoint'}) {
552  push(@tempsToMake, $tempMount);
553}
554
555if(command($gConfig{'cmd_mkdir'}, @tempsToMake) != 0) {
556  cleanupDie('mkdir tempRoot/tempMount failed');
557}
558
559# This cleanup object is not strictly necessary, because $tempRoot is inside
560# of $tempSubdir, but the rest of the script relies on this object being
561# on the cleanup stack and expects to remove it.
562push(@gCleanup,
563 sub {commandVerbosity(0, $gConfig{'cmd_rm'}, '-rf', $tempRoot);});
564
565# If $sourceFile is true, it means that $sourceFolder is to be treated as
566# a file and placed as a file within the volume root, as opposed to being
567# treated as the volume root itself.  rsync will do this by default, if no
568# trailing '/' is present.  With a trailing '/', $sourceFolder becomes
569# $tempRoot, instead of becoming an entry in $tempRoot.
570if(command($gConfig{'cmd_rsync'}, '-aC', '--include', '*.so',
571 '--copy-unsafe-links', $sourceFolder.($sourceFile?'':'/'),$tempRoot) != 0) {
572  cleanupDie('rsync failed');
573}
574
575if(@makeDirs) {
576  my($makeDir, @tempDirsToMake);
577  foreach $makeDir (@makeDirs) {
578    if($makeDir =~ /^\//) {
579      push(@tempDirsToMake, $tempRoot.$makeDir);
580    }
581    else {
582      push(@tempDirsToMake, $tempRoot.'/'.$makeDir);
583    }
584  }
585  if(command($gConfig{'cmd_mkdir'}, '-p', @tempDirsToMake) != 0) {
586    cleanupDie('mkdir failed');
587  }
588}
589
590# copy files and/or create symlinks
591copyFiles($tempRoot, 'copy', @copyFiles);
592copyFiles($tempRoot, 'symlink', @createSymlinks);
593
594if($gConfig{'create_directly'}) {
595  # If create_directly is false, the contents will be rsynced into a
596  # disk image and they would lose their attributes.
597  setAttributes($tempRoot, @attributes);
598}
599
600if(defined($iconFile)) {
601  if(command($gConfig{'cmd_rsync'}, '-aC', '--include', '*.so',
602   '--copy-unsafe-links', $iconFile, $tempRoot.'/.VolumeIcon.icns') != 0) {
603    cleanupDie('rsync failed for volume icon');
604  }
605
606  # It's pointless to set the attributes of the root when diskutil create
607  # -srcfolder is being used.  In that case, the attributes will be set
608  # later, after the image is already created.
609  if(isFormatReadOnly($outputFormat) &&
610   (command($gConfig{'cmd_SetFile'}, '-a', 'C', $tempRoot) != 0)) {
611    cleanupDie('SetFile failed');
612  }
613}
614
615if(command($gConfig{'cmd_chmod'}, '-R', 'a+rX,a-st,u+w,go-w',
616 $tempRoot) != 0) {
617  cleanupDie('chmod failed');
618}
619
620my($unflattenable);
621if(isFormatReadOnly($outputFormat)) {
622  $unflattenable = 1;
623}
624else {
625  $unflattenable = 0;
626}
627
628diskImageMaker($tempRoot, $targetImage, $outputFormat, $volumeName,
629 $tempSubdir, $tempMount, $targetImageFilename, defined($iconFile));
630
631if(defined($licenseFile) && $licenseFile ne '') {
632  my($licenseResource);
633  $licenseResource = $tempSubdir.'/license.r';
634  if(!licenseMaker($licenseFile, $licenseResource)) {
635    cleanupDie('licenseMaker failed');
636  }
637  push(@resourceFiles, $licenseResource);
638  # Don't add a cleanup object because licenseResource is in tempSubdir.
639}
640
641if(@resourceFiles) {
642  # Add resources, such as a license agreement.
643
644  # Only unflatten read-only and compressed images.  It's not supported
645  # on other image times.
646  if($unflattenable &&
647   (command($gConfig{'cmd_hdiutil'}, 'unflatten', $targetImage)) != 0) {
648    cleanupDie('hdiutil unflatten failed');
649  }
650  # Don't push flatten onto the cleanup stack.  If we fail now, we'll be
651  # removing $targetImage anyway.
652
653  # Type definitions come from Carbon.r.
654  if(command($gConfig{'cmd_Rez'}, 'Carbon.r', @resourceFiles, '-a', '-o',
655   $targetImage) != 0) {
656    cleanupDie('Rez failed');
657  }
658
659  # Flatten.  This merges the resource fork into the data fork, so no
660  # special encoding is needed to transfer the file.
661  if($unflattenable &&
662   (command($gConfig{'cmd_hdiutil'}, 'flatten', $targetImage)) != 0) {
663    cleanupDie('hdiutil flatten failed');
664  }
665}
666
667# $tempSubdir is no longer needed.  It's buried on the stack below the
668# rm of the fresh image file.  Splice in this fashion is equivalent to
669# pop-save, pop, push-save.
670splice(@gCleanup, -2, 1);
671# No need to remove licenseResource separately, it's in tempSubdir.
672if(command($gConfig{'cmd_rm'}, '-rf', $tempSubdir) != 0) {
673  cleanupDie('rm -rf tempSubdir failed');
674}
675
676if($idme) {
677  if(command($gConfig{'cmd_hdiutil'}, 'internet-enable', '-yes',
678   $targetImage) != 0) {
679    cleanupDie('hdiutil internet-enable failed');
680  }
681}
682
683# Done.
684
685exit(0);
686
687# argumentEscape(@arguments)
688#
689# Takes a list of @arguments and makes them shell-safe.
690sub argumentEscape(@) {
691  my(@arguments);
692  @arguments = @_;
693  my($argument, @argumentsOut);
694  foreach $argument (@arguments) {
695    $argument =~ s%([^A-Za-z0-9_\-/.=+,])%\\$1%g;
696    push(@argumentsOut, $argument);
697  }
698  return @argumentsOut;
699}
700
701# cleanupDie($message)
702#
703# Displays $message as an error message, and then runs through the
704# @gCleanup stack, performing any cleanup operations needed before
705# exiting.  Does not return, exits with exit status 1.
706sub cleanupDie($) {
707  my($message);
708  ($message) = @_;
709  print STDERR ($0.': '.$message.(@gCleanup?' (cleaning up)':'')."\n");
710  while(@gCleanup) {
711    my($subroutine);
712    $subroutine = pop(@gCleanup);
713    &$subroutine;
714  }
715  exit(1);
716}
717
718# command(@arguments)
719#
720# Runs the specified command at the verbosity level defined by $gVerbosity.
721# Returns nonzero on failure, returning the exit status if appropriate.
722# Discards command output.
723sub command(@) {
724  my(@arguments);
725  @arguments = @_;
726  return commandVerbosity($gVerbosity,@arguments);
727}
728
729# commandInternal($command, @arguments)
730#
731# Runs the specified internal command at the verbosity level defined by
732# $gVerbosity.
733# Returns zero(!) on failure, because commandInternal is supposed to be a
734# direct replacement for the Perl system call wrappers, which, unlike shell
735# commands and C equivalent system calls, return true (instead of 0) to
736# indicate success.
737sub commandInternal($@) {
738  my(@arguments, $command);
739  ($command, @arguments) = @_;
740  return commandInternalVerbosity($gVerbosity, $command, @arguments);
741}
742
743# commandInternalVerbosity($verbosity, $command, @arguments)
744#
745# Run an internal command, printing a bogus command invocation message if
746# $verbosity is true.
747#
748# If $command is unlink:
749# Removes the files specified by @arguments.  Wraps unlink.
750#
751# If $command is symlink:
752# Creates the symlink specified by @arguments. Wraps symlink.
753sub commandInternalVerbosity($$@) {
754  my(@arguments, $command, $verbosity);
755  ($verbosity, $command, @arguments) = @_;
756  if($command eq 'unlink') {
757    if($verbosity || $gDryRun) {
758      print(join(' ', 'rm', '-f', argumentEscape(@arguments))."\n");
759    }
760    if($gDryRun) {
761      return $#arguments+1;
762    }
763    return unlink(@arguments);
764  }
765  elsif($command eq 'symlink') {
766    if($verbosity || $gDryRun) {
767      print(join(' ', 'ln', '-s', argumentEscape(@arguments))."\n");
768    }
769    if($gDryRun) {
770      return 1;
771    }
772    my($source, $target);
773    ($source, $target) = @arguments;
774    return symlink($source, $target);
775  }
776}
777
778# commandOutput(@arguments)
779#
780# Runs the specified command at the verbosity level defined by $gVerbosity.
781# Output is returned in an array of lines.  undef is returned on failure.
782# The exit status is available in $?.
783sub commandOutput(@) {
784  my(@arguments);
785  @arguments = @_;
786  return commandOutputVerbosity($gVerbosity, @arguments);
787}
788
789# commandOutputVerbosity($verbosity, @arguments)
790#
791# Runs the specified command at the verbosity level defined by the
792# $verbosity argument.  Output is returned in an array of lines.  undef is
793# returned on failure.  The exit status is available in $?.
794#
795# If an error occurs in fork or exec, an error message is printed to
796# stderr and undef is returned.
797#
798# If $verbosity is 0, the command invocation is not printed, and its
799# stdout is not echoed back to stdout.
800#
801# If $verbosity is 1, the command invocation is printed.
802#
803# If $verbosity is 2, the command invocation is printed and the output
804# from stdout is echoed back to stdout.
805#
806# Regardless of $verbosity, stderr is left connected.
807sub commandOutputVerbosity($@) {
808  my(@arguments, $verbosity);
809  ($verbosity, @arguments) = @_;
810  my($pid);
811  if($verbosity || $gDryRun) {
812    print(join(' ', argumentEscape(@arguments))."\n");
813  }
814  if($gDryRun) {
815    return(1);
816  }
817  if (!defined($pid = open(*COMMAND, '-|'))) {
818    printf STDERR ($0.': fork: '.$!."\n");
819    return undef;
820  }
821  elsif ($pid) {
822    # parent
823    my(@lines);
824    while(!eof(*COMMAND)) {
825      my($line);
826      chop($line = <COMMAND>);
827      if($verbosity > 1) {
828        print($line."\n");
829      }
830      push(@lines, $line);
831    }
832    close(*COMMAND);
833    if ($? == -1) {
834      printf STDERR ($0.': fork: '.$!."\n");
835      return undef;
836    }
837    elsif ($? & 127) {
838      printf STDERR ($0.': exited on signal '.($? & 127).
839       ($? & 128 ? ', core dumped' : '')."\n");
840      return undef;
841    }
842    return @lines;
843  }
844  else {
845    # child; this form of exec is immune to shell games
846    if(!exec {$arguments[0]} (@arguments)) {
847      printf STDERR ($0.': exec: '.$!."\n");
848      exit(-1);
849    }
850  }
851}
852
853# commandVerbosity($verbosity, @arguments)
854#
855# Runs the specified command at the verbosity level defined by the
856# $verbosity argument.  Returns nonzero on failure, returning the exit
857# status if appropriate.  Discards command output.
858sub commandVerbosity($@) {
859  my(@arguments, $verbosity);
860  ($verbosity, @arguments) = @_;
861  if(!defined(commandOutputVerbosity($verbosity, @arguments))) {
862    return -1;
863  }
864  return $?;
865}
866
867# copyFiles($tempRoot, $method, @arguments)
868#
869# Copies files or create symlinks in the disk image.
870# See --copy and --symlink descriptions for details.
871# If $method is 'copy', @arguments are interpreted as source:target, if $method
872# is 'symlink', @arguments are interpreted as symlink:target.
873sub copyFiles($@) {
874  my(@fileList, $method, $tempRoot);
875  ($tempRoot, $method, @fileList) = @_;
876  my($file, $isSymlink);
877  $isSymlink = ($method eq 'symlink');
878  foreach $file (@fileList) {
879    my($source, $target);
880    ($source, $target) = split(/:/, $file);
881    if(!defined($target) and $isSymlink) {
882      # empty symlink targets would result in an invalid target and fail,
883      # but they shall be interpreted as "like source path, but inside dmg"
884      $target = $source;
885    }
886    if(!defined($target)) {
887      $target = $tempRoot;
888    }
889    elsif($target =~ /^\//) {
890      $target = $tempRoot.$target;
891    }
892    else {
893      $target = $tempRoot.'/'.$target;
894    }
895
896    my($success);
897    if($isSymlink) {
898      $success = commandInternal('symlink', $source, $target);
899    }
900    else {
901      $success = !command($gConfig{'cmd_rsync'}, '-aC', '--include', '*.so',
902                          '--copy-unsafe-links', $source, $target);
903    }
904    if(!$success) {
905      cleanupDie('copyFiles failed for method '.$method);
906    }
907  }
908}
909
910# diskImageMaker($source, $destination, $format, $name, $tempDir, $tempMount,
911#  $baseName, $setRootIcon)
912#
913# Creates a disk image in $destination of format $format corresponding to the
914# source directory $source.  $name is the volume name.  $tempDir is a good
915# place to write temporary files, which should be empty (aside from the other
916# things that this script might create there, like stage and mount).
917# $tempMount is a mount point for temporary disk images.  $baseName is the
918# name of the disk image, and is presently unused.  $setRootIcon is true if
919# a volume icon was added to the staged $source and indicates that the
920# custom volume icon bit on the volume root needs to be set.
921sub diskImageMaker($$$$$$$$) {
922  my($baseName, $destination, $format, $name, $setRootIcon, $source,
923   $tempDir, $tempMount);
924  ($source, $destination, $format, $name, $tempDir, $tempMount,
925   $baseName, $setRootIcon) = @_;
926  if(isFormatReadOnly($format)) {
927    my($uncompressedImage);
928
929    if($gConfig{'makehybrid'}) {
930      my($hybridImage);
931      $hybridImage = giveExtension($tempDir.'/hybrid', '.dmg');
932
933      if(command($gConfig{'cmd_hdiutil'}, 'makehybrid', '-hfs',
934       '-hfs-volume-name', $name,
935       ($gConfig{'openfolder_bless'} ? ('-hfs-openfolder', $source) : ()),
936       '-ov', $source, '-o', $hybridImage) != 0) {
937        cleanupDie('hdiutil makehybrid failed');
938      }
939
940      $uncompressedImage = $hybridImage;
941
942      # $source is no longer needed and will be removed before anything
943      # else can fail.  splice in this form is the same as pop/push.
944      splice(@gCleanup, -1, 1,
945       sub {commandInternalVerbosity(0, 'unlink', $hybridImage);});
946
947      if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
948        cleanupDie('rm -rf failed');
949      }
950
951      if(!$gConfig{'partition_table'} && $gConfig{'recursive_access'}) {
952        # Even if we do want to create disk images without partition tables,
953        # it's impossible unless recursive_access is set.
954        my($rootDevice, $partitionDevice, $partitionMountPoint);
955
956        if(!(($rootDevice, $partitionDevice, $partitionMountPoint) =
957         hdidMountImage($tempMount, '-readonly', $hybridImage))) {
958          cleanupDie('hdid mount failed');
959        }
960
961        push(@gCleanup, sub {commandVerbosity(0,
962         $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);});
963
964        my($udrwImage);
965        $udrwImage = giveExtension($tempDir.'/udrw', '.dmg');
966
967        if(command($gConfig{'cmd_hdiutil'}, 'create', '-format', 'UDRW',
968         '-ov', '-srcdevice', $partitionDevice, $udrwImage) != 0) {
969          cleanupDie('hdiutil create failed');
970        }
971
972        $uncompressedImage = $udrwImage;
973
974        # Going to eject before anything else can fail.  Get the eject off
975        # the stack.
976        pop(@gCleanup);
977
978        # $hybridImage will be removed soon, but until then, it needs to
979        # stay on the cleanup stack.  It needs to wait until after
980        # ejection.  $udrwImage is staying around.  Make it appear as
981        # though it's been done before $hybridImage.
982        #
983        # splice in this form is the same as popping one element to
984        # @tempCleanup and pushing the subroutine.
985        my(@tempCleanup);
986        @tempCleanup = splice(@gCleanup, -1, 1,
987         sub {commandInternalVerbosity(0, 'unlink', $udrwImage);});
988        push(@gCleanup, @tempCleanup);
989
990        if(command($gConfig{'cmd_diskutil'}, 'eject', $rootDevice) != 0) {
991          cleanupDie('diskutil eject failed');
992        }
993
994        # Pop unlink of $uncompressedImage
995        pop(@gCleanup);
996
997        if(commandInternal('unlink', $hybridImage) != 1) {
998          cleanupDie('unlink hybridImage failed: '.$!);
999        }
1000      }
1001    }
1002    else {
1003      # makehybrid is not available, fall back to making a UDRW and
1004      # converting to a compressed image.  It ought to be possible to
1005      # create a compressed image directly, but those come out far too
1006      # large (journaling?) and need to be read-write to fix up the
1007      # volume icon anyway.  Luckily, we can take advantage of a single
1008      # call back into this function.
1009      my($udrwImage);
1010      $udrwImage = giveExtension($tempDir.'/udrw', '.dmg');
1011
1012      diskImageMaker($source, $udrwImage, 'UDRW', $name, $tempDir,
1013       $tempMount, $baseName, $setRootIcon);
1014
1015      # The call back into diskImageMaker already removed $source.
1016
1017      $uncompressedImage = $udrwImage;
1018    }
1019
1020    # The uncompressed disk image is now in its final form.  Compress it.
1021    # Jaguar doesn't support hdiutil convert -ov, but it always allows
1022    # overwriting.
1023    # bzip2-compressed UDBZ images can only be created and mounted on 10.4
1024    # and later.  The bzip2-level imagekey is only effective when creating
1025    # images in 10.5.  In 10.4, bzip2-level is harmlessly ignored, and the
1026    # default value of 1 is always used.
1027    if(command($gConfig{'cmd_hdiutil'}, 'convert', '-format', $format,
1028     ($format eq 'UDZO' ? ('-imagekey', 'zlib-level=9') : ()),
1029     ($format eq 'UDBZ' ? ('-imagekey', 'bzip2-level=9') : ()),
1030     (defined($gDarwinMajor) && $gDarwinMajor <= 6 ? () : ('-ov')),
1031     $uncompressedImage, '-o', $destination) != 0) {
1032      cleanupDie('hdiutil convert failed');
1033    }
1034
1035    # $uncompressedImage is going to be unlinked before anything else can
1036    # fail.  splice in this form is the same as pop/push.
1037    splice(@gCleanup, -1, 1,
1038     sub {commandInternalVerbosity(0, 'unlink', $destination);});
1039
1040    if(commandInternal('unlink', $uncompressedImage) != 1) {
1041      cleanupDie('unlink uncompressedImage failed: '.$!);
1042    }
1043
1044    # At this point, the only thing that the compressed block has added to
1045    # the cleanup stack is the removal of $destination.  $source has already
1046    # been removed, and its cleanup entry has been removed as well.
1047  }
1048  elsif($format eq 'UDRW' || $format eq 'UDSP') {
1049    my(@extraArguments);
1050    if(!$gConfig{'partition_table'}) {
1051      @extraArguments = ('-layout', 'NONE');
1052    }
1053
1054    if($gConfig{'create_directly'}) {
1055      # Use -fs HFS+ to suppress the journal.
1056      if(command($gConfig{'cmd_hdiutil'}, 'create', '-format', $format,
1057       @extraArguments, '-fs', 'HFS+', '-volname', $name,
1058       '-ov', '-srcfolder', $source, $destination) != 0) {
1059        cleanupDie('hdiutil create failed');
1060      }
1061
1062      # $source is no longer needed and will be removed before anything
1063      # else can fail.  splice in this form is the same as pop/push.
1064      splice(@gCleanup, -1, 1,
1065       sub {commandInternalVerbosity(0, 'unlink', $destination);});
1066
1067      if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
1068        cleanupDie('rm -rf failed');
1069      }
1070    }
1071    else {
1072      # hdiutil create does not support -srcfolder or -srcdevice, it only
1073      # knows how to create blank images.  Figure out how large an image
1074      # is needed, create it, and fill it.  This is needed for Jaguar.
1075
1076      # Use native block size for hdiutil create -sectors.
1077      delete $ENV{'BLOCKSIZE'};
1078
1079      my(@duOutput, $ignore, $sizeBlocks, $sizeOverhead, $sizeTotal, $type);
1080      if(!(@output = commandOutput($gConfig{'cmd_du'}, '-s', $tempRoot)) ||
1081       $? != 0) {
1082        cleanupDie('du failed');
1083      }
1084      ($sizeBlocks, $ignore) = split(' ', $output[0], 2);
1085
1086      # The filesystem itself takes up 152 blocks of its own blocks for the
1087      # filesystem up to 8192 blocks, plus 64 blocks for every additional
1088      # 4096 blocks or portion thereof.
1089      $sizeOverhead = 152 + 64 * POSIX::ceil(
1090       (($sizeBlocks - 8192) > 0) ? (($sizeBlocks - 8192) / (4096 - 64)) : 0);
1091
1092      # The number of blocks must be divisible by 8.
1093      my($mod);
1094      if($mod = ($sizeOverhead % 8)) {
1095        $sizeOverhead += 8 - $mod;
1096      }
1097
1098      # sectors is taken as the size of a disk, not a filesystem, so the
1099      # partition table eats into it.
1100      if($gConfig{'partition_table'}) {
1101        $sizeOverhead += 80;
1102      }
1103
1104      # That was hard.  Leave some breathing room anyway.  Use 1024 sectors
1105      # (512kB).  These read-write images wouldn't be useful if they didn't
1106      # have at least a little free space.
1107      $sizeTotal = $sizeBlocks + $sizeOverhead + 1024;
1108
1109      # Minimum sizes - these numbers are larger on Jaguar than on later
1110      # systems.  Just use the Jaguar numbers, since it's unlikely to wind
1111      # up here on any other release.
1112      if($gConfig{'partition_table'} && $sizeTotal < 8272) {
1113        $sizeTotal = 8272;
1114      }
1115      if(!$gConfig{'partition_table'} && $sizeTotal < 8192) {
1116        $sizeTotal = 8192;
1117      }
1118
1119      # hdiutil create without -srcfolder or -srcdevice will not accept
1120      # -format.  It uses -type.  Fortunately, the two supported formats
1121      # here map directly to the only two supported types.
1122      if ($format eq 'UDSP') {
1123        $type = 'SPARSE';
1124      }
1125      else {
1126        $type = 'UDIF';
1127      }
1128
1129      if(command($gConfig{'cmd_hdiutil'}, 'create', '-type', $type,
1130       @extraArguments, '-fs', 'HFS+', '-volname', $name,
1131       '-ov', '-sectors', $sizeTotal, $destination) != 0) {
1132        cleanupDie('hdiutil create failed');
1133      }
1134
1135      push(@gCleanup,
1136       sub {commandInternalVerbosity(0, 'unlink', $destination);});
1137
1138      # The rsync will occur shortly.
1139    }
1140
1141    my($mounted, $rootDevice, $partitionDevice, $partitionMountPoint);
1142
1143    $mounted=0;
1144    if(!$gConfig{'create_directly'} || $gConfig{'openfolder_bless'} ||
1145     $setRootIcon) {
1146      # The disk image only needs to be mounted if:
1147      #  create_directly is false, because the content needs to be copied
1148      #  openfolder_bless is true, because bless -openfolder needs to run
1149      #  setRootIcon is true, because the root needs its attributes set.
1150      if(!(($rootDevice, $partitionDevice, $partitionMountPoint) =
1151       hdidMountImage($tempMount, $destination))) {
1152        cleanupDie('hdid mount failed');
1153      }
1154
1155      $mounted=1;
1156
1157      push(@gCleanup, sub {commandVerbosity(0,
1158       $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);});
1159    }
1160
1161    if(!$gConfig{'create_directly'}) {
1162      # Couldn't create and copy directly in one fell swoop.  Now that
1163      # the volume is mounted, copy the files.  --copy-unsafe-links is
1164      # unnecessary since it was used to copy everything to the staging
1165      # area.  There can be no more unsafe links.
1166      if(command($gConfig{'cmd_rsync'}, '-aC', '--include', '*.so',
1167       $source.'/',$partitionMountPoint) != 0) {
1168        cleanupDie('rsync to new volume failed');
1169      }
1170
1171      # We need to get the rm -rf of $source off the stack, because it's
1172      # being cleaned up here.  There are two items now on top of it:
1173      # removing the target image and, above that, ejecting it.  Splice it
1174      # out.
1175      my(@tempCleanup);
1176      @tempCleanup = splice(@gCleanup, -2);
1177      # The next splice is the same as popping once and pushing @tempCleanup.
1178      splice(@gCleanup, -1, 1, @tempCleanup);
1179
1180      if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
1181        cleanupDie('rm -rf failed');
1182      }
1183    }
1184
1185    if($gConfig{'openfolder_bless'}) {
1186      # On Tiger, the bless docs say to use --openfolder, but only
1187      # --openfolder is accepted on Panther.  Tiger takes it with a single
1188      # dash too.  Jaguar is out of luck.
1189      if(command($gConfig{'cmd_bless'}, '-openfolder',
1190       $partitionMountPoint) != 0) {
1191        cleanupDie('bless failed');
1192      }
1193    }
1194
1195    setAttributes($partitionMountPoint, @attributes);
1196
1197    if($setRootIcon) {
1198      # When "hdiutil create -srcfolder" is used, the root folder's
1199      # attributes are not copied to the new volume.  Fix up.
1200
1201      if(command($gConfig{'cmd_SetFile'}, '-a', 'C',
1202       $partitionMountPoint) != 0) {
1203        cleanupDie('SetFile failed');
1204      }
1205    }
1206
1207    if($mounted) {
1208      # Pop diskutil eject
1209      pop(@gCleanup);
1210
1211      if(command($gConfig{'cmd_diskutil'}, 'eject', $rootDevice) != 0) {
1212        cleanupDie('diskutil eject failed');
1213      }
1214    }
1215
1216    # End of UDRW/UDSP section.  At this point, $source has been removed
1217    # and its cleanup entry has been removed from the stack.
1218  }
1219  else {
1220    cleanupDie('unrecognized format');
1221    print STDERR ($0.": unrecognized format\n");
1222    exit(1);
1223  }
1224}
1225
1226# giveExtension($file, $extension)
1227#
1228# If $file does not end in $extension, $extension is added.  The new
1229# filename is returned.
1230sub giveExtension($$) {
1231  my($extension, $file);
1232  ($file, $extension) = @_;
1233  if(substr($file, -length($extension)) ne $extension) {
1234    return $file.$extension;
1235  }
1236  return $file;
1237}
1238
1239# hdidMountImage($mountPoint, @arguments)
1240#
1241# Runs the hdid command with arguments specified by @arguments.
1242# @arguments may be a single-element array containing the name of the
1243# disk image to mount.  Returns a three-element array, with elements
1244# corresponding to:
1245#  - The root device of the mounted image, suitable for ejection
1246#  - The device corresponding to the mounted partition
1247#  - The mounted partition's mount point
1248#
1249# If running on a system that supports easy mounting at points outside
1250# of the default /Volumes with hdiutil attach, it is used instead of hdid,
1251# and $mountPoint is used as the mount point.
1252#
1253# The root device will differ from the partition device when the disk
1254# image contains a partition table, otherwise, they will be identical.
1255#
1256# If hdid fails, undef is returned.
1257sub hdidMountImage($@) {
1258  my(@arguments, @command, $mountPoint);
1259  ($mountPoint, @arguments) = @_;
1260  my(@output);
1261
1262  if($gConfig{'hdiutil_mountpoint'}) {
1263    @command=($gConfig{'cmd_hdiutil'}, 'attach', @arguments,
1264     '-mountpoint', $mountPoint);
1265  }
1266  else {
1267    @command=($gConfig{'cmd_hdid'}, @arguments);
1268  }
1269
1270  if(!(@output = commandOutput(@command)) ||
1271   $? != 0) {
1272    return undef;
1273  }
1274
1275  if($gDryRun) {
1276    return('/dev/diskX','/dev/diskXsY','/Volumes/'.$volumeName);
1277  }
1278
1279  my($line, $restOfLine, $rootDevice);
1280
1281  foreach $line (@output) {
1282    my($device, $mountpoint);
1283    if($line !~ /^\/dev\//) {
1284      # Consider only lines that correspond to /dev entries
1285      next;
1286    }
1287    ($device, $restOfLine) = split(' ', $line, 2);
1288
1289    if(!defined($rootDevice) || $rootDevice eq '') {
1290      # If this is the first device seen, it's the root device to be
1291      # used for ejection.  Keep it.
1292      $rootDevice = $device;
1293    }
1294
1295    if($restOfLine =~ /(\/.*)/) {
1296      # The first partition with a mount point is the interesting one.  It's
1297      # usually Apple_HFS and usually the last one in the list, but beware of
1298      # the possibility of other filesystem types and the Apple_Free partition.
1299      # If the disk image contains no partition table, the partition will not
1300      # have a type, so look for the mount point by looking for a slash.
1301      $mountpoint = $1;
1302      return($rootDevice, $device, $mountpoint);
1303    }
1304  }
1305
1306  # No mount point?  This is bad.  If there's a root device, eject it.
1307  if(defined($rootDevice) && $rootDevice ne '') {
1308    # Failing anyway, so don't care about failure
1309    commandVerbosity(0, $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);
1310  }
1311
1312  return undef;
1313}
1314
1315# isFormatReadOnly($format)
1316#
1317# Returns true if $format corresponds to a read-only disk image format.
1318# Returns false otherwise.
1319sub isFormatReadOnly($) {
1320  my($format);
1321  ($format) = @_;
1322  return $format eq 'UDZO' || $format eq 'UDBZ' || $format eq 'UDRO';
1323}
1324
1325# licenseMaker($text, $resource)
1326#
1327# Takes a plain text file at path $text and creates a license agreement
1328# resource containing the text at path $license.  English-only, and
1329# no special formatting.  This is the bare-bones stuff.  For more
1330# intricate license agreements, create your own resource.
1331#
1332# ftp://ftp.apple.com/developer/Development_Kits/SLAs_for_UDIFs_1.0.dmg
1333sub licenseMaker($$) {
1334  my($resource, $text);
1335  ($text, $resource) = @_;
1336  if(!sysopen(*TEXT, $text, O_RDONLY)) {
1337    print STDERR ($0.': licenseMaker: sysopen text: '.$!."\n");
1338    return 0;
1339  }
1340  if(!sysopen(*RESOURCE, $resource, O_WRONLY|O_CREAT|O_EXCL)) {
1341    print STDERR ($0.': licenseMaker: sysopen resource: '.$!."\n");
1342    return 0;
1343  }
1344  print RESOURCE << '__EOT__';
1345// See /System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Headers/Script.h for language IDs.
1346data 'LPic' (5000) {
1347  // Default language ID, 0 = English
1348  $"0000"
1349  // Number of entries in list
1350  $"0001"
1351
1352  // Entry 1
1353  // Language ID, 0 = English
1354  $"0000"
1355  // Resource ID, 0 = STR#/TEXT/styl 5000
1356  $"0000"
1357  // Multibyte language, 0 = no
1358  $"0000"
1359};
1360
1361resource 'STR#' (5000, "English") {
1362  {
1363    // Language (unused?) = English
1364    "English",
1365    // Agree
1366    "Agree",
1367    // Disagree
1368    "Disagree",
1369__EOT__
1370    # This stuff needs double-quotes for interpolations to work.
1371    print RESOURCE ("    // Print, ellipsis is 0xC9\n");
1372    print RESOURCE ("    \"Print\xc9\",\n");
1373    print RESOURCE ("    // Save As, ellipsis is 0xC9\n");
1374    print RESOURCE ("    \"Save As\xc9\",\n");
1375    print RESOURCE ('    // Descriptive text, curly quotes are 0xD2 and 0xD3'.
1376     "\n");
1377    print RESOURCE ('    "If you agree to the terms of this license '.
1378     "agreement, click \xd2Agree\xd3 to access the software.  If you ".
1379     "do not agree, press \xd2Disagree.\xd3\"\n");
1380print RESOURCE << '__EOT__';
1381  };
1382};
1383
1384// Beware of 1024(?) byte (character?) line length limitation.  Split up long
1385// lines.
1386// If straight quotes are used ("), remember to escape them (\").
1387// Newline is \n, to leave a blank line, use two of them.
1388// 0xD2 and 0xD3 are curly double-quotes ("), 0xD4 and 0xD5 are curly
1389//   single quotes ('), 0xD5 is also the apostrophe.
1390data 'TEXT' (5000, "English") {
1391__EOT__
1392
1393  while(!eof(*TEXT)) {
1394    my($line);
1395    chop($line = <TEXT>);
1396
1397    while(defined($line)) {
1398      my($chunk);
1399
1400      # Rez doesn't care for lines longer than (1024?) characters.  Split
1401      # at less than half of that limit, in case everything needs to be
1402      # backwhacked.
1403      if(length($line)>500) {
1404        $chunk = substr($line, 0, 500);
1405        $line = substr($line, 500);
1406      }
1407      else {
1408        $chunk = $line;
1409        $line = undef;
1410      }
1411
1412      if(length($chunk) > 0) {
1413        # Unsafe characters are the double-quote (") and backslash (\), escape
1414        # them with backslashes.
1415        $chunk =~ s/(["\\])/\\$1/g;
1416
1417        print RESOURCE '  "'.$chunk.'"'."\n";
1418      }
1419    }
1420    print RESOURCE '  "\n"'."\n";
1421  }
1422  close(*TEXT);
1423
1424  print RESOURCE << '__EOT__';
1425};
1426
1427data 'styl' (5000, "English") {
1428  // Number of styles following = 1
1429  $"0001"
1430
1431  // Style 1.  This is used to display the first two lines in bold text.
1432  // Start character = 0
1433  $"0000 0000"
1434  // Height = 16
1435  $"0010"
1436  // Ascent = 12
1437  $"000C"
1438  // Font family = 1024 (Lucida Grande)
1439  $"0400"
1440  // Style bitfield, 0x1=bold 0x2=italic 0x4=underline 0x8=outline
1441  // 0x10=shadow 0x20=condensed 0x40=extended
1442  $"00"
1443  // Style, unused?
1444  $"02"
1445  // Size = 12 point
1446  $"000C"
1447  // Color, RGB
1448  $"0000 0000 0000"
1449};
1450__EOT__
1451  close(*RESOURCE);
1452
1453  return 1;
1454}
1455
1456# pathSplit($pathname)
1457#
1458# Splits $pathname into an array of path components.
1459sub pathSplit($) {
1460  my($pathname);
1461  ($pathname) = @_;
1462  return split(/\//, $pathname);
1463}
1464
1465# setAttributes($root, @attributeList)
1466#
1467# @attributeList is an array, each element of which must be in the form
1468# <a>:<file>.  <a> is a list of attributes, per SetFile.  <file> is a file
1469# which is taken as relative to $root (even if it appears as an absolute
1470# path.)  SetFile is called to set the attributes on each file in
1471# @attributeList.
1472sub setAttributes($@) {
1473  my(@attributes, $root);
1474  ($root, @attributes) = @_;
1475  my($attribute);
1476  foreach $attribute (@attributes) {
1477    my($attrList, $file, @fileList, @fixedFileList);
1478    ($attrList, @fileList) = split(/:/, $attribute);
1479    if(!defined($attrList) || !@fileList) {
1480      cleanupDie('--attribute requires <attributes>:<file>');
1481    }
1482    @fixedFileList=();
1483    foreach $file (@fileList) {
1484      if($file =~ /^\//) {
1485        push(@fixedFileList, $root.$file);
1486      }
1487      else {
1488        push(@fixedFileList, $root.'/'.$file);
1489      }
1490    }
1491    if(command($gConfig{'cmd_SetFile'}, '-a', $attrList, @fixedFileList)) {
1492      cleanupDie('SetFile failed to set attributes');
1493    }
1494  }
1495  return;
1496}
1497
1498sub trapSignal($) {
1499  my($signalName);
1500  ($signalName) = @_;
1501  cleanupDie('exiting on SIG'.$signalName);
1502}
1503
1504sub usage() {
1505  print STDERR (
1506"usage: pkg-dmg --source <source-folder>\n".
1507"               --target <target-image>\n".
1508"              [--format <format>]           (default: UDZO)\n".
1509"              [--volname <volume-name>]     (default: same name as source)\n".
1510"              [--tempdir <temp-dir>]        (default: same dir as target)\n".
1511"              [--mkdir <directory>]         (make directory in image)\n".
1512"              [--copy <source>[:<dest>]]    (extra files to add)\n".
1513"              [--symlink <source>[:<dest>]] (extra symlinks to add)\n".
1514"              [--license <file>]            (plain text license agreement)\n".
1515"              [--resource <file>]           (flat .r files to merge)\n".
1516"              [--icon <icns-file>]          (volume icon)\n".
1517"              [--attribute <a>:<file>]      (set file attributes)\n".
1518"              [--idme]                      (make Internet-enabled image)\n".
1519"              [--sourcefile]                (treat --source as a file)\n".
1520"              [--verbosity <level>]         (0, 1, 2; default=2)\n".
1521"              [--dry-run]                   (print what would be done)\n");
1522  return;
1523}
1524