reflex-packager 0.2.0 → 0.3.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/package.yml +310 -0
  3. data/.github/workflows/release-gem.yml +2 -49
  4. data/.github/workflows/tag.yml +11 -28
  5. data/.github/workflows/test.yml +16 -37
  6. data/.gitignore +2 -0
  7. data/ChangeLog.md +28 -0
  8. data/Gemfile.lock +26 -0
  9. data/README.md +232 -27
  10. data/Rakefile +21 -0
  11. data/VERSION +1 -1
  12. data/bin/reflex +2 -32
  13. data/examples/hello/icon.png +0 -0
  14. data/examples/hello/main.rb +18 -0
  15. data/examples/hello/reflex.yml +3 -0
  16. data/lib/reflex/packager/app_store_connect.rb +162 -0
  17. data/lib/reflex/packager/cli.rb +42 -9
  18. data/lib/reflex/packager/config.rb +233 -36
  19. data/lib/reflex/packager/data_compiler.rb +59 -0
  20. data/lib/reflex/packager/data_file.rb +183 -0
  21. data/lib/reflex/packager/data_loader.rb +127 -0
  22. data/lib/reflex/packager/gems.rb +282 -0
  23. data/lib/reflex/packager/library.rb +120 -0
  24. data/lib/reflex/packager/macos.rb +592 -94
  25. data/lib/reflex/packager/platform.rb +289 -10
  26. data/lib/reflex/packager/profile.rb +38 -26
  27. data/lib/reflex/packager/profiles/reflex.rb +43 -0
  28. data/lib/reflex/packager/rake.rb +115 -0
  29. data/lib/reflex/packager/stores/app_store.rb +60 -0
  30. data/lib/reflex/packager/stores/itchio.rb +53 -0
  31. data/lib/reflex/packager/templates/macos/AppStore.entitlements.erb +8 -0
  32. data/lib/reflex/packager/templates/macos/AppStoreExportOptions.plist.erb +14 -0
  33. data/lib/reflex/packager/templates/macos/ExportOptions.plist.erb +20 -0
  34. data/lib/reflex/packager/templates/macos/InfoPlist.strings.erb +5 -0
  35. data/lib/reflex/packager/templates/macos/boot.rb.erb +29 -0
  36. data/lib/reflex/packager/templates/macos/compile.rb.erb +7 -0
  37. data/lib/reflex/packager/templates/macos/main.mm.erb +10 -7
  38. data/lib/reflex/packager/templates/macos/project.yml.erb +60 -3
  39. data/lib/reflex/packager/templates/windows/app.manifest.erb +25 -0
  40. data/lib/reflex/packager/templates/windows/app.rc.erb +38 -0
  41. data/lib/reflex/packager/templates/windows/boot.rb.erb +32 -0
  42. data/lib/reflex/packager/templates/windows/main.cpp.erb +47 -0
  43. data/lib/reflex/packager/templates/windows/runtime.manifest.erb +7 -0
  44. data/lib/reflex/packager/windows.rb +370 -0
  45. data/lib/reflex/packager.rb +18 -1
  46. data/reflex-packager.gemspec +8 -7
  47. data/test/helper.rb +50 -5
  48. data/test/test_packager_app_store_connect.rb +108 -0
  49. data/test/test_packager_cli.rb +38 -3
  50. data/test/test_packager_config.rb +166 -27
  51. data/test/test_packager_data_file.rb +96 -0
  52. data/test/test_packager_data_loader.rb +114 -0
  53. data/test/test_packager_library.rb +81 -0
  54. data/test/test_packager_macos.rb +714 -86
  55. data/test/test_packager_platform.rb +16 -0
  56. data/test/test_packager_rake.rb +165 -0
  57. data/test/test_packager_stores_app_store.rb +66 -0
  58. data/test/test_packager_stores_itchio.rb +38 -0
  59. data/test/test_packager_windows.rb +685 -0
  60. metadata +72 -15
  61. data/.github/workflows/utils.rb +0 -127
  62. data/lib/reflex/packager/templates/macos/Podfile.erb +0 -30
