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
@@ -15,23 +15,17 @@ class TestPackagerMacOS < Test::Unit::TestCase
15
15
  FileUtils.touch path
16
16
  end
17
17
  File.write File.join(dir, 'reflex.yml'), yaml if yaml
18
- with_env({'REFLEX_PODS_PATH' => nil}.merge(env)) do
19
- config = RP::Config.load TEST_PROFILE, dir
20
- block.call MacOS.new(config), dir
18
+ Dir.mktmpdir do |cruby|
19
+ with_env({'CRUBY_PATH' => fake_cruby(cruby), **ENV_TO_SIGN}.merge(env)) do
20
+ config = RP::Config.load TEST_PROFILE, dir
21
+ block.call MacOS.new(config), dir
22
+ end
21
23
  end
22
24
  end
23
25
  end
24
26
 
25
- def with_env(env, &block)
26
- saved = env.map {|key, _| [key, ENV[key]]}
27
- env.each {|key, value| value ? ENV[key] = value : ENV.delete(key)}
28
- block.call
29
- ensure
30
- saved.each {|key, value| value ? ENV[key] = value : ENV.delete(key)}
31
- end
32
-
33
27
  def read(dir, path)
34
- File.read File.join(dir, 'build', 'macos', path)
28
+ File.read File.join(dir, '.build', 'macos', 'project', path)
35
29
  end
36
30
 
37
31
  # --- generate ----------------------------------------------------------
@@ -39,36 +33,221 @@ class TestPackagerMacOS < Test::Unit::TestCase
39
33
  def test_generate_creates_files()
40
34
  packager do |pkg, dir|
41
35
  pkg.generate
42
- %w[project.yml Podfile src/main.mm app/main.rb].each do |f|
43
- assert File.exist?(File.join dir, 'build/macos', f), "missing #{f}"
36
+ %w[
37
+ project.yml src/main.mm app/main.rb
38
+ Bundles/reflex.bundle/Contents/Resources/lib/reflex.rb
39
+ Bundles/reflex.bundle/Contents/Resources/VERSION
40
+ Bundles/CRuby.bundle/Contents/Resources/lib/ruby/4.0.0/set.rb
41
+ ].each do |f|
42
+ assert File.exist?(File.join dir, '.build/macos/project', f), "missing #{f}"
44
43
  end
44
+ assert_empty Dir.glob('*.bundle/**/lib/**/*.{bundle,so,o,a}',
45
+ base: File.join(dir, '.build/macos/project/Bundles'))
46
+ assert_empty Dir.glob('src/*.lproj', base: File.join(dir, '.build/macos/project')) # in english only
47
+ end
48
+ end
49
+
50
+ # Has the app write its data file as the build does, with the ruby here
51
+ # running the boot script in place of the app built with CRuby.
52
+ def compile_scripts(pkg)
53
+ pkg.define_singleton_method(:xcodebuild) {}
54
+ pkg.define_singleton_method :run do |*, chdir:, env: {}|
55
+ system env, RbConfig.ruby, File.join(chdir, 'boot.rb'), err: File::NULL or
56
+ raise RP::Error, 'failed to compile'
57
+ end
58
+ pkg.__send__ :compile_scripts
59
+ end
60
+
61
+ def test_pack()
62
+ packager "files: [sub.rb, data.txt]", files: %w[main.rb sub.rb data.txt] do |pkg, dir|
63
+ pkg = MacOS.new pkg.config, pack: true
64
+ assert_raise(RP::Error) {pkg.package generate_only: true} # it has to be built
65
+
66
+ pkg.generate
67
+ app = File.join dir, '.build/macos/project/app'
68
+ assert_equal %w[data.txt], Dir.children(app) # the data file is written when built
69
+ loader = File.join dir, '.build/macos/project/Bundles/reflex.bundle/Contents/Resources/lib/reflex/packager'
70
+ assert_equal %w[data_file.rb data_loader.rb], Dir.children(loader).sort
71
+ boot = read dir, 'boot.rb'
72
+ assert_include boot, 'DataLoader.setup'
73
+
74
+ # under bundle exec, whose RUBYOPT the app would fail with
75
+ with_env 'RUBYOPT' => '-rno_such_lib' do
76
+ compile_scripts pkg
77
+ end
78
+ RP::DataFile.open File.join(app, 'data.bin') do |data|
79
+ assert_equal %w[.ruby-version main.rbc sub.rbc], data.names.sort
80
+ end
81
+ assert_equal boot, read(dir, 'boot.rb') # as it was
82
+
83
+ File.write File.join(dir, 'main.rb'), 'def'
84
+ pkg = MacOS.new pkg.config, pack: true
85
+ pkg.generate
86
+ assert_raise(RP::Error) {compile_scripts pkg}
87
+ assert !File.exist?(File.join app, 'data.bin')
88
+ assert_equal boot, read(dir, 'boot.rb')
45
89
  end
46
90
  end
47
91
 
48
92
  def test_app_dir_includes_files_and_excludes_build()
49
93
  packager "files: [data]", files: %w[main.rb data/x.png] do |pkg, dir|
50
94
  pkg.generate
51
- pkg.generate # regenerating must not nest a previous build/ into app/
52
- app = File.join dir, 'build/macos/app'
95
+ pkg.generate # regenerating must not nest a previous .build/ into app/
96
+ app = File.join dir, '.build/macos/project/app'
53
97
  assert File.exist?(File.join app, 'data/x.png')
54
- assert !File.exist?(File.join app, 'build')
98
+ assert !File.exist?(File.join app, '.build')
99
+ end
100
+
101
+ # a glob has a directory and what is in it, which is not copied twice
102
+ files = %w[main.rb lib/a/x.rb lib/a/b/y.yml]
103
+ packager "files: ['lib/**/*']", files: files do |pkg, dir|
104
+ pkg.generate
105
+ assert_equal %w[lib/a lib/a/b lib/a/b/y.yml lib/a/x.rb],
106
+ Dir.glob('lib/**/*', base: File.join(dir, '.build/macos/project/app')).sort
55
107
  end
56
108
  end
57
109
 
58
110
  def test_project_yml()
59
- packager "name: My App\nbundle_id: com.example.myapp" do |pkg, dir|
111
+ yaml = <<~YML
112
+ name: My App
113
+ bundle_id: com.example.myapp
114
+ version: 1.2.3.4
115
+ copyright: © 2026 Me
116
+ localizations:
117
+ ja: {name: 'マイ"アプリ'}
118
+ YML
119
+ packager yaml do |pkg, dir|
120
+ FileUtils.mkdir_p File.join(dir, '.build/macos/project/src/fr.lproj') # of a previous build
60
121
  pkg.generate
