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.
@@ -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
- CLIENT_CHANNEL_MANIFEST = "ruflet_client-manifest.json"
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 = { target: "mobile", requested_port: 8550, reload: true }
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(options[:target], selected_port)
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 (Flet-style).
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
- # The backend serves the web client on its own port, so the client loads
304
- # and opens its websocket on the same origin. The explicit url parameter
305
- # is kept because prebuilt production clients have no default URL and use
306
- # it to select the websocket transport.
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}/?#{URI.encode_www_form(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 = ENV["RUFLET_CLIENT_DIR"]
462
- root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
463
- root = nil unless Dir.exist?(root)
464
- root ||= ensure_prebuilt_client(desktop: true)
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(web: false, desktop: false, platform: nil, force: false)
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: "ruflet_client-web.tar.gz" } if web
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
- cache_ready = wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
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] == :web ? "web" : "desktop"
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?(cache_root, web: web, desktop: desktop, platform: platform)
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
- def prebuilt_assets_present?(root, web:, desktop:, platform: nil)
607
- ok_web = !web || File.file?(File.join(root, "web", "index.html"))
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
- ok_web && ok_desktop
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
- File.file?(File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client"))
834
+ Dir.glob(File.join(desktop, "*.app")).any? { |app_bundle| macos_app_executable(app_bundle) }
619
835
  when "linux"
620
- File.file?(File.join(root, "desktop", "ruflet_client"))
836
+ Dir.children(desktop).any? { |entry| executable_file?(File.join(desktop, entry)) }
621
837
  when "windows"
622
- File.file?(File.join(root, "desktop", "ruflet_client.exe"))
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 "ruflet_client-macos-universal.zip"
640
- when "linux" then "ruflet_client-linux-x64.tar.gz"
641
- when "windows" then "ruflet_client-windows-x64.zip"
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
- value = ENV.fetch("RUFLET_CLIENT_CHANNEL", "prebuild-main").to_s.strip
664
- value.empty? ? "prebuild-main" : value
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.fetch("assets", []).any? { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
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
- marker = release.fetch("assets", []).find { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
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?("ruflet_client")
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 File.file?(File.join(root, "web", "index.html"))
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
- if target == :web
113
- File.file?(File.join(root, "web", "index.html"))
114
- else
115
- prebuilt_desktop_present?(root, platform: platform)
116
- end
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
 
@@ -111,7 +111,29 @@ module Ruflet
111
111
  end
112
112
 
113
113
  def watched_files
114
- files = Dir.glob(File.join(@watch_root, "**", "*.rb")).reject { |path| excluded_path?(path) }
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|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.20" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.22" unless const_defined?(:VERSION)
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.20
4
+ version: 0.0.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa