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
@@ -1,5 +1,8 @@
1
1
  require 'erb'
2
2
  require 'fileutils'
3
+ require 'open3'
4
+ require 'shellwords'
5
+ require 'tmpdir'
3
6
 
4
7
 
5
8
  module Reflex
@@ -14,8 +17,36 @@ module Reflex
14
17
  #
15
18
  class Platform
16
19
 
17
- def initialize(config, verbose: false)
18
- @config, @verbose = config, verbose
20
+ class << self
21
+
22
+ # Copies the app in +dir+ to +dest+, with the Ruby scripts in its data
23
+ # file in place of them, compiled by DataCompiler with the Ruby running
24
+ # this, and the other files as they are.
25
+ #
26
+ # Unless +compile+, the data file is left out, for the package to
27
+ # have it written by the Ruby in it.
28
+ #
29
+ def pack_app(dir, dest, compile: true)
30
+ files = Dir.glob('**/*', base: dir).select {File.file? File.join(dir, _1)}.sort
31
+ scripts = DataCompiler.scripts dir
32
+
33
+ FileUtils.mkdir_p dest
34
+ DataCompiler.write dir, File.join(dest, DataLoader::DATA_FILE) if compile
35
+
36
+ (files - scripts).each do |name|
37
+ path = File.join dest, name
38
+ FileUtils.mkdir_p File.dirname(path)
39
+ FileUtils.cp File.join(dir, name), path
40
+ end
41
+ end
42
+
43
+ end# self
44
+
45
+ def initialize(
46
+ config, verbose: false, pack: false, notarize: [], generic: false, appstore: false)
47
+
48
+ @config, @verbose, @pack, @notarize = config, verbose, pack, notarize || []
49
+ @generic, @appstore = generic, appstore
19
50
  end
20
51
 
21
52
  attr_reader :config
@@ -28,6 +59,40 @@ module Reflex
28
59
  @verbose
29
60
  end
30
61
 
62
+ # Whether to put the files of the app together in a data file, which
63
+ # the package reads them from, as for a release.
64
+ #
65
+ def pack?()
66
+ @pack
67
+ end
68
+
69
+ # The steps of the notarization, having Apple check the app for the
70
+ # system to open it without warnings when downloaded, as for a release:
71
+ # :submit to send the app, and :export to export the app notarized,
72
+ # waiting for it.
73
+ #
74
+ def notarize()
75
+ @notarize
76
+ end
77
+
78
+ def notarize?()
79
+ !@notarize.empty?
80
+ end
81
+
82
+ # Whether to package the generic app, for no store in particular: when
83
+ # asked, or notarized, which only the generic one is, or when no store
84
+ # is asked for.
85
+ #
86
+ def generic?()
87
+ @generic || notarize? || !appstore?
88
+ end
89
+
90
+ # Whether to package the app for the Mac App Store.
91
+ #
92
+ def appstore?()
93
+ @appstore
94
+ end
95
+
31
96
  # Package the application as a distributable bundle.
32
97
  #
33
98
  # @param [Boolean] generate_only generate the project files but do
@@ -38,17 +103,218 @@ module Reflex
38
103
  build unless generate_only
39
104
  end
40
105
 
