ruflet 0.0.18 → 0.0.20
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 +54 -1
- data/lib/ruflet/cli/build_command.rb +697 -118
- data/lib/ruflet/cli/extra_command.rb +11 -8
- data/lib/ruflet/cli/flutter_sdk.rb +42 -7
- data/lib/ruflet/cli/new_command.rb +112 -36
- data/lib/ruflet/cli/run_command.rb +326 -70
- data/lib/ruflet/cli/templates.rb +8 -5
- data/lib/ruflet/cli.rb +3 -3
- data/lib/ruflet/hot_reload/harness.rb +32 -0
- data/lib/ruflet/hot_reload.rb +226 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +3 -1
|
@@ -14,7 +14,6 @@ module Ruflet
|
|
|
14
14
|
module BuildCommand
|
|
15
15
|
include FlutterSdk
|
|
16
16
|
CLIENT_EXTENSION_MAP = {
|
|
17
|
-
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
18
17
|
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
19
18
|
"audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
20
19
|
"camera" => { package: "flet_camera", alias: "ruflet_camera" },
|
|
@@ -27,6 +26,7 @@ module Ruflet
|
|
|
27
26
|
"lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
|
|
28
27
|
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
29
28
|
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
29
|
+
"qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
|
|
30
30
|
"rive" => { package: "flet_rive", alias: "ruflet_rive" },
|
|
31
31
|
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
32
32
|
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
@@ -38,6 +38,9 @@ module Ruflet
|
|
|
38
38
|
"location" => %w[geolocator permission_handler],
|
|
39
39
|
"motion" => %w[permission_handler]
|
|
40
40
|
}.freeze
|
|
41
|
+
EXTENSION_REQUIRED_SERVICES = {
|
|
42
|
+
"qrcode_scanner" => %w[camera]
|
|
43
|
+
}.freeze
|
|
41
44
|
ANDROID_SERVICE_PERMISSIONS = {
|
|
42
45
|
"camera" => %w[android.permission.CAMERA],
|
|
43
46
|
"microphone" => %w[android.permission.RECORD_AUDIO],
|
|
@@ -50,6 +53,20 @@ module Ruflet
|
|
|
50
53
|
"location" => "NSLocationWhenInUseUsageDescription",
|
|
51
54
|
"motion" => "NSMotionUsageDescription"
|
|
52
55
|
}.freeze
|
|
56
|
+
# Clients that are told which server to use at launch rather than at build
|
|
57
|
+
# time, so they may be built without a configured backend_url.
|
|
58
|
+
RUNTIME_RESOLVED_BACKEND_PLATFORMS = %w[web macos windows linux].freeze
|
|
59
|
+
# What the underlying generators can actually produce per platform:
|
|
60
|
+
# flutter_native_splash covers android/ios/web, flutter_launcher_icons
|
|
61
|
+
# covers android/ios/web/windows/macos. Linux has neither.
|
|
62
|
+
PLATFORM_ASSET_SUPPORT = {
|
|
63
|
+
"android" => { splash: true, icon: true },
|
|
64
|
+
"ios" => { splash: true, icon: true },
|
|
65
|
+
"web" => { splash: true, icon: true },
|
|
66
|
+
"macos" => { splash: false, icon: true },
|
|
67
|
+
"windows" => { splash: false, icon: true },
|
|
68
|
+
"linux" => { splash: false, icon: false }
|
|
69
|
+
}.freeze
|
|
53
70
|
|
|
54
71
|
def command_build(args)
|
|
55
72
|
self_contained = args.delete("--self")
|
|
@@ -66,6 +83,15 @@ module Ruflet
|
|
|
66
83
|
return 1
|
|
67
84
|
end
|
|
68
85
|
|
|
86
|
+
# The embedded Ruby VM is a native plugin with no browser
|
|
87
|
+
# implementation, so a self-contained web build produces an app that
|
|
88
|
+
# cannot start. Say so rather than shipping one that hangs.
|
|
89
|
+
if self_contained && platform == "web"
|
|
90
|
+
warn "build config error: --self is not supported for web"
|
|
91
|
+
warn "A web client runs no embedded Ruby; build it with `ruflet build web`."
|
|
92
|
+
return 1
|
|
93
|
+
end
|
|
94
|
+
|
|
69
95
|
ensure_ruflet_build_assets(verbose: !!verbose)
|
|
70
96
|
client_dir = ensure_flutter_client_dir(verbose: !!verbose)
|
|
71
97
|
unless client_dir
|
|
@@ -99,15 +125,21 @@ module Ruflet
|
|
|
99
125
|
# deterministically instead of inferring from a single main.rb — the
|
|
100
126
|
# app tree now ships many main.rb files (standalone_apps/*/main.rb).
|
|
101
127
|
build_args += ["--dart-define", "RUFLET_EMBEDDED_PROJECT=#{self_contained_project_name}"]
|
|
102
|
-
|
|
103
|
-
unless backend_url
|
|
104
|
-
warn "build config error: backend_url is required for server-driven builds"
|
|
105
|
-
warn "Set app.backend_url or backend_url in ruflet.yaml"
|
|
106
|
-
return 1
|
|
107
|
-
end
|
|
128
|
+
elsif backend_url
|
|
108
129
|
build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"]
|
|
130
|
+
elsif RUNTIME_RESOLVED_BACKEND_PLATFORMS.include?(platform)
|
|
131
|
+
# These clients learn their server at launch: a web client from the
|
|
132
|
+
# origin it is served from, a desktop client from the URL the launcher
|
|
133
|
+
# passes. Baking one in would pin them to a single host and port,
|
|
134
|
+
# which a preview client cannot use.
|
|
135
|
+
build_note("No backend_url configured; the #{platform} client will resolve its server at launch")
|
|
136
|
+
else
|
|
137
|
+
warn "build config error: backend_url is required for server-driven builds"
|
|
138
|
+
warn "Set app.backend_url or backend_url in ruflet.yaml"
|
|
139
|
+
return 1
|
|
109
140
|
end
|
|
110
141
|
build_args << "-v" if verbose
|
|
142
|
+
stage_ios_simulator_ruby_runtime(client_dir, build_args, verbose: !!verbose) if self_contained
|
|
111
143
|
|
|
112
144
|
build_log(verbose, "mode=#{self_contained ? 'self' : 'server'}")
|
|
113
145
|
build_log(verbose, "client_dir=#{client_dir}")
|
|
@@ -175,13 +207,30 @@ module Ruflet
|
|
|
175
207
|
|
|
176
208
|
def ensure_flutter_client_dir(verbose: false)
|
|
177
209
|
client_dir = detect_flutter_client_dir
|
|
178
|
-
|
|
210
|
+
if client_dir
|
|
211
|
+
refresh_hidden_flutter_client_template(client_dir, verbose: verbose)
|
|
212
|
+
return client_dir
|
|
213
|
+
end
|
|
179
214
|
|
|
180
215
|
bootstrapped = bootstrap_flutter_client_template
|
|
181
216
|
build_log(verbose, "bootstrapped client template at #{bootstrapped}") if bootstrapped
|
|
182
217
|
bootstrapped
|
|
183
218
|
end
|
|
184
219
|
|
|
220
|
+
def refresh_hidden_flutter_client_template(client_dir, verbose: false)
|
|
221
|
+
return unless File.expand_path(client_dir) == File.expand_path(hidden_flutter_client_dir)
|
|
222
|
+
return unless File.file?(File.join(client_dir, ".metadata"))
|
|
223
|
+
return unless Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
224
|
+
|
|
225
|
+
template_root = Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
226
|
+
return unless template_root && Dir.exist?(template_root)
|
|
227
|
+
return if Ruflet::CLI.respond_to?(:client_template_current?, true) &&
|
|
228
|
+
Ruflet::CLI.send(:client_template_current?, client_dir, template_root)
|
|
229
|
+
|
|
230
|
+
Ruflet::CLI.send(:copy_ruflet_client_template, Dir.pwd)
|
|
231
|
+
build_log(verbose, "refreshed managed Flutter client from template #{template_root}")
|
|
232
|
+
end
|
|
233
|
+
|
|
185
234
|
def build_tool_env(env, platform, client_dir = nil)
|
|
186
235
|
return env unless %w[ios macos].include?(platform)
|
|
187
236
|
|
|
@@ -384,7 +433,9 @@ module Ruflet
|
|
|
384
433
|
|
|
385
434
|
def prepare_flutter_client(client_dir, platform:, tools:, config:, self_contained: false, verbose: false)
|
|
386
435
|
refresh_managed_client_template_files(client_dir, verbose: verbose)
|
|
387
|
-
sync_client_metadata(client_dir, config, verbose: verbose)
|
|
436
|
+
metadata = sync_client_metadata(client_dir, config, verbose: verbose)
|
|
437
|
+
return false unless validate_mobile_app_identity(metadata, platform: platform)
|
|
438
|
+
|
|
388
439
|
apply_native_service_permissions(client_dir, config)
|
|
389
440
|
configured = configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
|
|
390
441
|
return false if configured == false
|
|
@@ -408,6 +459,10 @@ module Ruflet
|
|
|
408
459
|
return false
|
|
409
460
|
end
|
|
410
461
|
|
|
462
|
+
unless apply_mobile_package_name(client_dir, metadata, platform: platform, tools: tools, verbose: verbose)
|
|
463
|
+
return false
|
|
464
|
+
end
|
|
465
|
+
|
|
411
466
|
unless ensure_native_build_dependencies(client_dir, platform, tools[:env], verbose: verbose)
|
|
412
467
|
return false
|
|
413
468
|
end
|
|
@@ -430,9 +485,60 @@ module Ruflet
|
|
|
430
485
|
end
|
|
431
486
|
end
|
|
432
487
|
|
|
488
|
+
verify_android_generated_assets(client_dir, asset_flags, platform, verbose: verbose)
|
|
489
|
+
|
|
433
490
|
true
|
|
434
491
|
end
|
|
435
492
|
|
|
493
|
+
# The generators can succeed while silently skipping Android output, which
|
|
494
|
+
# ships a build with the stock Flutter icon and splash. Confirm the native
|
|
495
|
+
# resources that ruflet.yaml asked for are actually on disk.
|
|
496
|
+
def verify_android_generated_assets(client_dir, asset_flags, platform, verbose: false)
|
|
497
|
+
return true unless %w[apk android aab appbundle].include?(platform.to_s)
|
|
498
|
+
|
|
499
|
+
res_dir = File.join(client_dir, "android", "app", "src", "main", "res")
|
|
500
|
+
return true unless File.directory?(res_dir)
|
|
501
|
+
|
|
502
|
+
ok = true
|
|
503
|
+
|
|
504
|
+
if asset_flags[:has_splash]
|
|
505
|
+
launch_background = File.join(res_dir, "drawable", "launch_background.xml")
|
|
506
|
+
if !File.file?(launch_background) || !read_text_file(launch_background).include?("splash")
|
|
507
|
+
warn "Android splash screen was not generated in res/drawable/launch_background.xml"
|
|
508
|
+
ok = false
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
styles_v31 = File.join(res_dir, "values-v31", "styles.xml")
|
|
512
|
+
if !File.file?(styles_v31) || !read_text_file(styles_v31).include?("windowSplashScreenBackground")
|
|
513
|
+
warn "Android 12+ splash screen is missing from res/values-v31/styles.xml; " \
|
|
514
|
+
"devices on Android 12 and newer will show the system default splash"
|
|
515
|
+
ok = false
|
|
516
|
+
else
|
|
517
|
+
build_log(verbose, "android 12+ splash present in values-v31/styles.xml")
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
if asset_flags[:has_icon]
|
|
522
|
+
adaptive_icon = File.join(res_dir, "mipmap-anydpi-v26", "launcher_icon.xml")
|
|
523
|
+
if File.file?(adaptive_icon)
|
|
524
|
+
build_log(verbose, "adaptive launcher icon present in mipmap-anydpi-v26")
|
|
525
|
+
else
|
|
526
|
+
warn "Android adaptive launcher icon was not generated in res/mipmap-anydpi-v26/; " \
|
|
527
|
+
"set android.adaptive_icon_foreground and android.adaptive_icon_background in ruflet.yaml"
|
|
528
|
+
ok = false
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
532
|
+
if File.file?(manifest) && !read_text_file(manifest).include?("@mipmap/launcher_icon")
|
|
533
|
+
warn "AndroidManifest.xml does not reference @mipmap/launcher_icon; the configured launcher icon is unused"
|
|
534
|
+
ok = false
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
build_note("Android launcher icon and splash resources verified") if ok && (asset_flags[:has_icon] || asset_flags[:has_splash])
|
|
539
|
+
ok
|
|
540
|
+
end
|
|
541
|
+
|
|
436
542
|
def ensure_flutter_platform_artifacts(client_dir, platform, env, flutter, verbose: false)
|
|
437
543
|
precache_flags = flutter_precache_flags(platform)
|
|
438
544
|
return true if precache_flags.empty?
|
|
@@ -479,6 +585,50 @@ module Ruflet
|
|
|
479
585
|
end
|
|
480
586
|
end
|
|
481
587
|
|
|
588
|
+
# CocoaPods can incorrectly treat the static-library XCFramework copy
|
|
589
|
+
# phase as up to date after Ruflet clears Debug-iphonesimulator. The
|
|
590
|
+
# Runner then links with -lruflet_vm while the selected simulator slice
|
|
591
|
+
# is absent. Stage that deterministic slice before Flutter invokes
|
|
592
|
+
# Xcode; CocoaPods may still copy over it normally.
|
|
593
|
+
def stage_ios_simulator_ruby_runtime(client_dir, build_args, verbose: false)
|
|
594
|
+
return true unless build_args.include?("ios")
|
|
595
|
+
return true unless build_args.include?("--simulator")
|
|
596
|
+
|
|
597
|
+
runtime_root = explicit_local_ruby_runtime_path || source_checkout_ruby_runtime_path
|
|
598
|
+
return true unless runtime_root
|
|
599
|
+
|
|
600
|
+
source = File.join(
|
|
601
|
+
runtime_root,
|
|
602
|
+
"ios",
|
|
603
|
+
"Frameworks",
|
|
604
|
+
"RufletVM.xcframework",
|
|
605
|
+
"ios-arm64_x86_64-simulator"
|
|
606
|
+
)
|
|
607
|
+
library = File.join(source, "libruflet_vm.a")
|
|
608
|
+
return true unless File.file?(library)
|
|
609
|
+
|
|
610
|
+
destination = File.join(
|
|
611
|
+
client_dir,
|
|
612
|
+
"build",
|
|
613
|
+
"ios",
|
|
614
|
+
"Debug-iphonesimulator",
|
|
615
|
+
"XCFrameworkIntermediates",
|
|
616
|
+
"ruby_runtime"
|
|
617
|
+
)
|
|
618
|
+
FileUtils.mkdir_p(destination)
|
|
619
|
+
FileUtils.cp(library, File.join(destination, "libruflet_vm.a"))
|
|
620
|
+
|
|
621
|
+
headers = File.join(source, "Headers")
|
|
622
|
+
if Dir.exist?(headers)
|
|
623
|
+
destination_headers = File.join(destination, "Headers")
|
|
624
|
+
FileUtils.rm_rf(destination_headers)
|
|
625
|
+
FileUtils.cp_r(headers, destination_headers)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
build_log(verbose, "staged iOS simulator Ruflet VM at #{destination}")
|
|
629
|
+
true
|
|
630
|
+
end
|
|
631
|
+
|
|
482
632
|
def ensure_cocoapods_install(client_dir, platform_dir, env, verbose: false)
|
|
483
633
|
pod_dir = File.join(client_dir, platform_dir)
|
|
484
634
|
return true unless Dir.exist?(pod_dir)
|
|
@@ -533,7 +683,7 @@ module Ruflet
|
|
|
533
683
|
shim_dir = File.join(client_dir, ".ruflet", "bin")
|
|
534
684
|
FileUtils.mkdir_p(shim_dir)
|
|
535
685
|
shim_path = File.join(shim_dir, "pod")
|
|
536
|
-
|
|
686
|
+
write_text_file(
|
|
537
687
|
shim_path,
|
|
538
688
|
<<~SH
|
|
539
689
|
#!/bin/sh
|
|
@@ -583,12 +733,16 @@ module Ruflet
|
|
|
583
733
|
alt = "ruflet.yml"
|
|
584
734
|
config_path = alt if File.file?(alt)
|
|
585
735
|
end
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
services_path = File.join(
|
|
736
|
+
config_exists = File.file?(config_path)
|
|
737
|
+
config = config_exists ? YAML.safe_load(read_text_file(config_path), aliases: true) || {} : {}
|
|
738
|
+
config_dir = File.dirname(File.expand_path(config_path))
|
|
739
|
+
services_path = File.join(config_dir, "services.yaml")
|
|
590
740
|
if File.file?(services_path)
|
|
591
|
-
service_config = YAML.safe_load(
|
|
741
|
+
service_config = YAML.safe_load(read_text_file(services_path), aliases: true) || {}
|
|
742
|
+
if service_config["app"].is_a?(Hash)
|
|
743
|
+
config["app"] = (config["app"].is_a?(Hash) ? config["app"] : {}).merge(service_config["app"])
|
|
744
|
+
config["_app_identity_source"] = "services.yaml"
|
|
745
|
+
end
|
|
592
746
|
config["services"] = service_config["services"] if service_config.key?("services")
|
|
593
747
|
end
|
|
594
748
|
config
|
|
@@ -624,16 +778,93 @@ module Ruflet
|
|
|
624
778
|
splash = resolve_asset.call(build["splash_screen"] || assets["splash_screen"] || config["splash_screen"])
|
|
625
779
|
splash_dark = resolve_asset.call(build["splash_dark"] || build["splash_dark_image"] || assets["splash_dark"])
|
|
626
780
|
icon = resolve_asset.call(build["icon_launcher"] || assets["icon_launcher"] || config["icon_launcher"])
|
|
627
|
-
icon_android = resolve_asset.call(build["icon_android"] || assets["icon_android"])
|
|
628
|
-
icon_ios = resolve_asset.call(build["icon_ios"] || assets["icon_ios"])
|
|
629
|
-
icon_web = resolve_asset.call(build["icon_web"] || assets["icon_web"])
|
|
630
|
-
icon_windows = resolve_asset.call(build["icon_windows"] || assets["icon_windows"])
|
|
631
|
-
icon_macos = resolve_asset.call(build["icon_macos"] || assets["icon_macos"])
|
|
632
781
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
782
|
+
# Splash and icon appearance belongs with the assets it styles. `build`
|
|
783
|
+
# is still read first so existing projects keep working.
|
|
784
|
+
splash_color = build["splash_color"] || assets["splash_color"]
|
|
785
|
+
splash_dark_color = build["splash_dark_color"] || build["splash_color_dark"] ||
|
|
786
|
+
assets["splash_dark_color"] || assets["splash_color_dark"]
|
|
787
|
+
icon_background = build["icon_background"] || assets["icon_background"]
|
|
788
|
+
theme_color = build["theme_color"] || assets["theme_color"]
|
|
789
|
+
|
|
790
|
+
# Every platform gets its own section with the same key names, falling
|
|
791
|
+
# back to the shared assets/build values when a key is not overridden.
|
|
792
|
+
platforms = PLATFORM_ASSET_SUPPORT.keys.each_with_object({}) do |name, resolved|
|
|
793
|
+
section = platform_build_config(config, name)
|
|
794
|
+
resolved[name] = {
|
|
795
|
+
config: section,
|
|
796
|
+
splash: resolve_asset.call(
|
|
797
|
+
section["splash_screen"] || section["splash_image"] ||
|
|
798
|
+
build["splash_#{name}"] || assets["splash_#{name}"]
|
|
799
|
+
),
|
|
800
|
+
splash_dark: resolve_asset.call(
|
|
801
|
+
section["splash_dark"] || section["splash_dark_image"] || assets["splash_#{name}_dark"]
|
|
802
|
+
),
|
|
803
|
+
icon: resolve_asset.call(
|
|
804
|
+
section["icon_launcher"] || section["icon"] ||
|
|
805
|
+
build["icon_#{name}"] || assets["icon_#{name}"]
|
|
806
|
+
),
|
|
807
|
+
background_image: resolve_asset.call(
|
|
808
|
+
section["splash_background_image"] || section["background_image"] || assets["splash_background_#{name}"]
|
|
809
|
+
),
|
|
810
|
+
background_image_dark: resolve_asset.call(
|
|
811
|
+
section["splash_background_image_dark"] || section["background_image_dark"]
|
|
812
|
+
),
|
|
813
|
+
branding: resolve_asset.call(section["splash_branding"] || section["branding"]),
|
|
814
|
+
branding_dark: resolve_asset.call(section["splash_branding_dark"] || section["branding_dark"]),
|
|
815
|
+
splash_color: section["splash_color"] || splash_color,
|
|
816
|
+
splash_dark_color: section["splash_dark_color"] || section["splash_color_dark"] || splash_dark_color,
|
|
817
|
+
icon_background: section["icon_background"] || icon_background,
|
|
818
|
+
theme_color: section["theme_color"] || theme_color
|
|
819
|
+
}
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
splash_background_image = resolve_asset.call(
|
|
823
|
+
build["splash_background_image"] || assets["splash_background_image"] || build["background_image"]
|
|
824
|
+
)
|
|
825
|
+
splash_background_image_dark = resolve_asset.call(
|
|
826
|
+
build["splash_background_image_dark"] || assets["splash_background_image_dark"]
|
|
827
|
+
)
|
|
828
|
+
splash_branding = resolve_asset.call(build["splash_branding"] || assets["splash_branding"])
|
|
829
|
+
splash_branding_dark = resolve_asset.call(build["splash_branding_dark"] || assets["splash_branding_dark"])
|
|
830
|
+
splash_branding_mode = build["splash_branding_mode"] || build["branding_mode"] ||
|
|
831
|
+
assets["splash_branding_mode"] || assets["branding_mode"]
|
|
832
|
+
splash_branding_padding = build["splash_branding_bottom_padding"] || build["branding_bottom_padding"] ||
|
|
833
|
+
assets["splash_branding_bottom_padding"] || assets["branding_bottom_padding"]
|
|
834
|
+
|
|
835
|
+
android = platforms.dig("android", :config)
|
|
836
|
+
android_splash = platforms.dig("android", :splash)
|
|
837
|
+
android_splash_dark = platforms.dig("android", :splash_dark)
|
|
838
|
+
android_12_splash = resolve_asset.call(
|
|
839
|
+
android["splash_android_12"] || android["android_12_image"] || assets["splash_android_12"]
|
|
840
|
+
)
|
|
841
|
+
android_12_splash_dark = resolve_asset.call(
|
|
842
|
+
android["splash_android_12_dark"] || android["android_12_image_dark"] || assets["splash_android_12_dark"]
|
|
843
|
+
)
|
|
844
|
+
adaptive_foreground = resolve_asset.call(
|
|
845
|
+
android["adaptive_icon_foreground"] || android["icon_foreground"] ||
|
|
846
|
+
assets["icon_adaptive_foreground"] || assets["icon_foreground"]
|
|
847
|
+
)
|
|
848
|
+
adaptive_background_image = resolve_asset.call(
|
|
849
|
+
android["adaptive_icon_background_image"] || assets["icon_adaptive_background_image"]
|
|
850
|
+
)
|
|
851
|
+
adaptive_monochrome = resolve_asset.call(
|
|
852
|
+
android["adaptive_icon_monochrome"] || android["icon_monochrome"] || assets["icon_adaptive_monochrome"]
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
android_splash_color = platforms.dig("android", :splash_color)
|
|
856
|
+
android_splash_dark_color = platforms.dig("android", :splash_dark_color)
|
|
857
|
+
android_12_icon_background = android["splash_android_12_icon_background_color"] ||
|
|
858
|
+
android["icon_background_color"] || android_splash_color
|
|
859
|
+
android_12_icon_background_dark = android["splash_android_12_icon_background_color_dark"] ||
|
|
860
|
+
android["icon_background_color_dark"] || android_splash_dark_color
|
|
861
|
+
android_12_color = android["splash_android_12_color"] || android["android_12_color"]
|
|
862
|
+
android_12_color_dark = android["splash_android_12_color_dark"] || android["android_12_color_dark"]
|
|
863
|
+
android_12_branding = resolve_asset.call(android["splash_android_12_branding"] || android["android_12_branding"])
|
|
864
|
+
adaptive_background_color = android["adaptive_icon_background"] || icon_background
|
|
865
|
+
android_min_sdk = android["min_sdk"] || android["min_sdk_android"] || build["min_sdk_android"]
|
|
866
|
+
android_splash_fullscreen = first_defined(android, "splash_fullscreen", "fullscreen")
|
|
867
|
+
android_splash_gravity = android["splash_gravity"] || android["android_gravity"]
|
|
637
868
|
|
|
638
869
|
assets_dir = File.join(client_dir, "assets")
|
|
639
870
|
FileUtils.mkdir_p(assets_dir)
|
|
@@ -646,14 +877,43 @@ module Ruflet
|
|
|
646
877
|
copy_asset.call(splash, "splash.png")
|
|
647
878
|
copy_asset.call(splash_dark, "splash_dark.png")
|
|
648
879
|
copy_asset.call(icon, "icon.png")
|
|
649
|
-
copy_asset.call(
|
|
650
|
-
copy_asset.call(
|
|
651
|
-
copy_asset.call(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
880
|
+
copy_asset.call(splash_background_image, "splash_background.png")
|
|
881
|
+
copy_asset.call(splash_background_image_dark, "splash_background_dark.png")
|
|
882
|
+
copy_asset.call(splash_branding, "splash_branding.png")
|
|
883
|
+
copy_asset.call(splash_branding_dark, "splash_branding_dark.png")
|
|
884
|
+
|
|
885
|
+
platforms.each do |name, entry|
|
|
886
|
+
support = PLATFORM_ASSET_SUPPORT.fetch(name)
|
|
887
|
+
if support[:splash]
|
|
888
|
+
copy_asset.call(entry[:splash], "splash_#{name}.png")
|
|
889
|
+
copy_asset.call(entry[:splash_dark], "splash_#{name}_dark.png")
|
|
890
|
+
copy_asset.call(entry[:background_image], "splash_background_#{name}.png")
|
|
891
|
+
copy_asset.call(entry[:background_image_dark], "splash_background_#{name}_dark.png")
|
|
892
|
+
copy_asset.call(entry[:branding], "splash_branding_#{name}.png")
|
|
893
|
+
copy_asset.call(entry[:branding_dark], "splash_branding_#{name}_dark.png")
|
|
894
|
+
elsif entry[:splash] || entry[:splash_dark]
|
|
895
|
+
build_note("#{name} has no splash screen generator; ignoring #{name}.splash_screen")
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
next unless entry[:icon]
|
|
899
|
+
|
|
900
|
+
unless support[:icon]
|
|
901
|
+
build_note("#{name} has no launcher icon generator; ignoring #{name}.icon_launcher")
|
|
902
|
+
next
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
if name == "windows" && File.extname(entry[:icon]).downcase == ".ico"
|
|
906
|
+
copy_asset.call(entry[:icon], "icon_windows.ico")
|
|
907
|
+
else
|
|
908
|
+
copy_asset.call(entry[:icon], "icon_#{name}.png")
|
|
909
|
+
end
|
|
655
910
|
end
|
|
656
|
-
|
|
911
|
+
|
|
912
|
+
copy_asset.call(android_12_splash, "splash_android_12.png")
|
|
913
|
+
copy_asset.call(android_12_splash_dark, "splash_android_12_dark.png")
|
|
914
|
+
copy_asset.call(adaptive_foreground, "icon_foreground.png")
|
|
915
|
+
copy_asset.call(adaptive_background_image, "icon_background.png")
|
|
916
|
+
copy_asset.call(adaptive_monochrome, "icon_monochrome.png")
|
|
657
917
|
|
|
658
918
|
default_splash = File.file?(File.join(assets_dir, "splash.png"))
|
|
659
919
|
default_icon = File.file?(File.join(assets_dir, "icon.png"))
|
|
@@ -678,43 +938,235 @@ module Ruflet
|
|
|
678
938
|
end
|
|
679
939
|
end
|
|
680
940
|
|
|
681
|
-
|
|
682
|
-
|
|
941
|
+
# A project may configure nothing shared and declare everything under the
|
|
942
|
+
# platform sections, so a platform asset alone has to run the generators.
|
|
943
|
+
platform_splash = platforms.any? { |name, entry| PLATFORM_ASSET_SUPPORT.fetch(name)[:splash] && entry[:splash] }
|
|
944
|
+
platform_icon = platforms.any? { |name, entry| PLATFORM_ASSET_SUPPORT.fetch(name)[:icon] && entry[:icon] }
|
|
945
|
+
|
|
946
|
+
platforms.each do |name, entry|
|
|
947
|
+
support = PLATFORM_ASSET_SUPPORT.fetch(name)
|
|
948
|
+
if support[:splash] && entry[:splash].nil? && key_defined?(entry[:config], "splash_screen")
|
|
949
|
+
build_note("#{name}.splash_screen was set but the file was not found")
|
|
950
|
+
end
|
|
951
|
+
if support[:icon] && entry[:icon].nil? && key_defined?(entry[:config], "icon_launcher")
|
|
952
|
+
build_note("#{name}.icon_launcher was set but the file was not found")
|
|
953
|
+
end
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
shared_splash_asset = !splash.nil? || default_splash
|
|
957
|
+
shared_icon_asset = !icon.nil? || default_icon
|
|
958
|
+
has_splash = shared_splash_asset || platform_splash
|
|
959
|
+
has_icon = shared_icon_asset || platform_icon
|
|
960
|
+
|
|
961
|
+
# Fall back to whatever Android configured when nothing is shared.
|
|
962
|
+
effective_icon_background = icon_background || platforms.dig("android", :icon_background)
|
|
963
|
+
effective_theme_color = theme_color || platforms.dig("android", :theme_color)
|
|
683
964
|
|
|
684
965
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
685
966
|
unless File.file?(pubspec_path)
|
|
686
967
|
return { has_icon: has_icon, has_splash: has_splash, error: nil }
|
|
687
968
|
end
|
|
688
969
|
|
|
970
|
+
ensure_pubspec_block(pubspec_path, "flutter_launcher_icons") if has_icon
|
|
971
|
+
ensure_pubspec_block(pubspec_path, "flutter_native_splash") if has_splash
|
|
972
|
+
|
|
689
973
|
if has_icon
|
|
690
|
-
|
|
974
|
+
if shared_icon_asset
|
|
975
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path", "\"assets/icon.png\"", multiple: true)
|
|
976
|
+
end
|
|
977
|
+
# Android 8+ renders adaptive icons. Without these keys flutter_launcher_icons
|
|
978
|
+
# never writes mipmap-anydpi-v26/, and the launcher falls back to the legacy
|
|
979
|
+
# bitmap, ignoring icon_background entirely.
|
|
980
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "android", "launcher_icon", multiple: true)
|
|
981
|
+
adaptive_foreground_path =
|
|
982
|
+
if adaptive_foreground then "assets/icon_foreground.png"
|
|
983
|
+
elsif platforms.dig("android", :icon) then "assets/icon_android.png"
|
|
984
|
+
else "assets/icon.png"
|
|
985
|
+
end
|
|
986
|
+
update_pubspec_value(
|
|
987
|
+
pubspec_path, "flutter_launcher_icons", "adaptive_icon_foreground",
|
|
988
|
+
"\"#{adaptive_foreground_path}\"", multiple: true
|
|
989
|
+
)
|
|
990
|
+
adaptive_background_value =
|
|
991
|
+
if adaptive_background_image
|
|
992
|
+
"\"assets/icon_background.png\""
|
|
993
|
+
elsif adaptive_background_color
|
|
994
|
+
"\"#{adaptive_background_color}\""
|
|
995
|
+
end
|
|
996
|
+
if adaptive_background_value
|
|
997
|
+
update_pubspec_value(
|
|
998
|
+
pubspec_path, "flutter_launcher_icons", "adaptive_icon_background",
|
|
999
|
+
adaptive_background_value, multiple: true
|
|
1000
|
+
)
|
|
1001
|
+
end
|
|
1002
|
+
if adaptive_monochrome
|
|
1003
|
+
update_pubspec_value(
|
|
1004
|
+
pubspec_path, "flutter_launcher_icons", "adaptive_icon_monochrome",
|
|
1005
|
+
"\"assets/icon_monochrome.png\"", multiple: true
|
|
1006
|
+
)
|
|
1007
|
+
end
|
|
1008
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "min_sdk_android", android_min_sdk.to_s) if android_min_sdk
|
|
691
1009
|
end
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1010
|
+
if has_icon
|
|
1011
|
+
# flutter_launcher_icons takes android/ios as flat image_path_* keys but
|
|
1012
|
+
# web/windows/macos as nested platform blocks.
|
|
1013
|
+
if platforms.dig("android", :icon)
|
|
1014
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_android", "\"assets/icon_android.png\"", multiple: true)
|
|
1015
|
+
end
|
|
1016
|
+
if platforms.dig("ios", :icon)
|
|
1017
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_ios", "\"assets/icon_ios.png\"", multiple: true)
|
|
1018
|
+
end
|
|
1019
|
+
if (remove_alpha = first_defined(platforms.dig("ios", :config), "remove_alpha", "remove_alpha_ios"))
|
|
1020
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "remove_alpha_ios", remove_alpha ? "true" : "false")
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
%w[web windows macos].each do |name|
|
|
1024
|
+
entry = platforms.fetch(name)
|
|
1025
|
+
section = entry[:config]
|
|
1026
|
+
next if entry[:icon].nil? && section.empty?
|
|
1027
|
+
|
|
1028
|
+
update_pubspec_nested_value(pubspec_path, "flutter_launcher_icons", name, "generate", "true")
|
|
1029
|
+
if entry[:icon]
|
|
1030
|
+
image = if name == "windows" && File.extname(entry[:icon]).downcase == ".ico"
|
|
1031
|
+
"assets/icon_windows.ico"
|
|
1032
|
+
else
|
|
1033
|
+
"assets/icon_#{name}.png"
|
|
1034
|
+
end
|
|
1035
|
+
update_pubspec_nested_value(pubspec_path, "flutter_launcher_icons", name, "image_path", "\"#{image}\"")
|
|
1036
|
+
end
|
|
1037
|
+
if name == "web"
|
|
1038
|
+
update_pubspec_nested_value(pubspec_path, "flutter_launcher_icons", "web", "background_color", "\"#{entry[:icon_background]}\"") if entry[:icon_background]
|
|
1039
|
+
update_pubspec_nested_value(pubspec_path, "flutter_launcher_icons", "web", "theme_color", "\"#{entry[:theme_color]}\"") if entry[:theme_color]
|
|
1040
|
+
end
|
|
1041
|
+
if name == "windows" && (icon_size = section["icon_size"])
|
|
1042
|
+
update_pubspec_nested_value(pubspec_path, "flutter_launcher_icons", "windows", "icon_size", icon_size.to_s)
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
699
1045
|
end
|
|
700
|
-
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "
|
|
701
|
-
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "
|
|
702
|
-
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "theme_color", "\"#{theme_color}\"") if theme_color
|
|
1046
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "background_color", "\"#{effective_icon_background}\"") if effective_icon_background
|
|
1047
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "theme_color", "\"#{effective_theme_color}\"") if effective_theme_color
|
|
703
1048
|
|
|
704
|
-
update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if
|
|
1049
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if shared_splash_asset
|
|
705
1050
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark", "\"assets/splash_dark.png\"") if splash_dark
|
|
706
1051
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "color", "\"#{splash_color}\"") if splash_color
|
|
707
1052
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark", "\"#{splash_dark_color}\"") if splash_dark_color
|
|
708
1053
|
|
|
1054
|
+
if has_splash
|
|
1055
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "background_image", "\"assets/splash_background.png\"") if splash_background_image
|
|
1056
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "background_image_dark", "\"assets/splash_background_dark.png\"") if splash_background_image_dark
|
|
1057
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding", "\"assets/splash_branding.png\"") if splash_branding
|
|
1058
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding_dark", "\"assets/splash_branding_dark.png\"") if splash_branding_dark
|
|
1059
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding_mode", splash_branding_mode.to_s) if splash_branding_mode
|
|
1060
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding_bottom_padding", splash_branding_padding.to_s) if splash_branding_padding
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
if has_splash
|
|
1064
|
+
# flutter_native_splash only generates for android, ios, and web; each
|
|
1065
|
+
# takes the shared keys suffixed with the platform name.
|
|
1066
|
+
%w[android ios web].each do |name|
|
|
1067
|
+
entry = platforms.fetch(name)
|
|
1068
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", name, "true")
|
|
1069
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "image_#{name}", "\"assets/splash_#{name}.png\"") if entry[:splash]
|
|
1070
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark_#{name}", "\"assets/splash_#{name}_dark.png\"") if entry[:splash_dark]
|
|
1071
|
+
if entry[:splash_color] && entry[:splash_color] != splash_color
|
|
1072
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "color_#{name}", "\"#{entry[:splash_color]}\"")
|
|
1073
|
+
end
|
|
1074
|
+
if entry[:splash_dark_color] && entry[:splash_dark_color] != splash_dark_color
|
|
1075
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark_#{name}", "\"#{entry[:splash_dark_color]}\"")
|
|
1076
|
+
end
|
|
1077
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "background_image_#{name}", "\"assets/splash_background_#{name}.png\"") if entry[:background_image]
|
|
1078
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "background_image_dark_#{name}", "\"assets/splash_background_#{name}_dark.png\"") if entry[:background_image_dark]
|
|
1079
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding_#{name}", "\"assets/splash_branding_#{name}.png\"") if entry[:branding]
|
|
1080
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "branding_dark_#{name}", "\"assets/splash_branding_#{name}_dark.png\"") if entry[:branding_dark]
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
if (ios_content_mode = platforms.dig("ios", :config)["content_mode"] || platforms.dig("ios", :config)["ios_content_mode"])
|
|
1084
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "ios_content_mode", ios_content_mode.to_s)
|
|
1085
|
+
end
|
|
1086
|
+
if (web_image_mode = platforms.dig("web", :config)["image_mode"] || platforms.dig("web", :config)["web_image_mode"])
|
|
1087
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "web_image_mode", web_image_mode.to_s)
|
|
1088
|
+
end
|
|
1089
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "fullscreen", android_splash_fullscreen ? "true" : "false") unless android_splash_fullscreen.nil?
|
|
1090
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "android_gravity", android_splash_gravity.to_s) if android_splash_gravity
|
|
1091
|
+
|
|
1092
|
+
# Android 12+ draws the splash itself and ignores the legacy `image`/`color`
|
|
1093
|
+
# keys. Without an android_12 section the OS shows the launcher icon on a
|
|
1094
|
+
# system background, so mirror the configured splash into it.
|
|
1095
|
+
android_12_image =
|
|
1096
|
+
if android_12_splash then "assets/splash_android_12.png"
|
|
1097
|
+
elsif android_splash then "assets/splash_android.png"
|
|
1098
|
+
else "assets/splash.png"
|
|
1099
|
+
end
|
|
1100
|
+
update_pubspec_nested_value(pubspec_path, "flutter_native_splash", "android_12", "image", "\"#{android_12_image}\"")
|
|
1101
|
+
if android_12_icon_background
|
|
1102
|
+
update_pubspec_nested_value(
|
|
1103
|
+
pubspec_path, "flutter_native_splash", "android_12",
|
|
1104
|
+
"icon_background_color", "\"#{android_12_icon_background}\""
|
|
1105
|
+
)
|
|
1106
|
+
end
|
|
1107
|
+
android_12_image_dark =
|
|
1108
|
+
if android_12_splash_dark then "assets/splash_android_12_dark.png"
|
|
1109
|
+
elsif android_splash_dark then "assets/splash_android_dark.png"
|
|
1110
|
+
elsif splash_dark then "assets/splash_dark.png"
|
|
1111
|
+
end
|
|
1112
|
+
if android_12_image_dark
|
|
1113
|
+
update_pubspec_nested_value(
|
|
1114
|
+
pubspec_path, "flutter_native_splash", "android_12",
|
|
1115
|
+
"image_dark", "\"#{android_12_image_dark}\""
|
|
1116
|
+
)
|
|
1117
|
+
end
|
|
1118
|
+
if android_12_icon_background_dark
|
|
1119
|
+
update_pubspec_nested_value(
|
|
1120
|
+
pubspec_path, "flutter_native_splash", "android_12",
|
|
1121
|
+
"icon_background_color_dark", "\"#{android_12_icon_background_dark}\""
|
|
1122
|
+
)
|
|
1123
|
+
end
|
|
1124
|
+
update_pubspec_nested_value(pubspec_path, "flutter_native_splash", "android_12", "color", "\"#{android_12_color}\"") if android_12_color
|
|
1125
|
+
update_pubspec_nested_value(pubspec_path, "flutter_native_splash", "android_12", "color_dark", "\"#{android_12_color_dark}\"") if android_12_color_dark
|
|
1126
|
+
if android_12_branding
|
|
1127
|
+
copy_asset.call(android_12_branding, "splash_android_12_branding.png")
|
|
1128
|
+
update_pubspec_nested_value(
|
|
1129
|
+
pubspec_path, "flutter_native_splash", "android_12",
|
|
1130
|
+
"branding", "\"assets/splash_android_12_branding.png\""
|
|
1131
|
+
)
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
709
1135
|
{
|
|
710
1136
|
has_icon: has_icon,
|
|
711
1137
|
has_splash: has_splash,
|
|
712
1138
|
using_default_icon: using_default_icon,
|
|
713
1139
|
using_default_splash: using_default_splash,
|
|
1140
|
+
android_adaptive_icon: has_icon,
|
|
1141
|
+
android_12_splash: has_splash,
|
|
714
1142
|
error: nil
|
|
715
1143
|
}
|
|
716
1144
|
end
|
|
717
1145
|
|
|
1146
|
+
# Platform-specific overrides may live in a top-level `<platform>:` block,
|
|
1147
|
+
# under `build.<platform>:`, or under `assets.<platform>:`. Later sources win.
|
|
1148
|
+
def platform_build_config(config, platform)
|
|
1149
|
+
name = platform.to_s
|
|
1150
|
+
build = config["build"].is_a?(Hash) ? config["build"] : {}
|
|
1151
|
+
assets = config["assets"].is_a?(Hash) ? config["assets"] : {}
|
|
1152
|
+
[config[name], build[name], assets[name]].each_with_object({}) do |source, merged|
|
|
1153
|
+
next unless source.is_a?(Hash)
|
|
1154
|
+
|
|
1155
|
+
source.each { |key, value| merged[key.to_s] = value }
|
|
1156
|
+
end
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
# Like first_present, but keeps `false` — needed for boolean toggles.
|
|
1160
|
+
def first_defined(hash, *keys)
|
|
1161
|
+
return nil unless hash.is_a?(Hash)
|
|
1162
|
+
|
|
1163
|
+
keys.each do |key|
|
|
1164
|
+
return hash[key] if hash.key?(key)
|
|
1165
|
+
return hash[key.to_sym] if hash.key?(key.to_sym)
|
|
1166
|
+
end
|
|
1167
|
+
nil
|
|
1168
|
+
end
|
|
1169
|
+
|
|
718
1170
|
def sync_client_metadata(client_dir, config = {}, verbose: false)
|
|
719
1171
|
metadata = build_client_metadata(config, client_dir)
|
|
720
1172
|
apply_pubspec_metadata(client_dir, metadata)
|
|
@@ -728,21 +1180,30 @@ module Ruflet
|
|
|
728
1180
|
verbose,
|
|
729
1181
|
"app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
|
|
730
1182
|
)
|
|
1183
|
+
metadata
|
|
731
1184
|
end
|
|
732
1185
|
|
|
733
1186
|
def build_client_metadata(config, client_dir)
|
|
734
1187
|
app = config["app"].is_a?(Hash) ? config["app"] : {}
|
|
735
1188
|
current_pubspec = load_client_pubspec(client_dir)
|
|
736
1189
|
current_name = current_pubspec["name"].to_s
|
|
737
|
-
inferred_display_name = app["name"] || config["name"] || humanize_name(File.basename(Dir.pwd))
|
|
738
|
-
|
|
739
|
-
|
|
1190
|
+
inferred_display_name = app["app_name"] || app["name"] || config["name"] || humanize_name(File.basename(Dir.pwd))
|
|
1191
|
+
configured_app_name = first_present(app["app_name"], app["display_name"], app["name"])
|
|
1192
|
+
package_source = first_present(app["package_name"], config["package_name"], configured_app_name)
|
|
1193
|
+
package_source = current_name if package_source.nil?
|
|
1194
|
+
package_name = normalize_package_name(package_source)
|
|
1195
|
+
display_name = first_present(app["app_name"], app["display_name"], app["name"], config["display_name"], config["name"], humanize_name(package_name))
|
|
740
1196
|
organization = normalize_bundle_prefix(
|
|
741
1197
|
first_present(app["org"], app["organization"], config["org"], config["organization"], "com.example")
|
|
742
1198
|
)
|
|
743
1199
|
bundle_identifier = normalize_bundle_identifier(
|
|
744
1200
|
first_present(app["bundle_identifier"], config["bundle_identifier"], "#{organization}.#{package_name}")
|
|
745
1201
|
)
|
|
1202
|
+
identity_errors = []
|
|
1203
|
+
identity_errors << "app.app_name" if app["app_name"].to_s.strip.empty?
|
|
1204
|
+
identity_errors << "app.package_name" if app["package_name"].to_s.strip.empty?
|
|
1205
|
+
identity_errors << "app.organization" if app["organization"].to_s.strip.empty?
|
|
1206
|
+
identity_errors << "services.yaml app section" unless config["_app_identity_source"] == "services.yaml"
|
|
746
1207
|
|
|
747
1208
|
{
|
|
748
1209
|
package_name: package_name,
|
|
@@ -764,15 +1225,28 @@ module Ruflet
|
|
|
764
1225
|
linux_application_id: normalize_bundle_identifier(
|
|
765
1226
|
first_present(app["linux_application_id"], config["linux_application_id"], bundle_identifier)
|
|
766
1227
|
),
|
|
767
|
-
short_name: first_present(app["short_name"], config["short_name"], display_name)
|
|
1228
|
+
short_name: first_present(app["short_name"], config["short_name"], display_name),
|
|
1229
|
+
mobile_identity_errors: identity_errors.uniq
|
|
768
1230
|
}
|
|
769
1231
|
end
|
|
770
1232
|
|
|
1233
|
+
def validate_mobile_app_identity(metadata, platform:)
|
|
1234
|
+
return true unless %w[apk android aab ios].include?(platform.to_s)
|
|
1235
|
+
return true unless metadata
|
|
1236
|
+
|
|
1237
|
+
errors = Array(metadata[:mobile_identity_errors])
|
|
1238
|
+
return true if errors.empty?
|
|
1239
|
+
|
|
1240
|
+
warn "build config error: services.yaml must define #{errors.join(', ')}"
|
|
1241
|
+
warn "Set app.app_name, app.package_name, and app.organization before building a mobile app."
|
|
1242
|
+
false
|
|
1243
|
+
end
|
|
1244
|
+
|
|
771
1245
|
def load_client_pubspec(client_dir)
|
|
772
1246
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
773
1247
|
return {} unless File.file?(pubspec_path)
|
|
774
1248
|
|
|
775
|
-
YAML.safe_load(
|
|
1249
|
+
YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
776
1250
|
rescue StandardError
|
|
777
1251
|
{}
|
|
778
1252
|
end
|
|
@@ -781,7 +1255,7 @@ module Ruflet
|
|
|
781
1255
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
782
1256
|
return unless File.file?(pubspec_path)
|
|
783
1257
|
|
|
784
|
-
data = YAML.safe_load(
|
|
1258
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
785
1259
|
data["name"] = metadata[:package_name]
|
|
786
1260
|
data["description"] = metadata[:description]
|
|
787
1261
|
data["version"] = metadata[:version]
|
|
@@ -789,28 +1263,6 @@ module Ruflet
|
|
|
789
1263
|
end
|
|
790
1264
|
|
|
791
1265
|
def apply_android_metadata(client_dir, metadata)
|
|
792
|
-
gradle_path = File.join(client_dir, "android", "app", "build.gradle.kts")
|
|
793
|
-
replace_in_file(
|
|
794
|
-
gradle_path,
|
|
795
|
-
/^\s*namespace = ".*"$/,
|
|
796
|
-
%( namespace = "#{metadata[:android_application_id]}")
|
|
797
|
-
)
|
|
798
|
-
replace_in_file(
|
|
799
|
-
gradle_path,
|
|
800
|
-
/^\s*applicationId = ".*"$/,
|
|
801
|
-
%( applicationId = "#{metadata[:android_application_id]}")
|
|
802
|
-
)
|
|
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
|
-
|
|
814
1266
|
manifest_path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
815
1267
|
replace_in_file(
|
|
816
1268
|
manifest_path,
|
|
@@ -827,17 +1279,37 @@ module Ruflet
|
|
|
827
1279
|
pbxproj_path = File.join(client_dir, "ios", "Runner.xcodeproj", "project.pbxproj")
|
|
828
1280
|
return unless File.file?(pbxproj_path)
|
|
829
1281
|
|
|
830
|
-
content =
|
|
1282
|
+
content = read_text_file(pbxproj_path)
|
|
831
1283
|
content.gsub!(/INFOPLIST_KEY_CFBundleDisplayName = "[^"]*";/, %(INFOPLIST_KEY_CFBundleDisplayName = "#{xcode_escape(metadata[:display_name])}";))
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1284
|
+
write_text_file(pbxproj_path, content)
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
def apply_mobile_package_name(client_dir, metadata, platform:, tools:, verbose: false)
|
|
1288
|
+
return true unless metadata
|
|
1289
|
+
|
|
1290
|
+
package_name, platform_flag = case platform.to_s
|
|
1291
|
+
when "apk", "android", "aab"
|
|
1292
|
+
[metadata[:android_application_id], "--android"]
|
|
1293
|
+
when "ios"
|
|
1294
|
+
[metadata[:ios_bundle_identifier], "--ios"]
|
|
1295
|
+
else
|
|
1296
|
+
return true
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
build_note("Applying #{platform_flag.delete_prefix('--')} package name #{package_name}")
|
|
1300
|
+
build_log(verbose, "running change_app_package_name for #{package_name} #{platform_flag}")
|
|
1301
|
+
ok = run_external_command(
|
|
1302
|
+
tools[:env],
|
|
1303
|
+
tools[:dart],
|
|
1304
|
+
"run",
|
|
1305
|
+
"change_app_package_name:main",
|
|
1306
|
+
package_name,
|
|
1307
|
+
platform_flag,
|
|
1308
|
+
chdir: client_dir,
|
|
1309
|
+
unbundled: true
|
|
1310
|
+
)
|
|
1311
|
+
warn "change_app_package_name failed for #{package_name}" unless ok
|
|
1312
|
+
ok
|
|
841
1313
|
end
|
|
842
1314
|
|
|
843
1315
|
def apply_macos_metadata(client_dir, metadata)
|
|
@@ -862,11 +1334,11 @@ module Ruflet
|
|
|
862
1334
|
def apply_web_metadata(client_dir, metadata)
|
|
863
1335
|
manifest_path = File.join(client_dir, "web", "manifest.json")
|
|
864
1336
|
if File.file?(manifest_path)
|
|
865
|
-
data = JSON.parse(
|
|
1337
|
+
data = JSON.parse(read_text_file(manifest_path))
|
|
866
1338
|
data["name"] = metadata[:display_name]
|
|
867
1339
|
data["short_name"] = metadata[:short_name]
|
|
868
1340
|
data["description"] = metadata[:description]
|
|
869
|
-
|
|
1341
|
+
write_text_file(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
870
1342
|
end
|
|
871
1343
|
|
|
872
1344
|
index_path = File.join(client_dir, "web", "index.html")
|
|
@@ -931,23 +1403,34 @@ module Ruflet
|
|
|
931
1403
|
replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
|
|
932
1404
|
end
|
|
933
1405
|
|
|
1406
|
+
# Native project files are UTF-8 and the values written into them are too
|
|
1407
|
+
# (the macOS copyright line carries a ©). Reading them with the default
|
|
1408
|
+
# external encoding fails outright under a non-UTF-8 locale, so pin it.
|
|
1409
|
+
def read_text_file(path)
|
|
1410
|
+
File.read(path, encoding: Encoding::UTF_8)
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1413
|
+
def write_text_file(path, content)
|
|
1414
|
+
File.write(path, content, encoding: Encoding::UTF_8)
|
|
1415
|
+
end
|
|
1416
|
+
|
|
934
1417
|
def replace_plist_value(path, key, value)
|
|
935
1418
|
return unless File.file?(path)
|
|
936
1419
|
|
|
937
|
-
content =
|
|
1420
|
+
content = read_text_file(path)
|
|
938
1421
|
pattern = %r{(<key>#{Regexp.escape(key)}</key>\s*<string>)(.*?)(</string>)}m
|
|
939
1422
|
updated = content.gsub(pattern) do
|
|
940
1423
|
"#{Regexp.last_match(1)}#{xml_escape(value)}#{Regexp.last_match(3)}"
|
|
941
1424
|
end
|
|
942
|
-
|
|
1425
|
+
write_text_file(path, updated) unless updated == content
|
|
943
1426
|
end
|
|
944
1427
|
|
|
945
1428
|
def replace_in_file(path, pattern, replacement)
|
|
946
1429
|
return unless File.file?(path)
|
|
947
1430
|
|
|
948
|
-
content =
|
|
1431
|
+
content = read_text_file(path)
|
|
949
1432
|
updated = content.gsub(pattern) { replacement }
|
|
950
|
-
|
|
1433
|
+
write_text_file(path, updated) unless updated == content
|
|
951
1434
|
end
|
|
952
1435
|
|
|
953
1436
|
def first_present(*values)
|
|
@@ -1055,6 +1538,13 @@ module Ruflet
|
|
|
1055
1538
|
|
|
1056
1539
|
def apply_native_service_permissions(client_dir, config)
|
|
1057
1540
|
entries = configured_service_entries(config)
|
|
1541
|
+
configured_extensions = Array(config["extensions"]).filter_map { |entry| normalize_extension_key(entry) }
|
|
1542
|
+
extension_services = configured_extensions.flat_map do |extension|
|
|
1543
|
+
EXTENSION_REQUIRED_SERVICES.fetch(extension, [])
|
|
1544
|
+
end
|
|
1545
|
+
extension_services.each do |service|
|
|
1546
|
+
entries << { name: service, description: "" } unless entries.any? { |entry| entry[:name] == service }
|
|
1547
|
+
end
|
|
1058
1548
|
return if entries.empty?
|
|
1059
1549
|
|
|
1060
1550
|
apply_android_service_permissions(client_dir, entries)
|
|
@@ -1065,20 +1555,20 @@ module Ruflet
|
|
|
1065
1555
|
path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1066
1556
|
return unless File.file?(path)
|
|
1067
1557
|
|
|
1068
|
-
content =
|
|
1558
|
+
content = read_text_file(path)
|
|
1069
1559
|
entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission|
|
|
1070
1560
|
next if content.include?(%(android:name="#{permission}"))
|
|
1071
1561
|
|
|
1072
1562
|
content.sub!(/<manifest\b[^>]*>\s*/, "\\0 <uses-permission android:name=\"#{permission}\"/>\n")
|
|
1073
1563
|
end
|
|
1074
|
-
|
|
1564
|
+
write_text_file(path, content)
|
|
1075
1565
|
end
|
|
1076
1566
|
|
|
1077
1567
|
def apply_ios_service_usage_descriptions(client_dir, entries)
|
|
1078
1568
|
path = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1079
1569
|
return unless File.file?(path)
|
|
1080
1570
|
|
|
1081
|
-
content =
|
|
1571
|
+
content = read_text_file(path)
|
|
1082
1572
|
entries.each do |entry|
|
|
1083
1573
|
key = IOS_SERVICE_USAGE_KEYS[entry[:name]]
|
|
1084
1574
|
next unless key
|
|
@@ -1094,7 +1584,7 @@ module Ruflet
|
|
|
1094
1584
|
content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
|
|
1095
1585
|
end
|
|
1096
1586
|
end
|
|
1097
|
-
|
|
1587
|
+
write_text_file(path, content)
|
|
1098
1588
|
end
|
|
1099
1589
|
|
|
1100
1590
|
def clear_flutter_build_state(client_dir, verbose: false)
|
|
@@ -1145,11 +1635,11 @@ module Ruflet
|
|
|
1145
1635
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
1146
1636
|
return unless File.file?(pubspec_path)
|
|
1147
1637
|
|
|
1148
|
-
data = YAML.safe_load(
|
|
1638
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
1149
1639
|
dependencies = data["dependencies"]
|
|
1150
1640
|
dependencies = data["dependencies"] = {} unless dependencies.is_a?(Hash)
|
|
1151
|
-
spinkit_dependency = template_client_pubspec_dependencies["
|
|
1152
|
-
dependencies["
|
|
1641
|
+
spinkit_dependency = template_client_pubspec_dependencies["flet_spinkit"]
|
|
1642
|
+
dependencies["flet_spinkit"] = spinkit_dependency if spinkit_dependency
|
|
1153
1643
|
flutter = data["flutter"]
|
|
1154
1644
|
flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
|
|
1155
1645
|
assets = Array(flutter["assets"]).map(&:to_s)
|
|
@@ -1173,11 +1663,16 @@ module Ruflet
|
|
|
1173
1663
|
write_pubspec_yaml(pubspec_path, data)
|
|
1174
1664
|
end
|
|
1175
1665
|
|
|
1666
|
+
# A self-contained build needs the embedded VM. Prefer a local checkout so
|
|
1667
|
+
# the runtime under development is the one packaged, and otherwise resolve
|
|
1668
|
+
# the published package.
|
|
1669
|
+
PUBLISHED_RUBY_RUNTIME_CONSTRAINT = "^0.0.9"
|
|
1670
|
+
|
|
1176
1671
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1177
1672
|
local_path = explicit_local_ruby_runtime_path || source_checkout_ruby_runtime_path
|
|
1178
1673
|
return { "path" => local_path } if local_path
|
|
1179
1674
|
|
|
1180
|
-
current_dependency ||
|
|
1675
|
+
current_dependency || PUBLISHED_RUBY_RUNTIME_CONSTRAINT
|
|
1181
1676
|
end
|
|
1182
1677
|
|
|
1183
1678
|
def explicit_local_ruby_runtime_path
|
|
@@ -1209,11 +1704,11 @@ module Ruflet
|
|
|
1209
1704
|
"lib/main.self.dart",
|
|
1210
1705
|
"lib/main.server.dart",
|
|
1211
1706
|
"lib/ruflet_file_picker_service.dart",
|
|
1212
|
-
"lib/ruflet_spinkit.dart",
|
|
1213
1707
|
"lib/connection_probe.dart",
|
|
1214
1708
|
"lib/connection_probe_io.dart",
|
|
1215
1709
|
"lib/connection_probe_stub.dart",
|
|
1216
|
-
"ios/Podfile"
|
|
1710
|
+
"ios/Podfile",
|
|
1711
|
+
"windows/CMakeLists.txt"
|
|
1217
1712
|
]
|
|
1218
1713
|
|
|
1219
1714
|
managed_files.each do |relative_path|
|
|
@@ -1233,7 +1728,7 @@ module Ruflet
|
|
|
1233
1728
|
|
|
1234
1729
|
content = indent_pubspec_sequences(content)
|
|
1235
1730
|
|
|
1236
|
-
|
|
1731
|
+
write_text_file(path, content)
|
|
1237
1732
|
end
|
|
1238
1733
|
|
|
1239
1734
|
def indent_pubspec_sequences(content)
|
|
@@ -1335,6 +1830,8 @@ module Ruflet
|
|
|
1335
1830
|
.vscode
|
|
1336
1831
|
build
|
|
1337
1832
|
coverage
|
|
1833
|
+
credentials
|
|
1834
|
+
fastlane
|
|
1338
1835
|
log
|
|
1339
1836
|
node_modules
|
|
1340
1837
|
pkg
|
|
@@ -1347,10 +1844,30 @@ module Ruflet
|
|
|
1347
1844
|
end
|
|
1348
1845
|
end
|
|
1349
1846
|
|
|
1847
|
+
# Anything embedded here is readable by anyone who unpacks the shipped
|
|
1848
|
+
# app, so signing keys and local environment files must never be copied
|
|
1849
|
+
# in even when a project keeps them beside its source.
|
|
1850
|
+
SECRET_ASSET_EXTENSIONS = %w[.p8 .p12 .pem .key .jks .keystore .mobileprovision].freeze
|
|
1851
|
+
SECRET_ASSET_BASENAMES = %w[.env .netrc].freeze
|
|
1852
|
+
|
|
1853
|
+
def secret_project_asset?(relative)
|
|
1854
|
+
basename = File.basename(relative)
|
|
1855
|
+
return true if SECRET_ASSET_BASENAMES.include?(basename)
|
|
1856
|
+
return true if basename.start_with?(".env.") && basename != ".env.example"
|
|
1857
|
+
return true if SECRET_ASSET_EXTENSIONS.include?(File.extname(basename).downcase)
|
|
1858
|
+
return true if basename.match?(/\Agoogle-play.*\.json\z/i)
|
|
1859
|
+
|
|
1860
|
+
false
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1350
1863
|
def include_project_asset_file?(relative)
|
|
1351
1864
|
basename = File.basename(relative)
|
|
1352
1865
|
return false if basename == ".DS_Store"
|
|
1353
1866
|
return false if %w[Gemfile.lock pubspec.lock Podfile.lock package-lock.json yarn.lock pnpm-lock.yaml].include?(basename)
|
|
1867
|
+
if secret_project_asset?(relative)
|
|
1868
|
+
build_note("Excluded #{relative} from the embedded project; it looks like a credential")
|
|
1869
|
+
return false
|
|
1870
|
+
end
|
|
1354
1871
|
true
|
|
1355
1872
|
end
|
|
1356
1873
|
|
|
@@ -1381,17 +1898,18 @@ module Ruflet
|
|
|
1381
1898
|
|
|
1382
1899
|
key.tr!("-", "_")
|
|
1383
1900
|
key.gsub!(/\A(flet_)+/, "")
|
|
1901
|
+
key.gsub!(/\A(ruflet_)+/, "")
|
|
1384
1902
|
key.gsub!(/\Aservice_/, "")
|
|
1385
1903
|
key
|
|
1386
1904
|
end
|
|
1387
1905
|
|
|
1388
1906
|
def prune_client_pubspec(path, selected_packages)
|
|
1389
|
-
data = YAML.safe_load(
|
|
1907
|
+
data = YAML.safe_load(read_text_file(path), aliases: true) || {}
|
|
1390
1908
|
deps = (data["dependencies"] || {}).dup
|
|
1909
|
+
optional_packages = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:package) }.uniq
|
|
1391
1910
|
|
|
1392
1911
|
deps.keys.each do |name|
|
|
1393
|
-
next unless
|
|
1394
|
-
next if name == "flet"
|
|
1912
|
+
next unless optional_packages.include?(name)
|
|
1395
1913
|
next if selected_packages.include?(name)
|
|
1396
1914
|
|
|
1397
1915
|
deps.delete(name)
|
|
@@ -1407,7 +1925,7 @@ module Ruflet
|
|
|
1407
1925
|
template_deps = template_client_pubspec_dependencies
|
|
1408
1926
|
return if template_deps.empty?
|
|
1409
1927
|
|
|
1410
|
-
data = YAML.safe_load(
|
|
1928
|
+
data = YAML.safe_load(read_text_file(path), aliases: true) || {}
|
|
1411
1929
|
deps = (data["dependencies"] || {}).dup
|
|
1412
1930
|
selected_packages.each do |package_name|
|
|
1413
1931
|
deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
|
|
@@ -1427,7 +1945,7 @@ module Ruflet
|
|
|
1427
1945
|
pubspec_path = File.join(template_root, "pubspec.yaml")
|
|
1428
1946
|
return {} unless File.file?(pubspec_path)
|
|
1429
1947
|
|
|
1430
|
-
data = YAML.safe_load(
|
|
1948
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
1431
1949
|
deps = data["dependencies"]
|
|
1432
1950
|
deps.is_a?(Hash) ? deps : {}
|
|
1433
1951
|
rescue StandardError
|
|
@@ -1440,18 +1958,20 @@ module Ruflet
|
|
|
1440
1958
|
template_path = template_client_entrypoint_path(File.basename(path))
|
|
1441
1959
|
return unless template_path
|
|
1442
1960
|
|
|
1443
|
-
content =
|
|
1444
|
-
template =
|
|
1961
|
+
content = read_text_file(path)
|
|
1962
|
+
template = read_text_file(template_path)
|
|
1445
1963
|
|
|
1446
1964
|
selected_aliases.each do |extension_alias|
|
|
1447
1965
|
import_line = template.lines.find { |line| line.match?(/\sas #{Regexp.escape(extension_alias)};\s*\z/) }
|
|
1448
|
-
extension_line = template.lines.find
|
|
1966
|
+
extension_line = template.lines.find do |line|
|
|
1967
|
+
line.match?(/^\s*#{Regexp.escape(extension_alias)}\.[A-Za-z0-9_]+\(\),\s*$/)
|
|
1968
|
+
end
|
|
1449
1969
|
|
|
1450
1970
|
content = insert_missing_import(content, import_line) if import_line && !content.include?(import_line)
|
|
1451
1971
|
content = insert_missing_extension(content, extension_line) if extension_line && !content.include?(extension_line)
|
|
1452
1972
|
end
|
|
1453
1973
|
|
|
1454
|
-
|
|
1974
|
+
write_text_file(path, content)
|
|
1455
1975
|
end
|
|
1456
1976
|
|
|
1457
1977
|
def template_client_entrypoint_path(name)
|
|
@@ -1489,38 +2009,97 @@ module Ruflet
|
|
|
1489
2009
|
end
|
|
1490
2010
|
|
|
1491
2011
|
def prune_client_main(path, selected_aliases)
|
|
1492
|
-
content =
|
|
2012
|
+
content = read_text_file(path)
|
|
1493
2013
|
alias_to_package = {}
|
|
2014
|
+
optional_aliases = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:alias) }.uniq
|
|
1494
2015
|
|
|
1495
|
-
content.scan(%r{^import 'package:(
|
|
2016
|
+
content.scan(%r{^import 'package:([^/]+)/[^']+'\s+as ([a-zA-Z0-9_]+);$}m) do |package_name, import_alias|
|
|
1496
2017
|
alias_to_package[import_alias] = package_name
|
|
1497
2018
|
end
|
|
2019
|
+
content.scan(%r{^import '([^']+)'\s+as ([a-zA-Z0-9_]+);$}m) do |_source, import_alias|
|
|
2020
|
+
alias_to_package[import_alias] = :local if optional_aliases.include?(import_alias)
|
|
2021
|
+
end
|
|
1498
2022
|
|
|
1499
|
-
content = content.gsub(%r{^import 'package:(
|
|
2023
|
+
content = content.gsub(%r{^import 'package:([^/]+)/[^']+'\s+as ([a-zA-Z0-9_]+);\n}m) do |match|
|
|
1500
2024
|
package_name = Regexp.last_match(1)
|
|
1501
2025
|
import_alias = Regexp.last_match(2)
|
|
1502
|
-
if
|
|
2026
|
+
if !optional_aliases.include?(import_alias) || selected_aliases.include?(import_alias)
|
|
2027
|
+
match
|
|
2028
|
+
else
|
|
2029
|
+
""
|
|
2030
|
+
end
|
|
2031
|
+
end
|
|
2032
|
+
content = content.gsub(%r{^import '([^']+)'\s+as ([a-zA-Z0-9_]+);\n}m) do |match|
|
|
2033
|
+
import_alias = Regexp.last_match(2)
|
|
2034
|
+
if !optional_aliases.include?(import_alias) || selected_aliases.include?(import_alias)
|
|
1503
2035
|
match
|
|
1504
2036
|
else
|
|
1505
2037
|
""
|
|
1506
2038
|
end
|
|
1507
2039
|
end
|
|
1508
2040
|
|
|
1509
|
-
content = content.gsub(/^(\s*)([a-zA-Z0-9_]+)\.
|
|
2041
|
+
content = content.gsub(/^(\s*)([a-zA-Z0-9_]+)\.[A-Za-z0-9_]+\(\),\s*$/) do |match|
|
|
1510
2042
|
extension_alias = Regexp.last_match(2)
|
|
1511
2043
|
package_name = alias_to_package[extension_alias]
|
|
1512
|
-
if package_name.nil? || selected_aliases.include?(extension_alias)
|
|
2044
|
+
if package_name.nil? || !optional_aliases.include?(extension_alias) || selected_aliases.include?(extension_alias)
|
|
1513
2045
|
match
|
|
1514
2046
|
else
|
|
1515
2047
|
""
|
|
1516
2048
|
end
|
|
1517
2049
|
end
|
|
1518
2050
|
|
|
1519
|
-
|
|
2051
|
+
write_text_file(path, content)
|
|
2052
|
+
end
|
|
2053
|
+
|
|
2054
|
+
# update_pubspec_value only rewrites blocks that already exist. Create the
|
|
2055
|
+
# block first so a template without it still receives the configured values.
|
|
2056
|
+
def ensure_pubspec_block(path, block)
|
|
2057
|
+
return unless File.file?(path)
|
|
2058
|
+
|
|
2059
|
+
content = read_text_file(path)
|
|
2060
|
+
return if content.lines.any? { |line| line.start_with?("#{block}:") }
|
|
2061
|
+
|
|
2062
|
+
content += "\n" unless content.empty? || content.end_with?("\n")
|
|
2063
|
+
write_text_file(path, "#{content}#{block}:\n")
|
|
2064
|
+
end
|
|
2065
|
+
|
|
2066
|
+
# Writes `block: -> section: -> key: value`, creating the block and the
|
|
2067
|
+
# nested section when they are missing (for example flutter_native_splash's
|
|
2068
|
+
# android_12 section).
|
|
2069
|
+
def update_pubspec_nested_value(path, block, section, key, value)
|
|
2070
|
+
return unless File.file?(path)
|
|
2071
|
+
|
|
2072
|
+
ensure_pubspec_block(path, block)
|
|
2073
|
+
lines = read_text_file(path).split("\n", -1)
|
|
2074
|
+
block_start = lines.index { |line| line.start_with?("#{block}:") }
|
|
2075
|
+
return unless block_start
|
|
2076
|
+
|
|
2077
|
+
block_end = block_start + 1
|
|
2078
|
+
block_end += 1 while block_end < lines.length && (lines[block_end].strip.empty? || lines[block_end].start_with?(" ", "\t"))
|
|
2079
|
+
block_end -= 1 while block_end > block_start + 1 && lines[block_end - 1].strip.empty?
|
|
2080
|
+
|
|
2081
|
+
section_index = (block_start + 1...block_end).find do |index|
|
|
2082
|
+
lines[index] =~ /\A\s{2}#{Regexp.escape(section)}:\s*(#.*)?\z/
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
if section_index.nil?
|
|
2086
|
+
lines.insert(block_end, " #{section}:", " #{key}: #{value}")
|
|
2087
|
+
else
|
|
2088
|
+
section_end = section_index + 1
|
|
2089
|
+
section_end += 1 while section_end < block_end && lines[section_end] =~ /\A\s{3,}\S/
|
|
2090
|
+
existing = (section_index + 1...section_end).find { |index| lines[index].strip.start_with?("#{key}:") }
|
|
2091
|
+
if existing
|
|
2092
|
+
lines[existing] = "#{lines[existing][/\A\s*/]}#{key}: #{value}"
|
|
2093
|
+
else
|
|
2094
|
+
lines.insert(section_end, " #{key}: #{value}")
|
|
2095
|
+
end
|
|
2096
|
+
end
|
|
2097
|
+
|
|
2098
|
+
write_text_file(path, indent_pubspec_sequences(lines.join("\n")))
|
|
1520
2099
|
end
|
|
1521
2100
|
|
|
1522
2101
|
def update_pubspec_value(path, block, key, value, multiple: false)
|
|
1523
|
-
lines =
|
|
2102
|
+
lines = read_text_file(path).split("\n", -1)
|
|
1524
2103
|
out = []
|
|
1525
2104
|
in_block = false
|
|
1526
2105
|
replaced = false
|
|
@@ -1555,7 +2134,7 @@ module Ruflet
|
|
|
1555
2134
|
if in_block && !replaced
|
|
1556
2135
|
out << "#{block_indent}#{key}: #{value}"
|
|
1557
2136
|
end
|
|
1558
|
-
|
|
2137
|
+
write_text_file(path, indent_pubspec_sequences(out.join("\n")))
|
|
1559
2138
|
end
|
|
1560
2139
|
|
|
1561
2140
|
def flutter_build_command(platform)
|