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