61
122
  str = read dir, 'project.yml'
62
123
  yml = YAML.safe_load str
63
124
  base = yml.dig 'settings', 'base'
64
125
  assert_equal 'MyApp', yml['name']
65
126
  assert_equal 'com.example.myapp', base['PRODUCT_BUNDLE_IDENTIFIER']
66
- assert_equal '0.1.0', base['MARKETING_VERSION']
67
- assert_equal 'arm64', base['ARCHS']
127
+ assert_equal '1.2.3', base['MARKETING_VERSION']
128
+ assert_equal '1.2.3.4', base['CURRENT_PROJECT_VERSION']
129
+ assert_equal 'arm64 x86_64', base['ARCHS']
68
130
  assert_equal '-', base['CODE_SIGN_IDENTITY']
69
131
  assert_equal '11.0', yml.dig('options', 'deploymentTarget', 'macOS')
70
132
  assert_not_include str, 'CFBundleIconFile'
133
+ assert_include str, 'GCC_WARN_INHIBIT_ALL_WARNINGS: YES'
71
134
  assert_not_include str, 'DEVELOPMENT_TEAM'
135
+ assert_not_include str, 'ENABLE_HARDENED_RUNTIME'
136
+ assert !File.exist?(File.join dir, '.build/macos/generic/ExportOptions.plist')
137
+
138
+ reflex, cruby = pkg.libraries.find {_1.name == 'reflex'}.root, ENV['CRUBY_PATH']
139
+ assert_include base['HEADER_SEARCH_PATHS'], "#{reflex}/include"
140
+ assert_include base['SYSTEM_HEADER_SEARCH_PATHS'], "#{reflex}/vendor/box2d/include"
141
+ assert_include base['SYSTEM_HEADER_SEARCH_PATHS'], "#{cruby}/CRuby/include"
142
+
143
+ target = yml.dig 'targets', 'MyApp'
144
+ sources = target['sources'].to_h {[_1['name'] || _1['path'], _1]}
145
+ assert_equal "#{cruby}/src", sources['CRuby']['path']
146
+ assert_include sources['reflex']['includes'], 'ext/reflex/reflex.cpp'
147
+ assert_empty sources['xot'] ['includes'].grep(%r{^ext/}) # only for its tests
148
+ assert_match(/-DB2_MAX_WORLDS=256 .*-w\z/, sources['reflex-vendor']['compilerFlags'])
149
+ assert_include sources['reflex-vendor']['includes'], 'box2d/src/world.c'
150
+ assert_include sources, 'Bundles/CRuby.bundle'
151
+ assert_include sources, 'Bundles/reflex.bundle'
152
+
153
+ info = target.dig 'info', 'properties'
154
+ assert_equal '1.2.3', info['CFBundleShortVersionString']
155
+ assert_equal '1.2.3.4', info['CFBundleVersion']
156
+ assert_equal '© 2026 Me', info['NSHumanReadableCopyright']
157
+ assert_equal true, info['LSHasLocalizedDisplayName']
158
+ assert_equal false, info['ITSAppUsesNonExemptEncryption']
159
+ assert_not_include info, 'LSApplicationCategoryType'
160
+
161
+ assert_equal <<~'STRINGS', read(dir, 'src/ja.lproj/InfoPlist.strings')
162
+ "CFBundleName" = "マイ\"アプリ";
163
+ "CFBundleDisplayName" = "マイ\"アプリ";
164
+ "NSHumanReadableCopyright" = "© 2026 Me";
165
+ STRINGS
166
+ assert_include read(dir, 'src/en.lproj/InfoPlist.strings'), '"CFBundleName" = "My App";'
167
+ assert !File.exist?(File.join dir, '.build/macos/project/src/fr.lproj')
168
+
169
+ deps = target['dependencies']
170
+ assert_include deps, {'framework' => "#{cruby}/CRuby/CRuby.xcframework", 'embed' => false}
171
+
172
+ # the sources, the macros and the frameworks of the builds of the
173
+ # libraries, which are the ones for the platform they are read on
174
+ if RUBY_PLATFORM.include? 'darwin'
175
+ assert_include sources['reflex']['includes'], 'src/osx/window.mm'
176
+ assert_empty sources['reflex']['includes'].grep(%r{/(win32|sdl|ios)/})
177
+ assert_match(/-DOSX\b.*/, sources['reflex']['compilerFlags'])
178
+ assert_include deps, {'sdk' => 'AppKit.framework'}
179
+ assert_include deps, {'sdk' => 'CoreMIDI.framework'}
180
+ end
181
+ end
182
+ end
183
+
184
+ def test_bundles_carry_the_gems_of_the_gemfile()
185
+ # resolved by bundler: the default group, with what it depends on
186
+ gemfile = <<~RUBY
187
+ source 'https://rubygems.org'
188
+ gem 'test-unit'
189
+ group :test do
190
+ gem 'rake'
191
+ end
192
+ RUBY
193
+ packager files: %w[main.rb Gemfile] do |pkg, dir|
194
+ File.write File.join(dir, 'Gemfile'), gemfile
195
+ pkg.generate
196
+ bundles = File.join dir, '.build/macos/project/Bundles'
197
+ assert File.exist?(File.join bundles, 'test-unit.bundle/Contents/Resources/lib/test/unit.rb')
198
+ assert File.exist?(File.join bundles, 'power_assert.bundle/Contents/Resources/lib/power_assert.rb')
199
+ assert !File.exist?(File.join bundles, 'rake.bundle')
200
+ # bundler/setup, which apps with a Gemfile often require, does nothing
201
+ assert File.exist?(File.join bundles, 'bundler.bundle/Contents/Resources/lib/bundler/setup.rb')
202
+ # CRuby has the standard gems
203
+ assert_equal %w[power_assert test-unit], pkg.gem_dirs.keys.sort
204
+ str = read dir, 'src/main.mm'
205
+ assert_include str, '@"test-unit"'
206
+ assert_include str, '@"bundler"'
207
+ end
208
+
209
+ # where a native extension is not supported
210
+ packager do |pkg, _|
211
+ Dir.mktmpdir do |gems|
212
+ gem_spec = -> name, ext = '.bundle' {
213
+ FileUtils.mkdir_p File.join(gems, name, 'lib')
214
+ FileUtils.touch File.join(gems, name, 'lib', "#{name}#{ext}")
215
+ {
216
+ 'name' => name,
217
+ 'root' => File.join(gems, name),
218
+ 'default_gem' => false,
219
+ 'require_paths' => [File.join(gems, name, 'lib')]
220
+ }
221
+ }
222
+ # CRuby has them in its standard library, as fiddle, which became a
223
+ # bundled gem
224
+ %w[fiddle.rb net/native.rb pure.rb].each do |path|
225
+ path = File.join ENV['CRUBY_PATH'], 'CRuby/lib/ruby/4.0.0', path
226
+ FileUtils.mkdir_p File.dirname(path)
227
+ FileUtils.touch path
228
+ end
229
+
230
+ specs = [gem_spec['fiddle'], gem_spec['net-native'], gem_spec['pure', '.rb']]
231
+ stub pkg, :gemfile_specs, specs do
232
+ # CRuby has them, so the ones with a native extension are left out,
233
+ # but the others are shipped as the Gemfile has them
234
+ assert_equal %w[pure], pkg.app_gem_dirs.keys
235
+ end
236
+
237
+ pkg = MacOS.new pkg.config
238
+ stub pkg, :gemfile_specs, specs + [gem_spec['native']] do
239
+ error = assert_raise(RP::Error) {pkg.gem_dirs}
240
+ assert_include error.message, "'native'"
241
+ end
242
+
243
+ # a CRuby broken, with no standard library
244
+ FileUtils.rm_rf File.join(ENV['CRUBY_PATH'], 'CRuby/lib/ruby/4.0.0')
245
+ pkg = MacOS.new pkg.config
246
+ stub pkg, :gemfile_specs, specs do
247
+ error = assert_raise(RP::Error) {pkg.gem_dirs}
248
+ assert_include error.message, 'no standard library'
249
+ end
250
+ end
72
251
  end
