ruflet 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd06c38b3f93c8ae4197f120eeda91d388b2a25114ca4d8a5df37f40a499ee95
4
- data.tar.gz: ba86fc65fd65202ed745c019f64a19a8ebfd6c759348ea7a6d2c165e98897bdb
3
+ metadata.gz: 877c730a76717b1a9623d285666db0cb3b5a47fb08eafe0e9d3bb49c681c0b88
4
+ data.tar.gz: a649878ca4f0a7fc8d752e6ba2e0f3eaa1963213fa96dfee3f4a52d2905e4b9a
5
5
  SHA512:
6
- metadata.gz: 1898d117b21f34c326a38da7a2b57db1902874839a3bf954d37090f620d0650589edf2ab47a493cbb71a9d8e5643f54a5e59c17b57b86624517b4bcec9e038e9
7
- data.tar.gz: aee1a5ae587c543fa957fd2a10254b3cd7e06069bc3525862750d2445aa17c9340fa0fbcc26c9b1fe9e86d6ad8ab91274a13991696f15794a5a69b4c20b5092d
6
+ metadata.gz: a1a15a38fa5973a84e921d3b57fd54a135d5d4090fdd996294991f0fede09a89e1553037388f7a07a9dace52b9b4e753273d4da2b0940ca6f326344b708b0de2
7
+ data.tar.gz: d10353a23bb436679d2a82226f490ad7c2ef1d470b33808ff3d523a94432f871982a1ed998a2c9fc80a9321ed07fbf2ad8ba23633dbe492e2ebc20c1b03fea96
@@ -46,6 +46,7 @@ module Ruflet
46
46
  return 1
47
47
  end
48
48
 
49
+ ensure_ruflet_build_assets(verbose: !!verbose)
49
50
  client_dir = ensure_flutter_client_dir(verbose: !!verbose)
50
51
  unless client_dir
51
52
  warn "Could not find Flutter client directory."
@@ -340,6 +341,15 @@ module Ruflet
340
341
  Dir.exist?(target) ? target : nil
341
342
  end
342
343
 
344
+ def ensure_ruflet_build_assets(force: false, verbose: false)
345
+ return true unless respond_to?(:download_ruflet_assets, true)
346
+
347
+ !!send(:download_ruflet_assets, force: force, verbose: verbose)
348
+ rescue StandardError => e
349
+ build_log(verbose, "ruflet asset bootstrap skipped: #{e.class}: #{e.message}")
350
+ false
351
+ end
352
+
343
353
  def hidden_flutter_client_dir(root = Dir.pwd)
344
354
  File.join(root, "build", "client")
345
355
  end
@@ -352,6 +362,7 @@ module Ruflet
352
362
  refresh_managed_client_template_files(client_dir, verbose: verbose)
353
363
  sync_client_metadata(client_dir, config, verbose: verbose)
354
364
  configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
365
+ @ruflet_self_contained_build = self_contained
355
366
  apply_service_extension_config(client_dir, config)
356
367
  asset_flags = apply_build_config(client_dir, config)
357
368
  if asset_flags[:error]
@@ -361,6 +372,9 @@ module Ruflet
361
372
  announce_asset_configuration(asset_flags)
362
373
  clear_flutter_build_state(client_dir, verbose: verbose)
363
374
  clear_stale_platform_outputs(client_dir, platform, verbose: verbose)
375
+ unless ensure_flutter_platform_artifacts(client_dir, platform, tools[:env], tools[:flutter], verbose: verbose)
376
+ return false
377
+ end
364
378
  build_note("Resolving Flutter packages")
365
379
  build_log(verbose, "running flutter pub get")
366
380
  unless run_external_command(tools[:env], tools[:flutter], "pub", "get", chdir: client_dir, unbundled: true)
@@ -393,6 +407,38 @@ module Ruflet
393
407
  true
394
408
  end
395
409
 
410
+ def ensure_flutter_platform_artifacts(client_dir, platform, env, flutter, verbose: false)
411
+ precache_flags = flutter_precache_flags(platform)
412
+ return true if precache_flags.empty?
413
+
414
+ build_note("Preparing Flutter #{platform} platform artifacts")
415
+ build_log(verbose, "running flutter precache #{precache_flags.join(' ')}")
416
+ ok = run_external_command(env, flutter, "precache", *precache_flags, chdir: client_dir, unbundled: true)
417
+ return true if ok
418
+
419
+ warn "Flutter platform artifact setup failed for #{platform}"
420
+ false
421
+ end
422
+
423
+ def flutter_precache_flags(platform)
424
+ case platform
425
+ when "apk", "android", "aab", "appbundle"
426
+ ["--android"]
427
+ when "ios"
428
+ ["--ios"]
429
+ when "macos"
430
+ ["--macos"]
431
+ when "windows"
432
+ ["--windows"]
433
+ when "linux"
434
+ ["--linux"]
435
+ when "web"
436
+ ["--web"]
437
+ else
438
+ []
439
+ end
440
+ end
441
+
396
442
  def ensure_native_build_dependencies(client_dir, platform, env, verbose: false)
