From: Jonathan P. <jp...@dc...> - 2005-09-16 17:14:42
|
In case anybody's interested, here's a repost of my script 'standaloneify.rb' for turning a development RubyCocoa app into a standalone application. Hopefully it'll survive the mailing list posting. Last time I made it an attachment it got lost. # standaloneify.rb # Author: Jonathan Paisley <jp-www dcs.gla.ac.uk> # $Id: standaloneify.rb 736 2005-04-23T09:31:55.719659Z jp $ # # Takes a built RubyCocoa app bundle (as produced by the # Xcode/ProjectBuilder template) and copies it into a new # app bundle that has all dependencies resolved. # # usage: # ruby standaloneify.rb -d mystandaloneprog.app mybuiltprog.app # # This creates a new application that should have dependencies resolved. # # The script attempts to identify dependencies by running the program # without OSX.NSApplicationMain, then grabbing the list of loaded # ruby scripts and extensions. This means that only the libraries that # you 'require' are bundled. # # NOTES: # # Your ruby installation MUST NOT be the standard Panther install - # the script depends on ruby libraries being in non-standard paths to # work. # # I've only tested it with a DarwinPorts install of ruby 1.8.2. # # Extension modules should be copied over correctly. # # Ruby gems that are used are copied over in their entirety (thanks to some # ideas borrowed from rubyscript2exe) # # install_name_tool is used to rewrite dyld load paths - this may not work # depending on how your libraries have been compiled. I've not had any # issues with it yet though. module Standaloneify MAGIC_ARGUMENT = '--standaloneify' def self.find_file_in_load_path(filename) return filename if filename[0] == ?/ paths = $LOAD_PATH.select do |p| path = File.join(p,filename) return path if File.exist?(path) end return nil end end if __FILE__ == $0 and ARGV[0] == Standaloneify::MAGIC_ARGUMENT then # Got magic argument ARGV.shift module Standaloneify LOADED_FILES = [] def self.notify_loaded(filename) LOADED_FILES << filename unless LOADED_FILES.include?(filename) end end module Kernel alias :pre_standaloneify_load :load def load(*args) if self.is_a?(OSX::OCObjWrapper) then return self.method_missing(:load,*args) end filename = args[0] result = pre_standaloneify_load(*args) Standaloneify.notify_loaded(filename) if filename and result return result end end module Standaloneify def self.find_files(loaded_features,loaded_files) loaded_features.delete("rubycocoa.bundle") files_and_paths = (loaded_features + loaded_files).map do |file| [file,find_file_in_load_path(file)] end files_and_paths.reject! { |f,p| p.nil? } if defined?(Gem) then resources_d = OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation gems_home_d = File.join(resources_d,"RubyGems") gems_gem_d = File.join(gems_home_d,"gems") gems_spec_d = File.join(gems_home_d,"specifications") FileUtils.mkdir_p(gems_spec_d) FileUtils.mkdir_p(gems_gem_d) Gem::Specification.list.each do |gem| next unless gem.loaded? $stderr.puts "Found gem #{gem.name}" FileUtils.cp_r(gem.full_gem_path,gems_gem_d) FileUtils.cp(File.join (gem.installation_path,"specifications",gem.full_name + ".gemspec"),gems_spec_d) # Remove any files that come from the GEM files_and_paths.reject! { |f,p| p.index(gem.full_gem_path) == 0 } end end return files_and_paths end end require 'osx/cocoa' module OSX def self.NSApplicationMain(*args) # Prevent application main loop from starting end end $LOADED_FEATURES << "rubycocoa.bundle" $0 = ARGV[0] require ARGV[0] loaded_features = $LOADED_FEATURES.dup loaded_files = Standaloneify::LOADED_FILES.dup require 'fileutils' result = Standaloneify.find_files(loaded_features, loaded_files) File.open(ENV["STANDALONEIFY_DUMP_FILE"],"w") {|fp| fp.write (result.inspect) } exit 0 end module Standaloneify RB_MAIN_PREFIX = <<-EOT.gsub(/^ */,'') ######################################################################## ######## # #{File.basename(__FILE__)} patch ######################################################################## ######## # Remove all entries that aren't in the application bundle COCOA_APP_RESOURCES_DIR = File.dirname(__FILE__) $LOAD_PATH.reject! { |d| d.index(File.dirname (COCOA_APP_RESOURCES_DIR))!=0 } $LOAD_PATH << File.join(COCOA_APP_RESOURCES_DIR,"ThirdParty") $LOAD_PATH << File.join(File.dirname(COCOA_APP_RESOURCES_DIR),"lib") $LOADED_FEATURES << "rubycocoa.bundle" ENV['GEM_HOME'] = ENV['GEM_PATH'] = File.join (COCOA_APP_RESOURCES_DIR,"RubyGems") ######################################################################## ######## EOT def self.patch_main_rb(resources_d) rb_main = File.join(resources_d,"rb_main.rb") main_script = RB_MAIN_PREFIX + File.read(rb_main) File.open(rb_main,"w") do |fp| fp.write(main_script) end end def self.get_dependencies(macos_d,resources_d) dump_file = File.join(resources_d,"__require_dump") # Run the main Mac program mainprog = Dir[File.join(macos_d,"*")][0] ENV['STANDALONEIFY_DUMP_FILE'] = dump_file system(mainprog,__FILE__,MAGIC_ARGUMENT) begin result = eval(File.read(dump_file)) rescue $stderr.puts "Couldn't read dependency list" exit 1 end File.unlink(dump_file) result end class LibraryFixer def initialize @done = {} end def self.needs_to_be_bundled(path) case path when %r:^/usr/lib/: return false when %r:^/lib/: return false when %r:^/Library/Frameworks: $stderr.puts "WARNING: don't know how to deal with frameworks (%s)" % path.inspect return false when %r:^/System/Library/Frameworks: return false when %r:^@executable_path: $stderr.puts "WARNING: can't handle library with existing @executable_path reference (%s)" % path.inspect return false end return true end ## For the given library, copy into the lib dir (if copy_self), ## iterate through dependent libraries and copy them if necessary, ## updating the name in self def fixup_library(relative_path,full_path,dest_root,copy_self=true) prefix = "@executable_path/../lib" lines = %x[otool -L '#{full_path}'].split("\n") paths = lines.map { |x| x.split[0] } paths.shift # argument name return if @done[full_path] if copy_self then @done[full_path] = true new_path = File.join(dest_root,relative_path) internal_path = File.join(prefix,relative_path) FileUtils.mkdir_p(File.dirname(new_path)) FileUtils.cp(full_path,new_path) File.chmod(0700,new_path) full_path = new_path system("install_name_tool","-id",internal_path,new_path) end paths.each do |path| next if File.basename(path) == File.basename(full_path) if self.class.needs_to_be_bundled(path) then puts "Fixing %s in %s" % [path.inspect,full_path.inspect] fixup_library(File.basename(path),path,dest_root) lib_name = File.basename(path) new_path = File.join(dest_root,lib_name) internal_path = File.join(prefix,lib_name) system("install_name_tool","- change",path,internal_path,full_path) end end end end def self.make_standalone_application(source,dest,extra_libs) FileUtils.cp_r(source,dest) dest_d = Pathname.new(dest).realpath.to_s # Calculate various paths in new app bundle contents_d = File.join(dest_d,"Contents") frameworks_d = File.join(contents_d,"Frameworks") resources_d = File.join(contents_d,"Resources") lib_d = File.join(contents_d,"lib") macos_d = File.join(contents_d,"MacOS") # Create Frameworks dir and copy RubyCocoa in there FileUtils.mkdir_p(frameworks_d) FileUtils.mkdir_p(lib_d) FileUtils.cp_r("/Library/Frameworks/ RubyCocoa.framework",frameworks_d) # Calculate paths to newly copied RubyCocoa framework ruby_cocoa_d = File.join(frameworks_d,"RubyCocoa.framework") ruby_cocoa_inc = File.join(ruby_cocoa_d,"Resources","ruby") ruby_cocoa_lib = File.join(ruby_cocoa_d,"RubyCocoa") # Copy in and update library references for RubyCocoa fixer = LibraryFixer.new fixer.fixup_library(File.basename (ruby_cocoa_lib),ruby_cocoa_lib,lib_d,false) third_party_d = File.join(resources_d,"ThirdParty") FileUtils.mkdir_p(third_party_d) # Calculate bundles and Ruby modules needed dependencies = get_dependencies(macos_d,resources_d) patch_main_rb(resources_d) extra_libs.each do |lib| dependencies << [lib,find_file_in_load_path(lib)] end dependencies.each do |feature,path| case feature when /\.rb$/ next if feature[0] == ?/ if File.exist?(File.join(ruby_cocoa_inc,feature)) then puts "Skipping RubyCocoa file " + feature.inspect next end if File.exist?(File.join(resources_d,feature)) then puts "Skipping existing Resource file " + feature.inspect next end dir = File.join(third_party_d,File.dirname(feature)) FileUtils.mkdir_p(dir) puts "Copying " + feature.inspect FileUtils.cp(path,File.join(dir,File.basename(feature))) when /\/rubycocoa.bundle$/ next when /\.bundle$/ puts "Copying bundle " + feature.inspect base = File.basename(path) if path then if feature[0] == ?/ then relative_path = File.basename(feature) else relative_path = feature end fixer.fixup_library(relative_path,path,lib_d) else puts "WARNING: Bundle #{extra} not found" end else $stderr.puts "WARNING: unknown feature %s loaded" % feature.inspect end end end end if $0 == __FILE__ then require 'ostruct' require 'optparse' require 'pathname' require 'fileutils' config = OpenStruct.new config.force = false config.extra_libs = [] config.dest = nil ARGV.options do |opts| opts.banner = "usage: #{File.basename(__FILE__)} -d DEST [options] APPLICATION" opts.on("-f","--force","Delete target app if it exists already") { |config.force| } opts.on("-d DEST","--dest","Place result at DEST (required)") {| config.dest|} opts.on("-l LIBRARY","--lib","Extra library to bundle") { |lib| config.extra_libs << lib } opts.parse! end if not config.dest or ARGV.length!=1 then $stderr.puts ARGV.options exit 1 end source_app_d = ARGV.shift if config.dest !~ /\.app$/ then $stderr.puts "Target must have '.app' extension" exit 1 end if File.exist?(config.dest) then if config.force then FileUtils.rm_rf(config.dest) else $stderr.puts "Target exists already (#{config.dest.inspect})" exit 1 end end Standaloneify.make_standalone_application (source_app_d,config.dest,config.extra_libs) end |