73
252
  end
74
253
 
@@ -76,18 +255,150 @@ class TestPackagerMacOS < Test::Unit::TestCase
76
255
  yaml = <<~YML
77
256
  icon: icon.png
78
257
  macos:
258
+ category: public.app-category.games
79
259
  codesign: {team_id: ABCDE12345}
80
260
  YML
81
261
  packager yaml, files: %w[main.rb icon.png] do |pkg, dir|
82
262
  # render only: a full generate would shell out to sips / iconutil
83
263
  str = pkg.__send__ :render, 'project.yml.erb'
84
- assert_include str, 'CFBundleIconFile: AppIcon'
85
- assert_include str, 'path: AppIcon.icns'
86
- assert_include str, 'DEVELOPMENT_TEAM: ABCDE12345'
264
+ assert_include str, 'CFBundleIconFile: AppIcon'
265
+ assert_include str, 'path: AppIcon.icns'
266
+ assert_include str, 'LSApplicationCategoryType: public.app-category.games'
267
+ assert_include str, 'DEVELOPMENT_TEAM: ABCDE12345'
268
+ assert_include str, 'ENABLE_HARDENED_RUNTIME: YES'
269
+ assert_not_include str, 'NSHumanReadableCopyright'
270
+ assert_not_include str, 'LSHasLocalizedDisplayName'
271
+
272
+ str = MacOS.new(pkg.config, verbose: true).__send__ :render, 'project.yml.erb'
273
+ assert_not_include str, 'GCC_WARN_INHIBIT_ALL_WARNINGS'
274
+ end
275
+ end
276
+
277
+ def test_run()
278
+ stdout = -> &block {
279
+ Tempfile.create do |file|
280
+ saved = $stdout.dup
281
+ $stdout.reopen file
282
+ block.call
283
+ $stdout.flush
284
+ File.read file.path
285
+ ensure
286
+ $stdout.reopen saved
287
+ saved.close
288
+ end
289
+ }
290
+ packager do |pkg, dir|
291
+ cmd = [RbConfig.ruby, '-e', 'puts 1 + 1']
292
+ assert_match (/^2$/), stdout.call {pkg.__send__ :run, *cmd, chdir: dir}
293
+ assert_not_match (/^2$/), stdout.call {pkg.__send__ :run, *cmd, chdir: dir, quiet: true}
294
+ assert_match (/^==> /), stdout.call {pkg.__send__ :run, *cmd, chdir: dir, quiet: true}
295
+
296
+ verbose = MacOS.new pkg.config, verbose: true
297
+ assert_match (/^2$/), stdout.call {verbose.__send__ :run, *cmd, chdir: dir, quiet: true}
298
+ assert_raise(RP::Error) {stdout.call {pkg.__send__ :run, RbConfig.ruby, '-e', 'exit 1', chdir: dir}}
299
+
300
+ script = File.join dir, 'My App'
301
+ File.write script, "#!/bin/sh\necho 3\n"
302
+ File.chmod 0755, script
303
+ assert_match (/^3$/), stdout.call {pkg.__send__ :run, script, chdir: dir}
304
+
305
+ # with no chdir, in a temporary directory, which is removed after
306
+ pwd = [RbConfig.ruby, '-e', 'puts Dir.pwd']
307
+ assert_equal "#{File.realpath dir}\n", pkg.__send__(:capture, *pwd, chdir: dir).first
308
+ assert_false File.exist?(pkg.__send__(:capture, *pwd).first.chomp)
309
+ assert_false File.exist?(stdout.call {pkg.__send__ :run, *pwd}.lines.last.chomp)
310
+ assert_equal ['', false], pkg.__send__(:capture, RbConfig.ruby, '-e', 'exit 1')
311
+ end
312
+ end
313
+
314
+ def test_frameworks()
315
+ assert_equal %w[Cocoa CoreMIDI], MacOS.makefile_frameworks(<<~MAKEFILE)
316
+ LIBS = $(LIBRUBYARG_SHARED) -lpthread
317
+ ldflags = -L. -fstack-protector-strong -framework Cocoa -framework CoreMIDI
318
+ MAKEFILE
319
+ assert_equal [], MacOS.makefile_frameworks("ldflags = -L.\n")
320
+ assert_equal [], MacOS.makefile_frameworks('')
321
+
322
+ # libraries whose gems are not built: no Makefiles next to the extconf.rbs
323
+ packager do |pkg, dir|
324
+ %w[xot/ext/xot/extconf.rb reflex/ext/reflex/extconf.rb reflex/lib/reflex/ext.rb].each do |path|
325
+ FileUtils.mkdir_p File.dirname(File.join dir, path)
326
+ FileUtils.touch File.join(dir, path)
327
+ end
328
+ xot, reflex = %w[xot reflex].map {RP::Library.new _1, File.join(dir, _1)}
329
+
330
+ stub pkg, :libraries, [xot] do
331
+ assert_equal [], pkg.frameworks # xot builds its extension only for its tests
332
+ end
333
+
334
+ stub pkg, :libraries, [xot, reflex] do
335
+ error = assert_raise(RP::Error) {pkg.frameworks}
336
+ assert_include error.message, "'reflex'"
337
+ assert_include error.message, 'was the gem built?'
338
+ end
339
+ end
340
+ end
341
+
342
+ # --- cruby_dir ---------------------------------------------------------
343
+
344
+ # Runs the block with a packager whose run records the commands instead of
345
+ # running them, and makes the clone a built one on 'rake'.
346
+ def fetching(yaml = nil, &block)
347
+ packager yaml, env: {'CRUBY_PATH' => nil} do |pkg, dir|
348
+ cmds = []
349
+ pkg.define_singleton_method :run do |*cmd, chdir:, env: {}|
350
+ cmds << cmd
351
+ FileUtils.mkdir_p File.join(chdir, 'CRuby', 'include') if cmd.first == 'rake'
352
+ end
353
+ block.call pkg, dir, cmds
354
+ end
355
+ end
356
+
357
+ def test_cruby_dir()
358
+ packager do |pkg, _|
359
+ assert_equal ENV['CRUBY_PATH'], pkg.cruby_dir
360
+ end
361
+
362
+ Dir.mktmpdir do |cruby|
363
+ packager "macos: {cruby: #{fake_cruby cruby}}" do |pkg, _|
364
+ assert_equal ENV['CRUBY_PATH'], pkg.cruby_dir # CRUBY_PATH overrides the config
365
+ end
366
+ packager "macos: {cruby: 1.2.3}" do |pkg, _|
367
+ assert_equal ENV['CRUBY_PATH'], pkg.cruby_dir
368
+ end
369
+ packager "macos: {cruby: #{cruby}}", env: {'CRUBY_PATH' => nil} do |pkg, _|
370
+ assert_equal cruby, pkg.cruby_dir
371
+ end
372
+ end
373
+
374
+ packager "macos: {cruby: not_built}", env: {'CRUBY_PATH' => nil} do |pkg, dir|
375
+ FileUtils.mkdir_p File.join(dir, 'not_built') # relative to the app
376
+ error = assert_raise(RP::Error) {pkg.cruby_dir}
377
+ assert_include error.message, "#{dir}/not_built"
378
+ assert_include error.message, 'download_or_build'
379
+ end
380
+
381
+ fetching do |pkg, dir, cmds|
382
+ cruby = File.join dir, ".build/macos/cruby/#{MacOS::CRUBY_VERSION}"
383
+ assert_equal cruby, pkg.cruby_dir
384
+ assert_include cmds.first, "v#{MacOS::CRUBY_VERSION}"
385
+ assert_equal cruby, cmds.first.last
386
+ assert_equal %w[rake download], cmds.last
387
+ end
388
+
389
+ fetching "macos: {cruby: 4.0.700}" do |pkg, _, cmds|
390
+ pkg.cruby_dir
391
+ assert_equal %w[rake download_or_build], cmds.last # with no task to download only
392
+ end
393
+
394
+ fetching "macos: {cruby: 1.2.3}" do |pkg, dir, cmds|
395
+ FileUtils.mkdir_p File.join(dir, '.build/macos/cruby/1.2.3/CRuby/include')
396
+ assert_equal File.join(dir, '.build/macos/cruby/1.2.3'), pkg.cruby_dir
397
+ assert_empty cmds # fetched already
87
398
  end
