#!/usr/bin/perl -w #============================================================================ # NAME: # dmgutil.pl # # DESCRIPTION: # Disk image creation utility. # # COPYRIGHT: # Copyright (c) 2006-2008, refNum Software # <http://www.refnum.com/> # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # o Redistributions of source code must retain the above # copyright notice, this list of conditions and the following # disclaimer. # # o Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials # provided with the distribution. # # o Neither the name of refNum Software nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #============================================================================ # Imports #---------------------------------------------------------------------------- use strict; use Getopt::Long; #============================================================================ # Constants #---------------------------------------------------------------------------- my $kLogging = "-quiet"; my $Rez = "/Developer/Tools/Rez"; my $SetFile = "/Developer/Tools/SetFile"; my $kManPage = <<MANPAGE; NAME dmgutil -- create, adjust, and compress a distribution disk image SYNOPSIS dmgutil --help dmgutil --open --volume=name file dmgutil --set [--x=integer] [--y=integer] [--width=integer] [--height=integer] [--iconsize=integer] [--icon=file] [--background=file] [--bgcol=r,g,b] [--toolbar=boolean] file dmgutil --close --volume=name file DESCRIPTION dmgutil is used to create distribution disk images, and to adjust the Finder view settings of these volumes or their contents. It can be invoked in three modes: open, set, and close. OPEN MODE Open Mode has the following options: --open Select open mode. --volume=name The volume name for the disk image. file The output path for the .dmg file. SET MODE Set Mode has the following options: --set Select set mode. --x=integer The x coordinate of the item. --y=integer The y coordinate of the item. --width=integer The width for a Finder window. --height=integer The height for a Finder window. --iconsize=integer The icon size for a Finder window. --icon=file.icns The icon to apply to the item. --background=file The background picture for a Finder window. --bgcol=r,g,b The background color for a Finder window. --toolbar=boolean The toolbar state for a Finder window. file The file or folder to be set. CLOSE MODE Close Mode has the following options: --close Select close mode. --volume=name The volume name for the disk image. file The output path for the .dmg file. EXAMPLES OPEN MODE To create a new disk image for "MyApp 1.0": dmgutil.pl --open --volume="MyApp 1.0" myapp_1.0.dmg SET MODE To set the position of a file or folder: dmgutil.pl --set --x=100 --y=100 "/Volumes/MyApp 1.0/Read Me.rtf" dmgutil.pl --set --x=200 --y=100 "/Volumes/MyApp 1.0/MyApp.app" To set the window size for the volume: dmgutil.pl --set --width=300 --height=200 "/Volumes/MyApp 1.0" To set the icon size for the volume: dmgutil.pl --set --iconsize=128 "/Volumes/MyApp 1.0" To set a custom icon for a volume, folder, or file: dmgutil.pl --set --icon=volume.icns "/Volumes/MyApp 1.0" dmgutil.pl --set --icon=folder.icns "/Volumes/MyApp 1.0/Extras" dmgutil.pl --set --icon=readme.icns "/Volumes/MyApp 1.0/Read Me.rtf" To set the background picture for the volume: dmgutil.pl --set --background=flowers.jpg "/Volumes/MyApp 1.0" To set the background color for the volume: dmgutil.pl --set --bgcol=0,65535,0 "/Volumes/MyApp 1.0" To hide the toolbar for the volume: dmgutil.pl --set --toolbar=false "/Volumes/MyApp 1.0" Multiple flags may be combined, to set all of the properties of an item simultaneously. CLOSE MODE To unmount and compress the disk image created for "MyApp 1.0": dmgutil.pl --close --volume="MyApp 1.0" myapp_1.0.dmg VERSION dmgutil 1.1 COPYRIGHT Copyright (c) refNum Software http://www.refnum.com/ MANPAGE #============================================================================ # appleScript : Execute an AppleScript. #---------------------------------------------------------------------------- sub appleScript { # Retrieve our parameters my ($theScript) = @_; # Save the script my $theFile = "/tmp/dmgutil_applescript.txt"; open( OUTPUT, ">$theFile") or die "Can't open $theFile for writing: $!\n"; print OUTPUT $theScript; close(OUTPUT); # And execute it system("osascript", $theFile); unlink($theFile); } #============================================================================ # isVolume : Is a path to the root of a volume? #---------------------------------------------------------------------------- sub isVolume { # Retrieve our parameters my ($thePath) = @_; # Check the state # # After stripping out the leading /Volumes, any further slashes # indicate we have a folder rather than a volume. $thePath =~ s/\/Volumes\///; my $isVolume = ($thePath =~ /.*\/.*/) ? 0 : 1; return($isVolume); } #============================================================================ # setFolderState : Set the state for a folder. #---------------------------------------------------------------------------- sub setFolderState { # Retrieve our parameters my ($thePath, $iconSize, $flagToolbar, $bgImage, $bgColor) = @_; # Initialise ourselves my $cmdBackground = ""; my $cmdIconSize = ""; my $cmdToolbar = ""; # Prepare the background # # As of 10.5, the Finder refuses to manipulate files whose names start with # a period (rdar://5582578). As such we need to use an underscore for the # image, then make it invisible using SetFile. if (-f $bgImage) { $bgImage =~ /.*\.(\w+)/; my $dstImage = "$thePath/_Background.$1"; `cp "$bgImage" "$dstImage"`; $cmdBackground .= "set theImage to posix file \"$dstImage\"\n"; $cmdBackground .= " set background picture of theOptions to theImage\n"; $cmdBackground .= " do shell script \"/Developer/Tools/SetFile -a V '$dstImage'\"\n"; } elsif ($bgColor ne "") { $cmdBackground = "set background color of theOptions to {$bgColor} as RGB color"; } # Prepare the icon size if ($iconSize != 0) { $cmdIconSize = "set icon size of theOptions to $iconSize"; } # Prepare the toolbar # # The window must be made visible in order to change the toolbar state. if ($flagToolbar ne "") { $cmdToolbar = " open theWindow \n"; $cmdToolbar .= " set toolbar visible of theWindow to $flagToolbar \n"; $cmdToolbar .= " close theWindow \n"; } # Identify the target # # AppleScript requires the correct nomenclature for the target item. my $theTarget = "folder \"$thePath\""; if (isVolume($thePath)) { $theTarget =~ s/folder "\/Volumes\//disk "/; } # Set the folder state # # Once a change has been made, it must be flushed to disk with update. my $theScript = ""; $theScript .= "tell application \"Finder\"\n"; $theScript .= " set theTarget to $theTarget \n"; $theScript .= " set theWindow to window of theTarget \n"; $theScript .= "\n"; $theScript .= " set current view of theWindow to icon view \n"; $theScript .= " set theOptions to icon view options of theWindow \n"; $theScript .= " set arrangement of theOptions to not arranged \n"; $theScript .= "\n"; $theScript .= " $cmdBackground\n"; $theScript .= " $cmdIconSize \n"; $theScript .= " $cmdToolbar \n"; $theScript .= "\n"; $theScript .= " update theTarget\n"; $theScript .= "end tell"; appleScript($theScript); } #============================================================================ # setCustomIcon : Set a custom icon. #---------------------------------------------------------------------------- sub setCustomIcon { # Retrieve our parameters my ($thePath, $theIcon) = @_; # Validate our state # # We require several tools inside /Developer/Tools. die("Setting an icon requires $Rez") if (! -e $Rez); die("Setting an icon requires $SetFile") if (! -e $SetFile); # Prepare the flags # # Prior to Mac OS X 10.4, SetFile can only set an attribute if it is # first cleared (rdar://3738867). my $sysVers = `uname -r`; my $setHidden = ($sysVers =~ /^[0-7]\./) ? "vV" : "V"; my $setIcon = ($sysVers =~ /^[0-7]\./) ? "cC" : "C"; # Set a volume icon # # Volume custom icons are contained in a .VolumeIcon.icns file. if (isVolume($thePath)) { my $iconFile = "$thePath/.VolumeIcon.icns"; `cp "$theIcon" "$iconFile"`; `$SetFile -a $setHidden "$iconFile"`; `$SetFile -a $setIcon "$thePath"`; } # Set a folder icon # # Folder custom icons are contained in an ('icns', -16455) resource, # placed in an invisible "Icon\r" file inside the folder. elsif (-d $thePath) { my $iconFile = "$thePath/Icon\r"; my $tmpR = "/tmp/dmgutil.r"; `echo "read 'icns' (-16455) \\"$theIcon\\";\n" > $tmpR`; `cd /tmp; $Rez dmgutil.r -append -o "$iconFile"`; `$SetFile -a $setHidden "$iconFile"`; `$SetFile -a $setIcon "$thePath"`; unlink($tmpR); } # Set a file icon # # File custom icons are contained in an ('icns', -16455) resource. else { my $tmpR = "/tmp/dmgutil.r"; `echo "read 'icns' (-16455) \\"$theIcon\\";\n" > $tmpR`; `cd /tmp; $Rez dmgutil.r -append -o "$thePath"`; `$SetFile -a $setIcon "$thePath"`; unlink($tmpR); } } #============================================================================ # setWindowPos : Set the position of a window. #---------------------------------------------------------------------------- sub setWindowPos { # Retrieve our parameters my ($thePath, $posX, $posY, $theWidth, $theHeight) = @_; # Initialise ourselves my $bottom = $posY + $theHeight; my $right = $posX + $theWidth; # Identify the target # # AppleScript requires the correct nomenclature for the target item. my $theTarget = "folder \"$thePath\""; if (isVolume($thePath)) { $theTarget =~ s/folder "\/Volumes\//disk "/; } # Set the window position # # In theory, the "set bounds" command is all that should be necessary # to set the bounds of a Finder window. # # Unfortunately, under 10.4 this will result in a window that will be # taller than the specified size when the window is next opened. # # To reliably set the bounds of a window we must open the window, show # the status bar, and set the window bounds to be 20 pixels taller (the # height of the status bar) than necessary. # # The status bar can then be hidden, the window closed, and the bounds # bounds will be the desired size when the window is next opened. my $theScript = ""; $theScript .= "tell application \"Finder\"\n"; $theScript .= " set theTarget to $theTarget \n"; $theScript .= " set theWindow to window of theTarget \n"; $theScript .= "\n"; $theScript .= " open theWindow \n"; $theScript .= " set statusbar visible of theWindow to true \n"; $theScript .= " set bounds of theWindow to {$posX, $posY, $right, $bottom+20} \n"; $theScript .= " set statusbar visible of theWindow to false \n"; $theScript .= " close theWindow \n"; $theScript .= "\n"; $theScript .= "end tell"; appleScript($theScript); } #============================================================================ # setIconPos : Set the position of an icon. #---------------------------------------------------------------------------- sub setIconPos { # Retrieve our parameters my ($theFile, $posX, $posY) = @_; # Identify the target # # Since the 'posix file' command follows symlinks, in order to set the # position of a symlink (vs its target) we need to use an HFS path and # reference it as a file rather than an alias. my $theTarget = "alias (posix file \"$theFile\")"; if (-l $theFile) { $theTarget = $theFile; $theTarget =~ s/\/Volumes\///; $theTarget =~ s/\//:/g; $theTarget = "file \"$theTarget\""; } # Set the icon position # # Once a change has been made, it must be flushed to disk with update. my $theScript = ""; $theScript .= "tell application \"Finder\"\n"; $theScript .= " set theTarget to $theTarget \n"; $theScript .= "\n"; $theScript .= " set position of theTarget to {$posX, $posY} \n"; $theScript .= " update theTarget\n"; $theScript .= "end tell"; appleScript($theScript); } #============================================================================ # doOpen : Open a new disk image. #---------------------------------------------------------------------------- sub doOpen { # Retrieve our parameters my ($dmgFile, $volName) = @_; # Clean up any previous image system("rm", "-f", "$dmgFile.sparseimage"); system("rm", "-f", "$dmgFile"); # Create the image # # A large sparse disk image is created, which will be shrunk down # and compressed when the disk image is finally closed. print " creating $dmgFile\n" if ($kLogging eq "-quiet"); system("hdiutil", "create", $dmgFile, "-volname", $volName, "-megabytes", "1000", "-type", "SPARSE", "-fs", "HFS+", $kLogging); system("hdiutil", "mount", $kLogging, "$dmgFile.sparseimage"); } #============================================================================ # doClose : Close a disk image. #---------------------------------------------------------------------------- sub doClose { # Retrieve our parameters my ($dmgFile, $volName) = @_; # Bless the volume # # Blessing the volume ensures that the volume always opens in the current # view, overriding the user's "Open new windows in column view" preference. system("bless", "--openfolder", "/Volumes/$volName"); # Compress the image # # On 10.5, the disk image must be ejected rather than unmounted to allow # it to be converted from a sparse image to a compressed image. print " compressing $dmgFile\n" if ($kLogging eq "-quiet"); system("hdiutil", "eject", $kLogging, "/Volumes/$volName"); system("hdiutil", "convert", "$dmgFile.sparseimage", "-format", "UDZO", "-o", $dmgFile, "-imagekey", "zlib-level=9", $kLogging); # Clean up system("rm", "-f", "$dmgFile.sparseimage"); } #============================================================================ # doSet : Set a file/folder state. #---------------------------------------------------------------------------- sub doSet { # Retrieve our parameters my ($thePath, $posX, $posY, $theWidth, $theHeight, $iconSize, $theIcon, $bgImage, $bgColor, $flagToolbar) = @_; # Set the custom icon if ($theIcon ne "") { setCustomIcon($thePath, $theIcon); } # Set the folder state if ($iconSize != 0 || $bgImage ne "" || $bgColor ne "" || $flagToolbar ne "") { setFolderState($thePath, $iconSize, $flagToolbar, $bgImage, $bgColor); } # Set the position # # Window position must be set after applying the folder state. if ($posX != 0 && $posY != 0) { if ($theWidth != 0 && $theHeight != 0) { setWindowPos($thePath, $posX, $posY, $theWidth, $theHeight); } else { setIconPos($thePath, $posX, $posY); } } } #============================================================================ # dmgUtil : Manipulate a disk image. #---------------------------------------------------------------------------- sub dmgUtil { # Retrieve our parameters my ($doOpen, $doClose, $doSet) = (0, 0, 0); my ($posX, $posY, $theWidth, $theHeight, $iconSize) = (0, 0, 0, 0, 0); my ($volName, $theIcon, $bgImage, $bgColor, $flagToolbar) = ("", "", "", "", ""); GetOptions( "--open+", => \$doOpen, "--close+", => \$doClose, "--set+", => \$doSet, "--volume=s", => \$volName, "--x=i", => \$posX, "--y=i", => \$posY, "--width=i", => \$theWidth, "--height=i", => \$theHeight, "--iconsize=i", => \$iconSize, "--icon=s", => \$theIcon, "--background=s", => \$bgImage, "--bgcol=s", => \$bgColor, "--toolbar=s", => \$flagToolbar); my ($thePath) = @ARGV; $thePath = "" if (!defined($thePath)); # Perform the action if ($doOpen && $thePath ne "" && $volName ne "") { doOpen($thePath, $volName); } elsif ($doClose && $thePath ne "" && $volName ne "") { doClose($thePath, $volName); } elsif ($doSet && $thePath ne "") { doSet($thePath, $posX, $posY, $theWidth, $theHeight, $iconSize, $theIcon, $bgImage, $bgColor, $flagToolbar); } else { print $kManPage; } } #============================================================================ # Script entry point #---------------------------------------------------------------------------- dmgUtil();