397
443
  case platform
398
444
  when "ios"
@@ -430,15 +476,7 @@ module Ruflet
430
476
  end
431
477
 
432
478
  def unbundled_command_env(env)
433
- sanitized_env = env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" }
434
- cleared_env = {}
435
- ENV.each_key do |key|
436
- next unless key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_")
437
-
438
- cleared_env[key] = nil
439
- end
440
-
441
- cleared_env.merge(sanitized_env)
479
+ env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_") }
442
480
  end
443
481
 
444
482
  def run_external_command(env, *cmd, chdir:, unbundled: false)
@@ -654,6 +692,7 @@ module Ruflet
654
692
  apply_web_metadata(client_dir, metadata)
655
693
  apply_windows_metadata(client_dir, metadata)
656
694
  apply_linux_metadata(client_dir, metadata)
695
+ apply_dart_metadata(client_dir, metadata)
657
696
  build_log(
658
697
  verbose,
659
698
  "app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
@@ -851,6 +890,14 @@ module Ruflet
851
890
  replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
852
891
  end
853
892
 
893
+ def apply_dart_metadata(client_dir, metadata)
894
+ title = dart_single_quote_escape(metadata[:display_name])
895
+ client_entrypoint_paths(client_dir).each do |entrypoint|
896
+ replace_in_file(entrypoint, /title: 'Ruflet'/, "title: '#{title}'")
897
+ replace_in_file(entrypoint, /AppBar\(title: const Text\('Ruflet'\)\)/, "AppBar(title: const Text('#{title}'))")
898
+ end
899
+ end
900
+
854
901
  def replace_plist_value(path, key, value)
855
902
  return unless File.file?(path)
856
903
 
@@ -866,7 +913,7 @@ module Ruflet
866
913
  return unless File.file?(path)
867
914
 
868
915
  content = File.read(path)
869
- updated = content.gsub(pattern, replacement)
916
+ updated = content.gsub(pattern) { replacement }
870
917
  File.write(path, updated) unless updated == content
871
918
  end
872
919
 
@@ -933,19 +980,27 @@ module Ruflet
933
980
  value.to_s.gsub('"', '""')
934
981
  end
935
982
 
983
+ def dart_single_quote_escape(value)
984
+ value.to_s.gsub("\\", "\\\\\\").gsub("'", "\\\\'")
985
+ end
986
+
936
987
  def key_defined?(hash, key)
937
988
  hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
938
989
  end
939
990
 
940
- def apply_service_extension_config(client_dir, config = {})
991
+ def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
941
992
  services = Array(config["services"])
942
993
  extension_keys = services.map { |v| normalize_extension_key(v) }.compact.uniq
943
994
  extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
944
995
  extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
945
996
 
946
997
  pubspec_path = File.join(client_dir, "pubspec.yaml")
947
- prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path)
998
+ if File.file?(pubspec_path)
999
+ sync_client_extension_dependencies(pubspec_path, extension_packages)
1000
+ prune_client_pubspec(pubspec_path, extension_packages)
1001
+ end
948
1002
  client_entrypoint_paths(client_dir).each do |entrypoint|
1003
+ sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
949
1004
  prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
950
1005
  end
951
1006
  end
@@ -1005,7 +1060,7 @@ module Ruflet
1005
1060
  assets = Array(flutter["assets"]).map(&:to_s)
1006
1061
 
1007
1062
  if self_contained
1008
- dependencies["ruby_runtime"] = "^0.0.3"
1063
+ dependencies["ruby_runtime"] = ruby_runtime_dependency(dependencies["ruby_runtime"])
1009
1064
  assets.delete("assets/main.rb")
1010
1065
  assets.delete("assets/ruby_project/")
1011
1066
  project_asset_path = "assets/#{self_contained_project_name}/"
@@ -1022,6 +1077,23 @@ module Ruflet
1022
1077
  write_pubspec_yaml(pubspec_path, data)
1023
1078
  end
1024
1079
 
1080
+ def ruby_runtime_dependency(current_dependency = nil)
1081
+ local_path = explicit_local_ruby_runtime_path
1082
+ return { "path" => local_path } if local_path
1083
+
1084
+ current_dependency || "^0.0.3"
1085
+ end
1086
+
1087
+ def explicit_local_ruby_runtime_path
1088
+ env_path = ENV["RUFLET_RUBY_RUNTIME_PATH"].to_s.strip
1089
+ return nil if env_path.empty?
1090
+
1091
+ candidate = Pathname.new(env_path).expand_path
1092
+ return candidate.to_s if candidate.join("pubspec.yaml").file?
1093
+
1094
+ nil
1095
+ end
1096
+
1025
1097
  def refresh_managed_client_template_files(client_dir, verbose: false)