88
399
  end
89
400
 
90
- # --- build (checked before shelling out to xcodegen / pod / xcodebuild) -
401
+ # --- build (checked before shelling out to xcodegen / xcodebuild) ------
91
402
 
92
403
  def test_check_tools_reports_missing()
93
404
  packager do |pkg, _|
@@ -99,42 +410,328 @@ class TestPackagerMacOS < Test::Unit::TestCase
99
410
  end
100
411
  end
101
412
 
102
- # Runs the block with a packager whose CRuby / Reflex pods resolve to
103
- # local directories under +repos+ (via REFLEX_PODS_PATH).
104
- def with_pods(repos, &block)
105
- packager(nil, env: {'REFLEX_PODS_PATH' => repos}) {|pkg, _| block.call pkg}
413
+ def test_executable_tries_pathext()
414
+ packager do |pkg, _|
415
+ Dir.mktmpdir do |bin|
416
+ FileUtils.touch File.join(bin, 'tool.EXE')
417
+ File.chmod 0755, File.join(bin, 'tool.EXE')
418
+ with_env 'PATH' => bin, 'PATHEXT' => nil do
419
+ assert !pkg.__send__(:executable?, 'tool')
420
+ end
421
+ with_env 'PATH' => bin, 'PATHEXT' => '.COM;.EXE' do
422
+ assert pkg.__send__(:executable?, 'tool')
423
+ assert pkg.__send__(:executable?, :tool)
424
+ assert !pkg.__send__(:executable?, 'other')
425
+ end
426
+ end
427
+ end
106
428
  end
107
429
 
108
- def test_check_dev_pods_missing_dir()
109
- Dir.mktmpdir do |repos|
110
- with_pods repos do |pkg| # neither repos/cruby nor repos/reflex exists
111
- error = assert_raise(RP::Error) {pkg.__send__ :check_dev_pods}
112
- assert_include error.message, 'pod directory not found'
430
+ def test_build_by_cloud_signing()
431
+ # with the security commands captured, and the files the commands take
432
+ # while they run, which are temporary
433
+ build = -> pkg, fail: false do
434
+ commands, copied, securities, files = [], [], [], {}
435
+ pkg.define_singleton_method(:check_tools) {|*|}
436
+ pkg.define_singleton_method(:copy_app) {|app = nil| copied << app}
437
+ pkg.define_singleton_method(:run) do |*cmd, **|
438
+ commands << cmd
439
+ cmd.each {files[_1] = File.read _1 if _1.end_with?('.p8') && File.file?(_1)}
440
+ raise RP::Error, 'failed' if fail && cmd.include?('-exportArchive')
113
441
  end