@@ -0,0 +1,685 @@
1
+ # -*- coding: utf-8 -*-
2
+ require_relative 'helper'
3
+ require 'reflex/packager/windows'
4
+
5
+
6
+ class TestPackagerWindows < Test::Unit::TestCase
7
+
8
+ RP = Reflex::Packager
9
+ Windows = RP::Windows
10
+
11
+ # Files of the fake libraries, laid out as an installed gem is after a
12
+ # gem build on Windows: the extension and the archives are left in lib/.
13
+ LIBS = {
14
+ 'fakebase' => %w[lib/fakebase.rb lib/libfakebase.a VERSION],
15
+ 'fakenative' => %w[
16
+ lib/fakenative.rb lib/fakenative/sub.rb lib/fakenative/ext.rb
17
+ lib/fakenative_ext.so lib/libfakenative.a lib/libfakenative.dll.a
18
+ ext/fakenative/b.o ext/fakenative/a.o ext/fakenative/Makefile VERSION],
19
+ 'fakepure' => %w[lib/fakepure.rb res/icon.png VERSION]
20
+ }
21
+
22
+ # With reflex, which a packed app needs to carry what reads its data file.
23
+ LIBS_WITH_REFLEX = LIBS.merge('reflex' => %w[lib/reflex.rb VERSION])
24
+
25
+ RBCONFIG = {
26
+ 'CXX' => 'g++ -std=gnu++11',
27
+ 'rubyhdrdir' => '/ruby/include/ruby-4.0.0',
28
+ 'rubyarchhdrdir' => '/ruby/include/ruby-4.0.0/x64-mingw-ucrt',
29
+ 'libdir' => '/ruby/lib',
30
+ 'LIBRUBYARG_SHARED' => '-lx64-ucrt-ruby400'
31
+ }
32
+
33
+ # Makes a directory of the files for each library, as its gem.
34
+ def fake_libs(libs = LIBS, &block)
35
+ Dir.mktmpdir do |root|
36
+ libs.each do |name, files|
37
+ files.each do |file|
38
+ path = File.join root, name, file
39
+ FileUtils.mkdir_p File.dirname(path)
40
+ File.write path, "# #{file}\n"
41
+ end
42
+ end
43
+ @fake_root = root
44
+ block.call root
45
+ ensure
46
+ @fake_root = nil
47
+ end
48
+ end
49
+
50
+ module FakeExtension
51
+ module_function
52
+ def name() = 'Fake'
53
+ def version() = '1.0'
54
+ end
55
+
56
+ # A profile of the fake libraries, which the gemspecs of real gems would
57
+ # tell otherwise.
58
+ def profile(libraries: LIBS.keys, boot: nil)
59
+ libs = libraries.map {RP::Library.new _1, File.join(@fake_root.to_s, _1)}
60
+ RP::Profile.new(
61
+ extension: FakeExtension,
62
+ config_files: %w[reflex.yml],
63
+ templates: {'main.rb': ''},
64
+ boot: boot
65
+ ).tap {|profile| profile.define_singleton_method(:libraries) {libs}}
66
+ end
67
+
68
+ def packager(
69
+ yaml = nil, profile: self.profile, files: {'main.rb' => ''}, standard_gems: {}, pack: false,
70
+ &block)
71
+
72
+ Dir.mktmpdir do |dir|
73
+ files.each do |file, content|
74
+ path = File.join dir, file
75
+ FileUtils.mkdir_p File.dirname(path)
76
+ File.write path, content
77
+ end
78
+ File.write File.join(dir, 'reflex.yml'), yaml if yaml
79
+ pkg = Windows.new RP::Config.load(profile, dir), pack: pack
80
+ # keep the tests off the gems of the Ruby running them
81
+ pkg.define_singleton_method(:standard_gem_dirs) {standard_gems}
82
+ block.call pkg, dir
83
+ end
84
+ end
85
+
86
+ def build_path(dir, path = '')
87
+ File.join dir, '.build', 'windows', path
88
+ end
89
+
90
+ def read(dir, path)
91
+ File.read build_path(dir, path)
92
+ end
93
+
94
+ # --- generate ----------------------------------------------------------
95
+
96
+ def test_generate_creates_files()
97
+ fake_libs do
98
+ packager do |pkg, dir|
99
+ pkg.generate
100
+ %w[
101
+ src/main.cpp src/app.manifest src/app.rc lib/boot.rb lib/app/main.rb
102
+ lib/fakebase/lib/fakebase.rb
103
+ lib/fakenative/lib/fakenative.rb lib/fakenative/lib/fakenative/sub.rb
104
+ lib/fakepure/lib/fakepure.rb
105
+ ].each do |path|
106
+ assert File.exist?(build_path dir, path), "missing #{path}"
107
+ end
108
+
109
+ error = assert_raise(RP::Error) {Windows.new(pkg.config, notarize: %i[submit]).package}
110
+ assert_include error.message, 'only for macos'
111
+ error = assert_raise(RP::Error) {Windows.new(pkg.config, appstore: true).package}
112
+ assert_include error.message, 'only for macos'
113
+ end
114
+ end
115
+ end
116
+
117
+ def test_libs_leave_out_binaries()
118
+ fake_libs do
119
+ packager "files: [native.so]", files: {'main.rb' => '', 'native.so' => ''} do |pkg, dir|
120
+ pkg.generate
121
+ # a rays_ext.so on the load path would win over the one linked in
122
+ lib = build_path dir, 'lib'
123
+ files = pkg.load_dirs.flat_map {Dir.glob "#{_1}/**/*", base: lib}
124
+ assert_empty files.grep(/\.(so|a|o)\z/)
125
+ assert_not_include files, 'fakenative/ext'
126
+ # the app itself ships what it likes
127
+ assert File.exist?(File.join lib, 'app/native.so')
128
+ end
129
+ end
130
+ end
131
+
132
+ def test_libs_carry_version_and_res()
133
+ fake_libs do
134
+ packager do |pkg, dir|
135
+ pkg.generate
136
+ # Extension.version reads VERSION and reight finds res/ from lib/
137
+ assert File.exist?(build_path dir, 'lib/fakenative/VERSION')
138
+ assert File.exist?(build_path dir, 'lib/fakepure/res/icon.png')
139
+ end
140
+ end
141
+ end
142
+
143
+ def test_libs_carry_standard_gems()
144
+ fake_libs do |root|
145
+ gems = {'fakegem' => [File.join(root, 'fakepure', 'lib')]}
146
+ packager standard_gems: gems do |pkg, dir|
147
+ pkg.generate
148
+ assert File.exist?(build_path dir, 'lib/gems/fakegem/lib/fakepure.rb')
149
+ assert_equal %w[fakepure fakenative fakebase fakegem], pkg.lib_names
150
+ assert_equal %w[fakepure fakenative fakebase gems/fakegem], pkg.load_dirs
151
+
152
+ # the ones this ruby has, looked for out of any bundle, with what they
153
+ # depend on: rexml has been a bundled gem since ruby 3.0, and rss
154
+ # depends on it
155
+ names = pkg.__send__(:standard_specs).map {_1['name']}
156
+ assert_include names, 'rexml'
157
+ assert_equal 1, names.count('rexml')
158
+ # but none of the tools for development
159
+ assert_empty names & Windows::DEVELOPMENT_GEMS
160
+ end
161
+ end
162
+ end
163
+
164
+ def test_libs_carry_the_gems_of_the_gemfile()
165
+ # resolved by bundler: the default group, with what it depends on
166
+ gemfile = <<~RUBY
167
+ source 'https://rubygems.org'
168
+ gem 'test-unit'
169
+ group :test do
170
+ gem 'rake'
171
+ end
172
+ RUBY
173
+ # bundler/setup, which apps with a Gemfile often require, does nothing
174
+ main = <<~RUBY
175
+ require 'bundler/setup'
176
+ require 'test/unit'
177
+ File.write '../result', $LOADED_FEATURES.grep(%r{bundler/setup}).join
178
+ RUBY
179
+ fake_libs do
180
+ packager files: {'main.rb' => main, 'Gemfile' => gemfile} do |pkg, dir|
181
+ pkg.generate
182
+ assert File.exist?(build_path dir, 'lib/gems/test-unit/lib/test/unit.rb')
183
+ assert File.exist?(build_path dir, 'lib/gems/power_assert/lib/power_assert.rb')
184
+ assert !File.exist?(build_path dir, 'lib/gems/rake')
185
+ assert_include pkg.lib_names, 'test-unit'
186
+ assert_equal 'bundler', pkg.lib_names[pkg.library_names.size]
187
+
188
+ # out of the bundle the tests may run under
189
+ env = ENV.keys.grep(/\ABUNDLER?_/).to_h {[_1, nil]}.merge 'RUBYOPT' => nil
190
+ assert system(env, RbConfig.ruby, build_path(dir, 'lib/boot.rb'))
191
+ assert_equal File.realpath(build_path dir, 'lib/gems/bundler/lib/bundler/setup.rb'),
192
+ File.realpath(read dir, 'lib/result')
193
+ end
194
+ end
195
+
196
+ # no bundler/setup of its own for an app with no Gemfile
197
+ fake_libs do
198
+ packager do |pkg, dir|
199
+ pkg.generate
200
+ assert !File.exist?(build_path dir, 'lib/gems/bundler')
201
+ assert_not_include pkg.lib_names, 'bundler'
202
+ end
203
+ end
204
+
205
+ # what is left out, and what goes as it is
206
+ fake_libs do |root|
207
+ Dir.mktmpdir do |gems|
208
+ files = %w[
209
+ native/lib/native.rb ext/native/native.so
210
+ rexml/lib/rexml.rb fakebase/lib/fakebase/extension.rb
211
+ ]
212
+ files.each do |file|
213
+ path = File.join gems, file
214
+ FileUtils.mkdir_p File.dirname(path)
215
+ File.write path, ''
216
+ end
217
+ spec = -> (name, *paths, default: false) {
218
+ {
219
+ 'name' => name,
220
+ 'root' => File.join(gems, name),
221
+ 'default_gem' => default,
222
+ 'require_paths' => paths.map {File.join gems, _1}
223
+ }
224
+ }
225
+ specs = [
226
+ spec['native', 'native/lib', 'ext/native'],
227
+ spec['rexml', 'rexml/lib'],
228
+ spec['fakebase', 'fakebase/lib'],# a library, as it has extension.rb
229
+ spec['bundler', 'bundler/lib'],
230
+ spec['json', 'json/lib', default: true]
231
+ ]
232
+ standard = {'rexml' => ['standard/rexml/lib'], 'ostruct' => [File.join(root, 'fakepure/lib')]}
233
+ packager standard_gems: standard do |pkg, dir|
234
+ pkg.define_singleton_method(:gemfile_specs) {specs}
235
+ pkg.generate
236
+ assert_equal %w[native rexml ostruct], pkg.gem_dirs.keys
237
+ assert_equal [File.join(gems, 'rexml/lib')], pkg.gem_dirs['rexml']
238
+ # an extension comes along, unlike the ones of the libraries
239
+ assert File.exist?(build_path dir, 'lib/gems/native/lib/native.so')
240
+ assert File.exist?(build_path dir, 'lib/gems/native/lib/native.rb')
241
+ end
242
+ # where a native extension is not supported
243
+ packager standard_gems: standard do |pkg, _|
244
+ pkg.define_singleton_method(:gemfile_specs) {specs}
245
+ pkg.define_singleton_method(:native_gems?) {false}
246
+ error = assert_raise(RP::Error) {pkg.gem_dirs}
247
+ assert_include error.message, "'native'"
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ def test_main_cpp_manifest_and_rc()
254
+ fake_libs do
255
+ packager "version: 1.2.3.4.5\ncopyright: © 2026 Me\nlocalizations: {ja: {name: アプリ}}" do |pkg, dir|
256
+ stub(RP::Windows, :lcid, 0x0411) {pkg.generate}
257
+ str = read dir, 'src/main.cpp'
258
+ assert_include str, 'void Init_fakenative_ext ();'
259
+ assert_include str, 'ruby_init_ext("fakenative_ext.so", Init_fakenative_ext);'
260
+ assert_include str, 'L"lib\\\\boot.rb"'
261
+
262
+ str = read dir, 'src/app.manifest'
263
+ assert_include str, %(name="#{pkg.target}" version="1.2.3.0")
264
+ assert_include str, '<dependentAssembly>'
265
+ assert_include str, %(name="lib" version="1.0.0.0")
266
+ assert_include str, '<supportedOS '
267
+ assert_include str, '<activeCodePage '
268
+ assert_include str, '>UTF-8<'
269
+
270
+ rc = read dir, 'src/app.rc'
271
+ assert_include rc, %(1 24 "app.manifest")
272
+ assert_not_include rc, 'ICON'
273
+ assert !File.exist?(build_path dir, 'src/app.ico')
274
+ assert_include rc, 'FILEVERSION 1,2,3,4'
275
+ assert_include rc, 'PRODUCTVERSION 1,2,3,0'
276
+ assert_include rc, %(VALUE "FileVersion", "1.2.3.4.5")
277
+ assert_include rc, %(VALUE "ProductVersion", "1.2.3")
278
+ assert_include rc, %(VALUE "OriginalFilename", "#{pkg.target}.exe")
279
+ assert_include rc, %(VALUE "LegalCopyright", "© 2026 Me")
280
+ assert_include rc, %(BLOCK "041104B0")
281
+ assert_include rc, %(VALUE "ProductName", "アプリ")
282
+ assert_include rc, %(VALUE "Translation", 0x409, 1200, 0x411, 1200)
283
+ assert_equal 2, rc.scan(%(VALUE "LegalCopyright", "© 2026 Me")).size # in both of the languages
284
+ end
285
+ packager %(name: 'ア"プ\\リ'\nbundle_id: com.example.app) do |pkg, dir|
286
+ pkg.generate
287
+ rc = read dir, 'src/app.rc'
288
+ assert_include rc, %(VALUE "FileDescription", "ア""プ\\\\リ")
289
+ assert_include rc, %(VALUE "ProductName", "ア""プ\\\\リ")
290
+ end
291
+ packager "version: '2.3'" do |pkg, dir|
292
+ pkg.generate
293
+ rc = read dir, 'src/app.rc'
294
+ assert_include rc, 'FILEVERSION 2,3,0,0'
295
+ assert_include rc, 'PRODUCTVERSION 2,3,0,0'
296
+ assert_include rc, %(VALUE "FileVersion", "2.3")
297
+ assert_include rc, %(VALUE "ProductVersion", "2.3")
298
+ assert_not_include rc, 'LegalCopyright'
299
+ assert_include rc, %(BLOCK "040904B0")
300
+ assert_include rc, %(VALUE "Translation", 0x409, 1200)
301
+ end
302
+ packager "version: 1.2.65536" do |pkg, _|
303
+ assert_raise(RP::Error) {pkg.file_version}
304
+ end
305
+ packager do |pkg, _|
306
+ assert_equal 0x0411, pkg.langid('ja')
307
+ assert_equal 0x0804, pkg.langid('zh-Hans')
308
+ assert_raise(RP::Error) {pkg.langid 'x-y'}
309
+ end if Gem.win_platform? # asks windows
310
+ assert_equal '1.2.3.4', RP::Windows.four_numbers('1.2.3.4.65536') # the fifth is not used
311
+ packager do |pkg, _|
312
+ # templates see the packager and what render is given, nothing else
313
+ assert_empty pkg.__send__(:template_binding).local_variables
314
+ end
315
+
316
+ # the icon: drawn by rays at each size, so fake the pngs here
317
+ pngs = [[16, "\x89PNG16".b], [256, "éPNG256"]]
318
+ packager "icon: icon.png", files: {'main.rb' => '', 'icon.png' => ''} do |pkg, dir|
319
+ pkg.define_singleton_method(:icon_pngs) {pngs}
320
+ pkg.generate
321
+ assert_include read(dir, 'src/app.rc'), %(1 ICON "app.ico")
322
+
323
+ ico = File.binread build_path(dir, 'src/app.ico')
324
+ a, b = pngs.map {_1.last.bytesize}
325
+ assert_equal [0, 1, 2], ico[0, 6].unpack('v3')
326
+ assert_equal [16, 16, 0, 0, 1, 32, a, 38], ico[6, 16].unpack('C4v2V2')
327
+ assert_equal [0, 0, 0, 0, 1, 32, b, 38 + a], ico[22, 16].unpack('C4v2V2')
328
+ assert_equal pngs.map {_1.last.b}.join, ico[38..]
329
+ end
330
+ end
331
+ end
332
+
333
+ def test_boot_rb_starts_the_app()
334
+ main = <<~RUBY
335
+ require 'fakepure'
336
+ File.write '../result', [Dir.pwd, *$LOAD_PATH.first(4)].join("\\n")
337
+ RUBY
338
+ fake_libs do
339
+ packager files: {'main.rb' => main} do |pkg, dir|
340
+ pkg.generate
341
+ assert system(RbConfig.ruby, build_path(dir, 'lib/boot.rb'))
342
+
343
+ pwd, *paths = read(dir, 'lib/result').lines chomp: true
344
+ assert_equal File.realpath(build_path dir, 'lib/app'), File.realpath(pwd)
345
+ assert_equal pwd, paths.first
346
+ assert_equal %w[lib/fakepure/lib lib/fakenative/lib lib/fakebase/lib],
347
+ paths[1..].map {_1.split('/').last(3).join '/'}
348
+ end
349
+ end
350
+ end
351
+
352
+ def test_boot_rb_reads_the_packed_app()
353
+ main = <<~RUBY
354
+ require 'fakepure'
355
+ require_relative 'sub'
356
+ File.write ENV['RESULT'], [__FILE__, $sub, File.read('data.txt'), *Dir.children('.').sort].join("\\n")
357
+ RUBY
358
+ files = {'main.rb' => main, 'sub.rb' => '$sub = :sub', 'data.txt' => 'data'}
359
+ fake_libs LIBS_WITH_REFLEX do
360
+ packager "files: [sub.rb, data.txt]", files: files, pack: true,
361
+ profile: profile(libraries: LIBS_WITH_REFLEX.keys) do |pkg, dir|
362
+
363
+ pkg.generate
364
+ assert_equal %w[data.bin data.txt], Dir.children(build_path dir, 'lib/app').sort # but the scripts as they are
365
+ assert_equal %w[data.txt main.rb sub.rb], Dir.children(File.join dir, '.build/app').sort # as they are
366
+ assert File.exist?(build_path dir, 'lib/reflex/lib/reflex/packager/data_file.rb')
367
+ assert File.exist?(build_path dir, 'lib/reflex/lib/reflex/packager/data_loader.rb')
368
+
369
+ assert system({'RESULT' => build_path(dir, 'lib/result')}, RbConfig.ruby, build_path(dir, 'lib/boot.rb'))
370
+ assert_equal %w[main.rb sub data data.bin data.txt], read(dir, 'lib/result').lines(chomp: true)
371
+ end
372
+ packager profile: profile(libraries: LIBS_WITH_REFLEX.keys) do |pkg, dir|
373
+ pkg.generate
374
+ assert_not_include read(dir, 'lib/boot.rb'), 'DataLoader'
375
+ assert !File.exist?(build_path dir, 'lib/reflex/lib/reflex/packager')
376
+ end
377
+ packager pack: true do |pkg, _| # with no reflex
378
+ assert_raise(RP::Error) {pkg.generate}
379
+ end
380
+ end
381
+ end
382
+
383
+ def test_boot_rb_starts_boot_main()
384
+ fake_libs do
385
+ packager profile: profile(boot: "puts 1\n") do |pkg, dir|
386
+ pkg.generate
387
+ assert_include read(dir, 'lib/boot.rb'), '"__reflex_main__.rb"'
388
+ assert_equal "puts 1\n", read(dir, 'lib/app/__reflex_main__.rb')
389
+ end
390
+ end
391
+ end
392
+
393
+ def test_boot_rb_shows_the_error()
394
+ # app/reflex.rb stands in for reflex, found first on the load path
395
+ reflex = <<~RUBY
396
+ module Reflex
397
+ def self.alert(message, title:) = File.write(ENV['ALERT'], "\#{title}\\n\#{message}")
398
+ end
399
+ RUBY
400
+ boot = -> (main, yaml = '', pack: false) {
401
+ files = {'main.rb' => main, 'reflex.rb' => reflex}
402
+ fake_libs LIBS_WITH_REFLEX do
403
+ packager "name: My App\nfiles: [reflex.rb]\n#{yaml}", files: files, pack: pack,
404
+ profile: profile(libraries: LIBS_WITH_REFLEX.keys) do |pkg, dir|
405
+
406
+ pkg.generate
407
+ env = {'ALERT' => build_path(dir, 'lib/alert')}
408
+ ok = system env, RbConfig.ruby, build_path(dir, 'lib/boot.rb'), err: File::NULL
409
+ alert = File.exist?(build_path dir, 'lib/alert') ? read(dir, 'lib/alert') : nil
410
+ return [ok, $?.exitstatus, alert]
411
+ end
412
+ end
413
+ }
414
+
415
+ ok, status, alert = boot["raise 'boom'"]
416
+ assert_equal [false, 1], [ok, status]
417
+ assert_equal 'My App', alert.lines.first.chomp
418
+ assert_include alert, 'boom (RuntimeError)'
419
+ assert_not_include alert, 'boot.rb'
420
+ assert_match(/^app\/main\.rb:1:/, alert) # from the directory of boot.rb
421
+
422
+ ok, status, alert = boot.call "raise 'boom'", pack: true
423
+ assert_equal [false, 1], [ok, status]
424
+ assert_include alert, 'boom (RuntimeError)'
425
+ assert_not_include alert, 'data_loader.rb'
426
+ assert_match(/^main\.rb:1:/, alert) # compiled with the paths in the app directory
427
+
428
+ assert_equal [false, 3, nil], boot['exit 3']
429
+ assert_equal [true, 0, nil], boot['']
430
+ assert_equal [false, 1, nil], boot["raise 'boom'", 'windows: {console: true}']
431
+ end
432
+
433
+ def test_libraries_of_the_gemfile()
434
+ fake_libs do |root|
435
+ # a library the app lists in its Gemfile, as rays-video
436
+ video = File.join root, 'fakevideo'
437
+ %w[lib/fakevideo/extension.rb lib/fakevideo/ext.rb lib/libfakevideo.a ext/fakevideo/a.o]
438
+ .each {FileUtils.mkdir_p File.dirname(File.join video, _1); FileUtils.touch File.join(video, _1)}
439
+ File.write File.join(video, 'fakevideo.gemspec'),
440
+ "Gem::Specification.new {|s| s.name = 'fakevideo'; s.version = '1.0'}"
441
+ specs = [{
442
+ 'name' => 'fakevideo',
443
+ 'root' => video,
444
+ 'default_gem' => false,
445
+ 'require_paths' => [File.join(video, 'lib')]
446
+ }]
447
+ packager do |pkg, _|
448
+ stub pkg, :gemfile_specs, specs do
449
+ assert_equal %w[fakebase fakenative fakepure fakevideo], pkg.libraries.map(&:name)
450
+ assert_equal %w[fakenative_ext fakevideo_ext], pkg.extensions
451
+ assert_empty pkg.gem_dirs # linked as a library, not shipped as a gem
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ # --- link --------------------------------------------------------------
458
+
459
+ def test_native_libraries()
460
+ fake_libs do
461
+ packager do |pkg, _|
462
+ assert_equal %w[fakebase fakenative], pkg.native_libraries.map(&:name)
463
+ end
464
+ end
465
+ end
466
+
467
+ def test_link_command()
468
+ fake_libs do |root|
469
+ packager do |pkg, _|
470
+ cmd = pkg.link_command RBCONFIG
471
+ assert_equal %w[g++ -std=gnu++11 src/main.cpp src/app.res.o -o], cmd.first(5)
472
+ assert_equal "#{pkg.target}.exe", cmd[5]
473
+
474
+ objs = %w[a.o b.o].map {File.join root, 'fakenative/ext/fakenative', _1}
475
+ assert_equal objs, cmd.grep(/\.o\z/) - %w[src/app.res.o]
476
+
477
+ # every object of the archives, the ones depending on others first
478
+ from, to = cmd.index('-Wl,--whole-archive'), cmd.index('-Wl,--no-whole-archive')
479
+ assert_equal %w[fakenative/lib/libfakenative.a fakebase/lib/libfakebase.a]
480
+ .map {File.join root, _1}, cmd[(from + 1)...to]
481
+ assert_operator cmd.index(objs.last), :<, from
482
+
483
+ assert_include cmd, '-lx64-ucrt-ruby400'
484
+ # the c++ runtime is shipped as dlls, not linked in
485
+ assert_empty cmd.grep(/static|winpthread/)
486
+ assert_equal '-mwindows', cmd.last
487
+ end
488
+ end
489
+ end
490
+
491
+ def test_link_command_with_console()
492
+ fake_libs do
493
+ packager "windows: {console: true}" do |pkg, _|
494
+ assert_not_include pkg.link_command(RBCONFIG), '-mwindows'
495
+ end
496
+ end
497
+ end
498
+
499
+ def test_ext_objects_missing()
500
+ libs = LIBS.merge 'fakenative' => %w[lib/fakenative.rb lib/fakenative/ext.rb lib/libfakenative.a]
501
+ fake_libs libs do
502
+ packager do |pkg, _|
503
+ error = assert_raise(RP::Error) {pkg.ext_objects}
504
+ assert_include error.message, 'fakenative_ext'
505
+ end
506
+ end
507
+ end
508
+
509
+ def test_system_libs()
510
+ makefile = <<~MAKEFILE
511
+ LOCAL_LIBS = -lrays.dll -lrucy -lxot -lstdc++
512
+ LIBS = $(LIBRUBYARG_SHARED) -lglew32 -lopengl32 -lgdi32 -lshell32 -lws2_32
513
+ DLDFLAGS = -L. -Wl,--out-implib=libfakenative.dll.a
514
+ MAKEFILE
515
+ assert_equal %w[glew32 opengl32 gdi32 shell32 ws2_32], Windows.makefile_libs(makefile)
516
+ assert_equal [], Windows.makefile_libs('')
517
+
518
+ # from the Makefiles the gem builds of the native libraries leave
519
+ fake_libs do |root|
520
+ File.write File.join(root, 'fakenative/ext/fakenative/Makefile'), makefile
521
+ packager do |pkg, _|
522
+ assert_equal %w[glew32 opengl32 gdi32 shell32 ws2_32], pkg.system_libs
523
+ end
524
+ end
525
+ end
526
+
527
+ def test_compiler_and_tools()
528
+ packager profile: profile(libraries: []) do |pkg, _|
529
+ assert_equal %w[g++ -std=gnu++11], pkg.compiler(RBCONFIG)
530
+ assert_equal %w[g++], pkg.compiler('CXX' => 'g++')
531
+
532
+ with_env 'PATH' => '' do
533
+ error = assert_raise(RP::Error) {pkg.__send__ :check_tools, pkg.tools}
534
+ assert_include error.message, 'windres'
535
+ assert_include error.message, 'objdump'
536
+ assert_include error.message, Windows::TOOLCHAIN_HINT
537
+ end
538
+ end
539
+ end
540
+
541
+ # --- runtime -----------------------------------------------------------
542
+
543
+ def with_ruby(&block)
544
+ Dir.mktmpdir do |ruby|
545
+ %w[
546
+ bin/x64-ucrt-ruby400.dll
547
+ bin/ruby_builtin_dlls/libgmp-10.dll
548
+ bin/ruby_builtin_dlls/libwinpthread-1.dll
549
+ bin/ruby_builtin_dlls/ruby_builtin_dlls.manifest
550
+ lib/ruby/4.0.0/json.rb
551
+ lib/ruby/4.0.0/x64-mingw-ucrt/json/ext/parser.so
552
+ msys64/ucrt64/bin/glew32.dll
553
+ msys64/ucrt64/bin/libstdc++-6.dll
554
+ msys64/ucrt64/bin/libgcc_s_seh-1.dll
555
+ msys64/ucrt64/bin/libwinpthread-1.dll
556
+ windows/System32/KERNEL32.dll
557
+ windows/System32/OPENGL32.dll
558
+ ].each do |path|
559
+ path = File.join ruby, path
560
+ FileUtils.mkdir_p File.dirname(path)
561
+ File.write path, path
562
+ end
563
+ # the compiler, which the dlls of the toolchain are beside
564
+ # with .exe on windows, which takes nothing else as executable
565
+ compiler = File.join ruby, "msys64/ucrt64/bin/fake-g++#{'.exe' if Gem.win_platform?}"
566
+ File.write compiler, ''
567
+ File.chmod 0755, compiler
568
+ rbconfig = {
569
+ 'CXX' => 'fake-g++ -std=gnu++11',
570
+ 'bindir' => File.join(ruby, 'bin'),
571
+ 'LIBRUBY_SO' => 'x64-ucrt-ruby400.dll',
572
+ 'rubylibprefix' => File.join(ruby, 'lib/ruby'),
573
+ 'ruby_version' => '4.0.0'
574
+ }
575
+ with_env 'SystemRoot' => File.join(ruby, 'windows') do
576
+ block.call rbconfig, File.join(ruby, 'msys64/ucrt64/bin')
577
+ end
578
+ end
579
+ end
580
+
581
+ # What objdump would find in the import tables, by file name.
582
+ IMPORTS = {
583
+ 'exe' => %w[
584
+ x64-ucrt-ruby400.dll glew32.dll libstdc++-6.dll KERNEL32.dll
585
+ api-ms-win-crt-heap-l1-1-0.dll],
586
+ 'glew32.dll' => %w[OPENGL32.dll KERNEL32.dll],
587
+ 'libstdc++-6.dll' => %w[libgcc_s_seh-1.dll libwinpthread-1.dll KERNEL32.dll],
588
+ 'libgcc_s_seh-1.dll' => %w[libwinpthread-1.dll],
589
+ 'libwinpthread-1.dll' => %w[KERNEL32.dll]
590
+ }
591
+
592
+ def fake_imports(pkg, imports = IMPORTS)
593
+ pkg.define_singleton_method(:dll_imports) do |path|
594
+ imports[path.end_with?('.exe') ? 'exe' : File.basename(path)] || []
595
+ end
596
+ end
597
+
598
+ def test_dist_paths()
599
+ packager "name: My App\nversion: 1.2.3" do |pkg, dir|
600
+ dist = File.join dir, 'dist/generic'
601
+ assert_equal "#{dist}/MyApp", pkg.dist_path
602
+ assert_match %r{^#{dist}/MyApp-1\.2\.3-windows-(x64|arm64)\.zip$}, pkg.archive_path
603
+ assert_equal 'windows-x64', pkg.archive_platform('host_cpu' => 'x86_64')
604
+ assert_equal 'windows-arm64', pkg.archive_platform('host_cpu' => 'aarch64')
605
+
606
+ with_env 'SystemRoot' => 'C:\Windows' do
607
+ assert_equal [
608
+ 'C:\Windows/System32/tar.exe', '-a', '-c', '-f', pkg.archive_path, '-C', dist, 'MyApp'
609
+ ], pkg.archive_command
610
+ end
611
+ end
612
+ end
613
+
614
+ def test_copy_runtime()
615
+ with_ruby do |rbconfig, msys_bin|
616
+ packager profile: profile(libraries: []) do |pkg, dir|
617
+ fake_imports pkg
618
+ # another toolchain earlier in PATH, as the one of git
619
+ other = File.join dir, 'other/bin'
620
+ FileUtils.mkdir_p other
621
+ File.write File.join(other, 'libstdc++-6.dll'), 'other'
622
+ with_env 'PATH' => [other, msys_bin].join(File::PATH_SEPARATOR) do
623
+ pkg.copy_runtime dir, rbconfig
624
+ end
625
+ # the one beside the compiler
626
+ assert_equal File.join(msys_bin, 'libstdc++-6.dll'),
627
+ File.read(File.join dir, 'lib/libstdc++-6.dll')
628
+
629
+ # a compiler given by its path, wherever PATH points
630
+ abs = rbconfig.merge 'CXX' => File.join(msys_bin, 'fake-g++')
631
+ with_env 'PATH' => other do
632
+ assert_equal msys_bin, pkg.__send__(:toolchain_dir, abs)
633
+ end
634
+
635
+ %w[
636
+ lib/x64-ucrt-ruby400.dll
637
+ lib/ruby_builtin_dlls/ruby_builtin_dlls.manifest
638
+ lib/ruby_builtin_dlls/libgmp-10.dll
639
+ lib/glew32.dll
640
+ lib/lib.manifest
641
+ lib/ruby/4.0.0/json.rb
642
+ lib/ruby/4.0.0/x64-mingw-ucrt/json/ext/parser.so
643
+ ].each do |path|
644
+ assert File.exist?(File.join dir, path), "missing #{path}"
645
+ end
646
+ # the ruby dll resolves them through the manifest in the directory
647
+ assert !File.exist?(File.join dir, 'lib/libgmp-10.dll')
648
+
649
+ manifest = File.read File.join(dir, 'lib/lib.manifest')
650
+ assert_include manifest, %(name="lib" version="1.0.0.0")
651
+ # the ones of the toolchain the executable loads, directly or through
652
+ # another one, but none of windows
653
+ assert_equal %w[
654
+ x64-ucrt-ruby400.dll
655
+ glew32.dll libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll
656
+ ], manifest.scan(/<file name="(.+?)"/).flatten
657
+ # the one of the toolchain for the executable, besides the one of ruby
658
+ assert File.exist?(File.join dir, 'lib/libwinpthread-1.dll')
659
+ assert File.exist?(File.join dir, 'lib/ruby_builtin_dlls/libwinpthread-1.dll')
660
+ end
661
+ end
662
+ end
663
+
664
+ def test_copy_runtime_without_system_dll()
665
+ with_ruby do |rbconfig, msys_bin|
666
+ packager profile: profile(libraries: []) do |pkg, dir|
667
+ # the toolchain without openal installed
668
+ fake_imports pkg, IMPORTS.merge('exe' => [*IMPORTS['exe'], 'libopenal-1.dll'])
669
+ with_env 'PATH' => msys_bin do
670
+ error = assert_raise(RP::Error) {pkg.copy_runtime dir, rbconfig}
671
+ assert_include error.message, "'libopenal-1.dll' needed by #{pkg.target}.exe"
672
+ end
673
+ end
674
+ # one a dll of the toolchain needs
675
+ packager profile: profile(libraries: []) do |pkg, dir|
676
+ fake_imports pkg, IMPORTS.merge('libgcc_s_seh-1.dll' => %w[libnone-1.dll])
677
+ with_env 'PATH' => msys_bin do
678
+ error = assert_raise(RP::Error) {pkg.copy_runtime dir, rbconfig}
679
+ assert_include error.message, "'libnone-1.dll' needed by libgcc_s_seh-1.dll"
680
+ end
681
+ end
682
+ end
683
+ end
684
+
685
+ end# TestPackagerWindows