ruflet 0.0.21 → 0.0.23

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.
@@ -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
- "audio" => { package: "flet_audio", alias: "ruflet_audio" },
17
- "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
18
- "camera" => { package: "flet_camera", alias: "ruflet_camera" },
19
- "charts" => { package: "flet_charts", alias: "ruflet_charts" },
20
- "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
21
- "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
22
- "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
23
- "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
24
- "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
25
- "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
26
- "map" => { package: "flet_map", alias: "ruflet_map" },
27
- "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
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: "flet_rive", alias: "ruflet_rive" },
30
- "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
31
- "video" => { package: "flet_video", alias: "ruflet_video" },
32
- "webview" => { package: "flet_webview", alias: "ruflet_webview" }
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)
@@ -72,7 +74,16 @@ module Ruflet
72
74
  target = hidden_flutter_client_dir(root)
73
75
  FileUtils.mkdir_p(File.dirname(target))
74
76
  FileUtils.rm_rf(target)
75
- FileUtils.cp_r(template_root, target)
77
+ # `target` is the Flutter project root, not a directory that contains
78
+ # the template root. Copying a source directory onto a target that was
79
+ # already created by FileUtils can produce
80
+ # `build/client/ruflet_flutter_template/...`, leaving `build/client`
81
+ # without pubspec.yaml and making Flutter reject it as a project.
82
+ FileUtils.mkdir_p(target)
83
+ Dir.children(template_root).each do |entry|
84
+ copy_client_template_entry(
85
+ template_root, target, entry)
86
+ end
76
87
  prune_client_template(target)
77
88
  write_client_template_revision(target, installed_template_revision(template_root))
78
89
  end
@@ -261,6 +272,43 @@ module Ruflet
261
272
  end
262
273
  end
263
274
 
275
+ def copy_client_template_entry(template_root, target, relative)
276
+ return if generated_client_template_path?(relative)
277
+
278
+ source = File.join(template_root, relative)
279
+ destination = File.join(target, relative)
280
+ stat = File.lstat(source)
281
+ if stat.symlink?
282
+ FileUtils.mkdir_p(File.dirname(destination))
283
+ FileUtils.ln_s(File.readlink(source), destination)
284
+ elsif stat.directory?
285
+ FileUtils.mkdir_p(destination)
286
+ Dir.children(source).each do |entry|
287
+ copy_client_template_entry(
288
+ template_root, target, File.join(relative, entry))
289
+ end
290
+ else
291
+ FileUtils.mkdir_p(File.dirname(destination))
292
+ FileUtils.cp(source, destination, preserve: true)
293
+ end
294
+ end
295
+
296
+ def generated_client_template_path?(relative)
297
+ components = relative.split(File::SEPARATOR)
298
+ generated_components = %w[
299
+ .build .cache .claude .dart_tool .git .idea .swiftpm .symlinks
300
+ build DerivedData Pods xcuserdata
301
+ ]
302
+ return true if components.any? { |component| generated_components.include?(component) }
303
+ return true if components.any? { |component| component.start_with?(".build-") }
304
+
305
+ generated_files = %w[
306
+ .DS_Store Package.resolved Podfile.lock local.properties
307
+ pubspec_overrides.yaml
308
+ ]
309
+ generated_files.include?(components.last) || components.last.end_with?(".xcuserstate")
310
+ end
311
+
264
312
  def write_default_ruflet_config(root, app_name)
265
313
  File.write(File.join(root, "ruflet.yaml"), <<~YAML)
266
314
  app:
@@ -11,23 +11,23 @@ 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"
20
27
  DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
21
28
 
22
29
  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)
30
+ options = parse_run_options(args)
31
31
 
32
32
  script_token = args.shift || "main"
33
33
  script_path = resolve_script(script_token)
@@ -87,7 +87,7 @@ module Ruflet
87
87
  return status.success? ? 0 : (status.exitstatus || 1) unless run_state[:restart]
88
88
 
89
89
  # Full restart requested ("R"): respawn the backend; connected
