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,370 @@
1
+ require 'rbconfig'
2
+ require 'shellwords'
3
+ require 'reflex/packager/platform'
4
+ require 'reflex/packager/gems'
5
+
6
+
7
+ module Reflex
8
+
9
+
10
+ module Packager
11
+
12
+
13
+ # Packages a Reflex application as a Windows executable with the Ruby
14
+ # runtime it runs on next to it.
15
+ #
16
+ # The executable links the Ruby DLL of RubyInstaller, and the native
17
+ # libraries (xot, rucy, rays, ...) statically from the archives and the
18
+ # ext objects a gem build leaves in the installed gems. Each extension is
19
+ # registered with ruby_init_ext, so requiring it initializes the copy
20
+ # linked in, as --with-static-linked-ext does.
21
+ #
22
+ class Windows < Platform
23
+
24
+ include Gems
25
+
26
+ TOOLCHAIN_HINT = 'install RubyInstaller with the MSYS2 DevKit (ridk install)'
27
+
28
+ # The dlls the executable loads go in this directory, which is a
29
+ # private assembly of the same name that the manifest embedded in the
30
+ # executable depends on. Having the ruby dll in lib/ (or bin/) also
31
+ # makes the parent directory the prefix, where Ruby looks for lib/ruby.
32
+ #
33
+ RUNTIME_DIR = 'lib'
34
+ RUNTIME_VERSION = '1.0.0.0'
35
+
36
+ # Sizes of the icon of the executable, one image each in the ICO.
37
+ #
38
+ ICON_SIZES = [16, 32, 48, 256]
39
+
40
+ def package(generate_only: false)
41
+ raise Error, 'notarizing is only for macos' if notarize?
42
+ raise Error, 'the Mac App Store is only for macos' if appstore?
43
+ super
44
+ end
45
+
46
+ def generate()
47
+ copy_libraries
48
+ copy_app_files 'lib/app'
49
+ write 'src/main.cpp', render('main.cpp.erb')
50
+ write 'src/app.manifest', render('app.manifest.erb')
51
+ write 'src/app.rc', render('app.rc.erb')
52
+ write 'lib/boot.rb', render('boot.rb.erb')
53
+ generate_icon if config.icon
54
+ end
55
+
56
+ def generate_icon()
57
+ File.binwrite File.join(build_dir, 'src', 'app.ico'), Windows.ico(icon_pngs)
58
+ end
59
+
60
+ # [[size, png], ...] of the icon drawn at each size with Rays.
61
+ #
62
+ def icon_pngs()
63
+ require 'rays'
64
+ icon = Rays::Image.load File.join(config.dir, config.icon), smooth: true
65
+ ICON_SIZES.map do |size|
66
+ path = File.join build_dir, 'src', "icon_#{size}.png"
67
+ Rays::Image.new(size, size).paint {|p| p.image icon, 0, 0, size, size}.save path
68
+ [size, File.binread(path)]
69
+ end
70
+ end
71
+
72
+ # An ICO of the PNGs: ICONDIR, an ICONDIRENTRY per image, then the
73
+ # images. Windows takes PNG images in an ICO since Vista, and a size of
74
+ # 256 is written as 0.
75
+ #
76
+ def self.ico(pngs)
77
+ offset = 6 + 16 * pngs.size
78
+ entries = pngs.map do |size, png|
79
+ entry = [size % 256, size % 256, 0, 0, 1, 32, png.bytesize, offset].pack 'C4v2V2'
80
+ offset += png.bytesize
81
+ entry
82
+ end
83
+ [[0, 1, pngs.size].pack('v3'), *entries, *pngs.map {_1.last.b}].join
84
+ end
85
+
86
+ def build()
87
+ enable_toolchain
88
+ check_tools tools
89
+ # the strings of the app name are in utf-8
90
+ run 'windres', '--codepage=65001', 'app.rc', '-o', 'app.res.o',
91
+ chdir: File.join(build_dir, 'src')
92
+ run(*link_command, chdir: build_dir)
93
+ copy_dist
94
+ end
95
+
96
+ # The command to zip the packaged app into archive_path, with its
97
+ # folder, by the tar of Windows, which writes a zip by the extension.
98
+ # The tar of MSYS2 or git, which may come first on PATH, cannot.
99
+ #
100
+ def archive_command()
101
+ tar = File.join ENV['SystemRoot'] || 'C:/Windows', 'System32', 'tar.exe'
102
+ [tar, '-a', '-c', '-f', archive_path, '-C', dist_dir, File.basename(dist_path)]
103
+ end
104
+
105
+ # The architecture is the one of the ruby the executable is made with.
106
+ #
107
+ def archive_platform(rbconfig = RbConfig::CONFIG)
108
+ "windows-#{rbconfig['host_cpu'].match?(/arm|aarch64/) ? 'arm64' : 'x64'}"
109
+ end
110
+
111
+ def tools()
112
+ {compiler.first => TOOLCHAIN_HINT, 'windres' => TOOLCHAIN_HINT, 'objdump' => TOOLCHAIN_HINT}
113
+ end
114
+
115
+ # Directories under lib/ boot.rb puts on the load path, in the order of
116
+ # lib_names: the libraries, and the gems under gems/, apart from what
117
+ # else lib/ has, as ruby/ and app/.
118
+ #
119
+ def load_dirs()
120
+ gems = gem_names
121
+ lib_names.map {gems.include?(_1) ? File.join('gems', _1) : _1}
122
+ end
123
+
124
+ # Libraries built from native code: the ones whose gem has the static
125
+ # archive the extension was linked from.
126
+ #
127
+ def native_libraries()
128
+ libraries.select {File.file? static_archive(_1)}
129
+ end
130
+
131
+ def ext_objects()
132
+ libraries.select(&:extension).flat_map do |lib|
133
+ objs = Dir.glob(File.join lib.root, 'ext', lib.name, '*.o').sort
134
+ raise Error, "no objects of '#{lib.extension}' in '#{lib.root}' (was the gem built?)" if
135
+ objs.empty?
136
+ objs
137
+ end
138
+ end
139
+
140
+ # Static archives of the native libraries, the ones depending on the
141
+ # others first.
142
+ #
143
+ def static_archives()
144
+ native_libraries.reverse.map {static_archive _1}
145
+ end
146
+
147
+ # System libraries the extensions link, as the Makefiles their gem builds
148
+ # leave have them.
149
+ #
150
+ def system_libs()
151
+ native_libraries.flat_map do |lib|
152
+ makefile = File.join lib.root, 'ext', lib.name, 'Makefile'
153
+ File.file?(makefile) ? Windows.makefile_libs(File.read makefile) : []
154
+ end.uniq
155
+ end
156
+
157
+ # The names in the -l options of the LIBS of a Makefile mkmf writes.
158
+ #
159
+ def self.makefile_libs(makefile)
160
+ line = makefile.lines.find {_1.start_with? 'LIBS ='} or return []
161
+ line.split.filter_map {_1[/\A-l(.+)\z/, 1]}
162
+ end
163
+
164
+ # Paths of the DLLs of the toolchain the executable loads, directly or
165
+ # through another one: the import tables are followed from the
166
+ # executable as far as the DLLs beside the compiler go. The ones of
167
+ # Windows and the ruby dll end it, and any other is not to be found.
168
+ #
169
+ def toolchain_dlls(rbconfig = RbConfig::CONFIG)
170
+ @toolchain_dlls ||= begin
171
+ dir = toolchain_dir(rbconfig) or
172
+ raise Error, "compiler not found: #{TOOLCHAIN_HINT}"
173
+ found = {}
174
+ exe = File.join build_dir, "#{target}.exe"
175
+ queue = dll_imports(exe).map {[_1, exe]}
176
+ until queue.empty?
177
+ dll, by = queue.shift
178
+ next if found.key? dll.downcase
179
+ path = File.join dir, dll
180
+ if File.file? path
181
+ found[dll.downcase] = path
182
+ queue.concat dll_imports(path).map {[_1, path]}
183
+ else
184
+ raise Error, "'#{dll}' needed by #{File.basename by} not found" unless
185
+ system_dll? dll, rbconfig
186
+ found[dll.downcase] = nil
187
+ end
188
+ end
189
+ found.values.compact
190
+ end
191
+ end
192
+
193
+ # DLLs the executable loads from lib/, listed in its manifest.
194
+ #
195
+ def runtime_dlls(rbconfig = RbConfig::CONFIG)
196
+ [rbconfig['LIBRUBY_SO'], *toolchain_dlls(rbconfig).map {File.basename _1}]
197
+ end
198
+
199
+ # The versions of the product and of the build, as the four numbers of
200
+ # the version resource and the assembly version.
201
+ #
202
+ def product_version()
203
+ Windows.four_numbers config.display_version
204
+ end
205
+
206
+ def file_version()
207
+ Windows.four_numbers config.build_version
208
+ end
209
+
210
+ # The first four numbers of +version+, filled up to four, which have to
211
+ # be in 0..65535.
212
+ #
213
+ def self.four_numbers(version)
214
+ numbers = version.split('.').first(4).map(&:to_i)
215
+ raise Error, "a number in the version '#{version}' is over 65535" if
216
+ numbers.any? {_1 > 65535}
217
+ (numbers + [0] * 4).first(4).join '.'
218
+ end
219
+
220
+ # The language id of a block of the version resource, for +lang+ of the
221
+ # localizations, as Windows has it.
222
+ #
223
+ def langid(lang)
224
+ return 0x0409 if lang == 'en' # U.S. English, the one of the values out of them
225
+ id = Windows.lcid(lang) & 0xffff
226
+ raise Error, "unknown language on windows: '#{lang}'" if
227
+ id == 0 || id == LOCALE_CUSTOM_UNSPECIFIED
228
+ id
229
+ end
230
+
231
+ LOCALE_CUSTOM_UNSPECIFIED = 0x1000
232
+
233
+ # The locale id Windows has for the locale +name+, or 0.
234
+ #
235
+ def self.lcid(name)
236
+ require 'fiddle'
237
+ @locale_name_to_lcid ||= Fiddle::Function.new(
238
+ Fiddle.dlopen('kernel32')['LocaleNameToLCID'],
239
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
240
+ @locale_name_to_lcid.call "#{name}\0".encode('UTF-16LE'), 0
241
+ end
242
+
243
+ # +str+ as a string literal of a resource script, which doubles a quote
244
+ # rather than escaping it.
245
+ #
246
+ def rc_string(str)
247
+ %("#{str.gsub('\\', '\\\\\\\\').gsub('"', '""')}")
248
+ end
249
+
250
+ def compiler(rbconfig = RbConfig::CONFIG)
251
+ # may carry flags, e.g. 'g++ -std=gnu++11'
252
+ rbconfig['CXX'].shellsplit
253
+ end
254
+
255
+ # Compiles src/main.cpp and links it into the executable, run in the
256
+ # build directory.
257
+ #
258
+ def link_command(rbconfig = RbConfig::CONFIG)
259
+ [
260
+ *compiler(rbconfig), 'src/main.cpp', 'src/app.res.o', '-o', "#{target}.exe",
261
+ "-I#{rbconfig['rubyhdrdir']}", "-I#{rbconfig['rubyarchhdrdir']}",
262
+ *ext_objects,
263
+ '-Wl,--whole-archive', *static_archives, '-Wl,--no-whole-archive',
264
+ "-L#{rbconfig['libdir']}", *rbconfig['LIBRUBYARG_SHARED'].shellsplit,
265
+ *system_libs.map {"-l#{_1}"},
266
+ *('-mwindows' unless config.windows.console?)
267
+ ]
268
+ end
269
+
270
+ # Copies what the executable needs from the Ruby it was built with:
271
+ # the dlls into lib/, the standard library into lib/ruby.
272
+ #
273
+ def copy_runtime(dest, rbconfig = RbConfig::CONFIG)
274
+ runtime = File.join dest, RUNTIME_DIR
275
+ bindir = rbconfig['bindir']
276
+ FileUtils.mkdir_p runtime
277
+ FileUtils.cp File.join(bindir, rbconfig['LIBRUBY_SO']), runtime
278
+
279
+ # the ruby dll finds these through the manifest in the directory, so
280
+ # the directory has to come along as it is
281
+ builtin = File.join bindir, 'ruby_builtin_dlls'
282
+ FileUtils.cp_r builtin, runtime if File.directory? builtin
283
+
284
+ toolchain_dlls(rbconfig).each {FileUtils.cp _1, runtime}
285
+ File.write File.join(runtime, "#{RUNTIME_DIR}.manifest"),
286
+ render('runtime.manifest.erb', dlls: runtime_dlls(rbconfig))
287
+
288
+ stdlib = File.join dest, 'lib', 'ruby'
289
+ FileUtils.mkdir_p stdlib
290
+ FileUtils.cp_r File.join(rbconfig['rubylibprefix'], rbconfig['ruby_version']), stdlib
291
+ end
292
+
293
+ private
294
+
295
+ def platform_name()
296
+ 'windows'
297
+ end
298
+
299
+ # The ruby dll the executable links is the one the gems are built for.
300
+ #
301
+ def native_gems?()
302
+ true
303
+ end
304
+
305
+ def static_archive(lib)
306
+ File.join lib.root, 'lib', "lib#{lib.name}.a"
307
+ end
308
+
309
+ # The lib directory of the library +name+ in the package.
310
+ #
311
+ def library_lib_dir(name)
312
+ File.join build_dir, 'lib', name, 'lib'
313
+ end
314
+
315
+ def copy_libraries()
316
+ dir = File.join build_dir, 'lib'
317
+ FileUtils.rm_rf dir
318
+ libraries.each {copy_library _1.root, File.join(dir, _1.name)}
319
+ copy_gems {File.join dir, 'gems', _1, 'lib'}
320
+ end
321
+
322
+ def copy_dist()
323
+ dist = dist_path
324
+ FileUtils.rm_rf dist
325
+ FileUtils.mkdir_p dist
326
+ %W[#{target}.exe lib].each do |path|
327
+ FileUtils.cp_r File.join(build_dir, path), dist
328
+ end
329
+ copy_runtime dist
330
+ puts "Created #{dist}"
331
+ end
332
+
333
+ # Puts the MSYS2 toolchain of RubyInstaller on the PATH as gem install
334
+ # does, so that packaging works from any shell without 'ridk enable'.
335
+ #
336
+ def enable_toolchain()
337
+ require 'ruby_installer/runtime'
338
+ RubyInstaller::Runtime.enable_msys_apps
339
+ rescue LoadError
340
+ end
341
+
342
+ # The directory of the compiler, where MSYS2 keeps the DLLs of the
343
+ # toolchain and its packages, rather than PATH, where another toolchain,
344
+ # as the one of git, may come first with its own.
345
+ #
346
+ def toolchain_dir(rbconfig = RbConfig::CONFIG)
347
+ cxx = rbconfig['CXX']&.shellsplit&.first
348
+ compiler = cxx && (File.absolute_path?(cxx) ? cxx : find_executable(cxx))
349
+ compiler && File.dirname(compiler)
350
+ end
351
+
352
+ def system_dll?(dll, rbconfig)
353
+ dll.casecmp?(rbconfig['LIBRUBY_SO']) ||
354
+ dll.match?(/\A(api|ext)-ms-/i) ||# api sets, which windows resolves
355
+ File.file?(File.join ENV['SystemRoot'].to_s, 'System32', dll)
356
+ end
357
+
358
+ def dll_imports(path)
359
+ out, status = Open3.capture2 'objdump', '-p', path
360
+ raise Error, "objdump failed: #{path}" unless status.success?
361
+ out.scan(/DLL Name: (\S+)/).flatten
362
+ end
363
+
364
+ end# Windows
365
+
366
+
367
+ end# Packager
368
+
369
+
370
+ end# Reflex
@@ -1,12 +1,29 @@
1
1
  require 'reflex/packager/extension'
2
2
 
3
+ require 'reflex/packager/data_file'
4
+ require 'reflex/packager/data_compiler'
5
+ require 'reflex/packager/data_loader'
6
+
3
7
  require 'reflex/packager/profile'
4
8
  require 'reflex/packager/config'
5
9
  require 'reflex/packager/platform'
10
+ require 'reflex/packager/gems'
11
+ require 'reflex/packager/app_store_connect'
6
12
  require 'reflex/packager/macos'
13
+ require 'reflex/packager/windows'
14
+ require 'reflex/packager/stores/app_store'
15
+ require 'reflex/packager/stores/itchio'
7
16
  require 'reflex/packager/cli'
8
17
 
9
18
 
10
19
  module Reflex::Packager
11
- PLATFORMS = {macos: MacOS}
20
+
21
+ PLATFORMS = {macos: MacOS, windows: Windows}
22
+
23
+ # The platform the packager runs on, the only one it can build for.
24
+ #
25
+ def self.default_platform()
26
+ Gem.win_platform? ? :windows : :macos
27
+ end
28
+
12
29
  end
@@ -11,13 +11,13 @@ Gem::Specification.new do |s|
11
11
 
12
12
  ext = Reflex::Packager::Extension
13
13
  name = ext.name true
14
- rdocs = glob.call *%w[README]
14
+ rdocs = glob.call(*%w[README])
15
15
 
16
16
  s.name = name
17
17
  s.version = ext.version
18
18
  s.license = 'MIT'
19
- s.summary = 'Package Reflex applications as native app bundles.'
20
- s.description = 'CLI tool to package Reflex applications as native macOS application bundles.'
19
+ s.summary = 'Package Reflex applications as native apps.'
20
+ s.description = 'CLI tool to package Reflex applications as native macOS and Windows applications.'
21
21
  s.authors = %w[xordog]
22
22
  s.email = 'xordog@gmail.com'
23
23
  s.homepage = "https://github.com/xord/reflex-packager"
@@ -25,10 +25,11 @@ Gem::Specification.new do |s|
25
25
  s.platform = Gem::Platform::RUBY
26
26
  s.required_ruby_version = '>= 3.0.0'
27
27
 
28
- s.add_dependency 'xot', '~> 0.4.0'
29
- s.add_dependency 'rucy', '~> 0.4.0'
30
- s.add_dependency 'rays', '~> 0.4.0'
31
- s.add_dependency 'reflexion', '~> 0.6.0'
28
+ s.add_dependency 'fiddle'
29
+ s.add_dependency 'xot', '~> 0.5.0'
30
+ s.add_dependency 'rucy', '~> 0.5.0'
31
+ s.add_dependency 'rays', '~> 0.5.0'
32
+ s.add_dependency 'reflexion', '~> 0.7.0'
32
33
 
33
34
  s.files = `git ls-files`.split $/
34
35
  s.executables = s.files.grep(%r{^bin/}) {|f| File.basename f}
data/test/helper.rb CHANGED
@@ -7,17 +7,53 @@ require 'reflex/extension'
7
7
  require 'reflex/packager'
8
8
 
9
9
  require 'test/unit'
10
+ require 'tempfile'
10
11
  require 'tmpdir'
11
12
 
12
13
  include Xot::Test
13
14
 
14
15
 
16
+ # Makes +dir+ a checkout of the cruby repository with what packaging for macOS
17
+ # reads of it, so that the tests do not fetch the real one.
18
+ #
19
+ def fake_cruby(dir)
20
+ %w[CRuby/include/ruby.h CRuby/lib/ruby/4.0.0/set.rb src/CRuby.m].each do |path|
21
+ path = File.join dir, path
22
+ FileUtils.mkdir_p File.dirname(path)
23
+ FileUtils.touch path
24
+ end
25
+ dir
26
+ end
27
+
28
+ # Runs the block with the method +name+ of +obj+ returning +value+.
29
+ #
30
+ def stub(obj, name, value, &block)
31
+ # removed before defined again, which warns on overwriting
32
+ klass = obj.singleton_class
33
+ hidden = klass.private_method_defined? name, false
34
+ saved = klass.instance_method name if hidden || klass.method_defined?(name, false)
35
+ klass.remove_method name if saved
36
+ klass.define_method(name) {|*| value}
37
+ block.call
38
+ ensure
39
+ klass.remove_method name
40
+ klass.define_method name, saved if saved
41
+ klass.send :private, name if hidden
42
+ end
43
+
44
+ # Runs the block with the environment variables of +env+, nil to unset.
45
+ #
46
+ def with_env(env, &block)
47
+ saved = env.map {|key, _| [key, ENV[key]]}
48
+ env.each {|key, value| value ? ENV[key] = value : ENV.delete(key)}
49
+ block.call
50
+ ensure
51
+ saved.each {|key, value| value ? ENV[key] = value : ENV.delete(key)}
52
+ end
53
+
54
+
15
55
  TEST_PROFILE = Reflex::Packager::Profile.new(
16
- pod: 'Reflex',
17
- git: 'https://github.com/xord/reflex',
18
- version: Reflex::Extension.version,
19
- libraries: %w[Xot Rucy Rays Reflex],
20
- extensions: %w[rays_ext reflex_ext],
56
+ extension: Reflex::Extension,
21
57
  config_files: %w[reflex.yml reflex.yaml],
22
58
  templates: {'main.rb': <<~MAIN, 'reflex.yml': <<~CONFIG})
23
59
  require 'reflex'
@@ -28,3 +64,12 @@ TEST_PROFILE = Reflex::Packager::Profile.new(
28
64
  MAIN
29
65
  name: myapp
30
66
  CONFIG
67
+
68
+ # The environment variables to sign with, to unset by with_env for the tests
69
+ # not to take the ones of the developer.
70
+ #
71
+ ENV_TO_SIGN = [
72
+ *Reflex::Packager::MacOSConfig::CODESIGN_ENV_KEYS.values,
73
+ *Reflex::Packager::MacOS::CODESIGN_P12_ENV_KEYS.values,
74
+ *Reflex::Packager::AppStoreConnect::AUTH_ENV_KEYS.values
75
+ ].to_h {[_1, nil]}
@@ -0,0 +1,108 @@
1
+ require_relative 'helper'
2
+
3
+
4
+ class TestPackagerAppStoreConnect < Test::Unit::TestCase
5
+
6
+ RP = Reflex::Packager
7
+ ASC = RP::AppStoreConnect
8
+
9
+ # App Store Connect with a key of its own, answering the requests to the
10
+ # paths under the notary service by +answers+.
11
+ def asc(answers = {}, &block)
12
+ Dir.mktmpdir do |dir|
13
+ path = File.join dir, 'AuthKey.p8'
14
+ File.write path, OpenSSL::PKey::EC.generate('prime256v1').to_pem
15
+ asc = ASC.new issuer_id: 'ISSUER', key_id: 'KEY', key_path: path
16
+ asc.define_singleton_method(:get) do |path|
17
+ answers.fetch(path.delete_prefix '/notary/v2/').then {_1.is_a?(Proc) ? _1.call : _1}
18
+ end
19
+ asc.define_singleton_method(:sleep) {|*|}
20
+ asc.define_singleton_method(:puts) {|*|}
21
+ block.call asc, path
22
+ end
23
+ end
24
+
25
+ def status(str)
26
+ {'data' => {'attributes' => {'status' => str}}}
27
+ end
28
+
29
+ def test_credentials()
30
+ keys = ASC::AUTH_ENV_KEYS
31
+ with_env ENV_TO_SIGN do
32
+ assert_nil ASC.credentials
33
+ assert_raise(RP::Error) {ASC.new}
34
+ end
35
+ creds = {issuer_id: 'ISSUER', key_id: 'KEY', key_path: '/a.p8'}
36
+ with_env ENV_TO_SIGN.merge(creds.to_h {[keys[_1], _2]}) do
37
+ assert_equal creds, ASC.credentials
38
+ end
39
+ with_env ENV_TO_SIGN.merge(keys[:key_id] => '', keys[:key_path] => '/a.p8') do
40
+ error = assert_raise(RP::Error) {ASC.credentials}
41
+ assert_include error.message, keys[:key_id]
42
+ end
43
+
44
+ # by the content of the .p8 file, as a secret of CI
45
+ creds = {issuer_id: 'ISSUER', key_id: 'KEY', key_p8: 'P8'}
46
+ with_env ENV_TO_SIGN.merge(creds.to_h {[keys[_1], _2]}) do
47
+ assert_equal creds, ASC.credentials
48
+ end
49
+ with_env ENV_TO_SIGN.merge(keys[:issuer_id] => 'ISSUER', keys[:key_id] => 'KEY') do
50
+ error = assert_raise(RP::Error) {ASC.credentials}
51
+ assert_include error.message, "#{keys[:key_p8]} or #{keys[:key_path]}"
52
+ end
53
+ end
54
+
55
+ def test_token()
56
+ asc do |asc, path|
57
+ header, payload, signature = asc.__send__(:token).split('.')
58
+ decode = -> str {(str + '=' * (-str.size % 4)).tr('-_', '+/').unpack1 'm0'}
59
+ assert_equal 'KEY', JSON.parse(decode[header])['kid']
60
+ assert_equal 'ISSUER', JSON.parse(decode[payload])['iss']
61
+
62
+ # the raw r and s, which OpenSSL takes in DER
63
+ r, s = decode[signature].unpack('a32a32').map {OpenSSL::BN.new _1, 2}
64
+ der = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(r), OpenSSL::ASN1::Integer(s)]).to_der
65
+ assert OpenSSL::PKey.read(File.read path).verify('SHA256', der, "#{header}.#{payload}")
66
+
67
+ # by the content of the .p8 file
68
+ p8 = File.read path
69
+ header, payload, signature = ASC.new(issuer_id: 'ISSUER', key_id: 'KEY', key_p8: p8)
70
+ .__send__(:token).split('.')
71
+ r, s = decode[signature].unpack('a32a32').map {OpenSSL::BN.new _1, 2}
72
+ der = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(r), OpenSSL::ASN1::Integer(s)]).to_der
73
+ assert OpenSSL::PKey.read(p8).verify('SHA256', der, "#{header}.#{payload}")
74
+ end
75
+ end
76
+
77
+ def test_wait_until_notarized()
78
+ statuses = [status('In Progress'), status('Accepted')]
79
+ asc 'submissions/abc' => -> {statuses.shift} do |asc, _|
80
+ assert_nothing_raised {asc.wait_until_notarized 'ABC'} # in lower case
81
+ assert_empty statuses
82
+ end
83
+
84
+ asc 'submissions/abc' => status('In Progress') do |asc, _|
85
+ error = assert_raise(RP::Error) {asc.wait_until_notarized 'abc', timeout: -1}
86
+ assert_include error.message, 'timed out'
87
+ end
88
+
89
+ asc 'submissions/abc' => {} do |asc, _|
90
+ error = assert_raise(RP::Error) {asc.wait_until_notarized 'abc'}
91
+ assert_include error.message, 'unknown status of the notarization: nil'
92
+ end
93
+
94
+ log = {'issues' => [{'path' => 'a.app/b', 'message' => 'not signed'}]}
95
+ answers = {
96
+ 'submissions/abc' => status('Invalid'),
97
+ 'submissions/abc/logs' => {'data' => {'attributes' => {'developerLogUrl' => 'https://log'}}}
98
+ }
99
+ asc answers do |asc, _|
100
+ stub Net::HTTP, :get, log.to_json do
101
+ error = assert_raise(RP::Error) {asc.wait_until_notarized 'abc'}
102
+ assert_include error.message, 'Invalid'
103
+ assert_include error.message, 'a.app/b: not signed'
104
+ end
105
+ end
106
+ end
107
+
108
+ end# TestPackagerAppStoreConnect
@@ -63,10 +63,27 @@ class TestPackagerCLI < Test::Unit::TestCase
63
63
  def test_package()
