ruflet 0.0.19 → 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 +2 -3
- data/lib/ruflet/cli/build_command.rb +655 -107
- data/lib/ruflet/cli/flutter_sdk.rb +17 -3
- data/lib/ruflet/cli/new_command.rb +28 -21
- data/lib/ruflet/cli/run_command.rb +219 -56
- data/lib/ruflet/cli/templates.rb +8 -5
- data/lib/ruflet/cli.rb +1 -1
- 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
|
@@ -26,6 +26,7 @@ module Ruflet
|
|
|
26
26
|
"lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
|
|
27
27
|
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
28
28
|
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
29
|
+
"qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
|
|
29
30
|
"rive" => { package: "flet_rive", alias: "ruflet_rive" },
|
|
30
31
|
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
31
32
|
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
@@ -37,6 +38,9 @@ module Ruflet
|
|
|
37
38
|
"location" => %w[geolocator permission_handler],
|
|
38
39
|
"motion" => %w[permission_handler]
|
|
39
40
|
}.freeze
|
|
41
|
+
EXTENSION_REQUIRED_SERVICES = {
|
|
42
|
+
"qrcode_scanner" => %w[camera]
|
|
43
|
+
}.freeze
|
|
40
44
|
ANDROID_SERVICE_PERMISSIONS = {
|
|
41
45
|
"camera" => %w[android.permission.CAMERA],
|
|
42
46
|
"microphone" => %w[android.permission.RECORD_AUDIO],
|
|
@@ -49,6 +53,20 @@ module Ruflet
|
|
|
49
53
|
"location" => "NSLocationWhenInUseUsageDescription",
|
|
50
54
|
"motion" => "NSMotionUsageDescription"
|
|
51
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
|
|
52
70
|
|
|
53
71
|
def command_build(args)
|
|
54
72
|
self_contained = args.delete("--self")
|
|
@@ -65,6 +83,15 @@ module Ruflet
|
|
|
65
83
|
return 1
|
|
66
84
|
end
|
|
67
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
|
+
|
|
68
95
|
ensure_ruflet_build_assets(verbose: !!verbose)
|
|
69
96
|
client_dir = ensure_flutter_client_dir(verbose: !!verbose)
|
|
70
97
|
unless client_dir
|
|
@@ -98,15 +125,21 @@ module Ruflet
|
|
|
98
125
|
# deterministically instead of inferring from a single main.rb — the
|
|
99
126
|
# app tree now ships many main.rb files (standalone_apps/*/main.rb).
|
|
100
127
|
build_args += ["--dart-define", "RUFLET_EMBEDDED_PROJECT=#{self_contained_project_name}"]
|
|
101
|
-
|
|
102
|
-
unless backend_url
|
|
103
|
-
warn "build config error: backend_url is required for server-driven builds"
|
|
104
|
-
warn "Set app.backend_url or backend_url in ruflet.yaml"
|
|
105
|
-
return 1
|
|
106
|
-
end
|
|
128
|
+
elsif backend_url
|
|
107
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
|
|
108
140
|
end
|
|
109
141
|
build_args << "-v" if verbose
|
|
142
|
+
stage_ios_simulator_ruby_runtime(client_dir, build_args, verbose: !!verbose) if self_contained
|
|
110
143
|
|
|
111
144
|
build_log(verbose, "mode=#{self_contained ? 'self' : 'server'}")
|
|
112
145
|
build_log(verbose, "client_dir=#{client_dir}")
|
|
@@ -400,7 +433,9 @@ module Ruflet
|
|
|
400
433
|
|
|
401
434
|
def prepare_flutter_client(client_dir, platform:, tools:, config:, self_contained: false, verbose: false)
|
|
402
435
|
refresh_managed_client_template_files(client_dir, verbose: verbose)
|
|
403
|
-
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
|
+
|
|
404
439
|
apply_native_service_permissions(client_dir, config)
|
|
405
440
|
configured = configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
|
|
406
441
|
return false if configured == false
|
|
@@ -424,6 +459,10 @@ module Ruflet
|
|
|
424
459
|
return false
|
|
425
460
|
end
|
|
426
461
|
|
|
462
|
+
unless apply_mobile_package_name(client_dir, metadata, platform: platform, tools: tools, verbose: verbose)
|
|
463
|
+
return false
|
|
464
|
+
end
|
|
465
|
+
|
|
427
466
|
unless ensure_native_build_dependencies(client_dir, platform, tools[:env], verbose: verbose)
|
|
428
467
|
return false
|
|
429
468
|
end
|
|
@@ -446,9 +485,60 @@ module Ruflet
|
|
|
446
485
|
end
|
|
447
486
|
end
|
|
448
487
|
|
|
488
|
+
verify_android_generated_assets(client_dir, asset_flags, platform, verbose: verbose)
|
|
489
|
+
|
|
449
490
|
true
|
|
450
491
|
end
|
|
451
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
|
+
|
|
452
542
|
def ensure_flutter_platform_artifacts(client_dir, platform, env, flutter, verbose: false)
|
|
453
543
|
precache_flags = flutter_precache_flags(platform)
|
|
454
544
|
return true if precache_flags.empty?
|
|
@@ -495,6 +585,50 @@ module Ruflet
|
|
|
495
585
|
end
|
|
496
586
|
end
|
|
497
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
|
+
|
|
498
632
|
def ensure_cocoapods_install(client_dir, platform_dir, env, verbose: false)
|
|
499
633
|
pod_dir = File.join(client_dir, platform_dir)
|
|
500
634
|
return true unless Dir.exist?(pod_dir)
|
|
@@ -549,7 +683,7 @@ module Ruflet
|
|
|
549
683
|
shim_dir = File.join(client_dir, ".ruflet", "bin")
|
|
550
684
|
FileUtils.mkdir_p(shim_dir)
|
|
551
685
|
shim_path = File.join(shim_dir, "pod")
|
|
552
|
-
|
|
686
|
+
write_text_file(
|
|
553
687
|
shim_path,
|
|
554
688
|
<<~SH
|
|
555
689
|
#!/bin/sh
|
|
@@ -599,12 +733,16 @@ module Ruflet
|
|
|
599
733
|
alt = "ruflet.yml"
|
|
600
734
|
config_path = alt if File.file?(alt)
|
|
601
735
|
end
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
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")
|
|
606
740
|
if File.file?(services_path)
|
|
607
|
-
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
|
|
608
746
|
config["services"] = service_config["services"] if service_config.key?("services")
|
|
609
747
|
end
|
|
610
748
|
config
|
|
@@ -640,16 +778,93 @@ module Ruflet
|
|
|
640
778
|
splash = resolve_asset.call(build["splash_screen"] || assets["splash_screen"] || config["splash_screen"])
|
|
641
779
|
splash_dark = resolve_asset.call(build["splash_dark"] || build["splash_dark_image"] || assets["splash_dark"])
|
|
642
780
|
icon = resolve_asset.call(build["icon_launcher"] || assets["icon_launcher"] || config["icon_launcher"])
|
|
643
|
-
icon_android = resolve_asset.call(build["icon_android"] || assets["icon_android"])
|
|
644
|
-
icon_ios = resolve_asset.call(build["icon_ios"] || assets["icon_ios"])
|
|
645
|
-
icon_web = resolve_asset.call(build["icon_web"] || assets["icon_web"])
|
|
646
|
-
icon_windows = resolve_asset.call(build["icon_windows"] || assets["icon_windows"])
|
|
647
|
-
icon_macos = resolve_asset.call(build["icon_macos"] || assets["icon_macos"])
|
|
648
781
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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"]
|
|
653
868
|
|
|
654
869
|
assets_dir = File.join(client_dir, "assets")
|
|
655
870
|
FileUtils.mkdir_p(assets_dir)
|
|
@@ -662,14 +877,43 @@ module Ruflet
|
|
|
662
877
|
copy_asset.call(splash, "splash.png")
|
|
663
878
|
copy_asset.call(splash_dark, "splash_dark.png")
|
|
664
879
|
copy_asset.call(icon, "icon.png")
|
|
665
|
-
copy_asset.call(
|
|
666
|
-
copy_asset.call(
|
|
667
|
-
copy_asset.call(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
671
910
|
end
|
|
672
|
-
|
|
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")
|
|
673
917
|
|
|
674
918
|
default_splash = File.file?(File.join(assets_dir, "splash.png"))
|
|
675
919
|
default_icon = File.file?(File.join(assets_dir, "icon.png"))
|
|
@@ -694,43 +938,235 @@ module Ruflet
|
|
|
694
938
|
end
|
|
695
939
|
end
|
|
696
940
|
|
|
697
|
-
|
|
698
|
-
|
|
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)
|
|
699
964
|
|
|
700
965
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
701
966
|
unless File.file?(pubspec_path)
|
|
702
967
|
return { has_icon: has_icon, has_splash: has_splash, error: nil }
|
|
703
968
|
end
|
|
704
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
|
+
|
|
705
973
|
if has_icon
|
|
706
|
-
|
|
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
|
|
707
1009
|
end
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
715
1045
|
end
|
|
716
|
-
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "
|
|
717
|
-
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "
|
|
718
|
-
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
|
|
719
1048
|
|
|
720
|
-
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
|
|
721
1050
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark", "\"assets/splash_dark.png\"") if splash_dark
|
|
722
1051
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "color", "\"#{splash_color}\"") if splash_color
|
|
723
1052
|
update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark", "\"#{splash_dark_color}\"") if splash_dark_color
|
|
724
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
|
+
|
|
725
1135
|
{
|
|
726
1136
|
has_icon: has_icon,
|
|
727
1137
|
has_splash: has_splash,
|
|
728
1138
|
using_default_icon: using_default_icon,
|
|
729
1139
|
using_default_splash: using_default_splash,
|
|
1140
|
+
android_adaptive_icon: has_icon,
|
|
1141
|
+
android_12_splash: has_splash,
|
|
730
1142
|
error: nil
|
|
731
1143
|
}
|
|
732
1144
|
end
|
|
733
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
|
+
|
|
734
1170
|
def sync_client_metadata(client_dir, config = {}, verbose: false)
|
|
735
1171
|
metadata = build_client_metadata(config, client_dir)
|
|
736
1172
|
apply_pubspec_metadata(client_dir, metadata)
|
|
@@ -744,21 +1180,30 @@ module Ruflet
|
|
|
744
1180
|
verbose,
|
|
745
1181
|
"app=#{metadata[:display_name]} package=#{metadata[:package_name]} org=#{metadata[:organization]} bundle=#{metadata[:bundle_identifier]}"
|
|
746
1182
|
)
|
|
1183
|
+
metadata
|
|
747
1184
|
end
|
|
748
1185
|
|
|
749
1186
|
def build_client_metadata(config, client_dir)
|
|
750
1187
|
app = config["app"].is_a?(Hash) ? config["app"] : {}
|
|
751
1188
|
current_pubspec = load_client_pubspec(client_dir)
|
|
752
1189
|
current_name = current_pubspec["name"].to_s
|
|
753
|
-
inferred_display_name = app["name"] || config["name"] || humanize_name(File.basename(Dir.pwd))
|
|
754
|
-
|
|
755
|
-
|
|
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))
|
|
756
1196
|
organization = normalize_bundle_prefix(
|
|
757
1197
|
first_present(app["org"], app["organization"], config["org"], config["organization"], "com.example")
|
|
758
1198
|
)
|
|
759
1199
|
bundle_identifier = normalize_bundle_identifier(
|
|
760
1200
|
first_present(app["bundle_identifier"], config["bundle_identifier"], "#{organization}.#{package_name}")
|
|
761
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"
|
|
762
1207
|
|
|
763
1208
|
{
|
|
764
1209
|
package_name: package_name,
|
|
@@ -780,15 +1225,28 @@ module Ruflet
|
|
|
780
1225
|
linux_application_id: normalize_bundle_identifier(
|
|
781
1226
|
first_present(app["linux_application_id"], config["linux_application_id"], bundle_identifier)
|
|
782
1227
|
),
|
|
783
|
-
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
|
|
784
1230
|
}
|
|
785
1231
|
end
|
|
786
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
|
+
|
|
787
1245
|
def load_client_pubspec(client_dir)
|
|
788
1246
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
789
1247
|
return {} unless File.file?(pubspec_path)
|
|
790
1248
|
|
|
791
|
-
YAML.safe_load(
|
|
1249
|
+
YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
792
1250
|
rescue StandardError
|
|
793
1251
|
{}
|
|
794
1252
|
end
|
|
@@ -797,7 +1255,7 @@ module Ruflet
|
|
|
797
1255
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
798
1256
|
return unless File.file?(pubspec_path)
|
|
799
1257
|
|
|
800
|
-
data = YAML.safe_load(
|
|
1258
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
801
1259
|
data["name"] = metadata[:package_name]
|
|
802
1260
|
data["description"] = metadata[:description]
|
|
803
1261
|
data["version"] = metadata[:version]
|
|
@@ -805,28 +1263,6 @@ module Ruflet
|
|
|
805
1263
|
end
|
|
806
1264
|
|
|
807
1265
|
def apply_android_metadata(client_dir, metadata)
|
|
808
|
-
gradle_path = File.join(client_dir, "android", "app", "build.gradle.kts")
|
|
809
|
-
replace_in_file(
|
|
810
|
-
gradle_path,
|
|
811
|
-
/^\s*namespace = ".*"$/,
|
|
812
|
-
%( namespace = "#{metadata[:android_application_id]}")
|
|
813
|
-
)
|
|
814
|
-
replace_in_file(
|
|
815
|
-
gradle_path,
|
|
816
|
-
/^\s*applicationId = ".*"$/,
|
|
817
|
-
%( applicationId = "#{metadata[:android_application_id]}")
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
Dir.glob(
|
|
821
|
-
File.join(client_dir, "android", "app", "src", "main", "kotlin", "**", "MainActivity.kt")
|
|
822
|
-
).each do |activity_path|
|
|
823
|
-
replace_in_file(
|
|
824
|
-
activity_path,
|
|
825
|
-
/^package\s+[^\s]+$/,
|
|
826
|
-
"package #{metadata[:android_application_id]}"
|
|
827
|
-
)
|
|
828
|
-
end
|
|
829
|
-
|
|
830
1266
|
manifest_path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
831
1267
|
replace_in_file(
|
|
832
1268
|
manifest_path,
|
|
@@ -843,17 +1279,37 @@ module Ruflet
|
|
|
843
1279
|
pbxproj_path = File.join(client_dir, "ios", "Runner.xcodeproj", "project.pbxproj")
|
|
844
1280
|
return unless File.file?(pbxproj_path)
|
|
845
1281
|
|
|
846
|
-
content =
|
|
1282
|
+
content = read_text_file(pbxproj_path)
|
|
847
1283
|
content.gsub!(/INFOPLIST_KEY_CFBundleDisplayName = "[^"]*";/, %(INFOPLIST_KEY_CFBundleDisplayName = "#{xcode_escape(metadata[:display_name])}";))
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
|
857
1313
|
end
|
|
858
1314
|
|
|
859
1315
|
def apply_macos_metadata(client_dir, metadata)
|
|
@@ -878,11 +1334,11 @@ module Ruflet
|
|
|
878
1334
|
def apply_web_metadata(client_dir, metadata)
|
|
879
1335
|
manifest_path = File.join(client_dir, "web", "manifest.json")
|
|
880
1336
|
if File.file?(manifest_path)
|
|
881
|
-
data = JSON.parse(
|
|
1337
|
+
data = JSON.parse(read_text_file(manifest_path))
|
|
882
1338
|
data["name"] = metadata[:display_name]
|
|
883
1339
|
data["short_name"] = metadata[:short_name]
|
|
884
1340
|
data["description"] = metadata[:description]
|
|
885
|
-
|
|
1341
|
+
write_text_file(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
886
1342
|
end
|
|
887
1343
|
|
|
888
1344
|
index_path = File.join(client_dir, "web", "index.html")
|
|
@@ -947,23 +1403,34 @@ module Ruflet
|
|
|
947
1403
|
replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
|
|
948
1404
|
end
|
|
949
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
|
+
|
|
950
1417
|
def replace_plist_value(path, key, value)
|
|
951
1418
|
return unless File.file?(path)
|
|
952
1419
|
|
|
953
|
-
content =
|
|
1420
|
+
content = read_text_file(path)
|
|
954
1421
|
pattern = %r{(<key>#{Regexp.escape(key)}</key>\s*<string>)(.*?)(</string>)}m
|
|
955
1422
|
updated = content.gsub(pattern) do
|
|
956
1423
|
"#{Regexp.last_match(1)}#{xml_escape(value)}#{Regexp.last_match(3)}"
|
|
957
1424
|
end
|
|
958
|
-
|
|
1425
|
+
write_text_file(path, updated) unless updated == content
|
|
959
1426
|
end
|
|
960
1427
|
|
|
961
1428
|
def replace_in_file(path, pattern, replacement)
|
|
962
1429
|
return unless File.file?(path)
|
|
963
1430
|
|
|
964
|
-
content =
|
|
1431
|
+
content = read_text_file(path)
|
|
965
1432
|
updated = content.gsub(pattern) { replacement }
|
|
966
|
-
|
|
1433
|
+
write_text_file(path, updated) unless updated == content
|
|
967
1434
|
end
|
|
968
1435
|
|
|
969
1436
|
def first_present(*values)
|
|
@@ -1071,6 +1538,13 @@ module Ruflet
|
|
|
1071
1538
|
|
|
1072
1539
|
def apply_native_service_permissions(client_dir, config)
|
|
1073
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
|
|
1074
1548
|
return if entries.empty?
|
|
1075
1549
|
|
|
1076
1550
|
apply_android_service_permissions(client_dir, entries)
|
|
@@ -1081,20 +1555,20 @@ module Ruflet
|
|
|
1081
1555
|
path = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1082
1556
|
return unless File.file?(path)
|
|
1083
1557
|
|
|
1084
|
-
content =
|
|
1558
|
+
content = read_text_file(path)
|
|
1085
1559
|
entries.flat_map { |entry| ANDROID_SERVICE_PERMISSIONS.fetch(entry[:name], []) }.uniq.each do |permission|
|
|
1086
1560
|
next if content.include?(%(android:name="#{permission}"))
|
|
1087
1561
|
|
|
1088
1562
|
content.sub!(/<manifest\b[^>]*>\s*/, "\\0 <uses-permission android:name=\"#{permission}\"/>\n")
|
|
1089
1563
|
end
|
|
1090
|
-
|
|
1564
|
+
write_text_file(path, content)
|
|
1091
1565
|
end
|
|
1092
1566
|
|
|
1093
1567
|
def apply_ios_service_usage_descriptions(client_dir, entries)
|
|
1094
1568
|
path = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1095
1569
|
return unless File.file?(path)
|
|
1096
1570
|
|
|
1097
|
-
content =
|
|
1571
|
+
content = read_text_file(path)
|
|
1098
1572
|
entries.each do |entry|
|
|
1099
1573
|
key = IOS_SERVICE_USAGE_KEYS[entry[:name]]
|
|
1100
1574
|
next unless key
|
|
@@ -1110,7 +1584,7 @@ module Ruflet
|
|
|
1110
1584
|
content.sub!(%r{</dict>\s*</plist>}m, "#{pair}</dict>\n</plist>")
|
|
1111
1585
|
end
|
|
1112
1586
|
end
|
|
1113
|
-
|
|
1587
|
+
write_text_file(path, content)
|
|
1114
1588
|
end
|
|
1115
1589
|
|
|
1116
1590
|
def clear_flutter_build_state(client_dir, verbose: false)
|
|
@@ -1161,11 +1635,11 @@ module Ruflet
|
|
|
1161
1635
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
1162
1636
|
return unless File.file?(pubspec_path)
|
|
1163
1637
|
|
|
1164
|
-
data = YAML.safe_load(
|
|
1638
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
1165
1639
|
dependencies = data["dependencies"]
|
|
1166
1640
|
dependencies = data["dependencies"] = {} unless dependencies.is_a?(Hash)
|
|
1167
|
-
spinkit_dependency = template_client_pubspec_dependencies["
|
|
1168
|
-
dependencies["
|
|
1641
|
+
spinkit_dependency = template_client_pubspec_dependencies["flet_spinkit"]
|
|
1642
|
+
dependencies["flet_spinkit"] = spinkit_dependency if spinkit_dependency
|
|
1169
1643
|
flutter = data["flutter"]
|
|
1170
1644
|
flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
|
|
1171
1645
|
assets = Array(flutter["assets"]).map(&:to_s)
|
|
@@ -1189,11 +1663,16 @@ module Ruflet
|
|
|
1189
1663
|
write_pubspec_yaml(pubspec_path, data)
|
|
1190
1664
|
end
|
|
1191
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
|
+
|
|
1192
1671
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1193
1672
|
local_path = explicit_local_ruby_runtime_path || source_checkout_ruby_runtime_path
|
|
1194
1673
|
return { "path" => local_path } if local_path
|
|
1195
1674
|
|
|
1196
|
-
current_dependency ||
|
|
1675
|
+
current_dependency || PUBLISHED_RUBY_RUNTIME_CONSTRAINT
|
|
1197
1676
|
end
|
|
1198
1677
|
|
|
1199
1678
|
def explicit_local_ruby_runtime_path
|
|
@@ -1225,7 +1704,6 @@ module Ruflet
|
|
|
1225
1704
|
"lib/main.self.dart",
|
|
1226
1705
|
"lib/main.server.dart",
|
|
1227
1706
|
"lib/ruflet_file_picker_service.dart",
|
|
1228
|
-
"lib/ruflet_spinkit.dart",
|
|
1229
1707
|
"lib/connection_probe.dart",
|
|
1230
1708
|
"lib/connection_probe_io.dart",
|
|
1231
1709
|
"lib/connection_probe_stub.dart",
|
|
@@ -1250,7 +1728,7 @@ module Ruflet
|
|
|
1250
1728
|
|
|
1251
1729
|
content = indent_pubspec_sequences(content)
|
|
1252
1730
|
|
|
1253
|
-
|
|
1731
|
+
write_text_file(path, content)
|
|
1254
1732
|
end
|
|
1255
1733
|
|
|
1256
1734
|
def indent_pubspec_sequences(content)
|
|
@@ -1352,6 +1830,8 @@ module Ruflet
|
|
|
1352
1830
|
.vscode
|
|
1353
1831
|
build
|
|
1354
1832
|
coverage
|
|
1833
|
+
credentials
|
|
1834
|
+
fastlane
|
|
1355
1835
|
log
|
|
1356
1836
|
node_modules
|
|
1357
1837
|
pkg
|
|
@@ -1364,10 +1844,30 @@ module Ruflet
|
|
|
1364
1844
|
end
|
|
1365
1845
|
end
|
|
1366
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
|
+
|
|
1367
1863
|
def include_project_asset_file?(relative)
|
|
1368
1864
|
basename = File.basename(relative)
|
|
1369
1865
|
return false if basename == ".DS_Store"
|
|
1370
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
|
|
1371
1871
|
true
|
|
1372
1872
|
end
|
|
1373
1873
|
|
|
@@ -1398,12 +1898,13 @@ module Ruflet
|
|
|
1398
1898
|
|
|
1399
1899
|
key.tr!("-", "_")
|
|
1400
1900
|
key.gsub!(/\A(flet_)+/, "")
|
|
1901
|
+
key.gsub!(/\A(ruflet_)+/, "")
|
|
1401
1902
|
key.gsub!(/\Aservice_/, "")
|
|
1402
1903
|
key
|
|
1403
1904
|
end
|
|
1404
1905
|
|
|
1405
1906
|
def prune_client_pubspec(path, selected_packages)
|
|
1406
|
-
data = YAML.safe_load(
|
|
1907
|
+
data = YAML.safe_load(read_text_file(path), aliases: true) || {}
|
|
1407
1908
|
deps = (data["dependencies"] || {}).dup
|
|
1408
1909
|
optional_packages = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:package) }.uniq
|
|
1409
1910
|
|
|
@@ -1424,7 +1925,7 @@ module Ruflet
|
|
|
1424
1925
|
template_deps = template_client_pubspec_dependencies
|
|
1425
1926
|
return if template_deps.empty?
|
|
1426
1927
|
|
|
1427
|
-
data = YAML.safe_load(
|
|
1928
|
+
data = YAML.safe_load(read_text_file(path), aliases: true) || {}
|
|
1428
1929
|
deps = (data["dependencies"] || {}).dup
|
|
1429
1930
|
selected_packages.each do |package_name|
|
|
1430
1931
|
deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
|
|
@@ -1444,7 +1945,7 @@ module Ruflet
|
|
|
1444
1945
|
pubspec_path = File.join(template_root, "pubspec.yaml")
|
|
1445
1946
|
return {} unless File.file?(pubspec_path)
|
|
1446
1947
|
|
|
1447
|
-
data = YAML.safe_load(
|
|
1948
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
1448
1949
|
deps = data["dependencies"]
|
|
1449
1950
|
deps.is_a?(Hash) ? deps : {}
|
|
1450
1951
|
rescue StandardError
|
|
@@ -1457,8 +1958,8 @@ module Ruflet
|
|
|
1457
1958
|
template_path = template_client_entrypoint_path(File.basename(path))
|
|
1458
1959
|
return unless template_path
|
|
1459
1960
|
|
|
1460
|
-
content =
|
|
1461
|
-
template =
|
|
1961
|
+
content = read_text_file(path)
|
|
1962
|
+
template = read_text_file(template_path)
|
|
1462
1963
|
|
|
1463
1964
|
selected_aliases.each do |extension_alias|
|
|
1464
1965
|
import_line = template.lines.find { |line| line.match?(/\sas #{Regexp.escape(extension_alias)};\s*\z/) }
|
|
@@ -1470,7 +1971,7 @@ module Ruflet
|
|
|
1470
1971
|
content = insert_missing_extension(content, extension_line) if extension_line && !content.include?(extension_line)
|
|
1471
1972
|
end
|
|
1472
1973
|
|
|
1473
|
-
|
|
1974
|
+
write_text_file(path, content)
|
|
1474
1975
|
end
|
|
1475
1976
|
|
|
1476
1977
|
def template_client_entrypoint_path(name)
|
|
@@ -1508,7 +2009,7 @@ module Ruflet
|
|
|
1508
2009
|
end
|
|
1509
2010
|
|
|
1510
2011
|
def prune_client_main(path, selected_aliases)
|
|
1511
|
-
content =
|
|
2012
|
+
content = read_text_file(path)
|
|
1512
2013
|
alias_to_package = {}
|
|
1513
2014
|
optional_aliases = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:alias) }.uniq
|
|
1514
2015
|
|
|
@@ -1547,11 +2048,58 @@ module Ruflet
|
|
|
1547
2048
|
end
|
|
1548
2049
|
end
|
|
1549
2050
|
|
|
1550
|
-
|
|
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")))
|
|
1551
2099
|
end
|
|
1552
2100
|
|
|
1553
2101
|
def update_pubspec_value(path, block, key, value, multiple: false)
|
|
1554
|
-
lines =
|
|
2102
|
+
lines = read_text_file(path).split("\n", -1)
|
|
1555
2103
|
out = []
|
|
1556
2104
|
in_block = false
|
|
1557
2105
|
replaced = false
|
|
@@ -1586,7 +2134,7 @@ module Ruflet
|
|
|
1586
2134
|
if in_block && !replaced
|
|
1587
2135
|
out << "#{block_indent}#{key}: #{value}"
|
|
1588
2136
|
end
|
|
1589
|
-
|
|
2137
|
+
write_text_file(path, indent_pubspec_sequences(out.join("\n")))
|
|
1590
2138
|
end
|
|
1591
2139
|
|
|
1592
2140
|
def flutter_build_command(platform)
|