ruflet 0.0.21 → 0.0.22
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 +30 -2
- data/lib/ruflet/cli/build_command.rb +1482 -86
- data/lib/ruflet/cli/new_command.rb +81 -17
- data/lib/ruflet/cli/run_command.rb +279 -39
- data/lib/ruflet/cli.rb +7 -2
- data/lib/ruflet/hot_reload.rb +23 -10
- data/lib/ruflet/version.rb +1 -1
- metadata +1 -1
|
@@ -13,23 +13,25 @@ module Ruflet
|
|
|
13
13
|
RUNTIME_REPO_URL = ENV.fetch("RUFLET_RUNTIME_REPO_URL", "https://github.com/AdamMusa/ruflet.git")
|
|
14
14
|
|
|
15
15
|
CLIENT_EXTENSION_MAP = {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
16
|
+
"ads" => { package: "ruflet_ads", alias: "ruflet_ads" },
|
|
17
|
+
"audio" => { package: "ruflet_audio", alias: "ruflet_audio" },
|
|
18
|
+
"audio_recorder" => { package: "ruflet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
19
|
+
"camera" => { package: "ruflet_camera", alias: "ruflet_camera" },
|
|
20
|
+
"charts" => { package: "ruflet_charts", alias: "ruflet_charts" },
|
|
21
|
+
"code_editor" => { package: "ruflet_code_editor", alias: "ruflet_code_editor" },
|
|
22
|
+
"color_pickers" => { package: "ruflet_color_pickers", alias: "ruflet_color_picker" },
|
|
23
|
+
"datatable2" => { package: "ruflet_datatable2", alias: "ruflet_datatable2" },
|
|
24
|
+
"flashlight" => { package: "ruflet_flashlight", alias: "ruflet_flashlight" },
|
|
25
|
+
"geolocator" => { package: "ruflet_geolocator", alias: "ruflet_geolocator" },
|
|
26
|
+
"lottie" => { package: "ruflet_lottie", alias: "ruflet_lottie" },
|
|
27
|
+
"map" => { package: "ruflet_map", alias: "ruflet_map" },
|
|
28
|
+
"permission_handler" => { package: "ruflet_permission_handler", alias: "ruflet_permission_handler" },
|
|
28
29
|
"qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
|
|
29
|
-
"rive" => { package: "
|
|
30
|
-
"secure_storage" => { package: "
|
|
31
|
-
"
|
|
32
|
-
"
|
|
30
|
+
"rive" => { package: "ruflet_rive", alias: "ruflet_rive" },
|
|
31
|
+
"secure_storage" => { package: "ruflet_secure_storage", alias: "ruflet_secure_storage" },
|
|
32
|
+
"spinkit" => { package: "ruflet_spinkit", alias: "ruflet_spinkit" },
|
|
33
|
+
"video" => { package: "ruflet_video", alias: "ruflet_video" },
|
|
34
|
+
"webview" => { package: "ruflet_webview", alias: "ruflet_webview" }
|
|
33
35
|
}.freeze
|
|
34
36
|
|
|
35
37
|
def command_new(args)
|
|
@@ -51,6 +53,7 @@ module Ruflet
|
|
|
51
53
|
File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
|
|
52
54
|
write_default_ruflet_config(root, File.basename(root))
|
|
53
55
|
copy_default_project_assets(root)
|
|
56
|
+
copy_default_apple_extensions(root)
|
|
54
57
|
project_name = File.basename(root)
|
|
55
58
|
puts "Run:"
|
|
56
59
|
puts " cd #{project_name}"
|
|
@@ -72,7 +75,16 @@ module Ruflet
|
|
|
72
75
|
target = hidden_flutter_client_dir(root)
|
|
73
76
|
FileUtils.mkdir_p(File.dirname(target))
|
|
74
77
|
FileUtils.rm_rf(target)
|
|
75
|
-
|
|
78
|
+
# `target` is the Flutter project root, not a directory that contains
|
|
79
|
+
# the template root. Copying a source directory onto a target that was
|
|
80
|
+
# already created by FileUtils can produce
|
|
81
|
+
# `build/client/ruflet_flutter_template/...`, leaving `build/client`
|
|
82
|
+
# without pubspec.yaml and making Flutter reject it as a project.
|
|
83
|
+
FileUtils.mkdir_p(target)
|
|
84
|
+
Dir.children(template_root).each do |entry|
|
|
85
|
+
copy_client_template_entry(
|
|
86
|
+
template_root, target, entry)
|
|
87
|
+
end
|
|
76
88
|
prune_client_template(target)
|
|
77
89
|
write_client_template_revision(target, installed_template_revision(template_root))
|
|
78
90
|
end
|
|
@@ -261,6 +273,43 @@ module Ruflet
|
|
|
261
273
|
end
|
|
262
274
|
end
|
|
263
275
|
|
|
276
|
+
def copy_client_template_entry(template_root, target, relative)
|
|
277
|
+
return if generated_client_template_path?(relative)
|
|
278
|
+
|
|
279
|
+
source = File.join(template_root, relative)
|
|
280
|
+
destination = File.join(target, relative)
|
|
281
|
+
stat = File.lstat(source)
|
|
282
|
+
if stat.symlink?
|
|
283
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
284
|
+
FileUtils.ln_s(File.readlink(source), destination)
|
|
285
|
+
elsif stat.directory?
|
|
286
|
+
FileUtils.mkdir_p(destination)
|
|
287
|
+
Dir.children(source).each do |entry|
|
|
288
|
+
copy_client_template_entry(
|
|
289
|
+
template_root, target, File.join(relative, entry))
|
|
290
|
+
end
|
|
291
|
+
else
|
|
292
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
293
|
+
FileUtils.cp(source, destination, preserve: true)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def generated_client_template_path?(relative)
|
|
298
|
+
components = relative.split(File::SEPARATOR)
|
|
299
|
+
generated_components = %w[
|
|
300
|
+
.build .cache .claude .dart_tool .git .idea .swiftpm .symlinks
|
|
301
|
+
build DerivedData Pods xcuserdata
|
|
302
|
+
]
|
|
303
|
+
return true if components.any? { |component| generated_components.include?(component) }
|
|
304
|
+
return true if components.any? { |component| component.start_with?(".build-") }
|
|
305
|
+
|
|
306
|
+
generated_files = %w[
|
|
307
|
+
.DS_Store Package.resolved Podfile.lock local.properties
|
|
308
|
+
pubspec_overrides.yaml
|
|
309
|
+
]
|
|
310
|
+
generated_files.include?(components.last) || components.last.end_with?(".xcuserstate")
|
|
311
|
+
end
|
|
312
|
+
|
|
264
313
|
def write_default_ruflet_config(root, app_name)
|
|
265
314
|
File.write(File.join(root, "ruflet.yaml"), <<~YAML)
|
|
266
315
|
app:
|
|
@@ -319,6 +368,21 @@ module Ruflet
|
|
|
319
368
|
end
|
|
320
369
|
end
|
|
321
370
|
|
|
371
|
+
def copy_default_apple_extensions(root)
|
|
372
|
+
template_root = resolve_ruflet_client_template_root || ensure_cached_ruflet_client_template!
|
|
373
|
+
source = template_root && File.join(template_root, "apple_extensions")
|
|
374
|
+
return unless source && File.file?(File.join(source, "Package.swift"))
|
|
375
|
+
|
|
376
|
+
destination = File.join(root, "apple_extensions")
|
|
377
|
+
FileUtils.mkdir_p(destination)
|
|
378
|
+
generated_entries = %w[.build .swiftpm .dart_tool .claude]
|
|
379
|
+
Dir.children(source).each do |entry|
|
|
380
|
+
next if generated_entries.include?(entry)
|
|
381
|
+
|
|
382
|
+
FileUtils.cp_r(File.join(source, entry), File.join(destination, entry), preserve: true)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
322
386
|
def default_project_assets_root
|
|
323
387
|
[
|
|
324
388
|
File.expand_path("../../../assets", __dir__),
|
|
@@ -11,23 +11,25 @@ require "net/http"
|
|
|
11
11
|
require "uri"
|
|
12
12
|
require "thread"
|
|
13
13
|
require "io/console"
|
|
14
|
+
require "open3"
|
|
14
15
|
require "time"
|
|
15
16
|
|
|
16
17
|
module Ruflet
|
|
17
18
|
module CLI
|
|
18
19
|
module RunCommand
|
|
19
|
-
|
|
20
|
+
# Release assets were published as ruflet_client-* before the preview
|
|
21
|
+
# client became Ruflet Explorer. Ask for the current names, but keep
|
|
22
|
+
# recognising the old ones so an existing release still resolves.
|
|
23
|
+
ASSET_PREFIX = "ruflet_explorer"
|
|
24
|
+
LEGACY_ASSET_PREFIX = "ruflet_client"
|
|
25
|
+
CLIENT_CHANNEL_MANIFEST = "#{ASSET_PREFIX}-manifest.json"
|
|
26
|
+
LEGACY_CLIENT_CHANNEL_MANIFEST = "#{LEGACY_ASSET_PREFIX}-manifest.json"
|
|
27
|
+
EXPERIMENTAL_IOS_SIMULATOR_ASSET = "#{ASSET_PREFIX}-ios-experimental-simulator.zip"
|
|
28
|
+
EXPERIMENTAL_MACOS_ASSET = "#{ASSET_PREFIX}-macos-experimental-universal.zip"
|
|
20
29
|
DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
|
|
21
30
|
|
|
22
31
|
def command_run(args)
|
|
23
|
-
options =
|
|
24
|
-
parser = OptionParser.new do |o|
|
|
25
|
-
o.on("--web") { options[:target] = "web" }
|
|
26
|
-
o.on("--desktop") { options[:target] = "desktop" }
|
|
27
|
-
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
28
|
-
o.on("--no-reload") { options[:reload] = false }
|
|
29
|
-
end
|
|
30
|
-
parser.parse!(args)
|
|
32
|
+
options = parse_run_options(args)
|
|
31
33
|
|
|
32
34
|
script_token = args.shift || "main"
|
|
33
35
|
script_path = resolve_script(script_token)
|
|
@@ -37,6 +39,9 @@ module Ruflet
|
|
|
37
39
|
return 1
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
experimental_client = prepare_experimental_run_client(options)
|
|
43
|
+
return 1 if options[:experimental] && !experimental_client
|
|
44
|
+
|
|
40
45
|
selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
|
|
41
46
|
return 1 unless selected_port
|
|
42
47
|
env = {
|
|
@@ -70,7 +75,10 @@ module Ruflet
|
|
|
70
75
|
|
|
71
76
|
run_state = { child_pid: Process.spawn(env, *cmd, pgroup: true), restart: false }
|
|
72
77
|
reload_input_thread = options[:reload] ? start_reload_input_thread(run_state) : nil
|
|
73
|
-
launched_client_pids = launch_target_client(
|
|
78
|
+
launched_client_pids = launch_target_client(
|
|
79
|
+
options[:target], selected_port,
|
|
80
|
+
experimental_client: experimental_client
|
|
81
|
+
)
|
|
74
82
|
forward_signal = lambda do |signal|
|
|
75
83
|
begin
|
|
76
84
|
Process.kill(signal, -run_state[:child_pid])
|
|
@@ -87,7 +95,7 @@ module Ruflet
|
|
|
87
95
|
return status.success? ? 0 : (status.exitstatus || 1) unless run_state[:restart]
|
|
88
96
|
|
|
89
97
|
# Full restart requested ("R"): respawn the backend; connected
|
|
90
|
-
# clients reconnect and re-register on their own (
|
|
98
|
+
# clients reconnect and re-register on their own (Ruflet-style).
|
|
91
99
|
run_state[:restart] = false
|
|
92
100
|
puts "Restarting app..."
|
|
93
101
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -125,6 +133,72 @@ module Ruflet
|
|
|
125
133
|
|
|
126
134
|
private
|
|
127
135
|
|
|
136
|
+
def parse_run_options(args)
|
|
137
|
+
options = { target: "mobile", requested_port: 8550, reload: true, experimental: false }
|
|
138
|
+
parser = OptionParser.new do |o|
|
|
139
|
+
o.on("--web") { options[:target] = "web" }
|
|
140
|
+
o.on("--desktop") { options[:target] = "desktop" }
|
|
141
|
+
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
142
|
+
o.on("--no-reload") { options[:reload] = false }
|
|
143
|
+
o.on("--experimental", "--exp") { options[:experimental] = true }
|
|
144
|
+
end
|
|
145
|
+
parser.parse!(args)
|
|
146
|
+
options
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def prepare_experimental_run_client(options)
|
|
150
|
+
return nil unless options[:experimental]
|
|
151
|
+
|
|
152
|
+
if options[:target] == "web"
|
|
153
|
+
warn "run config error: --experimental/--exp is supported only for iOS and macOS"
|
|
154
|
+
return nil
|
|
155
|
+
end
|
|
156
|
+
unless host_platform_name == "macos"
|
|
157
|
+
warn "run config error: the experimental Apple client requires macOS"
|
|
158
|
+
return nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if options[:target] == "desktop"
|
|
162
|
+
root = ensure_prebuilt_client(desktop_experimental: true, platform: "macos")
|
|
163
|
+
unless root
|
|
164
|
+
warn "Experimental macOS client is unavailable."
|
|
165
|
+
warn "The release must contain #{EXPERIMENTAL_MACOS_ASSET}."
|
|
166
|
+
return nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return { kind: :desktop, root: root }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
root = ensure_prebuilt_client(ios_experimental: true, platform: "macos")
|
|
173
|
+
unless root
|
|
174
|
+
warn "Experimental iOS Simulator client is unavailable."
|
|
175
|
+
warn "The release must contain #{EXPERIMENTAL_IOS_SIMULATOR_ASSET}."
|
|
176
|
+
return nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
app_bundle = experimental_ios_app_bundle(root)
|
|
180
|
+
simulator = booted_ios_simulator
|
|
181
|
+
unless simulator
|
|
182
|
+
warn "No booted iOS Simulator was found."
|
|
183
|
+
warn "Start the simulator you want to use, then run the command again."
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
bundle_identifier = ios_bundle_identifier(app_bundle)
|
|
188
|
+
if bundle_identifier.to_s.empty?
|
|
189
|
+
warn "Experimental client has no CFBundleIdentifier: #{app_bundle}"
|
|
190
|
+
return nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
{
|
|
194
|
+
kind: :ios_simulator,
|
|
195
|
+
app_bundle: app_bundle,
|
|
196
|
+
bundle_identifier: bundle_identifier,
|
|
197
|
+
simulator_udid: simulator.fetch("udid"),
|
|
198
|
+
simulator_name: simulator.fetch("name", simulator.fetch("udid"))
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
128
202
|
def build_runtime_command(script_path, gemfile_path:, env:, reload: false)
|
|
129
203
|
entry_script = script_path
|
|
130
204
|
if reload
|
|
@@ -287,9 +361,20 @@ module Ruflet
|
|
|
287
361
|
end
|
|
288
362
|
end
|
|
289
363
|
|
|
290
|
-
def launch_target_client(target, port)
|
|
364
|
+
def launch_target_client(target, port, experimental_client: nil)
|
|
291
365
|
wait_for_server_boot(port)
|
|
292
366
|
|
|
367
|
+
if experimental_client
|
|
368
|
+
if experimental_client[:kind] == :desktop
|
|
369
|
+
return launch_desktop_client(
|
|
370
|
+
"http://localhost:#{port}",
|
|
371
|
+
root: experimental_client.fetch(:root), experimental: true)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return launch_experimental_mobile_client(
|
|
375
|
+
"http://#{best_lan_host}:#{port}", client: experimental_client)
|
|
376
|
+
end
|
|
377
|
+
|
|
293
378
|
case target
|
|
294
379
|
when "web"
|
|
295
380
|
launch_web_client(port)
|
|
@@ -300,6 +385,33 @@ module Ruflet
|
|
|
300
385
|
end
|
|
301
386
|
end
|
|
302
387
|
|
|
388
|
+
def launch_experimental_mobile_client(url, client:)
|
|
389
|
+
udid = client.fetch(:simulator_udid)
|
|
390
|
+
app_bundle = client.fetch(:app_bundle)
|
|
391
|
+
bundle_identifier = client.fetch(:bundle_identifier)
|
|
392
|
+
|
|
393
|
+
unless system("xcrun", "simctl", "install", udid, app_bundle)
|
|
394
|
+
warn "Failed to install the experimental client on #{client[:simulator_name] || udid}."
|
|
395
|
+
return []
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
launched = system(
|
|
399
|
+
{ "SIMCTL_CHILD_RUFLET_URL" => url },
|
|
400
|
+
"xcrun", "simctl", "launch", "--terminate-running-process", udid, bundle_identifier
|
|
401
|
+
)
|
|
402
|
+
unless launched
|
|
403
|
+
warn "Failed to launch the experimental client on #{client[:simulator_name] || udid}."
|
|
404
|
+
return []
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
puts "Ruflet experimental client: #{client[:simulator_name] || udid}"
|
|
408
|
+
puts "Ruflet experimental backend: #{url}"
|
|
409
|
+
[]
|
|
410
|
+
rescue StandardError => e
|
|
411
|
+
warn "Failed to launch experimental client: #{e.class}: #{e.message}"
|
|
412
|
+
[]
|
|
413
|
+
end
|
|
414
|
+
|
|
303
415
|
# The backend serves the web client on its own port, so the client reads
|
|
304
416
|
# the origin it was loaded from and opens its websocket there. No URL is
|
|
305
417
|
# passed in the query string: nothing in the client parses one.
|
|
@@ -399,8 +511,8 @@ module Ruflet
|
|
|
399
511
|
nil
|
|
400
512
|
end
|
|
401
513
|
|
|
402
|
-
def launch_desktop_client(url)
|
|
403
|
-
cmd = detect_desktop_client_command(url)
|
|
514
|
+
def launch_desktop_client(url, root: nil, experimental: false)
|
|
515
|
+
cmd = detect_desktop_client_command(url, root: root, experimental: experimental)
|
|
404
516
|
unless cmd
|
|
405
517
|
warn "Desktop client executable not found."
|
|
406
518
|
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
@@ -456,18 +568,20 @@ module Ruflet
|
|
|
456
568
|
.find { |path| executable_file?(path) }
|
|
457
569
|
end
|
|
458
570
|
|
|
459
|
-
def detect_desktop_client_command(url)
|
|
460
|
-
root
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
571
|
+
def detect_desktop_client_command(url, root: nil, experimental: false)
|
|
572
|
+
unless root
|
|
573
|
+
root = ENV["RUFLET_CLIENT_DIR"]
|
|
574
|
+
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
575
|
+
root = nil unless Dir.exist?(root)
|
|
576
|
+
root ||= ensure_prebuilt_client(desktop: true)
|
|
577
|
+
end
|
|
464
578
|
return nil unless root && Dir.exist?(root)
|
|
465
579
|
|
|
466
580
|
host_os = RbConfig::CONFIG["host_os"]
|
|
467
581
|
client_build_roots(root).each do |base|
|
|
468
582
|
if host_os.match?(/darwin/i)
|
|
469
583
|
search = %w[Release Debug].map { |config| File.join(base, "build", "macos", "Build", "Products", config) }
|
|
470
|
-
search << File.join(base, "desktop")
|
|
584
|
+
search << File.join(base, experimental ? "desktop-experimental" : "desktop")
|
|
471
585
|
search.each do |dir|
|
|
472
586
|
next unless Dir.exist?(dir)
|
|
473
587
|
|
|
@@ -522,7 +636,10 @@ module Ruflet
|
|
|
522
636
|
nil
|
|
523
637
|
end
|
|
524
638
|
|
|
525
|
-
def ensure_prebuilt_client(
|
|
639
|
+
def ensure_prebuilt_client(
|
|
640
|
+
web: false, desktop: false, desktop_experimental: false,
|
|
641
|
+
ios_experimental: false, platform: nil, force: false
|
|
642
|
+
)
|
|
526
643
|
platform ||= host_platform_name
|
|
527
644
|
return nil if platform.nil?
|
|
528
645
|
|
|
@@ -530,13 +647,38 @@ module Ruflet
|
|
|
530
647
|
FileUtils.mkdir_p(cache_root)
|
|
531
648
|
|
|
532
649
|
wanted_assets = []
|
|
533
|
-
wanted_assets << { kind: :web, name: "
|
|
650
|
+
wanted_assets << { kind: :web, name: "#{ASSET_PREFIX}-web.tar.gz" } if web
|
|
534
651
|
if desktop
|
|
535
652
|
desktop_asset = desktop_asset_name_for(platform)
|
|
536
653
|
return nil if desktop_asset.nil?
|
|
537
654
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
538
655
|
end
|
|
539
|
-
|
|
656
|
+
if desktop_experimental
|
|
657
|
+
return nil unless platform == "macos"
|
|
658
|
+
|
|
659
|
+
wanted_assets << {
|
|
660
|
+
kind: :desktop_experimental,
|
|
661
|
+
name: EXPERIMENTAL_MACOS_ASSET,
|
|
662
|
+
platform: platform
|
|
663
|
+
}
|
|
664
|
+
end
|
|
665
|
+
if ios_experimental
|
|
666
|
+
return nil unless platform == "macos"
|
|
667
|
+
|
|
668
|
+
wanted_assets << {
|
|
669
|
+
kind: :ios_experimental,
|
|
670
|
+
name: EXPERIMENTAL_IOS_SIMULATOR_ASSET,
|
|
671
|
+
platform: platform
|
|
672
|
+
}
|
|
673
|
+
end
|
|
674
|
+
cache_ready = wanted_assets.empty? || prebuilt_assets_present?(
|
|
675
|
+
cache_root,
|
|
676
|
+
web: web,
|
|
677
|
+
desktop: desktop,
|
|
678
|
+
desktop_experimental: desktop_experimental,
|
|
679
|
+
ios_experimental: ios_experimental,
|
|
680
|
+
platform: platform
|
|
681
|
+
)
|
|
540
682
|
release = nil
|
|
541
683
|
if !force && cache_ready
|
|
542
684
|
ensure_client_manifest(cache_root, platform: platform)
|
|
@@ -573,7 +715,12 @@ module Ruflet
|
|
|
573
715
|
puts "Downloading prebuilt client asset: #{resolved_name}"
|
|
574
716
|
archive_path = File.join(tmpdir, resolved_name)
|
|
575
717
|
download_file(asset.fetch("browser_download_url"), archive_path)
|
|
576
|
-
subdir = wanted[:kind]
|
|
718
|
+
subdir = case wanted[:kind]
|
|
719
|
+
when :web then "web"
|
|
720
|
+
when :desktop then "desktop"
|
|
721
|
+
when :desktop_experimental then "desktop-experimental"
|
|
722
|
+
when :ios_experimental then "ios-experimental"
|
|
723
|
+
end
|
|
577
724
|
target = File.join(cache_root, subdir)
|
|
578
725
|
FileUtils.rm_rf(target) if force && Dir.exist?(target)
|
|
579
726
|
FileUtils.mkdir_p(target)
|
|
@@ -591,7 +738,14 @@ module Ruflet
|
|
|
591
738
|
end
|
|
592
739
|
end
|
|
593
740
|
|
|
594
|
-
if prebuilt_assets_present?(
|
|
741
|
+
if prebuilt_assets_present?(
|
|
742
|
+
cache_root,
|
|
743
|
+
web: web,
|
|
744
|
+
desktop: desktop,
|
|
745
|
+
desktop_experimental: desktop_experimental,
|
|
746
|
+
ios_experimental: ios_experimental,
|
|
747
|
+
platform: platform
|
|
748
|
+
)
|
|
595
749
|
write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
|
|
596
750
|
return cache_root
|
|
597
751
|
end
|
|
@@ -607,10 +761,61 @@ module Ruflet
|
|
|
607
761
|
# will actually accept later, or a half-extracted cache reports success
|
|
608
762
|
# here and then fails to launch -- and, because a present cache short
|
|
609
763
|
# circuits the download, never repairs itself without --force.
|
|
610
|
-
def prebuilt_assets_present?(
|
|
764
|
+
def prebuilt_assets_present?(
|
|
765
|
+
root, web:, desktop:, desktop_experimental: false,
|
|
766
|
+
ios_experimental: false, platform: nil
|
|
767
|
+
)
|
|
611
768
|
ok_web = !web || built_web_client_dir?(File.join(root, "web"))
|
|
612
769
|
ok_desktop = !desktop || prebuilt_desktop_present?(root, platform: platform)
|
|
613
|
-
|
|
770
|
+
ok_desktop_experimental = !desktop_experimental ||
|
|
771
|
+
prebuilt_experimental_desktop_present?(root, platform: platform)
|
|
772
|
+
ok_ios_experimental = !ios_experimental || !experimental_ios_app_bundle(root).nil?
|
|
773
|
+
ok_web && ok_desktop && ok_desktop_experimental && ok_ios_experimental
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def experimental_ios_app_bundle(root)
|
|
777
|
+
ios_root = File.join(root, "ios-experimental")
|
|
778
|
+
return nil unless Dir.exist?(ios_root)
|
|
779
|
+
|
|
780
|
+
Dir.glob(File.join(ios_root, "*.app")).sort.find do |app_bundle|
|
|
781
|
+
File.file?(File.join(app_bundle, "Info.plist")) && ios_app_executable(app_bundle)
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def ios_app_executable(app_bundle)
|
|
786
|
+
return nil unless Dir.exist?(app_bundle)
|
|
787
|
+
|
|
788
|
+
Dir.children(app_bundle)
|
|
789
|
+
.map { |entry| File.join(app_bundle, entry) }
|
|
790
|
+
.find { |path| executable_file?(path) }
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def booted_ios_simulator
|
|
794
|
+
output, status = Open3.capture2e("xcrun", "simctl", "list", "devices", "booted", "--json")
|
|
795
|
+
return nil unless status.success?
|
|
796
|
+
|
|
797
|
+
devices = JSON.parse(output).fetch("devices", {})
|
|
798
|
+
devices.each do |runtime, candidates|
|
|
799
|
+
next unless runtime.to_s.include?("SimRuntime.iOS")
|
|
800
|
+
|
|
801
|
+
match = Array(candidates).find do |device|
|
|
802
|
+
device["state"] == "Booted" && device.fetch("isAvailable", true)
|
|
803
|
+
end
|
|
804
|
+
return match if match
|
|
805
|
+
end
|
|
806
|
+
nil
|
|
807
|
+
rescue StandardError
|
|
808
|
+
nil
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def ios_bundle_identifier(app_bundle)
|
|
812
|
+
plist = File.join(app_bundle, "Info.plist")
|
|
813
|
+
output, status = Open3.capture2e(
|
|
814
|
+
"/usr/bin/plutil", "-extract", "CFBundleIdentifier", "raw", "-o", "-", plist
|
|
815
|
+
)
|
|
816
|
+
status.success? ? output.strip : nil
|
|
817
|
+
rescue StandardError
|
|
818
|
+
nil
|
|
614
819
|
end
|
|
615
820
|
|
|
616
821
|
# The prebuilt client is whatever `ruflet build` produced from Ruflet
|
|
@@ -636,6 +841,17 @@ module Ruflet
|
|
|
636
841
|
end
|
|
637
842
|
end
|
|
638
843
|
|
|
844
|
+
def prebuilt_experimental_desktop_present?(root, platform: nil)
|
|
845
|
+
return false unless (platform || host_platform_name) == "macos"
|
|
846
|
+
|
|
847
|
+
desktop = File.join(root, "desktop-experimental")
|
|
848
|
+
return false unless Dir.exist?(desktop)
|
|
849
|
+
|
|
850
|
+
Dir.glob(File.join(desktop, "*.app")).any? do |app_bundle|
|
|
851
|
+
macos_app_executable(app_bundle)
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
639
855
|
def host_platform_name
|
|
640
856
|
host_os = RbConfig::CONFIG["host_os"]
|
|
641
857
|
return "macos" if host_os.match?(/darwin/i)
|
|
@@ -647,9 +863,9 @@ module Ruflet
|
|
|
647
863
|
|
|
648
864
|
def desktop_asset_name_for(platform)
|
|
649
865
|
case platform
|
|
650
|
-
when "macos" then "
|
|
651
|
-
when "linux" then "
|
|
652
|
-
when "windows" then "
|
|
866
|
+
when "macos" then "#{ASSET_PREFIX}-macos-universal.zip"
|
|
867
|
+
when "linux" then "#{ASSET_PREFIX}-linux-x64.tar.gz"
|
|
868
|
+
when "windows" then "#{ASSET_PREFIX}-windows-x64.zip"
|
|
653
869
|
end
|
|
654
870
|
end
|
|
655
871
|
|
|
@@ -659,7 +875,7 @@ module Ruflet
|
|
|
659
875
|
|
|
660
876
|
def fetch_release_for_version(wanted_assets: [])
|
|
661
877
|
releases = []
|
|
662
|
-
channel = client_release_channel
|
|
878
|
+
channel = client_release_channel(wanted_assets: wanted_assets)
|
|
663
879
|
if channel != "stable"
|
|
664
880
|
rolling = release_by_tag(channel)
|
|
665
881
|
releases << rolling if rolling && rolling_release_complete?(rolling)
|
|
@@ -670,13 +886,24 @@ module Ruflet
|
|
|
670
886
|
releases.compact.find { |release| release_has_wanted_assets?(release, wanted_assets) }
|
|
671
887
|
end
|
|
672
888
|
|
|
673
|
-
def client_release_channel
|
|
674
|
-
|
|
675
|
-
|
|
889
|
+
def client_release_channel(wanted_assets: [])
|
|
890
|
+
default_channel = wanted_assets.any? do |wanted|
|
|
891
|
+
%i[desktop_experimental ios_experimental].include?(wanted[:kind])
|
|
892
|
+
end ? "prebuild-experimental" : "prebuild-main"
|
|
893
|
+
value = ENV.fetch("RUFLET_CLIENT_CHANNEL", default_channel).to_s.strip
|
|
894
|
+
value.empty? ? default_channel : value
|
|
676
895
|
end
|
|
677
896
|
|
|
678
897
|
def rolling_release_complete?(release)
|
|
679
|
-
release
|
|
898
|
+
!channel_manifest_asset(release).nil?
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# The manifest is written last, so its presence means the run finished
|
|
902
|
+
# uploading every platform. Accept either naming for releases published
|
|
903
|
+
# before the assets were renamed.
|
|
904
|
+
def channel_manifest_asset(release)
|
|
905
|
+
names = [CLIENT_CHANNEL_MANIFEST, LEGACY_CLIENT_CHANNEL_MANIFEST]
|
|
906
|
+
release.fetch("assets", []).find { |asset| names.include?(asset["name"]) }
|
|
680
907
|
end
|
|
681
908
|
|
|
682
909
|
def release_has_wanted_assets?(release, wanted_assets)
|
|
@@ -687,8 +914,7 @@ module Ruflet
|
|
|
687
914
|
end
|
|
688
915
|
|
|
689
916
|
def client_release_revision(release)
|
|
690
|
-
|
|
691
|
-
source = marker || release
|
|
917
|
+
source = channel_manifest_asset(release) || release
|
|
692
918
|
[source["id"], source["updated_at"] || source["published_at"], source["size"]].compact.join(":")
|
|
693
919
|
end
|
|
694
920
|
|
|
@@ -750,15 +976,23 @@ module Ruflet
|
|
|
750
976
|
|
|
751
977
|
def release_asset_matches?(name, kind, platform)
|
|
752
978
|
n = name.to_s.downcase
|
|
753
|
-
return false unless n.include?(
|
|
979
|
+
return false unless n.include?(ASSET_PREFIX) || n.include?(LEGACY_ASSET_PREFIX)
|
|
754
980
|
|
|
755
981
|
if kind == :web
|
|
756
982
|
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
757
983
|
end
|
|
758
984
|
|
|
985
|
+
if kind == :ios_experimental
|
|
986
|
+
return n.include?("ios") && n.include?("experimental") && n.include?("simulator") && n.end_with?(".zip")
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
if kind == :desktop_experimental
|
|
990
|
+
return n.include?("macos") && n.include?("experimental") && n.end_with?(".zip")
|
|
991
|
+
end
|
|
992
|
+
|
|
759
993
|
case platform
|
|
760
994
|
when "macos"
|
|
761
|
-
n.include?("macos") && n.end_with?(".zip")
|
|
995
|
+
n.include?("macos") && !n.include?("experimental") && n.end_with?(".zip")
|
|
762
996
|
when "linux"
|
|
763
997
|
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
764
998
|
when "windows"
|
|
@@ -838,6 +1072,12 @@ module Ruflet
|
|
|
838
1072
|
if prebuilt_desktop_present?(root, platform: platform)
|
|
839
1073
|
assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
|
|
840
1074
|
end
|
|
1075
|
+
if prebuilt_experimental_desktop_present?(root, platform: platform)
|
|
1076
|
+
assets << { "kind" => "desktop_experimental", "platform" => platform, "asset_name" => nil }
|
|
1077
|
+
end
|
|
1078
|
+
if experimental_ios_app_bundle(root)
|
|
1079
|
+
assets << { "kind" => "ios_experimental", "platform" => platform, "asset_name" => nil }
|
|
1080
|
+
end
|
|
841
1081
|
return if assets.empty?
|
|
842
1082
|
|
|
843
1083
|
write_client_manifest(root, platform: platform, release: nil, assets: assets)
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -66,14 +66,19 @@ module Ruflet
|
|
|
66
66
|
ruflet --version
|
|
67
67
|
ruflet create <appname>
|
|
68
68
|
ruflet new <appname>
|
|
69
|
-
ruflet run [scriptname|path] [--web|--desktop] [--port PORT] [--no-reload]
|
|
69
|
+
ruflet run [scriptname|path] [--web|--desktop] [--experimental|--exp] [--port PORT] [--no-reload]
|
|
70
70
|
ruflet update [web|desktop|all] [--check] [--force] [--platform PLATFORM]
|
|
71
71
|
ruflet debug [scriptname|path]
|
|
72
|
-
ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self] [--verbose]
|
|
72
|
+
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self] [--experimental|--exp] [--verbose]
|
|
73
73
|
ruflet install [--device DEVICE_ID] [--verbose]
|
|
74
74
|
ruflet devices
|
|
75
75
|
ruflet emulators
|
|
76
76
|
ruflet doctor [--fix] [--verbose]
|
|
77
|
+
|
|
78
|
+
Build profiles:
|
|
79
|
+
--self Self-contained lite build (default profile)
|
|
80
|
+
--self --full Self-contained full CRuby build with locked project gems
|
|
81
|
+
--full Also implies --self
|
|
77
82
|
HELP
|
|
78
83
|
end
|
|
79
84
|
|