zui 0.0.7 → 0.0.8
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/README.md +33 -25
- data/lib/zui/application_runtime.rb +43 -0
- data/lib/zui/cli.rb +31 -13
- data/lib/zui/component_registry.rb +14 -1
- data/lib/zui/dist_packager.rb +10 -5
- data/lib/zui/distribution.rb +113 -9
- data/lib/zui/full_runtime.rb +241 -0
- data/lib/zui/generator.rb +11 -0
- data/lib/zui/lite_entry.rb +11 -0
- data/lib/zui/lite_runtime.rb +119 -0
- data/lib/zui/lite_source.rb +38 -0
- data/lib/zui/lite_support.rb +21 -0
- data/lib/zui/node.rb +23 -2
- data/lib/zui.rb +5 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea2305d12a3ec2d53ad0edb93e6b87e3933b3ba84df4a694e08218598e9fc207
|
|
4
|
+
data.tar.gz: 4dd9ab11ce4db9e34f29f22e1c3831f28e1efbba1f8d33015e093a504f05be08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed270c4a23c21f39f84659bdbdfcdc3ec20bf498b2199a19926d2a9105a95d696aec76802183a1fd5037878ee732043a81827a6e865991cf0861c8df9f7776f7
|
|
7
|
+
data.tar.gz: 900bd4d8baca403284a9fabd3bc29c2bd263542cdd60184f2df9f1b117706d378bc0db5698628545abdbcbedbfd769ba4fe4b5947c27cda4c242a21c21b7adbe
|
data/README.md
CHANGED
|
@@ -56,8 +56,8 @@ a window.
|
|
|
56
56
|
|
|
57
57
|
## Quick start
|
|
58
58
|
|
|
59
|
-
Install Ruby 3.1 or newer and the gem. Zui downloads its version-matched native client
|
|
60
|
-
you explicitly configure it:
|
|
59
|
+
Install Ruby 3.1 or newer and the gem. Zui downloads its version-matched native client and small
|
|
60
|
+
mruby bundle runtime only when you explicitly configure it:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
gem install zui
|
|
@@ -70,9 +70,9 @@ zui run main.rb
|
|
|
70
70
|
That is the complete development setup. You do not need a Qt SDK, CMake, a C++ compiler, or a
|
|
71
71
|
system-wide Qt installation.
|
|
72
72
|
|
|
73
|
-
`zui doctor --fix` downloads the native client for the installed Zui
|
|
74
|
-
SHA-256
|
|
75
|
-
shell startup files or global Qt environment variables.
|
|
73
|
+
`zui doctor --fix` downloads the native client and lite mruby runtime for the installed Zui
|
|
74
|
+
version, verifies their SHA-256 checksums and manifests, and activates them atomically in the user
|
|
75
|
+
cache. It does not modify shell startup files or global Qt environment variables.
|
|
76
76
|
|
|
77
77
|
## Your first application
|
|
78
78
|
|
|
@@ -178,10 +178,9 @@ matching native client.
|
|
|
178
178
|
| macOS | Intel x86-64 | `zui-client-macos-x86_64` | Standard `.app` bundle | Supported and CI verified |
|
|
179
179
|
| Windows | x86-64 | `zui-client-windows-x86_64` | Portable application directory | Supported and CI verified |
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
`
|
|
183
|
-
|
|
184
|
-
`ZUI_RUBY` at that executable.
|
|
181
|
+
Application bundles do not require Ruby on the destination. The default `--lite` mode embeds Zui's
|
|
182
|
+
versioned mruby runtime. `--full` embeds a private CRuby plus only the non-Zui gems resolved by the
|
|
183
|
+
project's `Gemfile.lock`.
|
|
185
184
|
|
|
186
185
|
Unsupported architectures fail explicitly during configuration. Zui never silently compiles Qt,
|
|
187
186
|
uses a system Qt installation, or downloads an asset for a different platform. See the complete
|
|
@@ -189,7 +188,7 @@ uses a system Qt installation, or downloads an asset for a different platform. S
|
|
|
189
188
|
|
|
190
189
|
## Component catalog
|
|
191
190
|
|
|
192
|
-
Zui 0.0.
|
|
191
|
+
Zui 0.0.8 registers **241 built-in components**. Every entry has a named Ruby builder method,
|
|
193
192
|
property and event schema, native QML renderer, reactive patch support, and contract tests.
|
|
194
193
|
|
|
195
194
|
| Category | Count | Representative components |
|
|
@@ -218,12 +217,13 @@ an explicit error.
|
|
|
218
217
|
|
|
219
218
|
| Command | Purpose |
|
|
220
219
|
| --- | --- |
|
|
221
|
-
| `zui new NAME` | Generate a pure-Ruby application
|
|
220
|
+
| `zui new NAME` | Generate a pure-Ruby application, `Gemfile`, distribution config, and reusable UI module |
|
|
222
221
|
| `zui doctor` | Report Ruby, platform, native-client, run, and bundle readiness without changing anything |
|
|
223
|
-
| `zui doctor --fix` | Download, verify, and install the missing
|
|
224
|
-
| `zui configure` | Perform the same explicit
|
|
222
|
+
| `zui doctor --fix` | Download, verify, and install the missing native client and lite mruby runtime |
|
|
223
|
+
| `zui configure` | Perform the same explicit runtime installation directly |
|
|
225
224
|
| `zui run FILE` | Launch a Ruby entry point through the private native client |
|
|
226
|
-
| `zui bundle [DIRECTORY]` |
|
|
225
|
+
| `zui bundle [DIRECTORY]` | Build the default standalone `--lite` bundle with embedded mruby |
|
|
226
|
+
| `zui bundle --full [DIRECTORY]` | Embed private CRuby and only the gems locked by the project |
|
|
227
227
|
| `zui bundle --name NAME --output PATH` | Override the generated product name and destination |
|
|
228
228
|
| `zui bundle --no-tree-shake` | Retain the complete component and Qt feature catalog for metaprogrammed applications |
|
|
229
229
|
| `zui bundle --dist [DIRECTORY]` | Build release installers from the required project-root `config.rb` |
|
|
@@ -284,11 +284,12 @@ zui bundle
|
|
|
284
284
|
zui bundle path/to/application --name "Telemetry Console"
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
Every distribution combines
|
|
287
|
+
Every distribution combines four deliberately separate payloads:
|
|
288
288
|
|
|
289
289
|
```text
|
|
290
290
|
application Ruby source and assets
|
|
291
291
|
+ Zui Ruby/QML framework runtime
|
|
292
|
+
+ selected private Ruby runtime (`mruby` or `cruby`)
|
|
292
293
|
+ configured native Qt/QML client
|
|
293
294
|
```
|
|
294
295
|
|
|
@@ -298,8 +299,14 @@ application Ruby source and assets
|
|
|
298
299
|
| macOS | Standard `.app` directory | Sign, notarize, and distribute with the application's release identity |
|
|
299
300
|
| Windows | Self-contained directory with `run.cmd` | MSIX, MSI, WiX, Inno Setup, or another installer |
|
|
300
301
|
|
|
301
|
-
No system Qt installation is used by the finished bundle. Signing, notarization, installer
|
|
302
|
-
store submission, and application identity remain the release owner's responsibility.
|
|
302
|
+
No system Ruby or Qt installation is used by the finished bundle. Signing, notarization, installer
|
|
303
|
+
format, store submission, and application identity remain the release owner's responsibility.
|
|
304
|
+
|
|
305
|
+
`--lite` is the default. It compiles the project's local Ruby source into one mruby-compatible
|
|
306
|
+
program and rejects external gem `require` calls with a `--full` hint. Use `--full` for ordinary
|
|
307
|
+
CRuby behavior or third-party gems. Full bundles require a locked project `Gemfile`; run
|
|
308
|
+
`bundle install` after changing dependencies. Both modes are built for the current target OS and
|
|
309
|
+
architecture, and both use the same project-specific QML/native tree-shaking pass.
|
|
303
310
|
|
|
304
311
|
`zui bundle` statically analyzes every production Ruby source file, including code in conditional
|
|
305
312
|
branches, and retains only the referenced Zui adapters, QML modules, plugins, and native library
|
|
@@ -354,10 +361,9 @@ directory, and existing artifacts are never overwritten. Linux RPM creation requ
|
|
|
354
361
|
(`rpm-build` or `rpm-tools`), macOS uses the system `hdiutil`, and Windows requires Inno Setup 6's
|
|
355
362
|
`ISCC.exe` on `PATH`.
|
|
356
363
|
|
|
357
|
-
The installers carry the application, Zui, Qt, and
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
release-owner steps.
|
|
364
|
+
The installers carry the application, selected Ruby runtime, Zui, Qt, and selected native
|
|
365
|
+
dependencies. They do not declare or require system Ruby. Code signing, Apple notarization, and
|
|
366
|
+
Windows Authenticode signing remain release-owner steps.
|
|
361
367
|
|
|
362
368
|
## Showcase applications
|
|
363
369
|
|
|
@@ -412,19 +418,21 @@ Build and install the gem directly:
|
|
|
412
418
|
|
|
413
419
|
```bash
|
|
414
420
|
gem build zui.gemspec
|
|
415
|
-
gem install ./zui
|
|
421
|
+
gem install ./zui-*.gem
|
|
416
422
|
```
|
|
417
423
|
|
|
418
|
-
Release CI additionally
|
|
419
|
-
repairs a fresh
|
|
424
|
+
Release CI additionally builds the pinned mruby runtime, audits both release archives, installs the
|
|
425
|
+
built gem, repairs a fresh cache, and launches real `--lite` and `--full` bundles on every supported
|
|
426
|
+
target.
|
|
420
427
|
|
|
421
428
|
## Troubleshooting
|
|
422
429
|
|
|
423
430
|
| Symptom | Resolution |
|
|
424
431
|
| --- | --- |
|
|
425
432
|
| `Client: not configured` | Run `zui doctor --fix` once for the installed Zui version |
|
|
433
|
+
| `Lite runtime: not configured` | Run `zui doctor --fix` to install the checksummed mruby archive |
|
|
426
434
|
| Platform or architecture is unsupported | Use one of the release-gated targets above or add a verified native-client runner and artifact |
|
|
427
|
-
|
|
|
435
|
+
| `--lite` rejects a gem `require` | Use `--full` and lock the gem in the project `Gemfile.lock` |
|
|
428
436
|
| A component reports a resource or module error | Check the declared asset path and run `zui doctor`; Zui will not substitute another component |
|
|
429
437
|
| Audio, camera, or capture is unavailable | Confirm OS permissions, devices, codecs, and platform media services |
|
|
430
438
|
| The native client looks stale after a framework update | Run `zui doctor --fix`; client caches are isolated by Zui version |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Zui
|
|
6
|
+
class ApplicationRuntime
|
|
7
|
+
FORMAT = 1
|
|
8
|
+
|
|
9
|
+
attr_reader :engine, :version, :executable, :environment, :gems, :program, :load_path
|
|
10
|
+
|
|
11
|
+
def initialize(engine:, version:, executable:, environment: {}, gems: [], program: nil, load_path: nil)
|
|
12
|
+
@engine = engine.to_s
|
|
13
|
+
@version = version.to_s
|
|
14
|
+
@executable = executable.to_s
|
|
15
|
+
@environment = environment.transform_keys(&:to_s).transform_values do |paths|
|
|
16
|
+
Array(paths).map(&:to_s).freeze
|
|
17
|
+
end.freeze
|
|
18
|
+
@gems = Array(gems).map(&:to_s).sort.freeze
|
|
19
|
+
@program = program&.to_s
|
|
20
|
+
@load_path = load_path&.to_s
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write(directory)
|
|
25
|
+
File.write(File.join(directory, "runtime.json"), "#{JSON.pretty_generate(to_h)}\n")
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
result = {
|
|
31
|
+
"format" => FORMAT,
|
|
32
|
+
"engine" => engine,
|
|
33
|
+
"version" => version,
|
|
34
|
+
"executable" => executable,
|
|
35
|
+
"environment" => environment,
|
|
36
|
+
"gems" => gems
|
|
37
|
+
}
|
|
38
|
+
result["program"] = program unless program.nil?
|
|
39
|
+
result["load_path"] = load_path unless load_path.nil?
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/zui/cli.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "rbconfig"
|
|
|
5
5
|
|
|
6
6
|
module Zui
|
|
7
7
|
class CLI
|
|
8
|
-
USAGE = "Usage: zui <new NAME|configure|doctor [--fix]|run FILE|bundle [--dist] [--no-tree-shake] [DIRECTORY]|version>"
|
|
8
|
+
USAGE = "Usage: zui <new NAME|configure|doctor [--fix]|run FILE|bundle [--dist] [--lite|--full] [--no-tree-shake] [DIRECTORY]|version>"
|
|
9
9
|
|
|
10
10
|
def self.run(arguments, out: $stdout, err: $stderr)
|
|
11
11
|
new(out:, err:).run(arguments.dup)
|
|
@@ -58,20 +58,24 @@ module Zui
|
|
|
58
58
|
destination = option_value(arguments, "--output")
|
|
59
59
|
create_installers = !arguments.delete("--dist").nil?
|
|
60
60
|
tree_shake = arguments.delete("--no-tree-shake").nil?
|
|
61
|
+
lite = !arguments.delete("--lite").nil?
|
|
62
|
+
full = !arguments.delete("--full").nil?
|
|
63
|
+
raise ArgumentError, "bundle accepts only one of --lite or --full" if lite && full
|
|
64
|
+
runtime_mode = full ? :full : :lite
|
|
61
65
|
source = File.expand_path(arguments.shift || Dir.pwd)
|
|
62
66
|
raise ArgumentError, "bundle accepts one directory" unless arguments.empty?
|
|
63
67
|
|
|
64
68
|
if create_installers
|
|
65
69
|
raise ArgumentError, "--name cannot be used with --dist; set name in config.rb" if name
|
|
66
70
|
|
|
67
|
-
packager = DistPackager.new(tree_shake:)
|
|
71
|
+
packager = DistPackager.new(tree_shake:, runtime_mode:)
|
|
68
72
|
paths = packager.package(source, output: destination)
|
|
69
73
|
paths.each { |path| @out.puts("Created distribution artifact #{path}") }
|
|
70
74
|
report_tree_shaking(packager.tree_shake_report)
|
|
71
75
|
return 0
|
|
72
76
|
end
|
|
73
77
|
|
|
74
|
-
distribution = Distribution.new(tree_shake:)
|
|
78
|
+
distribution = Distribution.new(tree_shake:, runtime_mode:)
|
|
75
79
|
path = distribution.bundle(source, name:, destination:)
|
|
76
80
|
@out.puts("Bundled #{Platform.current.os} application in #{path}")
|
|
77
81
|
report_tree_shaking(distribution.tree_shake_report)
|
|
@@ -94,17 +98,27 @@ module Zui
|
|
|
94
98
|
return 1 unless platform.supported?
|
|
95
99
|
|
|
96
100
|
client = Client.new(platform:)
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
lite_runtime = LiteRuntime.new(platform:)
|
|
102
|
+
client_ready = client.configured?
|
|
103
|
+
lite_ready = lite_runtime.configured?
|
|
104
|
+
if client_ready && lite_ready
|
|
105
|
+
report_ready(client, lite_runtime)
|
|
99
106
|
0
|
|
100
107
|
elsif fix
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
unless client_ready
|
|
109
|
+
@out.puts("Repair: downloading the verified native client from GitHub Releases...")
|
|
110
|
+
client.configure!
|
|
111
|
+
end
|
|
112
|
+
unless lite_ready
|
|
113
|
+
@out.puts("Repair: downloading the verified lite mruby runtime from GitHub Releases...")
|
|
114
|
+
lite_runtime.configure!
|
|
115
|
+
end
|
|
116
|
+
report_ready(client, lite_runtime)
|
|
104
117
|
0
|
|
105
118
|
else
|
|
106
|
-
@out.puts("Client: not configured")
|
|
107
|
-
@out.puts(
|
|
119
|
+
@out.puts(client_ready ? "Client: #{client.root}" : "Client: not configured")
|
|
120
|
+
@out.puts(lite_ready ? "Lite runtime: #{lite_runtime.root}" : "Lite runtime: not configured")
|
|
121
|
+
@out.puts("Run `zui doctor --fix` to install the native client and lite bundle runtime.")
|
|
108
122
|
1
|
|
109
123
|
end
|
|
110
124
|
end
|
|
@@ -113,16 +127,20 @@ module Zui
|
|
|
113
127
|
raise ArgumentError, "configure accepts no arguments" unless arguments.empty?
|
|
114
128
|
platform = Platform.current.assert_supported!
|
|
115
129
|
client = Client.new(platform:)
|
|
130
|
+
lite_runtime = LiteRuntime.new(platform:)
|
|
116
131
|
@out.puts("Configuring Zui #{VERSION} for #{platform.id}...")
|
|
117
132
|
client.configure!
|
|
118
|
-
|
|
133
|
+
lite_runtime.configure!
|
|
134
|
+
report_ready(client, lite_runtime)
|
|
119
135
|
0
|
|
120
136
|
end
|
|
121
137
|
|
|
122
|
-
def report_ready(client)
|
|
138
|
+
def report_ready(client, lite_runtime)
|
|
123
139
|
@out.puts("Client: #{client.root}")
|
|
140
|
+
@out.puts("Lite runtime: #{lite_runtime.root}")
|
|
124
141
|
@out.puts("Run: ready")
|
|
125
|
-
@out.puts("Bundle: ready")
|
|
142
|
+
@out.puts("Bundle --lite: ready")
|
|
143
|
+
@out.puts("Bundle --full: ready (uses this Ruby and the project's locked gems)")
|
|
126
144
|
end
|
|
127
145
|
|
|
128
146
|
def option_value(arguments, name)
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zui
|
|
4
|
-
Component
|
|
4
|
+
class Component
|
|
5
|
+
attr_reader :name, :qml, :properties, :events, :property_map, :event_map, :container, :auto_bind
|
|
6
|
+
|
|
7
|
+
def initialize(name:, qml:, properties:, events:, property_map:, event_map:, container:, auto_bind:)
|
|
8
|
+
@name = name
|
|
9
|
+
@qml = qml
|
|
10
|
+
@properties = properties
|
|
11
|
+
@events = events
|
|
12
|
+
@property_map = property_map
|
|
13
|
+
@event_map = event_map
|
|
14
|
+
@container = container
|
|
15
|
+
@auto_bind = auto_bind
|
|
16
|
+
end
|
|
17
|
+
|
|
5
18
|
def to_h
|
|
6
19
|
{
|
|
7
20
|
"qml" => qml,
|
data/lib/zui/dist_packager.rb
CHANGED
|
@@ -9,16 +9,22 @@ require "zlib"
|
|
|
9
9
|
|
|
10
10
|
module Zui
|
|
11
11
|
class DistPackager
|
|
12
|
-
attr_reader :platform, :config, :tree_shake_report
|
|
12
|
+
attr_reader :platform, :config, :tree_shake_report, :runtime_mode
|
|
13
13
|
|
|
14
14
|
def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT,
|
|
15
|
-
ruby: RbConfig.ruby, tree_shake: true, environment: ENV
|
|
15
|
+
ruby: RbConfig.ruby, tree_shake: true, environment: ENV, runtime_mode: :lite,
|
|
16
|
+
runtime_builder: nil)
|
|
16
17
|
@platform = platform.assert_supported!
|
|
17
18
|
@client = client || Client.new(platform: @platform)
|
|
18
19
|
@framework_root = framework_root
|
|
19
20
|
@ruby = File.expand_path(ruby)
|
|
20
21
|
@tree_shake = tree_shake == true
|
|
22
|
+
@runtime_mode = runtime_mode.to_sym
|
|
23
|
+
unless Distribution::RUNTIME_MODES.include?(@runtime_mode)
|
|
24
|
+
raise ArgumentError, "unsupported application runtime: #{runtime_mode}"
|
|
25
|
+
end
|
|
21
26
|
@environment = environment.to_h
|
|
27
|
+
@runtime_builder = runtime_builder
|
|
22
28
|
@config = nil
|
|
23
29
|
@tree_shake_report = nil
|
|
24
30
|
end
|
|
@@ -62,7 +68,8 @@ module Zui
|
|
|
62
68
|
end
|
|
63
69
|
distribution = Distribution.new(
|
|
64
70
|
client: @client, platform:, framework_root: @framework_root, ruby: @ruby,
|
|
65
|
-
tree_shake: @tree_shake, release_config: config
|
|
71
|
+
tree_shake: @tree_shake, release_config: config, runtime_mode:,
|
|
72
|
+
runtime_builder: @runtime_builder
|
|
66
73
|
)
|
|
67
74
|
distribution.bundle(project, name: config.name, destination:)
|
|
68
75
|
@tree_shake_report = distribution.tree_shake_report
|
|
@@ -164,7 +171,6 @@ module Zui
|
|
|
164
171
|
"Architecture: #{deb_architecture}",
|
|
165
172
|
"Maintainer: #{config.publisher}",
|
|
166
173
|
"Installed-Size: #{installed_size}",
|
|
167
|
-
"Depends: ruby (>= 3.1)",
|
|
168
174
|
"Description: #{config.description}"
|
|
169
175
|
]
|
|
170
176
|
values.insert(-2, "Homepage: #{config.homepage}") if config.homepage
|
|
@@ -196,7 +202,6 @@ module Zui
|
|
|
196
202
|
License: #{rpm_escape(config.license)}
|
|
197
203
|
#{homepage}BuildArch: #{rpm_architecture}
|
|
198
204
|
AutoReqProv: no
|
|
199
|
-
Requires: ruby >= 3.1
|
|
200
205
|
|
|
201
206
|
%description
|
|
202
207
|
#{description}
|
data/lib/zui/distribution.rb
CHANGED
|
@@ -6,17 +6,26 @@ require "rbconfig"
|
|
|
6
6
|
|
|
7
7
|
module Zui
|
|
8
8
|
class Distribution
|
|
9
|
-
|
|
9
|
+
RUNTIME_MODES = %i[lite full].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :platform, :tree_shake_report, :runtime_mode
|
|
10
12
|
|
|
11
13
|
def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT,
|
|
12
|
-
ruby: RbConfig.ruby, tree_shake: true, release_config: nil
|
|
14
|
+
ruby: RbConfig.ruby, tree_shake: true, release_config: nil, runtime_mode: :lite,
|
|
15
|
+
runtime_builder: nil)
|
|
13
16
|
@platform = platform.assert_supported!
|
|
14
17
|
@client = client || Client.new(platform: @platform)
|
|
15
18
|
@framework_root = framework_root
|
|
16
19
|
@ruby = File.expand_path(ruby)
|
|
17
20
|
@tree_shake = tree_shake == true
|
|
21
|
+
@runtime_mode = runtime_mode.to_sym
|
|
22
|
+
unless RUNTIME_MODES.include?(@runtime_mode)
|
|
23
|
+
raise ArgumentError, "unsupported application runtime: #{runtime_mode}"
|
|
24
|
+
end
|
|
18
25
|
@tree_shake_report = nil
|
|
19
26
|
@release_config = release_config
|
|
27
|
+
@runtime_builder = runtime_builder
|
|
28
|
+
@application_runtime = nil
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def bundle(source, name: nil, destination: nil)
|
|
@@ -52,6 +61,7 @@ module Zui
|
|
|
52
61
|
File.join(destination, "runtime"), File.join(destination, "share", "applications")])
|
|
53
62
|
install_application(project, File.join(destination, "app"))
|
|
54
63
|
install_runtime(File.join(destination, "runtime"))
|
|
64
|
+
install_application_runtime(project, File.join(destination, "runtime"))
|
|
55
65
|
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
56
66
|
shake_bundle(project, File.join(destination, "runtime"))
|
|
57
67
|
write_linux_launcher(destination, app_name)
|
|
@@ -74,6 +84,7 @@ module Zui
|
|
|
74
84
|
FileUtils.mkdir_p([macos, File.join(resources, "app"), File.join(resources, "runtime")])
|
|
75
85
|
install_application(project, File.join(resources, "app"))
|
|
76
86
|
install_runtime(File.join(resources, "runtime"))
|
|
87
|
+
install_application_runtime(project, File.join(resources, "runtime"))
|
|
77
88
|
@client.copy_to(File.join(resources, "runtime", "native"))
|
|
78
89
|
shake_bundle(project, File.join(resources, "runtime"))
|
|
79
90
|
icon_name = install_macos_release_icon(resources)
|
|
@@ -88,11 +99,16 @@ module Zui
|
|
|
88
99
|
File.join(destination, "runtime")])
|
|
89
100
|
install_application(project, File.join(destination, "app"))
|
|
90
101
|
install_runtime(File.join(destination, "runtime"))
|
|
102
|
+
install_application_runtime(project, File.join(destination, "runtime"))
|
|
91
103
|
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
92
104
|
shake_bundle(project, File.join(destination, "runtime"))
|
|
93
105
|
install_windows_release_icon(destination)
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
if @application_runtime
|
|
107
|
+
File.write(File.join(destination, "run.cmd"), windows_private_runtime_launcher(app_name))
|
|
108
|
+
else
|
|
109
|
+
File.write(File.join(destination, "run.rb"), windows_ruby_launcher(app_name))
|
|
110
|
+
File.write(File.join(destination, "run.cmd"), windows_command_launcher)
|
|
111
|
+
end
|
|
96
112
|
write_manifest(destination, app_name)
|
|
97
113
|
end
|
|
98
114
|
|
|
@@ -116,6 +132,15 @@ module Zui
|
|
|
116
132
|
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, target) unless entries.empty?
|
|
117
133
|
end
|
|
118
134
|
|
|
135
|
+
def install_application_runtime(project, destination)
|
|
136
|
+
builder = @runtime_builder || if runtime_mode == :full
|
|
137
|
+
FullRuntime.new(platform:, ruby: @ruby)
|
|
138
|
+
else
|
|
139
|
+
LiteRuntime.new(platform:)
|
|
140
|
+
end
|
|
141
|
+
@application_runtime = builder.install(project:, destination: File.join(destination, "ruby"))
|
|
142
|
+
end
|
|
143
|
+
|
|
119
144
|
def write_linux_launcher(destination, app_name)
|
|
120
145
|
File.write(File.join(destination, "run"), <<~SH)
|
|
121
146
|
#!/bin/sh
|
|
@@ -127,9 +152,9 @@ module Zui
|
|
|
127
152
|
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
128
153
|
--qml-root "$bundle_dir/runtime/qml" \
|
|
129
154
|
--project "$bundle_dir/app" \
|
|
130
|
-
--program "$bundle_dir
|
|
155
|
+
--program #{posix_program("$bundle_dir")} \
|
|
131
156
|
--ruby "$ruby_command" \
|
|
132
|
-
--load-path "$bundle_dir
|
|
157
|
+
--load-path #{posix_load_path("$bundle_dir")} \
|
|
133
158
|
--name #{shell_quote(app_name)}
|
|
134
159
|
SH
|
|
135
160
|
FileUtils.chmod(0o755, File.join(destination, "run"))
|
|
@@ -147,9 +172,9 @@ module Zui
|
|
|
147
172
|
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
148
173
|
--qml-root "$resources/runtime/qml" \
|
|
149
174
|
--project "$resources/app" \
|
|
150
|
-
--program "$resources
|
|
175
|
+
--program #{posix_program("$resources")} \
|
|
151
176
|
--ruby "$ruby_command" \
|
|
152
|
-
--load-path "$resources
|
|
177
|
+
--load-path #{posix_load_path("$resources")} \
|
|
153
178
|
--name #{shell_quote(app_name)}
|
|
154
179
|
SH
|
|
155
180
|
end
|
|
@@ -190,6 +215,35 @@ module Zui
|
|
|
190
215
|
CMD
|
|
191
216
|
end
|
|
192
217
|
|
|
218
|
+
def windows_private_runtime_launcher(app_name)
|
|
219
|
+
executable = @application_runtime.executable.tr("/", "\\")
|
|
220
|
+
host = @client.executable_relative_path.tr("/", "\\")
|
|
221
|
+
environment = @application_runtime.environment.map do |name, paths|
|
|
222
|
+
value = paths.map { |path| "%ruby_root%\\#{path.tr('/', '\\')}" }.join(";")
|
|
223
|
+
if name == "PATH"
|
|
224
|
+
%(set "PATH=#{value};%PATH%")
|
|
225
|
+
else
|
|
226
|
+
%(set "#{name}=#{value}")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
environment.unshift(%(set "PATH=%ruby_root%\\bin;%ruby_root%\\lib;%PATH%"))
|
|
230
|
+
<<~CMD.gsub("\n", "\r\n")
|
|
231
|
+
@echo off
|
|
232
|
+
setlocal
|
|
233
|
+
set "bundle_dir=%~dp0"
|
|
234
|
+
set "ruby_root=%bundle_dir%runtime\\ruby"
|
|
235
|
+
#{environment.join("\n")}
|
|
236
|
+
"%bundle_dir%runtime\\native\\#{host}" ^
|
|
237
|
+
--qml-root "%bundle_dir%runtime\\qml" ^
|
|
238
|
+
--project "%bundle_dir%app" ^
|
|
239
|
+
--program #{windows_program} ^
|
|
240
|
+
--ruby "%ruby_root%\\#{executable}" ^
|
|
241
|
+
--load-path #{windows_load_path} ^
|
|
242
|
+
--name #{windows_quote(app_name)}
|
|
243
|
+
exit /b %ERRORLEVEL%
|
|
244
|
+
CMD
|
|
245
|
+
end
|
|
246
|
+
|
|
193
247
|
def info_plist(app_name, icon_name: nil)
|
|
194
248
|
identifier = @release_config&.identifier || "dev.zui.#{slug(app_name).tr("-", ".")}"
|
|
195
249
|
application_version = @release_config&.version || VERSION
|
|
@@ -215,7 +269,7 @@ module Zui
|
|
|
215
269
|
"format" => 1, "framework" => "zui", "version" => VERSION,
|
|
216
270
|
"platform" => platform.os.to_s, "architecture" => platform.arch.to_s,
|
|
217
271
|
"name" => app_name, "client_version" => @client.manifest.fetch("client_version"),
|
|
218
|
-
"tree_shaken" => !@tree_shake_report.nil
|
|
272
|
+
"tree_shaken" => !@tree_shake_report.nil?, "ruby_runtime" => runtime_mode.to_s
|
|
219
273
|
}
|
|
220
274
|
manifest["tree_shake"] = @tree_shake_report.to_h if @tree_shake_report
|
|
221
275
|
if @release_config
|
|
@@ -256,6 +310,7 @@ module Zui
|
|
|
256
310
|
def titleize(value) = value.split(/[-_]/).map(&:capitalize).join(" ")
|
|
257
311
|
def slug(value) = value.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
258
312
|
def shell_quote(value) = "'#{value.to_s.gsub("'", %q('"'"'))}'"
|
|
313
|
+
def windows_quote(value) = %Q("#{value.to_s.gsub('%', '%%').gsub('"', '""')}")
|
|
259
314
|
def xml_escape(value) = value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
260
315
|
|
|
261
316
|
def posix_client_environment
|
|
@@ -270,6 +325,8 @@ module Zui
|
|
|
270
325
|
end
|
|
271
326
|
|
|
272
327
|
def posix_ruby_command
|
|
328
|
+
return posix_private_ruby_command if @application_runtime
|
|
329
|
+
|
|
273
330
|
<<~SH.chomp
|
|
274
331
|
ruby_command=${ZUI_RUBY:-}
|
|
275
332
|
if [ -z "$ruby_command" ]; then
|
|
@@ -284,6 +341,53 @@ module Zui
|
|
|
284
341
|
SH
|
|
285
342
|
end
|
|
286
343
|
|
|
344
|
+
def posix_private_ruby_command
|
|
345
|
+
lines = ["ruby_root=\"$bundle_dir/runtime/ruby\""]
|
|
346
|
+
if platform.macos?
|
|
347
|
+
lines[0] = "ruby_root=\"$resources/runtime/ruby\""
|
|
348
|
+
end
|
|
349
|
+
@application_runtime.environment.each do |name, paths|
|
|
350
|
+
value = paths.map { |path| "${ruby_root}/#{path}" }.join(File::PATH_SEPARATOR)
|
|
351
|
+
if %w[LD_LIBRARY_PATH DYLD_LIBRARY_PATH].include?(name)
|
|
352
|
+
lines << "zui_environment=\"#{value}\""
|
|
353
|
+
lines << "[ -z \"${#{name}:-}\" ] || zui_environment=\"$zui_environment:${#{name}}\""
|
|
354
|
+
lines << "export #{name}=\"$zui_environment\""
|
|
355
|
+
else
|
|
356
|
+
lines << "export #{name}=\"#{value}\""
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
lines << "ruby_command=\"$ruby_root/#{@application_runtime.executable}\""
|
|
360
|
+
lines.join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def posix_program(bundle_root)
|
|
364
|
+
relative = @application_runtime&.program
|
|
365
|
+
relative ? %("#{bundle_root}/runtime/ruby/#{relative}") : %("#{bundle_root}/app/main.rb")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def posix_load_path(bundle_root)
|
|
369
|
+
relative = @application_runtime&.load_path
|
|
370
|
+
return %("#{bundle_root}/runtime/lib") if relative.nil?
|
|
371
|
+
return '""' if relative.empty?
|
|
372
|
+
|
|
373
|
+
%("#{bundle_root}/runtime/ruby/#{relative}")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def windows_program
|
|
377
|
+
relative = @application_runtime&.program
|
|
378
|
+
return '"%bundle_dir%app\\main.rb"' unless relative
|
|
379
|
+
|
|
380
|
+
%("%ruby_root%\\#{relative.tr('/', '\\')}")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def windows_load_path
|
|
384
|
+
relative = @application_runtime&.load_path
|
|
385
|
+
return '"%bundle_dir%runtime\\lib"' if relative.nil?
|
|
386
|
+
return '""' if relative.empty?
|
|
387
|
+
|
|
388
|
+
%("%ruby_root%\\#{relative.tr('/', '\\')}")
|
|
389
|
+
end
|
|
390
|
+
|
|
287
391
|
def windows_client_environment = @client.environment_entries
|
|
288
392
|
|
|
289
393
|
def windows_environment_builder
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "set"
|
|
8
|
+
|
|
9
|
+
module Zui
|
|
10
|
+
class FullRuntime
|
|
11
|
+
SYSTEM_LIBRARIES = /\A(?:ld-linux|libc\.|libdl\.|libgcc_s\.|libm\.|libpthread\.|librt\.|libSystem\.|libutil\.)/.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :platform
|
|
14
|
+
|
|
15
|
+
def initialize(platform: Platform.current, ruby: RbConfig.ruby, environment: ENV,
|
|
16
|
+
rbconfig: RbConfig::CONFIG, spec_loader: nil)
|
|
17
|
+
@platform = platform.assert_supported!
|
|
18
|
+
@ruby = File.expand_path(ruby)
|
|
19
|
+
@environment = environment.to_h
|
|
20
|
+
@rbconfig = rbconfig.to_h
|
|
21
|
+
@spec_loader = spec_loader || method(:project_specs)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def install(project:, destination:)
|
|
25
|
+
project = File.expand_path(project)
|
|
26
|
+
destination = File.expand_path(destination)
|
|
27
|
+
raise ArgumentError, "Ruby executable not found: #{@ruby}" unless File.file?(@ruby)
|
|
28
|
+
raise ArgumentError, "full runtime destination already exists: #{destination}" if File.exist?(destination)
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p(destination)
|
|
31
|
+
executable = install_executable(destination)
|
|
32
|
+
library_paths = install_standard_library(destination)
|
|
33
|
+
install_runtime_libraries(destination)
|
|
34
|
+
install_native_dependencies(destination)
|
|
35
|
+
gems = install_project_gems(project, destination)
|
|
36
|
+
environment = {
|
|
37
|
+
"RUBYLIB" => library_paths,
|
|
38
|
+
"GEM_HOME" => ["gems"],
|
|
39
|
+
"GEM_PATH" => ["gems"]
|
|
40
|
+
}
|
|
41
|
+
dynamic_library_variable = platform.macos? ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH"
|
|
42
|
+
environment[dynamic_library_variable] = ["lib"] unless platform.windows?
|
|
43
|
+
|
|
44
|
+
ApplicationRuntime.new(
|
|
45
|
+
engine: "cruby",
|
|
46
|
+
version: ruby_version,
|
|
47
|
+
executable:,
|
|
48
|
+
environment:,
|
|
49
|
+
gems: gems.map(&:full_name)
|
|
50
|
+
).write(destination)
|
|
51
|
+
rescue StandardError
|
|
52
|
+
FileUtils.remove_entry(destination) if destination && File.exist?(destination)
|
|
53
|
+
raise
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def install_executable(destination)
|
|
59
|
+
name = platform.windows? ? "ruby.exe" : "ruby"
|
|
60
|
+
target = File.join(destination, "bin", name)
|
|
61
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
62
|
+
FileUtils.cp(@ruby, target)
|
|
63
|
+
FileUtils.chmod(0o755, target) unless platform.windows?
|
|
64
|
+
strip_binary(target)
|
|
65
|
+
File.join("bin", name).tr(File::SEPARATOR, "/")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def install_standard_library(destination)
|
|
69
|
+
sources = [@rbconfig["rubylibdir"], @rbconfig["archdir"]].compact.map { |path| File.expand_path(path) }
|
|
70
|
+
sources.select! { |path| File.directory?(path) }
|
|
71
|
+
raise ArgumentError, "Ruby standard library was not found" if sources.empty?
|
|
72
|
+
|
|
73
|
+
entries = sources.map do |source|
|
|
74
|
+
{ source:, relative: runtime_relative_path(source) }
|
|
75
|
+
end
|
|
76
|
+
installed = []
|
|
77
|
+
entries.sort_by { |entry| entry.fetch(:source).length }.each do |entry|
|
|
78
|
+
source = entry.fetch(:source)
|
|
79
|
+
relative = entry.fetch(:relative)
|
|
80
|
+
target = File.join(destination, relative)
|
|
81
|
+
next if installed.any? { |entry| inside?(target, entry.fetch(:target)) }
|
|
82
|
+
|
|
83
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
84
|
+
FileUtils.cp_r(source, target)
|
|
85
|
+
installed << { source:, target:, relative: relative.tr(File::SEPARATOR, "/") }
|
|
86
|
+
end
|
|
87
|
+
entries.map { |entry| entry.fetch(:relative).tr(File::SEPARATOR, "/") }.uniq
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def install_runtime_libraries(destination)
|
|
91
|
+
patterns = if platform.windows?
|
|
92
|
+
[File.join(@rbconfig.fetch("bindir", File.dirname(@ruby)), "*.dll")]
|
|
93
|
+
else
|
|
94
|
+
libdir = @rbconfig["libdir"]
|
|
95
|
+
libdir ? [File.join(libdir, "libruby*.so*"), File.join(libdir, "libruby*.dylib")] : []
|
|
96
|
+
end
|
|
97
|
+
target = platform.windows? ? File.join(destination, "bin") : File.join(destination, "lib")
|
|
98
|
+
FileUtils.mkdir_p(target)
|
|
99
|
+
patterns.flat_map { |pattern| Dir[pattern] }.uniq.each do |source|
|
|
100
|
+
next unless File.file?(source)
|
|
101
|
+
|
|
102
|
+
FileUtils.cp(source, File.join(target, File.basename(source)))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def install_native_dependencies(destination)
|
|
107
|
+
return if platform.windows?
|
|
108
|
+
|
|
109
|
+
library_root = File.join(destination, "lib")
|
|
110
|
+
FileUtils.mkdir_p(library_root)
|
|
111
|
+
binaries = [File.join(destination, "bin", "ruby")]
|
|
112
|
+
binaries.concat(Dir[File.join(destination, "**", "*.{so,dylib}")])
|
|
113
|
+
inspected = Set.new
|
|
114
|
+
until binaries.empty?
|
|
115
|
+
binary = binaries.shift
|
|
116
|
+
next unless File.file?(binary) && inspected.add?(binary)
|
|
117
|
+
|
|
118
|
+
dependencies(binary).each do |source|
|
|
119
|
+
name = File.basename(source)
|
|
120
|
+
next if SYSTEM_LIBRARIES.match?(name)
|
|
121
|
+
|
|
122
|
+
target = File.join(library_root, name)
|
|
123
|
+
next if File.file?(target)
|
|
124
|
+
|
|
125
|
+
FileUtils.cp(source, target)
|
|
126
|
+
binaries << target
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def dependencies(binary)
|
|
132
|
+
if platform.linux?
|
|
133
|
+
output = IO.popen(["ldd", binary], err: File::NULL, &:read)
|
|
134
|
+
output.lines.filter_map do |line|
|
|
135
|
+
path = line[/=>\s+(\/\S+)/, 1] || line[/^\s*(\/\S+)/, 1]
|
|
136
|
+
path if path && File.file?(path)
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
output = IO.popen(["otool", "-L", binary], err: File::NULL, &:read)
|
|
140
|
+
output.lines.drop(1).filter_map do |line|
|
|
141
|
+
path = line.strip.split.first
|
|
142
|
+
next if path.nil? || path.start_with?("/System/", "/usr/lib/") || path.start_with?("@")
|
|
143
|
+
|
|
144
|
+
path if File.file?(path)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
rescue Errno::ENOENT
|
|
148
|
+
[]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def install_project_gems(project, destination)
|
|
152
|
+
specs = @spec_loader.call(project).reject { |spec| spec.name == "zui" || spec.default_gem? }
|
|
153
|
+
gem_home = File.join(destination, "gems")
|
|
154
|
+
FileUtils.mkdir_p([File.join(gem_home, "gems"), File.join(gem_home, "specifications")])
|
|
155
|
+
names = Set.new
|
|
156
|
+
specs.sort_by(&:full_name).each do |spec|
|
|
157
|
+
raise ArgumentError, "duplicate bundled gem: #{spec.full_name}" unless names.add?(spec.full_name)
|
|
158
|
+
unless File.directory?(spec.full_gem_path)
|
|
159
|
+
raise ArgumentError, "project gem is not installed: #{spec.full_name}; run `bundle install`"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
FileUtils.cp_r(spec.full_gem_path, File.join(gem_home, "gems", spec.full_name))
|
|
163
|
+
File.write(File.join(gem_home, "specifications", "#{spec.full_name}.gemspec"), spec.to_ruby)
|
|
164
|
+
install_gem_extensions(spec, gem_home)
|
|
165
|
+
end
|
|
166
|
+
specs
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def install_gem_extensions(spec, gem_home)
|
|
170
|
+
source = spec.extension_dir
|
|
171
|
+
return unless source && File.directory?(source)
|
|
172
|
+
|
|
173
|
+
parts = File.expand_path(source).split(File::SEPARATOR)
|
|
174
|
+
index = parts.rindex("extensions")
|
|
175
|
+
return unless index
|
|
176
|
+
|
|
177
|
+
relative = File.join(*parts[(index + 1)..])
|
|
178
|
+
target = File.join(gem_home, "extensions", relative)
|
|
179
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
180
|
+
FileUtils.cp_r(source, target)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def project_specs(project)
|
|
184
|
+
gemfile = File.join(project, "Gemfile")
|
|
185
|
+
lockfile = File.join(project, "Gemfile.lock")
|
|
186
|
+
raise ArgumentError, "Gemfile not found; add project gems before using --full" unless File.file?(gemfile)
|
|
187
|
+
unless File.file?(lockfile)
|
|
188
|
+
raise ArgumentError, "Gemfile.lock not found; run `bundle install` before using --full"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
previous = @environment["BUNDLE_GEMFILE"]
|
|
192
|
+
ENV["BUNDLE_GEMFILE"] = gemfile
|
|
193
|
+
Bundler.reset!
|
|
194
|
+
definition = Bundler::Definition.build(gemfile, lockfile, nil)
|
|
195
|
+
definition.validate_runtime!
|
|
196
|
+
definition.specs.to_a
|
|
197
|
+
rescue Bundler::BundlerError => error
|
|
198
|
+
raise ArgumentError, "project gems are not ready: #{error.message}"
|
|
199
|
+
ensure
|
|
200
|
+
previous.nil? ? ENV.delete("BUNDLE_GEMFILE") : ENV["BUNDLE_GEMFILE"] = previous
|
|
201
|
+
Bundler.reset!
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def runtime_relative_path(path)
|
|
205
|
+
prefix = File.expand_path(@rbconfig.fetch("prefix"))
|
|
206
|
+
return path.delete_prefix("#{prefix}#{File::SEPARATOR}") if inside?(path, prefix)
|
|
207
|
+
|
|
208
|
+
File.join("lib", "ruby", File.basename(path))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def ruby_version
|
|
212
|
+
@rbconfig["RUBY_PROGRAM_VERSION"] || RUBY_VERSION
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def inside?(path, parent)
|
|
216
|
+
path == parent || path.start_with?("#{parent}#{File::SEPARATOR}")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def strip_binary(path)
|
|
220
|
+
magic = File.binread(path, 4)
|
|
221
|
+
mach_o = ["\xCF\xFA\xED\xFE".b, "\xFE\xED\xFA\xCF".b, "\xCA\xFE\xBA\xBE".b]
|
|
222
|
+
return unless magic.start_with?("\x7FELF".b, "MZ".b) || mach_o.include?(magic)
|
|
223
|
+
|
|
224
|
+
strip = find_command(platform.macos? ? %w[strip] : %w[strip llvm-strip])
|
|
225
|
+
return unless strip
|
|
226
|
+
|
|
227
|
+
arguments = platform.macos? ? [strip, "-x", path] : [strip, "--strip-unneeded", path]
|
|
228
|
+
system(*arguments, out: File::NULL, err: File::NULL)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def find_command(names)
|
|
232
|
+
@environment.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
233
|
+
names.each do |name|
|
|
234
|
+
path = File.join(directory, name)
|
|
235
|
+
return path if File.file?(path) && File.executable?(path)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
data/lib/zui/generator.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Zui
|
|
|
14
14
|
created = true
|
|
15
15
|
FileUtils.mkdir_p(File.join(@path, "components"))
|
|
16
16
|
File.write(File.join(@path, "main.rb"), main_program)
|
|
17
|
+
File.write(File.join(@path, "Gemfile"), gemfile)
|
|
17
18
|
File.write(File.join(@path, "components", "welcome.rb"), welcome_component)
|
|
18
19
|
File.write(File.join(@path, "config.rb"), distribution_config)
|
|
19
20
|
FileUtils.mkdir_p(File.join(@path, "assets"))
|
|
@@ -50,6 +51,16 @@ module Zui
|
|
|
50
51
|
RUBY
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
def gemfile
|
|
55
|
+
<<~RUBY
|
|
56
|
+
source "https://rubygems.org"
|
|
57
|
+
|
|
58
|
+
gem "zui", "~> #{VERSION}"
|
|
59
|
+
|
|
60
|
+
# Add application gems here. They are included by `zui bundle --full`.
|
|
61
|
+
RUBY
|
|
62
|
+
end
|
|
63
|
+
|
|
53
64
|
def welcome_component
|
|
54
65
|
<<~RUBY
|
|
55
66
|
# frozen_string_literal: true
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Zui
|
|
7
|
+
class LiteRuntime < Client
|
|
8
|
+
FORMAT = 1
|
|
9
|
+
MRUBY_VERSION = "4.0.0"
|
|
10
|
+
MRUBY_REVISION = "831da26b9021de0369d17b71b5667e2941a1a32d"
|
|
11
|
+
MRUBY_JSON_REVISION = "f99d9428025469f2400f93c53b185f65f963e507"
|
|
12
|
+
|
|
13
|
+
def root
|
|
14
|
+
override = @environment["ZUI_LITE_RUNTIME_ROOT"]
|
|
15
|
+
return File.expand_path(override) if override && !override.empty?
|
|
16
|
+
|
|
17
|
+
File.join(cache_root, "zui", "runtimes", version, platform.id, "lite")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def archive_name = "zui-runtime-lite-#{platform.id}.tar.gz"
|
|
21
|
+
|
|
22
|
+
def configure!
|
|
23
|
+
return root if configured?
|
|
24
|
+
if client_root_override?
|
|
25
|
+
raise ArgumentError, "ZUI_LITE_RUNTIME_ROOT is not a valid configured lite runtime: #{root}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def archive_url
|
|
32
|
+
override = @environment["ZUI_LITE_RUNTIME_ARCHIVE"]
|
|
33
|
+
return File.expand_path(override) if override && !override.empty?
|
|
34
|
+
|
|
35
|
+
URI.join("#{release_base_url}/", archive_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def checksum_url
|
|
39
|
+
override = @environment["ZUI_LITE_RUNTIME_CHECKSUM"]
|
|
40
|
+
return File.expand_path(override) if override && !override.empty?
|
|
41
|
+
|
|
42
|
+
source = archive_url
|
|
43
|
+
source.is_a?(URI) ? URI("#{source}.sha256") : "#{source}.sha256"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def install(project:, destination:)
|
|
47
|
+
unless configured?
|
|
48
|
+
raise ArgumentError,
|
|
49
|
+
"Zui lite runtime is not configured for #{platform.id}; run `zui doctor --fix` before bundling"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
copy_to(destination)
|
|
53
|
+
File.write(File.join(destination, "app.rb"), LiteSource.new(project:).call)
|
|
54
|
+
ApplicationRuntime.new(
|
|
55
|
+
engine: "mruby", version: manifest.fetch("engine_version"),
|
|
56
|
+
executable: manifest.fetch("executable"), program: "app.rb", load_path: ""
|
|
57
|
+
).write(destination)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate!(directory)
|
|
63
|
+
manifest_path = File.join(directory, "lite-runtime.json")
|
|
64
|
+
raise ArgumentError, "Zui lite runtime manifest not found: #{manifest_path}" unless File.file?(manifest_path)
|
|
65
|
+
raise ArgumentError, "Zui lite runtime manifest is too large" if File.size(manifest_path) > 65_536
|
|
66
|
+
|
|
67
|
+
manifest = JSON.parse(File.read(manifest_path))
|
|
68
|
+
raise ArgumentError, "invalid Zui lite runtime manifest root" unless manifest.is_a?(Hash)
|
|
69
|
+
expected = {
|
|
70
|
+
"format" => FORMAT,
|
|
71
|
+
"framework" => "zui",
|
|
72
|
+
"zui_version" => version,
|
|
73
|
+
"platform" => platform.id,
|
|
74
|
+
"engine" => "mruby",
|
|
75
|
+
"engine_version" => MRUBY_VERSION,
|
|
76
|
+
"engine_revision" => MRUBY_REVISION,
|
|
77
|
+
"json_revision" => MRUBY_JSON_REVISION,
|
|
78
|
+
"payload" => ["mruby"]
|
|
79
|
+
}
|
|
80
|
+
expected.each do |key, value|
|
|
81
|
+
actual = manifest[key]
|
|
82
|
+
unless actual == value
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"invalid Zui lite runtime #{key}: expected #{value.inspect}, got #{actual.inspect}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
required_paths = manifest["required_paths"]
|
|
89
|
+
unless required_paths.is_a?(Array) && !required_paths.empty? &&
|
|
90
|
+
required_paths.all? { |path| path.is_a?(String) }
|
|
91
|
+
raise ArgumentError, "invalid Zui lite runtime required paths"
|
|
92
|
+
end
|
|
93
|
+
required_paths.each do |path|
|
|
94
|
+
relative = safe_manifest_path!(path, "lite runtime required path")
|
|
95
|
+
resolved = File.join(directory, relative)
|
|
96
|
+
raise ArgumentError, "Zui lite runtime required path is missing: #{resolved}" unless File.exist?(resolved)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
relative = safe_manifest_path!(manifest["executable"], "lite runtime executable")
|
|
100
|
+
executable = File.join(directory, relative)
|
|
101
|
+
unless File.file?(executable) && executable_for_target?(executable)
|
|
102
|
+
raise ArgumentError, "Zui lite runtime executable is missing or not executable: #{executable}"
|
|
103
|
+
end
|
|
104
|
+
manifest
|
|
105
|
+
rescue JSON::ParserError => error
|
|
106
|
+
raise ArgumentError, "invalid Zui lite runtime manifest: #{error.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def release_base_url
|
|
110
|
+
@release_base_url || @environment["ZUI_LITE_RUNTIME_BASE_URL"] ||
|
|
111
|
+
"https://github.com/AdamMusa/zui/releases/download/v#{version}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def client_root_override?
|
|
115
|
+
value = @environment["ZUI_LITE_RUNTIME_ROOT"]
|
|
116
|
+
value && !value.empty?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zui
|
|
4
|
+
class LiteSource
|
|
5
|
+
RUNTIME_FILES = %w[
|
|
6
|
+
lite_support.rb protocol.rb value.rb state_store.rb node.rb animation.rb scheduler.rb
|
|
7
|
+
component_registry.rb components.rb builder.rb application.rb lite_entry.rb
|
|
8
|
+
].freeze
|
|
9
|
+
REQUIRE = /^\s*require\s*(?:\(\s*)?["']([^"']+)["']/
|
|
10
|
+
|
|
11
|
+
def initialize(project:, framework_root: FRAMEWORK_ROOT)
|
|
12
|
+
@project = File.expand_path(project)
|
|
13
|
+
@framework_root = File.expand_path(framework_root)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
application = SourceBundle.new(
|
|
18
|
+
File.join(@project, "main.rb"), root: @project, embedded_frameworks: ["zui"]
|
|
19
|
+
).call
|
|
20
|
+
external = application.scan(REQUIRE).flatten.uniq
|
|
21
|
+
unless external.empty?
|
|
22
|
+
raise ArgumentError,
|
|
23
|
+
"--lite does not support CRuby gem requires (#{external.join(', ')}); use --full"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
runtime = RUNTIME_FILES.map do |name|
|
|
27
|
+
path = File.join(@framework_root, "lib", "zui", name)
|
|
28
|
+
raise ArgumentError, "lite runtime source is missing: #{path}" unless File.file?(path)
|
|
29
|
+
|
|
30
|
+
source = File.readlines(path).reject { |line| REQUIRE.match?(line) }.join
|
|
31
|
+
"# runtime: #{name}\n#{source}"
|
|
32
|
+
end
|
|
33
|
+
runtime << "module Zui\n VERSION = #{VERSION.dump}\nend"
|
|
34
|
+
runtime << application
|
|
35
|
+
"#{runtime.join("\n\n")}\n"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
unless Object.const_defined?(:Mutex)
|
|
4
|
+
class Mutex
|
|
5
|
+
def synchronize
|
|
6
|
+
yield
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless Object.const_defined?(:ConditionVariable)
|
|
12
|
+
class ConditionVariable
|
|
13
|
+
def wait(_mutex, _seconds = nil)
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def broadcast
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/zui/node.rb
CHANGED
|
@@ -25,6 +25,27 @@ module Zui
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
Binding
|
|
29
|
-
|
|
28
|
+
class Binding
|
|
29
|
+
attr_accessor :last_value
|
|
30
|
+
attr_reader :node, :property, :reader, :animation
|
|
31
|
+
|
|
32
|
+
def initialize(node:, property:, reader:, last_value:, animation:)
|
|
33
|
+
@node = node
|
|
34
|
+
@property = property
|
|
35
|
+
@reader = reader
|
|
36
|
+
@last_value = last_value
|
|
37
|
+
@animation = animation
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class StructuralBinding
|
|
42
|
+
attr_accessor :last_children
|
|
43
|
+
attr_reader :node, :renderer
|
|
44
|
+
|
|
45
|
+
def initialize(node:, renderer:, last_children:)
|
|
46
|
+
@node = node
|
|
47
|
+
@renderer = renderer
|
|
48
|
+
@last_children = last_children
|
|
49
|
+
end
|
|
50
|
+
end
|
|
30
51
|
end
|
data/lib/zui.rb
CHANGED
|
@@ -12,11 +12,15 @@ require_relative "zui/components"
|
|
|
12
12
|
require_relative "zui/builder"
|
|
13
13
|
require_relative "zui/application"
|
|
14
14
|
require_relative "zui/source_bundle"
|
|
15
|
+
require_relative "zui/lite_source"
|
|
15
16
|
require_relative "zui/platform"
|
|
16
17
|
require_relative "zui/runtime"
|
|
18
|
+
require_relative "zui/application_runtime"
|
|
19
|
+
require_relative "zui/full_runtime"
|
|
17
20
|
require_relative "zui/tree_shaker"
|
|
18
21
|
require_relative "zui/dist_config"
|
|
19
22
|
require_relative "zui/client"
|
|
23
|
+
require_relative "zui/lite_runtime"
|
|
20
24
|
require_relative "zui/host"
|
|
21
25
|
require_relative "zui/runner"
|
|
22
26
|
require_relative "zui/generator"
|
|
@@ -24,7 +28,7 @@ require_relative "zui/distribution"
|
|
|
24
28
|
require_relative "zui/dist_packager"
|
|
25
29
|
|
|
26
30
|
module Zui
|
|
27
|
-
VERSION = "0.0.
|
|
31
|
+
VERSION = "0.0.8"
|
|
28
32
|
FRAMEWORK_ROOT = File.expand_path("..", __dir__)
|
|
29
33
|
|
|
30
34
|
def self.app(&definition)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Moussa Ali
|
|
@@ -320,6 +320,7 @@ files:
|
|
|
320
320
|
- lib/zui.rb
|
|
321
321
|
- lib/zui/animation.rb
|
|
322
322
|
- lib/zui/application.rb
|
|
323
|
+
- lib/zui/application_runtime.rb
|
|
323
324
|
- lib/zui/builder.rb
|
|
324
325
|
- lib/zui/cli.rb
|
|
325
326
|
- lib/zui/client.rb
|
|
@@ -329,8 +330,13 @@ files:
|
|
|
329
330
|
- lib/zui/dist_config.rb
|
|
330
331
|
- lib/zui/dist_packager.rb
|
|
331
332
|
- lib/zui/distribution.rb
|
|
333
|
+
- lib/zui/full_runtime.rb
|
|
332
334
|
- lib/zui/generator.rb
|
|
333
335
|
- lib/zui/host.rb
|
|
336
|
+
- lib/zui/lite_entry.rb
|
|
337
|
+
- lib/zui/lite_runtime.rb
|
|
338
|
+
- lib/zui/lite_source.rb
|
|
339
|
+
- lib/zui/lite_support.rb
|
|
334
340
|
- lib/zui/node.rb
|
|
335
341
|
- lib/zui/platform.rb
|
|
336
342
|
- lib/zui/protocol.rb
|