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
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'rbconfig'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'tmpdir'
|
|
1
5
|
require 'reflex/packager/platform'
|
|
6
|
+
require 'reflex/packager/gems'
|
|
7
|
+
require 'reflex/packager/app_store_connect'
|
|
2
8
|
|
|
3
9
|
|
|
4
10
|
module Reflex
|
|
@@ -9,87 +15,222 @@ module Reflex
|
|
|
9
15
|
|
|
10
16
|
# Packages a Reflex application as a macOS application bundle.
|
|
11
17
|
#
|
|
18
|
+
# The native libraries (xot, rucy, rays, ...) are compiled from the
|
|
19
|
+
# sources in their installed gems, as their Rakefiles build them, into an
|
|
20
|
+
# executable that embeds CRuby, which comes as a prebuilt xcframework
|
|
21
|
+
# from a checkout of the cruby repository.
|
|
22
|
+
#
|
|
12
23
|
class MacOS < Platform
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
include Gems
|
|
26
|
+
|
|
27
|
+
CRUBY_GIT = 'https://github.com/xord/cruby'
|
|
28
|
+
|
|
29
|
+
# The CRuby used unless the config or CRUBY_PATH names another one.
|
|
30
|
+
#
|
|
31
|
+
CRUBY_VERSION = '4.0.701'
|
|
32
|
+
|
|
33
|
+
# The identity of the certificate of Developer ID in the keychain, which
|
|
34
|
+
# the full name of the certificate starts with.
|
|
35
|
+
#
|
|
36
|
+
DEVELOPER_ID = 'Developer ID Application'
|
|
37
|
+
|
|
38
|
+
# The environment variables of the certificate of Developer ID to sign
|
|
39
|
+
# with, put into a keychain only while signing, as on CI: the .p12 file
|
|
40
|
+
# in base64, and the password of it.
|
|
41
|
+
#
|
|
42
|
+
CODESIGN_P12_ENV_KEYS = {
|
|
43
|
+
p12: 'APPLE_CODESIGN_P12',
|
|
44
|
+
password: 'APPLE_CODESIGN_P12_PASSWORD'
|
|
45
|
+
}
|
|
15
46
|
|
|
16
47
|
TOOLS = {
|
|
48
|
+
git: 'install Xcode command line tools: xcode-select --install',
|
|
17
49
|
xcodegen: 'install with: brew install xcodegen',
|
|
18
|
-
pod: 'install with: brew install cocoapods (or gem install cocoapods)',
|
|
19
50
|
xcodebuild: 'install Xcode and run: sudo xcode-select --switch /Applications/Xcode.app'
|
|
20
51
|
}
|
|
21
52
|
|
|
53
|
+
# A packed app compiles its scripts when built, and a notarized one is
|
|
54
|
+
# sent to Apple when exported, as the one for the Mac App Store is
|
|
55
|
+
# exported from its archive, so they have to be built; the app only
|
|
56
|
+
# exported after its notarization is not built, but taken from the
|
|
57
|
+
# archive sent.
|
|
58
|
+
#
|
|
59
|
+
def package(generate_only: false)
|
|
60
|
+
case
|
|
61
|
+
# what can not be done together
|
|
62
|
+
when generate_only && (pack? || notarize? || appstore?)
|
|
63
|
+
'a packed, notarized or Mac App Store app has to be built on macos'
|
|
64
|
+
when appstore? && notarize == [:export]
|
|
65
|
+
'only exporting the notarized app, which is the generic one, makes no app' +
|
|
66
|
+
' for the Mac App Store'
|
|
67
|
+
# what macos does not take
|
|
68
|
+
when config.build_version.split('.').size > 3
|
|
69
|
+
'macos takes three numbers at most for the build version, as' +
|
|
70
|
+
" CFBundleVersion: '#{config.build_version}'"
|
|
71
|
+
# what the config lacks
|
|
72
|
+
when !config.macos.codesign_team_id &&
|
|
73
|
+
(notarize.include?(:submit) || appstore? || codesign_p12)
|
|
74
|
+
'notarizing, the Mac App Store and the certificate by' +
|
|
75
|
+
" #{CODESIGN_P12_ENV_KEYS[:p12]} need the team to sign with," +
|
|
76
|
+
" by macos.codesign.team_id or #{MacOSConfig::CODESIGN_ENV_KEYS[:team_id]}"
|
|
77
|
+
when appstore? && !config.macos.category
|
|
78
|
+
'the Mac App Store needs the category of the app, by macos.category'
|
|
79
|
+
# how the app is signed
|
|
80
|
+
when (notarize.include?(:submit) || @generic) && !signing_to_distribute?
|
|
81
|
+
'notarizing and --generic are only for the app signed with Developer ID,' +
|
|
82
|
+
' by the certificate Apple manages, with no identity to sign with,' +
|
|
83
|
+
" or by #{DEVELOPER_ID}"
|
|
84
|
+
when generic? && notarize != [:export] && !generate_only &&
|
|
85
|
+
cloud_signing? && AppStoreConnect.credentials
|
|
86
|
+
# FB16835802 of Apple, which may be fixed some day
|
|
87
|
+
'the API key is refused to sign with the certificate Apple manages,' +
|
|
88
|
+
" give the certificate by #{CODESIGN_P12_ENV_KEYS[:p12]}"
|
|
89
|
+
end&.tap {raise Error, _1}
|
|
90
|
+
notarize == [:export] ? export_notarized_app : super
|
|
91
|
+
end
|
|
92
|
+
|
|
22
93
|
def generate()
|
|
94
|
+
# the bundles first, which what reads the data file is copied into
|
|
95
|
+
copy_bundles
|
|
23
96
|
copy_app_files
|
|
24
97
|
generate_icon if config.icon
|
|
25
|
-
write 'project.yml',
|
|
26
|
-
write '
|
|
27
|
-
write '
|
|
98
|
+
write 'project.yml', render('project.yml.erb')
|
|
99
|
+
write 'src/main.mm', render('main.mm.erb')
|
|
100
|
+
write 'boot.rb', render('boot.rb.erb')
|
|
101
|
+
write export_options_path, render('ExportOptions.plist.erb') if signing_to_distribute?
|
|
102
|
+
if appstore?
|
|
103
|
+
write appstore_entitlements_path, render('AppStore.entitlements.erb')
|
|
104
|
+
write appstore_export_options_path, render('AppStoreExportOptions.plist.erb')
|
|
105
|
+
end
|
|
106
|
+
write_localizations
|
|
28
107
|
end
|
|
29
108
|
|
|
30
109
|
def build()
|
|
31
110
|
check_tools TOOLS
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
run 'xcodebuild',
|
|
37
|
-
'-workspace', "#{target}.xcworkspace",
|
|
38
|
-
'-scheme', target,
|
|
39
|
-
'-configuration', 'Release',
|
|
40
|
-
'-destination', 'platform=macOS',
|
|
41
|
-
'-derivedDataPath', 'DerivedData',
|
|
42
|
-
'build',
|
|
43
|
-
chdir: build_dir
|
|
44
|
-
copy_app
|
|
111
|
+
run 'xcodegen', 'generate', *('--quiet' unless verbose?), chdir: build_dir
|
|
112
|
+
compile_scripts if pack?
|
|
113
|
+
build_generic if generic?
|
|
114
|
+
export_appstore_pkg if appstore?
|
|
45
115
|
end
|
|
46
116
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
117
|
+
# The Xcode project, with what it is made of and what it builds, apart
|
|
118
|
+
# from the archives of the app in platform_dir.
|
|
49
119
|
#
|
|
50
|
-
def
|
|
51
|
-
|
|
120
|
+
def build_dir()
|
|
121
|
+
File.join platform_dir, 'project'
|
|
52
122
|
end
|
|
53
123
|
|
|
54
|
-
|
|
55
|
-
|
|
124
|
+
# The generic one is signed with Developer ID, to be handed out, and
|
|
125
|
+
# the one signed otherwise is adhoc, only to be run where it is built.
|
|
126
|
+
#
|
|
127
|
+
def variant()
|
|
128
|
+
signing_to_distribute? || notarize? ? super : 'adhoc'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The name of the .app and its executable, which Finder, the Dock and
|
|
132
|
+
# /Applications show: the name of the app as it is, with spaces, as Mac
|
|
133
|
+
# apps are named, unlike the target.
|
|
134
|
+
#
|
|
135
|
+
def app_name()
|
|
136
|
+
config.name
|
|
56
137
|
end
|
|
57
138
|
|
|
58
|
-
def
|
|
59
|
-
File.join
|
|
139
|
+
def dist_path()
|
|
140
|
+
File.join dist_dir, "#{app_name}.app"
|
|
60
141
|
end
|
|
61
142
|
|
|
62
|
-
#
|
|
143
|
+
# The installer package of the app for the Mac App Store, to upload to
|
|
144
|
+
# App Store Connect.
|
|
63
145
|
#
|
|
64
|
-
def
|
|
65
|
-
|
|
146
|
+
def appstore_pkg_path()
|
|
147
|
+
File.join dist_dir('appstore'), "#{target}-#{config.version}.pkg"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def store_package_paths()
|
|
151
|
+
{appstore: appstore_pkg_path}
|
|
66
152
|
end
|
|
67
153
|
|
|
68
|
-
#
|
|
154
|
+
# The command to zip the packaged app into archive_path, by ditto,
|
|
155
|
+
# which keeps the symlinks and the signature of the app.
|
|
69
156
|
#
|
|
70
|
-
def
|
|
71
|
-
|
|
157
|
+
def archive_command()
|
|
158
|
+
['ditto', '-c', '-k', '--keepParent', dist_path, archive_path]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def archive_platform()
|
|
162
|
+
archs = config.macos.archs
|
|
163
|
+
"macos-#{archs.size == 1 ? archs.first : 'universal'}"
|
|
72
164
|
end
|
|
73
165
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
166
|
+
# Libraries built from native code: the ones whose gem has an
|
|
167
|
+
# extension to build, even if only for its tests as xot and rucy.
|
|
76
168
|
#
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
'CRuby' => pod_ref(:cruby, {git: GIT_CRUBY}),
|
|
81
|
-
p.pod => pod_ref(p.pod_key, {git: p.git, tag: "v#{p.version}"})
|
|
82
|
-
}
|
|
169
|
+
def native_libraries()
|
|
170
|
+
libraries.select {File.file? File.join(_1.root, 'ext', _1.name, 'extconf.rb')}
|
|
83
171
|
end
|
|
84
172
|
|
|
85
|
-
# Returns
|
|
86
|
-
#
|
|
87
|
-
# pods under PODS_ROOT and does not run their prepare_command.
|
|
173
|
+
# Returns {library => {srcs:, vendor_srcs:, incdirs:, defs:}} of the native
|
|
174
|
+
# libraries, with absolute paths, as their Rakefiles have them.
|
|
88
175
|
#
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
176
|
+
def build_infos()
|
|
177
|
+
@build_infos ||= native_libraries.to_h do |lib|
|
|
178
|
+
info = rake_build_info lib.root
|
|
179
|
+
ext = File.join lib.root, 'ext', lib.name
|
|
180
|
+
info[:srcs] += Dir.glob(File.join ext, '*.{c,cpp,m,mm}').sort if lib.extension
|
|
181
|
+
[lib, info]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Frameworks the extensions link, as the Makefiles their gem builds
|
|
186
|
+
# leave have them.
|
|
187
|
+
#
|
|
188
|
+
def frameworks()
|
|
189
|
+
native_libraries.flat_map do |lib|
|
|
190
|
+
makefile = File.join lib.root, 'ext', lib.name, 'Makefile'
|
|
191
|
+
unless File.file? makefile
|
|
192
|
+
raise Error, "no Makefile of '#{lib}' in '#{lib.root}' (was the gem built?)" if
|
|
193
|
+
lib.extension
|
|
194
|
+
next [] # xot and rucy build their extensions only for their tests
|
|
195
|
+
end
|
|
196
|
+
MacOS.makefile_frameworks File.read(makefile)
|
|
197
|
+
end.uniq
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The names in the -framework options of the ldflags of a Makefile mkmf
|
|
201
|
+
# writes.
|
|
202
|
+
#
|
|
203
|
+
def self.makefile_frameworks(makefile)
|
|
204
|
+
line = makefile.lines.find {_1.start_with? 'ldflags'} or return []
|
|
205
|
+
line.scan(/-framework\s+(\S+)/).flatten
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def header_search_paths()
|
|
209
|
+
build_infos.values.flat_map {_1[:incdirs]}.uniq.reject {vendor? _1}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def system_header_search_paths()
|
|
213
|
+
[
|
|
214
|
+
*build_infos.values.flat_map {_1[:incdirs]}.uniq.select {vendor? _1},
|
|
215
|
+
File.join(cruby_dir, 'CRuby', 'include')
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The checkout of the cruby repository with the CRuby it built or
|
|
220
|
+
# downloaded: CRUBY_PATH, which overrides the config while working on
|
|
221
|
+
# CRuby, or the path in the config, or else the version in the config
|
|
222
|
+
# or the default one, fetched into the build directory.
|
|
223
|
+
#
|
|
224
|
+
def cruby_dir()
|
|
225
|
+
@cruby_dir ||= begin
|
|
226
|
+
env, cruby = ENV['CRUBY_PATH'], config.macos.cruby
|
|
227
|
+
case
|
|
228
|
+
when env then check_cruby File.expand_path(env)
|
|
229
|
+
when !cruby then fetch_cruby CRUBY_VERSION
|
|
230
|
+
when version?(cruby) then fetch_cruby cruby
|
|
231
|
+
else check_cruby File.expand_path(cruby, config.dir)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
93
234
|
end
|
|
94
235
|
|
|
95
236
|
# Returns sips command lines to resize the icon into an iconset.
|
|
@@ -104,34 +245,412 @@ module Reflex
|
|
|
104
245
|
|
|
105
246
|
private
|
|
106
247
|
|
|
248
|
+
# Prints what the Rakefile in the current directory builds with.
|
|
249
|
+
#
|
|
250
|
+
RAKE_BUILD_INFO = <<~RUBY
|
|
251
|
+
require 'json'
|
|
252
|
+
require 'rake'
|
|
253
|
+
Rake.application.init 'rake', []
|
|
254
|
+
load 'Rakefile'
|
|
255
|
+
puts JSON.generate(
|
|
256
|
+
srcs: srcs_map.keys,
|
|
257
|
+
vendor_srcs: vendor_srcs_map.keys,
|
|
258
|
+
incdirs: inc_dirs,
|
|
259
|
+
defs: make_cppflags_defs(defs))
|
|
260
|
+
RUBY
|
|
261
|
+
|
|
107
262
|
def platform_name()
|
|
108
263
|
'macos'
|
|
109
264
|
end
|
|
110
265
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
266
|
+
# The app compiles its scripts with the CRuby in it, when built.
|
|
267
|
+
#
|
|
268
|
+
def compile_on_generate?()
|
|
269
|
+
false
|
|
270
|
+
end
|
|
114
271
|
|
|
115
|
-
|
|
116
|
-
|
|
272
|
+
def xcodebuild(action = 'build', *args)
|
|
273
|
+
run 'xcodebuild', *('-quiet' unless verbose?),
|
|
274
|
+
'-project', "#{target}.xcodeproj",
|
|
275
|
+
'-scheme', target,
|
|
276
|
+
'-configuration', 'Release',
|
|
277
|
+
'-destination', 'generic/platform=macOS',
|
|
278
|
+
'-derivedDataPath', 'DerivedData',
|
|
279
|
+
action, *args,
|
|
280
|
+
chdir: build_dir
|
|
117
281
|
end
|
|
118
282
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
283
|
+
# The identity to sign with, which is of Developer ID by the certificate
|
|
284
|
+
# APPLE_CODESIGN_P12 gives, with no other identity.
|
|
285
|
+
#
|
|
286
|
+
def codesign_identity()
|
|
287
|
+
identity = config.macos.codesign_identity
|
|
288
|
+
identity == '-' && codesign_p12 ? DEVELOPER_ID : identity
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# The certificate APPLE_CODESIGN_P12 gives, as {data:, password:} with
|
|
292
|
+
# the data of the .p12 file decoded, or nil.
|
|
293
|
+
#
|
|
294
|
+
def codesign_p12()
|
|
295
|
+
p12, password = ENV.values_at(*CODESIGN_P12_ENV_KEYS.values)
|
|
296
|
+
return nil if p12.to_s.empty?
|
|
297
|
+
{data: p12.unpack1('m'), password: password.to_s}
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Whether the app is signed by the certificate of Developer ID in the
|
|
301
|
+
# keychain, as on CI, where Apple refuses the API key to sign with the
|
|
302
|
+
# one it manages.
|
|
303
|
+
#
|
|
304
|
+
def keychain_signing?()
|
|
305
|
+
config.macos.codesign_team_id && codesign_identity.start_with?(DEVELOPER_ID)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Whether the app is signed by the certificate of the team that Apple
|
|
309
|
+
# manages for it, which needs no certificate in the keychain: the team
|
|
310
|
+
# without the identity to sign with. The certificate signs the app
|
|
311
|
+
# only when exported from its archive.
|
|
312
|
+
#
|
|
313
|
+
def cloud_signing?()
|
|
314
|
+
config.macos.codesign_team_id && codesign_identity == '-'
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Whether the app is signed to be distributed, with Developer ID, which
|
|
318
|
+
# signs the app exported from its archive.
|
|
319
|
+
#
|
|
320
|
+
def signing_to_distribute?()
|
|
321
|
+
keychain_signing? || cloud_signing?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Builds the generic app into the dist directory: as it is built, or
|
|
325
|
+
# exported signed with Developer ID, or notarized too.
|
|
326
|
+
#
|
|
327
|
+
def build_generic()
|
|
328
|
+
case
|
|
329
|
+
when !signing_to_distribute?
|
|
330
|
+
xcodebuild
|
|
331
|
+
copy_app
|
|
332
|
+
when !notarize?
|
|
333
|
+
export_app
|
|
334
|
+
copy_app exported_app_path
|
|
335
|
+
else
|
|
336
|
+
export_app
|
|
337
|
+
puts "Submitted for the notarization: #{submission_id}"
|
|
338
|
+
export_notarized_app if notarize.include? :export
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Archives the app and exports it signed with Developer ID, by the
|
|
343
|
+
# account of Xcode, or by the API key of App Store Connect that
|
|
344
|
+
# APPLE_API_ISSUER_ID, APPLE_API_KEY_ID and APPLE_API_KEY_P8 or
|
|
345
|
+
# APPLE_API_KEY_PATH tell.
|
|
346
|
+
#
|
|
347
|
+
def export_app()
|
|
348
|
+
xcodebuild 'archive', '-archivePath', xcarchive_path
|
|
349
|
+
FileUtils.rm_rf File.dirname(exported_app_path)
|
|
350
|
+
with_codesign_keychain do
|
|
351
|
+
with_auth_options do |auth|
|
|
352
|
+
run 'xcodebuild', *('-quiet' unless verbose?),
|
|
353
|
+
'-exportArchive',
|
|
354
|
+
'-archivePath', xcarchive_path,
|
|
355
|
+
'-exportOptionsPlist', export_options_path,
|
|
356
|
+
'-exportPath', File.dirname(exported_app_path),
|
|
357
|
+
'-allowProvisioningUpdates',
|
|
358
|
+
*auth,
|
|
359
|
+
chdir: build_dir
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# The ID of the notarization the archive was submitted for last, in
|
|
365
|
+
# lower case as the notary service takes it.
|
|
366
|
+
#
|
|
367
|
+
def submission_id()
|
|
368
|
+
dir = File.join xcarchive_path, 'Submissions'
|
|
369
|
+
id = Dir.exist?(dir) && Dir.children(dir).max_by {File.mtime File.join(dir, _1)}
|
|
370
|
+
raise Error, "no submission for the notarization in '#{xcarchive_path}'" unless id
|
|
371
|
+
id.downcase
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Exports the app stapled with the ticket of the notarization into the
|
|
375
|
+
# dist directory, waiting for Apple to check the app sent and to issue
|
|
376
|
+
# the ticket, which takes some minutes.
|
|
377
|
+
#
|
|
378
|
+
def export_notarized_app()
|
|
379
|
+
raise Error, "no archive sent to be notarized: '#{xcarchive_path}'" unless
|
|
380
|
+
File.directory? xcarchive_path
|
|
381
|
+
|
|
382
|
+
FileUtils.rm_rf File.dirname(notarized_app_path)
|
|
383
|
+
with_auth_options do |auth|
|
|
384
|
+
cmd = [
|
|
385
|
+
'xcodebuild', '-exportNotarizedApp',
|
|
386
|
+
'-archivePath', xcarchive_path,
|
|
387
|
+
'-exportPath', File.dirname(notarized_app_path),
|
|
388
|
+
*auth
|
|
389
|
+
]
|
|
390
|
+
deadline = Time.now + AppStoreConnect::NOTARIZATION_TIMEOUT
|
|
391
|
+
loop do
|
|
392
|
+
out, ok = capture(*cmd)
|
|
393
|
+
break if ok
|
|
394
|
+
raise Error, "failed to export the notarized app:\n#{out}" unless
|
|
395
|
+
out.match?(/processing and not ready|Ticket not found/)
|
|
396
|
+
raise Error, 'timed out waiting for the notarization' if Time.now > deadline
|
|
397
|
+
puts 'waiting for the notarization...'
|
|
398
|
+
sleep AppStoreConnect::NOTARIZATION_INTERVAL
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
copy_app notarized_app_path
|
|
122
402
|
end
|
|
123
403
|
|
|
124
|
-
|
|
125
|
-
|
|
404
|
+
# Archives the app in the sandbox, which the Mac App Store needs, and
|
|
405
|
+
# exports it into the installer package to upload, signed by the
|
|
406
|
+
# certificates of the team that Apple manages, as the profile too.
|
|
407
|
+
#
|
|
408
|
+
# The archive is of its own, apart from the generic one out of the
|
|
409
|
+
# sandbox, which shares its data with the ones of the other stores.
|
|
410
|
+
#
|
|
411
|
+
def export_appstore_pkg()
|
|
412
|
+
xcodebuild 'archive', '-archivePath', appstore_xcarchive_path,
|
|
413
|
+
"CODE_SIGN_ENTITLEMENTS=#{appstore_entitlements_path}"
|
|
414
|
+
FileUtils.rm_rf File.dirname(appstore_exported_pkg_path)
|
|
415
|
+
with_auth_options do |auth|
|
|
416
|
+
run 'xcodebuild', *('-quiet' unless verbose?),
|
|
417
|
+
'-exportArchive',
|
|
418
|
+
'-archivePath', appstore_xcarchive_path,
|
|
419
|
+
'-exportOptionsPlist', appstore_export_options_path,
|
|
420
|
+
'-exportPath', File.dirname(appstore_exported_pkg_path),
|
|
421
|
+
'-allowProvisioningUpdates',
|
|
422
|
+
*auth,
|
|
423
|
+
chdir: build_dir
|
|
424
|
+
end
|
|
425
|
+
copy_pkg appstore_exported_pkg_path, appstore_pkg_path
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Calls the block with the certificate APPLE_CODESIGN_P12 gives in a
|
|
429
|
+
# keychain of its own, which is deleted then, not to leave the private
|
|
430
|
+
# key on the machine, as a runner of CI of one's own.
|
|
431
|
+
#
|
|
432
|
+
def with_codesign_keychain(&block)
|
|
433
|
+
p12 = codesign_p12
|
|
434
|
+
return block.call unless p12
|
|
435
|
+
|
|
436
|
+
Dir.mktmpdir do |dir|
|
|
437
|
+
keychain_path = File.join dir, 'codesign.keychain-db'
|
|
438
|
+
p12_path = File.join dir, 'codesign.p12'
|
|
439
|
+
secret = SecureRandom.hex 16
|
|
440
|
+
others = security('list-keychains', '-d', 'user').lines.map {_1.strip.delete '"'}
|
|
441
|
+
File.binwrite p12_path, p12[:data]
|
|
442
|
+
security 'create-keychain', '-p', secret, keychain_path
|
|
443
|
+
begin
|
|
444
|
+
security 'set-keychain-settings', '-lut', '21600', keychain_path
|
|
445
|
+
security 'unlock-keychain', '-p', secret, keychain_path
|
|
446
|
+
security 'import', p12_path, '-k', keychain_path, '-P', p12[:password],
|
|
447
|
+
'-T', '/usr/bin/codesign', '-T', '/usr/bin/security'
|
|
448
|
+
security 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k',
|
|
449
|
+
secret, keychain_path
|
|
450
|
+
security 'list-keychains', '-d', 'user', '-s', keychain_path, *others
|
|
451
|
+
block.call
|
|
452
|
+
ensure
|
|
453
|
+
# delete-keychain takes it out of the keychains to search too
|
|
454
|
+
security 'delete-keychain', keychain_path
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Runs the security command, not printing it with the passwords.
|
|
460
|
+
#
|
|
461
|
+
def security(*args)
|
|
462
|
+
out, ok = capture 'security', *args
|
|
463
|
+
raise Error, "failed to run security #{args.first}:\n#{out}" unless ok
|
|
464
|
+
out
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Calls the block with the options of xcodebuild to authenticate by the
|
|
468
|
+
# API key, the .p8 file of which is written for the while when given
|
|
469
|
+
# by the content.
|
|
470
|
+
#
|
|
471
|
+
def with_auth_options(&block)
|
|
472
|
+
creds = AppStoreConnect.credentials
|
|
473
|
+
return block.call [] unless creds
|
|
474
|
+
|
|
475
|
+
AppStoreConnect.new(creds).with_key_path do |path|
|
|
476
|
+
block.call [
|
|
477
|
+
'-authenticationKeyIssuerID', creds[:issuer_id],
|
|
478
|
+
'-authenticationKeyID', creds[:key_id],
|
|
479
|
+
'-authenticationKeyPath', path
|
|
480
|
+
]
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def built_app_path()
|
|
485
|
+
File.join build_dir, 'DerivedData', 'Build', 'Products', 'Release', "#{app_name}.app"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# The directory of the app archived and exported for no store in
|
|
489
|
+
# particular, apart from the ones for a store.
|
|
490
|
+
#
|
|
491
|
+
def generic_dir()
|
|
492
|
+
File.join platform_dir, 'generic'
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def xcarchive_path()
|
|
496
|
+
File.join generic_dir, "#{target}.xcarchive"
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def export_options_path()
|
|
500
|
+
File.join generic_dir, 'ExportOptions.plist'
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def exported_app_path()
|
|
504
|
+
File.join generic_dir, 'export', "#{app_name}.app"
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def notarized_app_path()
|
|
508
|
+
File.join generic_dir, 'notarized', "#{app_name}.app"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# The directory of the app archived and exported for the Mac App Store.
|
|
512
|
+
#
|
|
513
|
+
def appstore_dir()
|
|
514
|
+
File.join platform_dir, 'appstore'
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def appstore_xcarchive_path()
|
|
518
|
+
File.join appstore_dir, "#{target}.xcarchive"
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def appstore_entitlements_path()
|
|
522
|
+
File.join appstore_dir, 'AppStore.entitlements'
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def appstore_export_options_path()
|
|
526
|
+
File.join appstore_dir, 'ExportOptions.plist'
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def appstore_exported_pkg_path()
|
|
530
|
+
File.join appstore_dir, 'export', "#{app_name}.pkg"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Has the app write its data file with its scripts compiled by the
|
|
534
|
+
# CRuby in it, which runs them: the app is built and run once with a
|
|
535
|
+
# boot script that does only that, in place of the one it has.
|
|
536
|
+
#
|
|
537
|
+
def compile_scripts()
|
|
538
|
+
boot = File.join build_dir, 'boot.rb'
|
|
539
|
+
orig = File.read boot
|
|
540
|
+
File.write boot, render('compile.rb.erb',
|
|
541
|
+
compiler: File.expand_path('data_compiler.rb', __dir__),
|
|
542
|
+
app_dir: app_dir,
|
|
543
|
+
path: File.join(build_dir, 'app', DataLoader::DATA_FILE))
|
|
544
|
+
xcodebuild
|
|
545
|
+
# with nothing of the Ruby running this, as bundle exec sets, which
|
|
546
|
+
# would have the CRuby in the app load the gems of it
|
|
547
|
+
env = ENV.keys.grep(/\A(BUNDLER?_|GEM_)/)
|
|
548
|
+
.then {[*_1, 'RUBYOPT', 'RUBYLIB']}
|
|
549
|
+
.to_h {[_1, nil]}
|
|
550
|
+
run File.join(built_app_path, 'Contents', 'MacOS', app_name), chdir: build_dir, env: env
|
|
551
|
+
ensure
|
|
552
|
+
File.write boot, orig if orig
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# CRuby has the bundled gems in its standard library.
|
|
556
|
+
#
|
|
557
|
+
def standard_gems?()
|
|
558
|
+
false
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Whether CRuby has the gem in its standard library, where it has the
|
|
562
|
+
# bundled gems laid out flat, as fiddle, which was a default gem: the
|
|
563
|
+
# file the gem is required by, its name with '-' as '/', is there.
|
|
564
|
+
#
|
|
565
|
+
def embedded_gem?(name)
|
|
566
|
+
File.file? File.join(cruby_stdlib_dir, "#{name.tr '-', '/'}.rb")
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# The directory of the standard library of CRuby, lib/ruby/<version>.
|
|
570
|
+
#
|
|
571
|
+
def cruby_stdlib_dir()
|
|
572
|
+
@cruby_stdlib_dir ||= Dir.glob(File.join cruby_dir, 'CRuby', 'lib', 'ruby', '*')
|
|
573
|
+
.find {File.basename(_1).match?(/\A\d+\.\d+\.\d+\z/)} ||
|
|
574
|
+
raise(Error, "no standard library in the CRuby of '#{cruby_dir}'")
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def rake_build_info(root)
|
|
578
|
+
out, err, status = Open3.capture3 RbConfig.ruby, '-e', RAKE_BUILD_INFO, chdir: root
|
|
579
|
+
raise Error, "failed to read the Rakefile in '#{root}': #{err.strip}" unless
|
|
580
|
+
status.success?
|
|
581
|
+
# the last line, as the Rakefile may print something when loaded
|
|
582
|
+
JSON.parse(out.lines.last.to_s, symbolize_names: true).to_h do |key, value|
|
|
583
|
+
[key, key == :defs ? value : value.map {File.expand_path _1, root}]
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def vendor?(dir)
|
|
588
|
+
dir.include? '/vendor/'
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def version?(str)
|
|
592
|
+
str.match?(/\A\d+(\.\d+)*\z/)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def check_cruby(path)
|
|
596
|
+
unless File.directory? File.join(path, 'CRuby', 'include')
|
|
597
|
+
raise Error,
|
|
598
|
+
"'#{path}' has no CRuby binary, " +
|
|
599
|
+
"run: cd #{path} && rake download_or_build os=macos"
|
|
600
|
+
end
|
|
601
|
+
path
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def fetch_cruby(version)
|
|
605
|
+
dir = File.join platform_dir, 'cruby', version
|
|
606
|
+
return dir if File.directory? File.join(dir, 'CRuby', 'include')
|
|
607
|
+
|
|
608
|
+
FileUtils.rm_rf dir
|
|
609
|
+
FileUtils.mkdir_p File.dirname(dir)
|
|
610
|
+
run 'git', 'clone', '-q', '-c', 'advice.detachedHead=false',
|
|
611
|
+
'--depth', '1', '--branch', "v#{version}", CRUBY_GIT, dir,
|
|
612
|
+
chdir: File.dirname(dir)
|
|
613
|
+
# cruby has a Gemfile of its own, the one of the app must not be used
|
|
614
|
+
bundler = ENV.keys.grep(/\ABUNDLER?_/).to_h {[_1, nil]}
|
|
615
|
+
# the ones before 4.0.701 have no task to download only
|
|
616
|
+
download_only = Gem::Version.new(version) >= Gem::Version.new('4.0.701')
|
|
617
|
+
run 'rake', download_only ? 'download' : 'download_or_build',
|
|
618
|
+
chdir: dir, env: {**bundler, 'RUBYOPT' => nil, 'os' => 'macos'}
|
|
619
|
+
check_cruby dir
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Writes the bundles CRuby adds to the load path: <name>.bundle with
|
|
623
|
+
# lib/ in its resources, as the resource bundles of a pod are.
|
|
624
|
+
#
|
|
625
|
+
def copy_bundles()
|
|
626
|
+
dir = File.join build_dir, 'Bundles'
|
|
126
627
|
FileUtils.rm_rf dir
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
628
|
+
libraries.each {copy_library _1.root, bundle_resources(dir, _1.name)}
|
|
629
|
+
copy_gems {File.join bundle_resources(dir, _1), 'lib'}
|
|
630
|
+
res = bundle_resources dir, 'CRuby'
|
|
631
|
+
FileUtils.mkdir_p res
|
|
632
|
+
FileUtils.cp_r File.join(cruby_dir, 'CRuby', 'lib'), res
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# InfoPlist.strings of each language in its .lproj, which xcodegen
|
|
636
|
+
# finds in src/, unless the app is in English only.
|
|
637
|
+
#
|
|
638
|
+
def write_localizations()
|
|
639
|
+
FileUtils.rm_rf Dir.glob(File.join build_dir, 'src', '*.lproj')
|
|
640
|
+
return if config.localizations.size <= 1
|
|
641
|
+
config.localizations.each do |lang, values|
|
|
642
|
+
write "src/#{lang}.lproj/InfoPlist.strings", render('InfoPlist.strings.erb', **values)
|
|
132
643
|
end
|
|
133
|
-
|
|
134
|
-
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# The lib directory of the library +name+ in the package.
|
|
647
|
+
#
|
|
648
|
+
def library_lib_dir(name)
|
|
649
|
+
File.join bundle_resources(File.join(build_dir, 'Bundles'), name), 'lib'
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def bundle_resources(dir, name)
|
|
653
|
+
File.join dir, "#{name}.bundle", 'Contents', 'Resources'
|
|
135
654
|
end
|
|
136
655
|
|
|
137
656
|
def generate_icon()
|
|
@@ -139,48 +658,27 @@ module Reflex
|
|
|
139
658
|
FileUtils.rm_rf iconset
|
|
140
659
|
FileUtils.mkdir_p iconset
|
|
141
660
|
icon_commands(File.join(config.dir, config.icon), iconset)
|
|
142
|
-
.each {|cmd| run(*cmd, chdir: build_dir)}
|
|
661
|
+
.each {|cmd| run(*cmd, chdir: build_dir, quiet: true)}
|
|
143
662
|
run 'iconutil', '-c', 'icns', 'AppIcon.iconset', '-o', 'AppIcon.icns',
|
|
144
663
|
chdir: build_dir
|
|
145
664
|
end
|
|
146
665
|
|
|
147
|
-
|
|
148
|
-
# that the manual setups have been done.
|
|
149
|
-
#
|
|
150
|
-
def check_dev_pods()
|
|
151
|
-
dev_pod_paths.each do |name, path|
|
|
152
|
-
raise Error, "pod directory not found: '#{path}'" unless File.directory? path
|
|
153
|
-
if name == 'CRuby'
|
|
154
|
-
unless File.directory? File.join(path, 'CRuby', 'include')
|
|
155
|
-
raise Error,
|
|
156
|
-
"'#{path}' has no CRuby binary, " +
|
|
157
|
-
"run: cd #{path} && rake download_or_build os=macos"
|
|
158
|
-
end
|
|
159
|
-
else # the umbrella pod (Reflex / RubySketch / ...)
|
|
160
|
-
unless File.directory? File.join(path, 'xot')
|
|
161
|
-
raise Error,
|
|
162
|
-
"'#{path}' is not set up for CocoaPods, " +
|
|
163
|
-
"run: cd #{path} && rake -f pod.rake setup"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def copy_app()
|
|
170
|
-
app = File.join build_dir, 'DerivedData', 'Build', 'Products', 'Release', "#{target}.app"
|
|
666
|
+
def copy_app(app = built_app_path)
|
|
171
667
|
raise Error, "application not found: '#{app}'" unless File.directory? app
|
|
172
668
|
|
|
173
|
-
dist =
|
|
669
|
+
dist = dist_path
|
|
174
670
|
FileUtils.rm_rf dist
|
|
175
671
|
FileUtils.mkdir_p dist_dir
|
|
176
672
|
FileUtils.cp_r app, dist
|
|
177
673
|
puts "Created #{dist}"
|
|
178
674
|
end
|
|
179
675
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
File.
|
|
676
|
+
def copy_pkg(pkg, dist)
|
|
677
|
+
raise Error, "installer package not found: '#{pkg}'" unless File.file? pkg
|
|
678
|
+
|
|
679
|
+
FileUtils.mkdir_p File.dirname(dist)
|
|
680
|
+
FileUtils.cp pkg, dist
|
|
681
|
+
puts "Created #{dist}"
|
|
184
682
|
end
|
|
185
683
|
|
|
186
684
|
end# MacOS
|