442
+ pkg.define_singleton_method(:capture) do |*cmd, **|
443
+ securities << cmd.drop(1).join(' ')
444
+ files[cmd[2]] = File.binread cmd[2] if cmd[1] == 'import'
445
+ cmd.include?('list-keychains') && cmd.size == 4 ? [" \"/login.keychain-db\"\n", true] : ['', true]
446
+ end
447
+ begin
448
+ pkg.build
449
+ rescue RP::Error
450
+ raise unless fail
451
+ end
452
+ [commands.map {_1.join ' '}, copied, securities, files]
453
+ end
454
+
455
+ packager "macos: {codesign: {team_id: ABCDE12345}}" do |pkg, dir|
456
+ assert_include pkg.__send__(:render, 'ExportOptions.plist.erb'), '<string>ABCDE12345</string>'
457
+ assert_include pkg.__send__(:render, 'ExportOptions.plist.erb'), '<string>export</string>'
458
+ pkg.generate
459
+ assert File.exist?(File.join dir, '.build/macos/generic/ExportOptions.plist')
460
+
461
+ commands, copied = build.call pkg
462
+ assert_equal 1, commands.grep(/ archive -archivePath .*\/#{pkg.target}\.xcarchive$/).size
463
+ assert_equal 1, commands.grep(/-exportArchive .*-allowProvisioningUpdates$/).size
464
+ assert_equal [File.join(dir, ".build/macos/generic/export/#{pkg.target}.app")], copied
465
+ end
466
+
467
+ env = {
468
+ 'APPLE_API_ISSUER_ID' => 'ISSUER',
469
+ 'APPLE_API_KEY_ID' => 'KEY',
470
+ 'APPLE_API_KEY_PATH' => '/key.p8'
471
+ }
472
+ packager "macos: {codesign: {team_id: ABCDE12345}}", env: env do |pkg, _|
473
+ commands, _ = build.call pkg
474
+ assert_equal 1, commands.grep(
475
+ /-authenticationKeyIssuerID ISSUER -authenticationKeyID KEY -authenticationKeyPath \/key\.p8$/).size
476
+ end
477
+ packager "macos: {codesign: {team_id: ABCDE12345}}", env: env.merge('APPLE_API_KEY_ID' => '') do |pkg, _|
478
+ error = assert_raise(RP::Error) {build.call pkg}
479
+ assert_include error.message, 'APPLE_API_KEY_ID'
480
+ end
481
+
482
+ # Developer ID in the keychain, not the one managed, as CI needs
483
+ packager "macos: {codesign: {identity: Developer ID Application, team_id: ABCDE12345}}" do |pkg, dir|
484
+ options = pkg.__send__(:render, 'ExportOptions.plist.erb')
485
+ assert_include options, '<string>manual</string>'
486
+ assert_include options, "<key>signingCertificate</key>\n\t<string>Developer ID Application</string>"
487
+ assert_include pkg.__send__(:render, 'project.yml.erb'), 'CODE_SIGN_IDENTITY: "-"'
488
+
489
+ commands, copied = build.call pkg
490
+ assert_equal 1, commands.grep(/ archive -archivePath /).size
491
+ assert_equal 1, commands.grep(/-exportArchive /).size
492
+ assert_equal [File.join(dir, ".build/macos/generic/export/#{pkg.target}.app")], copied
493
+ end
494
+
495
+ # the certificate of Developer ID by the environment variables, as on CI,
496
+ # in a keychain only while exporting, even when failed to
497
+ p12 = {'APPLE_CODESIGN_P12' => ['P12'].pack('m0'), 'APPLE_CODESIGN_P12_PASSWORD' => 'PASS'}
498
+ packager "macos: {codesign: {team_id: ABCDE12345}}", env: p12 do |pkg, _|
499
+ assert_include pkg.__send__(:render, 'ExportOptions.plist.erb'), '<string>manual</string>'
500
+
501
+ [false, true].each do |fail|
502
+ commands, _, securities, files = build.call MacOS.new(pkg.config), fail: fail
503
+ assert_equal 1, commands.grep(/-exportArchive /).size
504
+ assert_equal [
505
+ 'list-keychains -d user',
506
+ 'create-keychain', 'set-keychain-settings', 'unlock-keychain', 'import',
507
+ 'set-key-partition-list', 'list-keychains -d user -s',
508
+ 'delete-keychain'
509
+ ], securities.map {_1[/^list-keychains -d user( -s)?|^\S+/]}
510
+ assert_match %r{^import .*/codesign\.p12 -k .*/codesign\.keychain-db -P PASS }, securities[4]
511
+ assert_match %r{^list-keychains -d user -s .*/codesign\.keychain-db /login\.keychain-db$},
512
+ securities[6] # with the others kept
513
+ assert_equal ['P12'], files.values
514
+ files.each_key {assert !File.exist?(_1)} # deleted
515
+ end
516
+ end
517
+
518
+ # the API key by the content of the .p8 file, as on CI, written only while
519
+ # exporting
520
+ p8 = {'APPLE_API_KEY_P8' => 'P8', 'APPLE_API_KEY_ID' => 'KEY', 'APPLE_API_ISSUER_ID' => 'ISSUER'}
521
+ packager "macos: {codesign: {team_id: ABCDE12345}}", env: p8 do |pkg, _|
522
+ commands, _, _, files = build.call pkg
523
+ assert_equal 1, commands.grep(%r{-authenticationKeyPath \S+/AuthKey_KEY\.p8$}).size
524
+ assert_equal ['P8'], files.values
525
+ files.each_key {assert !File.exist?(_1)}
526
+ end
527
+
528
+ # the other identities in the keychain, not archived
529
+ packager "macos: {codesign: {identity: Apple Development, team_id: ABCDE12345}}" do |pkg, _|
530
+ assert_include pkg.__send__(:render, 'project.yml.erb'), 'CODE_SIGN_IDENTITY: "Apple Development"'
531
+
532
+ commands, copied = build.call pkg
533
+ assert_empty commands.grep(/archive/i)
534
+ assert_equal 1, commands.grep(/ build$/).size
535
+ assert_equal [nil], copied # the one built
114
536
  end
115
537
  end
116
538
 
117
- def test_check_dev_pods_cruby_not_built()
118
- Dir.mktmpdir do |repos|
119
- FileUtils.mkdir_p File.join(repos, 'cruby') # exists but has no CRuby/include
120
- with_pods repos do |pkg|
121
- error = assert_raise(RP::Error) {pkg.__send__ :check_dev_pods}
122
- assert_include error.message, 'no CRuby binary'
123
- assert_include error.message, 'download_or_build'
539
+ def test_build_notarized()
540
+ all = %i[submit export]
541
+
542
+ # builds or only exports, as --notarize-export does, with the outputs of
543
+ # the tries to export the app notarized
544
+ build = -> config, notarize, outs = [] do
545
+ pkg = MacOS.new config, notarize: notarize
546
+ commands, captured, copied, printed = [], [], [], []
547
+ pkg.define_singleton_method(:check_tools) {|*|}
548
+ pkg.define_singleton_method(:run) {|*cmd, **| commands << cmd.join(' ')}
549
+ pkg.define_singleton_method(:sleep) {|*|}
550
+ pkg.define_singleton_method(:puts) {|str| printed << str}
551
+ pkg.define_singleton_method(:copy_app) {|app = nil| copied << app}
552
+ pkg.define_singleton_method(:capture) do |*cmd, **opts|
553
+ captured << cmd.join(' ')
554
+ # in no directory of the project, which is not there when only
555
+ # exporting on CI
556
+ raise "chdir to #{opts[:chdir]}" if opts[:chdir]
557
+ outs.shift
558
+ end
559
+ notarize == %i[export] ? pkg.package : pkg.build
560
+ [commands, captured, copied, printed]
561
+ end
562
+
563
+ packager "macos: {codesign: {team_id: ABCDE12345}}" do |pkg, dir|
564
+ notarized = File.join dir, ".build/macos/generic/notarized/#{pkg.target}.app"
565
+ outs = -> {[
566
+ ['error: Archive "a" is processing and not ready for distribution.', false],
567
+ ['error: Ticket not found.', false],
568
+ ['', true]
569
+ ]}
570
+ [all, %i[submit]].each do |notarize|
571
+ mac = MacOS.new pkg.config, notarize: notarize
572
+ assert_include mac.__send__(:render, 'ExportOptions.plist.erb'), '<string>upload</string>'
573
+ end
574
+
575
+ # no archive sent yet
576
+ error = assert_raise(RP::Error) {build.call pkg.config, %i[export]}
577
+ assert_include error.message, 'no archive sent'
578
+ FileUtils.mkdir_p File.join(dir, ".build/macos/generic/#{pkg.target}.xcarchive/Submissions/ABC-123")
579
+
580
+ commands, captured, copied = build.call pkg.config, all, outs.call
581
+ assert_equal 1, commands.grep(/-exportArchive/).size
582
+ assert_equal 3, captured.grep(/-exportNotarizedApp .* -exportPath .*\/generic\/notarized$/).size
583
+ assert_equal [notarized], copied
584
+
585
+ commands, captured, copied, printed = build.call pkg.config, %i[submit]
586
+ assert_equal 1, commands.grep(/-exportArchive/).size
587
+ assert_empty captured
588
+ assert_empty copied
589
+ assert_include printed, 'Submitted for the notarization: abc-123' # in lower case
590
+
591
+ commands, captured, copied = build.call pkg.config, %i[export], outs.call
592
+ assert_empty commands # not built
593
+ assert_equal 3, captured.size
594
+ assert_equal [notarized], copied
595
+
596
+ error = assert_raise(RP::Error) {build.call pkg.config, all, [['error: Invalid.', false]]}
597
+ assert_include error.message, 'error: Invalid.'
598
+
599
+ assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: all).package generate_only: true}
600
+ end
601
+
602
+ packager do |pkg, _|
603
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: all).package}
604
+ assert_include error.message, 'APPLE_TEAM_ID'
605
+
606
+ # only exporting, which needs no team, but the archive sent
607
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: %i[export]).package}
608
+ assert_include error.message, 'no archive sent'
609
+ end
610
+ packager "macos: {codesign: {identity: Developer ID Application, team_id: ABCDE12345}}" do |pkg, dir|
611
+ FileUtils.mkdir_p File.join(dir, ".build/macos/generic/#{pkg.target}.xcarchive/Submissions/ABC-123")
612
+ commands, _, _, printed = build.call pkg.config, %i[submit]
613
+ assert_equal 1, commands.grep(/-exportArchive/).size
614
+ assert_include printed, 'Submitted for the notarization: abc-123'
615
+ end
616
+ packager "macos: {codesign: {identity: Apple Development, team_id: ABCDE12345}}" do |pkg, _|
617
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: all).package}
618
+ assert_include error.message, 'Developer ID Application'
619
+ end
620
+
621
+ # the API key, which Apple refuses to sign with the certificate it manages
622
+ api = {'APPLE_API_ISSUER_ID' => 'ISSUER', 'APPLE_API_KEY_ID' => 'KEY', 'APPLE_API_KEY_P8' => 'P8'}
623
+ packager "macos: {codesign: {team_id: ABCDE12345}}", env: api do |pkg, _|
624
+ [[], all, %i[submit]].each do |notarize|
625
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: notarize).package}
626
+ assert_include error.message, 'APPLE_CODESIGN_P12'
124
627
  end
