ocran 1.4.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.txt +74 -0
  3. data/README.md +296 -9
  4. data/ext/extconf.rb +5 -1
  5. data/lib/ocran/aot_toolchain.rb +154 -0
  6. data/lib/ocran/command_output.rb +8 -1
  7. data/lib/ocran/compatibility_scan.rb +414 -0
  8. data/lib/ocran/cosmo_toolchain.rb +29 -7
  9. data/lib/ocran/dir_builder.rb +88 -38
  10. data/lib/ocran/direction.rb +232 -33
  11. data/lib/ocran/ed_resource.rb +113 -0
  12. data/lib/ocran/ed_resource_builder.rb +199 -0
  13. data/lib/ocran/file_path_set.rb +9 -5
  14. data/lib/ocran/gem_spec_queryable.rb +7 -3
  15. data/lib/ocran/host_config_helper.rb +11 -1
  16. data/lib/ocran/inno_setup_script_builder.rb +24 -14
  17. data/lib/ocran/launcher_batch_builder.rb +8 -11
  18. data/lib/ocran/npm_package.rb +106 -0
  19. data/lib/ocran/option.rb +433 -35
  20. data/lib/ocran/pe_imports.rb +96 -0
  21. data/lib/ocran/pe_resources.rb +226 -0
  22. data/lib/ocran/refine_pathname.rb +29 -32
  23. data/lib/ocran/roundhouse_builder.rb +316 -0
  24. data/lib/ocran/runner.rb +96 -11
  25. data/lib/ocran/runtime_environment.rb +14 -6
  26. data/lib/ocran/runtime_sdk.rb +252 -0
  27. data/lib/ocran/spinel_builder.rb +183 -0
  28. data/lib/ocran/spinel_compatibility.rb +149 -0
  29. data/lib/ocran/static_runtime/boot.rb +443 -0
  30. data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
  31. data/lib/ocran/static_runtime_builder.rb +814 -0
  32. data/lib/ocran/stub_builder.rb +70 -25
  33. data/lib/ocran/version.rb +1 -1
  34. data/lib/ocran/wasm_builder.rb +570 -0
  35. data/lib/ocran/wasm_compatibility.rb +198 -0
  36. data/lib/ocran/windows_command_escaping.rb +21 -0
  37. data/lib/ocran/zip_payload_builder.rb +18 -9
  38. data/lib/ocran/zip_writer.rb +48 -9
  39. data/src/Makefile +18 -2
  40. data/src/Makefile.mswin +65 -0
  41. data/src/inst_dir.c +34 -6
  42. data/src/path_utils.c +144 -0
  43. data/src/script_info.c +5 -0
  44. data/src/stub.c +77 -19
  45. data/src/stub.rc +1 -1
  46. data/src/system_utils.c +276 -155
  47. data/src/system_utils.h +46 -3
  48. data/src/system_utils_posix.c +243 -148
  49. data/src/unpack.c +9 -0
  50. metadata +23 -5
@@ -0,0 +1,443 @@
1
+ # ocran static runtime, stage 1 (ocran/boot.rb inside the archive).
2
+ #
3
+ # Serves the archive appended to the executable as a read-only directory
4
+ # tree rooted at the executable's own path, e.g. C:/apps/tool.exe/src/app.rb.
5
+ # A path below an existing *file* can never exist on disk, so archive paths
6
+ # cannot be shadowed by planted files, and anything this layer does not
7
+ # handle fails loudly (ENOTDIR) instead of silently reading something else.
8
+ #
9
+ # Intercepted, and nothing else:
10
+ # Kernel: require, require_relative, load, open (autoload goes through
11
+ # require), and gem_original_require for RubyGems
12
+ # IO/File: read, binread, readlines, foreach, open, File.new
13
+ # File: exist? file? directory? readable? size size? zero? empty?
14
+ # mtime ftype stat lstat realpath realdirpath symlink? executable?
15
+ # Dir: exist? glob [] children entries each_child foreach empty?
16
+ # Writes to archive paths raise Errno::EROFS.
17
+ module OcranRuntime
18
+ ROOT = EXE # archive members live at "#{ROOT}/<name>"
19
+ PREFIX = "#{ROOT}/".freeze
20
+ # Read now: once File is patched, File.mtime(EXE) is an archive lookup
21
+ # that would end up here again.
22
+ MTIME = File.mtime(EXE)
23
+
24
+ module VirtualFS
25
+ module_function
26
+
27
+ # Member names of files; "dir/" entries only contribute to dirs.
28
+ def files
29
+ @files ||= OcranRuntime.entries.keys.reject { |n| n.end_with?("/") }.freeze
30
+ end
31
+
32
+ def dirs
33
+ @dirs ||= begin
34
+ set = { ROOT => true }
35
+ OcranRuntime.entries.each_key do |name|
36
+ parts = name.split("/")
37
+ parts.pop unless name.end_with?("/")
38
+ parts.each_index { |i| set["#{ROOT}/#{parts[0..i].join("/")}"] = true }
39
+ end
40
+ set.freeze
41
+ end
42
+ end
43
+
44
+ def paths
45
+ @paths ||= (files.map { |n| "#{ROOT}/#{n}" } + dirs.keys).sort.freeze
46
+ end
47
+
48
+ # Absolute virtual path for +path+, or nil when it is not an archive path.
49
+ def vpath(path)
50
+ path = path.to_path if path.respond_to?(:to_path)
51
+ return nil unless path.is_a?(String)
52
+ path = File.expand_path(path)
53
+ path.start_with?(PREFIX) || path == ROOT ? path : nil
54
+ end
55
+
56
+ def file?(path) = (v = vpath(path)) && v != ROOT ? OcranRuntime.entries.key?(v.delete_prefix(PREFIX)) : false
57
+ def directory?(path) = (v = vpath(path)) ? dirs.key?(v) : false
58
+ def exist?(path) = file?(path) || directory?(path)
59
+
60
+ def read(path)
61
+ v = vpath(path)
62
+ raise Errno::EISDIR, path.to_s if v && dirs.key?(v)
63
+ data = v && OcranRuntime.read(v.delete_prefix(PREFIX))
64
+ raise Errno::ENOENT, path.to_s unless data
65
+ data
66
+ end
67
+
68
+ def children(path)
69
+ v = vpath(path)
70
+ raise Errno::ENOENT, path.to_s unless v && dirs.key?(v)
71
+ prefix = "#{v}/"
72
+ paths.filter_map { |p| p.delete_prefix(prefix).split("/").first if p.start_with?(prefix) }.uniq
73
+ end
74
+
75
+ def glob(pattern, flags, base)
76
+ pattern = File.join(File.expand_path(base), pattern) if base
77
+ flags |= File::FNM_PATHNAME | File::FNM_EXTGLOB
78
+ found = paths.select { |p| File.fnmatch?(pattern, p, flags) }
79
+ base ? found.map { |p| p.delete_prefix("#{File.expand_path(base)}/") } : found
80
+ end
81
+
82
+ def stat(path)
83
+ raise Errno::ENOENT, path.to_s unless exist?(path)
84
+ e = OcranRuntime.entries[vpath(path).delete_prefix(PREFIX)]
85
+ Stat.new(e ? e.size : 0, e.nil?)
86
+ end
87
+
88
+ # The subset of File::Stat that libraries ask about.
89
+ Stat = Struct.new(:size, :directory?) do
90
+ def file? = !directory?
91
+ def ftype = directory? ? "directory" : "file"
92
+ def mtime = MTIME
93
+ alias_method :ctime, :mtime
94
+ alias_method :atime, :mtime
95
+ def readable? = true
96
+ def writable? = false
97
+ def executable? = false
98
+ def symlink? = false
99
+ def zero? = size.zero?
100
+ def size? = size.zero? ? nil : size
101
+ def mode = directory? ? 0o40555 : 0o100444
102
+ end
103
+
104
+ def writing?(mode, opts)
105
+ mode = opts[:mode] || mode
106
+ case mode
107
+ when String then mode.match?(/[wa+]/)
108
+ when Integer then mode & (File::WRONLY | File::RDWR | File::CREAT) != 0
109
+ else false
110
+ end
111
+ end
112
+
113
+ def contents(path, mode, opts)
114
+ data = read(path)
115
+ mode = (opts[:mode] || mode).to_s
116
+ enc = opts[:encoding] || opts[:external_encoding] || mode[/:([^:]+)/, 1]
117
+ if opts[:binmode] || mode.include?("b") && enc.nil?
118
+ data.force_encoding(Encoding::BINARY)
119
+ else
120
+ data.force_encoding(enc ? Encoding.find(enc.to_s.sub(/\Abom\|/i, "")) : Encoding.default_external)
121
+ end
122
+ end
123
+
124
+ def open_io(path, mode, opts)
125
+ raise Errno::EROFS, path.to_s if writing?(mode, opts)
126
+
127
+ ArchivedFile.new(contents(path, mode, opts), vpath(path))
128
+ end
129
+ end
130
+
131
+ # ---- require / load -------------------------------------------------
132
+ @lock = Thread::Mutex.new
133
+ @loaded = {}
134
+
135
+ class << self
136
+ attr_reader :config
137
+
138
+ def synchronize(&)
139
+ @lock.owned? ? yield : @lock.synchronize(&)
140
+ end
141
+
142
+ def compile(path)
143
+ src = VirtualFS.read(path).force_encoding(Encoding::UTF_8)
144
+ RubyVM::InstructionSequence.compile(src, path, path, 1)
145
+ end
146
+
147
+ def load_file(path, wrap)
148
+ if wrap
149
+ mod = wrap.is_a?(Module) ? wrap : Module.new
150
+ mod.module_eval(VirtualFS.read(path).force_encoding(Encoding::UTF_8), path, 1)
151
+ else
152
+ compile(path).eval
153
+ end
154
+ end
155
+
156
+ # Absolute archive path for +feature+, or nil to let Ruby handle it
157
+ # (files on disk and the statically linked extensions).
158
+ def resolve(feature)
159
+ feature = feature.to_path if feature.respond_to?(:to_path)
160
+ feature = feature.to_str
161
+ ext = File.extname(feature)
162
+ return nil if ext == ".so" || ext == ".dll"
163
+ name = ext == ".rb" ? feature : "#{feature}.rb"
164
+ if File.absolute_path?(feature) || feature.start_with?("./", "../", "~")
165
+ path = File.expand_path(name)
166
+ return VirtualFS.file?(path) ? path : nil
167
+ end
168
+ $LOAD_PATH.each do |dir|
169
+ dir = VirtualFS.vpath(dir) or next
170
+ path = "#{dir}/#{name}"
171
+ return path if VirtualFS.file?(path)
172
+ end
173
+ nil
174
+ end
175
+
176
+ # Load-path-relative names an absolute archive path may be required by:
177
+ # C:/app.exe/lib/ruby/gems/4.0.0/gems/nokogiri-1.19.4/lib/nokogiri/nokogiri
178
+ # is nokogiri/nokogiri.
179
+ def linked_feature_candidates(feature)
180
+ feature = feature.to_path if feature.respond_to?(:to_path)
181
+ return [] unless feature.is_a?(String) && File.absolute_path?(feature)
182
+
183
+ path = VirtualFS.vpath(feature) or return []
184
+ path = path.sub(/\.(so|dll)\z/, "")
185
+ $LOAD_PATH.filter_map do |dir|
186
+ dir = VirtualFS.vpath(dir) or next
187
+ path.delete_prefix("#{dir}/") if path.start_with?("#{dir}/")
188
+ end.uniq
189
+ end
190
+
191
+ def require_virtual(path)
192
+ synchronize do
193
+ return false if @loaded[path] || $LOADED_FEATURES.include?(path)
194
+ # Recorded as loaded *before* evaluation: a circular require then
195
+ # returns false, and an autoload being satisfied by this file sees
196
+ # its feature as provided (MRI's own require uses an internal
197
+ # loading table for that, which Ruby code cannot reach).
198
+ @loaded[path] = true
199
+ $LOADED_FEATURES << path
200
+ begin
201
+ compile(path).eval
202
+ rescue Exception
203
+ @loaded.delete(path)
204
+ $LOADED_FEATURES.delete(path)
205
+ raise
206
+ end
207
+ true
208
+ end
209
+ end
210
+ end
211
+
212
+ # Kernel's loading methods, defined in Kernel itself the way RubyGems
213
+ # defines its require - not prepended: a later redefinition (RubyGems,
214
+ # Zeitwerk) must see these as Kernel's own methods to chain to them, and
215
+ # Ruby does not invalidate a prepended module's cached super target when
216
+ # the method it overrides is redefined.
217
+ RUBY_REQUIRE = ::Kernel.instance_method(:require)
218
+ RUBY_LOAD = ::Kernel.instance_method(:load)
219
+ RUBY_OPEN = ::Kernel.instance_method(:open)
220
+
221
+ def self.install_kernel_methods
222
+ ::Kernel.module_eval do
223
+ # Ruby's require, able to read the archive. RubyGems' Kernel#require
224
+ # ends in gem_original_require and keeps one that already exists.
225
+ def gem_original_require(feature)
226
+ archived = OcranRuntime.resolve(feature)
227
+ return OcranRuntime.require_virtual(archived) if archived
228
+
229
+ OcranRuntime::RUBY_REQUIRE.bind_call(self, feature)
230
+ rescue LoadError => e
231
+ # require_relative of a C extension (nokogiri, tiny_tds) asks for an
232
+ # absolute path inside the archive; it is linked into the runtime
233
+ # under its feature name, relative to the load path.
234
+ OcranRuntime.linked_feature_candidates(feature).each do |linked|
235
+ return OcranRuntime::RUBY_REQUIRE.bind_call(self, linked)
236
+ rescue LoadError
237
+ next
238
+ end
239
+ raise e
240
+ end
241
+
242
+ def require(feature) = gem_original_require(feature)
243
+
244
+ # Ruby's own require_relative resolves against its caller, which
245
+ # here would be this file; it is a require of the absolute path.
246
+ def require_relative(feature)
247
+ base = caller_locations(1, 1).first&.absolute_path
248
+ raise LoadError, "cannot infer basepath" unless base
249
+
250
+ require(File.expand_path(feature, File.dirname(base)))
251
+ end
252
+
253
+ def load(file, wrap = false)
254
+ return OcranRuntime::RUBY_LOAD.bind_call(self, file, wrap) unless VirtualFS.file?(file)
255
+
256
+ OcranRuntime.load_file(VirtualFS.vpath(file), wrap)
257
+ true
258
+ end
259
+
260
+ def open(path, *args, **opts, &block)
261
+ return OcranRuntime::RUBY_OPEN.bind_call(self, path, *args, **opts, &block) unless VirtualFS.vpath(path)
262
+
263
+ File.open(path, *args, **opts, &block)
264
+ end
265
+
266
+ private :gem_original_require, :require, :require_relative, :load, :open
267
+ end
268
+ # Kernel.require and friends, called with an explicit receiver.
269
+ %i[require require_relative load open].each do |name|
270
+ ::Kernel.define_singleton_method(name) { |*args, **kw, &block| Object.new.__send__(name, *args, **kw, &block) }
271
+ end
272
+ end
273
+
274
+ module IOPatch
275
+ def read(path, *args, **opts)
276
+ return super unless VirtualFS.vpath(path)
277
+ data = VirtualFS.contents(path, "r", opts)
278
+ length, offset = args
279
+ offset ? data.byteslice(offset, length || data.bytesize) : length ? data.byteslice(0, length) : data
280
+ end
281
+
282
+ def binread(path, *args)
283
+ return super unless VirtualFS.vpath(path)
284
+ read(path, *args, mode: "rb")
285
+ end
286
+
287
+ def readlines(path, *args, chomp: false, **opts)
288
+ return super unless VirtualFS.vpath(path)
289
+ VirtualFS.contents(path, "r", opts).lines(*args, chomp: chomp)
290
+ end
291
+
292
+ def foreach(path, *args, chomp: false, **opts, &block)
293
+ return super unless VirtualFS.vpath(path)
294
+ lines = VirtualFS.contents(path, "r", opts).each_line(*args, chomp: chomp)
295
+ block ? lines.each(&block) && nil : lines
296
+ end
297
+
298
+ def open(path, *args, **opts)
299
+ return super unless self <= File && VirtualFS.vpath(path)
300
+ io = VirtualFS.open_io(path, args.first, opts)
301
+ return io unless block_given?
302
+ begin
303
+ yield io
304
+ ensure
305
+ io.close
306
+ end
307
+ end
308
+ end
309
+
310
+ module FilePatch
311
+ def new(path, *args, **opts)
312
+ return super unless VirtualFS.vpath(path)
313
+
314
+ VirtualFS.open_io(path, args.first, opts)
315
+ end
316
+
317
+ %i[exist? file? directory?].each do |m|
318
+ define_method(m) { |path| VirtualFS.vpath(path) ? VirtualFS.__send__(m, path) : super(path) }
319
+ end
320
+
321
+ def readable?(path) = VirtualFS.vpath(path) ? VirtualFS.exist?(path) : super
322
+ def symlink?(path) = VirtualFS.vpath(path) ? false : super
323
+ def executable?(path) = VirtualFS.vpath(path) ? false : super
324
+ def size(path) = VirtualFS.vpath(path) ? VirtualFS.stat(path).size : super
325
+ def size?(path) = VirtualFS.vpath(path) ? (VirtualFS.exist?(path) ? VirtualFS.stat(path).size? : nil) : super
326
+ def zero?(path) = VirtualFS.vpath(path) ? VirtualFS.file?(path) && VirtualFS.stat(path).zero? : super
327
+ alias_method :empty?, :zero?
328
+ def mtime(path) = VirtualFS.vpath(path) ? VirtualFS.stat(path).mtime : super
329
+ def ftype(path) = VirtualFS.vpath(path) ? VirtualFS.stat(path).ftype : super
330
+ def stat(path) = VirtualFS.vpath(path) ? VirtualFS.stat(path) : super
331
+ alias_method :lstat, :stat
332
+
333
+ def realpath(path, dir = nil)
334
+ v = VirtualFS.vpath(dir ? File.expand_path(path, dir) : path)
335
+ return super unless v
336
+ raise Errno::ENOENT, path.to_s unless VirtualFS.exist?(v)
337
+ v
338
+ end
339
+
340
+ def realdirpath(path, dir = nil)
341
+ v = VirtualFS.vpath(dir ? File.expand_path(path, dir) : path)
342
+ v || super
343
+ end
344
+ end
345
+
346
+ module DirPatch
347
+ def exist?(path) = VirtualFS.vpath(path) ? VirtualFS.directory?(path) : super
348
+
349
+ def empty?(path)
350
+ VirtualFS.vpath(path) ? VirtualFS.children(path).empty? : super
351
+ end
352
+
353
+ def children(path, **opts) = VirtualFS.vpath(path) ? VirtualFS.children(path) : super
354
+ def entries(path, **opts) = VirtualFS.vpath(path) ? [".", ".."] + VirtualFS.children(path) : super
355
+
356
+ def each_child(path, **opts, &block)
357
+ return super unless VirtualFS.vpath(path)
358
+ block ? VirtualFS.children(path).each(&block) && nil : VirtualFS.children(path).each
359
+ end
360
+
361
+ def foreach(path, **opts, &block)
362
+ return super unless VirtualFS.vpath(path)
363
+ block ? entries(path).each(&block) && nil : entries(path).each
364
+ end
365
+
366
+ def glob(pattern, flags = 0, base: nil, sort: true, &block)
367
+ patterns = Array(pattern).flat_map { |p| p.respond_to?(:to_path) ? p.to_path : p }
368
+ virtual, real = patterns.partition do |p|
369
+ VirtualFS.vpath(base ? File.join(base, p) : p.sub(/[*?\[{].*/m, ""))
370
+ end
371
+ found = virtual.flat_map { |p| VirtualFS.glob(p, flags, base) }
372
+ found += super(real, flags, base: base, sort: sort) unless real.empty?
373
+ block ? found.each(&block) && nil : found
374
+ end
375
+
376
+ def [](*patterns, base: nil, sort: true) = glob(patterns, base: base, sort: sort)
377
+ end
378
+
379
+ # ---- start ------------------------------------------------------------
380
+ # ocran/config.rb, written by ocran (StaticRuntimeBuilder): a Hash literal.
381
+ @config = TOPLEVEL_BINDING.eval(read("ocran/config.rb").force_encoding(Encoding::UTF_8),
382
+ "#{PREFIX}ocran/config.rb", 1).freeze
383
+ in_root = ->(value) { value.gsub("<ROOT>", ROOT) }
384
+ $stderr.puts "ocran: starting #{EXE}" if @config[:debug]
385
+
386
+ $LOAD_PATH.replace(@config.fetch(:load_path).map { |dir| dir.empty? ? ROOT : "#{PREFIX}#{dir}" })
387
+ install_kernel_methods
388
+ require "stringio" # statically linked
389
+
390
+ # What File.open returns for an archive path: its contents in memory,
391
+ # with the File methods that make sense for them. There is no operating
392
+ # system file behind it, so asking for one is an IOError, as for a
393
+ # closed file (logger, for one, probes File.new(io.fileno) this way).
394
+ class ArchivedFile < StringIO
395
+ attr_reader :path
396
+ alias_method :to_path, :path
397
+
398
+ def initialize(data, path)
399
+ super(data)
400
+ @path = path
401
+ end
402
+
403
+ def fileno = raise(IOError, "#{@path} is inside the executable and has no file descriptor")
404
+ alias_method :to_i, :fileno
405
+ def stat = VirtualFS.stat(@path)
406
+ def size = string.bytesize
407
+ def mtime = stat.mtime
408
+ def flock(*) = 0
409
+ def inspect = "#<#{self.class} #{@path}>"
410
+ end
411
+ IO.singleton_class.prepend(IOPatch)
412
+ File.singleton_class.prepend(FilePatch)
413
+ Dir.singleton_class.prepend(DirPatch)
414
+
415
+ @config.fetch(:env).each { |name, value| ENV[name] = in_root.(value) }
416
+
417
+ if @config[:rubygems]
418
+ # What Ruby's gem_prelude.rb does at startup, which the runtime skips
419
+ # (--disable=gems) so that it happens only once the archive is readable.
420
+ require "rubygems"
421
+ %w[error_highlight did_you_mean syntax_suggest/core_ext].each do |feature|
422
+ require feature
423
+ rescue LoadError
424
+ # not packed: the application did not have it loaded at build time
425
+ end
426
+ end
427
+
428
+ Dir.chdir(File.dirname(ENV.fetch("OCRAN_EXECUTABLE"))) if @config[:chdir_exe_dir]
429
+
430
+ # RUBYOPT -I and -r (the runtime never reads RUBYOPT itself).
431
+ $LOAD_PATH.unshift(*@config.fetch(:rubyopt_includes).map(&in_root))
432
+ @config.fetch(:rubyopt_requires).each { |feature| require in_root.(feature) }
433
+
434
+ main = "#{PREFIX}#{@config.fetch(:main)}"
435
+ $0 = main
436
+ src = VirtualFS.read(main).force_encoding(Encoding::UTF_8)
437
+ if (i = src.index(/^__END__\r?\n/))
438
+ Object.const_set(:DATA, StringIO.new(src[src.index("\n", i) + 1..]))
439
+ end
440
+ ARGV.unshift(*@config.fetch(:argv).map(&in_root))
441
+ end
442
+
443
+ OcranRuntime.compile($0).eval
@@ -0,0 +1,86 @@
1
+ # Compiles one C extension (a gem's ext/<name>/ directory) into a static
2
+ # library for an ocran static runtime, the way RubyGems would build it -
3
+ # extconf.rb then make - except that mkmf targets the runtime and produces
4
+ # a static library instead of a DLL.
5
+ #
6
+ # Runs in a process of its own (mkmf is global state), under a Ruby whose
7
+ # RbConfig describes the runtime: for MinGW, any Ruby with the runtime's
8
+ # <arch>-fake.rb loaded (Ruby's own cross-build mechanism); for mswin, the
9
+ # runtime build's own Ruby.
10
+ #
11
+ # ruby --disable=gems -I SDK -r SDK/<arch>-fake build_static_ext.rb \
12
+ # INCLUDE_DIR ARCH_INCLUDE_DIR EXT_SRC_DIR OUT_DIR [extconf args...]
13
+ #
14
+ # INCLUDE_DIR holds ruby.h, ARCH_INCLUDE_DIR ruby/config.h. The compiler is
15
+ # RbConfig's, or OCRAN_RUNTIME_CC (with the matching ar and ranlib) when the
16
+ # build host names it differently; OCRAN_RUNTIME_LIBS are appended to $LIBS.
17
+ # Prints
18
+ # <feature> <Init function> <archive> <comma-separated libs>
19
+ # on success, for the runtime link (Ocran::RuntimeSdk#link).
20
+ require "rbconfig"
21
+ require "fileutils"
22
+
23
+ include_dir, arch_include_dir, ext_src, out, *extconf_args = ARGV
24
+ abort "usage: see #{__FILE__}" unless out
25
+
26
+ {
27
+ "rubyhdrdir" => File.expand_path(include_dir),
28
+ "rubyarchhdrdir" => File.expand_path(arch_include_dir),
29
+ }.each { |k, v| RbConfig.fire_update!(k, v) }
30
+
31
+ if (cc = ENV["OCRAN_RUNTIME_CC"]) && !cc.empty?
32
+ tool = ->(name) { cc.sub(/(?:gcc|cc|clang)(\.exe)?\z/) { "#{name}#{$1}" } }
33
+ { "CC" => cc, "CPP" => "#{cc} -E", "LDSHARED" => "#{cc} -shared", "AR" => tool.("ar"),
34
+ "RANLIB" => tool.("ranlib") }.each { |k, v| RbConfig.fire_update!(k, v) }
35
+ end
36
+
37
+ # RubyInstaller's RubyGems defaults (rubygems/defaults/operating_system.rb),
38
+ # loaded when an extconf.rb requires rubygems (nokogiri's does), set up the
39
+ # build host's Ruby - its DLL directories, MSYS2 - from RbConfig, which
40
+ # describes the target here, and fail. None of that applies to building for
41
+ # the target (MSYS2's gcc is on PATH already), so an empty file of that name
42
+ # comes first on the load path.
43
+ if $LOAD_PATH.any? { |dir| File.file?(File.join(dir, "ruby_installer/runtime.rb")) }
44
+ defaults = File.join(File.dirname(File.expand_path(out)), "ocran-rubygems-defaults")
45
+ FileUtils.mkdir_p(File.join(defaults, "rubygems/defaults"))
46
+ File.write(File.join(defaults, "rubygems/defaults/operating_system.rb"),
47
+ "# Left empty by ocran: see build_static_ext.rb.\n")
48
+ $LOAD_PATH.unshift(defaults)
49
+ end
50
+
51
+ # Built inside a copy of the whole gem, as RubyGems does: extconf.rb files
52
+ # may reach into the gem's lib/ (racc's does).
53
+ ext_src = File.expand_path(ext_src)
54
+ gem_root, sep, ext_rel = ext_src.rpartition("/ext/")
55
+ gem_root, ext_rel = File.dirname(ext_src), File.basename(ext_src) if sep.empty?
56
+ ext_rel = File.join("ext", ext_rel) unless sep.empty?
57
+ FileUtils.rm_rf(out)
58
+ FileUtils.mkdir_p(File.dirname(out))
59
+ FileUtils.cp_r(gem_root, out)
60
+ Dir.chdir(File.join(out, ext_rel))
61
+
62
+ $0 = File.expand_path("extconf.rb") # mkmf takes srcdir from $0
63
+ ARGV.replace(extconf_args)
64
+ require "mkmf"
65
+ $static = true # after require: loading mkmf resets it
66
+ # The static C libraries an extension's own (-lsybdb, -lxml2) need in turn -
67
+ # iconv, OpenSSL, zlib, Windows system libraries - in link order. mkmf puts
68
+ # $LIBS after the libraries extconf.rb finds, both in its link checks and
69
+ # in the Makefile, which is the order a static link needs.
70
+ $LIBS = [$LIBS, ENV["OCRAN_RUNTIME_LIBS"]].compact.join(" ")
71
+ # Defines the static libraries' headers need (LIBXSLT_STATIC, ...).
72
+ $CPPFLAGS = [$CPPFLAGS, ENV["OCRAN_RUNTIME_CPPFLAGS"]].compact.join(" ")
73
+ load "./extconf.rb"
74
+
75
+ mk = File.read("Makefile")
76
+ target = mk[/^TARGET = (\S+)/, 1] or abort "#{ext_src}: extconf.rb produced no TARGET"
77
+ prefix = mk[/^target_prefix = (\S*)/, 1].to_s.delete_prefix("/")
78
+ feature = [prefix, target].reject(&:empty?).join("/")
79
+ libs = mk[/^LIBS = (.*)$/, 1].split.reject { |l| l.include?("$(RUBY_SO_NAME)") || l.include?("$(LIBRUBY") }
80
+ lib = "#{target}.#{RbConfig::CONFIG["LIBEXT"]}"
81
+ if RbConfig::CONFIG["target_os"].start_with?("mswin")
82
+ system("nmake", "-nologo", "STATIC_LIB=#{lib}", lib, exception: true)
83
+ else
84
+ system("make", "-j#{ENV.fetch("JOBS", "4")}", "STATIC_LIB=#{lib}", lib, exception: true)
85
+ end
86
+ puts [feature, "Init_#{target}", File.join(Dir.pwd, lib), libs.join(",")].join(" ")