1026
1098
  template_root =
1027
1099
  if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
@@ -1035,7 +1107,10 @@ module Ruflet
1035
1107
  "lib/main.server.dart",
1036
1108
  "lib/connection_probe.dart",
1037
1109
  "lib/connection_probe_io.dart",
1038
- "lib/connection_probe_stub.dart"
1110
+ "lib/connection_probe_stub.dart",
1111
+ "lib/ruflet_file_picker_service.dart",
1112
+ "macos/Runner/DebugProfile.entitlements",
1113
+ "macos/Runner/Release.entitlements"
1039
1114
  ]
1040
1115
 
1041
1116
  managed_files.each do |relative_path|
@@ -1089,13 +1164,100 @@ module Ruflet
1089
1164
 
1090
1165
  destination = File.join(destination_root, relative_path)
1091
1166
  FileUtils.mkdir_p(File.dirname(destination))
1092
- FileUtils.cp(source.to_s, destination)
1167
+ copy_project_asset_file(source.to_s, destination, project_root: project_root.to_s)
1093
1168
  copied += 1
1094
1169
  end
1095
1170
 
1096
1171
  build_log(verbose, "copied #{copied} project file#{copied == 1 ? '' : 's'} to assets/#{self_contained_project_name}")
1097
1172
  end
1098
1173
 
1174
+ def copy_project_asset_file(source, destination, project_root: nil)
1175
+ if File.extname(source).downcase == ".rb"
1176
+ ruby_source =
1177
+ if File.basename(source) == "main.rb" && project_root
1178
+ bundled_embedded_ruby_source(source, project_root)
1179
+ else
1180
+ File.read(source)
1181
+ end
1182
+ File.write(destination, normalize_embedded_ruby_source(ruby_source))
1183
+ else
1184
+ FileUtils.cp(source, destination)
1185
+ end
1186
+ end
1187
+
1188
+ def bundled_embedded_ruby_source(source, project_root, visited = {})
1189
+ absolute = File.expand_path(source)
1190
+ return "" if visited[absolute]
1191
+
1192
+ visited[absolute] = true
1193
+ base = File.dirname(absolute)
1194
+ File.read(absolute).lines.map do |line|
1195
+ match = line.match(/^\s*require_relative\s+["']([^"']+)["']\s*$/)
1196
+ next line unless match
1197
+
1198
+ required = File.expand_path(match[1], base)
1199
+ required = "#{required}.rb" unless File.file?(required)
1200
+ next line unless File.file?(required) && required.start_with?(File.expand_path(project_root))
1201
+
1202
+ bundled_embedded_ruby_source(required, project_root, visited)
1203
+ end.join
1204
+ end
1205
+
1206
+ def normalize_embedded_ruby_source(source)
1207
+ source.lines.map do |line|
1208
+ expand_endless_method_line(line)
1209
+ end.join
1210
+ end
1211
+
1212
+ def expand_endless_method_line(line)
1213
+ match = line.match(/^(\s*)def\s+(.+)$/)
1214
+ return line unless match
1215
+
1216
+ indent = match[1]
1217
+ body = match[2].chomp
1218
+ newline = line.end_with?("\n") ? "\n" : ""
1219
+ split = endless_method_split(body)
1220
+ return line unless split
1221
+
1222
+ signature, expression = split
1223
+ "#{indent}def #{signature.rstrip}\n#{indent} #{expression.lstrip}\n#{indent}end#{newline}"
1224
+ end
1225
+
1226
+ def endless_method_split(body)
1227
+ depth = 0
1228
+ quote = nil
1229
+ escape = false
1230
+
1231
+ body.each_char.with_index do |char, index|
1232
+ if quote
1233
+ escape = char == "\\" && !escape
1234
+ if char == quote && !escape
1235
+ quote = nil
1236
+ elsif char != "\\"
1237
+ escape = false
1238
+ end
1239
+ next
1240
+ end
1241
+
1242
+ case char
1243
+ when "'", '"'
1244
+ quote = char
1245
+ when "(", "[", "{"
1246
+ depth += 1
1247
+ when ")", "]", "}"
1248
+ depth -= 1 if depth.positive?
1249
+ when "="
1250
+ next unless depth.zero? && body[index + 1] == " "
1251
+
1252
+ signature = body[0...index]
1253
+ expression = body[(index + 1)..]
1254
+ return [signature, expression] unless signature.strip.empty? || expression.to_s.strip.empty?
1255
+ end
1256
+ end
1257
+
1258
+ nil
1259
+ end
1260
+
1099
1261
  def remove_self_contained_project_assets(client_dir, verbose: false)
1100
1262
  assets_root = File.join(client_dir, "assets")
