1# Copyright (c) 2007, The RubyCocoa Project. 2# Copyright (c) 2005-2006, Jonathan Paisley. 3# All Rights Reserved. 4# 5# RubyCocoa is free software, covered under either the Ruby's license or the 6# LGPL. See the COPYRIGHT file for more information. 7 8# Takes a built RubyCocoa app bundle (as produced by the 9# Xcode/ProjectBuilder template) and copies it into a new 10# app bundle that has all dependencies resolved. 11# 12# usage: 13# ruby standaloneify.rb -d mystandaloneprog.app mybuiltprog.app 14# 15# This creates a new application that should have dependencies resolved. 16# 17# The script attempts to identify dependencies by running the program 18# without OSX.NSApplicationMain, then grabbing the list of loaded 19# ruby scripts and extensions. This means that only the libraries that 20# you 'require' are bundled. 21# 22# NOTES: 23# 24# Your ruby installation MUST NOT be the standard Panther install - 25# the script depends on ruby libraries being in non-standard paths to 26# work. 27# 28# I've only tested it with a DarwinPorts install of ruby 1.8.2. 29# 30# Extension modules should be copied over correctly. 31# 32# Ruby gems that are used are copied over in their entirety (thanks to some 33# ideas borrowed from rubyscript2exe) 34# 35# install_name_tool is used to rewrite dyld load paths - this may not work 36# depending on how your libraries have been compiled. I've not had any 37# issues with it yet though. 38# 39# Use ENV['RUBYCOCOA_STANDALONEIFYING?'] in your application to check if it's being standaloneified. 40 41# FIXME: Using evaluation is "evil", should use RubyNode instead. Eloy Duran. 42 43module Standaloneify 44 MAGIC_ARGUMENT = '--standaloneify' 45 46 def self.find_file_in_load_path(filename) 47 return filename if filename[0] == ?/ 48 paths = $LOAD_PATH.select do |p| 49 path = File.join(p,filename) 50 return path if File.exist?(path) 51 end 52 return nil 53 end 54 55end 56 57if __FILE__ == $0 and ARGV[0] == Standaloneify::MAGIC_ARGUMENT then 58 # Got magic argument 59 ARGV.shift 60 61 module Standaloneify 62 LOADED_FILES = [] 63 def self.notify_loaded(filename) 64 LOADED_FILES << filename unless LOADED_FILES.include?(filename) 65 end 66 end 67 68 module Kernel 69 alias :pre_standaloneify_load :load 70 def load(*args) 71 if self.is_a?(OSX::OCObjWrapper) then 72 return self.method_missing(:load,*args) 73 end 74 75 filename = args[0] 76 result = pre_standaloneify_load(*args) 77 Standaloneify.notify_loaded(filename) if filename and result 78 return result 79 end 80 end 81 82 module Standaloneify 83 def self.find_files(loaded_features,loaded_files) 84 85 loaded_features.delete("rubycocoa.bundle") 86 87 files_and_paths = (loaded_features + loaded_files).map do |file| 88 [file,find_file_in_load_path(file)] 89 end 90 91 files_and_paths.reject! { |f,p| p.nil? } 92 93 if defined?(Gem) then 94 resources_d = OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation 95 gems_home_d = File.join(resources_d,"RubyGems") 96 gems_gem_d = File.join(gems_home_d,"gems") 97 gems_spec_d = File.join(gems_home_d,"specifications") 98 99 FileUtils.mkdir_p(gems_spec_d) 100 FileUtils.mkdir_p(gems_gem_d) 101 102 Gem::Specification.list.each do |gem| 103 next unless gem.loaded? 104 $stderr.puts "Found gem #{gem.name}" 105 106 FileUtils.cp_r(gem.full_gem_path,gems_gem_d) 107 FileUtils.cp(File.join(gem.installation_path,"specifications",gem.full_name + ".gemspec"),gems_spec_d) 108 # Remove any files that come from the GEM 109 files_and_paths.reject! { |f,p| p.index(gem.full_gem_path) == 0 } 110 end 111 112 # Add basis RubyGems dependencies that are not detected since 113 # require is overwritten and doesn't modify $LOADED_FEATURES. 114 %w{fileutils.rb etc.bundle}.each { |f| 115 files_and_paths << [f, find_file_in_load_path(f)] 116 } 117 end 118 119 return files_and_paths 120 end 121 end 122 123 require 'osx/cocoa' 124 125 module OSX 126 def self.NSApplicationMain(*args) 127 # Prevent application main loop from starting 128 end 129 end 130 131 $LOADED_FEATURES << "rubycocoa.bundle" 132 133 $0 = ARGV[0] 134 require ARGV[0] 135 136 loaded_features = $LOADED_FEATURES.uniq.dup 137 loaded_files = Standaloneify::LOADED_FILES.dup 138 139 require 'fileutils' 140 141 result = Standaloneify.find_files(loaded_features, loaded_files) 142 File.open(ENV["STANDALONEIFY_DUMP_FILE"],"w") {|fp| fp.write(result.inspect) } 143 144 exit 0 145end 146 147 148module Standaloneify 149 150 RB_MAIN_PREFIX = <<-EOT.gsub(/^ */,'') 151 ################################################################################ 152 # #{File.basename(__FILE__)} patch 153 ################################################################################ 154 # Remove all entries that aren't in the application bundle 155 156 COCOA_APP_RESOURCES_DIR = File.dirname(__FILE__) 157 158 $LOAD_PATH.reject! { |d| d.index(File.dirname(COCOA_APP_RESOURCES_DIR))!=0 } 159 $LOAD_PATH << File.join(COCOA_APP_RESOURCES_DIR,"ThirdParty") 160 $LOAD_PATH << File.join(File.dirname(COCOA_APP_RESOURCES_DIR),"lib") 161 162 $LOADED_FEATURES << "rubycocoa.bundle" 163 164 ENV['GEM_HOME'] = ENV['GEM_PATH'] = File.join(COCOA_APP_RESOURCES_DIR,"RubyGems") 165 166 ################################################################################ 167 EOT 168 169 def self.patch_main_rb(resources_d) 170 rb_main = File.join(resources_d,"rb_main.rb") 171 main_script = RB_MAIN_PREFIX + File.read(rb_main) 172 File.open(rb_main,"w") do |fp| 173 fp.write(main_script) 174 end 175 end 176 177 def self.get_dependencies(macos_d,resources_d) 178 # Set an environment variable that can be checked inside the application. 179 # This is useful because standaloneify uses evaluation, so it might be possible 180 # that the application does something which leads to problems while standaloneifying. 181 ENV['RUBYCOCOA_STANDALONEIFYING?'] = 'true' 182 183 dump_file = File.join(resources_d,"__require_dump") 184 # Run the main Mac program 185 mainprog = Dir[File.join(macos_d,"*")][0] 186 ENV['STANDALONEIFY_DUMP_FILE'] = dump_file 187 system(mainprog,__FILE__,MAGIC_ARGUMENT) 188 189 begin 190 result = eval(File.read(dump_file)) 191 rescue 192 $stderr.puts "Couldn't read dependency list" 193 exit 1 194 end 195 File.unlink(dump_file) 196 result 197 end 198 199 class LibraryFixer 200 def initialize 201 @done = {} 202 end 203 204 def self.needs_to_be_bundled(path) 205 case path 206 when %r:^/usr/lib/: 207 return false 208 when %r:^/lib/: 209 return false 210 when %r:^/Library/Frameworks: 211 $stderr.puts "WARNING: don't know how to deal with frameworks (%s)" % path.inspect 212 return false 213 when %r:^/System/Library/Frameworks: 214 return false 215 when %r:^@executable_path: 216 $stderr.puts "WARNING: can't handle library with existing @executable_path reference (%s)" % path.inspect 217 return false 218 end 219 return true 220 end 221 222 ## For the given library, copy into the lib dir (if copy_self), 223 ## iterate through dependent libraries and copy them if necessary, 224 ## updating the name in self 225 226 def fixup_library(relative_path,full_path,dest_root,copy_self=true) 227 prefix = "@executable_path/../lib" 228 229 lines = %x[otool -L '#{full_path}'].split("\n") 230 paths = lines.map { |x| x.split[0] } 231 paths.shift # argument name 232 233 return if @done[full_path] 234 235 if copy_self then 236 @done[full_path] = true 237 new_path = File.join(dest_root,relative_path) 238 internal_path = File.join(prefix,relative_path) 239 FileUtils.mkdir_p(File.dirname(new_path)) 240 FileUtils.cp(full_path,new_path) 241 File.chmod(0700,new_path) 242 full_path = new_path 243 system("install_name_tool","-id",internal_path,new_path) 244 end 245 246 paths.each do |path| 247 next if File.basename(path) == File.basename(full_path) 248 249 if self.class.needs_to_be_bundled(path) then 250 puts "Fixing %s in %s" % [path.inspect,full_path.inspect] 251 fixup_library(File.basename(path),path,dest_root) 252 253 lib_name = File.basename(path) 254 new_path = File.join(dest_root,lib_name) 255 internal_path = File.join(prefix,lib_name) 256 257 system("install_name_tool","-change",path,internal_path,full_path) 258 end 259 end 260 end 261 end 262 263 def self.make_standalone_application(source,dest,extra_libs) 264 FileUtils.cp_r(source,dest) 265 dest_d = Pathname.new(dest).realpath.to_s 266 267 # Calculate various paths in new app bundle 268 contents_d = File.join(dest_d,"Contents") 269 frameworks_d = File.join(contents_d,"Frameworks") 270 resources_d = File.join(contents_d,"Resources") 271 lib_d = File.join(contents_d,"lib") 272 macos_d = File.join(contents_d,"MacOS") 273 274 # Calculate paths to the to-be copied RubyCocoa framework 275 ruby_cocoa_d = File.join(frameworks_d,"RubyCocoa.framework") 276 ruby_cocoa_inc = File.join(ruby_cocoa_d,"Resources","ruby") 277 ruby_cocoa_lib = File.join(ruby_cocoa_d,"RubyCocoa") 278 279 # First check if the developer might already have added the RubyCocoa framework (in a copy phase) 280 unless File.exist? ruby_cocoa_d 281 # Create Frameworks dir and copy RubyCocoa in there 282 FileUtils.mkdir_p(frameworks_d) 283 FileUtils.mkdir_p(lib_d) 284 rc_path = [ 285 "/System/Library/Frameworks/RubyCocoa.framework", 286 "/Library/Frameworks/RubyCocoa.framework" 287 ].find { |p| File.exist?(p) } 288 raise "Cannot locate RubyCocoa.framework" unless rc_path 289 # FileUtils.cp_r(rc_path,frameworks_d) 290 # Do not use FileUtils.cp_r because it tries to follow symlinks. 291 unless system("cp -R \"#{rc_path}\" \"#{frameworks_d}\"") 292 raise "cannot copy #{rc_path} to #{frameworks_d}" 293 end 294 end 295 296 # Copy in and update library references for RubyCocoa 297 fixer = LibraryFixer.new 298 fixer.fixup_library(File.basename(ruby_cocoa_lib),ruby_cocoa_lib,lib_d,false) 299 300 third_party_d = File.join(resources_d,"ThirdParty") 301 FileUtils.mkdir_p(third_party_d) 302 303 # Calculate bundles and Ruby modules needed 304 dependencies = get_dependencies(macos_d,resources_d) 305 306 patch_main_rb(resources_d) 307 308 extra_libs.each do |lib| 309 dependencies << [lib,find_file_in_load_path(lib)] 310 end 311 312 dependencies.each do |feature,path| 313 314 case feature 315 when /\.rb$/ 316 next if feature[0] == ?/ 317 if File.exist?(File.join(ruby_cocoa_inc,feature)) then 318 puts "Skipping RubyCocoa file " + feature.inspect 319 next 320 end 321 if path[0..(resources_d.length - 1)] == resources_d 322 puts "Skipping existing Resource file " + feature.inspect 323 next 324 end 325 dir = File.join(third_party_d,File.dirname(feature)) 326 FileUtils.mkdir_p(dir) 327 puts "Copying " + feature.inspect 328 FileUtils.cp(path,File.join(dir,File.basename(feature))) 329 330 when /\/rubycocoa.bundle$/ 331 next 332 333 when /\.bundle$/ 334 puts "Copying bundle " + feature.inspect 335 336 base = File.basename(path) 337 if path then 338 if feature[0] == ?/ then 339 relative_path = File.basename(feature) 340 else 341 relative_path = feature 342 end 343 fixer.fixup_library(relative_path,path,lib_d) 344 else 345 puts "WARNING: Bundle #{extra} not found" 346 end 347 348 else 349 $stderr.puts "WARNING: unknown feature %s loaded" % feature.inspect 350 end 351 end 352 end 353 354end 355 356if $0 == __FILE__ then 357 358 require 'ostruct' 359 require 'optparse' 360 require 'pathname' 361 require 'fileutils' 362 363 config = OpenStruct.new 364 config.force = false 365 config.extra_libs = [] 366 config.dest = nil 367 368 ARGV.options do |opts| 369 opts.banner = "usage: #{File.basename(__FILE__)} -d DEST [options] APPLICATION\n\nUse ENV['RUBYCOCOA_STANDALONEIFYING?'] in your application to check if it's being standaloneified.\n" 370 opts.on("-f","--force","Delete target app if it exists already") { |config.force| } 371 opts.on("-d DEST","--dest","Place result at DEST (required)") {|config.dest|} 372 opts.on("-l LIBRARY","--lib","Extra library to bundle") { |lib| config.extra_libs << lib } 373 374 opts.parse! 375 end 376 377 if not config.dest or ARGV.length!=1 then 378 $stderr.puts ARGV.options 379 exit 1 380 end 381 382 source_app_d = ARGV.shift 383 384 if config.dest !~ /\.app$/ then 385 $stderr.puts "Target must have '.app' extension" 386 exit 1 387 end 388 389 if File.exist?(config.dest) then 390 if config.force then 391 FileUtils.rm_rf(config.dest) 392 else 393 $stderr.puts "Target exists already (#{config.dest.inspect})" 394 exit 1 395 end 396 end 397 398 Standaloneify.make_standalone_application(source_app_d,config.dest,config.extra_libs) 399 400end 401 402