zui 0.0.6 → 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/ControlNode.qml +2 -909
- data/Desktop.qml +3 -0
- data/README.md +391 -66
- data/Service.qml +14 -2
- data/lib/zui/application_runtime.rb +43 -0
- data/lib/zui/cli.rb +62 -12
- data/lib/zui/component_registry.rb +14 -1
- data/lib/zui/dist_config.rb +180 -0
- data/lib/zui/dist_packager.rb +366 -0
- data/lib/zui/distribution.rb +184 -18
- data/lib/zui/full_runtime.rb +241 -0
- data/lib/zui/generator.rb +49 -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/tree_shaker.rb +527 -0
- data/lib/zui.rb +8 -1
- metadata +10 -1
data/lib/zui/distribution.rb
CHANGED
|
@@ -2,20 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
|
+
require "rbconfig"
|
|
5
6
|
|
|
6
7
|
module Zui
|
|
7
8
|
class Distribution
|
|
8
|
-
|
|
9
|
+
RUNTIME_MODES = %i[lite full].freeze
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
attr_reader :platform, :tree_shake_report, :runtime_mode
|
|
12
|
+
|
|
13
|
+
def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT,
|
|
14
|
+
ruby: RbConfig.ruby, tree_shake: true, release_config: nil, runtime_mode: :lite,
|
|
15
|
+
runtime_builder: nil)
|
|
11
16
|
@platform = platform.assert_supported!
|
|
12
17
|
@client = client || Client.new(platform: @platform)
|
|
13
18
|
@framework_root = framework_root
|
|
19
|
+
@ruby = File.expand_path(ruby)
|
|
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
|
|
25
|
+
@tree_shake_report = nil
|
|
26
|
+
@release_config = release_config
|
|
27
|
+
@runtime_builder = runtime_builder
|
|
28
|
+
@application_runtime = nil
|
|
14
29
|
end
|
|
15
30
|
|
|
16
31
|
def bundle(source, name: nil, destination: nil)
|
|
17
32
|
created = false
|
|
18
33
|
project = File.expand_path(source)
|
|
34
|
+
@project = project
|
|
19
35
|
raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
|
|
20
36
|
raise ArgumentError, "main.rb not found: #{project}" unless File.file?(File.join(project, "main.rb"))
|
|
21
37
|
unless @client.configured?
|
|
@@ -45,7 +61,9 @@ module Zui
|
|
|
45
61
|
File.join(destination, "runtime"), File.join(destination, "share", "applications")])
|
|
46
62
|
install_application(project, File.join(destination, "app"))
|
|
47
63
|
install_runtime(File.join(destination, "runtime"))
|
|
64
|
+
install_application_runtime(project, File.join(destination, "runtime"))
|
|
48
65
|
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
66
|
+
shake_bundle(project, File.join(destination, "runtime"))
|
|
49
67
|
write_linux_launcher(destination, app_name)
|
|
50
68
|
desktop_name = "#{slug(app_name)}.desktop"
|
|
51
69
|
File.write(File.join(destination, "share", "applications", desktop_name), <<~DESKTOP)
|
|
@@ -66,10 +84,13 @@ module Zui
|
|
|
66
84
|
FileUtils.mkdir_p([macos, File.join(resources, "app"), File.join(resources, "runtime")])
|
|
67
85
|
install_application(project, File.join(resources, "app"))
|
|
68
86
|
install_runtime(File.join(resources, "runtime"))
|
|
87
|
+
install_application_runtime(project, File.join(resources, "runtime"))
|
|
69
88
|
@client.copy_to(File.join(resources, "runtime", "native"))
|
|
89
|
+
shake_bundle(project, File.join(resources, "runtime"))
|
|
90
|
+
icon_name = install_macos_release_icon(resources)
|
|
70
91
|
File.write(File.join(macos, "run"), macos_launcher(app_name))
|
|
71
92
|
FileUtils.chmod(0o755, File.join(macos, "run"))
|
|
72
|
-
File.write(File.join(contents, "Info.plist"), info_plist(app_name))
|
|
93
|
+
File.write(File.join(contents, "Info.plist"), info_plist(app_name, icon_name:))
|
|
73
94
|
write_manifest(resources, app_name)
|
|
74
95
|
end
|
|
75
96
|
|
|
@@ -78,16 +99,24 @@ module Zui
|
|
|
78
99
|
File.join(destination, "runtime")])
|
|
79
100
|
install_application(project, File.join(destination, "app"))
|
|
80
101
|
install_runtime(File.join(destination, "runtime"))
|
|
102
|
+
install_application_runtime(project, File.join(destination, "runtime"))
|
|
81
103
|
@client.copy_to(File.join(destination, "runtime", "native"))
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
shake_bundle(project, File.join(destination, "runtime"))
|
|
105
|
+
install_windows_release_icon(destination)
|
|
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
|
|
84
112
|
write_manifest(destination, app_name)
|
|
85
113
|
end
|
|
86
114
|
|
|
87
115
|
def install_application(source, destination)
|
|
88
116
|
entries = Dir.children(source).reject do |entry|
|
|
89
117
|
entry_path = File.join(source, entry)
|
|
90
|
-
%w[.git dist].include?(entry) ||
|
|
118
|
+
%w[.git dist config.rb].include?(entry) ||
|
|
119
|
+
destination.start_with?("#{entry_path}#{File::SEPARATOR}")
|
|
91
120
|
end
|
|
92
121
|
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination) unless entries.empty?
|
|
93
122
|
end
|
|
@@ -103,6 +132,15 @@ module Zui
|
|
|
103
132
|
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, target) unless entries.empty?
|
|
104
133
|
end
|
|
105
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
|
+
|
|
106
144
|
def write_linux_launcher(destination, app_name)
|
|
107
145
|
File.write(File.join(destination, "run"), <<~SH)
|
|
108
146
|
#!/bin/sh
|
|
@@ -110,13 +148,13 @@ module Zui
|
|
|
110
148
|
bundle_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
111
149
|
native_dir="$bundle_dir/runtime/native"
|
|
112
150
|
#{posix_client_environment}
|
|
113
|
-
|
|
151
|
+
#{posix_ruby_command}
|
|
114
152
|
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
115
153
|
--qml-root "$bundle_dir/runtime/qml" \
|
|
116
154
|
--project "$bundle_dir/app" \
|
|
117
|
-
--program "$bundle_dir
|
|
155
|
+
--program #{posix_program("$bundle_dir")} \
|
|
118
156
|
--ruby "$ruby_command" \
|
|
119
|
-
--load-path "$bundle_dir
|
|
157
|
+
--load-path #{posix_load_path("$bundle_dir")} \
|
|
120
158
|
--name #{shell_quote(app_name)}
|
|
121
159
|
SH
|
|
122
160
|
FileUtils.chmod(0o755, File.join(destination, "run"))
|
|
@@ -130,13 +168,13 @@ module Zui
|
|
|
130
168
|
resources="$contents/Resources"
|
|
131
169
|
native_dir="$resources/runtime/native"
|
|
132
170
|
#{posix_client_environment}
|
|
133
|
-
|
|
171
|
+
#{posix_ruby_command}
|
|
134
172
|
exec "$native_dir/#{@client.executable_relative_path}" \
|
|
135
173
|
--qml-root "$resources/runtime/qml" \
|
|
136
174
|
--project "$resources/app" \
|
|
137
|
-
--program "$resources
|
|
175
|
+
--program #{posix_program("$resources")} \
|
|
138
176
|
--ruby "$ruby_command" \
|
|
139
|
-
--load-path "$resources
|
|
177
|
+
--load-path #{posix_load_path("$resources")} \
|
|
140
178
|
--name #{shell_quote(app_name)}
|
|
141
179
|
SH
|
|
142
180
|
end
|
|
@@ -177,8 +215,39 @@ module Zui
|
|
|
177
215
|
CMD
|
|
178
216
|
end
|
|
179
217
|
|
|
180
|
-
def
|
|
181
|
-
|
|
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
|
+
|
|
247
|
+
def info_plist(app_name, icon_name: nil)
|
|
248
|
+
identifier = @release_config&.identifier || "dev.zui.#{slug(app_name).tr("-", ".")}"
|
|
249
|
+
application_version = @release_config&.version || VERSION
|
|
250
|
+
icon_entry = icon_name ? "<key>CFBundleIconFile</key><string>#{xml_escape(icon_name)}</string>" : ""
|
|
182
251
|
<<~PLIST
|
|
183
252
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
184
253
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -187,18 +256,50 @@ module Zui
|
|
|
187
256
|
<key>CFBundleIdentifier</key><string>#{identifier}</string>
|
|
188
257
|
<key>CFBundleName</key><string>#{xml_escape(app_name)}</string>
|
|
189
258
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
190
|
-
<key>CFBundleShortVersionString</key><string>#{
|
|
259
|
+
<key>CFBundleShortVersionString</key><string>#{xml_escape(application_version)}</string>
|
|
260
|
+
<key>CFBundleVersion</key><string>#{xml_escape(application_version)}</string>
|
|
261
|
+
#{icon_entry}
|
|
191
262
|
<key>NSHighResolutionCapable</key><true/>
|
|
192
263
|
</dict></plist>
|
|
193
264
|
PLIST
|
|
194
265
|
end
|
|
195
266
|
|
|
196
267
|
def write_manifest(directory, app_name)
|
|
197
|
-
|
|
268
|
+
manifest = {
|
|
198
269
|
"format" => 1, "framework" => "zui", "version" => VERSION,
|
|
199
270
|
"platform" => platform.os.to_s, "architecture" => platform.arch.to_s,
|
|
200
|
-
"name" => app_name, "client_version" => @client.manifest.fetch("client_version")
|
|
201
|
-
|
|
271
|
+
"name" => app_name, "client_version" => @client.manifest.fetch("client_version"),
|
|
272
|
+
"tree_shaken" => !@tree_shake_report.nil?, "ruby_runtime" => runtime_mode.to_s
|
|
273
|
+
}
|
|
274
|
+
manifest["tree_shake"] = @tree_shake_report.to_h if @tree_shake_report
|
|
275
|
+
if @release_config
|
|
276
|
+
manifest["identifier"] = @release_config.identifier
|
|
277
|
+
manifest["application_version"] = @release_config.version
|
|
278
|
+
end
|
|
279
|
+
File.write(File.join(directory, "zui-bundle.json"), JSON.pretty_generate(manifest))
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def install_macos_release_icon(resources)
|
|
283
|
+
return nil unless @release_config
|
|
284
|
+
|
|
285
|
+
source = @release_config.icon_path(@project, platform)
|
|
286
|
+
name = "Application.icns"
|
|
287
|
+
FileUtils.cp(source, File.join(resources, name))
|
|
288
|
+
name
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def install_windows_release_icon(destination)
|
|
292
|
+
return unless @release_config
|
|
293
|
+
|
|
294
|
+
FileUtils.cp(@release_config.icon_path(@project, platform), File.join(destination, "app.ico"))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def shake_bundle(project, runtime)
|
|
298
|
+
return unless @tree_shake
|
|
299
|
+
|
|
300
|
+
@tree_shake_report = TreeShaker.new(
|
|
301
|
+
project:, framework: File.join(runtime, "qml"), native: File.join(runtime, "native"), platform:
|
|
302
|
+
).shake!
|
|
202
303
|
end
|
|
203
304
|
|
|
204
305
|
def default_destination(project, app_name)
|
|
@@ -209,6 +310,7 @@ module Zui
|
|
|
209
310
|
def titleize(value) = value.split(/[-_]/).map(&:capitalize).join(" ")
|
|
210
311
|
def slug(value) = value.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
211
312
|
def shell_quote(value) = "'#{value.to_s.gsub("'", %q('"'"'))}'"
|
|
313
|
+
def windows_quote(value) = %Q("#{value.to_s.gsub('%', '%%').gsub('"', '""')}")
|
|
212
314
|
def xml_escape(value) = value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
213
315
|
|
|
214
316
|
def posix_client_environment
|
|
@@ -222,6 +324,70 @@ module Zui
|
|
|
222
324
|
end.join("\n")
|
|
223
325
|
end
|
|
224
326
|
|
|
327
|
+
def posix_ruby_command
|
|
328
|
+
return posix_private_ruby_command if @application_runtime
|
|
329
|
+
|
|
330
|
+
<<~SH.chomp
|
|
331
|
+
ruby_command=${ZUI_RUBY:-}
|
|
332
|
+
if [ -z "$ruby_command" ]; then
|
|
333
|
+
packaged_ruby=#{shell_quote(@ruby)}
|
|
334
|
+
if [ -x "$packaged_ruby" ]; then
|
|
335
|
+
ruby_command=$packaged_ruby
|
|
336
|
+
else
|
|
337
|
+
ruby_command=$(command -v ruby || true)
|
|
338
|
+
[ -n "$ruby_command" ] || ruby_command=ruby
|
|
339
|
+
fi
|
|
340
|
+
fi
|
|
341
|
+
SH
|
|
342
|
+
end
|
|
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
|
+
|
|
225
391
|
def windows_client_environment = @client.environment_entries
|
|
226
392
|
|
|
227
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,7 +14,11 @@ 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)
|
|
19
|
+
File.write(File.join(@path, "config.rb"), distribution_config)
|
|
20
|
+
FileUtils.mkdir_p(File.join(@path, "assets"))
|
|
21
|
+
File.write(File.join(@path, "assets", "README.md"), icon_readme)
|
|
18
22
|
File.write(File.join(@path, "README.md"), readme)
|
|
19
23
|
@path
|
|
20
24
|
rescue StandardError
|
|
@@ -47,6 +51,16 @@ module Zui
|
|
|
47
51
|
RUBY
|
|
48
52
|
end
|
|
49
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
|
+
|
|
50
64
|
def welcome_component
|
|
51
65
|
<<~RUBY
|
|
52
66
|
# frozen_string_literal: true
|
|
@@ -72,14 +86,49 @@ module Zui
|
|
|
72
86
|
zui doctor --fix # once for each Zui version
|
|
73
87
|
zui run main.rb
|
|
74
88
|
zui bundle
|
|
89
|
+
zui bundle --dist # after adding the icons declared in config.rb
|
|
75
90
|
```
|
|
76
91
|
MARKDOWN
|
|
77
92
|
end
|
|
78
93
|
|
|
94
|
+
def distribution_config
|
|
95
|
+
<<~RUBY
|
|
96
|
+
# frozen_string_literal: true
|
|
97
|
+
|
|
98
|
+
Zui::Dist.configure do
|
|
99
|
+
name #{@name.dump}
|
|
100
|
+
identifier #{"com.example.#{slug_name}".dump}
|
|
101
|
+
version "0.1.0"
|
|
102
|
+
publisher "Your Name <you@example.com>"
|
|
103
|
+
description #{"A native #{@name} desktop application.".dump}
|
|
104
|
+
license "Proprietary"
|
|
105
|
+
icon linux: "assets/icon.png",
|
|
106
|
+
macos: "assets/icon.icns",
|
|
107
|
+
windows: "assets/icon.ico"
|
|
108
|
+
categories "Utility"
|
|
109
|
+
end
|
|
110
|
+
RUBY
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def icon_readme
|
|
114
|
+
<<~MARKDOWN
|
|
115
|
+
Add the release icons referenced by `config.rb` here:
|
|
116
|
+
|
|
117
|
+
- `icon.png` for Linux
|
|
118
|
+
- `icon.icns` for macOS
|
|
119
|
+
- `icon.ico` for Windows
|
|
120
|
+
MARKDOWN
|
|
121
|
+
end
|
|
122
|
+
|
|
79
123
|
def constant_name
|
|
80
124
|
value = @name.scan(/[a-z0-9]+/i).map { |part| part[0].upcase + part[1..].to_s }.join
|
|
81
125
|
value = "Application#{value}" if value.match?(/\A\d/)
|
|
82
126
|
value.empty? ? "ZuiApplication" : value
|
|
83
127
|
end
|
|
128
|
+
|
|
129
|
+
def slug_name
|
|
130
|
+
value = @name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
131
|
+
value.match?(/\A\d/) ? "app-#{value}" : value
|
|
132
|
+
end
|
|
84
133
|
end
|
|
85
134
|
end
|