reflex-packager 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/package.yml +310 -0
  3. data/.github/workflows/release-gem.yml +2 -49
  4. data/.github/workflows/tag.yml +11 -28
  5. data/.github/workflows/test.yml +16 -37
  6. data/.gitignore +2 -0
  7. data/ChangeLog.md +28 -0
  8. data/Gemfile.lock +26 -0
  9. data/README.md +232 -27
  10. data/Rakefile +21 -0
  11. data/VERSION +1 -1
  12. data/bin/reflex +2 -32
  13. data/examples/hello/icon.png +0 -0
  14. data/examples/hello/main.rb +18 -0
  15. data/examples/hello/reflex.yml +3 -0
  16. data/lib/reflex/packager/app_store_connect.rb +162 -0
  17. data/lib/reflex/packager/cli.rb +42 -9
  18. data/lib/reflex/packager/config.rb +233 -36
  19. data/lib/reflex/packager/data_compiler.rb +59 -0
  20. data/lib/reflex/packager/data_file.rb +183 -0
  21. data/lib/reflex/packager/data_loader.rb +127 -0
  22. data/lib/reflex/packager/gems.rb +282 -0
  23. data/lib/reflex/packager/library.rb +120 -0
  24. data/lib/reflex/packager/macos.rb +592 -94
  25. data/lib/reflex/packager/platform.rb +289 -10
  26. data/lib/reflex/packager/profile.rb +38 -26
  27. data/lib/reflex/packager/profiles/reflex.rb +43 -0
  28. data/lib/reflex/packager/rake.rb +115 -0
  29. data/lib/reflex/packager/stores/app_store.rb +60 -0
  30. data/lib/reflex/packager/stores/itchio.rb +53 -0
  31. data/lib/reflex/packager/templates/macos/AppStore.entitlements.erb +8 -0
  32. data/lib/reflex/packager/templates/macos/AppStoreExportOptions.plist.erb +14 -0
  33. data/lib/reflex/packager/templates/macos/ExportOptions.plist.erb +20 -0
  34. data/lib/reflex/packager/templates/macos/InfoPlist.strings.erb +5 -0
  35. data/lib/reflex/packager/templates/macos/boot.rb.erb +29 -0
  36. data/lib/reflex/packager/templates/macos/compile.rb.erb +7 -0
  37. data/lib/reflex/packager/templates/macos/main.mm.erb +10 -7
  38. data/lib/reflex/packager/templates/macos/project.yml.erb +60 -3
  39. data/lib/reflex/packager/templates/windows/app.manifest.erb +25 -0
  40. data/lib/reflex/packager/templates/windows/app.rc.erb +38 -0
  41. data/lib/reflex/packager/templates/windows/boot.rb.erb +32 -0
  42. data/lib/reflex/packager/templates/windows/main.cpp.erb +47 -0
  43. data/lib/reflex/packager/templates/windows/runtime.manifest.erb +7 -0
  44. data/lib/reflex/packager/windows.rb +370 -0
  45. data/lib/reflex/packager.rb +18 -1
  46. data/reflex-packager.gemspec +8 -7
  47. data/test/helper.rb +50 -5
  48. data/test/test_packager_app_store_connect.rb +108 -0
  49. data/test/test_packager_cli.rb +38 -3
  50. data/test/test_packager_config.rb +166 -27
  51. data/test/test_packager_data_file.rb +96 -0
  52. data/test/test_packager_data_loader.rb +114 -0
  53. data/test/test_packager_library.rb +81 -0
  54. data/test/test_packager_macos.rb +714 -86
  55. data/test/test_packager_platform.rb +16 -0
  56. data/test/test_packager_rake.rb +165 -0
  57. data/test/test_packager_stores_app_store.rb +66 -0
  58. data/test/test_packager_stores_itchio.rb +38 -0
  59. data/test/test_packager_windows.rb +685 -0
  60. metadata +72 -15
  61. data/.github/workflows/utils.rb +0 -127
  62. data/lib/reflex/packager/templates/macos/Podfile.erb +0 -30
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Reflex Packager - Package Reflex apps as native macOS bundles
1
+ # Reflex Packager - Package Reflex apps as native macOS and Windows apps
2
2
 
3
3
  ![License](https://img.shields.io/github/license/xord/reflex-packager)
4
4
  ![Gem Version](https://badge.fury.io/rb/reflex-packager.svg)
@@ -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 macOS `.app` bundles. It generates an Xcode project, fetches [CRuby](https://github.com/xord/cruby) and Reflex via CocoaPods, and builds a self-contained application that embeds the Ruby runtime.
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
- - [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)
28
- - [CocoaPods](https://cocoapods.org/) (`brew install cocoapods`)
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 a macOS app
66
+ ### Package as an app
62
67
 
63
68
  ```bash
64
69
  $ cd myapp
65
70
  $ reflex package .
66
71
  ```
67
72
 
68
- The built `.app` bundle is placed in `dist/`.
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: macos)
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
- # pods:
116
- # cruby:
117
- # path: /path/to/cruby
118
- # reflex:
119
- # path: /path/to/reflex
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.codesign.identity` | `-` | Code signing identity |
133
- | `macos.codesign.team_id` | none | Development team ID |
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
- ### Pod overrides
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
- By default the packager fetches CRuby and Reflex pods from their git repositories. To use local checkouts instead, set paths in the config or via the `REFLEX_PODS_PATH` environment variable:
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 REFLEX_PODS_PATH=/path/to/pods
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
- 1. Copies application files into a build directory
147
- 2. Generates an Xcode project (via XcodeGen) and a Podfile
148
- 3. Runs `pod install` to fetch CRuby and Reflex pods
149
- 4. Builds the `.app` bundle with `xcodebuild`
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
- The generated native wrapper embeds CRuby, registers the Reflex extensions, and runs the application's `main.rb` at launch.
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.2.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
- profile = Reflex::Packager::Profile.new(
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,3 @@
1
+ name: Hello
2
+ version: 1.0.0
3
+ icon: icon.png
@@ -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