zui 0.0.1 → 0.0.2
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 +24 -14
- data/lib/zui/cli.rb +36 -6
- data/lib/zui/client.rb +349 -0
- data/lib/zui/distribution.rb +53 -16
- data/lib/zui/generator.rb +1 -0
- data/lib/zui/host.rb +16 -94
- data/lib/zui/runner.rb +1 -1
- data/lib/zui.rb +2 -1
- metadata +5 -12
- data/native/CMakeLists.txt +0 -45
- data/native/ZuiClipboard.cpp +0 -16
- data/native/ZuiClipboard.h +0 -16
- data/native/ZuiProcess.cpp +0 -84
- data/native/ZuiProcess.h +0 -34
- data/native/main.cpp +0 -64
- data/vendor/host/linux-x86_64/PROVENANCE.md +0 -15
- data/vendor/host/linux-x86_64/zui-host +0 -0
- data/vendor/host/linux-x86_64/zui-host.sha256 +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00eeeccbcd56e9deb1c78c93b913ee8e137af12cbd00dab4cc8ba3f4c13e3b9b
|
|
4
|
+
data.tar.gz: dd8e6f8d38493ca5e060be45adf6ffdc7584adac357a4789d59dbdb11bcdc8d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bdcbd3b815283726e6854913ff620209452d989080a501dc2c717945a76ecd957e3cda6ddaa11e6ff0ca9c32d960eb07c0832b0012afebc4ca2e0f92c6a2b2c
|
|
7
|
+
data.tar.gz: da469681842aed14ff1176f66bf5b31767802a3097a9d6741560f594dbca66ae7af39a7d32a2d7b2c9931cd7af9adf9a796a083d224bcfc95445cfbc9b9c55a8
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Zui
|
|
2
2
|
|
|
3
|
-
Zui
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Zui builds beautiful native desktop applications in pure Ruby, with first-class UI, state,
|
|
4
|
+
events, bindings, animation, media, GPU effects, 3D, and application logic. Applications are
|
|
5
|
+
Ruby and assets—there is no application UI language to learn alongside Ruby.
|
|
6
6
|
|
|
7
7
|
This repository is the reusable core. Distribution integrations live at its edges:
|
|
8
8
|
|
|
@@ -38,6 +38,7 @@ end
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
gem install zui
|
|
41
|
+
zui doctor --fix
|
|
41
42
|
zui new telemetry-console
|
|
42
43
|
cd telemetry-console
|
|
43
44
|
zui run main.rb
|
|
@@ -48,23 +49,32 @@ Build and install the gem directly from a checkout:
|
|
|
48
49
|
|
|
49
50
|
```bash
|
|
50
51
|
gem build zui.gemspec
|
|
51
|
-
gem install ./zui-0.0.
|
|
52
|
+
gem install ./zui-0.0.2.gem
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
`zui
|
|
55
|
-
|
|
55
|
+
`zui doctor --fix` is the one-time setup for each Zui version and platform. It downloads the
|
|
56
|
+
matching versioned client from GitHub Releases, verifies its SHA-256 checksum and manifest, and
|
|
57
|
+
installs it in the native user cache. `zui configure` performs the same explicit setup operation.
|
|
58
|
+
The prebuilt client is never packaged in or downloaded from the RubyGem. It contains the native
|
|
59
|
+
host and its Qt/QML engine libraries—not the developer's application, Ruby source, or the Zui
|
|
60
|
+
Ruby/QML framework. Setup never changes the global shell environment; `zui run` supplies the
|
|
61
|
+
client paths only to the child application.
|
|
56
62
|
|
|
57
|
-
`zui
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
Developers install Ruby and the `zui` gem. They do not install CMake, a C++ compiler, or Qt. Those
|
|
64
|
+
tools are used only by Zui's release CI to produce the platform clients.
|
|
65
|
+
|
|
66
|
+
`zui bundle` uses a platform template to produce an application directory on Linux and Windows or
|
|
67
|
+
a standard `.app` on macOS. The package combines the application's Ruby/assets, the Zui framework
|
|
68
|
+
runtime and catalog, and a private copy of the configured native Qt/QML client. No system Qt
|
|
69
|
+
installation is used by the finished bundle. The current bundle format expects Ruby 3.1 or newer
|
|
70
|
+
on the destination and supports `ZUI_RUBY` when an installer provides a private Ruby executable.
|
|
62
71
|
|
|
63
72
|
See [Platform support](docs/platforms.md) for host requirements and bundle layouts.
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
errors are reported by the operation
|
|
74
|
+
`zui doctor` is read-only: it reports platform, Ruby, client, run, and bundle readiness. Add
|
|
75
|
+
`--fix` to download and install the missing client from GitHub Releases. Zui has no separate
|
|
76
|
+
validation command; Ruby, DSL, protocol, resource, and QML errors are reported by the operation
|
|
77
|
+
that encounters them.
|
|
68
78
|
|
|
69
79
|
## Reusable application UI
|
|
70
80
|
|
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|run FILE|bundle [DIRECTORY]|
|
|
8
|
+
USAGE = "Usage: zui <new NAME|configure|doctor [--fix]|run FILE|bundle [DIRECTORY]|version>"
|
|
9
9
|
|
|
10
10
|
def self.run(arguments, out: $stdout, err: $stderr)
|
|
11
11
|
new(out:, err:).run(arguments.dup)
|
|
@@ -20,6 +20,7 @@ module Zui
|
|
|
20
20
|
command = arguments.shift
|
|
21
21
|
case command
|
|
22
22
|
when "new" then new_project(arguments)
|
|
23
|
+
when "configure" then configure(arguments)
|
|
23
24
|
when "run" then run_file(arguments)
|
|
24
25
|
when "bundle" then bundle_project(arguments)
|
|
25
26
|
when "doctor" then doctor(arguments)
|
|
@@ -63,15 +64,44 @@ module Zui
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def doctor(arguments)
|
|
66
|
-
|
|
67
|
+
fix = arguments.delete("--fix")
|
|
68
|
+
raise ArgumentError, "doctor accepts only --fix" unless arguments.empty?
|
|
67
69
|
platform = Platform.current
|
|
68
|
-
host = Host.new(platform:)
|
|
69
70
|
@out.puts("Zui #{VERSION}")
|
|
70
71
|
@out.puts("Platform: #{platform.id}#{platform.supported? ? '' : ' (unsupported)'}")
|
|
71
72
|
@out.puts("Ruby: #{RbConfig.ruby} (#{RUBY_VERSION})")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
return 1 unless platform.supported?
|
|
74
|
+
|
|
75
|
+
client = Client.new(platform:)
|
|
76
|
+
if client.configured?
|
|
77
|
+
report_ready(client)
|
|
78
|
+
0
|
|
79
|
+
elsif fix
|
|
80
|
+
@out.puts("Repair: downloading the verified native client from GitHub Releases...")
|
|
81
|
+
client.configure!
|
|
82
|
+
report_ready(client)
|
|
83
|
+
0
|
|
84
|
+
else
|
|
85
|
+
@out.puts("Client: not configured")
|
|
86
|
+
@out.puts("Run `zui doctor --fix` to install the native client and bundle support.")
|
|
87
|
+
1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def configure(arguments)
|
|
92
|
+
raise ArgumentError, "configure accepts no arguments" unless arguments.empty?
|
|
93
|
+
platform = Platform.current.assert_supported!
|
|
94
|
+
client = Client.new(platform:)
|
|
95
|
+
@out.puts("Configuring Zui #{VERSION} for #{platform.id}...")
|
|
96
|
+
client.configure!
|
|
97
|
+
report_ready(client)
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def report_ready(client)
|
|
102
|
+
@out.puts("Client: #{client.root}")
|
|
103
|
+
@out.puts("Run: ready")
|
|
104
|
+
@out.puts("Bundle: ready")
|
|
75
105
|
end
|
|
76
106
|
|
|
77
107
|
def option_value(arguments, name)
|
data/lib/zui/client.rb
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "net/http"
|
|
7
|
+
require "openssl"
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
require "timeout"
|
|
10
|
+
require "uri"
|
|
11
|
+
require "zlib"
|
|
12
|
+
require "rubygems/package"
|
|
13
|
+
|
|
14
|
+
module Zui
|
|
15
|
+
class Client
|
|
16
|
+
FORMAT = 1
|
|
17
|
+
MAX_ARCHIVE_BYTES = 1_073_741_824
|
|
18
|
+
MAX_EXPANDED_BYTES = 2_147_483_648
|
|
19
|
+
MAX_ENTRIES = 100_000
|
|
20
|
+
REDIRECT_LIMIT = 5
|
|
21
|
+
|
|
22
|
+
attr_reader :platform, :version
|
|
23
|
+
|
|
24
|
+
def initialize(platform: Platform.current, version: VERSION, environment: ENV,
|
|
25
|
+
cache_root: nil, release_base_url: nil, downloader: nil)
|
|
26
|
+
@platform = platform.assert_supported!
|
|
27
|
+
@version = version.to_s
|
|
28
|
+
@environment = environment
|
|
29
|
+
@cache_root = cache_root
|
|
30
|
+
@release_base_url = release_base_url
|
|
31
|
+
@downloader = downloader || method(:download)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def root
|
|
35
|
+
override = @environment["ZUI_CLIENT_ROOT"]
|
|
36
|
+
return File.expand_path(override) if override && !override.empty?
|
|
37
|
+
|
|
38
|
+
File.join(cache_root, "zui", "clients", version, platform.id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configured?
|
|
42
|
+
validate!(root)
|
|
43
|
+
true
|
|
44
|
+
rescue ArgumentError, Errno::ENOENT
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure!
|
|
49
|
+
return root if configured?
|
|
50
|
+
raise ArgumentError, "ZUI_CLIENT_ROOT is not a valid configured client: #{root}" if client_root_override?
|
|
51
|
+
|
|
52
|
+
FileUtils.mkdir_p(File.dirname(root))
|
|
53
|
+
File.open("#{root}.lock", File::RDWR | File::CREAT, 0o600) do |lock|
|
|
54
|
+
lock.flock(File::LOCK_EX)
|
|
55
|
+
return root if configured?
|
|
56
|
+
|
|
57
|
+
install_download!
|
|
58
|
+
end
|
|
59
|
+
root
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def executable
|
|
63
|
+
manifest = validate!(root)
|
|
64
|
+
File.join(root, manifest.fetch("executable"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executable_relative_path
|
|
68
|
+
validate!(root).fetch("executable")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def environment_entries
|
|
72
|
+
manifest = validate!(root)
|
|
73
|
+
manifest.fetch("environment", {}).transform_values do |paths|
|
|
74
|
+
Array(paths).map { |path| safe_manifest_path!(path, "client environment path") }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def environment(base = ENV.to_h, client_root: root)
|
|
79
|
+
environment_entries.each_with_object(base.to_h.dup) do |(name, paths), result|
|
|
80
|
+
unless name.match?(/\A[A-Z][A-Z0-9_]*\z/)
|
|
81
|
+
raise ArgumentError, "invalid environment variable in Zui client: #{name.inspect}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
resolved = paths.map { |path| File.join(client_root, path) }
|
|
85
|
+
previous = result[name]
|
|
86
|
+
resolved << previous unless previous.nil? || previous.empty?
|
|
87
|
+
result[name] = resolved.join(File::PATH_SEPARATOR)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def copy_to(destination)
|
|
92
|
+
manifest = validate!(root)
|
|
93
|
+
raise ArgumentError, "client destination already exists: #{destination}" if File.exist?(destination)
|
|
94
|
+
|
|
95
|
+
created = true
|
|
96
|
+
FileUtils.mkdir_p(destination)
|
|
97
|
+
FileUtils.cp_r(Dir.children(root).map { |entry| File.join(root, entry) }, destination)
|
|
98
|
+
validate!(destination)
|
|
99
|
+
manifest
|
|
100
|
+
rescue StandardError
|
|
101
|
+
FileUtils.remove_entry(destination) if created && destination && File.exist?(destination)
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def manifest
|
|
106
|
+
validate!(root)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def archive_name = "zui-client-#{platform.id}.tar.gz"
|
|
110
|
+
|
|
111
|
+
def archive_url
|
|
112
|
+
override = @environment["ZUI_CLIENT_ARCHIVE"]
|
|
113
|
+
return File.expand_path(override) if override && !override.empty?
|
|
114
|
+
|
|
115
|
+
URI.join("#{release_base_url}/", archive_name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def checksum_url
|
|
119
|
+
override = @environment["ZUI_CLIENT_CHECKSUM"]
|
|
120
|
+
return File.expand_path(override) if override && !override.empty?
|
|
121
|
+
|
|
122
|
+
source = archive_url
|
|
123
|
+
source.is_a?(URI) ? URI("#{source}.sha256") : "#{source}.sha256"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def install_download!
|
|
129
|
+
Dir.mktmpdir(".zui-client-", File.dirname(root)) do |temporary|
|
|
130
|
+
archive = File.join(temporary, archive_name)
|
|
131
|
+
checksum = "#{archive}.sha256"
|
|
132
|
+
@downloader.call(archive_url, archive)
|
|
133
|
+
@downloader.call(checksum_url, checksum)
|
|
134
|
+
verify_checksum!(archive, checksum)
|
|
135
|
+
|
|
136
|
+
staged = File.join(temporary, "client")
|
|
137
|
+
FileUtils.mkdir_p(staged)
|
|
138
|
+
extract!(archive, staged)
|
|
139
|
+
validate!(staged)
|
|
140
|
+
|
|
141
|
+
installed = "#{root}.install-#{Process.pid}-#{rand(1_000_000)}"
|
|
142
|
+
File.rename(staged, installed)
|
|
143
|
+
if File.exist?(root)
|
|
144
|
+
invalid = "#{root}.invalid-#{Time.now.to_i}-#{Process.pid}"
|
|
145
|
+
File.rename(root, invalid)
|
|
146
|
+
end
|
|
147
|
+
File.rename(installed, root)
|
|
148
|
+
ensure
|
|
149
|
+
FileUtils.remove_entry(installed) if installed && File.exist?(installed)
|
|
150
|
+
end
|
|
151
|
+
validate!(root)
|
|
152
|
+
root
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def validate!(directory)
|
|
156
|
+
manifest_path = File.join(directory, "client.json")
|
|
157
|
+
raise ArgumentError, "Zui client manifest not found: #{manifest_path}" unless File.file?(manifest_path)
|
|
158
|
+
raise ArgumentError, "Zui client manifest is too large" if File.size(manifest_path) > 65_536
|
|
159
|
+
|
|
160
|
+
manifest = JSON.parse(File.read(manifest_path))
|
|
161
|
+
raise ArgumentError, "invalid Zui client manifest root" unless manifest.is_a?(Hash)
|
|
162
|
+
expected = {
|
|
163
|
+
"format" => FORMAT,
|
|
164
|
+
"framework" => "zui",
|
|
165
|
+
"client_version" => version,
|
|
166
|
+
"platform" => platform.id
|
|
167
|
+
}
|
|
168
|
+
expected.each do |key, value|
|
|
169
|
+
actual = manifest[key]
|
|
170
|
+
raise ArgumentError, "invalid Zui client #{key}: expected #{value.inspect}, got #{actual.inspect}" unless actual == value
|
|
171
|
+
end
|
|
172
|
+
raise ArgumentError, "Zui client is not bundle capable" unless manifest["bundle_capable"] == true
|
|
173
|
+
unless manifest["payload"] == %w[native-host qt-engine]
|
|
174
|
+
raise ArgumentError, "invalid Zui client payload: expected native host and Qt engine only"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
required_paths = manifest["required_paths"]
|
|
178
|
+
unless required_paths.is_a?(Array) && !required_paths.empty? &&
|
|
179
|
+
required_paths.all? { |path| path.is_a?(String) }
|
|
180
|
+
raise ArgumentError, "invalid Zui client required paths"
|
|
181
|
+
end
|
|
182
|
+
required_paths.each do |path|
|
|
183
|
+
relative_path = safe_manifest_path!(path, "client required path")
|
|
184
|
+
resolved_path = File.join(directory, relative_path)
|
|
185
|
+
raise ArgumentError, "Zui client required path is missing: #{resolved_path}" unless File.exist?(resolved_path)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
relative = safe_manifest_path!(manifest["executable"], "client executable")
|
|
189
|
+
executable = File.join(directory, relative)
|
|
190
|
+
unless File.file?(executable) && executable_for_target?(executable)
|
|
191
|
+
raise ArgumentError, "Zui client executable is missing or not executable: #{executable}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
client_environment = manifest.fetch("environment", {})
|
|
195
|
+
raise ArgumentError, "invalid Zui client environment" unless client_environment.is_a?(Hash)
|
|
196
|
+
client_environment.each do |name, paths|
|
|
197
|
+
unless name.is_a?(String) && name.match?(/\A[A-Z][A-Z0-9_]*\z/)
|
|
198
|
+
raise ArgumentError, "invalid client environment entry: #{name.inspect}"
|
|
199
|
+
end
|
|
200
|
+
unless paths.is_a?(Array) && !paths.empty? && paths.all? { |path| path.is_a?(String) }
|
|
201
|
+
raise ArgumentError, "invalid client environment paths for #{name}"
|
|
202
|
+
end
|
|
203
|
+
paths.each do |path|
|
|
204
|
+
relative_path = safe_manifest_path!(path, "client environment path")
|
|
205
|
+
resolved_path = File.join(directory, relative_path)
|
|
206
|
+
raise ArgumentError, "Zui client environment path is missing: #{resolved_path}" unless File.exist?(resolved_path)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
manifest
|
|
210
|
+
rescue JSON::ParserError => error
|
|
211
|
+
raise ArgumentError, "invalid Zui client manifest: #{error.message}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def executable_for_target?(path)
|
|
215
|
+
# Windows cannot represent the POSIX executable bit when validating a
|
|
216
|
+
# Linux or macOS archive. Native POSIX clients still require that bit.
|
|
217
|
+
platform.windows? || Gem.win_platform? || File.executable?(path)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def verify_checksum!(archive, checksum_file)
|
|
221
|
+
expected = File.read(checksum_file, 4096)[/\A\s*([0-9a-fA-F]{64})(?:\s|\z)/, 1]
|
|
222
|
+
raise ArgumentError, "invalid Zui client checksum file" unless expected
|
|
223
|
+
|
|
224
|
+
actual = Digest::SHA256.file(archive).hexdigest
|
|
225
|
+
raise ArgumentError, "Zui client checksum mismatch" unless actual.casecmp?(expected)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def extract!(archive, destination)
|
|
229
|
+
count = 0
|
|
230
|
+
expanded = 0
|
|
231
|
+
Zlib::GzipReader.open(archive) do |gzip|
|
|
232
|
+
Gem::Package::TarReader.new(gzip) do |tar|
|
|
233
|
+
tar.each do |entry|
|
|
234
|
+
count += 1
|
|
235
|
+
raise ArgumentError, "Zui client archive contains too many files" if count > MAX_ENTRIES
|
|
236
|
+
|
|
237
|
+
relative = safe_relative_path!(entry.full_name, "archive entry")
|
|
238
|
+
target = File.join(destination, relative)
|
|
239
|
+
if entry.directory?
|
|
240
|
+
FileUtils.mkdir_p(target)
|
|
241
|
+
elsif entry.file?
|
|
242
|
+
expanded += entry.header.size
|
|
243
|
+
raise ArgumentError, "Zui client archive expands beyond the safety limit" if expanded > MAX_EXPANDED_BYTES
|
|
244
|
+
|
|
245
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
246
|
+
File.open(target, "wb", entry.header.mode & 0o111 == 0 ? 0o644 : 0o755) do |file|
|
|
247
|
+
IO.copy_stream(entry, file)
|
|
248
|
+
end
|
|
249
|
+
else
|
|
250
|
+
raise ArgumentError, "Zui client archive contains an unsupported link or special file: #{entry.full_name}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
rescue Zlib::GzipFile::Error, Gem::Package::TarInvalidError => error
|
|
256
|
+
raise ArgumentError, "invalid Zui client archive: #{error.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def download(source, destination, redirects = REDIRECT_LIMIT)
|
|
260
|
+
if source.is_a?(String) && File.file?(source)
|
|
261
|
+
FileUtils.cp(source, destination)
|
|
262
|
+
return destination
|
|
263
|
+
end
|
|
264
|
+
uri = source.is_a?(URI) ? source : URI(source.to_s)
|
|
265
|
+
if uri.scheme == "file"
|
|
266
|
+
FileUtils.cp(URI::DEFAULT_PARSER.unescape(uri.path), destination)
|
|
267
|
+
return destination
|
|
268
|
+
end
|
|
269
|
+
raise ArgumentError, "Zui client download must use HTTPS" unless uri.scheme == "https"
|
|
270
|
+
raise ArgumentError, "too many Zui client download redirects" if redirects.negative?
|
|
271
|
+
|
|
272
|
+
http = Net::HTTP.new(uri.host, uri.port, :ENV)
|
|
273
|
+
http.use_ssl = true
|
|
274
|
+
http.open_timeout = 20
|
|
275
|
+
http.read_timeout = 120
|
|
276
|
+
http.write_timeout = 120 if http.respond_to?(:write_timeout=)
|
|
277
|
+
http.start do
|
|
278
|
+
http.request(Net::HTTP::Get.new(uri.request_uri)) do |response|
|
|
279
|
+
case response
|
|
280
|
+
when Net::HTTPSuccess
|
|
281
|
+
written = 0
|
|
282
|
+
File.open(destination, "wb") do |file|
|
|
283
|
+
response.read_body do |chunk|
|
|
284
|
+
written += chunk.bytesize
|
|
285
|
+
raise ArgumentError, "Zui client download exceeds the safety limit" if written > MAX_ARCHIVE_BYTES
|
|
286
|
+
file.write(chunk)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
when Net::HTTPRedirection
|
|
290
|
+
location = response["location"] || raise(ArgumentError, "Zui client redirect has no location")
|
|
291
|
+
return download(URI.join(uri, location), destination, redirects - 1)
|
|
292
|
+
else
|
|
293
|
+
raise ArgumentError, "Zui client download failed: HTTP #{response.code}"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
destination
|
|
298
|
+
rescue URI::InvalidURIError => error
|
|
299
|
+
raise ArgumentError, "invalid Zui client URL: #{error.message}"
|
|
300
|
+
rescue Timeout::Error, SocketError, OpenSSL::SSL::SSLError => error
|
|
301
|
+
raise ArgumentError, "Zui client download failed: #{error.class}: #{error.message}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def safe_relative_path!(value, label)
|
|
305
|
+
path = value.to_s
|
|
306
|
+
if path.empty? || path.include?("\\") || path.match?(/[[:cntrl:]]/) || path.start_with?("/") ||
|
|
307
|
+
path.match?(/\A[A-Za-z]:/) ||
|
|
308
|
+
path.split("/").any? { |part| part.empty? || part == "." || part == ".." }
|
|
309
|
+
raise ArgumentError, "unsafe #{label}: #{value.inspect}"
|
|
310
|
+
end
|
|
311
|
+
path
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def safe_manifest_path!(value, label)
|
|
315
|
+
path = safe_relative_path!(value, label)
|
|
316
|
+
unless path.match?(/\A[A-Za-z0-9._+ -]+(?:\/[A-Za-z0-9._+ -]+)*\z/)
|
|
317
|
+
raise ArgumentError, "unsafe #{label}: #{value.inspect}"
|
|
318
|
+
end
|
|
319
|
+
path
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def release_base_url
|
|
323
|
+
@release_base_url || @environment["ZUI_CLIENT_BASE_URL"] ||
|
|
324
|
+
"https://github.com/AdamMusa/zui/releases/download/v#{version}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def cache_root
|
|
328
|
+
return File.expand_path(@cache_root) if @cache_root
|
|
329
|
+
|
|
330
|
+
override = @environment["ZUI_CACHE_HOME"]
|
|
331
|
+
return File.expand_path(override) if override && !override.empty?
|
|
332
|
+
|
|
333
|
+
home = @environment["HOME"] || @environment["USERPROFILE"] || Dir.home
|
|
334
|
+
if platform.windows?
|
|
335
|
+
@environment["LOCALAPPDATA"] || @environment["APPDATA"] || File.join(home, "AppData", "Local")
|
|
336
|
+
elsif platform.macos?
|
|
337
|
+
File.join(home, "Library", "Caches")
|
|
338
|
+
else
|
|
339
|
+
@environment["XDG_CACHE_HOME"] || File.join(home, ".cache")
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def client_root_override?
|
|
344
|
+
value = @environment["ZUI_CLIENT_ROOT"]
|
|
345
|
+
value && !value.empty?
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
end
|
|
349
|
+
end
|
data/lib/zui/distribution.rb
CHANGED
|
@@ -7,20 +7,25 @@ module Zui
|
|
|
7
7
|
class Distribution
|
|
8
8
|
attr_reader :platform
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
11
|
-
@host = host
|
|
10
|
+
def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT)
|
|
12
11
|
@platform = platform.assert_supported!
|
|
12
|
+
@client = client || Client.new(platform: @platform)
|
|
13
13
|
@framework_root = framework_root
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def bundle(source, name: nil, destination: nil)
|
|
17
|
+
created = false
|
|
17
18
|
project = File.expand_path(source)
|
|
18
19
|
raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
|
|
19
20
|
raise ArgumentError, "main.rb not found: #{project}" unless File.file?(File.join(project, "main.rb"))
|
|
21
|
+
unless @client.configured?
|
|
22
|
+
raise ArgumentError, "Zui is not configured for #{platform.id}; run `zui doctor --fix` before bundling"
|
|
23
|
+
end
|
|
20
24
|
app_name = name || titleize(File.basename(project))
|
|
21
25
|
destination ||= default_destination(project, app_name)
|
|
22
26
|
destination = File.expand_path(destination)
|
|
23
27
|
raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
|
|
28
|
+
created = true
|
|
24
29
|
|
|
25
30
|
case platform.os
|
|
26
31
|
when :linux then bundle_linux(project, destination, app_name)
|
|
@@ -29,19 +34,18 @@ module Zui
|
|
|
29
34
|
end
|
|
30
35
|
destination
|
|
31
36
|
rescue StandardError
|
|
32
|
-
FileUtils.remove_entry(destination) if destination && File.exist?(destination)
|
|
37
|
+
FileUtils.remove_entry(destination) if created && destination && File.exist?(destination)
|
|
33
38
|
raise
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
private
|
|
37
42
|
|
|
38
43
|
def bundle_linux(project, destination, app_name)
|
|
39
|
-
FileUtils.mkdir_p([File.join(destination, "app"),
|
|
44
|
+
FileUtils.mkdir_p([File.join(destination, "app"),
|
|
40
45
|
File.join(destination, "runtime"), File.join(destination, "share", "applications")])
|
|
41
46
|
install_application(project, File.join(destination, "app"))
|
|
42
47
|
install_runtime(File.join(destination, "runtime"))
|
|
43
|
-
|
|
44
|
-
FileUtils.chmod(0o755, File.join(destination, "bin", "zui-host"))
|
|
48
|
+
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
45
49
|
write_linux_launcher(destination, app_name)
|
|
46
50
|
desktop_name = "#{slug(app_name)}.desktop"
|
|
47
51
|
File.write(File.join(destination, "share", "applications", desktop_name), <<~DESKTOP)
|
|
@@ -62,8 +66,7 @@ module Zui
|
|
|
62
66
|
FileUtils.mkdir_p([macos, File.join(resources, "app"), File.join(resources, "runtime")])
|
|
63
67
|
install_application(project, File.join(resources, "app"))
|
|
64
68
|
install_runtime(File.join(resources, "runtime"))
|
|
65
|
-
|
|
66
|
-
FileUtils.chmod(0o755, File.join(macos, "zui-host"))
|
|
69
|
+
@client.copy_to(File.join(resources, "runtime", "native"))
|
|
67
70
|
File.write(File.join(macos, "run"), macos_launcher(app_name))
|
|
68
71
|
FileUtils.chmod(0o755, File.join(macos, "run"))
|
|
69
72
|
File.write(File.join(contents, "Info.plist"), info_plist(app_name))
|
|
@@ -71,11 +74,11 @@ module Zui
|
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
def bundle_windows(project, destination, app_name)
|
|
74
|
-
FileUtils.mkdir_p([File.join(destination, "app"),
|
|
77
|
+
FileUtils.mkdir_p([File.join(destination, "app"),
|
|
75
78
|
File.join(destination, "runtime")])
|
|
76
79
|
install_application(project, File.join(destination, "app"))
|
|
77
80
|
install_runtime(File.join(destination, "runtime"))
|
|
78
|
-
|
|
81
|
+
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
79
82
|
File.write(File.join(destination, "run.rb"), windows_ruby_launcher(app_name))
|
|
80
83
|
File.write(File.join(destination, "run.cmd"), windows_command_launcher)
|
|
81
84
|
write_manifest(destination, app_name)
|
|
@@ -93,7 +96,11 @@ module Zui
|
|
|
93
96
|
Runtime.install_qml(File.join(destination, "qml"), framework_root: @framework_root)
|
|
94
97
|
FileUtils.mkdir_p(File.join(destination, "lib"))
|
|
95
98
|
FileUtils.cp(File.join(@framework_root, "lib", "zui.rb"), File.join(destination, "lib", "zui.rb"))
|
|
96
|
-
|
|
99
|
+
source = File.join(@framework_root, "lib", "zui")
|
|
100
|
+
target = File.join(destination, "lib", "zui")
|
|
101
|
+
FileUtils.mkdir_p(target)
|
|
102
|
+
entries = Dir.children(source).reject { |entry| %w[client_builder.rb client_packager.rb].include?(entry) }
|
|
103
|
+
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, target) unless entries.empty?
|
|
97
104
|
end
|
|
98
105
|
|
|
99
106
|
def write_linux_launcher(destination, app_name)
|
|
@@ -101,8 +108,10 @@ module Zui
|
|
|
101
108
|
#!/bin/sh
|
|
102
109
|
set -eu
|
|
103
110
|
bundle_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
111
|
+
native_dir="$bundle_dir/runtime/native"
|
|
112
|
+
#{posix_client_environment}
|
|
104
113
|
ruby_command=${ZUI_RUBY:-ruby}
|
|
105
|
-
exec "$
|
|
114
|
+
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
106
115
|
--qml-root "$bundle_dir/runtime/qml" \
|
|
107
116
|
--project "$bundle_dir/app" \
|
|
108
117
|
--program "$bundle_dir/app/main.rb" \
|
|
@@ -119,8 +128,10 @@ module Zui
|
|
|
119
128
|
set -eu
|
|
120
129
|
contents=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
121
130
|
resources="$contents/Resources"
|
|
131
|
+
native_dir="$resources/runtime/native"
|
|
132
|
+
#{posix_client_environment}
|
|
122
133
|
ruby_command=${ZUI_RUBY:-ruby}
|
|
123
|
-
exec "$
|
|
134
|
+
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
124
135
|
--qml-root "$resources/runtime/qml" \
|
|
125
136
|
--project "$resources/app" \
|
|
126
137
|
--program "$resources/app/main.rb" \
|
|
@@ -137,8 +148,11 @@ module Zui
|
|
|
137
148
|
require "rbconfig"
|
|
138
149
|
|
|
139
150
|
bundle_dir = File.expand_path(__dir__)
|
|
151
|
+
native_dir = File.join(bundle_dir, "runtime", "native")
|
|
152
|
+
environment = {}
|
|
153
|
+
#{windows_environment_builder}
|
|
140
154
|
arguments = [
|
|
141
|
-
File.join(
|
|
155
|
+
File.join(native_dir, #{@client.executable_relative_path.dump}),
|
|
142
156
|
"--qml-root", File.join(bundle_dir, "runtime", "qml"),
|
|
143
157
|
"--project", File.join(bundle_dir, "app"),
|
|
144
158
|
"--program", File.join(bundle_dir, "app", "main.rb"),
|
|
@@ -146,7 +160,7 @@ module Zui
|
|
|
146
160
|
"--load-path", File.join(bundle_dir, "runtime", "lib"),
|
|
147
161
|
"--name", #{app_name.to_s.dump}
|
|
148
162
|
]
|
|
149
|
-
exec(*arguments)
|
|
163
|
+
exec(environment, *arguments)
|
|
150
164
|
RUBY
|
|
151
165
|
end
|
|
152
166
|
|
|
@@ -183,7 +197,7 @@ module Zui
|
|
|
183
197
|
File.write(File.join(directory, "zui-bundle.json"), JSON.pretty_generate(
|
|
184
198
|
"format" => 1, "framework" => "zui", "version" => VERSION,
|
|
185
199
|
"platform" => platform.os.to_s, "architecture" => platform.arch.to_s,
|
|
186
|
-
"name" => app_name
|
|
200
|
+
"name" => app_name, "client_version" => @client.manifest.fetch("client_version")
|
|
187
201
|
))
|
|
188
202
|
end
|
|
189
203
|
|
|
@@ -196,5 +210,28 @@ module Zui
|
|
|
196
210
|
def slug(value) = value.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
197
211
|
def shell_quote(value) = "'#{value.to_s.gsub("'", %q('"'"'))}'"
|
|
198
212
|
def xml_escape(value) = value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
213
|
+
|
|
214
|
+
def posix_client_environment
|
|
215
|
+
@client.environment_entries.map do |name, paths|
|
|
216
|
+
value = paths.map { |path| "$native_dir/#{path}" }.join(":")
|
|
217
|
+
<<~SH.chomp
|
|
218
|
+
zui_environment="#{value}"
|
|
219
|
+
[ -z "${#{name}:-}" ] || zui_environment="$zui_environment:${#{name}}"
|
|
220
|
+
export #{name}="$zui_environment"
|
|
221
|
+
SH
|
|
222
|
+
end.join("\n")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def windows_client_environment = @client.environment_entries
|
|
226
|
+
|
|
227
|
+
def windows_environment_builder
|
|
228
|
+
@client.environment_entries.keys.map do |name|
|
|
229
|
+
<<~RUBY.chomp
|
|
230
|
+
values = #{windows_client_environment.fetch(name).inspect}.map { |path| File.join(native_dir, path) }
|
|
231
|
+
values << ENV[#{name.dump}] unless ENV[#{name.dump}].nil? || ENV[#{name.dump}].empty?
|
|
232
|
+
environment[#{name.dump}] = values.join(File::PATH_SEPARATOR)
|
|
233
|
+
RUBY
|
|
234
|
+
end.join("\n")
|
|
235
|
+
end
|
|
199
236
|
end
|
|
200
237
|
end
|
data/lib/zui/generator.rb
CHANGED
data/lib/zui/host.rb
CHANGED
|
@@ -1,123 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "tmpdir"
|
|
5
|
-
|
|
6
3
|
module Zui
|
|
7
4
|
class Host
|
|
8
|
-
attr_reader :platform
|
|
5
|
+
attr_reader :client, :platform
|
|
9
6
|
|
|
10
|
-
def initialize(platform: Platform.current,
|
|
7
|
+
def initialize(platform: Platform.current, environment: ENV, client: nil)
|
|
11
8
|
@platform = platform.assert_supported!
|
|
12
|
-
@framework_root = framework_root
|
|
13
9
|
@environment = environment
|
|
10
|
+
@client = client || Client.new(platform:, environment:)
|
|
14
11
|
end
|
|
15
12
|
|
|
16
|
-
def executable
|
|
13
|
+
def executable
|
|
17
14
|
override = @environment["ZUI_HOST"]
|
|
18
15
|
return checked_executable(override) if override && !override.empty?
|
|
19
|
-
return
|
|
20
|
-
return cached if File.executable?(cached)
|
|
21
|
-
return nil unless build
|
|
16
|
+
return client.executable if client.configured?
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
raise ArgumentError, "Zui is not configured for #{platform.id}; run `zui doctor --fix`"
|
|
24
19
|
end
|
|
25
20
|
|
|
26
|
-
def available?
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cmake = find_command("cmake")
|
|
30
|
-
raise ArgumentError, platform_help unless cmake
|
|
31
|
-
|
|
32
|
-
FileUtils.mkdir_p(File.dirname(destination))
|
|
33
|
-
Dir.mktmpdir("zui-host-build-") do |build_dir|
|
|
34
|
-
configure = Command.run([
|
|
35
|
-
cmake, "-S", File.join(@framework_root, "native"), "-B", build_dir,
|
|
36
|
-
"-DCMAKE_BUILD_TYPE=Release"
|
|
37
|
-
], timeout: 180, max_output_bytes: 4_194_304)
|
|
38
|
-
raise ArgumentError, "Zui host configuration failed:\n#{configure.stderr}" unless configure.success?
|
|
39
|
-
|
|
40
|
-
compile = Command.run([cmake, "--build", build_dir, "--config", "Release", "--parallel"],
|
|
41
|
-
timeout: 600, max_output_bytes: 8_388_608)
|
|
42
|
-
raise ArgumentError, "Zui host build failed:\n#{compile.stderr}" unless compile.success?
|
|
43
|
-
|
|
44
|
-
built = locate_build(build_dir)
|
|
45
|
-
raise ArgumentError, "Zui host build produced no executable" unless built
|
|
46
|
-
temporary = "#{destination}.install-#{Process.pid}"
|
|
47
|
-
FileUtils.cp(built, temporary)
|
|
48
|
-
FileUtils.chmod(0o755, temporary)
|
|
49
|
-
File.rename(temporary, destination)
|
|
50
|
-
ensure
|
|
51
|
-
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
52
|
-
end
|
|
53
|
-
destination
|
|
54
|
-
end
|
|
21
|
+
def available?
|
|
22
|
+
override = @environment["ZUI_HOST"]
|
|
23
|
+
return File.executable?(File.expand_path(override)) if override && !override.empty?
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
if platform.macos?
|
|
58
|
-
"building Zui on macOS requires CMake and Qt 6 (for example: brew install cmake qt)"
|
|
59
|
-
elsif platform.windows?
|
|
60
|
-
"building Zui on Windows requires CMake, Qt 6, and a C++17 toolchain (Visual Studio Build Tools or LLVM/MinGW)"
|
|
61
|
-
else
|
|
62
|
-
"building Zui on Linux requires CMake, a C++17 compiler, and Qt 6 Core/Gui/Qml/Quick development packages"
|
|
63
|
-
end
|
|
25
|
+
client.configured?
|
|
64
26
|
end
|
|
65
27
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def bundled
|
|
69
|
-
File.join(@framework_root, "vendor", "host", platform.id, executable_name)
|
|
70
|
-
end
|
|
28
|
+
def configure! = client.configure!
|
|
71
29
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
File.join(cache_root, "zui", "host", VERSION, platform.id, executable_name)
|
|
76
|
-
end
|
|
30
|
+
def environment(base = ENV.to_h)
|
|
31
|
+
override = @environment["ZUI_HOST"]
|
|
32
|
+
return base.to_h.dup if override && !override.empty?
|
|
77
33
|
|
|
78
|
-
|
|
79
|
-
home = @environment["HOME"] || @environment["USERPROFILE"] || Dir.home
|
|
80
|
-
if platform.windows?
|
|
81
|
-
@environment["LOCALAPPDATA"] || @environment["APPDATA"] || File.join(home, "AppData", "Local")
|
|
82
|
-
elsif platform.macos?
|
|
83
|
-
File.join(home, "Library", "Caches")
|
|
84
|
-
else
|
|
85
|
-
File.join(home, ".cache")
|
|
86
|
-
end
|
|
34
|
+
client.environment(base)
|
|
87
35
|
end
|
|
88
36
|
|
|
89
|
-
|
|
37
|
+
private
|
|
90
38
|
|
|
91
39
|
def checked_executable(path)
|
|
92
40
|
expanded = File.expand_path(path)
|
|
93
41
|
raise ArgumentError, "ZUI_HOST is not executable: #{expanded}" unless File.executable?(expanded)
|
|
94
42
|
expanded
|
|
95
43
|
end
|
|
96
|
-
|
|
97
|
-
def find_command(name)
|
|
98
|
-
@environment.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
99
|
-
command_names(name).each do |command_name|
|
|
100
|
-
path = File.join(directory, command_name)
|
|
101
|
-
return path if File.executable?(path) && !File.directory?(path)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def command_names(name)
|
|
108
|
-
return [name] unless platform.windows? && File.extname(name).empty?
|
|
109
|
-
|
|
110
|
-
extensions = @environment.fetch("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
|
|
111
|
-
[name] + extensions.flat_map { |extension| ["#{name}#{extension.downcase}", "#{name}#{extension.upcase}"] }.uniq
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def locate_build(build_dir)
|
|
115
|
-
candidates = [
|
|
116
|
-
File.join(build_dir, executable_name),
|
|
117
|
-
File.join(build_dir, "Release", executable_name),
|
|
118
|
-
File.join(build_dir, "zui-host.app", "Contents", "MacOS", "zui-host")
|
|
119
|
-
]
|
|
120
|
-
candidates.find { |path| File.executable?(path) }
|
|
121
|
-
end
|
|
122
44
|
end
|
|
123
45
|
end
|
data/lib/zui/runner.rb
CHANGED
data/lib/zui.rb
CHANGED
|
@@ -14,13 +14,14 @@ require_relative "zui/application"
|
|
|
14
14
|
require_relative "zui/source_bundle"
|
|
15
15
|
require_relative "zui/platform"
|
|
16
16
|
require_relative "zui/runtime"
|
|
17
|
+
require_relative "zui/client"
|
|
17
18
|
require_relative "zui/host"
|
|
18
19
|
require_relative "zui/runner"
|
|
19
20
|
require_relative "zui/generator"
|
|
20
21
|
require_relative "zui/distribution"
|
|
21
22
|
|
|
22
23
|
module Zui
|
|
23
|
-
VERSION = "0.0.
|
|
24
|
+
VERSION = "0.0.2"
|
|
24
25
|
FRAMEWORK_ROOT = File.expand_path("..", __dir__)
|
|
25
26
|
|
|
26
27
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Moussa Ali
|
|
@@ -9,7 +9,8 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
12
|
+
description: First-class UI, state, events, animation, media, GPU effects, and 3D
|
|
13
|
+
for Ruby desktop applications.
|
|
13
14
|
executables:
|
|
14
15
|
- zui
|
|
15
16
|
extensions: []
|
|
@@ -314,6 +315,7 @@ files:
|
|
|
314
315
|
- lib/zui/application.rb
|
|
315
316
|
- lib/zui/builder.rb
|
|
316
317
|
- lib/zui/cli.rb
|
|
318
|
+
- lib/zui/client.rb
|
|
317
319
|
- lib/zui/command.rb
|
|
318
320
|
- lib/zui/component_registry.rb
|
|
319
321
|
- lib/zui/components.rb
|
|
@@ -329,15 +331,6 @@ files:
|
|
|
329
331
|
- lib/zui/source_bundle.rb
|
|
330
332
|
- lib/zui/state_store.rb
|
|
331
333
|
- lib/zui/value.rb
|
|
332
|
-
- native/CMakeLists.txt
|
|
333
|
-
- native/ZuiClipboard.cpp
|
|
334
|
-
- native/ZuiClipboard.h
|
|
335
|
-
- native/ZuiProcess.cpp
|
|
336
|
-
- native/ZuiProcess.h
|
|
337
|
-
- native/main.cpp
|
|
338
|
-
- vendor/host/linux-x86_64/PROVENANCE.md
|
|
339
|
-
- vendor/host/linux-x86_64/zui-host
|
|
340
|
-
- vendor/host/linux-x86_64/zui-host.sha256
|
|
341
334
|
homepage: https://github.com/AdamMusa/zui
|
|
342
335
|
licenses:
|
|
343
336
|
- MIT
|
|
@@ -361,5 +354,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
361
354
|
requirements: []
|
|
362
355
|
rubygems_version: 4.0.18
|
|
363
356
|
specification_version: 4
|
|
364
|
-
summary: Build
|
|
357
|
+
summary: Build beautiful native desktop applications in pure Ruby
|
|
365
358
|
test_files: []
|
data/native/CMakeLists.txt
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
cmake_minimum_required(VERSION 3.21)
|
|
2
|
-
project(zui-host VERSION 0.1.0 LANGUAGES CXX)
|
|
3
|
-
|
|
4
|
-
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
-
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
-
set(CMAKE_AUTOMOC ON)
|
|
7
|
-
|
|
8
|
-
find_package(Qt6 6.8 REQUIRED COMPONENTS
|
|
9
|
-
Core
|
|
10
|
-
Gui
|
|
11
|
-
Multimedia
|
|
12
|
-
Qml
|
|
13
|
-
Quick
|
|
14
|
-
QuickControls2
|
|
15
|
-
QuickVectorImage
|
|
16
|
-
)
|
|
17
|
-
find_package(Qt6 6.8 QUIET COMPONENTS WebEngineQuick)
|
|
18
|
-
|
|
19
|
-
qt_add_executable(zui-host
|
|
20
|
-
main.cpp
|
|
21
|
-
ZuiClipboard.cpp
|
|
22
|
-
ZuiClipboard.h
|
|
23
|
-
ZuiProcess.cpp
|
|
24
|
-
ZuiProcess.h
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
target_link_libraries(zui-host PRIVATE
|
|
28
|
-
Qt6::Core
|
|
29
|
-
Qt6::Gui
|
|
30
|
-
Qt6::Multimedia
|
|
31
|
-
Qt6::Qml
|
|
32
|
-
Qt6::Quick
|
|
33
|
-
Qt6::QuickControls2
|
|
34
|
-
Qt6::QuickVectorImage
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
if(TARGET Qt6::WebEngineQuick)
|
|
38
|
-
target_link_libraries(zui-host PRIVATE Qt6::WebEngineQuick)
|
|
39
|
-
target_compile_definitions(zui-host PRIVATE ZUI_HAS_WEBENGINE=1)
|
|
40
|
-
endif()
|
|
41
|
-
|
|
42
|
-
install(TARGETS zui-host
|
|
43
|
-
BUNDLE DESTINATION .
|
|
44
|
-
RUNTIME DESTINATION bin
|
|
45
|
-
)
|
data/native/ZuiClipboard.cpp
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#include "ZuiClipboard.h"
|
|
2
|
-
|
|
3
|
-
#include <QGuiApplication>
|
|
4
|
-
#include <QClipboard>
|
|
5
|
-
|
|
6
|
-
ZuiClipboard::ZuiClipboard(QObject *parent) : QObject(parent) {
|
|
7
|
-
connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &ZuiClipboard::textChanged);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
QString ZuiClipboard::text() const { return QGuiApplication::clipboard()->text(); }
|
|
11
|
-
|
|
12
|
-
void ZuiClipboard::setText(const QString &text) {
|
|
13
|
-
if (this->text() == text)
|
|
14
|
-
return;
|
|
15
|
-
QGuiApplication::clipboard()->setText(text);
|
|
16
|
-
}
|
data/native/ZuiClipboard.h
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#pragma once
|
|
2
|
-
|
|
3
|
-
#include <QObject>
|
|
4
|
-
|
|
5
|
-
class ZuiClipboard final : public QObject {
|
|
6
|
-
Q_OBJECT
|
|
7
|
-
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
|
|
8
|
-
|
|
9
|
-
public:
|
|
10
|
-
explicit ZuiClipboard(QObject *parent = nullptr);
|
|
11
|
-
QString text() const;
|
|
12
|
-
void setText(const QString &text);
|
|
13
|
-
|
|
14
|
-
signals:
|
|
15
|
-
void textChanged();
|
|
16
|
-
};
|
data/native/ZuiProcess.cpp
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#include "ZuiProcess.h"
|
|
2
|
-
|
|
3
|
-
#include <QProcessEnvironment>
|
|
4
|
-
#include <QTimer>
|
|
5
|
-
|
|
6
|
-
ZuiProcess::ZuiProcess(QObject *parent) : QObject(parent) {
|
|
7
|
-
connect(&m_process, &QProcess::stateChanged, this, [this] { emit runningChanged(); });
|
|
8
|
-
connect(&m_process, &QProcess::readyReadStandardOutput, this, [this] {
|
|
9
|
-
m_stdoutBuffer += m_process.readAllStandardOutput();
|
|
10
|
-
consume(m_stdoutBuffer, false);
|
|
11
|
-
});
|
|
12
|
-
connect(&m_process, &QProcess::readyReadStandardError, this, [this] {
|
|
13
|
-
m_stderrBuffer += m_process.readAllStandardError();
|
|
14
|
-
consume(m_stderrBuffer, true);
|
|
15
|
-
});
|
|
16
|
-
connect(&m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
|
|
17
|
-
emit errorLineReceived(m_process.errorString());
|
|
18
|
-
});
|
|
19
|
-
connect(&m_process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
|
|
20
|
-
[this](int exitCode, QProcess::ExitStatus) {
|
|
21
|
-
m_stdoutBuffer += m_process.readAllStandardOutput();
|
|
22
|
-
m_stderrBuffer += m_process.readAllStandardError();
|
|
23
|
-
flush(m_stdoutBuffer, false);
|
|
24
|
-
flush(m_stderrBuffer, true);
|
|
25
|
-
emit exited(exitCode);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
ZuiProcess::~ZuiProcess() { stop(); }
|
|
30
|
-
|
|
31
|
-
bool ZuiProcess::running() const { return m_process.state() != QProcess::NotRunning; }
|
|
32
|
-
|
|
33
|
-
void ZuiProcess::start(const QString &executable, const QString &program,
|
|
34
|
-
const QString &workingDirectory, const QString &loadPath) {
|
|
35
|
-
if (running() || executable.isEmpty() || program.isEmpty())
|
|
36
|
-
return;
|
|
37
|
-
|
|
38
|
-
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
|
39
|
-
environment.insert(QStringLiteral("ZUI_PROJECT_DIR"), workingDirectory);
|
|
40
|
-
m_process.setProcessEnvironment(environment);
|
|
41
|
-
m_process.setWorkingDirectory(workingDirectory);
|
|
42
|
-
m_process.setProcessChannelMode(QProcess::SeparateChannels);
|
|
43
|
-
|
|
44
|
-
QStringList arguments;
|
|
45
|
-
if (!loadPath.isEmpty())
|
|
46
|
-
arguments << QStringLiteral("-I") << loadPath;
|
|
47
|
-
arguments << program;
|
|
48
|
-
m_process.start(executable, arguments, QIODevice::ReadWrite);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
void ZuiProcess::write(const QString &data) {
|
|
52
|
-
if (!running())
|
|
53
|
-
return;
|
|
54
|
-
m_process.write(data.toUtf8());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
void ZuiProcess::stop() {
|
|
58
|
-
if (!running())
|
|
59
|
-
return;
|
|
60
|
-
m_process.closeWriteChannel();
|
|
61
|
-
m_process.terminate();
|
|
62
|
-
if (!m_process.waitForFinished(1000)) {
|
|
63
|
-
m_process.kill();
|
|
64
|
-
m_process.waitForFinished(1000);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
void ZuiProcess::consume(QByteArray &buffer, bool errorStream) {
|
|
69
|
-
qsizetype newline = -1;
|
|
70
|
-
while ((newline = buffer.indexOf('\n')) >= 0) {
|
|
71
|
-
const QString line = QString::fromUtf8(buffer.left(newline));
|
|
72
|
-
buffer.remove(0, newline + 1);
|
|
73
|
-
errorStream ? emit errorLineReceived(line) : emit lineReceived(line);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
void ZuiProcess::flush(QByteArray &buffer, bool errorStream) {
|
|
78
|
-
consume(buffer, errorStream);
|
|
79
|
-
if (buffer.isEmpty())
|
|
80
|
-
return;
|
|
81
|
-
const QString line = QString::fromUtf8(buffer);
|
|
82
|
-
buffer.clear();
|
|
83
|
-
errorStream ? emit errorLineReceived(line) : emit lineReceived(line);
|
|
84
|
-
}
|
data/native/ZuiProcess.h
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#pragma once
|
|
2
|
-
|
|
3
|
-
#include <QObject>
|
|
4
|
-
#include <QProcess>
|
|
5
|
-
|
|
6
|
-
class ZuiProcess final : public QObject {
|
|
7
|
-
Q_OBJECT
|
|
8
|
-
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
|
|
9
|
-
|
|
10
|
-
public:
|
|
11
|
-
explicit ZuiProcess(QObject *parent = nullptr);
|
|
12
|
-
~ZuiProcess() override;
|
|
13
|
-
|
|
14
|
-
bool running() const;
|
|
15
|
-
|
|
16
|
-
Q_INVOKABLE void start(const QString &executable, const QString &program,
|
|
17
|
-
const QString &workingDirectory, const QString &loadPath);
|
|
18
|
-
Q_INVOKABLE void write(const QString &data);
|
|
19
|
-
Q_INVOKABLE void stop();
|
|
20
|
-
|
|
21
|
-
signals:
|
|
22
|
-
void runningChanged();
|
|
23
|
-
void lineReceived(const QString &line);
|
|
24
|
-
void errorLineReceived(const QString &line);
|
|
25
|
-
void exited(int exitCode);
|
|
26
|
-
|
|
27
|
-
private:
|
|
28
|
-
void consume(QByteArray &buffer, bool errorStream);
|
|
29
|
-
void flush(QByteArray &buffer, bool errorStream);
|
|
30
|
-
|
|
31
|
-
QProcess m_process;
|
|
32
|
-
QByteArray m_stdoutBuffer;
|
|
33
|
-
QByteArray m_stderrBuffer;
|
|
34
|
-
};
|
data/native/main.cpp
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#include "ZuiClipboard.h"
|
|
2
|
-
#include "ZuiProcess.h"
|
|
3
|
-
|
|
4
|
-
#include <QCommandLineParser>
|
|
5
|
-
#include <QDir>
|
|
6
|
-
#include <QFileInfo>
|
|
7
|
-
#include <QGuiApplication>
|
|
8
|
-
#include <QQmlApplicationEngine>
|
|
9
|
-
#include <QQmlContext>
|
|
10
|
-
#include <QQuickStyle>
|
|
11
|
-
|
|
12
|
-
#ifdef ZUI_HAS_WEBENGINE
|
|
13
|
-
#include <QtWebEngineQuick/qtwebenginequickglobal.h>
|
|
14
|
-
#endif
|
|
15
|
-
|
|
16
|
-
int main(int argc, char *argv[]) {
|
|
17
|
-
#ifdef ZUI_HAS_WEBENGINE
|
|
18
|
-
QtWebEngineQuick::initialize();
|
|
19
|
-
#endif
|
|
20
|
-
QGuiApplication application(argc, argv);
|
|
21
|
-
QCoreApplication::setApplicationName(QStringLiteral("Zui"));
|
|
22
|
-
QCoreApplication::setOrganizationName(QStringLiteral("Zui"));
|
|
23
|
-
QQuickStyle::setStyle(QStringLiteral("Fusion"));
|
|
24
|
-
|
|
25
|
-
QCommandLineParser parser;
|
|
26
|
-
parser.setApplicationDescription(QStringLiteral("Zui cross-platform Qt host"));
|
|
27
|
-
parser.addHelpOption();
|
|
28
|
-
parser.addOption({QStringLiteral("qml-root"), QStringLiteral("Zui QML runtime directory"), QStringLiteral("path")});
|
|
29
|
-
parser.addOption({QStringLiteral("project"), QStringLiteral("Application project directory"), QStringLiteral("path")});
|
|
30
|
-
parser.addOption({QStringLiteral("program"), QStringLiteral("Ruby application entrypoint"), QStringLiteral("path")});
|
|
31
|
-
parser.addOption({QStringLiteral("ruby"), QStringLiteral("Ruby executable"), QStringLiteral("path"), QStringLiteral("ruby")});
|
|
32
|
-
parser.addOption({QStringLiteral("load-path"), QStringLiteral("Ruby framework load path"), QStringLiteral("path")});
|
|
33
|
-
parser.addOption({QStringLiteral("name"), QStringLiteral("Application name"), QStringLiteral("name"), QStringLiteral("Zui Application")});
|
|
34
|
-
parser.process(application);
|
|
35
|
-
|
|
36
|
-
const QString qmlRootValue = parser.value(QStringLiteral("qml-root"));
|
|
37
|
-
const QString projectValue = parser.value(QStringLiteral("project"));
|
|
38
|
-
const QString programValue = parser.value(QStringLiteral("program"));
|
|
39
|
-
const QString qmlRoot = QFileInfo(qmlRootValue).absoluteFilePath();
|
|
40
|
-
const QString project = QFileInfo(projectValue).absoluteFilePath();
|
|
41
|
-
const QString program = QFileInfo(programValue).absoluteFilePath();
|
|
42
|
-
const QFileInfo desktopFile(QDir(qmlRoot).filePath(QStringLiteral("Desktop.qml")));
|
|
43
|
-
if (qmlRootValue.isEmpty() || projectValue.isEmpty() || programValue.isEmpty()
|
|
44
|
-
|| !desktopFile.isFile() || !QFileInfo(project).isDir() || !QFileInfo(program).isFile())
|
|
45
|
-
parser.showHelp(64);
|
|
46
|
-
|
|
47
|
-
ZuiProcess process;
|
|
48
|
-
ZuiClipboard clipboard;
|
|
49
|
-
QQmlApplicationEngine engine;
|
|
50
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiProcess"), &process);
|
|
51
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiClipboard"), &clipboard);
|
|
52
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiProjectDir"), project);
|
|
53
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiComponentDir"), QDir(qmlRoot).filePath(QStringLiteral("Components/Builtins")));
|
|
54
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyExecutable"), parser.value(QStringLiteral("ruby")));
|
|
55
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyProgram"), program);
|
|
56
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyLoadPath"), parser.value(QStringLiteral("load-path")));
|
|
57
|
-
engine.rootContext()->setContextProperty(QStringLiteral("zuiApplicationName"), parser.value(QStringLiteral("name")));
|
|
58
|
-
engine.addImportPath(qmlRoot);
|
|
59
|
-
|
|
60
|
-
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &application,
|
|
61
|
-
[] { QCoreApplication::exit(1); }, Qt::QueuedConnection);
|
|
62
|
-
engine.load(QUrl::fromLocalFile(QDir(qmlRoot).filePath(QStringLiteral("Desktop.qml"))));
|
|
63
|
-
return application.exec();
|
|
64
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Zui Linux host provenance
|
|
2
|
-
|
|
3
|
-
- Target: Linux x86-64
|
|
4
|
-
- Source: `native/` in this repository
|
|
5
|
-
- Framework version: Zui 0.1.0
|
|
6
|
-
- Compiler: GNU C++ 16.2.1
|
|
7
|
-
- Qt: 6.11.2
|
|
8
|
-
- Build type: CMake `Release`
|
|
9
|
-
|
|
10
|
-
The binary is reproducible from the checked-in C++ sources with:
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
cmake -S native -B build -DCMAKE_BUILD_TYPE=Release
|
|
14
|
-
cmake --build build --parallel
|
|
15
|
-
```
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
de446bb598f922a7a58fe30127d88cae9602cf54bd819aa2b5c84447a51da7a4 zui-host
|