90
- # clients reconnect and re-register on their own (Flet-style).
90
+ # clients reconnect and re-register on their own (Ruflet-style).
91
91
  run_state[:restart] = false
92
92
  puts "Restarting app..."
93
93
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -125,6 +125,18 @@ module Ruflet
125
125
 
126
126
  private
127
127
 
128
+ def parse_run_options(args)
129
+ options = { target: "mobile", requested_port: 8550, reload: true }
130
+ parser = OptionParser.new do |o|
131
+ o.on("--web") { options[:target] = "web" }
132
+ o.on("--desktop") { options[:target] = "desktop" }
133
+ o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
134
+ o.on("--no-reload") { options[:reload] = false }
135
+ end
136
+ parser.parse!(args)
137
+ options
138
+ end
139
+
128
140
  def build_runtime_command(script_path, gemfile_path:, env:, reload: false)
129
141
  entry_script = script_path
130
142
  if reload
@@ -399,8 +411,8 @@ module Ruflet
399
411
  nil
400
412
  end
401
413
 
402
- def launch_desktop_client(url)
403
- cmd = detect_desktop_client_command(url)
414
+ def launch_desktop_client(url, root: nil)
415
+ cmd = detect_desktop_client_command(url, root: root)
404
416
  unless cmd
405
417
  warn "Desktop client executable not found."
406
418
  warn "Set RUFLET_CLIENT_DIR to your client path."
@@ -456,11 +468,13 @@ module Ruflet
456
468
  .find { |path| executable_file?(path) }
457
469
  end
458
470
 
459
- def detect_desktop_client_command(url)
460
- root = ENV["RUFLET_CLIENT_DIR"]
461
- root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
462
- root = nil unless Dir.exist?(root)
463
- root ||= ensure_prebuilt_client(desktop: true)
471
+ def detect_desktop_client_command(url, root: nil)
472
+ unless root
473
+ root = ENV["RUFLET_CLIENT_DIR"]
474
+ root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
475
+ root = nil unless Dir.exist?(root)
476
+ root ||= ensure_prebuilt_client(desktop: true)
477
+ end
464
478
  return nil unless root && Dir.exist?(root)
465
479
 
466
480
  host_os = RbConfig::CONFIG["host_os"]
@@ -530,13 +544,15 @@ module Ruflet
530
544
  FileUtils.mkdir_p(cache_root)
531
545
 
532
546
  wanted_assets = []
533
- wanted_assets << { kind: :web, name: "ruflet_client-web.tar.gz" } if web
547
+ wanted_assets << { kind: :web, name: "#{ASSET_PREFIX}-web.tar.gz" } if web
534
548
  if desktop
535
549
  desktop_asset = desktop_asset_name_for(platform)
536
550
  return nil if desktop_asset.nil?
537
551
  wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
538
552
  end
539
- cache_ready = wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
553
+ cache_ready = wanted_assets.empty? || prebuilt_assets_present?(
554
+ cache_root, web: web, desktop: desktop, platform: platform
555
+ )
540
556
  release = nil
541
557
  if !force && cache_ready
542
558
  ensure_client_manifest(cache_root, platform: platform)
@@ -573,7 +589,10 @@ module Ruflet
573
589
  puts "Downloading prebuilt client asset: #{resolved_name}"
574
590
  archive_path = File.join(tmpdir, resolved_name)
575
591
  download_file(asset.fetch("browser_download_url"), archive_path)
576
- subdir = wanted[:kind] == :web ? "web" : "desktop"
592
+ subdir = case wanted[:kind]
593
+ when :web then "web"
594
+ when :desktop then "desktop"
595
+ end
577
596
  target = File.join(cache_root, subdir)
578
597
  FileUtils.rm_rf(target) if force && Dir.exist?(target)
579
598
  FileUtils.mkdir_p(target)
@@ -591,7 +610,9 @@ module Ruflet
591
610
  end
592
611
  end
593
612
 
594
- if prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
613
+ if prebuilt_assets_present?(
614
+ cache_root, web: web, desktop: desktop, platform: platform
615
+ )
595
616
  write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
596
617
  return cache_root
597
618
  end
@@ -647,9 +668,9 @@ module Ruflet
647
668
 
