ruflet 0.0.16 → 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)
@@ -738,7 +585,13 @@ module Ruflet
738
585
  end
739
586
  return {} unless File.file?(config_path)
740
587
 
741
- YAML.safe_load(File.read(config_path), aliases: true) || {}
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")
593
+ end
594
+ config
742
595
  rescue StandardError => e
743
596
  warn "Failed to load ruflet config: #{e.class}: #{e.message}"
744
597
  {}
@@ -871,7 +724,6 @@ module Ruflet
871
724
  apply_web_metadata(client_dir, metadata)
872
725
  apply_windows_metadata(client_dir, metadata)
873
726
  apply_linux_metadata(client_dir, metadata)
874
- apply_dart_metadata(client_dir, metadata)
875
727
  build_log(
876
728
  verbose,
877
729
  "app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
@@ -949,6 +801,16 @@ module Ruflet
949
801
  %( applicationId = "#{metadata[:android_application_id]}")
950
802
  )
951
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
+
952
814
  manifest_path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
953
815
  replace_in_file(
954
816
  manifest_path,
@@ -1067,31 +929,6 @@ module Ruflet
1067
929
  cmake_path = File.join(client_dir, "linux", "CMakeLists.txt")
1068
930
  replace_in_file(cmake_path, /^set\(BINARY_NAME ".*"\)$/, %(set(BINARY_NAME "#{metadata[:package_name]}")))
1069
931
  replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
1070
-
1071
- # The GTK runner hardcodes the window title (header-bar and fallback
1072
- # paths). Without this it shows the package name (e.g. "ruflet_client")
1073
- # instead of the configured app name. Match the call, not the literal,
1074
- # so re-runs stay idempotent.
1075
- my_application_path = File.join(client_dir, "linux", "runner", "my_application.cc")
1076
- title = c_string_escape(metadata[:display_name])
1077
- replace_in_file(
1078
- my_application_path,
1079
- /gtk_header_bar_set_title\(header_bar, "[^"]*"\)/,
1080
- %(gtk_header_bar_set_title(header_bar, "#{title}"))
1081
- )
1082
- replace_in_file(
1083
- my_application_path,
1084
- /gtk_window_set_title\(window, "[^"]*"\)/,
1085
- %(gtk_window_set_title(window, "#{title}"))
1086
- )
1087
- end
1088
-
1089
- def apply_dart_metadata(client_dir, metadata)
1090
- title = dart_single_quote_escape(metadata[:display_name])
1091
- client_entrypoint_paths(client_dir).each do |entrypoint|
1092
- replace_in_file(entrypoint, /title: 'Ruflet'/, "title: '#{title}'")
1093
- replace_in_file(entrypoint, /AppBar\(title: const Text\('Ruflet'\)\)/, "AppBar(title: const Text('#{title}'))")
1094
- end
1095
932
  end
1096
933
 
1097
934
  def replace_plist_value(path, key, value)
@@ -1176,31 +1013,18 @@ module Ruflet
1176
1013
  value.to_s.gsub('"', '""')
1177
1014
  end
1178
1015
 
1179
- # Escape a value for embedding inside a C string literal (used for the
1180
- # GTK runner window title in my_application.cc). Block form avoids
1181
- # backslash interpretation in the gsub replacement string.
1182
- def c_string_escape(value)
1183
- value.to_s.gsub(/[\\"]/) { |ch| "\\#{ch}" }
1184
- end
1185
-
1186
- def dart_single_quote_escape(value)
1187
- value.to_s.gsub("\\", "\\\\\\").gsub("'", "\\\\'")
1188
- end
1189
-
1190
1016
  def key_defined?(hash, key)
1191
1017
  hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
1192
1018
  end
1193
1019
 
1194
1020
  def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
1195
- service_definitions = load_service_definitions(client_dir)
1196
- service_extension_keys = service_definitions.keys.flat_map { |key| Array(SERVICE_EXTENSION_MAP[key]) }.uniq
1197
- protected_extension_keys = SERVICE_EXTENSION_MAP.values.flatten.uniq
1198
- configured_extension_keys = Array(config["extensions"]).map { |value| normalize_extension_key(value) }.compact
1199
- 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
1200
1025
  extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
1201
1026
  extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
1202
1027
 
1203
- sync_client_flet_packages(client_dir, extension_packages)
1204
1028
  pubspec_path = File.join(client_dir, "pubspec.yaml")
1205
1029
  if File.file?(pubspec_path)
1206
1030
  sync_client_extension_dependencies(pubspec_path, extension_packages)
@@ -1210,208 +1034,67 @@ module Ruflet
1210
1034
  sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
1211
1035
  prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
1212
1036
  end
1213
- apply_service_native_requirements(client_dir, service_definitions.keys, service_definitions)
1214
1037
  end
1215
1038
 
1216
- def apply_service_native_requirements(client_dir, extension_keys, service_definitions = load_service_definitions(client_dir))
1217
- service_requirements = service_native_requirements(service_definitions)
1218
- stale_keys = service_requirements.keys - extension_keys
1219
- remove_service_native_requirements(client_dir, stale_keys, service_requirements)
1220
-
1221
- requirements = merge_service_native_requirements(extension_keys, service_requirements)
1222
- sync_ios_permission_definitions(
1223
- File.join(client_dir, "ios", "Podfile"),
1224
- Array(requirements[:ios_permission_definitions])
1225
- )
1226
- return if requirements.empty?
1227
-
1228
- android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1229
- Array(requirements[:android_permissions]).each do |permission|
1230
- ensure_android_permission(android_manifest, permission)
1231
- 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
1232
1046
 
1233
- ios_info = File.join(client_dir, "ios", "Runner", "Info.plist")
1234
- Hash(requirements[:ios_info]).each do |key, value|
1235
- ensure_plist_string(ios_info, key, value)
1236
- end
1237
-
1238
- macos_info = File.join(client_dir, "macos", "Runner", "Info.plist")
1239
- Hash(requirements[:macos_info]).each do |key, value|
1240
- ensure_plist_string(macos_info, key, value)
1241
- end
1242
-
1243
- %w[DebugProfile Release].each do |name|
1244
- entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
1245
- Hash(requirements[:macos_entitlements]).each do |key, value|
1246
- ensure_plist_boolean(entitlements_path, key, value)
1247
- end
1248
- end
1249
- end
1250
-
1251
- def remove_service_native_requirements(client_dir, extension_keys, service_requirements = service_native_requirements(load_service_definitions(client_dir)))
1252
- requirements = merge_service_native_requirements(extension_keys, service_requirements)
1253
- return if requirements.empty?
1254
-
1255
- android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1256
- Array(requirements[:android_permissions]).each do |permission|
1257
- remove_android_permission(android_manifest, permission)
1258
- end
1259
-
1260
- [File.join(client_dir, "ios", "Runner", "Info.plist"), File.join(client_dir, "macos", "Runner", "Info.plist")].each do |path|
1261
- (Hash(requirements[:ios_info]).keys + Hash(requirements[:macos_info]).keys).uniq.each do |key|
1262
- remove_plist_entry(path, key)
1263
- end
1264
- end
1265
-
1266
- %w[DebugProfile Release].each do |name|
1267
- entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
1268
- Hash(requirements[:macos_entitlements]).each_key do |key|
1269
- remove_plist_entry(entitlements_path, key)
1270
- end
1271
- end
1272
- end
1273
-
1274
- def merge_service_native_requirements(extension_keys, service_requirements = DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
1275
- extension_keys.each_with_object({}) do |key, memo|
1276
- requirements = service_requirements[key]
1277
- next unless requirements
1278
-
1279
- memo[:android_permissions] ||= []
1280
- memo[:android_permissions] |= Array(requirements[:android_permissions])
1281
- memo[:ios_permission_definitions] ||= []
1282
- memo[:ios_permission_definitions] |= Array(requirements[:ios_permission_definitions])
1283
- %i[ios_info macos_info macos_entitlements].each do |section|
1284
- memo[section] ||= {}
1285
- 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
1286
1052
  end
1287
1053
  end
1288
1054
  end
1289
1055
 
1290
- def load_service_definitions(client_dir = nil)
1291
- path = services_config_path(client_dir)
1292
- return {} unless path
1293
-
1294
- data = YAML.safe_load(File.read(path), aliases: true) || {}
1295
- Array(data["services"]).each_with_object({}) do |entry, memo|
1296
- name, metadata =
1297
- if entry.is_a?(Hash)
1298
- entry.first
1299
- else
1300
- [entry, {}]
1301
- end
1302
- key = normalize_extension_key(name)
1303
- memo[key] = metadata.is_a?(Hash) ? metadata : {} if key
1304
- end
1305
- rescue Psych::Exception => e
1306
- warn "Could not parse #{path}: #{e.message}"
1307
- {}
1308
- end
1309
-
1310
- def services_config_path(client_dir = nil)
1311
- candidates = [ENV["RUFLET_SERVICES"], File.expand_path("services.yaml", Dir.pwd)]
1312
- candidates << File.join(client_dir, "services.yaml") if client_dir
1313
- candidates.compact.find { |path| File.file?(path) }
1314
- end
1056
+ def apply_native_service_permissions(client_dir, config)
1057
+ entries = configured_service_entries(config)
1058
+ return if entries.empty?
1315
1059
 
1316
- def service_native_requirements(service_definitions)
1317
- all_keys = (DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys | service_definitions.keys)
1318
- all_keys.each_with_object({}) do |key, memo|
1319
- defaults = DEFAULT_SERVICE_NATIVE_REQUIREMENTS[key] || {}
1320
- metadata = service_definitions[key] || {}
1321
- native = metadata["native"].is_a?(Hash) ? metadata["native"] : metadata
1322
- memo[key] = {
1323
- android_permissions: Array(native["android_permissions"] || defaults[:android_permissions]),
1324
- ios_permission_definitions: Array(native["ios_permission_definitions"] || defaults[:ios_permission_definitions]),
1325
- ios_info: native["ios_info"].is_a?(Hash) ? native["ios_info"] : (defaults[:ios_info] || {}),
1326
- macos_info: native["macos_info"].is_a?(Hash) ? native["macos_info"] : (defaults[:macos_info] || {}),
1327
- macos_entitlements: native["macos_entitlements"].is_a?(Hash) ? native["macos_entitlements"] : (defaults[:macos_entitlements] || {})
1328
- }
1329
- end
1060
+ apply_android_service_permissions(client_dir, entries)
1061
+ apply_ios_service_usage_descriptions(client_dir, entries)
1330
1062
  end
1331
1063
 
1332
- def sync_ios_permission_definitions(path, definitions)
1064
+ def apply_android_service_permissions(client_dir, entries)
1065
+ path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
1333
1066
  return unless File.file?(path)
1334
1067
 
1335
1068
  content = File.read(path)
1336
- marker_pattern = %r{\n?\s*# BEGIN RUFLET PERMISSION DEFINITIONS.*?# END RUFLET PERMISSION DEFINITIONS\n?}m
1337
- updated = content.gsub(marker_pattern, "\n")
1338
- definitions = Array(definitions).map(&:to_s).reject(&:empty?).uniq.sort
1339
- if definitions.any?
1340
- block = <<~RUBY.chomp
1341
- # BEGIN RUFLET PERMISSION DEFINITIONS
1342
- target.build_configurations.each do |config|
1343
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
1344
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] += #{definitions.inspect}
1345
- end
1346
- # END RUFLET PERMISSION DEFINITIONS
1347
- RUBY
1348
- updated = updated.sub(
1349
- /^(\s*)flutter_additional_ios_build_settings\(target\)\s*$/,
1350
- "\\0\n\n#{block}"
1351
- )
1352
- end
1353
- File.write(path, updated) unless updated == content
1354
- end
1069
+ entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission|
1070
+ next if content.include?(%(android:name="#{permission}"))
1355
1071
 
1356
- def ensure_android_permission(path, permission)
1357
- return unless File.file?(path)
1358
-
1359
- content = File.read(path)
1360
- return if content.include?(permission)
1361
-
1362
- permission_line = %( <uses-permission android:name="#{xml_escape(permission)}"/>\n)
1363
- updated = content.sub(/(<manifest\b[^>]*>\s*)/m) { "#{Regexp.last_match(1)}#{permission_line}" }
1364
- 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)
1365
1075
  end
1366
1076
 
1367
- 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")
1368
1079
  return unless File.file?(path)
1369
1080
 
1370
1081
  content = File.read(path)
1371
- updated = content.gsub(%r{^\s*<uses-permission\s+android:name="#{Regexp.escape(permission)}"\s*/>\s*\n?}, "")
1372
- File.write(path, updated) unless updated == content
1373
- end
1374
-
1375
- def ensure_plist_string(path, key, value)
1376
- ensure_plist_entry(path, key, "<string>#{xml_escape(value)}</string>")
1377
- end
1082
+ entries.each do |entry|
1083
+ key = IOS_SERVICE_USAGE_KEYS[entry[:name]]
1084
+ next unless key
1378
1085
 
1379
- def ensure_plist_boolean(path, key, value)
1380
- ensure_plist_entry(path, key, value ? "<true/>" : "<false/>")
1381
- end
1382
-
1383
- def ensure_plist_entry(path, key, value_xml)
1384
- 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"
1385
1090
 
1386
- content = File.read(path)
1387
- return if content.include?("<key>#{key}</key>")
1388
-
1389
- entry = "\t<key>#{key}</key>\n\t#{value_xml}\n"
1390
-
1391
- # Insert into the ROOT <dict> (opened right after <plist ...>), never the
1392
- # first nested </dict>. Modern Flutter Info.plists nest a dict inside
1393
- # UIApplicationSceneManifest, so subbing the first </dict> would bury
1394
- # top-level keys (e.g. NS*UsageDescription) where iOS can't read them —
1395
- # which crashes the app the moment a permission is requested.
1396
- root_open = content.match(%r{<plist\b[^>]*>\s*<dict>}m)
1397
- updated =
1398
- if root_open
1399
- insert_at = root_open.end(0)
1400
- content[0...insert_at] + "\n#{entry}" + content[insert_at..]
1401
- elsif (last = content.rindex("</dict>"))
1402
- 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>")
1403
1093
  else
1404
- "#{content}\n#{entry}"
1094
+ content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
1405
1095
  end
1406
- File.write(path, updated)
1407
- end
1408
-
1409
- def remove_plist_entry(path, key)
1410
- return unless File.file?(path)
1411
-
1412
- content = File.read(path)
1413
- updated = content.gsub(%r{\s*<key>#{Regexp.escape(key)}</key>\s*<(?:string>.*?</string|true/|false/)>\s*}m, "\n")
1414
- File.write(path, updated) unless updated == content
1096
+ end
1097
+ File.write(path, content)
1415
1098
  end
1416
1099
 
1417
1100
  def clear_flutter_build_state(client_dir, verbose: false)
@@ -1455,6 +1138,7 @@ module Ruflet
1455
1138
  remove_self_contained_project_assets(client_dir, verbose: verbose)
1456
1139
  remove_local_ruby_runtime_override(client_dir, verbose: verbose)
1457
1140
  end
1141
+ true
1458
1142
  end
1459
1143
 
1460
1144
  def sync_client_pubspec_for_runtime_mode(client_dir, self_contained:)
@@ -1464,23 +1148,24 @@ module Ruflet
1464
1148
  data = YAML.safe_load(File.read(pubspec_path), aliases: true) || {}
1465
1149
  dependencies = data["dependencies"]
1466
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
1467
1153
  flutter = data["flutter"]
1468
1154
  flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
1469
1155
  assets = Array(flutter["assets"]).map(&:to_s)
1470
- project_asset_prefix = "assets/#{self_contained_project_name}/"
1471
- assets.reject! { |asset| asset.start_with?(project_asset_prefix) }
1472
1156
 
1473
1157
  if self_contained
1474
1158
  dependencies["ruby_runtime"] = ruby_runtime_dependency(dependencies["ruby_runtime"])
1475
- assets.delete("assets/main.rb")
1476
- assets.delete("assets/ruby_project/")
1477
- project_asset_relative_paths.each do |relative_path|
1478
- 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)
1479
1164
  end
1480
1165
  else
1481
1166
  dependencies.delete("ruby_runtime")
1482
- assets.delete("assets/main.rb")
1483
- 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) }
1484
1169
  end
1485
1170
 
1486
1171
  flutter["assets"] = assets unless assets.empty?
@@ -1488,46 +1173,11 @@ module Ruflet
1488
1173
  write_pubspec_yaml(pubspec_path, data)
1489
1174
  end
1490
1175
 
1491
- RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.6"
1492
-
1493
1176
  def ruby_runtime_dependency(current_dependency = nil)
1494
- 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
1495
1178
  return { "path" => local_path } if local_path
1496
1179
 
1497
- template_dependency = template_client_pubspec_dependencies["ruby_runtime"]
1498
- return template_dependency if usable_ruby_runtime_dependency?(template_dependency)
1499
-
1500
- return current_dependency if usable_ruby_runtime_dependency?(current_dependency)
1501
-
1502
- RUBY_RUNTIME_FALLBACK_REQUIREMENT
1503
- end
1504
-
1505
- # In a ruflet repo checkout the plugin lives next to templates/; build
1506
- # against it so framework changes are exercised without publishing.
1507
- def repo_checkout_ruby_runtime_path
1508
- template_root =
1509
- if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1510
- Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1511
- end
1512
- return nil unless template_root
1513
-
1514
- candidate = File.expand_path(File.join(template_root, "..", "..", "ruby_runtime"))
1515
- File.file?(File.join(candidate, "pubspec.yaml")) ? candidate : nil
1516
- end
1517
-
1518
- # Relative path dependencies only resolve from the directory the
1519
- # template was authored in — never copy them into a user's client.
1520
- def usable_ruby_runtime_dependency?(dependency)
1521
- case dependency
1522
- when nil then false
1523
- when Hash
1524
- path = dependency["path"] || dependency[:path]
1525
- return true if path.nil? # hosted/git table form
1526
-
1527
- Pathname.new(path.to_s).absolute? && File.file?(File.join(path, "pubspec.yaml"))
1528
- else
1529
- !dependency.to_s.strip.empty?
1530
- end
1180
+ current_dependency || "^0.0.3"
1531
1181
  end
1532
1182
 
1533
1183
  def explicit_local_ruby_runtime_path
@@ -1540,6 +1190,13 @@ module Ruflet
1540
1190
  nil
1541
1191
  end
1542
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
+
1543
1200
  def refresh_managed_client_template_files(client_dir, verbose: false)
1544
1201
  template_root =
1545
1202
  if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
@@ -1551,12 +1208,12 @@ module Ruflet
1551
1208
  "lib/main.dart",
1552
1209
  "lib/main.self.dart",
1553
1210
  "lib/main.server.dart",
1211
+ "lib/ruflet_file_picker_service.dart",
1212
+ "lib/ruflet_spinkit.dart",
1554
1213
  "lib/connection_probe.dart",
1555
1214
  "lib/connection_probe_io.dart",
1556
1215
  "lib/connection_probe_stub.dart",
1557
- "lib/ruflet_file_picker_service.dart",
1558
- "macos/Runner/DebugProfile.entitlements",
1559
- "macos/Runner/Release.entitlements"
1216
+ "ios/Podfile"
1560
1217
  ]
1561
1218
 
1562
1219
  managed_files.each do |relative_path|
@@ -1568,27 +1225,6 @@ module Ruflet
1568
1225
  FileUtils.cp(source, destination)
1569
1226
  build_log(verbose, "refreshed template file #{relative_path}")
1570
1227
  end
1571
-
1572
- repair_legacy_self_contained_bootstrap(client_dir, verbose: verbose)
1573
- end
1574
-
1575
- def repair_legacy_self_contained_bootstrap(client_dir, verbose: false)
1576
- path = File.join(client_dir, "lib", "main.self.dart")
1577
- return unless File.file?(path)
1578
-
1579
- content = File.read(path)
1580
- updated = content.gsub(
1581
- /^\s*await RubyRuntime\.eval\("ENV\['RUFLET_DEBUG'\].*?\n/,
1582
- ""
1583
- )
1584
- updated = updated.gsub(
1585
- /^\s*final digestLength = await RubyRuntime\.eval\(\n.*?^\s*\);\n\s*debugPrint\('Embedded Digest::SHA1 bytesize: \$digestLength'\);\n/m,
1586
- ""
1587
- )
1588
- return if updated == content
1589
-
1590
- File.write(path, updated)
1591
- build_log(verbose, "removed legacy pre-server RubyRuntime.eval diagnostics")
1592
1228
  end
1593
1229
 
1594
1230
  def write_pubspec_yaml(path, data)
@@ -1613,17 +1249,23 @@ module Ruflet
1613
1249
  end.join
1614
1250
  end
1615
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
+
1616
1262
  def sync_self_contained_project_assets(client_dir, verbose: false)
1617
1263
  project_root = Pathname.new(Dir.pwd)
1618
1264
  assets_root = File.join(client_dir, "assets")
1619
1265
  destination_root = File.join(assets_root, self_contained_project_name)
1620
1266
  FileUtils.rm_rf(destination_root)
1621
- FileUtils.rm_rf(File.join(assets_root, "ruby_project"))
1622
1267
  FileUtils.mkdir_p(destination_root)
1623
1268
 
1624
- legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
1625
- FileUtils.rm_f(legacy_entrypoint)
1626
-
1627
1269
  copied = 0
1628
1270
  project_asset_relative_paths.each do |relative_path|
1629
1271
  source = project_root.join(relative_path)
@@ -1631,104 +1273,15 @@ module Ruflet
1631
1273
 
1632
1274
  destination = File.join(destination_root, relative_path)
1633
1275
  FileUtils.mkdir_p(File.dirname(destination))
1634
- copy_project_asset_file(source.to_s, destination, project_root: project_root.to_s)
1276
+ FileUtils.cp(source.to_s, destination)
1635
1277
  copied += 1
1636
1278
  end
1637
1279
 
1638
1280
  build_log(verbose, "copied #{copied} project file#{copied == 1 ? '' : 's'} to assets/#{self_contained_project_name}")
1639
1281
  end
1640
1282
 
1641
- def copy_project_asset_file(source, destination, project_root: nil)
1642
- if File.extname(source).downcase == ".rb"
1643
- ruby_source =
1644
- if File.basename(source) == "main.rb" && project_root
1645
- bundled_embedded_ruby_source(source, project_root)
1646
- else
1647
- File.read(source)
1648
- end
1649
- File.write(destination, normalize_embedded_ruby_source(ruby_source))
1650
- else
1651
- FileUtils.cp(source, destination)
1652
- end
1653
- end
1654
-
1655
- def bundled_embedded_ruby_source(source, project_root, visited = {})
1656
- absolute = File.expand_path(source)
1657
- return "" if visited[absolute]
1658
-
1659
- visited[absolute] = true
1660
- base = File.dirname(absolute)
1661
- File.read(absolute).lines.map do |line|
1662
- match = line.match(/^\s*require_relative\s+["']([^"']+)["']\s*$/)
1663
- next line unless match
1664
-
1665
- required = File.expand_path(match[1], base)
1666
- required = "#{required}.rb" unless File.file?(required)
1667
- next line unless File.file?(required) && required.start_with?(File.expand_path(project_root))
1668
-
1669
- bundled_embedded_ruby_source(required, project_root, visited)
1670
- end.join
1671
- end
1672
-
1673
- def normalize_embedded_ruby_source(source)
1674
- source.lines.map do |line|
1675
- expand_endless_method_line(line)
1676
- end.join
1677
- end
1678
-
1679
- def expand_endless_method_line(line)
1680
- match = line.match(/^(\s*)def\s+(.+)$/)
1681
- return line unless match
1682
-
1683
- indent = match[1]
1684
- body = match[2].chomp
1685
- newline = line.end_with?("\n") ? "\n" : ""
1686
- split = endless_method_split(body)
1687
- return line unless split
1688
-
1689
- signature, expression = split
1690
- "#{indent}def #{signature.rstrip}\n#{indent} #{expression.lstrip}\n#{indent}end#{newline}"
1691
- end
1692
-
1693
- def endless_method_split(body)
1694
- depth = 0
1695
- quote = nil
1696
- escape = false
1697
-
1698
- body.each_char.with_index do |char, index|
1699
- if quote
1700
- escape = char == "\\" && !escape
1701
- if char == quote && !escape
1702
- quote = nil
1703
- elsif char != "\\"
1704
- escape = false
1705
- end
1706
- next
1707
- end
1708
-
1709
- case char
1710
- when "'", '"'
1711
- quote = char
1712
- when "(", "[", "{"
1713
- depth += 1
1714
- when ")", "]", "}"
1715
- depth -= 1 if depth.positive?
1716
- when "="
1717
- next unless depth.zero? && body[index + 1] == " "
1718
-
1719
- signature = body[0...index]
1720
- expression = body[(index + 1)..]
1721
- return [signature, expression] unless signature.strip.empty? || expression.to_s.strip.empty?
1722
- end
1723
- end
1724
-
1725
- nil
1726
- end
1727
-
1728
1283
  def remove_self_contained_project_assets(client_dir, verbose: false)
1729
1284
  assets_root = File.join(client_dir, "assets")
1730
- legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
1731
- FileUtils.rm_f(legacy_entrypoint)
1732
1285
  removed = false
1733
1286
 
1734
1287
  project_root = File.join(assets_root, self_contained_project_name)
@@ -1737,12 +1290,6 @@ module Ruflet
1737
1290
  removed = true
1738
1291
  end
1739
1292
 
1740
- legacy_root = File.join(assets_root, "ruby_project")
1741
- if Dir.exist?(legacy_root)
1742
- FileUtils.rm_rf(legacy_root)
1743
- removed = true
1744
- end
1745
-
1746
1293
  build_log(verbose, "removed embedded self-contained project assets") if removed
1747
1294
  end
1748
1295
 
@@ -1753,11 +1300,12 @@ module Ruflet
1753
1300
  Find.find(root.to_s) do |path|
1754
1301
  pathname = Pathname.new(path)
1755
1302
  relative = pathname.relative_path_from(root).to_s
1756
- next if relative.empty?
1303
+ next if relative.empty? || relative == "."
1757
1304
 
1758
1305
  if pathname.directory?
1759
1306
  if skip_project_asset_directory?(relative)
1760
1307
  Find.prune
1308
+ next
1761
1309
  else
1762
1310
  next
1763
1311
  end
@@ -1778,8 +1326,7 @@ module Ruflet
1778
1326
  end
1779
1327
 
1780
1328
  def skip_project_asset_directory?(relative)
1781
- first = relative.split(File::SEPARATOR).first
1782
- %w[
1329
+ excluded_directories = %w[
1783
1330
  .git
1784
1331
  .bundle
1785
1332
  .dart_tool
@@ -1794,21 +1341,17 @@ module Ruflet
1794
1341
  ruflet_client
1795
1342
  tmp
1796
1343
  vendor
1797
- ].include?(first)
1344
+ ]
1345
+ relative.split(File::SEPARATOR).any? do |component|
1346
+ component.start_with?(".") || excluded_directories.include?(component)
1347
+ end
1798
1348
  end
1799
1349
 
1800
1350
  def include_project_asset_file?(relative)
1801
1351
  basename = File.basename(relative)
1352
+ return false if basename == ".DS_Store"
1802
1353
  return false if %w[Gemfile.lock pubspec.lock Podfile.lock package-lock.json yarn.lock pnpm-lock.yaml].include?(basename)
1803
- return true if %w[main.rb Gemfile ruflet.yaml ruflet.yml manifest.json].include?(basename)
1804
-
1805
- ext = File.extname(relative).downcase
1806
- return true if %w[.rb .json .yml .yaml].include?(ext)
1807
-
1808
- first = relative.split(File::SEPARATOR).first
1809
- return true if first == "assets"
1810
-
1811
- false
1354
+ true
1812
1355
  end
1813
1356
 
1814
1357
  def flutter_target_entrypoint(client_dir, self_contained:)
@@ -1858,50 +1401,6 @@ module Ruflet
1858
1401
  write_pubspec_yaml(path, data)
1859
1402
  end
1860
1403
 
1861
- def sync_client_flet_packages(client_dir, selected_packages)
1862
- template_root =
1863
- if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1864
- Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1865
- end
1866
- return unless template_root
1867
-
1868
- source_root = File.join(template_root, "flet_packages")
1869
- target_root = File.join(client_dir, "flet_packages")
1870
- return unless Dir.exist?(source_root)
1871
- return if File.expand_path(source_root) == File.expand_path(target_root)
1872
-
1873
- # The repository's standalone client intentionally carries the full
1874
- # local package catalog. Only generated/template-derived clients are
1875
- # conditioned for a particular Ruflet application.
1876
- return if standalone_ruflet_client_source?(client_dir, template_root)
1877
-
1878
- known_packages = CLIENT_EXTENSION_MAP.values.map { |meta| meta.fetch(:package) }.uniq
1879
- required_packages = (["flet"] + selected_packages).uniq
1880
-
1881
- FileUtils.mkdir_p(target_root)
1882
- required_packages.each do |package_name|
1883
- source = File.join(source_root, package_name)
1884
- target = File.join(target_root, package_name)
1885
- next unless Dir.exist?(source)
1886
- next if Dir.exist?(target)
1887
-
1888
- FileUtils.cp_r(source, target)
1889
- end
1890
-
1891
- (known_packages - selected_packages).each do |package_name|
1892
- FileUtils.rm_rf(File.join(target_root, package_name))
1893
- end
1894
- end
1895
-
1896
- def standalone_ruflet_client_source?(client_dir, template_root)
1897
- client_root = File.expand_path(client_dir)
1898
- candidates = [
1899
- File.expand_path("../../ruflet_client", template_root),
1900
- File.expand_path("../../../../../ruflet_client", __dir__)
1901
- ]
1902
- candidates.any? { |candidate| File.expand_path(candidate) == client_root }
1903
- end
1904
-
1905
1404
  def sync_client_extension_dependencies(path, selected_packages)
1906
1405
  return if selected_packages.empty?
1907
1406
 
@@ -1910,30 +1409,14 @@ module Ruflet
1910
1409
 
1911
1410
  data = YAML.safe_load(File.read(path), aliases: true) || {}
1912
1411
  deps = (data["dependencies"] || {}).dup
1913
- if selected_packages.include?("flet_rive")
1914
- deps.delete("rive")
1915
- deps.delete("rive_native")
1916
- end
1917
1412
  selected_packages.each do |package_name|
1918
1413
  deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
1919
1414
  end
1920
1415
 
1921
1416
  data["dependencies"] = deps
1922
- sync_local_flet_dependency_override(data, deps["flet"])
1923
1417
  write_pubspec_yaml(path, data)
1924
1418
  end
1925
1419
 
1926
- # Git-backed Flet extensions declare their own Flet source. When Ruflet
1927
- # vendors the core engine locally, force every extension to resolve that
1928
- # same copy instead of letting Pub reject the mixed sources.
1929
- def sync_local_flet_dependency_override(data, flet_dependency)
1930
- return unless flet_dependency.is_a?(Hash) && key_defined?(flet_dependency, "path")
1931
-
1932
- overrides = data["dependency_overrides"]
1933
- overrides = data["dependency_overrides"] = {} unless overrides.is_a?(Hash)
1934
- overrides["flet"] = flet_dependency
1935
- end
1936
-
1937
1420
  def template_client_pubspec_dependencies
1938
1421
  template_root =
1939
1422
  if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)