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.
- checksums.yaml +4 -4
- data/README.md +1 -40
- data/lib/ruflet/cli/build_command.rb +123 -640
- data/lib/ruflet/cli/extra_command.rb +3 -15
- data/lib/ruflet/cli/flutter_sdk.rb +9 -91
- data/lib/ruflet/cli/new_command.rb +7 -36
- data/lib/ruflet/cli/run_command.rb +97 -186
- data/lib/ruflet/cli/templates.rb +8 -9
- data/lib/ruflet/cli.rb +0 -13
- data/lib/ruflet/version.rb +1 -1
- metadata +2 -4
- data/lib/ruflet/cli/android_sdk.rb +0 -306
- data/lib/ruflet/cli/environment_setup.rb +0 -241
|
@@ -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
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
176
|
-
#
|
|
177
|
-
#
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
|
1291
|
-
|
|
1292
|
-
return
|
|
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
|
-
|
|
1317
|
-
|
|
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
|
|
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
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
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
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
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
|
-
"#{
|
|
1094
|
+
content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
|
|
1405
1095
|
end
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
1483
|
-
assets.
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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)
|