628
+
629
+ # only exporting, which signs nothing
630
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, notarize: %i[export]).package}
631
+ assert_include error.message, 'no archive sent'
632
+ end
633
+
634
+ # the certificate with no team
635
+ p12 = {'APPLE_CODESIGN_P12' => ['P12'].pack('m0')}
636
+ packager env: p12 do |pkg, _|
637
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config).package}
638
+ assert_include error.message, 'APPLE_TEAM_ID'
125
639
  end
126
640
  end
127
641
 
128
- def test_check_dev_pods_reflex_not_set_up()
129
- Dir.mktmpdir do |repos|
130
- FileUtils.mkdir_p File.join(repos, 'cruby', 'CRuby', 'include') # CRuby is OK
131
- FileUtils.mkdir_p File.join(repos, 'reflex') # exists but has no xot
132
- with_pods repos do |pkg|
133
- error = assert_raise(RP::Error) {pkg.__send__ :check_dev_pods}
134
- assert_include error.message, 'not set up for CocoaPods'
135
- assert_include error.message, 'pod.rake setup'
642
+ def test_build_for_appstore()
643
+ # with the commands captured, and what is copied into dist/
644
+ build = -> config, **options do
645
+ mac = MacOS.new config, **options
646
+ commands, copied = [], []
647
+ mac.define_singleton_method(:check_tools) {|*|}
648
+ mac.define_singleton_method(:run) {|*cmd, **| commands << cmd.join(' ')}
649
+ mac.define_singleton_method(:puts) {|*|}
650
+ mac.define_singleton_method(:copy_app) {|app = nil| copied << app}
651
+ mac.define_singleton_method(:copy_pkg) {|pkg, dist| copied << [pkg, dist]}
652
+ mac.build
653
+ [commands, copied]
654
+ end
655
+
656
+ yaml = "name: My App\nmacos: {category: public.app-category.games, codesign: {team_id: ABCDE12345}}"
657
+ packager yaml do |pkg, dir|
658
+ appstore = File.join dir, '.build/macos/appstore'
659
+ generic = File.join dir, '.build/macos/generic'
660
+ mac = MacOS.new pkg.config, appstore: true
661
+ mac.generate
662
+ assert_include File.read("#{appstore}/AppStore.entitlements"),
663
+ "<key>com.apple.security.app-sandbox</key>\n\t<true/>"
664
+ options = File.read "#{appstore}/ExportOptions.plist"
665
+ assert_include options, '<string>app-store-connect</string>'
666
+ assert_include options, '<string>automatic</string>'
667
+ assert_include options, '<string>ABCDE12345</string>'
668
+ assert_equal File.join(dir, "dist/appstore/#{pkg.target}-#{pkg.config.version}.pkg"),
669
+ mac.appstore_pkg_path
670
+
671
+ # only the one for the App Store, archived in the sandbox
672
+ commands, copied = build.call pkg.config, appstore: true
673
+ assert_equal [
674
+ "#{appstore}/MyApp.xcarchive CODE_SIGN_ENTITLEMENTS=#{appstore}/AppStore.entitlements"
675
+ ], commands.grep(/ archive /).map {_1[/(?<=-archivePath ).*/]}
676
+ assert_equal 1, commands.grep(
677
+ %r{-exportArchive -archivePath #{appstore}/\S+ -exportOptionsPlist #{appstore}/ExportOptions\.plist -exportPath #{appstore}/export -allowProvisioningUpdates$}).size
678
+ # the package named after the name of the app, not the target
679
+ assert_equal [["#{appstore}/export/My App.pkg", mac.appstore_pkg_path]], copied
680
+
681
+ # the generic one too, archived out of the sandbox, with nothing left of
682
+ # the exports before
683
+ stales = ["#{generic}/export/My App.app", "#{appstore}/export/My App.pkg"]
684
+ stales.each {FileUtils.mkdir_p _1}
685
+ commands, copied = build.call pkg.config, generic: true, appstore: true
686
+ stales.each {assert_false File.exist?(_1)}
687
+ assert_equal ["#{generic}/#{pkg.target}.xcarchive", "#{appstore}/#{pkg.target}.xcarchive"],
688
+ commands.grep(/ archive /).map {_1[/(?<=-archivePath )\S+/]}
689
+ assert_equal "#{generic}/export/My App.app", copied.first
690
+
691
+ # notarized, which only the generic one is
692
+ FileUtils.mkdir_p "#{generic}/#{pkg.target}.xcarchive/Submissions/ABC-123"
693
+ commands, copied = build.call pkg.config, notarize: %i[submit], appstore: true
694
+ assert_equal 2, commands.grep(/ archive /).size
695
+ assert_equal [["#{appstore}/export/My App.pkg", mac.appstore_pkg_path]], copied
696
+
697
+ [
698
+ [{appstore: true}, {generate_only: true}, 'built on macos'],
699
+ [{appstore: true, notarize: %i[export]}, {}, 'only exporting the notarized app']
700
+ ].each do |options, package_options, message|
701
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, **options).package(**package_options)}
702
+ assert_include error.message, message
136
703
  end
