ruflet 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -40
- data/lib/ruflet/cli/build_command.rb +124 -697
- 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 +1 -3
- 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)
|
|
@@ -736,60 +583,20 @@ module Ruflet
|
|
|
736
583
|
alt = "ruflet.yml"
|
|
737
584
|
config_path = alt if File.file?(alt)
|
|
738
585
|
end
|
|
739
|
-
return
|
|
740
|
-
|
|
741
|
-
YAML.safe_load(File.read(config_path), aliases: true) || {}
|
|
742
|
-
rescue StandardError => e
|
|
743
|
-
warn "Failed to load ruflet config: #{e.class}: #{e.message}"
|
|
744
|
-
{}
|
|
745
|
-
end
|
|
586
|
+
return {} unless File.file?(config_path)
|
|
746
587
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
with_minimal_rails_config_context do
|
|
754
|
-
load initializer
|
|
755
|
-
config = Ruflet::Rails.config
|
|
756
|
-
config.respond_to?(:to_ruflet_yaml_hash) ? config.to_ruflet_yaml_hash : {}
|
|
588
|
+
config = YAML.safe_load(File.read(config_path), aliases: true) || {}
|
|
589
|
+
services_path = File.join(File.dirname(File.expand_path(config_path)), "services.yaml")
|
|
590
|
+
if File.file?(services_path)
|
|
591
|
+
service_config = YAML.safe_load(File.read(services_path), aliases: true) || {}
|
|
592
|
+
config["services"] = service_config["services"] if service_config.key?("services")
|
|
757
593
|
end
|
|
758
|
-
|
|
759
|
-
warn "Failed to load Rails Ruflet config: #{e.class}: #{e.message}"
|
|
760
|
-
{}
|
|
594
|
+
config
|
|
761
595
|
rescue StandardError => e
|
|
762
|
-
warn "Failed to load
|
|
596
|
+
warn "Failed to load ruflet config: #{e.class}: #{e.message}"
|
|
763
597
|
{}
|
|
764
598
|
end
|
|
765
599
|
|
|
766
|
-
def with_minimal_rails_config_context
|
|
767
|
-
return yield if defined?(::Rails)
|
|
768
|
-
|
|
769
|
-
rails_module = Module.new
|
|
770
|
-
root = Pathname.new(Dir.pwd)
|
|
771
|
-
env = minimal_rails_env(ENV.fetch("RAILS_ENV", "development"))
|
|
772
|
-
rails_module.define_singleton_method(:root) { root }
|
|
773
|
-
rails_module.define_singleton_method(:env) { env }
|
|
774
|
-
|
|
775
|
-
Object.const_set(:Rails, rails_module)
|
|
776
|
-
yield
|
|
777
|
-
ensure
|
|
778
|
-
Object.send(:remove_const, :Rails) if defined?(rails_module) && Object.const_defined?(:Rails, false) && ::Rails.equal?(rails_module)
|
|
779
|
-
end
|
|
780
|
-
|
|
781
|
-
def minimal_rails_env(name)
|
|
782
|
-
value = name.to_s
|
|
783
|
-
Object.new.tap do |env|
|
|
784
|
-
env.define_singleton_method(:to_s) { value }
|
|
785
|
-
env.define_singleton_method(:to_str) { value }
|
|
786
|
-
env.define_singleton_method(:==) { |other| value == other.to_s }
|
|
787
|
-
env.define_singleton_method(:development?) { value == "development" }
|
|
788
|
-
env.define_singleton_method(:test?) { value == "test" }
|
|
789
|
-
env.define_singleton_method(:production?) { value == "production" }
|
|
790
|
-
end
|
|
791
|
-
end
|
|
792
|
-
|
|
793
600
|
def apply_build_config(client_dir, config = {})
|
|
794
601
|
config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
|
|
795
602
|
config_present = File.file?(config_path)
|
|
@@ -917,7 +724,6 @@ module Ruflet
|
|
|
917
724
|
apply_web_metadata(client_dir, metadata)
|
|
918
725
|
apply_windows_metadata(client_dir, metadata)
|
|
919
726
|
apply_linux_metadata(client_dir, metadata)
|
|
920
|
-
apply_dart_metadata(client_dir, metadata)
|
|
921
727
|
build_log(
|
|
922
728
|
verbose,
|
|
923
729
|
"app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
|
|
@@ -995,6 +801,16 @@ module Ruflet
|
|
|
995
801
|
%( applicationId = "#{metadata[:android_application_id]}")
|
|
996
802
|
)
|
|
997
803
|
|
|
804
|
+
Dir.glob(
|
|
805
|
+
File.join(client_dir, "android", "app", "src", "main", "kotlin", "**", "MainActivity.kt")
|
|
806
|
+
).each do |activity_path|
|
|
807
|
+
replace_in_file(
|
|
808
|
+
activity_path,
|
|
809
|
+
/^package\s+[^\s]+$/,
|
|
810
|
+
"package #{metadata[:android_application_id]}"
|
|
811
|
+
)
|
|
812
|
+
end
|
|
813
|
+
|
|
998
814
|
manifest_path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
999
815
|
replace_in_file(
|
|
1000
816
|
manifest_path,
|
|
@@ -1113,31 +929,6 @@ module Ruflet
|
|
|
1113
929
|
cmake_path = File.join(client_dir, "linux", "CMakeLists.txt")
|
|
1114
930
|
replace_in_file(cmake_path, /^set\(BINARY_NAME ".*"\)$/, %(set(BINARY_NAME "#{metadata[:package_name]}")))
|
|
1115
931
|
replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
|
|
1116
|
-
|
|
1117
|
-
# The GTK runner hardcodes the window title (header-bar and fallback
|
|
1118
|
-
# paths). Without this it shows the package name (e.g. "ruflet_client")
|
|
1119
|
-
# instead of the configured app name. Match the call, not the literal,
|
|
1120
|
-
# so re-runs stay idempotent.
|
|
1121
|
-
my_application_path = File.join(client_dir, "linux", "runner", "my_application.cc")
|
|
1122
|
-
title = c_string_escape(metadata[:display_name])
|
|
1123
|
-
replace_in_file(
|
|
1124
|
-
my_application_path,
|
|
1125
|
-
/gtk_header_bar_set_title\(header_bar, "[^"]*"\)/,
|
|
1126
|
-
%(gtk_header_bar_set_title(header_bar, "#{title}"))
|
|
1127
|
-
)
|
|
1128
|
-
replace_in_file(
|
|
1129
|
-
my_application_path,
|
|
1130
|
-
/gtk_window_set_title\(window, "[^"]*"\)/,
|
|
1131
|
-
%(gtk_window_set_title(window, "#{title}"))
|
|
1132
|
-
)
|
|
1133
|
-
end
|
|
1134
|
-
|
|
1135
|
-
def apply_dart_metadata(client_dir, metadata)
|
|
1136
|
-
title = dart_single_quote_escape(metadata[:display_name])
|
|
1137
|
-
client_entrypoint_paths(client_dir).each do |entrypoint|
|
|
1138
|
-
replace_in_file(entrypoint, /title: 'Ruflet'/, "title: '#{title}'")
|
|
1139
|
-
replace_in_file(entrypoint, /AppBar\(title: const Text\('Ruflet'\)\)/, "AppBar(title: const Text('#{title}'))")
|
|
1140
|
-
end
|
|
1141
932
|
end
|
|
1142
933
|
|
|
1143
934
|
def replace_plist_value(path, key, value)
|
|
@@ -1222,31 +1013,18 @@ module Ruflet
|
|
|
1222
1013
|
value.to_s.gsub('"', '""')
|
|
1223
1014
|
end
|
|
1224
1015
|
|
|
1225
|
-
# Escape a value for embedding inside a C string literal (used for the
|
|
1226
|
-
# GTK runner window title in my_application.cc). Block form avoids
|
|
1227
|
-
# backslash interpretation in the gsub replacement string.
|
|
1228
|
-
def c_string_escape(value)
|
|
1229
|
-
value.to_s.gsub(/[\\"]/) { |ch| "\\#{ch}" }
|
|
1230
|
-
end
|
|
1231
|
-
|
|
1232
|
-
def dart_single_quote_escape(value)
|
|
1233
|
-
value.to_s.gsub("\\", "\\\\\\").gsub("'", "\\\\'")
|
|
1234
|
-
end
|
|
1235
|
-
|
|
1236
1016
|
def key_defined?(hash, key)
|
|
1237
1017
|
hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
|
|
1238
1018
|
end
|
|
1239
1019
|
|
|
1240
1020
|
def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
extension_keys = ((configured_extension_keys - protected_extension_keys) | service_extension_keys).uniq
|
|
1021
|
+
services = configured_service_entries(config).map { |entry| entry[:name] }
|
|
1022
|
+
requested_extensions = Array(config["extensions"]).map { |value| normalize_extension_key(value) }.compact
|
|
1023
|
+
protected_extensions = services.flat_map { |name| PROTECTED_SERVICE_EXTENSIONS.fetch(name, []) }
|
|
1024
|
+
extension_keys = (requested_extensions + protected_extensions + services).uniq
|
|
1246
1025
|
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
1247
1026
|
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
1248
1027
|
|
|
1249
|
-
sync_client_flet_packages(client_dir, extension_packages)
|
|
1250
1028
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
1251
1029
|
if File.file?(pubspec_path)
|
|
1252
1030
|
sync_client_extension_dependencies(pubspec_path, extension_packages)
|
|
@@ -1256,208 +1034,67 @@ module Ruflet
|
|
|
1256
1034
|
sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1257
1035
|
prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1258
1036
|
end
|
|
1259
|
-
apply_service_native_requirements(client_dir, service_definitions.keys, service_definitions)
|
|
1260
1037
|
end
|
|
1261
1038
|
|
|
1262
|
-
def
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
File.join(client_dir, "ios", "Podfile"),
|
|
1270
|
-
Array(requirements[:ios_permission_definitions])
|
|
1271
|
-
)
|
|
1272
|
-
return if requirements.empty?
|
|
1273
|
-
|
|
1274
|
-
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1275
|
-
Array(requirements[:android_permissions]).each do |permission|
|
|
1276
|
-
ensure_android_permission(android_manifest, permission)
|
|
1277
|
-
end
|
|
1278
|
-
|
|
1279
|
-
ios_info = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1280
|
-
Hash(requirements[:ios_info]).each do |key, value|
|
|
1281
|
-
ensure_plist_string(ios_info, key, value)
|
|
1282
|
-
end
|
|
1283
|
-
|
|
1284
|
-
macos_info = File.join(client_dir, "macos", "Runner", "Info.plist")
|
|
1285
|
-
Hash(requirements[:macos_info]).each do |key, value|
|
|
1286
|
-
ensure_plist_string(macos_info, key, value)
|
|
1287
|
-
end
|
|
1288
|
-
|
|
1289
|
-
%w[DebugProfile Release].each do |name|
|
|
1290
|
-
entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
|
|
1291
|
-
Hash(requirements[:macos_entitlements]).each do |key, value|
|
|
1292
|
-
ensure_plist_boolean(entitlements_path, key, value)
|
|
1293
|
-
end
|
|
1294
|
-
end
|
|
1295
|
-
end
|
|
1296
|
-
|
|
1297
|
-
def remove_service_native_requirements(client_dir, extension_keys, service_requirements = service_native_requirements(load_service_definitions(client_dir)))
|
|
1298
|
-
requirements = merge_service_native_requirements(extension_keys, service_requirements)
|
|
1299
|
-
return if requirements.empty?
|
|
1300
|
-
|
|
1301
|
-
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1302
|
-
Array(requirements[:android_permissions]).each do |permission|
|
|
1303
|
-
remove_android_permission(android_manifest, permission)
|
|
1304
|
-
end
|
|
1305
|
-
|
|
1306
|
-
[File.join(client_dir, "ios", "Runner", "Info.plist"), File.join(client_dir, "macos", "Runner", "Info.plist")].each do |path|
|
|
1307
|
-
(Hash(requirements[:ios_info]).keys + Hash(requirements[:macos_info]).keys).uniq.each do |key|
|
|
1308
|
-
remove_plist_entry(path, key)
|
|
1309
|
-
end
|
|
1310
|
-
end
|
|
1311
|
-
|
|
1312
|
-
%w[DebugProfile Release].each do |name|
|
|
1313
|
-
entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
|
|
1314
|
-
Hash(requirements[:macos_entitlements]).each_key do |key|
|
|
1315
|
-
remove_plist_entry(entitlements_path, key)
|
|
1316
|
-
end
|
|
1317
|
-
end
|
|
1318
|
-
end
|
|
1039
|
+
def configured_service_entries(config)
|
|
1040
|
+
Array(config["services"]).filter_map do |entry|
|
|
1041
|
+
case entry
|
|
1042
|
+
when Hash
|
|
1043
|
+
name, options = entry.first
|
|
1044
|
+
key = normalize_extension_key(name)
|
|
1045
|
+
next unless key
|
|
1319
1046
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
memo[:android_permissions] ||= []
|
|
1326
|
-
memo[:android_permissions] |= Array(requirements[:android_permissions])
|
|
1327
|
-
memo[:ios_permission_definitions] ||= []
|
|
1328
|
-
memo[:ios_permission_definitions] |= Array(requirements[:ios_permission_definitions])
|
|
1329
|
-
%i[ios_info macos_info macos_entitlements].each do |section|
|
|
1330
|
-
memo[section] ||= {}
|
|
1331
|
-
memo[section].merge!(requirements[section] || {})
|
|
1047
|
+
description = options.is_a?(Hash) ? options["description"] || options[:description] : options
|
|
1048
|
+
{ name: key, description: description.to_s.strip }
|
|
1049
|
+
else
|
|
1050
|
+
key = normalize_extension_key(entry)
|
|
1051
|
+
{ name: key, description: "" } if key
|
|
1332
1052
|
end
|
|
1333
1053
|
end
|
|
1334
1054
|
end
|
|
1335
1055
|
|
|
1336
|
-
def
|
|
1337
|
-
|
|
1338
|
-
return
|
|
1339
|
-
|
|
1340
|
-
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
1341
|
-
Array(data["services"]).each_with_object({}) do |entry, memo|
|
|
1342
|
-
name, metadata =
|
|
1343
|
-
if entry.is_a?(Hash)
|
|
1344
|
-
entry.first
|
|
1345
|
-
else
|
|
1346
|
-
[entry, {}]
|
|
1347
|
-
end
|
|
1348
|
-
key = normalize_extension_key(name)
|
|
1349
|
-
memo[key] = metadata.is_a?(Hash) ? metadata : {} if key
|
|
1350
|
-
end
|
|
1351
|
-
rescue Psych::Exception => e
|
|
1352
|
-
warn "Could not parse #{path}: #{e.message}"
|
|
1353
|
-
{}
|
|
1354
|
-
end
|
|
1355
|
-
|
|
1356
|
-
def services_config_path(client_dir = nil)
|
|
1357
|
-
candidates = [ENV["RUFLET_SERVICES"], File.expand_path("services.yaml", Dir.pwd)]
|
|
1358
|
-
candidates << File.join(client_dir, "services.yaml") if client_dir
|
|
1359
|
-
candidates.compact.find { |path| File.file?(path) }
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
|
-
def service_native_requirements(service_definitions)
|
|
1363
|
-
all_keys = (DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys | service_definitions.keys)
|
|
1364
|
-
all_keys.each_with_object({}) do |key, memo|
|
|
1365
|
-
defaults = DEFAULT_SERVICE_NATIVE_REQUIREMENTS[key] || {}
|
|
1366
|
-
metadata = service_definitions[key] || {}
|
|
1367
|
-
native = metadata["native"].is_a?(Hash) ? metadata["native"] : metadata
|
|
1368
|
-
memo[key] = {
|
|
1369
|
-
android_permissions: Array(native["android_permissions"] || defaults[:android_permissions]),
|
|
1370
|
-
ios_permission_definitions: Array(native["ios_permission_definitions"] || defaults[:ios_permission_definitions]),
|
|
1371
|
-
ios_info: native["ios_info"].is_a?(Hash) ? native["ios_info"] : (defaults[:ios_info] || {}),
|
|
1372
|
-
macos_info: native["macos_info"].is_a?(Hash) ? native["macos_info"] : (defaults[:macos_info] || {}),
|
|
1373
|
-
macos_entitlements: native["macos_entitlements"].is_a?(Hash) ? native["macos_entitlements"] : (defaults[:macos_entitlements] || {})
|
|
1374
|
-
}
|
|
1375
|
-
end
|
|
1376
|
-
end
|
|
1377
|
-
|
|
1378
|
-
def sync_ios_permission_definitions(path, definitions)
|
|
1379
|
-
return unless File.file?(path)
|
|
1056
|
+
def apply_native_service_permissions(client_dir, config)
|
|
1057
|
+
entries = configured_service_entries(config)
|
|
1058
|
+
return if entries.empty?
|
|
1380
1059
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
updated = content.gsub(marker_pattern, "\n")
|
|
1384
|
-
definitions = Array(definitions).map(&:to_s).reject(&:empty?).uniq.sort
|
|
1385
|
-
if definitions.any?
|
|
1386
|
-
block = <<~RUBY.chomp
|
|
1387
|
-
# BEGIN RUFLET PERMISSION DEFINITIONS
|
|
1388
|
-
target.build_configurations.each do |config|
|
|
1389
|
-
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
|
1390
|
-
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] += #{definitions.inspect}
|
|
1391
|
-
end
|
|
1392
|
-
# END RUFLET PERMISSION DEFINITIONS
|
|
1393
|
-
RUBY
|
|
1394
|
-
updated = updated.sub(
|
|
1395
|
-
/^(\s*)flutter_additional_ios_build_settings\(target\)\s*$/,
|
|
1396
|
-
"\\0\n\n#{block}"
|
|
1397
|
-
)
|
|
1398
|
-
end
|
|
1399
|
-
File.write(path, updated) unless updated == content
|
|
1060
|
+
apply_android_service_permissions(client_dir, entries)
|
|
1061
|
+
apply_ios_service_usage_descriptions(client_dir, entries)
|
|
1400
1062
|
end
|
|
1401
1063
|
|
|
1402
|
-
def
|
|
1064
|
+
def apply_android_service_permissions(client_dir, entries)
|
|
1065
|
+
path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1403
1066
|
return unless File.file?(path)
|
|
1404
1067
|
|
|
1405
1068
|
content = File.read(path)
|
|
1406
|
-
|
|
1069
|
+
entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission|
|
|
1070
|
+
next if content.include?(%(android:name="#{permission}"))
|
|
1407
1071
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
File.write(path,
|
|
1072
|
+
content.sub!(/<manifest\b[^>]*>\s*/, "\\0 <uses-permission android:name=\"#{permission}\"/>\n")
|
|
1073
|
+
end
|
|
1074
|
+
File.write(path, content)
|
|
1411
1075
|
end
|
|
1412
1076
|
|
|
1413
|
-
def
|
|
1077
|
+
def apply_ios_service_usage_descriptions(client_dir, entries)
|
|
1078
|
+
path = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1414
1079
|
return unless File.file?(path)
|
|
1415
1080
|
|
|
1416
1081
|
content = File.read(path)
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
def ensure_plist_string(path, key, value)
|
|
1422
|
-
ensure_plist_entry(path, key, "<string>#{xml_escape(value)}</string>")
|
|
1423
|
-
end
|
|
1424
|
-
|
|
1425
|
-
def ensure_plist_boolean(path, key, value)
|
|
1426
|
-
ensure_plist_entry(path, key, value ? "<true/>" : "<false/>")
|
|
1427
|
-
end
|
|
1082
|
+
entries.each do |entry|
|
|
1083
|
+
key = IOS_SERVICE_USAGE_KEYS[entry[:name]]
|
|
1084
|
+
next unless key
|
|
1428
1085
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1086
|
+
description = entry[:description]
|
|
1087
|
+
description = "This app uses #{entry[:name]} access for its Ruflet features." if description.empty?
|
|
1088
|
+
escaped_description = xml_escape(description)
|
|
1089
|
+
pair = "\t<key>#{key}</key>\n\t<string>#{escaped_description}</string>\n"
|
|
1431
1090
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
entry = "\t<key>#{key}</key>\n\t#{value_xml}\n"
|
|
1436
|
-
|
|
1437
|
-
# Insert into the ROOT <dict> (opened right after <plist ...>), never the
|
|
1438
|
-
# first nested </dict>. Modern Flutter Info.plists nest a dict inside
|
|
1439
|
-
# UIApplicationSceneManifest, so subbing the first </dict> would bury
|
|
1440
|
-
# top-level keys (e.g. NS*UsageDescription) where iOS can't read them —
|
|
1441
|
-
# which crashes the app the moment a permission is requested.
|
|
1442
|
-
root_open = content.match(%r{<plist\b[^>]*>\s*<dict>}m)
|
|
1443
|
-
updated =
|
|
1444
|
-
if root_open
|
|
1445
|
-
insert_at = root_open.end(0)
|
|
1446
|
-
content[0...insert_at] + "\n#{entry}" + content[insert_at..]
|
|
1447
|
-
elsif (last = content.rindex("</dict>"))
|
|
1448
|
-
content[0...last] + entry + content[last..]
|
|
1091
|
+
if content.match?(%r{<key>#{Regexp.escape(key)}</key>})
|
|
1092
|
+
content.sub!(%r{<key>#{Regexp.escape(key)}</key>\s*<string>.*?</string>}m, "<key>#{key}</key>\n\t<string>#{escaped_description}</string>")
|
|
1449
1093
|
else
|
|
1450
|
-
"#{
|
|
1094
|
+
content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
|
|
1451
1095
|
end
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
def remove_plist_entry(path, key)
|
|
1456
|
-
return unless File.file?(path)
|
|
1457
|
-
|
|
1458
|
-
content = File.read(path)
|
|
1459
|
-
updated = content.gsub(%r{\s*<key>#{Regexp.escape(key)}</key>\s*<(?:string>.*?</string|true/|false/)>\s*}m, "\n")
|
|
1460
|
-
File.write(path, updated) unless updated == content
|
|
1096
|
+
end
|
|
1097
|
+
File.write(path, content)
|
|
1461
1098
|
end
|
|
1462
1099
|
|
|
1463
1100
|
def clear_flutter_build_state(client_dir, verbose: false)
|
|
@@ -1501,6 +1138,7 @@ module Ruflet
|
|
|
1501
1138
|
remove_self_contained_project_assets(client_dir, verbose: verbose)
|
|
1502
1139
|
remove_local_ruby_runtime_override(client_dir, verbose: verbose)
|
|
1503
1140
|
end
|
|
1141
|
+
true
|
|
1504
1142
|
end
|
|
1505
1143
|
|
|
1506
1144
|
def sync_client_pubspec_for_runtime_mode(client_dir, self_contained:)
|
|
@@ -1510,23 +1148,24 @@ module Ruflet
|
|
|
1510
1148
|
data = YAML.safe_load(File.read(pubspec_path), aliases: true) || {}
|
|
1511
1149
|
dependencies = data["dependencies"]
|
|
1512
1150
|
dependencies = data["dependencies"] = {} unless dependencies.is_a?(Hash)
|
|
1151
|
+
spinkit_dependency = template_client_pubspec_dependencies["flutter_spinkit"]
|
|
1152
|
+
dependencies["flutter_spinkit"] = spinkit_dependency if spinkit_dependency
|
|
1513
1153
|
flutter = data["flutter"]
|
|
1514
1154
|
flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
|
|
1515
1155
|
assets = Array(flutter["assets"]).map(&:to_s)
|
|
1516
|
-
project_asset_prefix = "assets/#{self_contained_project_name}/"
|
|
1517
|
-
assets.reject! { |asset| asset.start_with?(project_asset_prefix) }
|
|
1518
1156
|
|
|
1519
1157
|
if self_contained
|
|
1520
1158
|
dependencies["ruby_runtime"] = ruby_runtime_dependency(dependencies["ruby_runtime"])
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1159
|
+
# Flutter does not recurse into asset directories, so every subdirectory
|
|
1160
|
+
# of the embedded project (e.g. standalone_apps/<slug>/) must be listed
|
|
1161
|
+
# explicitly or its files never reach the bundle/manifest on device.
|
|
1162
|
+
self_contained_project_asset_dirs.each do |dir_entry|
|
|
1163
|
+
assets << dir_entry unless assets.include?(dir_entry)
|
|
1525
1164
|
end
|
|
1526
1165
|
else
|
|
1527
1166
|
dependencies.delete("ruby_runtime")
|
|
1528
|
-
|
|
1529
|
-
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) }
|
|
1530
1169
|
end
|
|
1531
1170
|
|
|
1532
1171
|
flutter["assets"] = assets unless assets.empty?
|
|
@@ -1534,46 +1173,11 @@ module Ruflet
|
|
|
1534
1173
|
write_pubspec_yaml(pubspec_path, data)
|
|
1535
1174
|
end
|
|
1536
1175
|
|
|
1537
|
-
RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.6"
|
|
1538
|
-
|
|
1539
1176
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1540
|
-
local_path = explicit_local_ruby_runtime_path ||
|
|
1177
|
+
local_path = explicit_local_ruby_runtime_path || source_checkout_ruby_runtime_path
|
|
1541
1178
|
return { "path" => local_path } if local_path
|
|
1542
1179
|
|
|
1543
|
-
|
|
1544
|
-
return template_dependency if usable_ruby_runtime_dependency?(template_dependency)
|
|
1545
|
-
|
|
1546
|
-
return current_dependency if usable_ruby_runtime_dependency?(current_dependency)
|
|
1547
|
-
|
|
1548
|
-
RUBY_RUNTIME_FALLBACK_REQUIREMENT
|
|
1549
|
-
end
|
|
1550
|
-
|
|
1551
|
-
# In a ruflet repo checkout the plugin lives next to templates/; build
|
|
1552
|
-
# against it so framework changes are exercised without publishing.
|
|
1553
|
-
def repo_checkout_ruby_runtime_path
|
|
1554
|
-
template_root =
|
|
1555
|
-
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
1556
|
-
Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
1557
|
-
end
|
|
1558
|
-
return nil unless template_root
|
|
1559
|
-
|
|
1560
|
-
candidate = File.expand_path(File.join(template_root, "..", "..", "ruby_runtime"))
|
|
1561
|
-
File.file?(File.join(candidate, "pubspec.yaml")) ? candidate : nil
|
|
1562
|
-
end
|
|
1563
|
-
|
|
1564
|
-
# Relative path dependencies only resolve from the directory the
|
|
1565
|
-
# template was authored in — never copy them into a user's client.
|
|
1566
|
-
def usable_ruby_runtime_dependency?(dependency)
|
|
1567
|
-
case dependency
|
|
1568
|
-
when nil then false
|
|
1569
|
-
when Hash
|
|
1570
|
-
path = dependency["path"] || dependency[:path]
|
|
1571
|
-
return true if path.nil? # hosted/git table form
|
|
1572
|
-
|
|
1573
|
-
Pathname.new(path.to_s).absolute? && File.file?(File.join(path, "pubspec.yaml"))
|
|
1574
|
-
else
|
|
1575
|
-
!dependency.to_s.strip.empty?
|
|
1576
|
-
end
|
|
1180
|
+
current_dependency || "^0.0.3"
|
|
1577
1181
|
end
|
|
1578
1182
|
|
|
1579
1183
|
def explicit_local_ruby_runtime_path
|
|
@@ -1586,6 +1190,13 @@ module Ruflet
|
|
|
1586
1190
|
nil
|
|
1587
1191
|
end
|
|
1588
1192
|
|
|
1193
|
+
def source_checkout_ruby_runtime_path
|
|
1194
|
+
candidate = Pathname.new(File.expand_path("../../../../../ruby_runtime", __dir__))
|
|
1195
|
+
return candidate.to_s if candidate.join("pubspec.yaml").file?
|
|
1196
|
+
|
|
1197
|
+
nil
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1589
1200
|
def refresh_managed_client_template_files(client_dir, verbose: false)
|
|
1590
1201
|
template_root =
|
|
1591
1202
|
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
@@ -1598,13 +1209,11 @@ module Ruflet
|
|
|
1598
1209
|
"lib/main.self.dart",
|
|
1599
1210
|
"lib/main.server.dart",
|
|
1600
1211
|
"lib/ruflet_file_picker_service.dart",
|
|
1601
|
-
"
|
|
1602
|
-
"macos/Runner/Release.entitlements"
|
|
1603
|
-
]
|
|
1604
|
-
stale_files = [
|
|
1212
|
+
"lib/ruflet_spinkit.dart",
|
|
1605
1213
|
"lib/connection_probe.dart",
|
|
1606
1214
|
"lib/connection_probe_io.dart",
|
|
1607
|
-
"lib/connection_probe_stub.dart"
|
|
1215
|
+
"lib/connection_probe_stub.dart",
|
|
1216
|
+
"ios/Podfile"
|
|
1608
1217
|
]
|
|
1609
1218
|
|
|
1610
1219
|
managed_files.each do |relative_path|
|
|
@@ -1616,35 +1225,6 @@ module Ruflet
|
|
|
1616
1225
|
FileUtils.cp(source, destination)
|
|
1617
1226
|
build_log(verbose, "refreshed template file #{relative_path}")
|
|
1618
1227
|
end
|
|
1619
|
-
|
|
1620
|
-
stale_files.each do |relative_path|
|
|
1621
|
-
path = File.join(client_dir, relative_path)
|
|
1622
|
-
next unless File.file?(path)
|
|
1623
|
-
|
|
1624
|
-
FileUtils.rm_f(path)
|
|
1625
|
-
build_log(verbose, "removed stale template file #{relative_path}")
|
|
1626
|
-
end
|
|
1627
|
-
|
|
1628
|
-
repair_legacy_self_contained_bootstrap(client_dir, verbose: verbose)
|
|
1629
|
-
end
|
|
1630
|
-
|
|
1631
|
-
def repair_legacy_self_contained_bootstrap(client_dir, verbose: false)
|
|
1632
|
-
path = File.join(client_dir, "lib", "main.self.dart")
|
|
1633
|
-
return unless File.file?(path)
|
|
1634
|
-
|
|
1635
|
-
content = File.read(path)
|
|
1636
|
-
updated = content.gsub(
|
|
1637
|
-
/^\s*await RubyRuntime\.eval\("ENV\['RUFLET_DEBUG'\].*?\n/,
|
|
1638
|
-
""
|
|
1639
|
-
)
|
|
1640
|
-
updated = updated.gsub(
|
|
1641
|
-
/^\s*final digestLength = await RubyRuntime\.eval\(\n.*?^\s*\);\n\s*debugPrint\('Embedded Digest::SHA1 bytesize: \$digestLength'\);\n/m,
|
|
1642
|
-
""
|
|
1643
|
-
)
|
|
1644
|
-
return if updated == content
|
|
1645
|
-
|
|
1646
|
-
File.write(path, updated)
|
|
1647
|
-
build_log(verbose, "removed legacy pre-server RubyRuntime.eval diagnostics")
|
|
1648
1228
|
end
|
|
1649
1229
|
|
|
1650
1230
|
def write_pubspec_yaml(path, data)
|
|
@@ -1669,17 +1249,23 @@ module Ruflet
|
|
|
1669
1249
|
end.join
|
|
1670
1250
|
end
|
|
1671
1251
|
|
|
1252
|
+
# Flutter asset directory entries for every folder of the embedded project
|
|
1253
|
+
# that contains packaged files. Derived from the exact copy list so the
|
|
1254
|
+
# pubspec asset dirs match what sync_self_contained_project_assets writes.
|
|
1255
|
+
def self_contained_project_asset_dirs
|
|
1256
|
+
prefix = "assets/#{self_contained_project_name}"
|
|
1257
|
+
dirs = project_asset_relative_paths.map { |rel| File.dirname(rel) }.uniq
|
|
1258
|
+
project_dirs = dirs.map { |dir| dir == "." ? "#{prefix}/" : "#{prefix}/#{dir}/" }
|
|
1259
|
+
project_dirs.sort
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1672
1262
|
def sync_self_contained_project_assets(client_dir, verbose: false)
|
|
1673
1263
|
project_root = Pathname.new(Dir.pwd)
|
|
1674
1264
|
assets_root = File.join(client_dir, "assets")
|
|
1675
1265
|
destination_root = File.join(assets_root, self_contained_project_name)
|
|
1676
1266
|
FileUtils.rm_rf(destination_root)
|
|
1677
|
-
FileUtils.rm_rf(File.join(assets_root, "ruby_project"))
|
|
1678
1267
|
FileUtils.mkdir_p(destination_root)
|
|
1679
1268
|
|
|
1680
|
-
legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
|
|
1681
|
-
FileUtils.rm_f(legacy_entrypoint)
|
|
1682
|
-
|
|
1683
1269
|
copied = 0
|
|
1684
1270
|
project_asset_relative_paths.each do |relative_path|
|
|
1685
1271
|
source = project_root.join(relative_path)
|
|
@@ -1687,104 +1273,15 @@ module Ruflet
|
|
|
1687
1273
|
|
|
1688
1274
|
destination = File.join(destination_root, relative_path)
|
|
1689
1275
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
1690
|
-
|
|
1276
|
+
FileUtils.cp(source.to_s, destination)
|
|
1691
1277
|
copied += 1
|
|
1692
1278
|
end
|
|
1693
1279
|
|
|
1694
1280
|
build_log(verbose, "copied #{copied} project file#{copied == 1 ? '' : 's'} to assets/#{self_contained_project_name}")
|
|
1695
1281
|
end
|
|
1696
1282
|
|
|
1697
|
-
def copy_project_asset_file(source, destination, project_root: nil)
|
|
1698
|
-
if File.extname(source).downcase == ".rb"
|
|
1699
|
-
ruby_source =
|
|
1700
|
-
if File.basename(source) == "main.rb" && project_root
|
|
1701
|
-
bundled_embedded_ruby_source(source, project_root)
|
|
1702
|
-
else
|
|
1703
|
-
File.read(source)
|
|
1704
|
-
end
|
|
1705
|
-
File.write(destination, normalize_embedded_ruby_source(ruby_source))
|
|
1706
|
-
else
|
|
1707
|
-
FileUtils.cp(source, destination)
|
|
1708
|
-
end
|
|
1709
|
-
end
|
|
1710
|
-
|
|
1711
|
-
def bundled_embedded_ruby_source(source, project_root, visited = {})
|
|
1712
|
-
absolute = File.expand_path(source)
|
|
1713
|
-
return "" if visited[absolute]
|
|
1714
|
-
|
|
1715
|
-
visited[absolute] = true
|
|
1716
|
-
base = File.dirname(absolute)
|
|
1717
|
-
File.read(absolute).lines.map do |line|
|
|
1718
|
-
match = line.match(/^\s*require_relative\s+["']([^"']+)["']\s*$/)
|
|
1719
|
-
next line unless match
|
|
1720
|
-
|
|
1721
|
-
required = File.expand_path(match[1], base)
|
|
1722
|
-
required = "#{required}.rb" unless File.file?(required)
|
|
1723
|
-
next line unless File.file?(required) && required.start_with?(File.expand_path(project_root))
|
|
1724
|
-
|
|
1725
|
-
bundled_embedded_ruby_source(required, project_root, visited)
|
|
1726
|
-
end.join
|
|
1727
|
-
end
|
|
1728
|
-
|
|
1729
|
-
def normalize_embedded_ruby_source(source)
|
|
1730
|
-
source.lines.map do |line|
|
|
1731
|
-
expand_endless_method_line(line)
|
|
1732
|
-
end.join
|
|
1733
|
-
end
|
|
1734
|
-
|
|
1735
|
-
def expand_endless_method_line(line)
|
|
1736
|
-
match = line.match(/^(\s*)def\s+(.+)$/)
|
|
1737
|
-
return line unless match
|
|
1738
|
-
|
|
1739
|
-
indent = match[1]
|
|
1740
|
-
body = match[2].chomp
|
|
1741
|
-
newline = line.end_with?("\n") ? "\n" : ""
|
|
1742
|
-
split = endless_method_split(body)
|
|
1743
|
-
return line unless split
|
|
1744
|
-
|
|
1745
|
-
signature, expression = split
|
|
1746
|
-
"#{indent}def #{signature.rstrip}\n#{indent} #{expression.lstrip}\n#{indent}end#{newline}"
|
|
1747
|
-
end
|
|
1748
|
-
|
|
1749
|
-
def endless_method_split(body)
|
|
1750
|
-
depth = 0
|
|
1751
|
-
quote = nil
|
|
1752
|
-
escape = false
|
|
1753
|
-
|
|
1754
|
-
body.each_char.with_index do |char, index|
|
|
1755
|
-
if quote
|
|
1756
|
-
escape = char == "\\" && !escape
|
|
1757
|
-
if char == quote && !escape
|
|
1758
|
-
quote = nil
|
|
1759
|
-
elsif char != "\\"
|
|
1760
|
-
escape = false
|
|
1761
|
-
end
|
|
1762
|
-
next
|
|
1763
|
-
end
|
|
1764
|
-
|
|
1765
|
-
case char
|
|
1766
|
-
when "'", '"'
|
|
1767
|
-
quote = char
|
|
1768
|
-
when "(", "[", "{"
|
|
1769
|
-
depth += 1
|
|
1770
|
-
when ")", "]", "}"
|
|
1771
|
-
depth -= 1 if depth.positive?
|
|
1772
|
-
when "="
|
|
1773
|
-
next unless depth.zero? && body[index + 1] == " "
|
|
1774
|
-
|
|
1775
|
-
signature = body[0...index]
|
|
1776
|
-
expression = body[(index + 1)..]
|
|
1777
|
-
return [signature, expression] unless signature.strip.empty? || expression.to_s.strip.empty?
|
|
1778
|
-
end
|
|
1779
|
-
end
|
|
1780
|
-
|
|
1781
|
-
nil
|
|
1782
|
-
end
|
|
1783
|
-
|
|
1784
1283
|
def remove_self_contained_project_assets(client_dir, verbose: false)
|
|
1785
1284
|
assets_root = File.join(client_dir, "assets")
|
|
1786
|
-
legacy_entrypoint = File.join(client_dir, "assets", "main.rb")
|
|
1787
|
-
FileUtils.rm_f(legacy_entrypoint)
|
|
1788
1285
|
removed = false
|
|
1789
1286
|
|
|
1790
1287
|
project_root = File.join(assets_root, self_contained_project_name)
|
|
@@ -1793,12 +1290,6 @@ module Ruflet
|
|
|
1793
1290
|
removed = true
|
|
1794
1291
|
end
|
|
1795
1292
|
|
|
1796
|
-
legacy_root = File.join(assets_root, "ruby_project")
|
|
1797
|
-
if Dir.exist?(legacy_root)
|
|
1798
|
-
FileUtils.rm_rf(legacy_root)
|
|
1799
|
-
removed = true
|
|
1800
|
-
end
|
|
1801
|
-
|
|
1802
1293
|
build_log(verbose, "removed embedded self-contained project assets") if removed
|
|
1803
1294
|
end
|
|
1804
1295
|
|
|
@@ -1809,11 +1300,12 @@ module Ruflet
|
|
|
1809
1300
|
Find.find(root.to_s) do |path|
|
|
1810
1301
|
pathname = Pathname.new(path)
|
|
1811
1302
|
relative = pathname.relative_path_from(root).to_s
|
|
1812
|
-
next if relative.empty?
|
|
1303
|
+
next if relative.empty? || relative == "."
|
|
1813
1304
|
|
|
1814
1305
|
if pathname.directory?
|
|
1815
1306
|
if skip_project_asset_directory?(relative)
|
|
1816
1307
|
Find.prune
|
|
1308
|
+
next
|
|
1817
1309
|
else
|
|
1818
1310
|
next
|
|
1819
1311
|
end
|
|
@@ -1834,8 +1326,7 @@ module Ruflet
|
|
|
1834
1326
|
end
|
|
1835
1327
|
|
|
1836
1328
|
def skip_project_asset_directory?(relative)
|
|
1837
|
-
|
|
1838
|
-
%w[
|
|
1329
|
+
excluded_directories = %w[
|
|
1839
1330
|
.git
|
|
1840
1331
|
.bundle
|
|
1841
1332
|
.dart_tool
|
|
@@ -1850,21 +1341,17 @@ module Ruflet
|
|
|
1850
1341
|
ruflet_client
|
|
1851
1342
|
tmp
|
|
1852
1343
|
vendor
|
|
1853
|
-
]
|
|
1344
|
+
]
|
|
1345
|
+
relative.split(File::SEPARATOR).any? do |component|
|
|
1346
|
+
component.start_with?(".") || excluded_directories.include?(component)
|
|
1347
|
+
end
|
|
1854
1348
|
end
|
|
1855
1349
|
|
|
1856
1350
|
def include_project_asset_file?(relative)
|
|
1857
1351
|
basename = File.basename(relative)
|
|
1352
|
+
return false if basename == ".DS_Store"
|
|
1858
1353
|
return false if %w[Gemfile.lock pubspec.lock Podfile.lock package-lock.json yarn.lock pnpm-lock.yaml].include?(basename)
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
ext = File.extname(relative).downcase
|
|
1862
|
-
return true if %w[.rb .json .yml .yaml].include?(ext)
|
|
1863
|
-
|
|
1864
|
-
first = relative.split(File::SEPARATOR).first
|
|
1865
|
-
return true if first == "assets"
|
|
1866
|
-
|
|
1867
|
-
false
|
|
1354
|
+
true
|
|
1868
1355
|
end
|
|
1869
1356
|
|
|
1870
1357
|
def flutter_target_entrypoint(client_dir, self_contained:)
|
|
@@ -1914,50 +1401,6 @@ module Ruflet
|
|
|
1914
1401
|
write_pubspec_yaml(path, data)
|
|
1915
1402
|
end
|
|
1916
1403
|
|
|
1917
|
-
def sync_client_flet_packages(client_dir, selected_packages)
|
|
1918
|
-
template_root =
|
|
1919
|
-
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
1920
|
-
Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
1921
|
-
end
|
|
1922
|
-
return unless template_root
|
|
1923
|
-
|
|
1924
|
-
source_root = File.join(template_root, "flet_packages")
|
|
1925
|
-
target_root = File.join(client_dir, "flet_packages")
|
|
1926
|
-
return unless Dir.exist?(source_root)
|
|
1927
|
-
return if File.expand_path(source_root) == File.expand_path(target_root)
|
|
1928
|
-
|
|
1929
|
-
# The repository's standalone client intentionally carries the full
|
|
1930
|
-
# local package catalog. Only generated/template-derived clients are
|
|
1931
|
-
# conditioned for a particular Ruflet application.
|
|
1932
|
-
return if standalone_ruflet_client_source?(client_dir, template_root)
|
|
1933
|
-
|
|
1934
|
-
known_packages = CLIENT_EXTENSION_MAP.values.map { |meta| meta.fetch(:package) }.uniq
|
|
1935
|
-
required_packages = (["flet"] + selected_packages).uniq
|
|
1936
|
-
|
|
1937
|
-
FileUtils.mkdir_p(target_root)
|
|
1938
|
-
required_packages.each do |package_name|
|
|
1939
|
-
source = File.join(source_root, package_name)
|
|
1940
|
-
target = File.join(target_root, package_name)
|
|
1941
|
-
next unless Dir.exist?(source)
|
|
1942
|
-
next if Dir.exist?(target)
|
|
1943
|
-
|
|
1944
|
-
FileUtils.cp_r(source, target)
|
|
1945
|
-
end
|
|
1946
|
-
|
|
1947
|
-
(known_packages - selected_packages).each do |package_name|
|
|
1948
|
-
FileUtils.rm_rf(File.join(target_root, package_name))
|
|
1949
|
-
end
|
|
1950
|
-
end
|
|
1951
|
-
|
|
1952
|
-
def standalone_ruflet_client_source?(client_dir, template_root)
|
|
1953
|
-
client_root = File.expand_path(client_dir)
|
|
1954
|
-
candidates = [
|
|
1955
|
-
File.expand_path("../../ruflet_client", template_root),
|
|
1956
|
-
File.expand_path("../../../../../ruflet_client", __dir__)
|
|
1957
|
-
]
|
|
1958
|
-
candidates.any? { |candidate| File.expand_path(candidate) == client_root }
|
|
1959
|
-
end
|
|
1960
|
-
|
|
1961
1404
|
def sync_client_extension_dependencies(path, selected_packages)
|
|
1962
1405
|
return if selected_packages.empty?
|
|
1963
1406
|
|
|
@@ -1966,30 +1409,14 @@ module Ruflet
|
|
|
1966
1409
|
|
|
1967
1410
|
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
1968
1411
|
deps = (data["dependencies"] || {}).dup
|
|
1969
|
-
if selected_packages.include?("flet_rive")
|
|
1970
|
-
deps.delete("rive")
|
|
1971
|
-
deps.delete("rive_native")
|
|
1972
|
-
end
|
|
1973
1412
|
selected_packages.each do |package_name|
|
|
1974
1413
|
deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
|
|
1975
1414
|
end
|
|
1976
1415
|
|
|
1977
1416
|
data["dependencies"] = deps
|
|
1978
|
-
sync_local_flet_dependency_override(data, deps["flet"])
|
|
1979
1417
|
write_pubspec_yaml(path, data)
|
|
1980
1418
|
end
|
|
1981
1419
|
|
|
1982
|
-
# Git-backed Flet extensions declare their own Flet source. When Ruflet
|
|
1983
|
-
# vendors the core engine locally, force every extension to resolve that
|
|
1984
|
-
# same copy instead of letting Pub reject the mixed sources.
|
|
1985
|
-
def sync_local_flet_dependency_override(data, flet_dependency)
|
|
1986
|
-
return unless flet_dependency.is_a?(Hash) && key_defined?(flet_dependency, "path")
|
|
1987
|
-
|
|
1988
|
-
overrides = data["dependency_overrides"]
|
|
1989
|
-
overrides = data["dependency_overrides"] = {} unless overrides.is_a?(Hash)
|
|
1990
|
-
overrides["flet"] = flet_dependency
|
|
1991
|
-
end
|
|
1992
|
-
|
|
1993
1420
|
def template_client_pubspec_dependencies
|
|
1994
1421
|
template_root =
|
|
1995
1422
|
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|