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/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Reflex Packager - Package Reflex apps as native macOS
|
|
1
|
+
# Reflex Packager - Package Reflex apps as native macOS and Windows apps
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
@@ -17,16 +17,21 @@ For more details, check out our [Contribution Guidelines](./CONTRIBUTING.md).
|
|
|
17
17
|
|
|
18
18
|
## :rocket: About
|
|
19
19
|
|
|
20
|
-
**Reflex Packager** is a CLI tool that packages [Reflex](https://github.com/xord/reflex) applications as native
|
|
20
|
+
**Reflex Packager** is a CLI tool that packages [Reflex](https://github.com/xord/reflex) applications as native apps, which carry the Ruby runtime they run on and run where Ruby is not installed:
|
|
21
|
+
|
|
22
|
+
- On macOS, a `.app` bundle: it generates an Xcode project that compiles the libraries from the sources of their installed gems together with [CRuby](https://github.com/xord/cruby).
|
|
23
|
+
- On Windows, a folder with an `.exe`: it links what the installed gems of the libraries built into an executable, which runs on the Ruby DLL of [RubyInstaller](https://rubyinstaller.org/) shipped beside it.
|
|
21
24
|
|
|
22
25
|
The packager is runtime-agnostic — each gem (Reflex, [RubySketch](https://github.com/xord/rubysketch), ...) supplies its own profile and reuses this packager as the engine.
|
|
23
26
|
|
|
24
27
|
## :clipboard: Requirements
|
|
25
28
|
|
|
26
29
|
- Ruby **3.0.0** or later
|
|
27
|
-
-
|
|
28
|
-
- [
|
|
29
|
-
- Xcode (with command line tools)
|
|
30
|
+
- On macOS
|
|
31
|
+
- [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)
|
|
32
|
+
- Xcode (with command line tools)
|
|
33
|
+
- On Windows
|
|
34
|
+
- [RubyInstaller](https://rubyinstaller.org/) with the MSYS2 DevKit (`ridk install`), which the gems are built with too
|
|
30
35
|
- The dependent gems are installed automatically: `xot`, `rucy`, `rays`, `reflexion`
|
|
31
36
|
|
|
32
37
|
## :package: Installation
|
|
@@ -58,14 +63,19 @@ $ ruby main.rb # run the application directly
|
|
|
58
63
|
|
|
59
64
|
This generates a project directory with `main.rb` and `reflex.yml`.
|
|
60
65
|
|
|
61
|
-
### Package as
|
|
66
|
+
### Package as an app
|
|
62
67
|
|
|
63
68
|
```bash
|
|
64
69
|
$ cd myapp
|
|
65
70
|
$ reflex package .
|
|
66
71
|
```
|
|
67
72
|
|
|
68
|
-
The
|
|
73
|
+
The app is packaged for the platform the packager runs on, as each one builds only on itself, and placed in `dist/`:
|
|
74
|
+
|
|
75
|
+
- macOS: `dist/generic/<app name>.app` signed with Developer ID, or `dist/adhoc/<app name>.app` signed otherwise, only to be run where it is built, and `dist/appstore/<name>-<version>.pkg` for the Mac App Store
|
|
76
|
+
- Windows: `dist/generic/<name>/`, with `<name>.exe`, and the DLLs it loads, the standard library, the libraries, the gems and the app in `lib/`
|
|
77
|
+
|
|
78
|
+
The zip of the app is put beside it. `<name>` is the name of the app with no spaces or symbols, as `MyApp`, and `<app name>` is the name as it is, as `My App`, which Finder and the Dock show.
|
|
69
79
|
|
|
70
80
|
### CLI options
|
|
71
81
|
|
|
@@ -86,9 +96,16 @@ Package command options:
|
|
|
86
96
|
```
|
|
87
97
|
reflex package [options] [DIR]
|
|
88
98
|
|
|
89
|
-
--platform PLATFORM target platform (default:
|
|
99
|
+
--platform PLATFORM target platform (default: the one it runs on)
|
|
90
100
|
--config PATH config file path (default: DIR/reflex.yml)
|
|
91
101
|
--generate-only generate project files but do not build
|
|
102
|
+
--pack put the files of the app together in a data file, for a release
|
|
103
|
+
--generic package the app signed with Developer ID, also with --appstore
|
|
104
|
+
--appstore package the app for the Mac App Store, only it unless --generic
|
|
105
|
+
--notarize have Apple check the app with Developer ID, on macos
|
|
106
|
+
--notarize-submit only submit the app to be notarized, not waiting
|
|
107
|
+
--notarize-wait ID only wait for the notarization ID, by the API key
|
|
108
|
+
--notarize-export only wait for the notarization submitted and export the app
|
|
92
109
|
--verbose verbose output
|
|
93
110
|
```
|
|
94
111
|
|
|
@@ -100,64 +117,252 @@ The project is configured via `reflex.yml` (or `reflex.yaml`) in the project dir
|
|
|
100
117
|
name: MyApp
|
|
101
118
|
bundle_id: com.example.myapp
|
|
102
119
|
version: 1.0.0
|
|
120
|
+
# copyright: © 2026 Example
|
|
103
121
|
icon: icon.png
|
|
104
122
|
# main: main.rb
|
|
105
123
|
# files:
|
|
106
124
|
# - "lib/**/*.rb"
|
|
125
|
+
# excludes:
|
|
126
|
+
# - lib/debug.rb
|
|
107
127
|
|
|
108
128
|
# macos:
|
|
109
129
|
# deployment_target: "11.0"
|
|
110
|
-
# archs: arm64
|
|
130
|
+
# archs: [arm64, x86_64]
|
|
131
|
+
# category: public.app-category.games
|
|
111
132
|
# codesign:
|
|
112
133
|
# identity: "-"
|
|
113
134
|
# team_id: XXXXXXXXXX
|
|
114
135
|
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
#
|
|
136
|
+
# windows:
|
|
137
|
+
# console: false
|
|
138
|
+
|
|
139
|
+
# upload:
|
|
140
|
+
# itchio:
|
|
141
|
+
# project: user/game
|
|
142
|
+
|
|
143
|
+
# localizations:
|
|
144
|
+
# ja:
|
|
145
|
+
# name: マイアプリ
|
|
120
146
|
```
|
|
121
147
|
|
|
122
148
|
| Key | Default | Description |
|
|
123
149
|
|-----|---------|-------------|
|
|
124
150
|
| `name` | directory name | Application name |
|
|
125
151
|
| `bundle_id` | `org.xord.reflex.<name>` | macOS bundle identifier |
|
|
126
|
-
| `version` | `0.1.0` | Application version |
|
|
152
|
+
| `version` | the `VERSION` file, or `0.1.0` | Application version |
|
|
153
|
+
| `copyright` | none | Copyright notice, shown in the About panel on macOS and in the properties of the executable on Windows |
|
|
127
154
|
| `main` | `main.rb` | Entry point script |
|
|
128
155
|
| `icon` | none | Path to an icon image (PNG) |
|
|
129
|
-
| `files` | none | Additional files to bundle (glob patterns) |
|
|
156
|
+
| `files` | none | Additional files to bundle (glob patterns); the dotfiles in a directory are bundled only by their own names |
|
|
157
|
+
| `excludes` | none | Files not to bundle out of `files` (glob patterns), such as the ones only for development |
|
|
130
158
|
| `macos.deployment_target` | `11.0` | Minimum macOS version |
|
|
131
|
-
| `macos.archs` | `arm64` | Target architectures |
|
|
132
|
-
| `macos.
|
|
133
|
-
| `macos.
|
|
159
|
+
| `macos.archs` | `[arm64, x86_64]` | Target architectures |
|
|
160
|
+
| `macos.cruby` | the packager's | CRuby version, or a path to a cruby checkout |
|
|
161
|
+
| `macos.category` | none | [Category](https://developer.apple.com/documentation/bundleresources/information-property-list/lsapplicationcategorytype) of the app in Info.plist, as `public.app-category.games`, which the App Store requires |
|
|
162
|
+
| `macos.codesign.identity` | `-` | Code signing identity, `-` to sign ad hoc, or by the certificate Apple manages with `team_id`, and `Developer ID Application` by the certificate in the keychain; `APPLE_CODESIGN_IDENTITY` overrides it |
|
|
163
|
+
| `macos.codesign.team_id` | none | Development team ID; `APPLE_TEAM_ID` overrides it |
|
|
164
|
+
| `windows.console` | `false` | Keep a console window, which shows what the app prints and the error it dies of |
|
|
165
|
+
| `upload.itchio.project` | none | Project of itch.io to upload the app to, as `user/game` of `https://user.itch.io/game` |
|
|
166
|
+
| `localizations` | none | `name` and `copyright` in other languages than English |
|
|
167
|
+
|
|
168
|
+
### Versions
|
|
169
|
+
|
|
170
|
+
`version` is the version to show, and the one to tell builds apart with, of numbers separated by dots:
|
|
171
|
+
|
|
172
|
+
| | Shown (`CFBundleShortVersionString`, `ProductVersion`) | Build (`CFBundleVersion`, `FileVersion`) |
|
|
173
|
+
|-----|-----|-----|
|
|
174
|
+
| `version: 1.2.3` | `1.2.3` | `1.2.3` |
|
|
175
|
+
| `version: 1.2.3.4` | `1.2.3` | `1.2.3.4` |
|
|
176
|
+
|
|
177
|
+
Without `version`, the version is read from the `VERSION` file in the directory of the app, if there is one, so that the file can be the only place to keep it in, as the gems do.
|
|
178
|
+
|
|
179
|
+
A fourth number of `version` tells a build apart from another of the same version, but not on macOS, where the build has to be of three numbers at most, as `CFBundleVersion`. On Windows, each number has to be 65535 or less. Quote a version of two numbers, as `'1.10'`, which YAML reads as the number 1.1 otherwise.
|
|
180
|
+
|
|
181
|
+
### Localizations
|
|
182
|
+
|
|
183
|
+
`name` and `copyright` are in English, and `localizations` has them in other languages, by language tags as `ja` and `zh-Hans`; what a language leaves out is the English one:
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
name: My App
|
|
187
|
+
copyright: © 2026 Example
|
|
188
|
+
localizations:
|
|
189
|
+
ja:
|
|
190
|
+
name: マイアプリ
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
On macOS, the name in the language of the system is shown in the Finder, the Dock, the menu bar and the application menu, and the copyright in the About panel. On Windows, they are in the version resource of the executable, in the languages Windows knows, and the name in the language of the user is the one the app has by default (`Reflex::Application#name`), which a tray shows. The properties of the executable show the English ones, though, and its file name stays as it is.
|
|
194
|
+
|
|
195
|
+
### Packing for a release
|
|
196
|
+
|
|
197
|
+
With `--pack`, the Ruby scripts of the app are compiled into instruction sequences and put together in `app/data.bin`, so the package carries none of the scripts. `require`, `require_relative` and `load` read them from the data file. The other files of the app are in `app/` as they are.
|
|
198
|
+
|
|
199
|
+
The bytes of the data file are substituted with others, which keeps the scripts from being read as they are, though it is no encryption.
|
|
200
|
+
|
|
201
|
+
The scripts are compiled by the Ruby the package runs them on: on Windows, the one running the packager, and on macOS, the CRuby in the app. On macOS, the app is built and run once to compile them, then built again, so a packed app has to be built, not only generated.
|
|
202
|
+
|
|
203
|
+
### Signing and notarizing on macOS
|
|
204
|
+
|
|
205
|
+
By default, the app is signed ad hoc, which runs on the Mac it is built on, but which macOS warns of when the app is downloaded. To have the app opened with no warning, sign it with Developer ID and have Apple notarize it.
|
|
206
|
+
|
|
207
|
+
With `team_id` and either of the identities below, the app is archived and exported by `xcodebuild`, signed with Developer ID and the hardened runtime:
|
|
208
|
+
|
|
209
|
+
- With the identity `-`, by the certificate Apple manages for the team, by the account of Xcode, so no certificate is needed in the keychain
|
|
210
|
+
- With the identity `Developer ID Application`, by the certificate in the keychain, as on CI
|
|
211
|
+
|
|
212
|
+
The certificate can be given by the environment variables too, put into a keychain of its own only while the app is signed, as on CI, and the identity is `Developer ID Application` then:
|
|
213
|
+
|
|
214
|
+
| Variable | Value |
|
|
215
|
+
|-----|-----|
|
|
216
|
+
| `APPLE_CODESIGN_P12` | The certificate with its private key, exported from the keychain into a `.p12` file, in base64 |
|
|
217
|
+
| `APPLE_CODESIGN_P12_PASSWORD` | Password of the `.p12` file |
|
|
218
|
+
|
|
219
|
+
Where no account of Xcode is, as on CI, Apple is asked by an API key of App Store Connect, which the environment variables tell:
|
|
220
|
+
|
|
221
|
+
| Variable | Value |
|
|
222
|
+
|-----|-----|
|
|
223
|
+
| `APPLE_API_ISSUER_ID` | Issuer ID |
|
|
224
|
+
| `APPLE_API_KEY_ID` | Key ID |
|
|
225
|
+
| `APPLE_API_KEY_P8` | Content of the private key file, `AuthKey_<ID>.p8` |
|
|
226
|
+
| `APPLE_API_KEY_PATH` | Path to the private key file, in place of `APPLE_API_KEY_P8` |
|
|
227
|
+
|
|
228
|
+
The team ID can be given by `APPLE_TEAM_ID` in place of the config, as the identity by `APPLE_CODESIGN_IDENTITY`, to keep them out of the repository of the app.
|
|
229
|
+
|
|
230
|
+
With `--notarize`, the app is sent to Apple to be notarized, which takes some minutes, and exported to `dist/generic/` with the ticket of the notarization stapled, which macOS sees with no network:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
$ export APPLE_TEAM_ID=XXXXXXXXXX
|
|
234
|
+
$ reflex package --pack --notarize .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The notarization can be split into the steps, so as not to keep a Mac waiting, as on CI:
|
|
238
|
+
|
|
239
|
+
1. `--notarize-submit` builds the app and sends it, printing `Submitted for the notarization: <ID>`; the archive sent is `.build/macos/generic/<name>.xcarchive`
|
|
240
|
+
2. `--notarize-wait <ID>` waits for Apple to check the app, by the API key, on any system
|
|
241
|
+
3. `--notarize-export` exports the app notarized from the archive, waiting for the notarization if not finished yet, on macOS
|
|
242
|
+
|
|
243
|
+
`--generic` makes sure the app is signed with Developer ID, failing if not, as with no team.
|
|
244
|
+
|
|
245
|
+
### Packaging for the Mac App Store
|
|
246
|
+
|
|
247
|
+
With `--appstore`, the app is archived in the sandbox, which the Mac App Store needs, and exported into `dist/appstore/<name>-<version>.pkg` to upload to App Store Connect, signed by the certificates of the team that Apple manages, as the profile. It needs the team and `macos.category` in the config, and makes only the one for the App Store, unless `--generic` or `--notarize` asks for the generic one too, archived apart out of the sandbox. `rake package:appstore` packs and packages it so:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
$ export APPLE_TEAM_ID=XXXXXXXXXX
|
|
251
|
+
$ reflex package --pack --appstore .
|
|
252
|
+
$ reflex package --pack --notarize --appstore . # the generic one too
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The app in the sandbox keeps its data in a container of its own, `~/Library/Containers/<bundle_id>/`, apart from the generic one.
|
|
256
|
+
|
|
257
|
+
Info.plist of the app has `ITSAppUsesNonExemptEncryption` of `false`, which tells Apple that it uses no encryption but the exempt one, as of HTTPS. The config has no way to change it yet, so an app with an encryption of its own is not to be packaged so.
|
|
258
|
+
|
|
259
|
+
### Releasing on GitHub
|
|
260
|
+
|
|
261
|
+
`rake release` makes the release of the tag of the version on GitHub by `gh`, with the changes of the version in `ChangeLog.md`, and attaches the zip of the app and the package for the Mac App Store, the ones made, or adds them to the release made already. It makes neither, as the jobs on CI make them apart, so archive or package the app first to release it by hand:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
$ rake archive release
|
|
265
|
+
$ rake archive package:appstore release # the one for the Mac App Store too
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Uploading to App Store Connect
|
|
269
|
+
|
|
270
|
+
`rake upload:appstore` uploads the package for the Mac App Store, packaged if not yet, to App Store Connect by `altool` of Xcode, by the API key of App Store Connect in the environment variables above, which App Store Connect tells the app and the build of by the package. The app has to be made in App Store Connect beforehand, by its bundle ID, and the build is chosen for a version there to submit it for review:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
$ export APPLE_TEAM_ID=XXXXXXXXXX
|
|
274
|
+
$ export APPLE_API_ISSUER_ID=... APPLE_API_KEY_ID=... APPLE_API_KEY_PATH=~/AuthKey_XXXXXXXXXX.p8
|
|
275
|
+
$ rake upload:appstore
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
A build can not be uploaded twice by the same number, so raise `version` to upload it again.
|
|
279
|
+
|
|
280
|
+
### Uploading to itch.io
|
|
281
|
+
|
|
282
|
+
With `upload.itchio.project` in the config, `rake upload:itchio` uploads the zip of the app, archived if not yet, by [butler](https://itch.io/docs/butler/), into the channel of the platform, as `macos` or `windows`, with the version of the app. butler uploads it as the user `butler login` logged in, or by the API key in `BUTLER_API_KEY`, made at https://itch.io/user/settings/api-keys, as on CI. Download butler of the platform from https://broth.itch.zone/butler, not by `brew install butler`, which is another app:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
$ rake upload:itchio
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Packaging on GitHub Actions
|
|
134
289
|
|
|
135
|
-
|
|
290
|
+
The workflow `.github/workflows/package.yml` of this repository packages the app on GitHub Actions, called by the workflow of the app, and releases it when run by a tag. It packages the app for macOS and Windows, or the ones in its input `platforms`, as `platforms: macos`. With the secrets below, the app for macOS is notarized: a job on macOS submits it, one on Linux, which is cheaper, waits for it, and one on macOS again exports it. The app for Windows is not signed.
|
|
136
291
|
|
|
137
|
-
|
|
292
|
+
```yaml
|
|
293
|
+
jobs:
|
|
294
|
+
release:
|
|
295
|
+
uses: xord/reflex-packager/.github/workflows/package.yml@master
|
|
296
|
+
secrets:
|
|
297
|
+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
298
|
+
APPLE_CODESIGN_P12: ${{ secrets.APPLE_CODESIGN_P12 }} # the .p12 file in base64
|
|
299
|
+
APPLE_CODESIGN_P12_PASSWORD: ${{ secrets.APPLE_CODESIGN_P12_PASSWORD }}
|
|
300
|
+
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
|
301
|
+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
302
|
+
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} # the .p8 file as it is
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
For `APPLE_CODESIGN_P12`, create the certificate of Developer ID in Xcode, by Settings → Accounts → Manage Certificates, export it by Keychain Access into a `.p12` file, and put it in base64 into the secret:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
$ base64 -i DeveloperID.p12 | pbcopy
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
With `APPLE_TEAM_ID` but no `APPLE_CODESIGN_P12`, the workflow fails. Without the secrets, the app is packaged with no notarization, signed as the config tells.
|
|
312
|
+
|
|
313
|
+
With the input `macos-appstore: true` and the secrets to notarize, the app for the Mac App Store is packaged too, in the job that submits the notarized one, and on a tag, attached to the release by `rake release`, then uploaded to App Store Connect by `rake upload:appstore`. It is signed by the certificate Apple manages, through the API key, which has to be of the Admin role to make the certificate and the profile, so `APPLE_CODESIGN_P12` needs no more than Developer ID. A failure of it fails the job, and the notarization with it. With the input but no `APPLE_TEAM_ID`, the workflow fails at first.
|
|
314
|
+
|
|
315
|
+
With `BUTLER_API_KEY` in the secrets too, the app released by a tag is uploaded to itch.io by `rake upload:itchio`, after it is released on GitHub, unless the input `itchio` is `false`.
|
|
316
|
+
|
|
317
|
+
### CRuby
|
|
318
|
+
|
|
319
|
+
By default the packager clones the [cruby](https://github.com/xord/cruby) repository at the tag of its CRuby version into `.build/macos/cruby/<version>`, and downloads the prebuilt CRuby there. To use a local checkout instead, set its path as `macos.cruby` in the config, or via the `CRUBY_PATH` environment variable, which overrides the config:
|
|
138
320
|
|
|
139
321
|
```bash
|
|
140
|
-
$ export
|
|
322
|
+
$ export CRUBY_PATH=/path/to/cruby
|
|
141
323
|
$ reflex package .
|
|
142
324
|
```
|
|
143
325
|
|
|
326
|
+
### Libraries
|
|
327
|
+
|
|
328
|
+
The libraries the app runs on (xot, rucy, rays, reflex, ...) are the gems with `lib/<name>/extension.rb` the gemspec of the profile depends on, taken from the gems installed for the Ruby running the packager: macOS compiles their sources, and Windows links what their gem builds left. To package with local checkouts of them, put their `lib` directories on `RUBYLIB`.
|
|
329
|
+
|
|
330
|
+
### Gems
|
|
331
|
+
|
|
332
|
+
The gems in the default group of the `Gemfile` of the app are shipped with it, with what they depend on, and `require 'bundler/setup'` does nothing in the package.
|
|
333
|
+
|
|
334
|
+
- A library in the `Gemfile`, as `rays-video`, is built in or linked as the ones of the profile.
|
|
335
|
+
- On macOS, a gem with a native extension is refused, as the extension is built for the Ruby running the packager rather than for CRuby.
|
|
336
|
+
- On Windows, the standard gems which were default gems once, as `csv` and `fiddle`, are shipped too; CRuby has them in its standard library on macOS.
|
|
337
|
+
|
|
144
338
|
## :wrench: How it works
|
|
145
339
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
340
|
+
On macOS:
|
|
341
|
+
|
|
342
|
+
1. Copies the application files, the libraries and the gems into a build directory
|
|
343
|
+
2. Fetches CRuby unless it is there already
|
|
344
|
+
3. Generates an Xcode project (via XcodeGen) with the sources the Rakefiles of the libraries build
|
|
345
|
+
4. Builds the `.app` bundle with `xcodebuild`, or archives and exports it signed with Developer ID, and notarized with `--notarize`
|
|
150
346
|
5. Copies the result to `dist/`
|
|
151
347
|
|
|
152
|
-
|
|
348
|
+
On Windows:
|
|
349
|
+
|
|
350
|
+
1. Copies the application files, the libraries and the gems into a build directory
|
|
351
|
+
2. Compiles the executable and links the extensions and the archives the gem builds of the libraries left into it, with the Ruby DLL
|
|
352
|
+
3. Copies the executable, the DLLs it loads, found through their import tables, and the standard library to `dist/`
|
|
353
|
+
|
|
354
|
+
The executable registers the extensions of the libraries and runs `boot.rb`, which loads the application's `main.rb` and shows an error it dies of with `Reflex.alert`, unless a terminal or a console shows it.
|
|
153
355
|
|
|
154
356
|
## :hammer_and_wrench: Development
|
|
155
357
|
|
|
156
358
|
```bash
|
|
157
359
|
$ rake test # run the test suite
|
|
158
360
|
$ rake # default task
|
|
361
|
+
$ rake example # package examples/hello with the libraries in this repository
|
|
159
362
|
```
|
|
160
363
|
|
|
364
|
+
`rake example` takes `name=` to package another app under `examples/`, `platform=` to package for another platform, and `pack=1` to package with `--pack`.
|
|
365
|
+
|
|
161
366
|
In the [`xord/all`](https://github.com/xord/all) monorepo you can scope by module.
|
|
162
367
|
|
|
163
368
|
## :scroll: License
|
data/Rakefile
CHANGED
|
@@ -23,3 +23,24 @@ use_bundler
|
|
|
23
23
|
test_ruby_extension
|
|
24
24
|
generate_documents
|
|
25
25
|
build_ruby_gem
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
task :example => 'example:build'
|
|
29
|
+
|
|
30
|
+
namespace :example do
|
|
31
|
+
dir = File.expand_path "examples/#{ENV['name'] || 'hello'}", __dir__
|
|
32
|
+
|
|
33
|
+
desc 'package an example app with the libraries in this repository (name=hello platform=macos|windows pack=1)'
|
|
34
|
+
task :build do
|
|
35
|
+
libs = %w[xot rucy beeps rays reflex reflex-packager].map {File.expand_path "../#{_1}/lib", __dir__}
|
|
36
|
+
envs = {'RUBYLIB' => libs.join(File::PATH_SEPARATOR)}
|
|
37
|
+
opts = ENV['platform']&.then {['--platform', _1]} || []
|
|
38
|
+
opts << '--pack' if %w[1 true yes].include? ENV['pack']
|
|
39
|
+
sh(envs, RbConfig.ruby, File.expand_path('bin/reflex', __dir__), 'package', *opts, dir)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'delete what packaging an example app left (name=hello)'
|
|
43
|
+
task :clean do
|
|
44
|
+
rm_rf %w[.build dist].map {File.join dir, _1}
|
|
45
|
+
end
|
|
46
|
+
end
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
data/bin/reflex
CHANGED
|
@@ -1,37 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require 'reflex/extension'
|
|
4
3
|
require 'reflex/packager'
|
|
4
|
+
require 'reflex/packager/profiles/reflex'
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
pod: 'Reflex',
|
|
9
|
-
git: 'https://github.com/xord/reflex',
|
|
10
|
-
version: Reflex::Extension.version,
|
|
11
|
-
libraries: %w[Xot Rucy Rays Reflex],
|
|
12
|
-
extensions: %w[rays_ext reflex_ext],
|
|
13
|
-
config_files: %w[reflex.yml reflex.yaml],
|
|
14
|
-
templates: {'main.rb': <<~MAIN, 'reflex.yml': <<~CONFIG})
|
|
15
|
-
require 'reflex'
|
|
16
|
-
|
|
17
|
-
view = Reflex::View.new
|
|
18
|
-
def view.on_draw(e)
|
|
19
|
-
e.painter.font nil, 48
|
|
20
|
-
e.painter.text 'hello, world!', 100, 100
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
win = Reflex::Window.new
|
|
24
|
-
win.title = '{{name}}'
|
|
25
|
-
win.frame = [100, 100, 400, 300]
|
|
26
|
-
win.add view
|
|
27
|
-
win.show
|
|
28
|
-
|
|
29
|
-
Reflex.start
|
|
30
|
-
MAIN
|
|
31
|
-
name: {{name}}
|
|
32
|
-
version: 1.0.0
|
|
33
|
-
#bundle_id: org.xord.reflex.example.{{name_id}}
|
|
34
|
-
#icon: icon.png
|
|
35
|
-
CONFIG
|
|
36
|
-
|
|
37
|
-
Reflex::Packager::CLI.new(profile).run ARGV.dup
|
|
7
|
+
Reflex::Packager::CLI.new(Reflex::Packager::REFLEX).run ARGV.dup
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'reflex'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
view = Reflex::View.new
|
|
5
|
+
def view.on_draw(e)
|
|
6
|
+
e.painter.font nil, 32
|
|
7
|
+
e.painter.text 'hello, world!', 20, 20
|
|
8
|
+
e.painter.font nil, 14
|
|
9
|
+
e.painter.text "ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})", 20, 70
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
win = Reflex::Window.new
|
|
13
|
+
win.title = 'Hello'
|
|
14
|
+
win.frame = [100, 100, 400, 200]
|
|
15
|
+
win.add view
|
|
16
|
+
win.show
|
|
17
|
+
|
|
18
|
+
Reflex.start
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module Reflex
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
module Packager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# The API of App Store Connect, which the notary service of Apple is a
|
|
14
|
+
# part of, checking the apps for the system to open them without
|
|
15
|
+
# warnings, asked by the API key of it on any system, as the runners of
|
|
16
|
+
# CI cheaper than of macOS.
|
|
17
|
+
#
|
|
18
|
+
class AppStoreConnect
|
|
19
|
+
|
|
20
|
+
# The environment variables of the API key of App Store Connect, for
|
|
21
|
+
# where no account of Xcode is, as on CI: the private key by the .p8
|
|
22
|
+
# file, or by the content of it, as a secret of CI.
|
|
23
|
+
#
|
|
24
|
+
AUTH_ENV_KEYS = {
|
|
25
|
+
issuer_id: 'APPLE_API_ISSUER_ID',
|
|
26
|
+
key_id: 'APPLE_API_KEY_ID',
|
|
27
|
+
key_p8: 'APPLE_API_KEY_P8',
|
|
28
|
+
key_path: 'APPLE_API_KEY_PATH'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Seconds to wait for the notarization in all, and between the tries
|
|
32
|
+
# to see whether it finished.
|
|
33
|
+
#
|
|
34
|
+
NOTARIZATION_TIMEOUT = 60 * 60
|
|
35
|
+
NOTARIZATION_INTERVAL = 30
|
|
36
|
+
|
|
37
|
+
# The host of the notary service, not of the other APIs of App Store
|
|
38
|
+
# Connect, which is api.appstoreconnect.apple.com.
|
|
39
|
+
#
|
|
40
|
+
HOST = 'https://appstoreconnect.apple.com'
|
|
41
|
+
|
|
42
|
+
# The credentials of the API key the environment variables tell, as
|
|
43
|
+
# {issuer_id:, key_id:, key_p8:, key_path:} with either of key_p8 and
|
|
44
|
+
# key_path, or nil without any of them.
|
|
45
|
+
#
|
|
46
|
+
def self.credentials()
|
|
47
|
+
creds = AUTH_ENV_KEYS.transform_values {ENV[_1].then {|s| s unless s.to_s.empty?}}
|
|
48
|
+
return nil if creds.values.none?
|
|
49
|
+
|
|
50
|
+
missing = %i[issuer_id key_id].filter_map {AUTH_ENV_KEYS[_1] unless creds[_1]}
|
|
51
|
+
missing << "#{AUTH_ENV_KEYS[:key_p8]} or #{AUTH_ENV_KEYS[:key_path]}" if
|
|
52
|
+
!creds[:key_p8] && !creds[:key_path]
|
|
53
|
+
raise Error, "no #{missing.join ', '} with the others for the API key" unless
|
|
54
|
+
missing.empty?
|
|
55
|
+
|
|
56
|
+
creds.compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(credentials = self.class.credentials)
|
|
60
|
+
@credentials = credentials ||
|
|
61
|
+
raise(Error, "no API key by #{AUTH_ENV_KEYS.values.join ', '}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The credentials of the API key, which the tools of Xcode, as
|
|
65
|
+
# xcodebuild and altool, are given too.
|
|
66
|
+
#
|
|
67
|
+
attr_reader :credentials
|
|
68
|
+
|
|
69
|
+
# The path to the private key file of the API key, for the tools of
|
|
70
|
+
# Xcode, written only while the block runs if only the content of it is
|
|
71
|
+
# given, as a secret of CI.
|
|
72
|
+
#
|
|
73
|
+
def with_key_path(&block)
|
|
74
|
+
creds = @credentials
|
|
75
|
+
return block.call File.expand_path(creds[:key_path]) if creds[:key_path]
|
|
76
|
+
|
|
77
|
+
Dir.mktmpdir do |dir|
|
|
78
|
+
path = File.join dir, "AuthKey_#{creds[:key_id]}.p8"
|
|
79
|
+
File.write path, creds[:key_p8], perm: 0600
|
|
80
|
+
block.call path
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Waits until the app is notarized by +id+, raising the issues Apple
|
|
85
|
+
# found unless it accepted the app.
|
|
86
|
+
#
|
|
87
|
+
def wait_until_notarized(
|
|
88
|
+
id, timeout: NOTARIZATION_TIMEOUT, interval: NOTARIZATION_INTERVAL)
|
|
89
|
+
|
|
90
|
+
deadline = Time.now + timeout
|
|
91
|
+
loop do
|
|
92
|
+
case status = notarization_status(id)
|
|
93
|
+
when 'Accepted'
|
|
94
|
+
return
|
|
95
|
+
when 'In Progress'
|
|
96
|
+
raise Error, 'timed out waiting for the notarization' if Time.now > deadline
|
|
97
|
+
puts 'waiting for the notarization...'
|
|
98
|
+
sleep interval
|
|
99
|
+
when 'Invalid', 'Rejected'
|
|
100
|
+
raise Error, "the notarization is #{status}:\n" + notarization_issues(id).join("\n")
|
|
101
|
+
else
|
|
102
|
+
raise Error, "unknown status of the notarization: #{status.inspect}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# In Progress, Accepted, Invalid or Rejected.
|
|
108
|
+
#
|
|
109
|
+
def notarization_status(id)
|
|
110
|
+
get("/notary/v2/submissions/#{id.downcase}").dig 'data', 'attributes', 'status'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The issues Apple found in the app, in the log of the notarization.
|
|
114
|
+
#
|
|
115
|
+
def notarization_issues(id)
|
|
116
|
+
url = get("/notary/v2/submissions/#{id.downcase}/logs")
|
|
117
|
+
.dig 'data', 'attributes', 'developerLogUrl'
|
|
118
|
+
log = JSON.parse Net::HTTP.get(URI url)
|
|
119
|
+
(log['issues'] || []).map {"#{_1['path']}: #{_1['message']}"}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def get(path)
|
|
125
|
+
uri = URI HOST + path
|
|
126
|
+
res = Net::HTTP.start uri.host, uri.port, use_ssl: true do |http|
|
|
127
|
+
http.get uri.path, 'Authorization' => "Bearer #{token}"
|
|
128
|
+
end
|
|
129
|
+
raise Error, "failed to ask App Store Connect: #{res.code} #{res.body}" unless
|
|
130
|
+
res.is_a? Net::HTTPSuccess
|
|
131
|
+
JSON.parse res.body
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The JSON Web Token of App Store Connect, signed by ES256, whose
|
|
135
|
+
# signature is the raw r and s, not the DER that OpenSSL makes.
|
|
136
|
+
#
|
|
137
|
+
def token()
|
|
138
|
+
creds = @credentials
|
|
139
|
+
key = OpenSSL::PKey.read(creds[:key_p8] || File.read(File.expand_path creds[:key_path]))
|
|
140
|
+
now = Time.now.to_i
|
|
141
|
+
header = {alg: 'ES256', kid: creds[:key_id], typ: 'JWT'}
|
|
142
|
+
payload = {iss: creds[:issuer_id], iat: now, exp: now + 10 * 60, aud: 'appstoreconnect-v1'}
|
|
143
|
+
input = [header, payload].map {base64url _1.to_json}.join '.'
|
|
144
|
+
r, s = OpenSSL::ASN1.decode(key.sign 'SHA256', input).value
|
|
145
|
+
.map {_1.value.to_s(2).rjust 32, "\0"}
|
|
146
|
+
"#{input}.#{base64url r + s}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Base64 for URLs with no padding, not by the base64 gem, which is no
|
|
150
|
+
# longer a default gem.
|
|
151
|
+
#
|
|
152
|
+
def base64url(str)
|
|
153
|
+
[str].pack('m0').tr('+/', '-_').delete('=')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
end# AppStoreConnect
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
end# Packager
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
end# Reflex
|