# Copyright (c) 2007, The RubyCocoa Project.
# Copyright (c) 2005-2006, Jonathan Paisley.
# All Rights Reserved.
#
# RubyCocoa is free software, covered under either the Ruby's license or the
# LGPL. See the COPYRIGHT file for more information.
# 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.
#
# Use ENV['RUBYCOCOA_STANDALONEIFYING?'] in your application to check if it's being standaloneified.
# FIXME: Using evaluation is "evil", should use RubyNode instead. Eloy Duran.
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
$stderr.puts "standaloneifying Gem::VERSION: #{Gem::VERSION}"
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.loaded_specs.values.each do |gem|
begin
next unless gem.activated?
rescue NoMethodError
next unless gem.loaded?
end
$stderr.puts "Found gem #{gem.name}"
FileUtils.cp_r(gem.full_gem_path,gems_gem_d)
begin
spec_path = gem.spec_file
rescue NoMethodError
spec_path = File.join(gem.installation_path,"specifications",gem.full_name)
end
unless File.exist?(spec_path)
spec_path = File.join(gem.installation_path,"specifications",gem.full_name + ".gemspec")
end
FileUtils.cp(spec_path,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
# Add basis RubyGems dependencies that are not detected since
# require is overwritten and doesn't modify $LOADED_FEATURES.
%w{fileutils.rb etc.bundle}.each { |f|
files_and_paths << [f, find_file_in_load_path(f)]
}
end
return files_and_paths
end
end
require 'osx/cocoa'
$stderr.puts "standaloneifying Ruby Version: #{RUBY_VERSION}"
$stderr.puts "standaloneifying RubyCocoa Version: #{OSX::RUBYCOCOA_VERSION}"
rcbundle = OSX::NSBundle.bundleForClass_(OSX::RBObject)
$stderr.puts "standaloneifying RubyCocoa path: #{rcbundle.bundlePath}"
module OSX
def self.NSApplicationMain(*args)
# Prevent application main loop from starting
end
end
$LOADED_FEATURES << "rubycocoa.bundle"
if RUBY_VERSION >= '2.0'
procinfo = OSX::NSProcessInfo.processInfo
procinfo.processName = ARGV[0]
else
$0 = ARGV[0]
end
require ARGV[0]
loaded_features = $LOADED_FEATURES.uniq.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)
# Set an environment variable that can be checked inside the application.
# This is useful because standaloneify uses evaluation, so it might be possible
# that the application does something which leads to problems while standaloneifying.
ENV['RUBYCOCOA_STANDALONEIFYING?'] = 'true'
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
# get RubyCocoa.framework version
rc_path = File.join(resources_d, '../Frameworks/RubyCocoa.framework')
rc_vers = `/usr/bin/defaults read "#{File.join(rc_path,'Resources','Info')}" CFBundleShortVersionString`
if rc_vers.to_f >= 1.0 && rc_vers >= "1.0.4"
# 1.0.5 or later requires "--ruby-cocoa-opt" to process args as ruby options
opts = ["--rubycocoa-ruby-opt",__FILE__,MAGIC_ARGUMENT]
else
opts = [__FILE__,MAGIC_ARGUMENT]
end
system(mainprog, *opts)
begin
result = eval(File.read(dump_file))
rescue
# print failed command and loaded libraries
$stderr.puts ">> command: #{mainprog} #{opts.join(' ')}"
begin
env_print_lib = ENV["DYLD_PRINT_LIBRARIES"]
ENV["DYLD_PRINT_LIBRARIES"] = "1"
require 'open3'
libs = Open3.popen3(mainprog, *opts) do |stdin, stdout, stderr|
stdin.close
stderr.readlines
end
$stderr.puts libs.grep(/^dyld.*ruby/i).map {|line| '>> ' + line}
ensure
ENV["DYLD_PRINT_LIBRARIES"] = env_print_lib
end
$stderr.puts "Error: 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(0744,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.bundle_version(bundle)
`/usr/bin/defaults read "#{File.join(bundle,'Resources','Info')}" CFBundleShortVersionString`.chomp
end
def self.make_standalone_application(source,dest,extra_libs,framework_paths)
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")
# Calculate paths to the to-be 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")
# First check if the developer might already have added the RubyCocoa framework (in a copy phase)
unless File.exist? ruby_cocoa_d
# Create Frameworks dir and copy RubyCocoa in there
FileUtils.mkdir_p(frameworks_d)
FileUtils.mkdir_p(lib_d)
rc_search_path = framework_paths.collect { |dir| File.join(dir, 'RubyCocoa.framework') }
rc_search_path +=
["/Library/Frameworks/RubyCocoa.framework",
"/System/Library/Frameworks/RubyCocoa.framework",]
rc_path = rc_search_path.find { |p| File.exist?(p) }
raise "Cannot locate RubyCocoa.framework" unless rc_path
# FileUtils.cp_r(rc_path,frameworks_d)
# Do not use FileUtils.cp_r because it tries to follow symlinks.
if system("cp -R \"#{rc_path}\" \"#{frameworks_d}\"")
$stderr.puts "INFO: RubyCocoa.framework (version #{bundle_version(rc_path)}) copied from \"#{rc_path}\"."
else
raise "cannot copy #{rc_path} to #{frameworks_d}"
end
else
$stderr.puts "INFO: framework \"#{ruby_cocoa_d}\" (version #{bundle_version(ruby_cocoa_d)}) already exists. standaloneifying.rb will not overwrite this file."
end
# 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 path[0..(resources_d.length - 1)] == resources_d
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
$stderr.puts "\nFinish: \"#{dest}\" generated."
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
config.framework_paths = []
ARGV.options do |opts|
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"
opts.on("-f","--force","Delete target app if it exists already") {|v| config.force=v}
opts.on("-d DEST","--dest","Place result at DEST (required)") {|v| config.dest=v}
opts.on("-l LIBRARY","--lib","Extra library to bundle") { |lib| config.extra_libs << lib }
opts.on("--framework DIR","Add search path for RubyCocoa.framework") { |dir| config.framework_paths << dir }
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,config.framework_paths)
end