137
704
  end
705
+
706
+ packager do |pkg, _|
707
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, appstore: true).package}
708
+ assert_include error.message, 'APPLE_TEAM_ID'
709
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, generic: true).package}
710
+ assert_include error.message, 'Developer ID Application'
711
+ end
712
+ packager "macos: {codesign: {team_id: ABCDE12345}}" do |pkg, _|
713
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, appstore: true).package}
714
+ assert_include error.message, 'macos.category'
715
+ end
716
+ packager "version: 1.2.3.4" do |pkg, _|
717
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config).package}
718
+ assert_include error.message, 'CFBundleVersion'
719
+ end
720
+ packager "macos: {codesign: {identity: Apple Development, team_id: ABCDE12345}}" do |pkg, _|
721
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, generic: true).package}
722
+ assert_include error.message, 'Developer ID Application'
723
+ end
724
+
725
+ # the API key, refused to sign with Developer ID, but not the generic one
726
+ api = {'APPLE_API_ISSUER_ID' => 'ISSUER', 'APPLE_API_KEY_ID' => 'KEY', 'APPLE_API_KEY_P8' => 'P8'}
727
+ packager yaml, env: api do |pkg, _|
728
+ mac = MacOS.new pkg.config, appstore: true
729
+ mac.define_singleton_method(:generate) {}
730
+ mac.define_singleton_method(:build) {}
731
+ assert_nothing_raised {mac.package}
732
+ error = assert_raise(RP::Error) {MacOS.new(pkg.config, generic: true, appstore: true).package}
733
+ assert_include error.message, 'APPLE_CODESIGN_P12'
734
+ end
138
735
  end
139
736
 
140
737
  def test_copy_app_without_build_product_raises()
@@ -154,58 +751,89 @@ class TestPackagerMacOS < Test::Unit::TestCase
154
751
  packager("name: アプリ\nbundle_id: com.example.app") {|pkg, _| assert_equal 'App', pkg.target}
155
752
  end
156
753
 
