ruflet 0.0.22 → 0.0.23
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 +7 -16
- data/lib/ruflet/cli/build_command.rb +104 -250
- data/lib/ruflet/cli/new_command.rb +0 -16
- data/lib/ruflet/cli/run_command.rb +16 -224
- data/lib/ruflet/cli.rb +2 -2
- data/lib/ruflet/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb526fe95e57e72ddf182e86be54239b05a6f782a0f574780aa6fabca3a69ef6
|
|
4
|
+
data.tar.gz: fd8a42692977eb964011e2d087e838345e9d836dca2f6d11f7560197b9cae562
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2b0ef3741d51fe6cdba91d41db6fd5dba388974dedcd00751981ee89a399bfad1f0d07e9b8d7c32e512f80fd652ee784fcda80d71105f70fad323e63e7876c0
|
|
7
|
+
data.tar.gz: cfe6ec3fe141f6edc414e866d204bc7831cbc1aa8fed27d82cae43a84b20df6e78e56819bd7341818025a3f1ed4dc63322b46014cccb8007acffae090e0ec14e
|
data/README.md
CHANGED
|
@@ -25,21 +25,16 @@ dependencies. Run Ruflet commands directly inside the generated project.
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
ruflet new <appname>
|
|
28
|
-
ruflet run [scriptname|path] [--web|--desktop] [--
|
|
28
|
+
ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
|
|
29
29
|
ruflet debug [scriptname|path]
|
|
30
30
|
ruflet devices
|
|
31
31
|
ruflet emulators
|
|
32
32
|
ruflet doctor [--fix]
|
|
33
33
|
ruflet update [web|desktop|all] [--check] [--force]
|
|
34
|
-
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self]
|
|
34
|
+
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self]
|
|
35
35
|
ruflet install [--device DEVICE_ID]
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`--experimental` is available only for iOS and macOS builds. It selects the
|
|
39
|
-
native Apple renderer while the normal Ruflet build pipeline continues to
|
|
40
|
-
resolve the declared services, extensions, permissions, assets, and runtime
|
|
41
|
-
mode. Without it, Apple builds use only the Flutter renderer.
|
|
42
|
-
|
|
43
38
|
`--lite` creates the compact self-contained build: project Ruby is precompiled
|
|
44
39
|
when the matching mruby compiler is available, and the bundled VM starts in
|
|
45
40
|
parallel with Flutter. `--self` remains an alias for this profile.
|
|
@@ -53,15 +48,11 @@ a target-specific CRuby runtime. Set `RUFLET_FULL_RUNTIME_PATH`, or
|
|
|
53
48
|
Ruflet refuses to substitute the lite mruby engine for a requested full build.
|
|
54
49
|
|
|
55
50
|
Before Flutter resolves or bundles packages, Ruflet removes extension plugins
|
|
56
|
-
that are not selected by the current services/extensions configuration.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
experimental Ruflet Explorer iOS Simulator prebuild the first time, reuses its
|
|
62
|
-
versioned cache afterward, and launches it on the already-booted simulator
|
|
63
|
-
with the current backend URL. Add `--desktop` to download and launch the native
|
|
64
|
-
macOS prebuild instead: `ruflet run --desktop --exp`.
|
|
51
|
+
that are not selected by the current services/extensions configuration.
|
|
52
|
+
|
|
53
|
+
`ruflet run --desktop` downloads the Ruflet Explorer prebuild for the host the
|
|
54
|
+
first time, reuses its versioned cache afterward, and launches it with the
|
|
55
|
+
current backend URL.
|
|
65
56
|
|
|
66
57
|
Commands that create, diagnose, or build a Flutter client compare the cached
|
|
67
58
|
template revision with `AdamMusa/ruflet-template` on GitHub. When `main`
|
|
@@ -35,30 +35,6 @@ module Ruflet
|
|
|
35
35
|
"video" => { package: "ruflet_video", alias: "ruflet_video" },
|
|
36
36
|
"webview" => { package: "ruflet_webview", alias: "ruflet_webview" }
|
|
37
37
|
}.freeze
|
|
38
|
-
# Native Swift products matching the Ruflet extensions selected by the
|
|
39
|
-
# ordinary Ruflet build configuration. The same resolved extension keys
|
|
40
|
-
# drive Dart imports, Swift imports/registration, and Xcode linkage.
|
|
41
|
-
NATIVE_APPLE_EXTENSION_MAP = {
|
|
42
|
-
"ads" => { module: "RufletAds", initializer: "RufletAds.Extension()" },
|
|
43
|
-
"audio" => { module: "RufletAudio", initializer: "RufletAudioExtension()" },
|
|
44
|
-
"audio_recorder" => { module: "RufletAudioRecorder", initializer: "RufletAudioRecorderExtension()" },
|
|
45
|
-
"camera" => { module: "RufletCamera", initializer: "RufletCameraExtension()" },
|
|
46
|
-
"charts" => { module: "RufletCharts", initializer: "RufletChartsExtension()" },
|
|
47
|
-
"code_editor" => { module: "RufletCodeEditor", initializer: "RufletCodeEditorExtension()" },
|
|
48
|
-
"color_pickers" => { module: "RufletColorPickers", initializer: "RufletColorPickersExtension()" },
|
|
49
|
-
"datatable2" => { module: "RufletDataTable2", initializer: "RufletDataTable2Extension()" },
|
|
50
|
-
"flashlight" => { module: "RufletFlashlight", initializer: "RufletFlashlightExtension()" },
|
|
51
|
-
"geolocator" => { module: "RufletGeolocator", initializer: "RufletGeolocatorExtension()" },
|
|
52
|
-
"lottie" => { module: "RufletLottie", initializer: "RufletLottieExtension()" },
|
|
53
|
-
"map" => { module: "RufletMap", initializer: "RufletMap.Extension()" },
|
|
54
|
-
"permission_handler" => { module: "RufletPermissionHandler", initializer: "RufletPermissionHandlerExtension()" },
|
|
55
|
-
"qrcode_scanner" => { module: "RufletQRScanner", initializer: "RufletQRScannerExtension()" },
|
|
56
|
-
"rive" => { module: "RufletRive", initializer: "RufletRiveExtension()" },
|
|
57
|
-
"secure_storage" => { module: "RufletSecureStorage", initializer: "RufletSecureStorageExtension()" },
|
|
58
|
-
"spinkit" => { module: "RufletSpinKit", initializer: "RufletSpinKitExtension()" },
|
|
59
|
-
"video" => { module: "RufletVideo", initializer: "RufletVideoExtension()" },
|
|
60
|
-
"webview" => { module: "RufletWebView", initializer: "RufletWebViewExtension()" }
|
|
61
|
-
}.freeze
|
|
62
38
|
PROTECTED_SERVICE_EXTENSIONS = {
|
|
63
39
|
"camera" => %w[camera permission_handler],
|
|
64
40
|
"microphone" => %w[audio_recorder permission_handler],
|
|
@@ -152,18 +128,10 @@ module Ruflet
|
|
|
152
128
|
runtime_profile = full ? :full : :lite
|
|
153
129
|
@ruflet_runtime_profile = self_contained ? runtime_profile : :server
|
|
154
130
|
@ruflet_runtime_profile_explicit = !!(lite || full)
|
|
155
|
-
experimental = args.delete("--experimental")
|
|
156
|
-
experimental_alias = args.delete("--exp")
|
|
157
|
-
experimental ||= experimental_alias
|
|
158
131
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
159
132
|
platform = (args.shift || "").downcase
|
|
160
133
|
if platform.empty?
|
|
161
|
-
warn "Usage: ruflet build <apk|android|aab|ios|ipa|web|macos|windows|linux> [--lite|--full|--self] [--
|
|
162
|
-
return 1
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
if experimental && !%w[ios ipa macos].include?(platform)
|
|
166
|
-
warn "build config error: --experimental is supported only for ios and macos"
|
|
134
|
+
warn "Usage: ruflet build <apk|android|aab|ios|ipa|web|macos|windows|linux> [--lite|--full|--self] [--verbose]"
|
|
167
135
|
return 1
|
|
168
136
|
end
|
|
169
137
|
|
|
@@ -178,7 +146,6 @@ module Ruflet
|
|
|
178
146
|
requested_platform = platform
|
|
179
147
|
platform = "ios" if platform == "ipa"
|
|
180
148
|
@ruflet_build_platform = platform
|
|
181
|
-
@ruflet_experimental_native_renderer = !!experimental
|
|
182
149
|
|
|
183
150
|
# The embedded Ruby VM is a native plugin with no browser
|
|
184
151
|
# implementation, so a self-contained web build produces an app that
|
|
@@ -198,9 +165,8 @@ module Ruflet
|
|
|
198
165
|
return 1
|
|
199
166
|
end
|
|
200
167
|
|
|
201
|
-
renderer = experimental ? ", experimental native renderer" : ""
|
|
202
168
|
mode = self_contained ? "self-contained #{runtime_profile}" : "server-driven"
|
|
203
|
-
build_note("Preparing #{platform} build (#{mode}
|
|
169
|
+
build_note("Preparing #{platform} build (#{mode})")
|
|
204
170
|
config = load_ruflet_config
|
|
205
171
|
if runtime_profile == :full && self_contained
|
|
206
172
|
return 1 unless configure_full_runtime_distribution(config, platform, verbose: !!verbose)
|
|
@@ -230,6 +196,9 @@ module Ruflet
|
|
|
230
196
|
if @ruflet_runtime_profile_explicit
|
|
231
197
|
build_args += ["--dart-define", "RUFLET_RUNTIME_PROFILE=#{runtime_profile}"]
|
|
232
198
|
end
|
|
199
|
+
# The platform layer starts the VM before Flutter exists, so the app
|
|
200
|
+
# bundle -- not a Dart define -- has to carry the same decision.
|
|
201
|
+
configure_platform_autostart(client_dir, platform, verbose: !!verbose)
|
|
233
202
|
elsif backend_url
|
|
234
203
|
build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"]
|
|
235
204
|
elsif RUNTIME_RESOLVED_BACKEND_PLATFORMS.include?(platform)
|
|
@@ -243,9 +212,6 @@ module Ruflet
|
|
|
243
212
|
warn "Set app.backend_url or backend_url in ruflet.yaml"
|
|
244
213
|
return 1
|
|
245
214
|
end
|
|
246
|
-
if experimental
|
|
247
|
-
build_args += ["--dart-define", "RUFLET_EXPERIMENTAL_NATIVE_RENDERER=true"]
|
|
248
|
-
end
|
|
249
215
|
build_args << "-v" if verbose
|
|
250
216
|
stage_ios_simulator_ruby_runtime(client_dir, build_args, verbose: !!verbose) if self_contained
|
|
251
217
|
|
|
@@ -326,10 +292,6 @@ module Ruflet
|
|
|
326
292
|
|
|
327
293
|
private
|
|
328
294
|
|
|
329
|
-
def experimental_native_renderer?
|
|
330
|
-
@ruflet_experimental_native_renderer == true
|
|
331
|
-
end
|
|
332
|
-
|
|
333
295
|
def extract_option_value!(args, *flags)
|
|
334
296
|
flags.each do |flag|
|
|
335
297
|
index = args.index(flag)
|
|
@@ -586,15 +548,10 @@ module Ruflet
|
|
|
586
548
|
refreshed = refresh_managed_client_template_files(
|
|
587
549
|
client_dir, platform: platform, verbose: verbose)
|
|
588
550
|
return false if refreshed == false
|
|
589
|
-
unless
|
|
590
|
-
client_dir, platform: platform,
|
|
591
|
-
verbose: verbose)
|
|
551
|
+
unless remove_native_apple_renderer_integration(
|
|
552
|
+
client_dir, platform: platform, verbose: verbose)
|
|
592
553
|
return false
|
|
593
554
|
end
|
|
594
|
-
if experimental_native_renderer?
|
|
595
|
-
sync_application_apple_extensions(
|
|
596
|
-
client_dir, platform: platform, verbose: verbose)
|
|
597
|
-
end
|
|
598
555
|
metadata = sync_client_metadata(client_dir, config, verbose: verbose)
|
|
599
556
|
return false unless validate_mobile_app_identity(metadata, platform: platform)
|
|
600
557
|
|
|
@@ -605,7 +562,7 @@ module Ruflet
|
|
|
605
562
|
return false if configured == false
|
|
606
563
|
configure_native_apple_runtime(
|
|
607
564
|
client_dir, platform: platform, self_contained: self_contained,
|
|
608
|
-
config: config,
|
|
565
|
+
config: config, verbose: verbose)
|
|
609
566
|
configure_android_runtime(
|
|
610
567
|
client_dir, platform: platform, self_contained: self_contained,
|
|
611
568
|
verbose: verbose)
|
|
@@ -614,11 +571,6 @@ module Ruflet
|
|
|
614
571
|
if @ruflet_extension_selection_applied && !validate_flutter_extension_selection(client_dir)
|
|
615
572
|
return false
|
|
616
573
|
end
|
|
617
|
-
if experimental_native_renderer?
|
|
618
|
-
apply_native_apple_extension_config(
|
|
619
|
-
client_dir, platform: platform, extension_keys: extension_keys,
|
|
620
|
-
verbose: verbose)
|
|
621
|
-
end
|
|
622
574
|
asset_flags = apply_build_config(client_dir, config)
|
|
623
575
|
if asset_flags[:error]
|
|
624
576
|
warn asset_flags[:error]
|
|
@@ -1731,15 +1683,12 @@ module Ruflet
|
|
|
1731
1683
|
|
|
1732
1684
|
def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
|
|
1733
1685
|
extension_keys = configured_extension_keys(config)
|
|
1734
|
-
|
|
1735
|
-
# the resolved keys for native registration, but do not also ship the
|
|
1736
|
-
# equivalent Dart plugins and their native Flutter frameworks.
|
|
1737
|
-
flutter_extension_keys = experimental_native_renderer? ? [] : extension_keys
|
|
1686
|
+
flutter_extension_keys = extension_keys
|
|
1738
1687
|
extension_packages = flutter_extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
1739
1688
|
extension_aliases = flutter_extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
1740
1689
|
|
|
1741
1690
|
configured_external = external_extension_entries(config)
|
|
1742
|
-
external =
|
|
1691
|
+
external = configured_external
|
|
1743
1692
|
previous_external_names = managed_external_extension_names(client_dir)
|
|
1744
1693
|
discovered_external_names = discover_registered_external_extension_names(client_dir)
|
|
1745
1694
|
removable_external_names = (
|
|
@@ -1773,9 +1722,6 @@ module Ruflet
|
|
|
1773
1722
|
@ruflet_selected_external_extension_packages = external.map { |entry| entry[:name] }
|
|
1774
1723
|
@ruflet_managed_external_extension_packages = removable_external_names
|
|
1775
1724
|
@ruflet_extension_selection_applied = true
|
|
1776
|
-
if experimental_native_renderer? && (!extension_keys.empty? || !configured_external.empty?)
|
|
1777
|
-
build_note("Native Apple extensions selected; excluded equivalent Flutter plugins from the app bundle")
|
|
1778
|
-
end
|
|
1779
1725
|
extension_keys
|
|
1780
1726
|
end
|
|
1781
1727
|
|
|
@@ -2249,7 +2195,10 @@ module Ruflet
|
|
|
2249
2195
|
# A self-contained build needs the embedded VM. Prefer a local checkout so
|
|
2250
2196
|
# the runtime under development is the one packaged, and otherwise resolve
|
|
2251
2197
|
# the published package.
|
|
2252
|
-
|
|
2198
|
+
# 0.0.13 is the first release where the platform layer starts the VM and
|
|
2199
|
+
# exposes serverUrl(); the client entrypoint calls it, so an older runtime
|
|
2200
|
+
# would not build.
|
|
2201
|
+
PUBLISHED_RUBY_RUNTIME_CONSTRAINT = "^0.0.14"
|
|
2253
2202
|
|
|
2254
2203
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
2255
2204
|
if embedded_runtime_profile == :full && @ruflet_full_runtime_path
|
|
@@ -2341,16 +2290,10 @@ module Ruflet
|
|
|
2341
2290
|
return false unless validate_template_ruflet_source_integrity(
|
|
2342
2291
|
template_root, verbose: verbose)
|
|
2343
2292
|
|
|
2344
|
-
if experimental_native_renderer? && %w[ios ipa macos].include?(platform.to_s)
|
|
2345
|
-
return false unless validate_native_apple_renderer_distribution(
|
|
2346
|
-
template_root, verbose: verbose)
|
|
2347
|
-
end
|
|
2348
|
-
|
|
2349
2293
|
managed_files = [
|
|
2350
2294
|
"lib/main.dart",
|
|
2351
2295
|
"lib/main.self.dart",
|
|
2352
2296
|
"lib/main.server.dart",
|
|
2353
|
-
"lib/native_renderer.dart",
|
|
2354
2297
|
"lib/connection_probe.dart",
|
|
2355
2298
|
"lib/connection_probe_io.dart",
|
|
2356
2299
|
"lib/connection_probe_stub.dart",
|
|
@@ -2371,7 +2314,6 @@ module Ruflet
|
|
|
2371
2314
|
managed_files.concat(
|
|
2372
2315
|
[
|
|
2373
2316
|
"ios/Runner/AppDelegate.swift",
|
|
2374
|
-
"ios/Runner/RufletEngineChoice.swift",
|
|
2375
2317
|
"ios/Runner.xcodeproj/project.pbxproj"
|
|
2376
2318
|
]
|
|
2377
2319
|
)
|
|
@@ -2379,7 +2321,6 @@ module Ruflet
|
|
|
2379
2321
|
managed_files.concat(
|
|
2380
2322
|
[
|
|
2381
2323
|
"macos/Runner/MainFlutterWindow.swift",
|
|
2382
|
-
"macos/Runner/RufletEngineChoice.swift",
|
|
2383
2324
|
"macos/Runner.xcodeproj/project.pbxproj"
|
|
2384
2325
|
]
|
|
2385
2326
|
)
|
|
@@ -2395,11 +2336,6 @@ module Ruflet
|
|
|
2395
2336
|
build_log(verbose, "refreshed template file #{relative_path}")
|
|
2396
2337
|
end
|
|
2397
2338
|
|
|
2398
|
-
if experimental_native_renderer? && %w[ios ipa macos].include?(platform.to_s)
|
|
2399
|
-
sync_managed_template_tree(
|
|
2400
|
-
template_root, client_dir, "apple_packages/ruflet_apple", verbose: verbose)
|
|
2401
|
-
end
|
|
2402
|
-
|
|
2403
2339
|
# Older managed clients carried a macOS-only FilePicker override. Ruflet's
|
|
2404
2340
|
# core service owns FilePicker now, so this duplicate must not survive a
|
|
2405
2341
|
# template refresh.
|
|
@@ -2475,44 +2411,17 @@ module Ruflet
|
|
|
2475
2411
|
false
|
|
2476
2412
|
end
|
|
2477
2413
|
|
|
2478
|
-
#
|
|
2479
|
-
#
|
|
2480
|
-
#
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
required = [
|
|
2484
|
-
"Package.swift",
|
|
2485
|
-
"Sources/RufletEngine/RufletApp.swift",
|
|
2486
|
-
"Sources/RufletEngine/RufletAppView.swift"
|
|
2487
|
-
]
|
|
2488
|
-
missing = required.reject { |relative| File.file?(File.join(package_root, relative)) }
|
|
2489
|
-
return true if missing.empty?
|
|
2490
|
-
|
|
2491
|
-
warn "build config error: the authoritative Apple renderer is incomplete"
|
|
2492
|
-
missing.each { |relative| warn " missing: apple_packages/ruflet_apple/#{relative}" }
|
|
2493
|
-
false
|
|
2494
|
-
end
|
|
2495
|
-
|
|
2496
|
-
def configure_apple_renderer_integration(client_dir, platform:, enabled:, verbose: false)
|
|
2414
|
+
# Ruflet had a second, Swift-rendered Apple host. It is gone: every
|
|
2415
|
+
# platform renders through the Ruflet Flutter engine. A client generated
|
|
2416
|
+
# while it existed still carries the Swift package, the engine-choice
|
|
2417
|
+
# shim and the Xcode references, so a rebuild strips them.
|
|
2418
|
+
def remove_native_apple_renderer_integration(client_dir, platform:, verbose: false)
|
|
2497
2419
|
apple_platform = case platform.to_s
|
|
2498
2420
|
when "ios", "ipa" then "ios"
|
|
2499
2421
|
when "macos" then "macos"
|
|
2500
2422
|
end
|
|
2501
2423
|
return true unless apple_platform
|
|
2502
2424
|
|
|
2503
|
-
if enabled
|
|
2504
|
-
package = File.join(client_dir, "apple_packages", "ruflet_apple", "Package.swift")
|
|
2505
|
-
choice = File.join(client_dir, apple_platform, "Runner", "RufletEngineChoice.swift")
|
|
2506
|
-
unless File.file?(package) && File.file?(choice)
|
|
2507
|
-
warn "build config error: the Ruflet client template does not contain the native Apple renderer"
|
|
2508
|
-
warn "Refresh the Ruflet template, then run the build again."
|
|
2509
|
-
return false
|
|
2510
|
-
end
|
|
2511
|
-
configure_ios_scene_delegate(client_dir, native: true) if apple_platform == "ios"
|
|
2512
|
-
build_log(verbose, "enabled experimental native Apple renderer")
|
|
2513
|
-
return true
|
|
2514
|
-
end
|
|
2515
|
-
|
|
2516
2425
|
unless restore_flutter_apple_host(client_dir, apple_platform, verbose: verbose)
|
|
2517
2426
|
return false
|
|
2518
2427
|
end
|
|
@@ -2520,12 +2429,12 @@ module Ruflet
|
|
|
2520
2429
|
FileUtils.rm_f(File.join(client_dir, apple_platform, "Runner", "RufletEngineChoice.swift"))
|
|
2521
2430
|
FileUtils.rm_rf(File.join(client_dir, "apple_packages"))
|
|
2522
2431
|
FileUtils.rm_rf(File.join(client_dir, "apple_extensions"))
|
|
2523
|
-
configure_ios_scene_delegate(client_dir
|
|
2432
|
+
configure_ios_scene_delegate(client_dir) if apple_platform == "ios"
|
|
2524
2433
|
apple_info_plist_paths(client_dir).each do |path|
|
|
2525
2434
|
remove_plist_value(path, "RufletExperimentalNativeRenderer")
|
|
2526
2435
|
remove_plist_value(path, "GADApplicationIdentifier")
|
|
2527
2436
|
end
|
|
2528
|
-
build_log(verbose, "
|
|
2437
|
+
build_log(verbose, "removed native Apple renderer integration")
|
|
2529
2438
|
true
|
|
2530
2439
|
end
|
|
2531
2440
|
|
|
@@ -2564,15 +2473,19 @@ module Ruflet
|
|
|
2564
2473
|
write_text_file(path, content)
|
|
2565
2474
|
end
|
|
2566
2475
|
|
|
2567
|
-
|
|
2476
|
+
# Only the Flutter host remains, so the scene delegate is Flutter's own.
|
|
2477
|
+
# A client built against the Swift renderer names RufletSceneDelegate,
|
|
2478
|
+
# which lived in the deleted package -- launching it would trap on a
|
|
2479
|
+
# missing class. FlutterSceneDelegate comes from the Flutter framework,
|
|
2480
|
+
# so it carries no $(PRODUCT_MODULE_NAME) prefix.
|
|
2481
|
+
def configure_ios_scene_delegate(client_dir)
|
|
2568
2482
|
path = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
2569
2483
|
return unless File.file?(path)
|
|
2570
2484
|
|
|
2571
2485
|
content = read_text_file(path)
|
|
2572
|
-
delegate = native ? "RufletSceneDelegate" : "SceneDelegate"
|
|
2573
2486
|
content.gsub!(
|
|
2574
|
-
%r{(<key>UISceneDelegateClassName</key>\s*<string>)
|
|
2575
|
-
"\\
|
|
2487
|
+
%r{(<key>UISceneDelegateClassName</key>\s*<string>)(?:\$\(PRODUCT_MODULE_NAME\)\.)?(?:RufletSceneDelegate|FlutterSceneDelegate|SceneDelegate)(</string>)}m,
|
|
2488
|
+
"\\1FlutterSceneDelegate\\2"
|
|
2576
2489
|
)
|
|
2577
2490
|
write_text_file(path, content)
|
|
2578
2491
|
end
|
|
@@ -2637,136 +2550,8 @@ module Ruflet
|
|
|
2637
2550
|
write_text_file(path, content)
|
|
2638
2551
|
end
|
|
2639
2552
|
|
|
2640
|
-
def apply_native_apple_extension_config(
|
|
2641
|
-
client_dir, platform:, extension_keys:, verbose: false
|
|
2642
|
-
)
|
|
2643
|
-
apple_platform = %w[ios ipa].include?(platform.to_s) ? "ios" : platform.to_s
|
|
2644
|
-
return unless %w[ios macos].include?(apple_platform)
|
|
2645
|
-
|
|
2646
|
-
selected = Array(extension_keys).filter_map do |key|
|
|
2647
|
-
spec = NATIVE_APPLE_EXTENSION_MAP[key]
|
|
2648
|
-
[key, spec] if spec
|
|
2649
|
-
end
|
|
2650
|
-
selected_modules = selected.map { |_key, spec| spec.fetch(:module) }
|
|
2651
|
-
unselected_modules = NATIVE_APPLE_EXTENSION_MAP.values
|
|
2652
|
-
.map { |spec| spec.fetch(:module) }
|
|
2653
|
-
.uniq - selected_modules
|
|
2654
|
-
|
|
2655
|
-
choice_path = File.join(client_dir, apple_platform, "Runner", "RufletEngineChoice.swift")
|
|
2656
|
-
if File.file?(choice_path)
|
|
2657
|
-
imports = selected_modules.map { |name| "import #{name}" }.join("\n")
|
|
2658
|
-
registrations = selected.map do |_key, spec|
|
|
2659
|
-
" result.append(#{spec.fetch(:initializer)})"
|
|
2660
|
-
end.join("\n")
|
|
2661
|
-
source = <<~SWIFT
|
|
2662
|
-
import Foundation
|
|
2663
|
-
import RufletApple
|
|
2664
|
-
import RufletAppExtensions
|
|
2665
|
-
#{imports}
|
|
2666
|
-
|
|
2667
|
-
/// Generated by `ruflet build --experimental` from the same
|
|
2668
|
-
/// services/extensions configuration used by the Flutter client.
|
|
2669
|
-
enum RufletEngineChoice {
|
|
2670
|
-
static var usesNativeRenderer: Bool {
|
|
2671
|
-
let value = Bundle.main.object(
|
|
2672
|
-
forInfoDictionaryKey: "RufletExperimentalNativeRenderer")
|
|
2673
|
-
if let flag = value as? Bool { return flag }
|
|
2674
|
-
if let flag = value as? NSNumber { return flag.boolValue }
|
|
2675
|
-
return false
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
static func pageURL(from raw: String) -> URL? {
|
|
2679
|
-
RufletPageAddress.parse(raw)
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
@MainActor static var extensions: [any RufletExtension] {
|
|
2683
|
-
var result = RufletAppExtensionRegistry.extensions
|
|
2684
|
-
#{registrations}
|
|
2685
|
-
return result
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
SWIFT
|
|
2689
|
-
write_text_file(choice_path, source)
|
|
2690
|
-
end
|
|
2691
|
-
|
|
2692
|
-
pbxproj = File.join(client_dir, apple_platform, "Runner.xcodeproj", "project.pbxproj")
|
|
2693
|
-
if File.file?(pbxproj)
|
|
2694
|
-
content = read_text_file(pbxproj)
|
|
2695
|
-
content = content.lines.reject do |line|
|
|
2696
|
-
unselected_modules.any? do |name|
|
|
2697
|
-
line.match?(/\/\* #{Regexp.escape(name)}(?: in Frameworks)? \*\//)
|
|
2698
|
-
end
|
|
2699
|
-
end.join
|
|
2700
|
-
write_text_file(pbxproj, content)
|
|
2701
|
-
end
|
|
2702
|
-
|
|
2703
|
-
unless selected_modules.include?("RufletAds")
|
|
2704
|
-
remove_plist_value(
|
|
2705
|
-
File.join(client_dir, "ios", "Runner", "Info.plist"),
|
|
2706
|
-
"GADApplicationIdentifier") if apple_platform == "ios"
|
|
2707
|
-
end
|
|
2708
|
-
build_log(
|
|
2709
|
-
verbose,
|
|
2710
|
-
"native Apple extensions=#{selected.empty? ? 'core only' : selected.map(&:first).join(',')}"
|
|
2711
|
-
)
|
|
2712
|
-
end
|
|
2713
|
-
|
|
2714
|
-
def sync_managed_template_tree(template_root, client_dir, relative_path, verbose: false)
|
|
2715
|
-
source = File.join(template_root, relative_path)
|
|
2716
|
-
return unless Dir.exist?(source)
|
|
2717
|
-
|
|
2718
|
-
destination = File.join(client_dir, relative_path)
|
|
2719
|
-
sync_apple_source_tree(source, destination)
|
|
2720
|
-
build_log(verbose, "refreshed template tree #{relative_path}")
|
|
2721
|
-
end
|
|
2722
|
-
|
|
2723
|
-
# Application-defined RufletExtension implementations live outside the
|
|
2724
|
-
# generated Flutter client. New projects own <project>/apple_extensions;
|
|
2725
|
-
# existing projects receive the empty template registry until they opt
|
|
2726
|
-
# in. Both server-driven and self-contained builds use the same package.
|
|
2727
|
-
def sync_application_apple_extensions(client_dir, platform:, verbose: false)
|
|
2728
|
-
return unless %w[ios ipa macos].include?(platform.to_s)
|
|
2729
|
-
|
|
2730
|
-
template_root = if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
2731
|
-
Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
2732
|
-
end
|
|
2733
|
-
project_source = File.join(Dir.pwd, "apple_extensions")
|
|
2734
|
-
template_source = template_root && File.join(template_root, "apple_extensions")
|
|
2735
|
-
source = if File.file?(File.join(project_source, "Package.swift"))
|
|
2736
|
-
project_source
|
|
2737
|
-
elsif template_source && File.file?(File.join(template_source, "Package.swift"))
|
|
2738
|
-
template_source
|
|
2739
|
-
end
|
|
2740
|
-
return unless source
|
|
2741
|
-
|
|
2742
|
-
destination = File.join(client_dir, "apple_extensions")
|
|
2743
|
-
sync_apple_source_tree(source, destination)
|
|
2744
|
-
owner = source == project_source ? "project" : "template default"
|
|
2745
|
-
build_log(verbose, "refreshed #{owner} Apple extensions")
|
|
2746
|
-
end
|
|
2747
|
-
|
|
2748
|
-
def sync_apple_source_tree(source, destination)
|
|
2749
|
-
FileUtils.rm_rf(destination)
|
|
2750
|
-
FileUtils.mkdir_p(destination)
|
|
2751
|
-
# SwiftPM and Dart create mutable caches directly inside the package.
|
|
2752
|
-
# Copying the whole directory and deleting those caches afterwards is
|
|
2753
|
-
# too late: framework resource symlinks inside `.build` can collide or
|
|
2754
|
-
# escape while FileUtils is still traversing them. Treat only the
|
|
2755
|
-
# package's source tree as managed template input from the outset.
|
|
2756
|
-
generated_entries = %w[
|
|
2757
|
-
.swiftpm .dart_tool .claude DerivedData xcuserdata
|
|
2758
|
-
]
|
|
2759
|
-
Dir.children(source).each do |entry|
|
|
2760
|
-
next if generated_entries.include?(entry) || entry == ".build" || entry.start_with?(".build-")
|
|
2761
|
-
|
|
2762
|
-
FileUtils.cp_r(
|
|
2763
|
-
File.join(source, entry), File.join(destination, entry),
|
|
2764
|
-
preserve: true)
|
|
2765
|
-
end
|
|
2766
|
-
end
|
|
2767
|
-
|
|
2768
2553
|
def configure_native_apple_runtime(
|
|
2769
|
-
client_dir, platform:, self_contained:, config: {},
|
|
2554
|
+
client_dir, platform:, self_contained:, config: {}, verbose: false
|
|
2770
2555
|
)
|
|
2771
2556
|
plist_paths = case platform.to_s
|
|
2772
2557
|
when "ios", "ipa"
|
|
@@ -2787,9 +2572,7 @@ module Ruflet
|
|
|
2787
2572
|
upsert_plist_string(
|
|
2788
2573
|
path, "RufletRuntimeProfile",
|
|
2789
2574
|
self_contained ? embedded_runtime_profile.to_s : "")
|
|
2790
|
-
|
|
2791
|
-
path, "RufletExperimentalNativeRenderer",
|
|
2792
|
-
experimental)
|
|
2575
|
+
remove_plist_value(path, "RufletExperimentalNativeRenderer")
|
|
2793
2576
|
apple_platform = platform.to_s == "ipa" ? "ios" : platform.to_s
|
|
2794
2577
|
platform_config = platform_build_config(config, apple_platform)
|
|
2795
2578
|
if platform_config["local_network"] == true
|
|
@@ -3317,6 +3100,62 @@ module Ruflet
|
|
|
3317
3100
|
name
|
|
3318
3101
|
end
|
|
3319
3102
|
|
|
3103
|
+
# Tells the platform layer to start the embedded runtime, and which
|
|
3104
|
+
# packaged project to run.
|
|
3105
|
+
#
|
|
3106
|
+
# This is platform configuration rather than a dart-define because the VM
|
|
3107
|
+
# starts before Dart does: on Apple platforms from the plugin's +load, on
|
|
3108
|
+
# Android from an androidx.startup provider. Neither can read a value that
|
|
3109
|
+
# only exists inside the Dart isolate.
|
|
3110
|
+
#
|
|
3111
|
+
# Desktop needs nothing here -- it has no manifest to carry a flag, and
|
|
3112
|
+
# treats the presence of a packaged project as the opt-in, which is the
|
|
3113
|
+
# same distinction this flag draws on the other platforms.
|
|
3114
|
+
def configure_platform_autostart(client_dir, platform, verbose: false)
|
|
3115
|
+
project = self_contained_project_name
|
|
3116
|
+
case platform
|
|
3117
|
+
when "ios", "macos"
|
|
3118
|
+
plist = File.join(client_dir, platform, "Runner", "Info.plist")
|
|
3119
|
+
return unless File.file?(plist)
|
|
3120
|
+
|
|
3121
|
+
set_plist_value(plist, "RufletRuntimeAutostart", "bool", "true")
|
|
3122
|
+
set_plist_value(plist, "RufletEmbeddedProject", "string", project)
|
|
3123
|
+
build_log(verbose, "enabled runtime autostart in #{platform}/Runner/Info.plist")
|
|
3124
|
+
when "apk", "appbundle", "android"
|
|
3125
|
+
manifest = File.join(
|
|
3126
|
+
client_dir, "android", "app", "src", "main", "AndroidManifest.xml"
|
|
3127
|
+
)
|
|
3128
|
+
return unless File.file?(manifest)
|
|
3129
|
+
|
|
3130
|
+
set_android_meta_data(manifest, "ruflet.runtime.autostart", "true")
|
|
3131
|
+
set_android_meta_data(manifest, "ruflet.runtime.project", project)
|
|
3132
|
+
build_log(verbose, "enabled runtime autostart in AndroidManifest.xml")
|
|
3133
|
+
end
|
|
3134
|
+
end
|
|
3135
|
+
|
|
3136
|
+
def set_plist_value(plist, key, type, value)
|
|
3137
|
+
# Delete first: PlistBuddy's Add fails on an existing key, and a rebuild
|
|
3138
|
+
# after renaming the project would otherwise keep the stale name.
|
|
3139
|
+
system("/usr/libexec/PlistBuddy", "-c", "Delete :#{key}", plist,
|
|
3140
|
+
out: File::NULL, err: File::NULL)
|
|
3141
|
+
system("/usr/libexec/PlistBuddy", "-c", "Add :#{key} #{type} #{value}",
|
|
3142
|
+
plist, out: File::NULL, err: File::NULL)
|
|
3143
|
+
end
|
|
3144
|
+
|
|
3145
|
+
def set_android_meta_data(manifest, name, value)
|
|
3146
|
+
contents = File.read(manifest)
|
|
3147
|
+
entry = %(<meta-data android:name="#{name}" android:value="#{value}" />)
|
|
3148
|
+
existing = /<meta-data\s+android:name="#{Regexp.escape(name)}"[^>]*\/>/
|
|
3149
|
+
|
|
3150
|
+
contents =
|
|
3151
|
+
if contents.match?(existing)
|
|
3152
|
+
contents.sub(existing, entry)
|
|
3153
|
+
else
|
|
3154
|
+
contents.sub(%r{(\n(\s*)</application>)}) { "\n#{$2} #{entry}#{$1}" }
|
|
3155
|
+
end
|
|
3156
|
+
File.write(manifest, contents)
|
|
3157
|
+
end
|
|
3158
|
+
|
|
3320
3159
|
def skip_project_asset_directory?(relative)
|
|
3321
3160
|
excluded_directories = %w[
|
|
3322
3161
|
.git
|
|
@@ -3518,13 +3357,13 @@ module Ruflet
|
|
|
3518
3357
|
queue << local
|
|
3519
3358
|
end
|
|
3520
3359
|
end
|
|
3521
|
-
base =
|
|
3360
|
+
base = file_uri_for(File.expand_path(config_path))
|
|
3522
3361
|
expected.each do |name, root|
|
|
3523
3362
|
entry = resolved.find { |package| package["name"] == name }
|
|
3524
3363
|
raise "missing local package #{name}" unless entry
|
|
3525
3364
|
|
|
3526
3365
|
uri = URI.join(base, entry.fetch("rootUri"))
|
|
3527
|
-
actual =
|
|
3366
|
+
actual = path_from_file_uri(uri)
|
|
3528
3367
|
unless uri.scheme == "file" && File.realpath(actual) == File.realpath(root)
|
|
3529
3368
|
raise "#{name} must resolve to the bundled Ruflet source at #{root}, got #{uri}"
|
|
3530
3369
|
end
|
|
@@ -3536,6 +3375,21 @@ module Ruflet
|
|
|
3536
3375
|
false
|
|
3537
3376
|
end
|
|
3538
3377
|
|
|
3378
|
+
# A POSIX path already starts with "/", so "file://" + path gives the
|
|
3379
|
+
# three slashes a file URI needs. A Windows path starts with the drive
|
|
3380
|
+
# ("D:/..."), and without the extra slash "D:" is parsed as the URI's
|
|
3381
|
+
# host -- which is why this read every bundled package as missing there.
|
|
3382
|
+
def file_uri_for(path)
|
|
3383
|
+
escaped = URI::DEFAULT_PARSER.escape(path.tr("\\", "/"))
|
|
3384
|
+
escaped.start_with?("/") ? "file://#{escaped}" : "file:///#{escaped}"
|
|
3385
|
+
end
|
|
3386
|
+
|
|
3387
|
+
# And back: URI#path keeps that leading slash, so a Windows path returns
|
|
3388
|
+
# as "/D:/..." which no file API accepts.
|
|
3389
|
+
def path_from_file_uri(uri)
|
|
3390
|
+
URI::DEFAULT_PARSER.unescape(uri.path).sub(%r{\A/(?=[A-Za-z]:)}, "")
|
|
3391
|
+
end
|
|
3392
|
+
|
|
3539
3393
|
def template_client_pubspec_dependencies
|
|
3540
3394
|
template_root =
|
|
3541
3395
|
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
@@ -53,7 +53,6 @@ module Ruflet
|
|
|
53
53
|
File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
|
|
54
54
|
write_default_ruflet_config(root, File.basename(root))
|
|
55
55
|
copy_default_project_assets(root)
|
|
56
|
-
copy_default_apple_extensions(root)
|
|
57
56
|
project_name = File.basename(root)
|
|
58
57
|
puts "Run:"
|
|
59
58
|
puts " cd #{project_name}"
|
|
@@ -368,21 +367,6 @@ module Ruflet
|
|
|
368
367
|
end
|
|
369
368
|
end
|
|
370
369
|
|
|
371
|
-
def copy_default_apple_extensions(root)
|
|
372
|
-
template_root = resolve_ruflet_client_template_root || ensure_cached_ruflet_client_template!
|
|
373
|
-
source = template_root && File.join(template_root, "apple_extensions")
|
|
374
|
-
return unless source && File.file?(File.join(source, "Package.swift"))
|
|
375
|
-
|
|
376
|
-
destination = File.join(root, "apple_extensions")
|
|
377
|
-
FileUtils.mkdir_p(destination)
|
|
378
|
-
generated_entries = %w[.build .swiftpm .dart_tool .claude]
|
|
379
|
-
Dir.children(source).each do |entry|
|
|
380
|
-
next if generated_entries.include?(entry)
|
|
381
|
-
|
|
382
|
-
FileUtils.cp_r(File.join(source, entry), File.join(destination, entry), preserve: true)
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
370
|
def default_project_assets_root
|
|
387
371
|
[
|
|
388
372
|
File.expand_path("../../../assets", __dir__),
|
|
@@ -24,8 +24,6 @@ module Ruflet
|
|
|
24
24
|
LEGACY_ASSET_PREFIX = "ruflet_client"
|
|
25
25
|
CLIENT_CHANNEL_MANIFEST = "#{ASSET_PREFIX}-manifest.json"
|
|
26
26
|
LEGACY_CLIENT_CHANNEL_MANIFEST = "#{LEGACY_ASSET_PREFIX}-manifest.json"
|
|
27
|
-
EXPERIMENTAL_IOS_SIMULATOR_ASSET = "#{ASSET_PREFIX}-ios-experimental-simulator.zip"
|
|
28
|
-
EXPERIMENTAL_MACOS_ASSET = "#{ASSET_PREFIX}-macos-experimental-universal.zip"
|
|
29
27
|
DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
|
|
30
28
|
|
|
31
29
|
def command_run(args)
|
|
@@ -39,9 +37,6 @@ module Ruflet
|
|
|
39
37
|
return 1
|
|
40
38
|
end
|
|
41
39
|
|
|
42
|
-
experimental_client = prepare_experimental_run_client(options)
|
|
43
|
-
return 1 if options[:experimental] && !experimental_client
|
|
44
|
-
|
|
45
40
|
selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
|
|
46
41
|
return 1 unless selected_port
|
|
47
42
|
env = {
|
|
@@ -75,10 +70,7 @@ module Ruflet
|
|
|
75
70
|
|
|
76
71
|
run_state = { child_pid: Process.spawn(env, *cmd, pgroup: true), restart: false }
|
|
77
72
|
reload_input_thread = options[:reload] ? start_reload_input_thread(run_state) : nil
|
|
78
|
-
launched_client_pids = launch_target_client(
|
|
79
|
-
options[:target], selected_port,
|
|
80
|
-
experimental_client: experimental_client
|
|
81
|
-
)
|
|
73
|
+
launched_client_pids = launch_target_client(options[:target], selected_port)
|
|
82
74
|
forward_signal = lambda do |signal|
|
|
83
75
|
begin
|
|
84
76
|
Process.kill(signal, -run_state[:child_pid])
|
|
@@ -134,71 +126,17 @@ module Ruflet
|
|
|
134
126
|
private
|
|
135
127
|
|
|
136
128
|
def parse_run_options(args)
|
|
137
|
-
options = { target: "mobile", requested_port: 8550, reload: true
|
|
129
|
+
options = { target: "mobile", requested_port: 8550, reload: true }
|
|
138
130
|
parser = OptionParser.new do |o|
|
|
139
131
|
o.on("--web") { options[:target] = "web" }
|
|
140
132
|
o.on("--desktop") { options[:target] = "desktop" }
|
|
141
133
|
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
142
134
|
o.on("--no-reload") { options[:reload] = false }
|
|
143
|
-
o.on("--experimental", "--exp") { options[:experimental] = true }
|
|
144
135
|
end
|
|
145
136
|
parser.parse!(args)
|
|
146
137
|
options
|
|
147
138
|
end
|
|
148
139
|
|
|
149
|
-
def prepare_experimental_run_client(options)
|
|
150
|
-
return nil unless options[:experimental]
|
|
151
|
-
|
|
152
|
-
if options[:target] == "web"
|
|
153
|
-
warn "run config error: --experimental/--exp is supported only for iOS and macOS"
|
|
154
|
-
return nil
|
|
155
|
-
end
|
|
156
|
-
unless host_platform_name == "macos"
|
|
157
|
-
warn "run config error: the experimental Apple client requires macOS"
|
|
158
|
-
return nil
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
if options[:target] == "desktop"
|
|
162
|
-
root = ensure_prebuilt_client(desktop_experimental: true, platform: "macos")
|
|
163
|
-
unless root
|
|
164
|
-
warn "Experimental macOS client is unavailable."
|
|
165
|
-
warn "The release must contain #{EXPERIMENTAL_MACOS_ASSET}."
|
|
166
|
-
return nil
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
return { kind: :desktop, root: root }
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
root = ensure_prebuilt_client(ios_experimental: true, platform: "macos")
|
|
173
|
-
unless root
|
|
174
|
-
warn "Experimental iOS Simulator client is unavailable."
|
|
175
|
-
warn "The release must contain #{EXPERIMENTAL_IOS_SIMULATOR_ASSET}."
|
|
176
|
-
return nil
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
app_bundle = experimental_ios_app_bundle(root)
|
|
180
|
-
simulator = booted_ios_simulator
|
|
181
|
-
unless simulator
|
|
182
|
-
warn "No booted iOS Simulator was found."
|
|
183
|
-
warn "Start the simulator you want to use, then run the command again."
|
|
184
|
-
return nil
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
bundle_identifier = ios_bundle_identifier(app_bundle)
|
|
188
|
-
if bundle_identifier.to_s.empty?
|
|
189
|
-
warn "Experimental client has no CFBundleIdentifier: #{app_bundle}"
|
|
190
|
-
return nil
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
{
|
|
194
|
-
kind: :ios_simulator,
|
|
195
|
-
app_bundle: app_bundle,
|
|
196
|
-
bundle_identifier: bundle_identifier,
|
|
197
|
-
simulator_udid: simulator.fetch("udid"),
|
|
198
|
-
simulator_name: simulator.fetch("name", simulator.fetch("udid"))
|
|
199
|
-
}
|
|
200
|
-
end
|
|
201
|
-
|
|
202
140
|
def build_runtime_command(script_path, gemfile_path:, env:, reload: false)
|
|
203
141
|
entry_script = script_path
|
|
204
142
|
if reload
|
|
@@ -361,20 +299,9 @@ module Ruflet
|
|
|
361
299
|
end
|
|
362
300
|
end
|
|
363
301
|
|
|
364
|
-
def launch_target_client(target, port
|
|
302
|
+
def launch_target_client(target, port)
|
|
365
303
|
wait_for_server_boot(port)
|
|
366
304
|
|
|
367
|
-
if experimental_client
|
|
368
|
-
if experimental_client[:kind] == :desktop
|
|
369
|
-
return launch_desktop_client(
|
|
370
|
-
"http://localhost:#{port}",
|
|
371
|
-
root: experimental_client.fetch(:root), experimental: true)
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
return launch_experimental_mobile_client(
|
|
375
|
-
"http://#{best_lan_host}:#{port}", client: experimental_client)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
305
|
case target
|
|
379
306
|
when "web"
|
|
380
307
|
launch_web_client(port)
|
|
@@ -385,33 +312,6 @@ module Ruflet
|
|
|
385
312
|
end
|
|
386
313
|
end
|
|
387
314
|
|
|
388
|
-
def launch_experimental_mobile_client(url, client:)
|
|
389
|
-
udid = client.fetch(:simulator_udid)
|
|
390
|
-
app_bundle = client.fetch(:app_bundle)
|
|
391
|
-
bundle_identifier = client.fetch(:bundle_identifier)
|
|
392
|
-
|
|
393
|
-
unless system("xcrun", "simctl", "install", udid, app_bundle)
|
|
394
|
-
warn "Failed to install the experimental client on #{client[:simulator_name] || udid}."
|
|
395
|
-
return []
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
launched = system(
|
|
399
|
-
{ "SIMCTL_CHILD_RUFLET_URL" => url },
|
|
400
|
-
"xcrun", "simctl", "launch", "--terminate-running-process", udid, bundle_identifier
|
|
401
|
-
)
|
|
402
|
-
unless launched
|
|
403
|
-
warn "Failed to launch the experimental client on #{client[:simulator_name] || udid}."
|
|
404
|
-
return []
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
puts "Ruflet experimental client: #{client[:simulator_name] || udid}"
|
|
408
|
-
puts "Ruflet experimental backend: #{url}"
|
|
409
|
-
[]
|
|
410
|
-
rescue StandardError => e
|
|
411
|
-
warn "Failed to launch experimental client: #{e.class}: #{e.message}"
|
|
412
|
-
[]
|
|
413
|
-
end
|
|
414
|
-
|
|
415
315
|
# The backend serves the web client on its own port, so the client reads
|
|
416
316
|
# the origin it was loaded from and opens its websocket there. No URL is
|
|
417
317
|
# passed in the query string: nothing in the client parses one.
|
|
@@ -511,8 +411,8 @@ module Ruflet
|
|
|
511
411
|
nil
|
|
512
412
|
end
|
|
513
413
|
|
|
514
|
-
def launch_desktop_client(url, root: nil
|
|
515
|
-
cmd = detect_desktop_client_command(url, root: root
|
|
414
|
+
def launch_desktop_client(url, root: nil)
|
|
415
|
+
cmd = detect_desktop_client_command(url, root: root)
|
|
516
416
|
unless cmd
|
|
517
417
|
warn "Desktop client executable not found."
|
|
518
418
|
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
@@ -568,7 +468,7 @@ module Ruflet
|
|
|
568
468
|
.find { |path| executable_file?(path) }
|
|
569
469
|
end
|
|
570
470
|
|
|
571
|
-
def detect_desktop_client_command(url, root: nil
|
|
471
|
+
def detect_desktop_client_command(url, root: nil)
|
|
572
472
|
unless root
|
|
573
473
|
root = ENV["RUFLET_CLIENT_DIR"]
|
|
574
474
|
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
@@ -581,7 +481,7 @@ module Ruflet
|
|
|
581
481
|
client_build_roots(root).each do |base|
|
|
582
482
|
if host_os.match?(/darwin/i)
|
|
583
483
|
search = %w[Release Debug].map { |config| File.join(base, "build", "macos", "Build", "Products", config) }
|
|
584
|
-
search << File.join(base,
|
|
484
|
+
search << File.join(base, "desktop")
|
|
585
485
|
search.each do |dir|
|
|
586
486
|
next unless Dir.exist?(dir)
|
|
587
487
|
|
|
@@ -636,10 +536,7 @@ module Ruflet
|
|
|
636
536
|
nil
|
|
637
537
|
end
|
|
638
538
|
|
|
639
|
-
def ensure_prebuilt_client(
|
|
640
|
-
web: false, desktop: false, desktop_experimental: false,
|
|
641
|
-
ios_experimental: false, platform: nil, force: false
|
|
642
|
-
)
|
|
539
|
+
def ensure_prebuilt_client(web: false, desktop: false, platform: nil, force: false)
|
|
643
540
|
platform ||= host_platform_name
|
|
644
541
|
return nil if platform.nil?
|
|
645
542
|
|
|
@@ -653,31 +550,8 @@ module Ruflet
|
|
|
653
550
|
return nil if desktop_asset.nil?
|
|
654
551
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
655
552
|
end
|
|
656
|
-
if desktop_experimental
|
|
657
|
-
return nil unless platform == "macos"
|
|
658
|
-
|
|
659
|
-
wanted_assets << {
|
|
660
|
-
kind: :desktop_experimental,
|
|
661
|
-
name: EXPERIMENTAL_MACOS_ASSET,
|
|
662
|
-
platform: platform
|
|
663
|
-
}
|
|
664
|
-
end
|
|
665
|
-
if ios_experimental
|
|
666
|
-
return nil unless platform == "macos"
|
|
667
|
-
|
|
668
|
-
wanted_assets << {
|
|
669
|
-
kind: :ios_experimental,
|
|
670
|
-
name: EXPERIMENTAL_IOS_SIMULATOR_ASSET,
|
|
671
|
-
platform: platform
|
|
672
|
-
}
|
|
673
|
-
end
|
|
674
553
|
cache_ready = wanted_assets.empty? || prebuilt_assets_present?(
|
|
675
|
-
cache_root,
|
|
676
|
-
web: web,
|
|
677
|
-
desktop: desktop,
|
|
678
|
-
desktop_experimental: desktop_experimental,
|
|
679
|
-
ios_experimental: ios_experimental,
|
|
680
|
-
platform: platform
|
|
554
|
+
cache_root, web: web, desktop: desktop, platform: platform
|
|
681
555
|
)
|
|
682
556
|
release = nil
|
|
683
557
|
if !force && cache_ready
|
|
@@ -718,8 +592,6 @@ module Ruflet
|
|
|
718
592
|
subdir = case wanted[:kind]
|
|
719
593
|
when :web then "web"
|
|
720
594
|
when :desktop then "desktop"
|
|
721
|
-
when :desktop_experimental then "desktop-experimental"
|
|
722
|
-
when :ios_experimental then "ios-experimental"
|
|
723
595
|
end
|
|
724
596
|
target = File.join(cache_root, subdir)
|
|
725
597
|
FileUtils.rm_rf(target) if force && Dir.exist?(target)
|
|
@@ -739,12 +611,7 @@ module Ruflet
|
|
|
739
611
|
end
|
|
740
612
|
|
|
741
613
|
if prebuilt_assets_present?(
|
|
742
|
-
cache_root,
|
|
743
|
-
web: web,
|
|
744
|
-
desktop: desktop,
|
|
745
|
-
desktop_experimental: desktop_experimental,
|
|
746
|
-
ios_experimental: ios_experimental,
|
|
747
|
-
platform: platform
|
|
614
|
+
cache_root, web: web, desktop: desktop, platform: platform
|
|
748
615
|
)
|
|
749
616
|
write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
|
|
750
617
|
return cache_root
|
|
@@ -761,61 +628,10 @@ module Ruflet
|
|
|
761
628
|
# will actually accept later, or a half-extracted cache reports success
|
|
762
629
|
# here and then fails to launch -- and, because a present cache short
|
|
763
630
|
# circuits the download, never repairs itself without --force.
|
|
764
|
-
def prebuilt_assets_present?(
|
|
765
|
-
root, web:, desktop:, desktop_experimental: false,
|
|
766
|
-
ios_experimental: false, platform: nil
|
|
767
|
-
)
|
|
631
|
+
def prebuilt_assets_present?(root, web:, desktop:, platform: nil)
|
|
768
632
|
ok_web = !web || built_web_client_dir?(File.join(root, "web"))
|
|
769
633
|
ok_desktop = !desktop || prebuilt_desktop_present?(root, platform: platform)
|
|
770
|
-
|
|
771
|
-
prebuilt_experimental_desktop_present?(root, platform: platform)
|
|
772
|
-
ok_ios_experimental = !ios_experimental || !experimental_ios_app_bundle(root).nil?
|
|
773
|
-
ok_web && ok_desktop && ok_desktop_experimental && ok_ios_experimental
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
def experimental_ios_app_bundle(root)
|
|
777
|
-
ios_root = File.join(root, "ios-experimental")
|
|
778
|
-
return nil unless Dir.exist?(ios_root)
|
|
779
|
-
|
|
780
|
-
Dir.glob(File.join(ios_root, "*.app")).sort.find do |app_bundle|
|
|
781
|
-
File.file?(File.join(app_bundle, "Info.plist")) && ios_app_executable(app_bundle)
|
|
782
|
-
end
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
def ios_app_executable(app_bundle)
|
|
786
|
-
return nil unless Dir.exist?(app_bundle)
|
|
787
|
-
|
|
788
|
-
Dir.children(app_bundle)
|
|
789
|
-
.map { |entry| File.join(app_bundle, entry) }
|
|
790
|
-
.find { |path| executable_file?(path) }
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
def booted_ios_simulator
|
|
794
|
-
output, status = Open3.capture2e("xcrun", "simctl", "list", "devices", "booted", "--json")
|
|
795
|
-
return nil unless status.success?
|
|
796
|
-
|
|
797
|
-
devices = JSON.parse(output).fetch("devices", {})
|
|
798
|
-
devices.each do |runtime, candidates|
|
|
799
|
-
next unless runtime.to_s.include?("SimRuntime.iOS")
|
|
800
|
-
|
|
801
|
-
match = Array(candidates).find do |device|
|
|
802
|
-
device["state"] == "Booted" && device.fetch("isAvailable", true)
|
|
803
|
-
end
|
|
804
|
-
return match if match
|
|
805
|
-
end
|
|
806
|
-
nil
|
|
807
|
-
rescue StandardError
|
|
808
|
-
nil
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
def ios_bundle_identifier(app_bundle)
|
|
812
|
-
plist = File.join(app_bundle, "Info.plist")
|
|
813
|
-
output, status = Open3.capture2e(
|
|
814
|
-
"/usr/bin/plutil", "-extract", "CFBundleIdentifier", "raw", "-o", "-", plist
|
|
815
|
-
)
|
|
816
|
-
status.success? ? output.strip : nil
|
|
817
|
-
rescue StandardError
|
|
818
|
-
nil
|
|
634
|
+
ok_web && ok_desktop
|
|
819
635
|
end
|
|
820
636
|
|
|
821
637
|
# The prebuilt client is whatever `ruflet build` produced from Ruflet
|
|
@@ -841,17 +657,6 @@ module Ruflet
|
|
|
841
657
|
end
|
|
842
658
|
end
|
|
843
659
|
|
|
844
|
-
def prebuilt_experimental_desktop_present?(root, platform: nil)
|
|
845
|
-
return false unless (platform || host_platform_name) == "macos"
|
|
846
|
-
|
|
847
|
-
desktop = File.join(root, "desktop-experimental")
|
|
848
|
-
return false unless Dir.exist?(desktop)
|
|
849
|
-
|
|
850
|
-
Dir.glob(File.join(desktop, "*.app")).any? do |app_bundle|
|
|
851
|
-
macos_app_executable(app_bundle)
|
|
852
|
-
end
|
|
853
|
-
end
|
|
854
|
-
|
|
855
660
|
def host_platform_name
|
|
856
661
|
host_os = RbConfig::CONFIG["host_os"]
|
|
857
662
|
return "macos" if host_os.match?(/darwin/i)
|
|
@@ -887,9 +692,7 @@ module Ruflet
|
|
|
887
692
|
end
|
|
888
693
|
|
|
889
694
|
def client_release_channel(wanted_assets: [])
|
|
890
|
-
default_channel =
|
|
891
|
-
%i[desktop_experimental ios_experimental].include?(wanted[:kind])
|
|
892
|
-
end ? "prebuild-experimental" : "prebuild-main"
|
|
695
|
+
default_channel = "prebuild-main"
|
|
893
696
|
value = ENV.fetch("RUFLET_CLIENT_CHANNEL", default_channel).to_s.strip
|
|
894
697
|
value.empty? ? default_channel : value
|
|
895
698
|
end
|
|
@@ -982,16 +785,11 @@ module Ruflet
|
|
|
982
785
|
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
983
786
|
end
|
|
984
787
|
|
|
985
|
-
if kind == :ios_experimental
|
|
986
|
-
return n.include?("ios") && n.include?("experimental") && n.include?("simulator") && n.end_with?(".zip")
|
|
987
|
-
end
|
|
988
|
-
|
|
989
|
-
if kind == :desktop_experimental
|
|
990
|
-
return n.include?("macos") && n.include?("experimental") && n.end_with?(".zip")
|
|
991
|
-
end
|
|
992
|
-
|
|
993
788
|
case platform
|
|
994
789
|
when "macos"
|
|
790
|
+
# Releases cut before the experimental channel was retired still carry
|
|
791
|
+
# ruflet_explorer-macos-experimental-universal.zip. Installing that as
|
|
792
|
+
# the desktop client would hand back the wrong app, so skip it.
|
|
995
793
|
n.include?("macos") && !n.include?("experimental") && n.end_with?(".zip")
|
|
996
794
|
when "linux"
|
|
997
795
|
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
@@ -1072,12 +870,6 @@ module Ruflet
|
|
|
1072
870
|
if prebuilt_desktop_present?(root, platform: platform)
|
|
1073
871
|
assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
|
|
1074
872
|
end
|
|
1075
|
-
if prebuilt_experimental_desktop_present?(root, platform: platform)
|
|
1076
|
-
assets << { "kind" => "desktop_experimental", "platform" => platform, "asset_name" => nil }
|
|
1077
|
-
end
|
|
1078
|
-
if experimental_ios_app_bundle(root)
|
|
1079
|
-
assets << { "kind" => "ios_experimental", "platform" => platform, "asset_name" => nil }
|
|
1080
|
-
end
|
|
1081
873
|
return if assets.empty?
|
|
1082
874
|
|
|
1083
875
|
write_client_manifest(root, platform: platform, release: nil, assets: assets)
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -66,10 +66,10 @@ module Ruflet
|
|
|
66
66
|
ruflet --version
|
|
67
67
|
ruflet create <appname>
|
|
68
68
|
ruflet new <appname>
|
|
69
|
-
ruflet run [scriptname|path] [--web|--desktop] [--
|
|
69
|
+
ruflet run [scriptname|path] [--web|--desktop] [--port PORT] [--no-reload]
|
|
70
70
|
ruflet update [web|desktop|all] [--check] [--force] [--platform PLATFORM]
|
|
71
71
|
ruflet debug [scriptname|path]
|
|
72
|
-
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self] [--
|
|
72
|
+
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self] [--verbose]
|
|
73
73
|
ruflet install [--device DEVICE_ID] [--verbose]
|
|
74
74
|
ruflet devices
|
|
75
75
|
ruflet emulators
|
data/lib/ruflet/version.rb
CHANGED