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.
- checksums.yaml +4 -4
- data/.github/workflows/package.yml +310 -0
- data/.github/workflows/release-gem.yml +2 -49
- data/.github/workflows/tag.yml +11 -28
- data/.github/workflows/test.yml +16 -37
- data/.gitignore +2 -0
- data/ChangeLog.md +28 -0
- data/Gemfile.lock +26 -0
- data/README.md +232 -27
- data/Rakefile +21 -0
- data/VERSION +1 -1
- data/bin/reflex +2 -32
- data/examples/hello/icon.png +0 -0
- data/examples/hello/main.rb +18 -0
- data/examples/hello/reflex.yml +3 -0
- data/lib/reflex/packager/app_store_connect.rb +162 -0
- data/lib/reflex/packager/cli.rb +42 -9
- data/lib/reflex/packager/config.rb +233 -36
- data/lib/reflex/packager/data_compiler.rb +59 -0
- data/lib/reflex/packager/data_file.rb +183 -0
- data/lib/reflex/packager/data_loader.rb +127 -0
- data/lib/reflex/packager/gems.rb +282 -0
- data/lib/reflex/packager/library.rb +120 -0
- data/lib/reflex/packager/macos.rb +592 -94
- data/lib/reflex/packager/platform.rb +289 -10
- data/lib/reflex/packager/profile.rb +38 -26
- data/lib/reflex/packager/profiles/reflex.rb +43 -0
- data/lib/reflex/packager/rake.rb +115 -0
- data/lib/reflex/packager/stores/app_store.rb +60 -0
- data/lib/reflex/packager/stores/itchio.rb +53 -0
- data/lib/reflex/packager/templates/macos/AppStore.entitlements.erb +8 -0
- data/lib/reflex/packager/templates/macos/AppStoreExportOptions.plist.erb +14 -0
- data/lib/reflex/packager/templates/macos/ExportOptions.plist.erb +20 -0
- data/lib/reflex/packager/templates/macos/InfoPlist.strings.erb +5 -0
- data/lib/reflex/packager/templates/macos/boot.rb.erb +29 -0
- data/lib/reflex/packager/templates/macos/compile.rb.erb +7 -0
- data/lib/reflex/packager/templates/macos/main.mm.erb +10 -7
- data/lib/reflex/packager/templates/macos/project.yml.erb +60 -3
- data/lib/reflex/packager/templates/windows/app.manifest.erb +25 -0
- data/lib/reflex/packager/templates/windows/app.rc.erb +38 -0
- data/lib/reflex/packager/templates/windows/boot.rb.erb +32 -0
- data/lib/reflex/packager/templates/windows/main.cpp.erb +47 -0
- data/lib/reflex/packager/templates/windows/runtime.manifest.erb +7 -0
- data/lib/reflex/packager/windows.rb +370 -0
- data/lib/reflex/packager.rb +18 -1
- data/reflex-packager.gemspec +8 -7
- data/test/helper.rb +50 -5
- data/test/test_packager_app_store_connect.rb +108 -0
- data/test/test_packager_cli.rb +38 -3
- data/test/test_packager_config.rb +166 -27
- data/test/test_packager_data_file.rb +96 -0
- data/test/test_packager_data_loader.rb +114 -0
- data/test/test_packager_library.rb +81 -0
- data/test/test_packager_macos.rb +714 -86
- data/test/test_packager_platform.rb +16 -0
- data/test/test_packager_rake.rb +165 -0
- data/test/test_packager_stores_app_store.rb +66 -0
- data/test/test_packager_stores_itchio.rb +38 -0
- data/test/test_packager_windows.rb +685 -0
- metadata +72 -15
- data/.github/workflows/utils.rb +0 -127
- data/lib/reflex/packager/templates/macos/Podfile.erb +0 -30
data/test/test_packager_macos.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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[
|
|
43
|
-
|
|
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
|
-
|
|
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 '
|
|
67
|
-
assert_equal '
|
|
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
|
|
85
|
-
assert_include
|
|
86
|
-
assert_include
|
|
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 /
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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, '@"
|
|
167
|
-
assert_include str, '
|
|
168
|
-
assert_include str, '
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 -----------------------------------------------------
|