754
+ def test_dist_paths()
755
+ packager "name: My App\nversion: 1.2.3" do |pkg, dir|
756
+ dist = File.join dir, 'dist/adhoc'
757
+ # the app by its name with the spaces, the zip by the target
758
+ assert_equal "#{dist}/My App.app", pkg.dist_path
759
+ assert_equal "#{dist}/MyApp-1.2.3-macos-universal.zip", pkg.archive_path
760
+ assert_equal ['ditto', '-c', '-k', '--keepParent', pkg.dist_path, pkg.archive_path],
761
+ pkg.archive_command
762
+
763
+ # notarized, which needs no team to only be exported
764
+ assert_equal File.join(dir, 'dist/generic/My App.app'),
765
+ MacOS.new(pkg.config, notarize: %i[export]).dist_path
766
+ end
767
+ packager "name: My App\nmacos: {codesign: {team_id: ABCDE12345}}" do |pkg, dir|
768
+ assert_equal File.join(dir, 'dist/generic/My App.app'), pkg.dist_path
769
+ end
770
+ packager "name: My App\nmacos: {codesign: {identity: Apple Development, team_id: ABCDE12345}}" do |pkg, dir|
771
+ assert_equal File.join(dir, 'dist/adhoc/My App.app'), pkg.dist_path
772
+ end
773
+ packager("macos: {archs: [arm64]}") {|pkg, _| assert_equal 'macos-arm64', pkg.archive_platform}
774
+ end
775
+
157
776
  # --- extensions / libraries (registered with CRuby in main.mm) ---------
158
777
 
159
778
  def test_main_mm_registers_runtime_and_starts()
160
779
  packager "main: app.rb", files: %w[app.rb] do |pkg, dir|
161
780
  pkg.generate
162
781
  str = read dir, 'src/main.mm'
163
- assert_include str, '@"app"' # the bundled app dir
164
782
  assert_include str, 'Init_reflex_ext' # native ext registered
165
783
  assert_include str, 'Init_rays_ext'
166
- assert_include str, '@"Reflex"' # library bundle added
167
- assert_include str, 'changeCurrentDirectoryPath' # cwd set to app dir
168
- assert_include str, '@"app.rb"' # the entry script
784
+ assert_include str, '@"reflex"' # library bundle added
785
+ assert_include str, '@"boot.rb"' # started with boot.rb
786
+ assert_include str, 'return [CRuby start:' # ends with its exit status
787
+ assert_include read(dir, 'boot.rb'), '"app.rb"' # the entry script
169
788
  end
170
789
  end
171
790
 
172
- # --- pod_refs / dev_pod_paths (rendered into the Podfile) --------------
791
+ def test_boot_rb()
792
+ # app/reflex.rb stands in for reflex, found first on the load path
793
+ reflex = <<~RUBY
794
+ module Reflex
795
+ def self.alert(message, title:) = File.write('../alert', "\#{title}\\n\#{message}")
796
+ end
797
+ RUBY
798
+ boot = -> (main, tty: false, pack: false) {
799
+ packager "name: My App\nfiles: [reflex.rb]", files: %w[main.rb reflex.rb] do |pkg, dir|
800
+ File.write File.join(dir, 'main.rb'), main
801
+ File.write File.join(dir, 'reflex.rb'), reflex
802
+ pkg = MacOS.new pkg.config, pack: pack
803
+ pkg.generate
804
+ compile_scripts pkg if pack
805
+ # boot.rb is beside app/ and the bundles in the resources of the app,
806
+ # as here, and CRuby has the lib of each bundle on the load path
807
+ FileUtils.cp_r File.join(dir, '.build/macos/project/Bundles/reflex.bundle'),
808
+ File.join(dir, '.build/macos/project')
809
+ lib = File.join dir, '.build/macos/project/reflex.bundle/Contents/Resources/lib'
810
+ tty = "$stderr.define_singleton_method(:tty?) {#{tty}}"
811
+ _, err, status = Open3.capture3 RbConfig.ruby, '-I', lib, '-e', "#{tty}; load ARGV[0]",
812
+ File.join(dir, '.build/macos/project/boot.rb')
813
+ alert = File.exist?(File.join dir, '.build/macos/project/alert') ? read(dir, 'alert') : nil
814
+ return [status.exitstatus, alert, err]
815
+ end
816
+ }
173
817
 
174
- def test_podfile_defaults_to_git_pods()
175
- packager do |pkg, dir|
176
- pkg.generate
177
- str = read dir, 'Podfile'
178
- assert_include str, "pod 'CRuby', git: 'https://github.com/xord/cruby'"
179
- assert_include str,
180
- "pod 'Reflex', git: 'https://github.com/xord/reflex', " +
181
- "tag: 'v#{Reflex::Extension.version}'"
182
- assert_not_include str, 'PODS_ROOT'
183
- end
184
- end
818
+ status, alert, = boot["raise 'boom'"]
819
+ assert_equal 1, status
820
+ assert_equal 'My App', alert.lines.first.chomp
821
+ assert_include alert, 'boom (RuntimeError)'
822
+ assert_not_include alert, 'boot.rb'
823
+ assert_match(/^app\/main\.rb:1:/, alert) # from the directory of boot.rb
185
824
 
186
- def test_podfile_with_pods_path_env()
187
- packager nil, env: {'REFLEX_PODS_PATH' => '/repos'} do |pkg, dir|
188
- pkg.generate
189
- str = read dir, 'Podfile'
190
- assert_include str, "pod 'CRuby', path: '/repos/cruby'"
191
- assert_include str, "pod 'Reflex', path: '/repos/reflex'"
192
- # development pods need the ${PODS_ROOT} rewrite block
193
- assert_include str, "s.gsub! '${PODS_ROOT}/CRuby', '/repos/cruby'"
194
- assert_include str, "s.gsub! '${PODS_ROOT}/Reflex', '/repos/reflex'"
195
- end
196
- end
825
+ status, alert, = boot["raise 'boom'", pack: true]
826
+ assert_equal 1, status
827
+ assert_include alert, 'boom (RuntimeError)'
828
+ assert_not_include alert, 'data_loader.rb'
829
+ assert_match(/^main\.rb:1:/, alert) # compiled with the path relative to app/
197
830
 
198
- def test_podfile_with_pods_config()
199
- yaml = <<~YML
200
- pods:
201
- cruby: {git: https://example.com/cruby, branch: dev}
202
- YML
203
- packager yaml do |pkg, dir|
204
- pkg.generate
205
- line = read(dir, 'Podfile').lines.grep(/pod 'CRuby'/).first
206
- assert_match %r{git: 'https://example.com/cruby'}, line
207
- assert_match %r{branch: 'dev'}, line
208
- end
831
+ status, alert, err = boot["raise 'boom'", tty: true] # shown in the terminal
832
+ assert_equal [1, nil], [status, alert]
833
+ assert_include err, 'boom (RuntimeError)'
834
+
835
+ assert_equal [3, nil], boot['exit 3'].first(2)
836
+ assert_equal [0, nil], boot[''] .first(2)
209
837
  end
210
838
 
211
839
  # --- icon_commands -----------------------------------------------------