64
64
  tmpdir do
65
65
  capture {cli.create ['myapp']}
66
- cli.package ['--generate-only', 'myapp']
67
- %w[project.yml Podfile src/main.mm app/main.rb].each do |f|
68
- assert File.exist?("myapp/build/macos/#{f}"), "missing #{f}"
66
+ Dir.mktmpdir do |cruby|
67
+ saved, ENV['CRUBY_PATH'] = ENV['CRUBY_PATH'], fake_cruby(cruby)
68
+ cli.package ['--generate-only', '--platform', 'macos', 'myapp']
69
+ ensure
70
+ ENV['CRUBY_PATH'] = saved
69
71
  end
72
+ %w[project.yml src/main.mm app/main.rb].each do |f|
73
+ assert File.exist?("myapp/.build/macos/project/#{f}"), "missing #{f}"
74
+ end
75
+ end
76
+
77
+ tmpdir do
78
+ capture {cli.create ['myapp']}
79
+ cli.package ['--generate-only', '--platform', 'windows', 'myapp']
80
+ %w[src/main.cpp src/app.manifest lib/boot.rb lib/app/main.rb lib/reflex/lib/reflex.rb].each do |f|
81
+ assert File.exist?("myapp/.build/windows/#{f}"), "missing #{f}"
82
+ end
83
+
84
+ cli.package ['--generate-only', '--platform', 'windows', '--pack', 'myapp']
85
+ assert_include Dir.children('myapp/.build/windows/lib/app'), 'data.bin'
86
+ assert_not_include Dir.children('myapp/.build/windows/lib/app'), 'main.rb'
70
87
  end
71
88
 
72
89
  tmpdir do
@@ -74,7 +91,25 @@ class TestPackagerCLI < Test::Unit::TestCase
74
91
  assert_raise(RP::Error) do
75
92
  cli.package ['--generate-only', '--platform', 'unknown', 'myapp']
76
93
  end
94
+ error = assert_raise(RP::Error) do
95
+ cli.package ['--notarize-submit', '--notarize-export', 'myapp']
96
+ end
97
+ assert_include error.message, 'only one of'
98
+
99
+ error = assert_raise(RP::Error) do
100
+ cli.package ['--notarize-wait', 'ID', '--appstore', 'myapp']
101
+ end
102
+ assert_include error.message, '--notarize-wait'
103
+ error = assert_raise(RP::Error) do
104
+ cli.package ['--platform', 'windows', '--appstore', 'myapp']
105
+ end
106
+ assert_include error.message, 'only for macos'
77
107
  end
78
108
  end
79
109
 
110
+ def test_default_platform()
111
+ stub(Gem, :win_platform?, true) {assert_equal :windows, RP.default_platform}
112
+ stub(Gem, :win_platform?, false) {assert_equal :macos, RP.default_platform}
113
+ end
114
+
80
115
  end# TestPackagerCLI