106
+ # Generates the files to build the app from, in build_dir.
107
+ #
108
+ def generate()
109
+ raise "#{self.class} must implement #{__method__}"
110
+ end
111
+
112
+ # Builds the app from the files generated, into dist_path.
113
+ #
114
+ def build()
115
+ raise "#{self.class} must implement #{__method__}"
116
+ end
117
+
118
+ # The directory of what the packager makes for the platform.
119
+ #
120
+ def platform_dir()
121
+ File.join config.dir, '.build', platform_name
122
+ end
123
+
124
+ # The directory the app is generated and built in.
125
+ #
126
+ def build_dir()
127
+ platform_dir
128
+ end
129
+
130
+ # The variant of the package, which the directory of the package is
131
+ # named after: generic, for no store in particular.
132
+ #
133
+ def variant()
134
+ 'generic'
135
+ end
136
+
137
+ # The directory of the package of the variant, in dist/.
138
+ #
139
+ def dist_dir(variant = self.variant)
140
+ File.join config.dir, 'dist', variant
141
+ end
142
+
143
+ # The packaged app in dist_dir.
144
+ #
145
+ def dist_path()
146
+ File.join dist_dir, target
147
+ end
148
+
149
+ # The packages of the app for the stores, as {store => path}, each of
150
+ # which --<store> packages, in the dist directory of the store.
151
+ #
152
+ def store_package_paths()
153
+ {}
154
+ end
155
+
156
+ # The command to zip the packaged app into archive_path.
157
+ #
158
+ def archive_command()
159
+ raise "#{self.class} must implement #{__method__}"
160
+ end
161
+
162
+ # The zip of the packaged app to hand out, in dist_dir.
163
+ #
164
+ def archive_path()
165
+ File.join dist_dir, "#{target}-#{config.version}-#{archive_platform}.zip"
166
+ end
167
+
168
+ # The platform and the architecture the packaged app runs on, as the
169
+ # name of its zip tells them.
170
+ #
171
+ def archive_platform()
172
+ platform_name
173
+ end
174
+
175
+ # Returns the app name without characters unsafe for target, scheme or
176
+ # file names.
177
+ #
178
+ def target()
179
+ config.name.gsub(/[^A-Za-z0-9_\-]+/, '').then {_1.empty? ? 'App' : _1}
180
+ end
181
+
182
+ # The script the app starts with, in the app directory: the boot script
183
+ # of the profile, or the main script of the app.
184
+ #
185
+ def start_script()
186
+ profile.boot_main || config.main
187
+ end
188
+
41
189
  private
42
190
 
43
- def render(template)
191
+ # The name of the platform, for the build directory, the templates and
192
+ # the zip of the app.
193
+ #
194
+ def platform_name()
195
+ raise "#{self.class} must implement #{__method__}"
196
+ end
197
+
198
+ # Left out when copying a library: a gem build leaves its binaries in
199
+ # lib/, while a package links the extensions in, and the Ruby of a
200
+ # windows package would even prefer a rays_ext.so on the load path to
201
+ # the one linked in.
202
+ #
203
+ BINARY_EXTS = %w[.so .dll .a .o .bundle]
204
+
205
+ # Copies the Ruby code of a library and what it reads at run time into
206
+ # +dest+: lib/ without the binaries, VERSION and res/.
207
+ #
208
+ def copy_library(root, dest)
209
+ copy_tree File.join(root, 'lib'), File.join(dest, 'lib')
210
+ %w[VERSION res].map {File.join root, _1}.select {File.exist? _1}.each do |path|
211
+ FileUtils.mkdir_p dest
212
+ FileUtils.cp_r path, dest
213
+ end
214
+ end
215
+
216
+ def copy_tree(src, dest)
217
+ Dir.glob('**/*', base: src).each do |path|
218
+ from = File.join src, path
219
+ next if File.directory?(from) || BINARY_EXTS.include?(File.extname path)
220
+ to = File.join dest, path
221
+ FileUtils.mkdir_p File.dirname(to)
222
+ FileUtils.cp from, to
223
+ end
224
+ end
225
+
226
+ # The files of the app, as they are, for any platform.
227
+ #
228
+ def app_dir()
229
+ File.join config.dir, '.build', 'app'
230
+ end
231
+
232
+ # Copies the files of the app to app_dir, and to +dir+ in the build
233
+ # directory, as they are, or packed with what reads its data file in
234
+ # the reflex library of the package, if the app is to be packed.
235
+ #
236
+ def copy_app_files(dir = 'app')
237
+ FileUtils.rm_rf app_dir
238
+ FileUtils.mkdir_p app_dir
239
+ config.app_files.each do |file|
240
+ dest = File.join app_dir, file
241
+ FileUtils.mkdir_p File.dirname(dest)
242
+ FileUtils.cp File.join(config.dir, file), dest
243
+ end
244
+ File.write File.join(app_dir, profile.boot_main), profile.boot if
245
+ profile.boot_main && profile.boot
246
+
247
+ dir = File.join build_dir, dir
248
+ FileUtils.rm_rf dir
249
+ FileUtils.mkdir_p dir
250
+ if pack?
251
+ raise Error, 'a packed app needs reflex in its libraries' unless
252
+ libraries.any? {_1.name == 'reflex'}
253
+ Platform.pack_app app_dir, dir, compile: compile_on_generate?
254
+ loader = File.join library_lib_dir('reflex'), 'reflex', 'packager'
255
+ FileUtils.mkdir_p loader
256
+ %w[data_file.rb data_loader.rb].each {FileUtils.cp File.join(__dir__, _1), loader}
257
+ else
258
+ FileUtils.cp_r File.join(app_dir, '.'), dir
259
+ end
260
+ end
261
+
262
+ # Whether the scripts of a packed app are compiled when generating, by
263
+ # the Ruby running this, which is the one the package runs them on.
264
+ #
265
+ def compile_on_generate?()
266
+ true
267
+ end
268
+
269
+ def write(path, content)
270
+ path = File.expand_path path, build_dir
271
+ FileUtils.mkdir_p File.dirname(path)
272
+ File.write path, content
273
+ end
274
+
275
+ # Renders the template with the packager's methods, and +vars+ as
276
+ # local variables.
277
+ #
278
+ def render(template, **vars)
44
279
  path = File.join TEMPLATES_DIR, platform_name, template
45
- ERB.new(File.read(path), trim_mode: '-').result binding
280
+ b = template_binding
281
+ vars.each {|name, value| b.local_variable_set name, value}
282
+ ERB.new(File.read(path), trim_mode: '-').result b
46
283
  end
47
284
 
48
- def run(*cmd, chdir:, env: {})
49
- puts "==> #{cmd.join ' '}"
50
- return if system env, *cmd, chdir: chdir
51
- raise Error, "command failed: #{cmd.join ' '}"
285
+ # A binding with the packager's methods and nothing else: a template
286
+ # sees every local variable of the method its binding is made in.
287
+ #
288
+ def template_binding()
289
+ binding
290
+ end
291
+
292
+ # Runs +cmd+, dropping what it prints if +quiet+ unless verbose, though
293
+ # not the errors it tells. The command is run as it is, not split by
294
+ # spaces even if it is only a path, as of the app named with spaces.
295
+ #
296
+ def run(*cmd, chdir: nil, env: {}, quiet: false)
297
+ puts "==> #{cmd.shelljoin}"
298
+ out = quiet && !verbose? ? {out: File::NULL} : {}
299
+ ok = with_dir(chdir) {system env, [cmd.first, cmd.first], *cmd.drop(1), chdir: _1, **out}
300
+ return if ok
301
+ raise Error, "command failed: #{cmd.shelljoin}"
302
+ end
303
+
304
+ # Runs +cmd+, returning what it prints and whether it succeeded, for
305
+ # the caller to tell what failed.
306
+ #
307
+ def capture(*cmd, chdir: nil)
308
+ out, status = with_dir(chdir) {Open3.capture2e(*cmd, chdir: _1)}
309
+ [out, status.success?]
310
+ end
311
+
312
+ # Calls the block with +dir+ for a command to run in, or with no +dir+,
313
+ # a temporary directory removed after, not for the command to depend
314
+ # on where the packager is run from.
315
+ #
316
+ def with_dir(dir, &block)
317
+ dir ? block.call(dir) : Dir.mktmpdir(&block)
52
318
  end
53
319
 
54
320
  def check_tools(tools)
@@ -60,8 +326,21 @@ module Reflex
60
326
  end
61
327
 
62
328
  def executable?(name)
63
- ENV['PATH'].to_s.split(File::PATH_SEPARATOR)
64
- .any? {|dir| File.executable? File.join(dir, name.to_s)}
329
+ !!find_executable(name)
330
+ end
331
+
332
+ # Windows finds 'g++' as 'g++.exe' through PATHEXT, which is separated
333
+ # by ';' whatever the platform.
334
+ #
335
+ def find_executable(name)
336
+ exts = ['', *ENV['PATHEXT'].to_s.split(';')]
337
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).each do |dir|
338
+ exts.each do |ext|
339
+ path = File.join dir, "#{name}#{ext}"
340
+ return path if File.file?(path) && File.executable?(path)
341
+ end
342
+ end
343
+ nil
65
344
  end
66
345
 
67
346
  end# Platform
@@ -1,48 +1,64 @@
1
+ require 'reflex/packager/library'
2
+
3
+
1
4
  module Reflex
2
5
 
3
6
 
4
7
  module Packager
5
8
 
6
9
 
7
- # Describes the runtime a packaged app embeds: the umbrella pod that
8
- # provides the native build, the extensions and libraries registered with
9
- # CRuby, and the scaffold for the 'new' command.
10
+ # Describes the runtime a packaged app embeds: the gem it is made of, the
11
+ # libraries and extensions the gem depends on, and the scaffold for the
12
+ # 'new' command.
10
13
  #
11
14
  # The packager itself is runtime-agnostic; each gem (reflex, rubysketch,
12
15
  # ...) supplies its own profile and reuses this packager as the engine.
13
16
  #
14
17
  class Profile
15
18
 
16
- # @param [String] pod umbrella pod name (e.g. 'Reflex')
17
- # @param [String] git umbrella pod git repository
18
- # @param [String] version umbrella pod version (for the tag)
19
- # @param [Array<String>] libraries ruby lib bundles to add to load path
20
- # @param [Array<String>] extensions native exts to register (Init_<name>)
19
+ # @param [Module] extension Extension of the gem (e.g. Reflex::Extension)
21
20
  # @param [Array<String>] config_files config file names, preferred first
22
21
  # @param [Hash{String=>String}] templates 'new' scaffold files ({filename => content})
23
- # @param [String, nil] command CLI command name (default: pod_key)
22
+ # @param [String, nil] command CLI command name (default: the name in lower case)
24
23
  # @param [String, nil] boot boot script overrides config main (default: nil)
25
24
  #
26
- def initialize(
27
- pod:, git:, version:, libraries:, extensions:, config_files:, templates:,
28
- command: nil, boot: nil)
29
-
30
- @pod = pod
31
- @git = git
32
- @version = version
33
- @libraries = libraries
34
- @extensions = extensions
25
+ def initialize(extension:, config_files:, templates:, command: nil, boot: nil)
26
+ @extension = extension
35
27
  @config_files = config_files
36
28
  @templates = templates
37
29
  @command = command
38
30
  @boot = boot
39
31
  end
40
32
 
41
- attr_reader :pod, :git, :version, :libraries, :extensions, :config_files,
42
- :templates, :boot
33
+ attr_reader :extension, :config_files, :templates, :boot
34
+
35
+ # The name of the gem (e.g. 'Reflex').
36
+ #
37
+ def name()
38
+ extension.name
39
+ end
40
+
41
+ def version()
42
+ extension.version
43
+ end
44
+
45
+ # The libraries the gem depends on, and the gem itself, each after the
46
+ # ones it depends on.
47
+ #
48
+ # @return [Array<Library>] libraries
49
+ #
50
+ def libraries()
51
+ @libraries ||= Library.collect extension.root_dir
52
+ end
53
+
54
+ # Native extensions to register (Init_<name>).
55
+ #
56
+ def extensions()
57
+ libraries.filter_map(&:extension)
58
+ end
43
59
 
44
60
  def command()
45
- @command || pod_key
61
+ @command || name.downcase
46
62
  end
47
63
 
48
64
  def main()
@@ -53,12 +69,8 @@ module Reflex
53
69
  @boot ? '__reflex_main__.rb' : nil
54
70
  end
55
71
 
56
- def pod_key()
57
- pod.downcase.to_sym
58
- end
59
-
60
72
  def bundle_id_prefix()
61
- "org.xord.#{pod_key}"
73
+ "org.xord.#{name.downcase}"
62
74
  end
63
75
 
64
76
  end# Profile
@@ -0,0 +1,43 @@
1
+ require 'reflex/extension'
2
+ require 'reflex/packager/profile'
3
+
4
+
5
+ module Reflex
6
+
7
+
8
+ module Packager
9
+
10
+
11
+ # The profile to package an app of Reflex with.
12
+ #
13
+ REFLEX = Profile.new(
14
+ extension: Reflex::Extension,
15
+ config_files: %w[reflex.yml reflex.yaml],
16
+ templates: {'main.rb': <<~MAIN, 'reflex.yml': <<~CONFIG})
17
+ require 'reflex'
18
+
19
+ view = Reflex::View.new
20
+ def view.on_draw(e)
21
+ e.painter.font nil, 48
22
+ e.painter.text 'hello, world!', 100, 100
23
+ end
24
+
25
+ win = Reflex::Window.new
26
+ win.title = '{{name}}'
27
+ win.frame = [100, 100, 400, 300]
28
+ win.add view
29
+ win.show
30
+
31
+ Reflex.start
32
+ MAIN
33
+ name: {{name}}
34
+ version: 1.0.0
35
+ #bundle_id: org.xord.reflex.example.{{name_id}}
36
+ #icon: icon.png
37
+ CONFIG
38
+
39
+
40
+ end# Packager
41
+
42
+
43
+ end# Reflex
@@ -0,0 +1,115 @@
1
+ require 'rbconfig'
2
+ require 'xot/rake'
3
+ require 'reflex/packager'
4
+ require 'reflex/packager/profiles/reflex'
5
+
6
+
7
+ module Reflex
8
+
9
+
10
+ module Packager
11
+
12
+
13
+ module Rake
14
+
15
+ def package_application()
16
+ config = Config.load REFLEX, '.'
17
+ platform = PLATFORMS[Packager.default_platform].new config, pack: true
18
+ app_path = platform.dist_path
19
+ archive_path = platform.archive_path
20
+ tag = "v#{config.version}"
21
+ srcs = [
22
+ *config.app_files,
23
+ *REFLEX.config_files,
24
+ 'VERSION',
25
+ 'Gemfile.lock'
26
+ ].select {File.file? _1}
27
+
28
+ relative = -> path {path.delete_prefix "#{config.dir}/"}
29
+ reflex_package = -> *args do
30
+ reflex = File.expand_path '../../../bin/reflex', __dir__
31
+ sh RbConfig.ruby, reflex, 'package', '--pack', *args, '.'
32
+ end
33
+
34
+ desc "package the application into #{relative[app_path]}"
35
+ alias_task :package => app_path
36
+
37
+ desc "archive the application into #{relative[archive_path]}"
38
+ alias_task :archive => archive_path
39
+
40
+ namespace :package do
41
+ platform.store_package_paths.each do |store, path|
42
+ desc "package the application for #{store} into #{relative[path]}"
43
+ alias_task store => path
44
+ end
45
+ end
46
+
47
+ desc "release the application archived or packaged on GitHub, by the tag #{tag}"
48
+ task :release do
49
+ ref = ENV['GITHUB_REF_NAME']
50
+ raise "#{ref} is not the tag of the version, #{tag}" if ref && ref != tag
51
+ paths = [archive_path, *platform.store_package_paths.values].select {File.file? _1}
52
+ raise 'nothing to release, archive or package the application first' if paths.empty?
53
+ release_on_github(*paths, tag: tag, notes: release_notes(tag))
54
+ end
55
+
56
+ namespace :upload do
57
+ desc "upload #{relative[archive_path]} to itch.io"
58
+ task :itchio => archive_path do
59
+ project = config.upload.itchio_project ||
60
+ raise("no upload.itchio.project in #{REFLEX.config_files.first}")
61
+ Stores::Itchio.new(project).upload archive_path,
62
+ channel: Packager.default_platform.to_s, version: config.version
63
+ end
64
+
65
+ platform.store_package_paths[:appstore]&.then do |pkg_path|
66
+ desc "upload #{relative[pkg_path]} to App Store Connect"
67
+ task :appstore => pkg_path do
68
+ Stores::AppStore.new.upload pkg_path, platform: Packager.default_platform.to_s
69
+ end
70
+ end
71
+ end
72
+
73
+ task :clean do
74
+ rm_rf File.dirname(platform.dist_dir)
75
+ end
76
+
77
+ task :clobber => :clean do
78
+ rm_rf File.dirname(platform.platform_dir)
79
+ end
80
+
81
+ file app_path => srcs do
82
+ reflex_package.call
83
+ end
84
+
85
+ file archive_path => app_path do
86
+ sh(*platform.archive_command)
87
+ end
88
+
89
+ platform.store_package_paths.each do |store, path|
90
+ file path => srcs do
91
+ reflex_package.call "--#{store}"
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def release_notes(tag)
99
+ commit = git('-C', Reflex::Extension.root_dir, 'rev-parse', 'HEAD', err: File::NULL).strip
100
+ [
101
+ Xot::ChangeLog.new.changes(tag),
102
+ ("Built with xord/all@#{commit}." unless commit.empty?)
103
+ ].compact.join("\n\n")
104
+ end
105
+
106
+ end# Rake
107
+
108
+
109
+ end# Packager
110
+
111
+
112
+ end# Reflex
113
+
114
+
115
+ include Reflex::Packager::Rake
@@ -0,0 +1,60 @@
1
+ require 'shellwords'
2
+
3
+
4
+ module Reflex
5
+
6
+
7
+ module Packager
8
+
9
+
10
+ # The stores to hand out the app at.
11
+ #
12
+ module Stores
13
+
14
+
15
+ # The App Store, which the app is uploaded to by altool of Xcode, as an
16
+ # installer package, by the API key of App Store Connect in the
17
+ # environment variables, as notarizing on CI is.
18
+ #
19
+ class AppStore
20
+
21
+ def initialize(app_store_connect = AppStoreConnect.new)
22
+ @app_store_connect = app_store_connect
23
+ end
24
+
25
+ # Uploads +path+, the installer package of the app for +platform+, as
26
+ # macos, which App Store Connect tells the app and the version of by
27
+ # its Info.plist.
28
+ #
29
+ def upload(path, platform:)
30
+ raise Error, "nothing to upload: '#{path}'" unless File.file? path
31
+
32
+ asc = @app_store_connect
33
+ asc.with_key_path do |key_path|
34
+ cmd = [
35
+ 'xcrun', 'altool', '--upload-package', path,
36
+ '--platform', platform,
37
+ '--api-key', asc.credentials[:key_id],
38
+ '--api-issuer', asc.credentials[:issuer_id],
39
+ '--p8-file-path', key_path
40
+ ]
41
+ puts "==> #{cmd.shelljoin}"
42
+ case system(*cmd)
43
+ when nil
44
+ raise Error, 'xcrun not found, install Xcode'
45
+ when false
46
+ raise Error, "failed to upload: #{cmd.shelljoin}"
47
+ end
48
+ end
49
+ end
50
+
51
+ end# AppStore
52
+
53
+
54
+ end# Stores
55
+
56
+
57
+ end# Packager
58
+
59
+
60
+ end# Reflex