648
669
  def desktop_asset_name_for(platform)
649
670
  case platform
650
- when "macos" then "ruflet_client-macos-universal.zip"
651
- when "linux" then "ruflet_client-linux-x64.tar.gz"
652
- when "windows" then "ruflet_client-windows-x64.zip"
671
+ when "macos" then "#{ASSET_PREFIX}-macos-universal.zip"
672
+ when "linux" then "#{ASSET_PREFIX}-linux-x64.tar.gz"
673
+ when "windows" then "#{ASSET_PREFIX}-windows-x64.zip"
653
674
  end
654
675
  end
655
676
 
@@ -659,7 +680,7 @@ module Ruflet
659
680
 
660
681
  def fetch_release_for_version(wanted_assets: [])
661
682
  releases = []
662
- channel = client_release_channel
683
+ channel = client_release_channel(wanted_assets: wanted_assets)
663
684
  if channel != "stable"
664
685
  rolling = release_by_tag(channel)
665
686
  releases << rolling if rolling && rolling_release_complete?(rolling)
@@ -670,13 +691,22 @@ module Ruflet
670
691
  releases.compact.find { |release| release_has_wanted_assets?(release, wanted_assets) }
671
692
  end
672
693
 
673
- def client_release_channel
674
- value = ENV.fetch("RUFLET_CLIENT_CHANNEL", "prebuild-main").to_s.strip
675
- value.empty? ? "prebuild-main" : value
694
+ def client_release_channel(wanted_assets: [])
695
+ default_channel = "prebuild-main"
696
+ value = ENV.fetch("RUFLET_CLIENT_CHANNEL", default_channel).to_s.strip
697
+ value.empty? ? default_channel : value
676
698
  end
677
699
 
678
700
  def rolling_release_complete?(release)
679
- release.fetch("assets", []).any? { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
701
+ !channel_manifest_asset(release).nil?
702
+ end
703
+
704
+ # The manifest is written last, so its presence means the run finished
705
+ # uploading every platform. Accept either naming for releases published
706
+ # before the assets were renamed.
707
+ def channel_manifest_asset(release)
708
+ names = [CLIENT_CHANNEL_MANIFEST, LEGACY_CLIENT_CHANNEL_MANIFEST]
709
+ release.fetch("assets", []).find { |asset| names.include?(asset["name"]) }
680
710
  end
681
711
 
682
712
  def release_has_wanted_assets?(release, wanted_assets)
@@ -687,8 +717,7 @@ module Ruflet
687
717
  end
688
718
 
689
719
  def client_release_revision(release)
690
- marker = release.fetch("assets", []).find { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
691
- source = marker || release
720
+ source = channel_manifest_asset(release) || release
692
721
  [source["id"], source["updated_at"] || source["published_at"], source["size"]].compact.join(":")
693
722
  end
694
723
 
@@ -750,7 +779,7 @@ module Ruflet
750
779
 
751
780
  def release_asset_matches?(name, kind, platform)
752
781
  n = name.to_s.downcase
753
- return false unless n.include?("ruflet_client")
782
+ return false unless n.include?(ASSET_PREFIX) || n.include?(LEGACY_ASSET_PREFIX)
754
783
 
755
784
  if kind == :web
756
785
  return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
@@ -758,7 +787,10 @@ module Ruflet
758
787
 
759
788
  case platform
760
789
  when "macos"
761
- n.include?("macos") && n.end_with?(".zip")
790
+ # Releases cut before the experimental channel was retired still carry
791
+ # ruflet_explorer-macos-experimental-universal.zip. Installing that as
792
+ # the desktop client would hand back the wrong app, so skip it.
793
+ n.include?("macos") && !n.include?("experimental") && n.end_with?(".zip")
762
794
  when "linux"
763
795
  n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
764
796
  when "windows"
data/lib/ruflet/cli.rb CHANGED
@@ -69,11 +69,16 @@ module Ruflet
69
69
  ruflet run [scriptname|path] [--web|--desktop] [--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] [--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.21" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.23" 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.21
4
+ version: 0.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa