|
| 1 | +#!/usr/bin/perl -w |
| 2 | +# |
| 3 | +# mkdmg.pl |
| 4 | +# Version 1.0 |
| 5 | +# $Revision: 1.17 $ |
| 6 | +# $Date: 2004/02/05 02:14:39 $ |
| 7 | +# |
| 8 | +# Copyright 2002 Evan Jones <ejones@uwaterloo.ca> |
| 9 | +# http://www.eng.uwaterloo.ca/~ejones/ |
| 10 | +# |
| 11 | +# Released under the BSD Licence, but I would appreciate it if you email |
| 12 | +# me if you find this script useful, or if you find any bugs I should fix. |
| 13 | +# |
| 14 | +# Permission to use, copy, modify, distribute, and sell this software and its |
| 15 | +# documentation for any purpose is hereby granted without fee, provided that |
| 16 | +# the above copyright notice appear in all copies and that both that |
| 17 | +# copyright notice and this permission notice appear in supporting |
| 18 | +# documentation. No representations are made about the suitability of this |
| 19 | +# software for any purpose. It is provided "as is" without express or |
| 20 | +# implied warranty. |
| 21 | +# |
| 22 | +# Inspired by createDiskImage: |
| 23 | +# http://graphics.stepwise.com/Articles/Technical/ProjectBuilder-DMG-Scripts.dmg |
| 24 | +# |
| 25 | +# Contributions: |
| 26 | +# Ben Hines <bhines@alumni.ucsd.edu> - File names with periods and spaces |
| 27 | +# |
| 28 | +# TODO: Handle spaces in script mode. |
| 29 | + |
| 30 | +use strict; |
| 31 | + |
| 32 | +# Parse command line options |
| 33 | +use Getopt::Std; |
| 34 | + |
| 35 | +# TOOL CONFIGURATION |
| 36 | +# Customize the tools that are used as you wish |
| 37 | +# Note: These are all arrays to avoid undesired shell expansion |
| 38 | + |
| 39 | +# Program that performs copies. You may prefer CpMac. |
| 40 | +# If you want to use UFS, you need to run "cp" as root (I suggest using sudo) |
| 41 | +my @copy = qw{cp}; |
| 42 | +my @copyOptions = qw{-R}; |
| 43 | + |
| 44 | +# Command to use to expand shell metacharacters: A string because we WANT to use the shell |
| 45 | +my $expand = "/bin/ls -d"; |
| 46 | +sub shellExpandFileName( $ ) |
| 47 | +{ |
| 48 | + # expands the parameter using ls and the shell |
| 49 | + my $arg = shift(); |
| 50 | + return split( ' ', `$expand $arg 2> /dev/null` ); |
| 51 | +} |
| 52 | + |
| 53 | +# Create disk images |
| 54 | +my @createImage = qw{hdiutil create}; |
| 55 | +my @createImageOptions = qw{-megabytes 200 -type SPARSE -fs HFS+ -quiet -volname}; |
| 56 | + |
| 57 | +# Mounting disk images |
| 58 | +my $mountImage = "hdid"; |
| 59 | + |
| 60 | +# Unmounting disk images |
| 61 | +my @unmountImage = qw{hdiutil eject}; |
| 62 | +my @unmountImageOptions = qw{-quiet}; |
| 63 | + |
| 64 | +# Converting disk images |
| 65 | +my @convertImage = qw{hdiutil convert}; |
| 66 | +#my @convertImageOptions = qw{-format UDZO -imagekey zlib-level=9 -quiet -o}; |
| 67 | +my @convertImageOptions = qw{-format UDRO -quiet -o}; |
| 68 | + |
| 69 | +# Compressing disk images |
| 70 | +my %compressCommands = ( |
| 71 | + '.dmg.bz2' => [ qw{nice bzip2 --best --force} ], |
| 72 | + '.dmg.gz' => [ qw{nice gzip --best --force} ], |
| 73 | + '.dmg' => undef, |
| 74 | +); |
| 75 | +my $compressCommand = undef; |
| 76 | +my $compressedImage = undef; |
| 77 | + |
| 78 | +# BEGIN PROGRAM |
| 79 | + |
| 80 | +my %commandLineSwitches; |
| 81 | +my $getoptsSucceeded = getopts( 'sv', \%commandLineSwitches ); |
| 82 | + |
| 83 | +my $verbose = exists( $commandLineSwitches{v} ); |
| 84 | +my $scriptMode = exists( $commandLineSwitches{s} ); |
| 85 | + |
| 86 | +# Validate command line options |
| 87 | +if ( ! $getoptsSucceeded || ( $scriptMode && @ARGV < 1 ) || ( ! $scriptMode && @ARGV < 2 ) ) |
| 88 | +{ |
| 89 | + print <<EOF; |
| 90 | +usage: mkdmg.pl [OPTION]... <ImageName> [FILE]... |
| 91 | + -v Verbose: Prints messages about each step |
| 92 | + -s Script mode: Reads source/destination from standard input |
| 93 | +EOF |
| 94 | + exit( 0 ); |
| 95 | +} |
| 96 | + |
| 97 | +# Grab the size and image name arguments. |
| 98 | +my $readonlyImage = shift(); |
| 99 | +my $specifiedImage = $readonlyImage; |
| 100 | + |
| 101 | +# Match a compression format based on extension |
| 102 | +$compressedImage = undef; |
| 103 | +while ( my ($extension, $command) = each %compressCommands ) |
| 104 | +{ |
| 105 | + my $re = $extension; |
| 106 | + $re =~ s/\./\\./; |
| 107 | + if ( $readonlyImage =~ /^(.*)$re$/ ) |
| 108 | + { |
| 109 | + $readonlyImage = $1; |
| 110 | + $compressCommand = $compressCommands{$extension}; |
| 111 | + $compressedImage = "$readonlyImage$extension"; |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +if ( ! defined $compressedImage ) |
| 116 | +{ |
| 117 | + # We must have a "compressed image" name, or else no extension (default compressed image) |
| 118 | + fail( "Unable to determine output file type based on the extension: $2" ) if ( $readonlyImage =~ /^(.*)(\.[^.]*)$/ ); |
| 119 | + |
| 120 | + $compressCommand = $compressCommands{".dmg.bz2"}; |
| 121 | + $compressedImage = "$readonlyImage.dmg.bz2"; |
| 122 | +} |
| 123 | + |
| 124 | +fail( "Zero length file name: Please specify a name without extension" ) if ( length $readonlyImage == 0 ); |
| 125 | + |
| 126 | +$readonlyImage .= ".dmg"; # Make sure it ends in .dmg |
| 127 | + |
| 128 | +# Other file names and paths: based on the read-only image name |
| 129 | +my $sparseImage = "$readonlyImage.sparseimage"; |
| 130 | +my $volumeName = $readonlyImage; |
| 131 | +$volumeName =~ s/\.dmg$//; |
| 132 | +my $mountPath = "/Volumes/$volumeName"; |
| 133 | + |
| 134 | +# Don't overwrite temporary files or mount when a volume with the same name is mounted |
| 135 | +my @createdFiles = ( $sparseImage, $readonlyImage ); |
| 136 | +push( @createdFiles, $compressedImage ) if ( defined $compressCommand ); |
| 137 | +foreach my $file ( @createdFiles, $mountPath ) |
| 138 | +{ |
| 139 | + # If the file is specified on the command line, it is okay to clobber it |
| 140 | + if ( $file ne $specifiedImage && -e $file ) |
| 141 | + { |
| 142 | + print( "error: $file already exists\n" ); |
| 143 | + exit( 1 ); |
| 144 | + } |
| 145 | +} |
| 146 | +# Since we haven't created any files yet, don't put any on the list |
| 147 | +@createdFiles = (); |
| 148 | + |
| 149 | +# Find all the files we are going to copy |
| 150 | +my @source; |
| 151 | +my @destination; |
| 152 | +if ( $scriptMode ) |
| 153 | +{ |
| 154 | + # Split input on white space |
| 155 | + my @values = (); |
| 156 | + while ( <> ) |
| 157 | + { |
| 158 | + push( @values, split( ' ', $_ ) ); |
| 159 | + } |
| 160 | + |
| 161 | + my $input = 1; |
| 162 | + foreach my $value ( @values ) |
| 163 | + { |
| 164 | + if ( $input ) |
| 165 | + { |
| 166 | + push( @source, $value ); |
| 167 | + $input = 0; |
| 168 | + } |
| 169 | + else |
| 170 | + { |
| 171 | + push( @destination, $value ); |
| 172 | + $input = 1; |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + # There is an error if the number of source files does not match the number of destination files |
| 177 | + fail( "Uneven number of input lines: A source file does not have a destination" ) if ( @source != @destination ); |
| 178 | +} |
| 179 | +else |
| 180 | +{ |
| 181 | + # For command line mode we do no renaming or shell expansion: Just do it as is |
| 182 | + foreach my $line ( @ARGV ) |
| 183 | + { |
| 184 | + push( @source, $line ); |
| 185 | + push( @destination, "" ); |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +# Verify that we have input |
| 190 | +fail( "No input files specified" ) if ( ! @source ); |
| 191 | + |
| 192 | +# Verify that we can find all the source files |
| 193 | +foreach my $sourceFile ( @source ) |
| 194 | +{ |
| 195 | + my @expandedSource = ( $sourceFile ); |
| 196 | + # If we are in script mode, expand shell characters |
| 197 | + if ( $scriptMode ) |
| 198 | + { |
| 199 | + @expandedSource = shellExpandFileName( $sourceFile ); |
| 200 | + fail( "Files not found: $sourceFile" ) if ( @expandedSource == 0 ); |
| 201 | + } |
| 202 | + |
| 203 | + foreach my $file ( @expandedSource ) |
| 204 | + { |
| 205 | + fail( "Cannot not find: $file (matched $sourceFile)" ) if ( ! -e $file ); |
| 206 | + fail( "Cannot read: $file (matched $sourceFile)" ) if ( ! -r $file ); |
| 207 | + } |
| 208 | + print "$sourceFile matches " . join( ", ", @expandedSource ) . "\n" if ( $verbose ); |
| 209 | +} |
| 210 | + |
| 211 | +# Create the image and format it |
| 212 | +print( "Creating temporary disk image: $sparseImage...\n" ) if ( $verbose ); |
| 213 | +push( @createdFiles, $sparseImage ); |
| 214 | +my $code = system( @createImage, $readonlyImage, @createImageOptions, $volumeName ); |
| 215 | +fail( "Creating temporary disk image failed" ) if ( $code || ! -e $sparseImage ); |
| 216 | + |
| 217 | +# Mount the disk image |
| 218 | +print( "Mounting temporary disk image: $sparseImage...\n" ) if ( $verbose ); |
| 219 | +my $output = `$mountImage '$sparseImage'`; |
| 220 | +fail( "Disk image not mounted at $mountPath" ) if ( ! -e $mountPath ); |
| 221 | + |
| 222 | +fail( "Could not determine mount device" ) if ( $output !~ m{^(/dev/disk\d+)\s+\S+\s*$}m ); |
| 223 | +my $mountDevice = $1; |
| 224 | + |
| 225 | +# Copy files to the disk image |
| 226 | +print( "Copying files...\n" ) if ( $verbose ); |
| 227 | + |
| 228 | +$code = 0; |
| 229 | +for ( my $i = 0; $i < @source && ! $code; ++ $i ) |
| 230 | +{ |
| 231 | + my @expandedSource = ( $source[$i] ); |
| 232 | + # If we are in script mode, expand shell characters |
| 233 | + @expandedSource = shellExpandFileName( $source[$i] ) if ( $scriptMode ); |
| 234 | + |
| 235 | + my $destDir = "."; |
| 236 | + # If the source expands to multiple items, and a destination is specified, |
| 237 | + # the destination is a directory to be created. |
| 238 | + if ( @expandedSource > 1 && length $destination[$i] ) |
| 239 | + { |
| 240 | + $destDir = $destination[$i]; |
| 241 | + } |
| 242 | + # If the source is a single file and the destination contains slashes, make sure that each |
| 243 | + # directory before the file name is created. |
| 244 | + elsif ( $destination[$i] =~ m{(.*)/[^/]*$} ) |
| 245 | + { |
| 246 | + $destDir = $1; |
| 247 | + } |
| 248 | + |
| 249 | + if ( ! -e "$mountPath/$destDir" ) |
| 250 | + { |
| 251 | + print( "Creating destination directory: $mountPath/$destDir\n" ) if ( $verbose ); |
| 252 | + |
| 253 | + # Split the directory into seperate parts ( dir1/dir2/dir3 => dir1, dir2, dir3 ) |
| 254 | + my @hierarchy = split( /\//, $destDir ); |
| 255 | + $destDir = ""; |
| 256 | + foreach my $subdirectory ( @hierarchy ) |
| 257 | + { |
| 258 | + $destDir .= "/$subdirectory"; |
| 259 | + mkdir( "$mountPath$destDir" ) or fail( "Could not create directory: $mountPath$destDir: $!" ) if ( ! -e "$mountPath$destDir" ); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + $code = system( @copy, @copyOptions, @expandedSource, "$mountPath/$destination[$i]" ); |
| 264 | + fail( "Files did not copy successfully" ) if ( $code ); |
| 265 | +} |
| 266 | + |
| 267 | +# Unmount the disk image |
| 268 | +print( "Unmounting temporary disk image: $sparseImage...\n" ) if ( $verbose ); |
| 269 | +$code = system( @unmountImage, $mountDevice, @unmountImageOptions ); |
| 270 | +fail( "Unmounting disk image failed" ) if ( $code or -e $mountPath ); |
| 271 | + |
| 272 | +# Create the read-only image |
| 273 | +# Formats (from largest to smallest): |
| 274 | +# UDRO - Read only uncompressed |
| 275 | +# UDCO - ADC compressed |
| 276 | +# UDZO - Zlib compress (specify -imagekey zlib-level=9) |
| 277 | +# UDRO.gz - Read only gzip compressed |
| 278 | +# UDRO.bz2 - Read only bzip2 compressed (BEST) |
| 279 | +print( "Creating read only disk image: $readonlyImage\n" ) if ( $verbose ); |
| 280 | +push( @createdFiles, $readonlyImage ); |
| 281 | +system( @convertImage, $sparseImage, @convertImageOptions, $readonlyImage ); |
| 282 | +unlink( $sparseImage ); |
| 283 | +unlink( "._$sparseImage" ) if ( -e "._$sparseImage" ); # Remove "resource fork" on any non HFS file systems |
| 284 | +fail( "Read only image: $readonlyImage does not exist" ) if ( ! -e $readonlyImage ); |
| 285 | +unlink( "._$readonlyImage" ) or die "Could not unlink: $!" if ( -e "._$readonlyImage" ); # Remove "resource fork" on any non HFS file systems |
| 286 | + |
| 287 | +print( "Read only disk image sucessfully created: $readonlyImage\n" ) if ( $verbose ); |
| 288 | + |
| 289 | +# Compress the disk image |
| 290 | +if ( defined $compressCommand ) |
| 291 | +{ |
| 292 | + print( "Compressing disk image: $readonlyImage to $compressedImage\n" ) if ( $verbose ); |
| 293 | + push( @createdFiles, $compressedImage ); |
| 294 | + $code = system( @$compressCommand, $readonlyImage ); |
| 295 | + fail( "Compressing image failed" ) if ( $code or ! -e $compressedImage ); |
| 296 | + |
| 297 | + print( "Compressed image sucessfully created: $compressedImage\n" ) if ( $verbose ); |
| 298 | +} |
| 299 | + |
| 300 | +# Cleans up and quits in a gross fashion |
| 301 | +sub fail |
| 302 | +{ |
| 303 | + my $string = shift(); |
| 304 | + print( "error: $string\n" ); |
| 305 | + |
| 306 | + # Gross hack: We exploit global variables to try and clean up gracefully |
| 307 | + system( @unmountImage, $mountDevice, @unmountImageOptions ) if ( defined $mountDevice ); |
| 308 | + foreach my $file ( @createdFiles ) |
| 309 | + { |
| 310 | + unlink $file if ( -e $file ); |
| 311 | + unlink( "._$file" ) if ( -e "._$file" ); # Remove "resource fork" on any non HFS file systems |
| 312 | + } |
| 313 | + |
| 314 | + exit( 1 ); |
| 315 | +} |
0 commit comments