ruflet 0.0.20 → 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 +1689 -86
- data/lib/ruflet/cli/new_command.rb +89 -19
- data/lib/ruflet/cli/run_command.rb +300 -49
- data/lib/ruflet/cli/update_command.rb +6 -6
- 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
|
@@ -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,13 +385,39 @@ module Ruflet
|
|
|
300
385
|
end
|
|
301
386
|
end
|
|
302
387
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
|
|
415
|
+
# The backend serves the web client on its own port, so the client reads
|
|
416
|
+
# the origin it was loaded from and opens its websocket there. No URL is
|
|
417
|
+
# passed in the query string: nothing in the client parses one.
|
|
307
418
|
def launch_web_client(port)
|
|
308
419
|
backend_url = "http://localhost:#{port}"
|
|
309
|
-
web_url = "#{backend_url}
|
|
420
|
+
web_url = "#{backend_url}/"
|
|
310
421
|
browser_pid = open_in_browser_app_mode(web_url)
|
|
311
422
|
open_in_browser(web_url) if browser_pid.nil?
|
|
312
423
|
puts "Ruflet web client: #{web_url}"
|
|
@@ -400,8 +511,8 @@ module Ruflet
|
|
|
400
511
|
nil
|
|
401
512
|
end
|
|
402
513
|
|
|
403
|
-
def launch_desktop_client(url)
|
|
404
|
-
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)
|
|
405
516
|
unless cmd
|
|
406
517
|
warn "Desktop client executable not found."
|
|
407
518
|
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
@@ -457,18 +568,20 @@ module Ruflet
|
|
|
457
568
|
.find { |path| executable_file?(path) }
|
|
458
569
|
end
|
|
459
570
|
|
|
460
|
-
def detect_desktop_client_command(url)
|
|
461
|
-
root
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
465
578
|
return nil unless root && Dir.exist?(root)
|
|
466
579
|
|
|
467
580
|
host_os = RbConfig::CONFIG["host_os"]
|
|
468
581
|
client_build_roots(root).each do |base|
|
|
469
582
|
if host_os.match?(/darwin/i)
|
|
470
583
|
search = %w[Release Debug].map { |config| File.join(base, "build", "macos", "Build", "Products", config) }
|
|
471
|
-
search << File.join(base, "desktop")
|
|
584
|
+
search << File.join(base, experimental ? "desktop-experimental" : "desktop")
|
|
472
585
|
search.each do |dir|
|
|
473
586
|
next unless Dir.exist?(dir)
|
|
474
587
|
|
|
@@ -523,7 +636,10 @@ module Ruflet
|
|
|
523
636
|
nil
|
|
524
637
|
end
|
|
525
638
|
|
|
526
|
-
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
|
+
)
|
|
527
643
|
platform ||= host_platform_name
|
|
528
644
|
return nil if platform.nil?
|
|
529
645
|
|
|
@@ -531,13 +647,38 @@ module Ruflet
|
|
|
531
647
|
FileUtils.mkdir_p(cache_root)
|
|
532
648
|
|
|
533
649
|
wanted_assets = []
|
|
534
|
-
wanted_assets << { kind: :web, name: "
|
|
650
|
+
wanted_assets << { kind: :web, name: "#{ASSET_PREFIX}-web.tar.gz" } if web
|
|
535
651
|
if desktop
|
|
536
652
|
desktop_asset = desktop_asset_name_for(platform)
|
|
537
653
|
return nil if desktop_asset.nil?
|
|
538
654
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
539
655
|
end
|
|
540
|
-
|
|
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
|
+
)
|
|
541
682
|
release = nil
|
|
542
683
|
if !force && cache_ready
|
|
543
684
|
ensure_client_manifest(cache_root, platform: platform)
|
|
@@ -574,7 +715,12 @@ module Ruflet
|
|
|
574
715
|
puts "Downloading prebuilt client asset: #{resolved_name}"
|
|
575
716
|
archive_path = File.join(tmpdir, resolved_name)
|
|
576
717
|
download_file(asset.fetch("browser_download_url"), archive_path)
|
|
577
|
-
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
|
|
578
724
|
target = File.join(cache_root, subdir)
|
|
579
725
|
FileUtils.rm_rf(target) if force && Dir.exist?(target)
|
|
580
726
|
FileUtils.mkdir_p(target)
|
|
@@ -592,7 +738,14 @@ module Ruflet
|
|
|
592
738
|
end
|
|
593
739
|
end
|
|
594
740
|
|
|
595
|
-
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
|
+
)
|
|
596
749
|
write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
|
|
597
750
|
return cache_root
|
|
598
751
|
end
|
|
@@ -603,28 +756,102 @@ module Ruflet
|
|
|
603
756
|
nil
|
|
604
757
|
end
|
|
605
758
|
|
|
606
|
-
|
|
607
|
-
|
|
759
|
+
# This decides both "the download installed correctly" and "the cache is
|
|
760
|
+
# still usable, skip the network". It has to agree with what `ruflet run`
|
|
761
|
+
# will actually accept later, or a half-extracted cache reports success
|
|
762
|
+
# here and then fails to launch -- and, because a present cache short
|
|
763
|
+
# circuits the download, never repairs itself without --force.
|
|
764
|
+
def prebuilt_assets_present?(
|
|
765
|
+
root, web:, desktop:, desktop_experimental: false,
|
|
766
|
+
ios_experimental: false, platform: nil
|
|
767
|
+
)
|
|
768
|
+
ok_web = !web || built_web_client_dir?(File.join(root, "web"))
|
|
608
769
|
ok_desktop = !desktop || prebuilt_desktop_present?(root, platform: platform)
|
|
609
|
-
|
|
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
|
|
610
774
|
end
|
|
611
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
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# The prebuilt client is whatever `ruflet build` produced from Ruflet
|
|
822
|
+
# Explorer, so its bundle and binary carry that project's app name --
|
|
823
|
+
# "Ruflet Explorer.app", not the old "ruflet_client.app". Match on shape
|
|
824
|
+
# the way detect_desktop_client_command does rather than on a fixed name.
|
|
612
825
|
def prebuilt_desktop_present?(root, platform: nil)
|
|
613
826
|
platform ||= host_platform_name
|
|
614
827
|
return false if platform.nil?
|
|
615
828
|
|
|
829
|
+
desktop = File.join(root, "desktop")
|
|
830
|
+
return false unless Dir.exist?(desktop)
|
|
831
|
+
|
|
616
832
|
case platform
|
|
617
833
|
when "macos"
|
|
618
|
-
|
|
834
|
+
Dir.glob(File.join(desktop, "*.app")).any? { |app_bundle| macos_app_executable(app_bundle) }
|
|
619
835
|
when "linux"
|
|
620
|
-
|
|
836
|
+
Dir.children(desktop).any? { |entry| executable_file?(File.join(desktop, entry)) }
|
|
621
837
|
when "windows"
|
|
622
|
-
|
|
838
|
+
Dir.glob(File.join(desktop, "*.exe")).any? { |path| File.file?(path) }
|
|
623
839
|
else
|
|
624
840
|
false
|
|
625
841
|
end
|
|
626
842
|
end
|
|
627
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
|
+
|
|
628
855
|
def host_platform_name
|
|
629
856
|
host_os = RbConfig::CONFIG["host_os"]
|
|
630
857
|
return "macos" if host_os.match?(/darwin/i)
|
|
@@ -636,9 +863,9 @@ module Ruflet
|
|
|
636
863
|
|
|
637
864
|
def desktop_asset_name_for(platform)
|
|
638
865
|
case platform
|
|
639
|
-
when "macos" then "
|
|
640
|
-
when "linux" then "
|
|
641
|
-
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"
|
|
642
869
|
end
|
|
643
870
|
end
|
|
644
871
|
|
|
@@ -648,7 +875,7 @@ module Ruflet
|
|
|
648
875
|
|
|
649
876
|
def fetch_release_for_version(wanted_assets: [])
|
|
650
877
|
releases = []
|
|
651
|
-
channel = client_release_channel
|
|
878
|
+
channel = client_release_channel(wanted_assets: wanted_assets)
|
|
652
879
|
if channel != "stable"
|
|
653
880
|
rolling = release_by_tag(channel)
|
|
654
881
|
releases << rolling if rolling && rolling_release_complete?(rolling)
|
|
@@ -659,13 +886,24 @@ module Ruflet
|
|
|
659
886
|
releases.compact.find { |release| release_has_wanted_assets?(release, wanted_assets) }
|
|
660
887
|
end
|
|
661
888
|
|
|
662
|
-
def client_release_channel
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
665
895
|
end
|
|
666
896
|
|
|
667
897
|
def rolling_release_complete?(release)
|
|
668
|
-
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"]) }
|
|
669
907
|
end
|
|
670
908
|
|
|
671
909
|
def release_has_wanted_assets?(release, wanted_assets)
|
|
@@ -676,8 +914,7 @@ module Ruflet
|
|
|
676
914
|
end
|
|
677
915
|
|
|
678
916
|
def client_release_revision(release)
|
|
679
|
-
|
|
680
|
-
source = marker || release
|
|
917
|
+
source = channel_manifest_asset(release) || release
|
|
681
918
|
[source["id"], source["updated_at"] || source["published_at"], source["size"]].compact.join(":")
|
|
682
919
|
end
|
|
683
920
|
|
|
@@ -739,15 +976,23 @@ module Ruflet
|
|
|
739
976
|
|
|
740
977
|
def release_asset_matches?(name, kind, platform)
|
|
741
978
|
n = name.to_s.downcase
|
|
742
|
-
return false unless n.include?(
|
|
979
|
+
return false unless n.include?(ASSET_PREFIX) || n.include?(LEGACY_ASSET_PREFIX)
|
|
743
980
|
|
|
744
981
|
if kind == :web
|
|
745
982
|
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
746
983
|
end
|
|
747
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
|
+
|
|
748
993
|
case platform
|
|
749
994
|
when "macos"
|
|
750
|
-
n.include?("macos") && n.end_with?(".zip")
|
|
995
|
+
n.include?("macos") && !n.include?("experimental") && n.end_with?(".zip")
|
|
751
996
|
when "linux"
|
|
752
997
|
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
753
998
|
when "windows"
|
|
@@ -823,10 +1068,16 @@ module Ruflet
|
|
|
823
1068
|
return if read_client_manifest(root)
|
|
824
1069
|
|
|
825
1070
|
assets = []
|
|
826
|
-
assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if
|
|
1071
|
+
assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if built_web_client_dir?(File.join(root, "web"))
|
|
827
1072
|
if prebuilt_desktop_present?(root, platform: platform)
|
|
828
1073
|
assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
|
|
829
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
|
|
830
1081
|
return if assets.empty?
|
|
831
1082
|
|
|
832
1083
|
write_client_manifest(root, platform: platform, release: nil, assets: assets)
|
|
@@ -108,12 +108,12 @@ module Ruflet
|
|
|
108
108
|
|
|
109
109
|
installed = true
|
|
110
110
|
targets.each do |target|
|
|
111
|
-
present =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
present = prebuilt_assets_present?(
|
|
112
|
+
root,
|
|
113
|
+
web: target == :web,
|
|
114
|
+
desktop: target == :desktop,
|
|
115
|
+
platform: platform
|
|
116
|
+
)
|
|
117
117
|
installed &&= present
|
|
118
118
|
puts "#{target}: #{present ? 'installed' : 'missing'}"
|
|
119
119
|
end
|
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
|
|
data/lib/ruflet/hot_reload.rb
CHANGED
|
@@ -111,7 +111,29 @@ module Ruflet
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def watched_files
|
|
114
|
-
files =
|
|
114
|
+
files = []
|
|
115
|
+
directories = [@watch_root]
|
|
116
|
+
until directories.empty?
|
|
117
|
+
directory = directories.pop
|
|
118
|
+
begin
|
|
119
|
+
Dir.each_child(directory) do |name|
|
|
120
|
+
# Prune before descending: generated Flutter/build trees can
|
|
121
|
+
# contain thousands of files and are never reload inputs.
|
|
122
|
+
next if name.start_with?(".") || EXCLUDED_DIRECTORIES.include?(name)
|
|
123
|
+
|
|
124
|
+
path = File.join(directory, name)
|
|
125
|
+
if File.directory?(path)
|
|
126
|
+
directories << path unless File.symlink?(path)
|
|
127
|
+
elsif name.end_with?(".rb") && File.file?(path)
|
|
128
|
+
files << path
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
|
|
132
|
+
# Editors/build tools may remove a directory during a snapshot.
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
files.sort!
|
|
115
137
|
files << @script unless files.include?(@script) || !File.file?(@script)
|
|
116
138
|
files
|
|
117
139
|
end
|
|
@@ -158,15 +180,6 @@ module Ruflet
|
|
|
158
180
|
$LOADED_FEATURES.reject! { |feature| watched.key?(feature) }
|
|
159
181
|
end
|
|
160
182
|
|
|
161
|
-
def excluded_path?(path)
|
|
162
|
-
relative = path.delete_prefix("#{@watch_root}#{File::SEPARATOR}")
|
|
163
|
-
return false if relative == path
|
|
164
|
-
|
|
165
|
-
relative.split(File::SEPARATOR).any? do |component|
|
|
166
|
-
component.start_with?(".") || EXCLUDED_DIRECTORIES.include?(component)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
183
|
def file_snapshot
|
|
171
184
|
snapshot = {}
|
|
172
185
|
watched_files.each do |path|
|
data/lib/ruflet/version.rb
CHANGED