00001 #!/usr/bin/perl -w
00002 # ============================================================================
00003 # = NAME
00004 # osx-bundler.pl - General purpose application bundling utility
00005 #
00006 # = USAGE
00007 my $usage = '
00008 osx-bundler.pl executable [lib-dir] [lib-dir...]
00009 osx-bundler.pl target.app [lib-dir] [lib-dir...]
00010 osx-bundler.pl target1.app/Contents/MacOS/target2 [lib-dir...]
00011 osx-bundler.pl target1.app/Contents/Resources/lib/extra.dylib [lib-dir...]
00012 ';
00013 # The first form builds a new bundle, executable.app
00014 # The second form checks/adds Frameworks in an existing bundle
00015 # The third form checks/updates/adds Frameworks
00016 # for an extra executable in an existing bundle,
00017 # and the fourth form does the same for an extra library.
00018 #
00019 # = DESCRIPTION
00020 # On OS X, make usually just gives you an executable.
00021 # Some toolsets (e.g. TrollTech's Qt) even build a .app bundle.
00022 # But, I couldn't find anything to build a bundle ready to package and
00023 # ship to the customer as a fully standalone application, so here is one.
00024 #
00025 # Basically, this script:
00026 # 1) uses otool to determine any shared libraries that the executable calls
00027 # (and any that the shared libs call)
00028 # 2) copies them into the bundle's Framework directory
00029 # 3) uses install_name_tool to update the library load paths
00030 #
00031 # = KNOWN BUGS
00032 # Doesn't do anything about Universal Binaries yet
00033 #
00034 # = TO DO
00035 # Add more arguments to allow the user to specify
00036 # .pinfo fields like CFBundleIdentifier, CFBundleSignature,
00037 # NSHumanReadableCopyright, CFBundleGetInfoString, et c.
00038 #
00039 # = REVISION
00040 # $Id: osx-bundler.pl 15443 2008-01-14 22:14:47Z nigel $
00041 #
00042 # = AUTHORS
00043 # Nigel Pearson. Based on osx-packager by Jeremiah Morris,
00044 # with improvements by Geoffrey Kruse and David Abrahams
00045 # ============================================================================
00046
00047 use strict;
00048 use Cwd;
00049 use File::Basename;
00050 use Getopt::Long;
00051
00052 sub usage($)
00053 {
00054 print "Usage:$usage";
00055 exit @_;
00056 }
00057
00058 if ( $#ARGV < 0 )
00059 {
00060 print "No application or executable provided\n";
00061 usage(-1);
00062 }
00063
00064 # ============================================================================
00065
00066 my $verbose = 0;
00067 my $Id = '$Id: osx-bundler.pl 15443 2008-01-14 22:14:47Z nigel $'; # Version of this script. From version control system
00068 my $binary;
00069 my $binbase; # $binary without any directory path
00070 my $bundle;
00071 my @libdirs;
00072 my $target; # Full path to the binary under $bundle
00073
00074 # Process arguments:
00075
00076 Getopt::Long::GetOptions('verbose' => \$verbose) or usage(-1);
00077
00078 $binary = shift @ARGV;
00079 @libdirs = @ARGV;
00080
00081 # ============================================================================
00082
00083 if ( $binary =~ m/(.*)\.app$/ )
00084 {
00085 $bundle = $binary;
00086
00087 # executable name, which in blah.app is usually blah
00088 $binbase = basename($1);
00089 $target = "$bundle/Contents/MacOS/$binbase";
00090
00091 if ( ! -e $target )
00092 {
00093 &Complain("Couldn't locate $target");
00094 exit -2;
00095 }
00096 }
00097 elsif ( $binary !~ m/\.app/ ) # No .app means second form (binary executable)
00098 {
00099 if ( ! -e $binary )
00100 {
00101 &Complain("Couldn't locate $binary");
00102 exit -3;
00103 }
00104
00105 $bundle = "$binary.app";
00106
00107 $binbase = basename($binary);
00108 $target = "$bundle/Contents/MacOS/$binbase";
00109
00110 mkdir $bundle || die;
00111 mkdir "$bundle/Contents";
00112 mkdir "$bundle/Contents/MacOS";
00113 &Syscall([ '/bin/cp', '-p', $binary, $target ]) or die;
00114
00115 # write a custom Info.plist
00116 &GeneratePlist($binary, $binbase, $bundle, '1.0');
00117 }
00118 elsif ( $binary =~ m/\.app/ ) # Third/fourth form (exe/lib in existing bundle)
00119 {
00120 $target = $binary;
00121
00122 if ( ! -e $target )
00123 {
00124 &Complain("Couldn't locate $target");
00125 exit -4;
00126 }
00127
00128 $bundle = $target;
00129 $bundle =~ s/\.app.*/.app/;
00130 }
00131
00132
00133 &Verbose("Installing frameworks into $target");
00134 &PackagedExecutable($bundle, $target, @libdirs);
00135 exit 0;
00136
00137
00138 ######################################
00139 ## Given an application package $bundle and an executable
00140 ## $target that has been copied into it, PackagedExecutable
00141 ## makes sure the package contains all the library dependencies as
00142 ## frameworks and that all the paths internal to the executable have
00143 ## been adjusted appropriately.
00144 ######################################
00145
00146 sub PackagedExecutable($$@)
00147 {
00148 my ($bundle, $target, @libdirs) = @_;
00149
00150 my $fw_dir = "$bundle/Contents/Frameworks";
00151 mkdir $fw_dir;
00152 my $dephash = &ProcessDependencies($target);
00153 my @deps = values %$dephash;
00154 while (scalar @deps)
00155 {
00156 my $dep = shift @deps;
00157 next if $dep =~ m/executable_path/;
00158
00159 $dep = &FindLibraryFile($dep, $bundle, @libdirs);
00160 my $file = &MakeFramework($dep, $fw_dir);
00161 if ( $file )
00162 {
00163 my $newhash = &ProcessDependencies($file);
00164 foreach my $base (keys %$newhash)
00165 {
00166 next if exists $dephash->{$base};
00167 $dephash->{$base} = $newhash->{$base};
00168 push(@deps, $newhash->{$base});
00169 }
00170 }
00171 }
00172 }
00173
00174
00175 ######################################
00176 ## MakeFramework copies a dylib into a
00177 ## framework bundle.
00178 ######################################
00179
00180 sub MakeFramework
00181 {
00182 my ($dylib, $dest) = @_;
00183
00184 my ($base, $vers) = &BaseVers($dylib);
00185 my $fw_dir = $dest . '/' . $base . '.framework';
00186
00187 return '' if ( -e $fw_dir );
00188
00189 &Verbose("Building $base framework");
00190
00191 &Syscall([ '/bin/mkdir', '-p',
00192 "$fw_dir/Versions/A/Resources" ]) or die;
00193 &Syscall([ '/bin/cp', $dylib,
00194 "$fw_dir/Versions/A/$base" ]) or die;
00195
00196 &Syscall([ '/usr/bin/install_name_tool',
00197 '-id', $base, "$fw_dir/Versions/A/$base" ]) or die;
00198
00199 symlink('A', "$fw_dir/Versions/Current") or die;
00200 symlink('Versions/Current/Resources', "$fw_dir/Resources") or die;
00201 symlink("Versions/A/$base", "$fw_dir/$base") or die;
00202
00203 &Verbose("Writing Info.plist for $base framework");
00204 my $plist;
00205 unless (open($plist, '>' . "$fw_dir/Versions/A/Resources/Info.plist"))
00206 {
00207 &Complain("Failed to open $base framework's plist for writing");
00208 die;
00209 }
00210 print $plist <<END;
00211 <?xml version="1.0" encoding="UTF-8"?>
00212 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
00213 <plist version="1.0">
00214 <dict>
00215 <key>CFBundleName</key>
00216 <string>$base</string>
00217 <key>CFBundleIdentifier</key>
00218 <string>org.osx-bundler.$base</string>
00219 <key>CFBundleVersion</key>
00220 <string>$vers</string>
00221 <key>CFBundleSignature</key>
00222 <string>osx-bundler</string>
00223 <key>CFBundlePackageType</key>
00224 <string>FMWK</string>
00225 <key>NSHumanReadableCopyright</key>
00226 <string>Packaged by $Id</string>
00227 <key>CFBundleGetInfoString</key>
00228 <string>lib$base-$vers.dylib, packaged by $Id</string>
00229 </dict>
00230 </plist>
00231 END
00232 close($plist);
00233
00234 return "$fw_dir/Versions/A/$base";
00235 }
00236
00237
00238 ######################################
00239 ## GeneratePlist .
00240 ######################################
00241
00242 sub GeneratePlist
00243 {
00244 my ($name, $binary, $path, $vers) = @_;
00245
00246 &Verbose("Writing Info.plist for $name");
00247 my $plist;
00248 $path .= '/Contents/Info.plist';
00249 unless (open($plist, ">$path"))
00250 {
00251 &Complain("Could not open $path for writing");
00252 die;
00253 }
00254 print $plist <<END;
00255 <?xml version="1.0" encoding="UTF-8"?>
00256 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
00257 <plist version="1.0">
00258 <dict>
00259 <key>CFBundleExecutable</key>
00260 <string>$binary</string>
00261 <key>CFBundleIconFile</key>
00262 <string>application.icns</string>
00263 <key>CFBundleIdentifier</key>
00264 <string>org.osx-bundler.$binary</string>
00265 <key>CFBundleInfoDictionaryVersion</key>
00266 <string>6.0</string>
00267 <key>CFBundlePackageType</key>
00268 <string>APPL</string>
00269 <key>CFBundleShortVersionString</key>
00270 <string>$vers</string>
00271 <key>CFBundleSignature</key>
00272 <string>osx-bundler</string>
00273 <key>CFBundleVersion</key>
00274 <string>$vers</string>
00275 <key>NSAppleScriptEnabled</key>
00276 <string>NO</string>
00277 <key>CFBundleGetInfoString</key>
00278 <string>$vers, $Id</string>
00279 <key>CFBundleName</key>
00280 <string>$binary</string>
00281 <key>NSHumanReadableCopyright</key>
00282 <string>Packaged by $Id</string>
00283 </dict>
00284 </plist>
00285 END
00286 close($plist);
00287
00288 $path =~ s/Info\.plist$/PkgInfo/;
00289 unless (open($plist, ">$path"))
00290 {
00291 &Complain("Could not open $path for writing");
00292 die;
00293 }
00294 print $plist <<END;
00295 APPLMyth
00296 END
00297 close($plist);
00298 }
00299
00300 ######################################
00301 ## FindLibraryFile locates a dylib.
00302 ######################################
00303
00304 sub FindLibraryFile($@)
00305 {
00306 my ($dylib, $bundle, @libdirs) = @_;
00307 my $path;
00308
00309 return Cwd::abs_path($dylib) if (-e $dylib);
00310
00311 #
00312
00313 foreach my $dir ( @libdirs )
00314 {
00315 $path = "$dir/$dylib";
00316 if ( -e $path ) { return Cwd::abs_path($path) }
00317 }
00318 &Complain("Could not find $dylib");
00319 die;
00320 }
00321
00322
00323 ######################################
00324 ## ProcessDependencies catalogs and
00325 ## rewrites dependencies that will be
00326 ## packaged into our app bundle.
00327 ######################################
00328
00329 sub ProcessDependencies(@)
00330 {
00331 my (%depfiles);
00332
00333 foreach my $file (@_)
00334 {
00335 &Verbose("Processing shared library dependencies for $file");
00336 my ($filebase) = &BaseVers($file);
00337
00338 my $cmd = "otool -L $file";
00339 &Verbose($cmd);
00340 my @deps = `$cmd`;
00341 shift @deps; # first line just repeats filename
00342 &Verbose("Dependencies for $file =\n @deps");
00343 foreach my $dep (@deps)
00344 {
00345 chomp $dep;
00346
00347 # otool returns lines like:
00348 # libblah-7.dylib (compatibility version 7, current version 7)
00349 # but we only want the file part
00350 $dep =~ s/\s+(.*) \(.*\)$/$1/;
00351
00352 # Paths like /usr/lib/libstdc++ contain chars that must be escaped
00353 $dep =~ s/([+*?])/\\$1/;
00354
00355 # otool sometimes lists the framework as depending on itself
00356 next if ($file =~ m,/Versions/A/$dep,);
00357
00358 # Any dependency which is already package relative can be ignored
00359 next if $dep =~ m/\@executable_path/;
00360
00361 # skip system library locations
00362 next if ($dep =~ m|^/System| ||
00363 $dep =~ m|^/usr/lib|);
00364
00365 my ($base) = &BaseVers($dep);
00366
00367 # Only add this dependency if needed. This assumes that
00368 # we aren't mixing versions of the same library name
00369 if ( ! -e "$bundle/Contents/Frameworks/$base.framework/$base" )
00370 { $depfiles{$base} = $dep }
00371
00372 &Syscall([ '/usr/bin/install_name_tool', '-change', $dep,
00373 "\@executable_path/../Frameworks/$base.framework/$base",
00374 $file ]) or die;
00375 }
00376 }
00377 return \%depfiles;
00378 }
00379
00380
00381 ######################################
00382 ## BaseVers splits up a dylib file
00383 ## name for framework naming.
00384 ######################################
00385
00386 sub BaseVers
00387 {
00388 my ($filename) = @_;
00389
00390 if ($filename =~ m|^(?:.*/)?lib(.*)\-(\d.*)\.dylib$|)
00391 {
00392 return ($1, $2);
00393 }
00394 elsif ($filename =~ m|^(?:.*/)?lib(.*?)\.(\d.*)\.dylib$|)
00395 {
00396 return ($1, $2);
00397 }
00398 elsif ($filename =~ m|^(?:.*/)?lib(.*?)\.dylib$|)
00399 {
00400 return ($1, undef);
00401 }
00402
00403 &Verbose("Not a library file: $filename");
00404 return $filename;
00405 }
00406
00407
00408 # ============================================================================
00409 # Syscall() wraps the Perl "system" routine with verbosity and error checking
00410 # ============================================================================
00411
00412 sub Syscall
00413 {
00414 my ($arglist, %opts) = @_;
00415
00416 unless (ref $arglist)
00417 {
00418 $arglist = [ $arglist ];
00419 }
00420 # clean out any null arguments
00421 $arglist = [ map $_, @$arglist ];
00422 &Verbose(@$arglist);
00423 my $ret = system(@$arglist);
00424 if ($ret)
00425 {
00426 &Complain('Failed system call: "', @$arglist,
00427 '" with error code', $ret >> 8);
00428 }
00429 return ($ret == 0);
00430 }
00431
00432
00433 sub Verbose
00434 {
00435 if ( $verbose ) { print STDERR 'osx-bundler: ' . join(' ', @_) . "\n" }
00436 }
00437
00438 sub Complain
00439 {
00440 print STDERR 'osx-bundler: ' . join(' ', @_) . "\n";
00441 }
00442
00443 # ============================================================================
00444
00445 1;