ruflet 0.0.17 → 0.0.18

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,8 +13,6 @@ module Ruflet
13
13
  module CLI
14
14
  module BuildCommand
15
15
  include FlutterSdk
16
- include EnvironmentSetup
17
- include AndroidSdk
18
16
  CLIENT_EXTENSION_MAP = {
19
17
  "ads" => { package: "flet_ads", alias: "ruflet_ads" },
20
18
  "audio" => { package: "flet_audio", alias: "ruflet_audio" },
@@ -31,104 +29,29 @@ module Ruflet
31
29
  "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
32
30
  "rive" => { package: "flet_rive", alias: "ruflet_rive" },
33
31
  "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
34
- "spinkit" => { package: "flet_spinkit", alias: "ruflet_spinkit" },
35
32
  "video" => { package: "flet_video", alias: "ruflet_video" },
36
33
  "webview" => { package: "flet_webview", alias: "ruflet_webview" }
37
34
  }.freeze
38
-
39
- SERVICE_EXTENSION_MAP = {
35
+ PROTECTED_SERVICE_EXTENSIONS = {
40
36
  "camera" => %w[camera permission_handler],
41
37
  "microphone" => %w[audio_recorder permission_handler],
42
38
  "location" => %w[geolocator permission_handler],
43
39
  "motion" => %w[permission_handler]
44
40
  }.freeze
45
-
46
- DEFAULT_SERVICE_NATIVE_REQUIREMENTS = {
47
- "camera" => {
48
- android_permissions: ["android.permission.CAMERA"],
49
- ios_info: {
50
- "NSCameraUsageDescription" => "Camera access is required for camera experiences."
51
- },
52
- macos_info: {
53
- "NSCameraUsageDescription" => "Camera access is required for camera experiences."
54
- },
55
- macos_entitlements: {
56
- "com.apple.security.device.camera" => true
57
- },
58
- ios_permission_definitions: %w[
59
- PERMISSION_CAMERA=1
60
- ]
61
- },
62
- "microphone" => {
63
- android_permissions: ["android.permission.RECORD_AUDIO"],
64
- ios_info: {
65
- "NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
66
- },
67
- macos_info: {
68
- "NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
69
- },
70
- macos_entitlements: {
71
- "com.apple.security.device.audio-input" => true
72
- },
73
- ios_permission_definitions: %w[
74
- PERMISSION_MICROPHONE=1
75
- ]
76
- },
77
- "motion" => {
78
- ios_info: {
79
- "NSMotionUsageDescription" => "Motion access is required for motion and sensor readings."
80
- },
81
- ios_permission_definitions: %w[
82
- PERMISSION_SENSORS=1
83
- ]
84
- },
85
- "location" => {
86
- android_permissions: [
87
- "android.permission.ACCESS_FINE_LOCATION",
88
- "android.permission.ACCESS_COARSE_LOCATION"
89
- ],
90
- ios_info: {
91
- "NSLocationWhenInUseUsageDescription" => "Location access is required for location-aware experiences."
92
- },
93
- macos_info: {
94
- "NSLocationUsageDescription" => "Location access is required for location-aware experiences."
95
- },
96
- macos_entitlements: {
97
- "com.apple.security.personal-information.location" => true
98
- },
99
- ios_permission_definitions: %w[
100
- PERMISSION_LOCATION=1
101
- ]
102
- }
41
+ ANDROID_SERVICE_PERMISSIONS = {
42
+ "camera" => %w[android.permission.CAMERA],
43
+ "microphone" => %w[android.permission.RECORD_AUDIO],
44
+ "location" => %w[android.permission.ACCESS_COARSE_LOCATION android.permission.ACCESS_FINE_LOCATION],
45
+ "motion" => %w[android.permission.ACTIVITY_RECOGNITION android.permission.HIGH_SAMPLING_RATE_SENSORS]
46
+ }.freeze
47
+ IOS_SERVICE_USAGE_KEYS = {
48
+ "camera" => "NSCameraUsageDescription",
49
+ "microphone" => "NSMicrophoneUsageDescription",
50
+ "location" => "NSLocationWhenInUseUsageDescription",
51
+ "motion" => "NSMotionUsageDescription"
103
52
  }.freeze
104
53
 
105
54
  def command_build(args)
106
- with_project_build_lock { run_build_command(args) }
107
- end
108
-
109
- # Concurrent builds share build/client and corrupt each other's state
110
- # (missing app.dill, Xcode build.db I/O errors). flock is released
111
- # automatically when the process exits, so the lock cannot go stale.
112
- def with_project_build_lock
113
- lock_dir = File.join(Dir.pwd, "build")
114
- FileUtils.mkdir_p(lock_dir)
115
- lock_path = File.join(lock_dir, ".ruflet_build.lock")
116
- File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |file|
117
- unless file.flock(File::LOCK_EX | File::LOCK_NB)
118
- owner = file.read.to_s.strip
119
- warn "Another ruflet build is already running for this project#{owner.empty? ? '' : " (#{owner})"}."
120
- warn "Concurrent builds share the same build directory and corrupt each other."
121
- warn "Wait for it to finish, then retry."
122
- return 1
123
- end
124
- file.truncate(0)
125
- file.write("pid=#{Process.pid} started=#{Time.now}")
126
- file.flush
127
- yield
128
- end
129
- end
130
-
131
- def run_build_command(args)
132
55
  self_contained = args.delete("--self")
133
56
  verbose = args.delete("--verbose") || args.delete("-v")
134
57
  platform = (args.shift || "").downcase
@@ -172,9 +95,9 @@ module Ruflet
172
95
  backend_url = configured_backend_url(config)
173
96
  if self_contained
174
97
  build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"] if backend_url
175
- # Pin the embedded entry project so the runtime doesn't have to guess.
176
- # Apps that bundle nested projects (e.g. ruflet_studio's standalone_apps,
177
- # each with their own main.rb) otherwise trip auto-discovery.
98
+ # Pin the embedded project so main.self.dart extracts assets/<name>/
99
+ # deterministically instead of inferring from a single main.rb the
100
+ # app tree now ships many main.rb files (standalone_apps/*/main.rb).
178
101
  build_args += ["--dart-define", "RUFLET_EMBEDDED_PROJECT=#{self_contained_project_name}"]
179
102
  else
180
103
  unless backend_url
@@ -211,16 +134,8 @@ module Ruflet
211
134
  end
212
135
 
213
136
  tools = ensure_flutter!("install", client_dir: client_dir)
214
- discovery_env = unbundled_command_env(tools[:env])
215
- device_id ||= select_install_device(
216
- flutter: tools[:flutter],
217
- env: discovery_env,
218
- client_dir: client_dir
219
- )
220
- return 1 unless device_id
221
-
137
+ command_env = install_tool_env(tools[:env], client_dir)
222
138
  install_platform = install_platform_for_device(device_id)
223
- command_env = install_tool_env(tools[:env], client_dir, platform: install_platform)
224
139
  unless sync_built_outputs_for_install(client_dir, platform: install_platform, verbose: !!verbose)
225
140
  warn "Could not find built app outputs under ./build"
226
141
  warn "Run `ruflet build ...` first, then `ruflet install`."
@@ -258,59 +173,6 @@ module Ruflet
258
173
  nil
259
174
  end
260
175
 
261
- def select_install_device(flutter:, env:, client_dir:, input: $stdin, output: $stdout)
262
- devices = discover_install_devices(flutter: flutter, env: env, client_dir: client_dir)
263
- if devices.empty?
264
- warn "No supported devices are connected."
265
- warn "Run `ruflet devices` to check device availability."
266
- return nil
267
- end
268
-
269
- output.puts "Available devices:"
270
- devices.each_with_index do |device, index|
271
- details = [device["targetPlatform"], device["emulator"] ? "emulator" : "physical"].compact
272
- output.puts " #{index + 1}) #{device.fetch("name", device["id"])} (#{details.join(", ")}) [#{device["id"]}]"
273
- end
274
-
275
- loop do
276
- output.print "Choose a device [1-#{devices.length}]: "
277
- output.flush
278
- choice = input.gets
279
- unless choice
280
- warn "Device selection cancelled. Use `--device DEVICE_ID` for noninteractive installs."
281
- return nil
282
- end
283
-
284
- index = Integer(choice.strip, exception: false)
285
- return devices[index - 1]["id"] if index && index.between?(1, devices.length)
286
-
287
- output.puts "Enter a number from 1 to #{devices.length}."
288
- end
289
- end
290
-
291
- def discover_install_devices(flutter:, env:, client_dir:)
292
- stdout, stderr, status = Open3.capture3(
293
- env,
294
- flutter,
295
- "devices",
296
- "--machine",
297
- chdir: client_dir
298
- )
299
- unless status.success?
300
- warn "Could not list Flutter devices."
301
- warn stderr.strip unless stderr.to_s.strip.empty?
302
- return []
303
- end
304
-
305
- devices = JSON.parse(stdout)
306
- Array(devices).select do |device|
307
- device.is_a?(Hash) && !device["id"].to_s.empty? && device["isSupported"] != false
308
- end
309
- rescue JSON::ParserError => e
310
- warn "Could not parse Flutter device list: #{e.message}"
311
- []
312
- end
313
-
314
176
  def ensure_flutter_client_dir(verbose: false)
315
177
  client_dir = detect_flutter_client_dir
316
178
  return client_dir if client_dir
@@ -321,9 +183,6 @@ module Ruflet
321
183
  end
322
184
 
323
185
  def build_tool_env(env, platform, client_dir = nil)
324
- if %w[android apk aab appbundle].include?(platform)
325
- return android_build_env(unbundled_command_env(env))
326
- end
327
186
  return env unless %w[ios macos].include?(platform)
328
187
 
329
188
  apple_env = unbundled_command_env(env)
@@ -332,9 +191,8 @@ module Ruflet
332
191
  apple_env
333
192
  end
334
193
 
335
- def install_tool_env(env, client_dir, platform: nil)
336
- platform ||= inferred_install_platform
337
- return build_tool_env(env, platform, client_dir) if platform
194
+ def install_tool_env(env, client_dir)
195
+ return build_tool_env(env, inferred_install_platform, client_dir) if inferred_install_platform
338
196
 
339
197
  command_env = unbundled_command_env(env)
340
198
  command_env["PATH"] = apple_build_path(command_env["PATH"])
@@ -527,7 +385,9 @@ module Ruflet
527
385
  def prepare_flutter_client(client_dir, platform:, tools:, config:, self_contained: false, verbose: false)
528
386
  refresh_managed_client_template_files(client_dir, verbose: verbose)
529
387
  sync_client_metadata(client_dir, config, verbose: verbose)
530
- configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
388
+ apply_native_service_permissions(client_dir, config)
389
+ configured = configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
390
+ return false if configured == false
531
391
  @ruflet_self_contained_build = self_contained
532
392
  apply_service_extension_config(client_dir, config)
533
393
  asset_flags = apply_build_config(client_dir, config)
@@ -642,20 +502,7 @@ module Ruflet
642
502
  end
643
503
 
644
504
  def unbundled_command_env(env)
645
- command_env = env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_") }
646
- ensure_utf8_locale(command_env)
647
- end
648
-
649
- # CocoaPods refuses to run in a non-UTF-8 terminal and Xcode project
650
- # files contain UTF-8; guarantee a UTF-8 locale for child tools.
651
- def ensure_utf8_locale(env)
652
- locale = env["LC_ALL"].to_s
653
- locale = env["LANG"].to_s if locale.empty?
654
- return env if locale.downcase.include?("utf-8") || locale.downcase.include?("utf8")
655
-
656
- env["LANG"] = "en_US.UTF-8"
657
- env["LC_ALL"] = "en_US.UTF-8"
658
- env
505
+ env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_") }
659
506
  end
660
507
 
661
508
  def run_external_command(env, *cmd, chdir:, unbundled: false)
@@ -736,60 +583,20 @@ module Ruflet
736
583
  alt = "ruflet.yml"
737
584
  config_path = alt if File.file?(alt)
738
585
  end
739
- return load_rails_ruflet_initializer_config unless File.file?(config_path)
740
-
741
- YAML.safe_load(File.read(config_path), aliases: true) || {}
742
- rescue StandardError => e
743
- warn "Failed to load ruflet config: #{e.class}: #{e.message}"
744
- {}
745
- end
586
+ return {} unless File.file?(config_path)
746
587
 
747
- def load_rails_ruflet_initializer_config
748
- initializer = File.join(Dir.pwd, "config", "initializers", "ruflet.rb")
749
- return {} unless File.file?(initializer)
750
-
751
- require "ruflet_rails"
752
-
753
- with_minimal_rails_config_context do
754
- load initializer
755
- config = Ruflet::Rails.config
756
- config.respond_to?(:to_ruflet_yaml_hash) ? config.to_ruflet_yaml_hash : {}
588
+ config = YAML.safe_load(File.read(config_path), aliases: true) || {}
589
+ services_path = File.join(File.dirname(File.expand_path(config_path)), "services.yaml")
590
+ if File.file?(services_path)
591
+ service_config = YAML.safe_load(File.read(services_path), aliases: true) || {}
592
+ config["services"] = service_config["services"] if service_config.key?("services")
757
593
  end
758
- rescue LoadError => e
759
- warn "Failed to load Rails Ruflet config: #{e.class}: #{e.message}"
760
- {}
594
+ config
761
595
  rescue StandardError => e
762
- warn "Failed to load Rails Ruflet config: #{e.class}: #{e.message}"
596
+ warn "Failed to load ruflet config: #{e.class}: #{e.message}"
763
597
  {}
764
598
  end
765
599
 
766
- def with_minimal_rails_config_context
767
- return yield if defined?(::Rails)
768
-
769
- rails_module = Module.new
770
- root = Pathname.new(Dir.pwd)
771
- env = minimal_rails_env(ENV.fetch("RAILS_ENV", "development"))
772
- rails_module.define_singleton_method(:root) { root }
773
- rails_module.define_singleton_method(:env) { env }
774
-
775
- Object.const_set(:Rails, rails_module)
776
- yield
777
- ensure
778
- Object.send(:remove_const, :Rails) if defined?(rails_module) && Object.const_defined?(:Rails, false) && ::Rails.equal?(rails_module)
779
- end
780
-
781
- def minimal_rails_env(name)
782
- value = name.to_s
783
- Object.new.tap do |env|
784
- env.define_singleton_method(:to_s) { value }
785
- env.define_singleton_method(:to_str) { value }
786
- env.define_singleton_method(:==) { |other| value == other.to_s }
787
- env.define_singleton_method(:development?) { value == "development" }
788
- env.define_singleton_method(:test?) { value == "test" }
789
- env.define_singleton_method(:production?) { value == "production" }
790
- end
791
- end
792
-
793
600
  def apply_build_config(client_dir, config = {})
794
601
  config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
795
602
  config_present = File.file?(config_path)
@@ -917,7 +724,6 @@ module Ruflet
917
724
  apply_web_metadata(client_dir, metadata)
918
725
  apply_windows_metadata(client_dir, metadata)
919
726
  apply_linux_metadata(client_dir, metadata)
920
- apply_dart_metadata(client_dir, metadata)
921
727
  build_log(
922
728
  verbose,
923
729
  "app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
@@ -995,6 +801,16 @@ module Ruflet
995
801
  %( applicationId = "#{metadata[:android_application_id]}")
996
802
  )
997
803
 
804
+ Dir.glob(
805
+ File.join(client_dir, "android", "app", "src", "main", "kotlin", "**", "MainActivity.kt")
806
+ ).each do |activity_path|
807
+ replace_in_file(
808
+ activity_path,
809
+ /^package\s+[^\s]+$/,
810
+ "package #{metadata[:android_application_id]}"
811
+ )
812
+ end
813
+
998
814
  manifest_path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
999
815
  replace_in_file(
1000
816
  manifest_path,
@@ -1113,31 +929,6 @@ module Ruflet
1113
929
  cmake_path = File.join(client_dir, "linux", "CMakeLists.txt")
1114
930
  replace_in_file(cmake_path, /^set\(BINARY_NAME ".*"\)$/, %(set(BINARY_NAME "#{metadata[:package_name]}")))
1115
931
  replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
1116
-
1117
- # The GTK runner hardcodes the window title (header-bar and fallback
1118
- # paths). Without this it shows the package name (e.g. "ruflet_client")
1119
- # instead of the configured app name. Match the call, not the literal,
1120
- # so re-runs stay idempotent.
1121
- my_application_path = File.join(client_dir, "linux", "runner", "my_application.cc")
1122
- title = c_string_escape(metadata[:display_name])
1123
- replace_in_file(
1124
- my_application_path,
1125
- /gtk_header_bar_set_title\(header_bar, "[^"]*"\)/,
1126
- %(gtk_header_bar_set_title(header_bar, "#{title}"))
1127
- )
1128
- replace_in_file(
1129
- my_application_path,
1130
- /gtk_window_set_title\(window, "[^"]*"\)/,
1131
- %(gtk_window_set_title(window, "#{title}"))
1132
- )
1133
- end
1134
-
1135
- def apply_dart_metadata(client_dir, metadata)
1136
- title = dart_single_quote_escape(metadata[:display_name])
1137
- client_entrypoint_paths(client_dir).each do |entrypoint|
1138
- replace_in_file(entrypoint, /title: 'Ruflet'/, "title: '#{title}'")
1139
- replace_in_file(entrypoint, /AppBar\(title: const Text\('Ruflet'\)\)/, "AppBar(title: const Text('#{title}'))")
1140
- end
1141
932
  end
1142
933
 
1143
934
  def replace_plist_value(path, key, value)
@@ -1222,31 +1013,18 @@ module Ruflet
1222
1013
  value.to_s.gsub('"', '""')
1223
1014
  end
1224
1015
 
1225
- # Escape a value for embedding inside a C string literal (used for the
1226
- # GTK runner window title in my_application.cc). Block form avoids
1227
- # backslash interpretation in the gsub replacement string.
1228
- def c_string_escape(value)
1229
- value.to_s.gsub(/[\\"]/) { |ch| "\\#{ch}" }
1230
- end
1231
-
1232
- def dart_single_quote_escape(value)
1233
- value.to_s.gsub("\\", "\\\\\\").gsub("'", "\\\\'")
1234
- end
1235
-
1236
1016
  def key_defined?(hash, key)
1237
1017
  hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
1238
1018
  end
1239
1019
 
1240
1020
  def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
1241
- service_definitions = load_service_definitions(client_dir)
1242
- service_extension_keys = service_definitions.keys.flat_map { |key| Array(SERVICE_EXTENSION_MAP[key]) }.uniq
1243
- protected_extension_keys = SERVICE_EXTENSION_MAP.values.flatten.uniq
1244
- configured_extension_keys = Array(config["extensions"]).map { |value| normalize_extension_key(value) }.compact
1245
- extension_keys = ((configured_extension_keys - protected_extension_keys) | service_extension_keys).uniq
1021
+ services = configured_service_entries(config).map { |entry| entry[:name] }
1022
+ requested_extensions = Array(config["extensions"]).map { |value| normalize_extension_key(value) }.compact
1023
+ protected_extensions = services.flat_map { |name| PROTECTED_SERVICE_EXTENSIONS.fetch(name, []) }
1024
+ extension_keys = (requested_extensions + protected_extensions + services).uniq
1246
1025
  extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
1247
1026
  extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
1248
1027
 
1249
- sync_client_flet_packages(client_dir, extension_packages)
1250
1028
  pubspec_path = File.join(client_dir, "pubspec.yaml")
1251
1029
  if File.file?(pubspec_path)
1252
1030
  sync_client_extension_dependencies(pubspec_path, extension_packages)
@@ -1256,208 +1034,67 @@ module Ruflet
1256
1034
  sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
1257
1035
  prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
1258
1036
  end
1259
- apply_service_native_requirements(client_dir, service_definitions.keys, service_definitions)
1260
1037
  end
1261
1038
 
1262
- def apply_service_native_requirements(client_dir, extension_keys, service_definitions = load_service_definitions(client_dir))
1263
- service_requirements = service_native_requirements(service_definitions)
1264
- stale_keys = service_requirements.keys - extension_keys
1265
- remove_service_native_requirements(client_dir, stale_keys, service_requirements)
1266
-
1267
- requirements = merge_service_native_requirements(extension_keys, service_requirements)
1268
- sync_ios_permission_definitions(
1269
- File.join(client_dir, "ios", "Podfile"),
1270
- Array(requirements[:ios_permission_definitions])
1271
- )
1272
- return if requirements.empty?
1273
-
1274
- android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1275
- Array(requirements[:android_permissions]).each do |permission|
1276
- ensure_android_permission(android_manifest, permission)
1277
- end
1278
-
1279
- ios_info = File.join(client_dir, "ios", "Runner", "Info.plist")
1280
- Hash(requirements[:ios_info]).each do |key, value|
1281
- ensure_plist_string(ios_info, key, value)
1282
- end
1283
-
1284
- macos_info = File.join(client_dir, "macos", "Runner", "Info.plist")
1285
- Hash(requirements[:macos_info]).each do |key, value|
1286
- ensure_plist_string(macos_info, key, value)
1287
- end
1288
-
1289
- %w[DebugProfile Release].each do |name|
1290
- entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
1291
- Hash(requirements[:macos_entitlements]).each do |key, value|
1292
- ensure_plist_boolean(entitlements_path, key, value)
1293
- end
1294
- end
1295
- end
1296
-
1297
- def remove_service_native_requirements(client_dir, extension_keys, service_requirements = service_native_requirements(load_service_definitions(client_dir)))
1298
- requirements = merge_service_native_requirements(extension_keys, service_requirements)
1299
- return if requirements.empty?
1300
-
1301
- android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1302
- Array(requirements[:android_permissions]).each do |permission|
1303
- remove_android_permission(android_manifest, permission)
1304
- end
1305
-
1306
- [File.join(client_dir, "ios", "Runner", "Info.plist"), File.join(client_dir, "macos", "Runner", "Info.plist")].each do |path|
1307
- (Hash(requirements[:ios_info]).keys + Hash(requirements[:macos_info]).keys).uniq.each do |key|
1308
- remove_plist_entry(path, key)
1309
- end
1310
- end
1311
-
1312
- %w[DebugProfile Release].each do |name|
1313
- entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
1314
- Hash(requirements[:macos_entitlements]).each_key do |key|
1315
- remove_plist_entry(entitlements_path, key)
1316
- end
1317
- end
1318
- end
1039
+ def configured_service_entries(config)
1040
+ Array(config["services"]).filter_map do |entry|
1041
+ case entry
1042
+ when Hash
1043
+ name, options = entry.first
1044
+ key = normalize_extension_key(name)
1045
+ next unless key
1319
1046
 
1320
- def merge_service_native_requirements(extension_keys, service_requirements = DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
1321
- extension_keys.each_with_object({}) do |key, memo|
1322
- requirements = service_requirements[key]
1323
- next unless requirements
1324
-
1325
- memo[:android_permissions] ||= []
1326
- memo[:android_permissions] |= Array(requirements[:android_permissions])
1327
- memo[:ios_permission_definitions] ||= []
1328
- memo[:ios_permission_definitions] |= Array(requirements[:ios_permission_definitions])
1329
- %i[ios_info macos_info macos_entitlements].each do |section|
1330
- memo[section] ||= {}
1331
- memo[section].merge!(requirements[section] || {})
1047
+ description = options.is_a?(Hash) ? options["description"] || options[:description] : options
1048
+ { name: key, description: description.to_s.strip }
1049
+ else
1050
+ key = normalize_extension_key(entry)
1051
+ { name: key, description: "" } if key
1332
1052
  end
1333
1053
  end
1334
1054
  end
1335
1055
 
1336
- def load_service_definitions(client_dir = nil)
1337
- path = services_config_path(client_dir)
1338
- return {} unless path
1339
-
1340
- data = YAML.safe_load(File.read(path), aliases: true) || {}
1341
- Array(data["services"]).each_with_object({}) do |entry, memo|
1342
- name, metadata =
1343
- if entry.is_a?(Hash)
1344
- entry.first
1345
- else
1346
- [entry, {}]
1347
- end
1348
- key = normalize_extension_key(name)
1349
- memo[key] = metadata.is_a?(Hash) ? metadata : {} if key
1350
- end
1351
- rescue Psych::Exception => e
1352
- warn "Could not parse #{path}: #{e.message}"
1353
- {}
1354
- end
1355
-
1356
- def services_config_path(client_dir = nil)
1357
- candidates = [ENV["RUFLET_SERVICES"], File.expand_path("services.yaml", Dir.pwd)]
1358
- candidates << File.join(client_dir, "services.yaml") if client_dir
1359
- candidates.compact.find { |path| File.file?(path) }
1360
- end
1361
-
1362
- def service_native_requirements(service_definitions)
1363
- all_keys = (DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys | service_definitions.keys)
1364
- all_keys.each_with_object({}) do |key, memo|
1365
- defaults = DEFAULT_SERVICE_NATIVE_REQUIREMENTS[key] || {}
1366
- metadata = service_definitions[key] || {}
1367
- native = metadata["native"].is_a?(Hash) ? metadata["native"] : metadata
1368
- memo[key] = {
1369
- android_permissions: Array(native["android_permissions"] || defaults[:android_permissions]),
1370
- ios_permission_definitions: Array(native["ios_permission_definitions"] || defaults[:ios_permission_definitions]),
1371
- ios_info: native["ios_info"].is_a?(Hash) ? native["ios_info"] : (defaults[:ios_info] || {}),
1372
- macos_info: native["macos_info"].is_a?(Hash) ? native["macos_info"] : (defaults[:macos_info] || {}),
1373
- macos_entitlements: native["macos_entitlements"].is_a?(Hash) ? native["macos_entitlements"] : (defaults[:macos_entitlements] || {})
1374
- }
1375
- end
1376
- end
1377
-
1378
- def sync_ios_permission_definitions(path, definitions)
1379
- return unless File.file?(path)
1056
+ def apply_native_service_permissions(client_dir, config)
1057
+ entries = configured_service_entries(config)
1058
+ return if entries.empty?
1380
1059
 
1381
- content = File.read(path)
1382
- marker_pattern = %r{\n?\s*# BEGIN RUFLET PERMISSION DEFINITIONS.*?# END RUFLET PERMISSION DEFINITIONS\n?}m
1383
- updated = content.gsub(marker_pattern, "\n")
1384
- definitions = Array(definitions).map(&:to_s).reject(&:empty?).uniq.sort
1385
- if definitions.any?
1386
- block = <<~RUBY.chomp
1387
- # BEGIN RUFLET PERMISSION DEFINITIONS
1388
- target.build_configurations.each do |config|
1389
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
1390
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] += #{definitions.inspect}
1391
- end
1392
- # END RUFLET PERMISSION DEFINITIONS
1393
- RUBY
1394
- updated = updated.sub(
1395
- /^(\s*)flutter_additional_ios_build_settings\(target\)\s*$/,
1396
- "\\0\n\n#{block}"
1397
- )
1398
- end
1399
- File.write(path, updated) unless updated == content
1060
+ apply_android_service_permissions(client_dir, entries)
1061
+ apply_ios_service_usage_descriptions(client_dir, entries)
1400
1062
  end
1401
1063
 
1402
- def ensure_android_permission(path, permission)
1064
+ def apply_android_service_permissions(client_dir, entries)
1065
+ path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1403
1066
  return unless File.file?(path)
1404
1067
 
1405
1068
  content = File.read(path)
1406
- return if content.include?(permission)
1069
+ entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission|
1070
+ next if content.include?(%(android:name="#{permission}"))
1407
1071
 
1408
- permission_line = %( <uses-permission android:name="#{xml_escape(permission)}"/>\n)
1409
- updated = content.sub(/(<manifest\b[^>]*>\s*)/m) { "#{Regexp.last_match(1)}#{permission_line}" }
1410
- File.write(path, updated == content ? "#{permission_line}#{content}" : updated)
1072
+ content.sub!(/<manifest\b[^>]*>\s*/, "\\0 <uses-permission android:name=\"#{permission}\"/>\n")
1073
+ end
1074
+ File.write(path, content)
1411
1075
  end
1412
1076
 
1413
- def remove_android_permission(path, permission)
1077
+ def apply_ios_service_usage_descriptions(client_dir, entries)
1078
+ path = File.join(client_dir, "ios", "Runner", "Info.plist")
1414
1079
  return unless File.file?(path)
1415
1080
 
1416
1081
  content = File.read(path)
1417
- updated = content.gsub(%r{^\s*<uses-permission\s+android:name="#{Regexp.escape(permission)}"\s*/>\s*\n?}, "")
1418
- File.write(path, updated) unless updated == content
1419
- end
1420
-
1421
- def ensure_plist_string(path, key, value)
1422
- ensure_plist_entry(path, key, "<string>#{xml_escape(value)}</string>")
1423
- end
1424
-
1425
- def ensure_plist_boolean(path, key, value)
1426
- ensure_plist_entry(path, key, value ? "<true/>" : "<false/>")
1427
- end
1082
+ entries.each do |entry|
1083
+ key = IOS_SERVICE_USAGE_KEYS[entry[:name]]
1084
+ next unless key
1428
1085
 
1429
- def ensure_plist_entry(path, key, value_xml)
1430
- return unless File.file?(path)
1086
+ description = entry[:description]
1087
+ description = "This app uses #{entry[:name]} access for its Ruflet features." if description.empty?
1088
+ escaped_description = xml_escape(description)
1089
+ pair = "\t<key>#{key}</key>\n\t<string>#{escaped_description}</string>\n"
1431
1090
 
1432
- content = File.read(path)
1433
- return if content.include?("<key>#{key}</key>")
1434
-
1435
- entry = "\t<key>#{key}</key>\n\t#{value_xml}\n"
1436
-
1437
- # Insert into the ROOT <dict> (opened right after <plist ...>), never the
1438
- # first nested </dict>. Modern Flutter Info.plists nest a dict inside
1439
- # UIApplicationSceneManifest, so subbing the first </dict> would bury
1440
- # top-level keys (e.g. NS*UsageDescription) where iOS can't read them —
1441
- # which crashes the app the moment a permission is requested.
1442
- root_open = content.match(%r{<plist\b[^>]*>\s*<dict>}m)
1443
- updated =
1444
- if root_open
1445
- insert_at = root_open.end(0)
1446
- content[0...insert_at] + "\n#{entry}" + content[insert_at..]
1447
- elsif (last = content.rindex("</dict>"))
1448
- content[0...last] + entry + content[last..]
1091
+ if content.match?(%r{<key>#{Regexp.escape(key)}</key>})
1092
+ content.sub!(%r{<key>#{Regexp.escape(key)}</key>\s*<string>.*?</string>}m, "<key>#{key}</key>\n\t<string>#{escaped_description}</string>")
1449
1093
  else
1450
- "#{content}\n#{entry}"
1094
+ content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
1451
1095
  end
1452
- File.write(path, updated)
1453
- end
1454
-
1455
- def remove_plist_entry(path, key)
1456
- return unless File.file?(path)
1457
-
1458
- content = File.read(path)
1459
- updated = content.gsub(%r{\s*<key>#{Regexp.escape(key)}</key>\s*<(?:string>.*?</string|true/|false/)>\s*}m, "\n")
1460
- File.write(path, updated) unless updated == content
1096
+ end
1097
+ File.write(path, content)
1461
1098
  end
1462
1099
 
1463
1100
  def clear_flutter_build_state(client_dir, verbose: false)
@@ -1501,6 +1138,7 @@ module Ruflet
1501
1138
  remove_self_contained_project_assets(client_dir, verbose: verbose)
1502
1139
  remove_local_ruby_runtime_override(client_dir, verbose: verbose)
1503
1140
  end
1141
+ true
1504
1142
  end
1505
1143
 
1506
1144
  def sync_client_pubspec_for_runtime_mode(client_dir, self_contained:)
@@ -1510,23 +1148,24 @@ module Ruflet
1510
1148
  data = YAML.safe_load(File.read(pubspec_path), aliases: true) || {}
1511
1149
  dependencies = data["dependencies"]
1512
1150
  dependencies = data["dependencies"] = {} unless dependencies.is_a?(Hash)
1151
+ spinkit_dependency = template_client_pubspec_dependencies["flutter_spinkit"]
1152
+ dependencies["flutter_spinkit"] = spinkit_dependency if spinkit_dependency
1513
1153
  flutter = data["flutter"]
1514
1154
  flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
1515
1155
  assets = Array(flutter["assets"]).map(&:to_s)
1516
- project_asset_prefix = "assets/#{self_contained_project_name}/"
1517
- assets.reject! { |asset| asset.start_with?(project_asset_prefix) }
1518
1156
 
1519
1157
  if self_contained
1520
1158
  dependencies["ruby_runtime"] = ruby_runtime_dependency(dependencies["ruby_runtime"])
1521
- assets.delete("assets/main.rb")
1522
- assets.delete("assets/ruby_project/")
1523
- project_asset_relative_paths.each do |relative_path|
1524
- assets << "#{project_asset_prefix}#{relative_path}"
1159
+ # Flutter does not recurse into asset directories, so every subdirectory
1160
+ # of the embedded project (e.g. standalone_apps/<slug>/) must be listed
1161
+ # explicitly or its files never reach the bundle/manifest on device.
1162
+ self_contained_project_asset_dirs.each do |dir_entry|
1163
+ assets << dir_entry unless assets.include?(dir_entry)
1525
1164
  end
1526
1165
  else
1527
1166
  dependencies.delete("ruby_runtime")
1528
- assets.delete("assets/main.rb")
1529
- assets.delete("assets/ruby_project/")
1167
+ project_prefix = "assets/#{self_contained_project_name}/"
1168
+ assets.reject! { |a| a.to_s == project_prefix || a.to_s.start_with?(project_prefix) }
1530
1169
  end
1531
1170
 
1532
1171
  flutter["assets"] = assets unless assets.empty?
@@ -1534,46 +1173,11 @@ module Ruflet
1534
1173
  write_pubspec_yaml(pubspec_path, data)
1535
1174
  end
1536
1175
 
1537
- RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.6"
1538
-
1539
1176
  def ruby_runtime_dependency(current_dependency = nil)
1540
- local_path = explicit_local_ruby_runtime_path || repo_checkout_ruby_runtime_path
1177
+ local_path = explicit_local_ruby_runtime_path || source_checkout_ruby_runtime_path
1541
1178
  return { "path" => local_path } if local_path
1542
1179
 
1543
- template_dependency = template_client_pubspec_dependencies["ruby_runtime"]
1544
- return template_dependency if usable_ruby_runtime_dependency?(template_dependency)
1545
-
1546
- return current_dependency if usable_ruby_runtime_dependency?(current_dependency)
1547
-
1548
- RUBY_RUNTIME_FALLBACK_REQUIREMENT
1549
- end
1550
-
1551
- # In a ruflet repo checkout the plugin lives next to templates/; build
1552
- # against it so framework changes are exercised without publishing.
1553
- def repo_checkout_ruby_runtime_path
1554
- template_root =
1555
- if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1556
- Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1557
- end
1558
- return nil unless template_root
1559
-
1560
- candidate = File.expand_path(File.join(template_root, "..", "..", "ruby_runtime"))
1561
- File.file?(File.join(candidate, "pubspec.yaml")) ? candidate : nil
1562
- end
1563
-
1564
- # Relative path dependencies only resolve from the directory the
1565
- # template was authored in — never copy them into a user's client.
1566
- def usable_ruby_runtime_dependency?(dependency)
1567
- case dependency
1568
- when nil then false
1569
- when Hash
1570
- path = dependency["path"] || dependency[:path]
1571
- return true if path.nil? # hosted/git table form
1572
-
1573
- Pathname.new(path.to_s).absolute? && File.file?(File.join(path, "pubspec.yaml"))
1574
- else
1575
- !dependency.to_s.strip.empty?
1576
- end
1180
+ current_dependency || "^0.0.3"
1577
1181
  end
1578
1182
 
1579
1183
  def explicit_local_ruby_runtime_path
@@ -1586,6 +1190,13 @@ module Ruflet
1586
1190
  nil
1587
1191
  end
1588
1192
 
1193
+ def source_checkout_ruby_runtime_path
1194
+ candidate = Pathname.new(File.expand_path("../../../../../ruby_runtime", __dir__))
1195
+ return candidate.to_s if candidate.join("pubspec.yaml").file?
1196
+
1197
+ nil
1198
+ end
1199
+
1589
1200
  def refresh_managed_client_template_files(client_dir, verbose: false)
1590
1201
  template_root =
1591
1202
  if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
@@ -1598,13 +1209,11 @@ module Ruflet
1598
1209
  "lib/main.self.dart",
1599
1210
  "lib/main.server.dart",
1600
1211
  "lib/ruflet_file_picker_service.dart",
1601
- "macos/Runner/DebugProfile.entitlements",
1602
- "macos/Runner/Release.entitlements"
1603
- ]
1604
- stale_files = [
1212
+ "lib/ruflet_spinkit.dart",
1605
1213
  "lib/connection_probe.dart",
1606
1214
  "lib/connection_probe_io.dart",
1607
- "lib/connection_probe_stub.dart"
1215
+ "lib/connection_probe_stub.dart",
1216
+ "ios/Podfile"
1608
1217
  ]
1609
1218
 
1610
1219
  managed_files.each do |relative_path|
@@ -1616,35 +1225,6 @@ module Ruflet
1616
1225
  FileUtils.cp(source, destination)
1617
1226
  build_log(verbose, "refreshed template file #{relative_path}")
1618
1227
  end
1619
-
1620
- stale_files.each do |relative_path|
1621
- path = File.join(client_dir, relative_path)
1622
- next unless File.file?(path)
1623
-
1624
- FileUtils.rm_f(path)
1625
- build_log(verbose, "removed stale template file #{relative_path}")
1626
- end
1627
-
1628
- repair_legacy_self_contained_bootstrap(client_dir, verbose: verbose)
1629
- end
1630
-
1631
- def repair_legacy_self_contained_bootstrap(client_dir, verbose: false)
1632
- path = File.join(client_dir, "lib", "main.self.dart")
1633
- return unless File.file?(path)
1634
-
1635
- content = File.read(path)
1636
- updated = content.gsub(
1637
- /^\s*await RubyRuntime\.eval\("ENV\['RUFLET_DEBUG'\].*?\n/,
1638
- ""
1639
- )
1640
- updated = updated.gsub(
1641
- /^\s*final digestLength = await RubyRuntime\.eval\(\n.*?^\s*\);\n\s*debugPrint\('Embedded Digest::SHA1 bytesize: \$digestLength'\);\n/m,
1642
- ""
1643
- )
1644
- return if updated == content
1645
-
1646
- File.write(path, updated)
1647
- build_log(verbose, "removed legacy pre-server RubyRuntime.eval diagnostics")
1648
1228
  end
1649
1229
 
1650
1230
  def write_pubspec_yaml(path, data)
@@ -1669,17 +1249,23 @@ module Ruflet
1669
1249
  end.join
1670
1250
  end
1671
1251
 
1252
+ # Flutter asset directory entries for every folder of the embedded project
1253
+ # that contains packaged files. Derived from the exact copy list so the
1254
+ # pubspec asset dirs match what sync_self_contained_project_assets writes.
1255
+ def self_contained_project_asset_dirs
1256
+ prefix = "assets/#{self_contained_project_name}"
1257
+ dirs = project_asset_relative_paths.map { |rel| File.dirname(rel) }.uniq
1258
+ project_dirs = dirs.map { |dir| dir == "." ? "#{prefix}/" : "#{prefix}/#{dir}/" }
1259
+ project_dirs.sort
1260
+ end
1261
+
1672
1262
  def sync_self_contained_project_assets(client_dir, verbose: false)
1673
1263
  project_root = Pathname.new(Dir.pwd)
1674
1264
  assets_root = File.join(client_dir, "assets")
1675
1265
  destination_root = File.join(assets_root, self_contained_project_name)
1676
1266
  FileUtils.rm_rf(destination_root)
1677
- FileUtils.rm_rf(File.join(assets_root, "ruby_project"))
1678
1267
  FileUtils.mkdir_p(destination_root)
1679
1268
 
1680
- legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
1681
- FileUtils.rm_f(legacy_entrypoint)
1682
-
1683
1269
  copied = 0
1684
1270
  project_asset_relative_paths.each do |relative_path|
1685
1271
  source = project_root.join(relative_path)
@@ -1687,104 +1273,15 @@ module Ruflet
1687
1273
 
1688
1274
  destination = File.join(destination_root, relative_path)
1689
1275
  FileUtils.mkdir_p(File.dirname(destination))
1690
- copy_project_asset_file(source.to_s, destination, project_root: project_root.to_s)
1276
+ FileUtils.cp(source.to_s, destination)
1691
1277
  copied += 1
1692
1278
  end
1693
1279
 
1694
1280
  build_log(verbose, "copied #{copied} project file#{copied == 1 ? '' : 's'} to assets/#{self_contained_project_name}")
1695
1281
  end
1696
1282
 
1697
- def copy_project_asset_file(source, destination, project_root: nil)
1698
- if File.extname(source).downcase == ".rb"
1699
- ruby_source =
1700
- if File.basename(source) == "main.rb" && project_root
1701
- bundled_embedded_ruby_source(source, project_root)
1702
- else
1703
- File.read(source)
1704
- end
1705
- File.write(destination, normalize_embedded_ruby_source(ruby_source))
1706
- else
1707
- FileUtils.cp(source, destination)
1708
- end
1709
- end
1710
-
1711
- def bundled_embedded_ruby_source(source, project_root, visited = {})
1712
- absolute = File.expand_path(source)
1713
- return "" if visited[absolute]
1714
-
1715
- visited[absolute] = true
1716
- base = File.dirname(absolute)
1717
- File.read(absolute).lines.map do |line|
1718
- match = line.match(/^\s*require_relative\s+["']([^"']+)["']\s*$/)
1719
- next line unless match
1720
-
1721
- required = File.expand_path(match[1], base)
1722
- required = "#{required}.rb" unless File.file?(required)
1723
- next line unless File.file?(required) && required.start_with?(File.expand_path(project_root))
1724
-
1725
- bundled_embedded_ruby_source(required, project_root, visited)
1726
- end.join
1727
- end
1728
-
1729
- def normalize_embedded_ruby_source(source)
1730
- source.lines.map do |line|
1731
- expand_endless_method_line(line)
1732
- end.join
1733
- end
1734
-
1735
- def expand_endless_method_line(line)
1736
- match = line.match(/^(\s*)def\s+(.+)$/)
1737
- return line unless match
1738
-
1739
- indent = match[1]
1740
- body = match[2].chomp
1741
- newline = line.end_with?("\n") ? "\n" : ""
1742
- split = endless_method_split(body)
1743
- return line unless split
1744
-
1745
- signature, expression = split
1746
- "#{indent}def #{signature.rstrip}\n#{indent} #{expression.lstrip}\n#{indent}end#{newline}"
1747
- end
1748
-
1749
- def endless_method_split(body)
1750
- depth = 0
1751
- quote = nil
1752
- escape = false
1753
-
1754
- body.each_char.with_index do |char, index|
1755
- if quote
1756
- escape = char == "\\" && !escape
1757
- if char == quote && !escape
1758
- quote = nil
1759
- elsif char != "\\"
1760
- escape = false
1761
- end
1762
- next
1763
- end
1764
-
1765
- case char
1766
- when "'", '"'
1767
- quote = char
1768
- when "(", "[", "{"
1769
- depth += 1
1770
- when ")", "]", "}"
1771
- depth -= 1 if depth.positive?
1772
- when "="
1773
- next unless depth.zero? && body[index + 1] == " "
1774
-
1775
- signature = body[0...index]
1776
- expression = body[(index + 1)..]
1777
- return [signature, expression] unless signature.strip.empty? || expression.to_s.strip.empty?
1778
- end
1779
- end
1780
-
1781
- nil
1782
- end
1783
-
1784
1283
  def remove_self_contained_project_assets(client_dir, verbose: false)
1785
1284
  assets_root = File.join(client_dir, "assets")
1786
- legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
1787
- FileUtils.rm_f(legacy_entrypoint)
1788
1285
  removed = false
1789
1286
 
1790
1287
  project_root = File.join(assets_root, self_contained_project_name)
@@ -1793,12 +1290,6 @@ module Ruflet
1793
1290
  removed = true
1794
1291
  end
1795
1292
 
1796
- legacy_root = File.join(assets_root, "ruby_project")
1797
- if Dir.exist?(legacy_root)
1798
- FileUtils.rm_rf(legacy_root)
1799
- removed = true
1800
- end
1801
-
1802
1293
  build_log(verbose, "removed embedded self-contained project assets") if removed
1803
1294
  end
1804
1295
 
@@ -1809,11 +1300,12 @@ module Ruflet
1809
1300
  Find.find(root.to_s) do |path|
1810
1301
  pathname = Pathname.new(path)
1811
1302
  relative = pathname.relative_path_from(root).to_s
1812
- next if relative.empty?
1303
+ next if relative.empty? || relative == "."
1813
1304
 
1814
1305
  if pathname.directory?
1815
1306
  if skip_project_asset_directory?(relative)
1816
1307
  Find.prune
1308
+ next
1817
1309
  else
1818
1310
  next
1819
1311
  end
@@ -1834,8 +1326,7 @@ module Ruflet
1834
1326
  end
1835
1327
 
1836
1328
  def skip_project_asset_directory?(relative)
1837
- first = relative.split(File::SEPARATOR).first
1838
- %w[
1329
+ excluded_directories = %w[
1839
1330
  .git
1840
1331
  .bundle
1841
1332
  .dart_tool
@@ -1850,21 +1341,17 @@ module Ruflet
1850
1341
  ruflet_client
1851
1342
  tmp
1852
1343
  vendor
1853
- ].include?(first)
1344
+ ]
1345
+ relative.split(File::SEPARATOR).any? do |component|
1346
+ component.start_with?(".") || excluded_directories.include?(component)
1347
+ end
1854
1348
  end
1855
1349
 
1856
1350
  def include_project_asset_file?(relative)
1857
1351
  basename = File.basename(relative)
1352
+ return false if basename == ".DS_Store"
1858
1353
  return false if %w[Gemfile.lock pubspec.lock Podfile.lock package-lock.json yarn.lock pnpm-lock.yaml].include?(basename)
1859
- return true if %w[main.rb Gemfile ruflet.yaml ruflet.yml manifest.json].include?(basename)
1860
-
1861
- ext = File.extname(relative).downcase
1862
- return true if %w[.rb .json .yml .yaml].include?(ext)
1863
-
1864
- first = relative.split(File::SEPARATOR).first
1865
- return true if first == "assets"
1866
-
1867
- false
1354
+ true
1868
1355
  end
1869
1356
 
1870
1357
  def flutter_target_entrypoint(client_dir, self_contained:)
@@ -1914,50 +1401,6 @@ module Ruflet
1914
1401
  write_pubspec_yaml(path, data)
1915
1402
  end
1916
1403
 
1917
- def sync_client_flet_packages(client_dir, selected_packages)
1918
- template_root =
1919
- if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1920
- Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1921
- end
1922
- return unless template_root
1923
-
1924
- source_root = File.join(template_root, "flet_packages")
1925
- target_root = File.join(client_dir, "flet_packages")
1926
- return unless Dir.exist?(source_root)
1927
- return if File.expand_path(source_root) == File.expand_path(target_root)
1928
-
1929
- # The repository's standalone client intentionally carries the full
1930
- # local package catalog. Only generated/template-derived clients are
1931
- # conditioned for a particular Ruflet application.
1932
- return if standalone_ruflet_client_source?(client_dir, template_root)
1933
-
1934
- known_packages = CLIENT_EXTENSION_MAP.values.map { |meta| meta.fetch(:package) }.uniq
1935
- required_packages = (["flet"] + selected_packages).uniq
1936
-
1937
- FileUtils.mkdir_p(target_root)
1938
- required_packages.each do |package_name|
1939
- source = File.join(source_root, package_name)
1940
- target = File.join(target_root, package_name)
1941
- next unless Dir.exist?(source)
1942
- next if Dir.exist?(target)
1943
-
1944
- FileUtils.cp_r(source, target)
1945
- end
1946
-
1947
- (known_packages - selected_packages).each do |package_name|
1948
- FileUtils.rm_rf(File.join(target_root, package_name))
1949
- end
1950
- end
1951
-
1952
- def standalone_ruflet_client_source?(client_dir, template_root)
1953
- client_root = File.expand_path(client_dir)
1954
- candidates = [
1955
- File.expand_path("../../ruflet_client", template_root),
1956
- File.expand_path("../../../../../ruflet_client", __dir__)
1957
- ]
1958
- candidates.any? { |candidate| File.expand_path(candidate) == client_root }
1959
- end
1960
-
1961
1404
  def sync_client_extension_dependencies(path, selected_packages)
1962
1405
  return if selected_packages.empty?
1963
1406
 
@@ -1966,30 +1409,14 @@ module Ruflet
1966
1409
 
1967
1410
  data = YAML.safe_load(File.read(path), aliases: true) || {}
1968
1411
  deps = (data["dependencies"] || {}).dup
1969
- if selected_packages.include?("flet_rive")
1970
- deps.delete("rive")
1971
- deps.delete("rive_native")
1972
- end
1973
1412
  selected_packages.each do |package_name|
1974
1413
  deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
1975
1414
  end
1976
1415
 
1977
1416
  data["dependencies"] = deps
1978
- sync_local_flet_dependency_override(data, deps["flet"])
1979
1417
  write_pubspec_yaml(path, data)
1980
1418
  end
1981
1419
 
1982
- # Git-backed Flet extensions declare their own Flet source. When Ruflet
1983
- # vendors the core engine locally, force every extension to resolve that
1984
- # same copy instead of letting Pub reject the mixed sources.
1985
- def sync_local_flet_dependency_override(data, flet_dependency)
1986
- return unless flet_dependency.is_a?(Hash) && key_defined?(flet_dependency, "path")
1987
-
1988
- overrides = data["dependency_overrides"]
1989
- overrides = data["dependency_overrides"] = {} unless overrides.is_a?(Hash)
1990
- overrides["flet"] = flet_dependency
1991
- end
1992
-
1993
1420
  def template_client_pubspec_dependencies
1994
1421
  template_root =
1995
1422
  if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)