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,127 @@
1
+ require_relative 'data_file'
2
+
3
+
4
+ module Reflex
5
+
6
+
7
+ module Packager
8
+
9
+
10
+ # Lets require, require_relative and load read the Ruby scripts of an app
11
+ # from its data file, as the instruction sequences the packager compiled
12
+ # them into, named as the scripts but with .rbc, in place of the scripts,
13
+ # which the app directory has none of. The other files of the app are in
14
+ # it as they are.
15
+ #
16
+ # The scripts are compiled with their paths relative to the app directory,
17
+ # which a package runs in, so that __FILE__ and __dir__ have them as they
18
+ # are, and the others, as the libraries, are required as they are.
19
+ #
20
+ # A package carries this file in the reflex library of it, with DataFile
21
+ # beside, which is all it requires.
22
+ #
23
+ module DataLoader
24
+
25
+ DATA_FILE = 'data.bin'
26
+
27
+ class << self
28
+
29
+ # Reads the scripts of the app in +dir+ from the data file in it.
30
+ #
31
+ # @param [String] dir the app directory, which has data.bin
32
+ #
33
+ def setup(dir)
34
+ # as the current directory has it, for the paths to compare
35
+ @dir = File.realpath dir
36
+ @data = DataFile.new File.join(@dir, DATA_FILE)
37
+
38
+ version = @data.read('.ruby-version') || '(unknown)'
39
+ raise "the data file is for Ruby #{version}, not #{RUBY_VERSION}" if
40
+ version != RUBY_VERSION
41
+
42
+ Kernel.prepend Require
43
+ end
44
+
45
+ attr_reader :dir, :data
46
+
47
+ # The name of the script in the data file for +feature+ to require, or
48
+ # nil.
49
+ #
50
+ def find(feature)
51
+ feature = feature.to_s
52
+ return script(File.expand_path feature) if
53
+ File.absolute_path?(feature) || feature.start_with?('./', '../')
54
+
55
+ $LOAD_PATH.each do |load_path|
56
+ name = script File.expand_path(feature, load_path.to_s)
57
+ return name if name
58
+ end
59
+ nil
60
+ end
61
+
62
+ # The name of the script in the data file at +path+, or nil.
63
+ #
64
+ def script(path)
65
+ path = File.expand_path path, @dir
66
+ return nil unless path.start_with? "#{@dir}/"
67
+ name = path[(@dir.size + 1)..].sub(/\.rb\z/, '') + '.rbc'
68
+ @data.include?(name) ? name : nil
69
+ end
70
+
71
+ # Requires the script +name+ in the data file unless it is already.
72
+ #
73
+ def require_script(name)
74
+ path = File.join @dir, name.sub(/\.rbc\z/, '.rb')
75
+ return false if $LOADED_FEATURES.include? path
76
+
77
+ # before running it, as require does, for a script it requires to
78
+ # require it back
79
+ $LOADED_FEATURES << path
80
+ begin
81
+ load_script name
82
+ rescue Exception
83
+ $LOADED_FEATURES.delete path
84
+ raise
85
+ end
86
+ true
87
+ end
88
+
89
+ def load_script(name)
90
+ RubyVM::InstructionSequence.load_from_binary(@data.read name).eval
91
+ end
92
+
93
+ end# self
94
+
95
+ module Require
96
+
97
+ private
98
+
99
+ def require(feature)
100
+ name = DataLoader.find feature
101
+ name ? DataLoader.require_script(name) : super
102
+ end
103
+
104
+ def require_relative(feature)
105
+ location = caller_locations(1, 1).first
106
+ base = File.expand_path(location.absolute_path || location.path, DataLoader.dir)
107
+ path = File.expand_path feature, File.dirname(base)
108
+ name = DataLoader.script path
109
+ name ? DataLoader.require_script(name) : require(path)
110
+ end
111
+
112
+ def load(file, wrap = false)
113
+ name = DataLoader.script File.expand_path(file)
114
+ return super unless name
115
+ DataLoader.load_script name
116
+ true
117
+ end
118
+
119
+ end# Require
120
+
121
+ end# DataLoader
122
+
123
+
124
+ end# Packager
125
+
126
+
127
+ end# Reflex
@@ -0,0 +1,282 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'open3'
4
+ require 'rbconfig'
5
+ require 'reflex/packager/library'
6
+
7
+
8
+ module Reflex
9
+
10
+
11
+ module Packager
12
+
13
+
14
+ # Gems shipped with an app as they are: the ones of the Gemfile of the
15
+ # app, and the standard gems, which were default gems once. Included by a
16
+ # platform, which puts them where its load path finds them, and has the
17
+ # profile and the config they are looked for with.
18
+ #
19
+ module Gems
20
+
21
+ # Default gems that became bundled gems, as the NEWS of each Ruby says.
22
+ # Gem::BUNDLED_GEMS::SINCE, which Ruby 3.3 and later have, misses some
23
+ # of them; it adds the ones of Ruby versions newer than this list.
24
+ #
25
+ FORMER_DEFAULT_GEMS = [
26
+ *%w[rexml rss], # 3.0
27
+ *%w[net-ftp net-imap net-pop net-smtp matrix prime debug], # 3.1
28
+ *%w[racc], # 3.3
29
+ *%w[mutex_m getoptlong base64 bigdecimal observer abbrev
30
+ resolv-replace rinda drb nkf syslog csv], # 3.4
31
+ *%w[ostruct pstore benchmark logger rdoc win32ole irb reline
32
+ readline fiddle win32-registry] # 4.0
33
+ ]
34
+
35
+ # Former default gems for development rather than for an app to run
36
+ # with, left out of the standard gems; with what they depend on, as
37
+ # prism and rbs for irb and rdoc, they would add over 10 MB. An app
38
+ # that runs with them lists them in its Gemfile.
39
+ #
40
+ DEVELOPMENT_GEMS = %w[irb rdoc debug]
41
+
42
+ # Extensions of the binaries a gem with a native extension has.
43
+ #
44
+ NATIVE_EXTS = %w[.so .bundle .dll]
45
+
46
+ # Names of the libraries and the gems, put on the load path in this
47
+ # order: a library before the ones it depends on, as bundler puts them,
48
+ # and the libraries before the gems, which the runtime is not to be
49
+ # hidden by.
50
+ #
51
+ def lib_names()
52
+ [*library_names.reverse, *gem_names]
53
+ end
54
+
55
+ # Names of the gems copy_gems copies, put on the load path in this order.
56
+ #
57
+ def gem_names()
58
+ [*('bundler' if gemfile), *gem_dirs.keys]
59
+ end
60
+
61
+ # Require paths of the gems by name: the ones of the Gemfile of the app,
62
+ # then the standard gems.
63
+ #
64
+ def gem_dirs()
65
+ @gem_dirs ||= standard_gem_dirs
66
+ .reject {|name, _| app_gem_dirs.key? name}
67
+ .then {app_gem_dirs.merge _1}
68
+ end
69
+
70
+ # Require paths of the gems in the default group of the Gemfile of the
71
+ # app, by name. A gem with a native extension, where it is not
72
+ # supported, is left out if the Ruby a platform embeds has it.
73
+ #
74
+ def app_gem_dirs()
75
+ @app_gem_dirs ||= gemfile_specs.each.with_object({}) do |spec, dirs|
76
+ name, paths = spec.values_at 'name', 'require_paths'
77
+ next unless shipped? spec
78
+ if !native_gems? && native_gem?(paths)
79
+ next if embedded_gem? name
80
+ raise Error, "gem '#{name}' has a native extension, which is not supported"
81
+ end
82
+ dirs[name] = paths
83
+ end
84
+ end
85
+
86
+ # Require paths of the standard gems installed, by name, leaving out the
87
+ # ones with a native extension where it is not supported.
88
+ #
89
+ def standard_gem_dirs()
90
+ return {} unless standard_gems?
91
+ @standard_gem_dirs ||= standard_specs.each.with_object({}) do |spec, dirs|
92
+ name, paths = spec.values_at 'name', 'require_paths'
93
+ next unless shipped? spec
94
+ next if !native_gems? && native_gem?(paths)
95
+ dirs[name] = paths
96
+ end
97
+ end
98
+
99
+ # The libraries the app runs on: the ones of the profile, and the ones in
100
+ # the Gemfile of the app, with what they depend on.
101
+ #
102
+ def libraries()
103
+ @libraries ||= Library.collect(*gemfile_library_roots, known: profile.libraries)
104
+ .then {profile.libraries + _1}
105
+ end
106
+
107
+ # Native extensions to register (Init_<name>).
108
+ #
109
+ def extensions()
110
+ libraries.filter_map(&:extension)
111
+ end
112
+
113
+ def library_names()
114
+ libraries.map(&:name)
115
+ end
116
+
117
+ private
118
+
119
+ # Whether to ship the standard gems, which the Ruby a platform embeds may
120
+ # have in its standard library already.
121
+ #
122
+ def standard_gems?()
123
+ true
124
+ end
125
+
126
+ # Whether the Ruby a platform embeds has the gem +name+ in its standard
127
+ # library, which the app runs with in place of the one of its Gemfile
128
+ # that cannot be shipped.
129
+ #
130
+ def embedded_gem?(name)
131
+ false
132
+ end
133
+
134
+ # Whether a gem with a native extension can be shipped.
135
+ #
136
+ def native_gems?()
137
+ false
138
+ end
139
+
140
+ def native_gem?(paths)
141
+ paths.any? do |path|
142
+ Dir.glob('**/*', base: path).any? {NATIVE_EXTS.include? File.extname(_1)}
143
+ end
144
+ end
145
+
146
+ # Leaves out bundler, default gems, which are in the standard library
147
+ # already, and the libraries, known by their extension.rb.
148
+ #
149
+ def shipped?(spec)
150
+ name, default_gem, paths = spec.values_at 'name', 'default_gem', 'require_paths'
151
+ return false if name == 'bundler' || default_gem
152
+ library_names.none? do |lib|
153
+ paths.any? {File.file? File.join(_1, lib, 'extension.rb')}
154
+ end
155
+ end
156
+
157
+ # Copies the gems as they are into the directories the block returns for
158
+ # their names, which the platform puts on the load path.
159
+ #
160
+ def copy_gems(&lib_dir)
161
+ gem_dirs.each do |name, paths|
162
+ dest = lib_dir.call name
163
+ FileUtils.mkdir_p dest
164
+ paths.each {FileUtils.cp_r File.join(_1, '.'), dest}
165
+ end
166
+ write_bundler_setup lib_dir.call('bundler') if gemfile
167
+ end
168
+
169
+ # An app with a Gemfile may require bundler/setup, which would find the
170
+ # bundler of the standard library, and no Gemfile to set up with. The
171
+ # gems are on the load path already, so the one found first does
172
+ # nothing.
173
+ #
174
+ def write_bundler_setup(dir)
175
+ path = File.join dir, 'bundler', 'setup.rb'
176
+ FileUtils.mkdir_p File.dirname(path)
177
+ File.write path, "# the gems of the Gemfile are on the load path already\n"
178
+ end
179
+
180
+ # Directories of the gems in the Gemfile which are libraries, known by
181
+ # their extension.rb, and not the ones of the profile; the ones RUBYLIB
182
+ # points to come first, as for the libraries of the profile.
183
+ #
184
+ def gemfile_library_roots()
185
+ names = profile.libraries.map(&:name)
186
+ gemfile_specs.filter_map do |spec|
187
+ name = Library.library_name spec['root']
188
+ next if !name || names.include?(name)
189
+ Library.source_root(spec['name']) || spec['root']
190
+ end
191
+ end
192
+
193
+ def gemfile()
194
+ path = File.join config.dir, 'Gemfile'
195
+ File.file?(path) ? path : nil
196
+ end
197
+
198
+ SPECS_TO_JSON = <<~RUBY
199
+ def specs_to_json(specs)
200
+ $stdout.write JSON.generate(specs.map {|s|
201
+ {
202
+ name: s.name,
203
+ root: s.full_gem_path,
204
+ default_gem: s.default_gem?,
205
+ require_paths: s.full_require_paths
206
+ }
207
+ })
208
+ end
209
+ RUBY
210
+
211
+ GEMFILE_SPECS = <<~RUBY
212
+ require 'bundler'
213
+ require 'json'
214
+ #{SPECS_TO_JSON}
215
+ Bundler.ui = Bundler::UI::Silent.new
216
+ begin
217
+ specs_to_json Bundler.definition.specs_for([:default])
218
+ rescue Bundler::BundlerError => e
219
+ abort e.message
220
+ end
221
+ RUBY
222
+
223
+ STANDARD_SPECS = <<~RUBY
224
+ require 'json'
225
+ #{SPECS_TO_JSON}
226
+ names, excluded = JSON.parse ARGV[0]
227
+ begin
228
+ require 'bundled_gems'
229
+ names |= Gem::BUNDLED_GEMS::SINCE.keys
230
+ rescue LoadError
231
+ end
232
+ names -= excluded
233
+
234
+ # with what they depend on, which may be neither a default gem nor
235
+ # one of these
236
+ specs = {}
237
+ deps = names.map {Gem::Dependency.new _1}
238
+ until deps.empty?
239
+ dep = deps.shift
240
+ next if specs.key? dep.name
241
+ specs[dep.name] = begin
242
+ dep.to_spec
243
+ rescue Gem::LoadError
244
+ end
245
+ deps.concat specs[dep.name].runtime_dependencies if specs[dep.name]
246
+ end
247
+ specs_to_json specs.values.compact
248
+ RUBY
249
+
250
+ # The gems of the default group of the Gemfile of the app.
251
+ #
252
+ def gemfile_specs()
253
+ return [] unless gemfile
254
+ run_ruby GEMFILE_SPECS, env: {'BUNDLE_GEMFILE' => gemfile},
255
+ error: 'failed to resolve the Gemfile (bundle install?)'
256
+ end
257
+
258
+ # The standard gems installed, and the gems they depend on.
259
+ #
260
+ def standard_specs()
261
+ run_ruby STANDARD_SPECS, JSON.generate([FORMER_DEFAULT_GEMS, DEVELOPMENT_GEMS]),
262
+ error: 'failed to look for the standard gems'
263
+ end
264
+
265
+ # Runs the script in a ruby of its own, out of the reach of any bundler
266
+ # this process runs under, which would hide the gems out of its bundle.
267
+ #
268
+ def run_ruby(script, *args, env: {}, error:)
269
+ env = ENV.keys.grep(/\ABUNDLER?_/).to_h {[_1, nil]}
270
+ .merge 'RUBYOPT' => nil, **env
271
+ out, err, status = Open3.capture3 env, RbConfig.ruby, '-e', script, *args
272
+ raise Error, "#{error}: #{err.strip}" unless status.success?
273
+ JSON.parse out
274
+ end
275
+
276
+ end# Gems
277
+
278
+
279
+ end# Packager
280
+
281
+
282
+ end# Reflex
@@ -0,0 +1,120 @@
1
+ require 'rubygems'
2
+
3
+
4
+ module Reflex
5
+
6
+
7
+ module Packager
8
+
9
+
10
+ # A library a packaged app runs on: a gem with lib/<name>/extension.rb, as
11
+ # xot, rays and reflex are, whose Ruby code a package bundles and whose
12
+ # extension, if it has lib/<name>/ext.rb, a package links.
13
+ #
14
+ class Library
15
+
16
+ # @param [String] name the directory of the library under lib/
17
+ # @param [String] root the directory of the gem
18
+ #
19
+ def initialize(name, root)
20
+ @name, @root = name, root
21
+ end
22
+
23
+ attr_reader :name, :root
24
+
25
+ # The extension to register (Init_<extension>), or nil.
26
+ #
27
+ def extension()
28
+ return nil unless File.file? File.join(root, 'lib', name, 'ext.rb')
29
+ "#{name.tr '-', '_'}_ext"
30
+ end
31
+
32
+ def to_s()
33
+ name
34
+ end
35
+
36
+ # Returns the libraries the gems at +roots+ depend on, as their gemspecs
37
+ # have them, and the gems themselves, each after the ones it depends on,
38
+ # leaving out the +known+ ones.
39
+ #
40
+ # A gem is looked for in the directories on the load path first, as
41
+ # RUBYLIB points to the ones of a repository being worked on, and then in
42
+ # the installed gems.
43
+ #
44
+ # @param [Array<String>] roots the directories of the gems
45
+ # @param [Array<Library>] known the libraries collected already
46
+ #
47
+ # @return [Array<Library>] libraries
48
+ #
49
+ def self.collect(*roots, known: [])
50
+ libraries, visited, sources = [], known.to_h {[_1.root, true]}, source_roots
51
+ visit = -> (root, spec) do
52
+ name = library_name root
53
+ next if !name || visited[root]
54
+ visited[root] = true
55
+ spec.runtime_dependencies.each {|dep| find_gem(dep, sources)&.then {visit.call(*_1)}}
56
+ libraries << new(name, root)
57
+ end
58
+ roots.map {File.expand_path _1}.each {visit.call _1, spec_of(_1)}
59
+ libraries
60
+ end
61
+
62
+ # The directory of the gem +name+ on the load path, which is not an
63
+ # installed one, as RUBYLIB points to, or nil.
64
+ #
65
+ def self.source_root(name)
66
+ source_roots[name]
67
+ end
68
+
69
+ # The directory under lib/ with extension.rb, or nil.
70
+ #
71
+ def self.library_name(root)
72
+ path = Dir.glob(File.join root, 'lib', '*', 'extension.rb').first
73
+ path && File.basename(File.dirname path)
74
+ end
75
+
76
+ # Returns [root, spec] of the gem +dep+ asks for, or nil.
77
+ #
78
+ def self.find_gem(dep, sources)
79
+ root = sources[dep.name]
80
+ return [root, spec_of(root)] if root
81
+ spec = dep.to_spec
82
+ [spec.full_gem_path, spec]
83
+ rescue Gem::LoadError
84
+ nil
85
+ end
86
+
87
+ # The gems on the load path which are not installed ones, by name.
88
+ #
89
+ def self.source_roots()
90
+ $LOAD_PATH
91
+ .map {File.dirname File.expand_path(_1.to_s)}
92
+ .uniq
93
+ .reject {installed? _1}
94
+ .select {Dir.glob(File.join _1, '*.gemspec').any?}
95
+ .to_h {[spec_of(_1).name, _1]}
96
+ end
97
+
98
+ def self.spec_of(root)
99
+ installed = Gem::Specification.find {_1.full_gem_path == root}
100
+ return installed if installed
101
+ path = Dir.glob(File.join root, '*.gemspec').first or
102
+ raise Error, "no gemspec in '#{root}'"
103
+ # a gemspec may run git ls-files, which lists the files of the repository it is run in
104
+ Dir.chdir(root) {Gem::Specification.load path} or
105
+ raise Error, "failed to load '#{path}'"
106
+ end
107
+
108
+ def self.installed?(root)
109
+ Gem.path.any? {root.start_with? File.join(_1, 'gems', '')}
110
+ end
111
+
112
+ private_class_method :find_gem, :source_roots, :spec_of, :installed?
113
+
114
+ end# Library
115
+
116
+
117
+ end# Packager
118
+
119
+
120
+ end# Reflex