-
Notifications
You must be signed in to change notification settings - Fork 1
/
standaloneify.rb
401 lines (323 loc) · 12.3 KB
/
standaloneify.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# 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
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
# 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'
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.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
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")
# 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_path = [
"/System/Library/Frameworks/RubyCocoa.framework",
"/Library/Frameworks/RubyCocoa.framework"
].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.
unless system("cp -R \"#{rc_path}\" \"#{frameworks_d}\"")
raise "cannot copy #{rc_path} to #{frameworks_d}"
end
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
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\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") { |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