1101
1263
  legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
@@ -1226,7 +1388,94 @@ module Ruflet
1226
1388
  end
1227
1389
 
1228
1390
  data["dependencies"] = deps
1229
- File.write(path, YAML.dump(data))
1391
+ write_pubspec_yaml(path, data)
1392
+ end
1393
+
1394
+ def sync_client_extension_dependencies(path, selected_packages)
1395
+ return if selected_packages.empty?
1396
+
1397
+ template_deps = template_client_pubspec_dependencies
1398
+ return if template_deps.empty?
1399
+
1400
+ data = YAML.safe_load(File.read(path), aliases: true) || {}
1401
+ deps = (data["dependencies"] || {}).dup
1402
+ selected_packages.each do |package_name|
1403
+ deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
1404
+ end
1405
+
1406
+ data["dependencies"] = deps
1407
+ write_pubspec_yaml(path, data)
1408
+ end
1409
+
1410
+ def template_client_pubspec_dependencies
1411
+ template_root =
1412
+ if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1413
+ Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1414
+ end
1415
+ return {} unless template_root
1416
+
1417
+ pubspec_path = File.join(template_root, "pubspec.yaml")
1418
+ return {} unless File.file?(pubspec_path)
1419
+
1420
+ data = YAML.safe_load(File.read(pubspec_path), aliases: true) || {}
1421
+ deps = data["dependencies"]
1422
+ deps.is_a?(Hash) ? deps : {}
1423
+ rescue StandardError
1424
+ {}
1425
+ end
1426
+
1427
+ def sync_client_main_extensions(path, selected_aliases)
1428
+ return if selected_aliases.empty?
1429
+
1430
+ template_path = template_client_entrypoint_path(File.basename(path))
1431
+ return unless template_path
1432
+
1433
+ content = File.read(path)
1434
+ template = File.read(template_path)
1435
+
1436
+ selected_aliases.each do |extension_alias|
1437
+ import_line = template.lines.find { |line| line.match?(/\sas #{Regexp.escape(extension_alias)};\s*\z/) }
1438
+ extension_line = template.lines.find { |line| line.match?(/^\s*#{Regexp.escape(extension_alias)}\.Extension\(\),\s*$/) }
1439
+
1440
+ content = insert_missing_import(content, import_line) if import_line && !content.include?(import_line)
1441
+ content = insert_missing_extension(content, extension_line) if extension_line && !content.include?(extension_line)
1442
+ end
1443
+
1444
+ File.write(path, content)
1445
+ end
1446
+
1447
+ def template_client_entrypoint_path(name)
1448
+ template_root =
1449
+ if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
1450
+ Ruflet::CLI.send(:resolve_ruflet_client_template_root)
1451
+ end
1452
+ return nil unless template_root
1453
+
1454
+ path = File.join(template_root, "lib", name)
1455
+ File.file?(path) ? path : nil
1456
+ end
1457
+
1458
+ def insert_missing_import(content, import_line)
1459
+ lines = content.lines
1460
+ last_import_index = lines.rindex { |line| line.start_with?("import ") }
1461
+ if last_import_index
1462
+ lines.insert(last_import_index + 1, import_line)
1463
+ else
1464
+ lines.unshift(import_line)
1465
+ end
1466
+ lines.join
1467
+ end
1468
+
1469
+ def insert_missing_extension(content, extension_line)
1470
+ lines = content.lines
1471
+ marker_index = lines.index { |line| line.include?("// --FAT_CLIENT_START--") }
1472
+ if marker_index
1473
+ lines.insert(marker_index, extension_line)
1474
+ else
1475
+ list_index = lines.index { |line| line.include?("final extensions = <FletExtension>[") }
1476
+ lines.insert(list_index ? list_index + 1 : lines.length, extension_line)
1477
+ end
1478
+ lines.join
1230
1479
  end
1231
1480
 
1232
1481
  def prune_client_main(path, selected_aliases)
@@ -1296,7 +1545,7 @@ module Ruflet
1296
1545
  if in_block && !replaced
1297
1546
  out << "#{block_indent}#{key}: #{value}"
1298
1547
  end
1299
- File.write(path, out.join("\n"))
1548
+ File.write(path, indent_pubspec_sequences(out.join("\n")))
1300
1549
  end
1301
1550
 
1302
1551
  def flutter_build_command(platform)
@@ -23,7 +23,7 @@ module Ruflet
23
23
  if template_root
24
24
  puts " Template: #{template_root}"
25
25
  elsif fix
26
- template_root = ensure_cached_ruflet_client_template!(verbose: !!verbose)
26
+ template_root = ensure_cached_ruflet_client_template!(force: true, verbose: !!verbose)
27
27
  unless template_root
28
28
  warn " Template: missing"
29
29
  warn "Failed to fetch the Ruflet Flutter template from GitHub."
@@ -34,6 +34,7 @@ module Ruflet
34
34
  warn " Template: missing"
35
35
  warn "Run `ruflet doctor --fix` to fetch the Flutter template from GitHub."
36
36
  end
37
+ puts " Ruby runtime: pub.dev package"
37
38
  if fix
38
39
  tools = ensure_flutter!("doctor", client_dir: client_dir, auto_install: true)
39
40
  else
@@ -144,6 +145,15 @@ module Ruflet
144
145
 
145
146
  private
146
147
 
148
+ def resolve_ruby_runtime_root
149
+ env_path = ENV["RUFLET_RUBY_RUNTIME_PATH"].to_s.strip
150
+ candidates = []
151
+ candidates << File.expand_path(env_path) unless env_path.empty?
152
+ candidates << File.expand_path("../../../../../ruby_runtime", __dir__)
153
+ candidates << cached_ruby_runtime_root
154
+ candidates.find { |path| File.file?(File.join(path, "pubspec.yaml")) }
155
+ end
156
+
147
157
  def detect_client_dir
148
158
  env_dir = ENV["RUFLET_CLIENT_DIR"]
149
159
  return env_dir if env_dir && Dir.exist?(env_dir)
@@ -47,13 +47,25 @@ module Ruflet
47
47
  private
48
48
 
49
49
  def flutter_tools(client_dir: nil, auto_install: true)
50
- # Always use FVM so Flutter/Dart match pinned SDK.
50
+ # Prefer FVM when available so Flutter/Dart match a pinned SDK.
51
51
  fvm_tools = flutter_tools_via_fvm(client_dir: client_dir, auto_install: auto_install)
52
52
  return fvm_tools if fvm_tools
53
53
 
54
+ managed_tools = flutter_tools_via_managed_sdk(client_dir: client_dir, auto_install: auto_install)
55
+ return managed_tools if managed_tools
56
+
54
57
  nil
55
58
  end
56
59
 
60
+ def flutter_tools_via_managed_sdk(client_dir: nil, auto_install: true)
61
+ return nil unless auto_install
62
+
63
+ sdk_root = ensure_flutter_sdk_downloaded(client_dir: client_dir)
64
+ return nil unless sdk_root
65
+
66
+ tools_from_flutter_bin(File.join(sdk_root, "bin", flutter_executable_name))
67
+ end
68
+
57
69
  def flutter_tools_via_fvm(client_dir: nil, auto_install: true)
58
70
  version = desired_flutter_version(client_dir: client_dir)
59
71
  return nil if version.to_s.strip.empty?
@@ -226,6 +238,9 @@ module Ruflet
226
238
  template_client = File.expand_path("../../../../../templates/ruflet_flutter_template/.metadata", __dir__)
227
239
  candidates << repo_client
228
240
  candidates << template_client
241
+ if Ruflet::CLI.respond_to?(:cached_ruflet_client_template_root, true)
242
+ candidates << File.join(Ruflet::CLI.send(:cached_ruflet_client_template_root), ".metadata")
243
+ end
229
244
  candidates.find { |path| File.file?(path) }
230
245
  end
231
246
 
@@ -7,6 +7,9 @@ require "yaml"
7
7
  module Ruflet
8
8
  module CLI
9
9
  module NewCommand
10
+ TEMPLATE_REPO_URL = ENV.fetch("RUFLET_TEMPLATE_REPO_URL", "https://github.com/AdamMusa/ruflet-template.git")
11
+ RUNTIME_REPO_URL = ENV.fetch("RUFLET_RUNTIME_REPO_URL", "https://github.com/AdamMusa/ruflet.git")
12
+
10
13
  CLIENT_EXTENSION_MAP = {
11
14
  "ads" => { package: "flet_ads", alias: "ruflet_ads" },
12
15
  "audio" => { package: "flet_audio", alias: "ruflet_audio" },
@@ -74,40 +77,69 @@ module Ruflet
74
77
  end
75
78
 
76
79
  def resolve_ruflet_client_template_root
77
- repo_template = File.expand_path("../../../../../templates/ruflet_flutter_template", __dir__)
78
- return repo_template if Dir.exist?(repo_template)
80
+ [
81
+ File.expand_path("../../../../../templates/ruflet_flutter_template", __dir__)
82
+ ].each do |template|
83
+ return template if Dir.exist?(template)
84
+ end
79
85
 
80
86
  cached_template = cached_ruflet_client_template_root
81
87
  return cached_template if Dir.exist?(cached_template)
82
88
 
83
- fallback = File.expand_path("../../../../../ruflet_client", __dir__)
84
- return fallback if Dir.exist?(fallback)
89
+ [
90
+ File.expand_path("../../../ruflet_client", __dir__),
91
+ File.expand_path("../../../../../ruflet_client", __dir__)
92
+ ].each do |fallback|
93
+ return fallback if Dir.exist?(fallback)
94
+ end
85
95
 
86
96
  nil
87
97
  end
88
98
 
89
- def ensure_cached_ruflet_client_template!(verbose: false)
99
+ def ensure_cached_ruflet_client_template!(force: false, verbose: false)
90
100
  cached_template = cached_ruflet_client_template_root
91
- return cached_template if Dir.exist?(cached_template)
101
+ return cached_template if !force && Dir.exist?(cached_template)
102
+
103
+ download_ruflet_template(force: force, verbose: verbose)
104
+ Dir.exist?(cached_template) ? cached_template : nil
105
+ end
92
106
 
93
- download_ruflet_client_template(verbose: verbose)
107
+ def ensure_cached_ruby_runtime!(force: false, verbose: false)
108
+ nil
94
109
  end
95
110
 
96
111
  def cached_ruflet_client_template_root
97
112
  File.join(template_cache_root, "ruflet_flutter_template")
98
113
  end
99
114
 
115
+ def cached_ruby_runtime_root
116
+ File.join(cache_root, "ruby_runtime")
117
+ end
118
+
100
119
  def template_cache_root
101
- File.join(Dir.home, ".ruflet", "templates")
120
+ File.join(cache_root, "templates")
121
+ end
122
+
123
+ def cache_root
124
+ ENV.fetch("RUFLET_CACHE_DIR", File.join(Dir.home, ".ruflet"))
102
125
  end
103
126
 
104
- def download_ruflet_client_template(verbose: false)
127
+ def download_ruflet_assets(force: false, verbose: false)
128
+ template_target = cached_ruflet_client_template_root
129
+ return true if !force && Dir.exist?(template_target)
130
+
131
+ Dir.exist?(template_target) || download_ruflet_template(force: force, verbose: verbose)
132
+ end
133
+
134
+ def download_ruflet_template(force: false, verbose: false)
105
135
  target = cached_ruflet_client_template_root
136
+ return target if !force && Dir.exist?(target)
137
+
106
138
  FileUtils.mkdir_p(template_cache_root)
107
139
 
108
- Dir.mktmpdir("ruflet-template") do |tmp|
140
+ Dir.mktmpdir("ruflet-assets") do |tmp|
109
141
  repo_dir = File.join(tmp, "Ruflet")
110
- clone_cmd = ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", "https://github.com/AdamMusa/Ruflet.git", repo_dir]
142
+ clone_cmd = ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", TEMPLATE_REPO_URL, repo_dir]
111
143
  return nil unless run_template_command(clone_cmd, verbose: verbose)
112
144
  return nil unless run_template_command(["git", "-C", repo_dir, "sparse-checkout", "set", "templates/ruflet_flutter_template"], verbose: verbose)
113
145
 
@@ -164,7 +196,10 @@ module Ruflet
164
196
  backend_url: ""
165
197
 
166
198
  # Source of truth for Flutter client extensions/plugins.
167
- # Examples: camera, video, audio, flashlight, webview, map
199
+ # To test every extension, list all services:
200
+ # ads, audio, audio_recorder, camera, charts, code_editor, color_pickers,
201
+ # datatable2, flashlight, geolocator, lottie, map, permission_handler,
202
+ # secure_storage, video, webview
168
203
  services: []
169
204
 
170
205
  # Build assets configuration consumed by `ruflet build`.
@@ -12,6 +12,7 @@ require "uri"
12
12
  require "thread"
13
13
  require "io/console"
14
14
  require "time"
15
+ require "yaml"
15
16
 
16
17
  module Ruflet
17
18
  module CLI
@@ -285,7 +286,7 @@ module Ruflet
285
286
  end
286
287
 
287
288
  def launch_desktop_client(url)
288
- cmd = detect_desktop_client_command(url)
289
+ cmd = detect_project_desktop_client_command(url) || detect_desktop_client_command(url)
289
290
  unless cmd
290
291
  warn "Desktop client executable not found."
291
292
  warn "Set RUFLET_CLIENT_DIR to your client path."
@@ -306,6 +307,76 @@ module Ruflet
306
307
  []
307
308
  end
308
309
 
310
+ def detect_project_desktop_client_command(url)
311
+ return nil unless project_run_requires_managed_client?
312
+ return nil unless respond_to?(:ensure_flutter_client_dir, true)
313
+ return nil unless respond_to?(:prepare_flutter_client, true)
314
+ return nil unless respond_to?(:ensure_flutter!, true)
315
+
316
+ platform = host_platform_name
317
+ return nil unless %w[macos windows linux].include?(platform)
318
+
319
+ ensure_ruflet_build_assets(verbose: false) if respond_to?(:ensure_ruflet_build_assets, true)
320
+ client_dir = ensure_flutter_client_dir(verbose: false)
321
+ return nil unless client_dir
322
+
323
+ config = project_run_config
324
+ tools = ensure_flutter!("run", client_dir: client_dir)
325
+ env = build_tool_env(tools[:env], platform, client_dir)
326
+ return nil unless prepare_flutter_client(
327
+ client_dir,
328
+ platform: platform,
329
+ tools: tools,
330
+ config: config,
331
+ self_contained: false,
332
+ verbose: false
333
+ )
334
+
335
+ [
336
+ env,
337
+ tools[:flutter],
338
+ "run",
339
+ "-d",
340
+ platform,
341
+ "--target",
342
+ "lib/main.server.dart",
343
+ "--dart-define",
344
+ "RUFLET_BACKEND_URL=#{url}"
345
+ ]
346
+ rescue StandardError => e
347
+ warn "Project desktop client setup failed: #{e.class}: #{e.message}"
348
+ nil
349
+ end
350
+
351
+ def project_run_requires_managed_client?
352
+ return false unless defined?(Ruflet::CLI::BuildCommand::CLIENT_EXTENSION_MAP)
353
+
354
+ services = Array(project_run_config["services"]).map { |value| normalize_run_extension_key(value) }.compact
355
+ return false if services.empty?
356
+
357
+ extension_keys = Ruflet::CLI::BuildCommand::CLIENT_EXTENSION_MAP.keys
358
+ (services & extension_keys).any?
359
+ end
360
+
361
+ def project_run_config
362
+ config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
363
+ return {} unless File.file?(config_path)
364
+
365
+ YAML.safe_load(File.read(config_path), aliases: true) || {}
366
+ rescue StandardError
367
+ {}
368
+ end
369
+
370
+ def normalize_run_extension_key(value)
371
+ key = value.to_s.strip.downcase
372
+ return nil if key.empty?
373
+
374
+ key.tr!("-", "_")
375
+ key.gsub!(/\A(flet_)+/, "")
376
+ key.gsub!(/\Aservice_/, "")
377
+ key
378
+ end
379
+
309
380
  def detect_desktop_client_command(url)
310
381
  root = ENV["RUFLET_CLIENT_DIR"]
311
382
  root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
@@ -315,12 +386,15 @@ module Ruflet
315
386
 
316
387
  host_os = RbConfig::CONFIG["host_os"]
317
388
  if host_os.match?(/darwin/i)
318
- release_bin = File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
319
- debug_bin = File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
320
- prebuilt_bin = File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
321
- executable = [release_bin, debug_bin].find { |p| File.file?(p) && File.executable?(p) }
322
- executable ||= prebuilt_bin if File.file?(prebuilt_bin) && File.executable?(prebuilt_bin)
323
- return [executable, url] if executable
389
+ app_path = [
390
+ File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app"),
391
+ File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app"),
392
+ File.join(root, "desktop", "ruflet_client.app")
393
+ ].find do |candidate|
394
+ bin = File.join(candidate, "Contents", "MacOS", "ruflet_client")
395
+ File.file?(bin) && File.executable?(bin) && ensure_macos_file_picker_entitlement(candidate)
396
+ end
397
+ return [File.join(app_path, "Contents", "MacOS", "ruflet_client"), url] if app_path
324
398
  elsif host_os.match?(/mswin|mingw|cygwin/i)
325
399
  exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
326
400
  prebuilt = File.join(root, "desktop", "ruflet_client.exe")
@@ -436,7 +510,9 @@ module Ruflet
436
510
 
437
511
  case platform
438
512
  when "macos"
439
- File.file?(File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client"))
513
+ app_path = File.join(root, "desktop", "ruflet_client.app")
514
+ bin = File.join(app_path, "Contents", "MacOS", "ruflet_client")
515
+ File.file?(bin) && ensure_macos_file_picker_entitlement(app_path)
440
516
  when "linux"
441
517
  File.file?(File.join(root, "desktop", "ruflet_client"))
442
518
  when "windows"
@@ -492,6 +568,53 @@ module Ruflet
492
568
  nil
493
569
  end
494
570
 
571
+ def ensure_macos_file_picker_entitlement(app_path)
572
+ return true if macos_app_has_file_picker_entitlement?(app_path)
573
+
574
+ Dir.mktmpdir("ruflet-entitlements-") do |dir|
575
+ entitlements_path = File.join(dir, "ruflet_file_picker.entitlements")
576
+ File.write(entitlements_path, macos_file_picker_entitlements_plist)
577
+ system(
578
+ "/usr/bin/codesign",
579
+ "--force",
580
+ "--deep",
581
+ "--sign",
582
+ "-",
583
+ "--entitlements",
584
+ entitlements_path,
585
+ app_path,
586
+ out: File::NULL,
587
+ err: File::NULL
588
+ )
589
+ end
590
+
591
+ macos_app_has_file_picker_entitlement?(app_path)
592
+ end
593
+
594
+ def macos_app_has_file_picker_entitlement?(app_path)
595
+ output = IO.popen(["/usr/bin/codesign", "-d", "--entitlements", ":-", app_path], err: [:child, :out], &:read)
596
+ $?.success? && output.include?("com.apple.security.files.user-selected.read-write")
597
+ rescue StandardError
598
+ false
599
+ end
600
+
601
+ def macos_file_picker_entitlements_plist
602
+ <<~PLIST
603
+ <?xml version="1.0" encoding="UTF-8"?>
604
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
605
+ <plist version="1.0">
606
+ <dict>
607
+ \t<key>com.apple.security.app-sandbox</key>
608
+ \t<true/>
609
+ \t<key>com.apple.security.files.user-selected.read-write</key>
610
+ \t<true/>
611
+ \t<key>com.apple.security.network.client</key>
612
+ \t<true/>
613
+ </dict>
614
+ </plist>
615
+ PLIST
616
+ end
617
+
495
618
  def fallback_release_asset(assets, wanted)
496
619
  kind = wanted[:kind]
497
620
  platform = wanted[:platform]
@@ -5,7 +5,7 @@ module Ruflet
5
5
  MAIN_TEMPLATE = <<~RUBY
6
6
  require "ruflet"
7
7
  Ruflet.run do |page|
8
- page.title = "Counter Demo"
8
+ page.title = "%<app_title>s"
9
9
  count = 0
10
10
  count_text = text(count.to_s, style: {size: 40})
11
11
  page.add(
@@ -36,8 +36,9 @@ module Ruflet
36
36
  GEMFILE_TEMPLATE = <<~GEMFILE
37
37
  source "https://rubygems.org"
38
38
 
39
- gem "ruflet_core", ">= 0.0.10"
40
- gem "ruflet_server", ">= 0.0.10"
39
+ gem "ruflet", ">= #{Ruflet::VERSION}"
40
+ gem "ruflet_core", ">= #{Ruflet::VERSION}"
41
+ gem "ruflet_server", ">= #{Ruflet::VERSION}"
41
42
  GEMFILE
42
43
 
43
44
  README_TEMPLATE = <<~MD
@@ -61,6 +61,9 @@ module Ruflet
61
61
 
62
62
  return check_client_updates(targets, platform: platform) if options[:check]
63
63
 
64
+ ensure_cached_ruflet_assets_for_update(force: options[:force])
65
+ ensure_flutter!("update", client_dir: nil, auto_install: true) if respond_to?(:ensure_flutter!, true)
66
+
64
67
  targets.each do |target|
65
68
  root =
66
69
  if target == :web
@@ -83,6 +86,17 @@ module Ruflet
83
86
 
84
87
  private
85
88
 
89
+ def ensure_cached_ruflet_assets_for_update(force: false)
90
+ if Ruflet::CLI.respond_to?(:download_ruflet_assets, true)
91
+ Ruflet::CLI.send(:download_ruflet_assets, force: force)
92
+ return
93
+ end
94
+
95
+ if Ruflet::CLI.respond_to?(:ensure_cached_ruflet_client_template!, true)
96
+ Ruflet::CLI.send(:ensure_cached_ruflet_client_template!, force: force)
97
+ end
98
+ end
99
+
86
100
  def check_client_updates(targets, platform:)
87
101
  root = client_cache_root_for(platform)
88
102
  manifest = read_client_manifest(root)
data/lib/ruflet/cli.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "optparse"
4
4
 
5
+ require_relative "version"
5
6
  require_relative "cli/templates"
6
7
  require_relative "cli/new_command"
7
8
  require_relative "cli/flutter_sdk"
@@ -9,7 +10,6 @@ require_relative "cli/run_command"
9
10
  require_relative "cli/update_command"
10
11
  require_relative "cli/build_command"
11
12
  require_relative "cli/extra_command"
12
- require_relative "version"
13
13
 
14
14
  module Ruflet
15
15
  module CLI
@@ -22,6 +22,7 @@ module Ruflet
22
22
 
23
23
  def run(argv = ARGV)
24
24
  command = (argv.shift || "help").downcase
25
+ ensure_first_run_assets(command)
25
26
 
26
27
  case command
27
28
  when "version", "-v", "--version"
@@ -84,5 +85,16 @@ module Ruflet
84
85
  def version_string
85
86
  "ruflet #{Ruflet::VERSION}"
86
87
  end
88
+
89
+ private
90
+
91
+ def ensure_first_run_assets(command)
92
+ return if %w[version -v --version help -h --help].include?(command)
93
+ return unless respond_to?(:download_ruflet_assets, true)
94
+
95
+ send(:download_ruflet_assets)
96
+ rescue StandardError
97
+ nil
98
+ end
87
99
  end
88
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.11" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.13" unless const_defined?(:VERSION)
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa