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.
@@ -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
- attr_reader :platform
9
+ RUNTIME_MODES = %i[lite full].freeze
9
10
 
10
- def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT)
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
- File.write(File.join(destination, "run.rb"), windows_ruby_launcher(app_name))
83
- File.write(File.join(destination, "run.cmd"), windows_command_launcher)
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) || destination.start_with?("#{entry_path}#{File::SEPARATOR}")
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
- ruby_command=${ZUI_RUBY:-ruby}
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/app/main.rb" \
155
+ --program #{posix_program("$bundle_dir")} \
118
156
  --ruby "$ruby_command" \
119
- --load-path "$bundle_dir/runtime/lib" \
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
- ruby_command=${ZUI_RUBY:-ruby}
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/app/main.rb" \
175
+ --program #{posix_program("$resources")} \
138
176
  --ruby "$ruby_command" \
139
- --load-path "$resources/runtime/lib" \
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 info_plist(app_name)
181
- identifier = "dev.zui.#{slug(app_name).tr("-", ".")}"
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>#{VERSION}</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
- File.write(File.join(directory, "zui-bundle.json"), JSON.pretty_generate(
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("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zui
4
+ def self.app(&definition)
5
+ Application.new(&definition).run
6
+ end
7
+
8
+ class << self
9
+ alias application app
10
+ end
11
+ end