ruflet 0.0.15 → 0.0.16
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 +40 -1
- data/lib/ruflet/cli/android_sdk.rb +3 -2
- data/lib/ruflet/cli/build_command.rb +180 -9
- data/lib/ruflet/cli/environment_setup.rb +5 -2
- data/lib/ruflet/cli/extra_command.rb +1 -2
- data/lib/ruflet/cli/flutter_sdk.rb +71 -7
- data/lib/ruflet/cli/new_command.rb +1 -0
- data/lib/ruflet/cli/run_command.rb +106 -184
- data/lib/ruflet/cli.rb +7 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 653ba520f85466110b49be97b22029f8f1e30629edefcd3057def6ecbf20be2c
|
|
4
|
+
data.tar.gz: 66107e193cfd961763921dafcec2d67f21216f34daad33a22eeffbef279b1b0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73608863d7814a25d421f4a1b26bf07a231673b50679e3815eeb7e22df39c0da3c92bbdc696c207a36af38b427413d1d2ed6d61ba36d2720e8a0f444bb3ac3a7
|
|
7
|
+
data.tar.gz: 9a25b923371b68c7a8f51dbab32a30a71c0034033760c89cdf1773ffb9295707728107072f88523c9a5f7db4b43c5ded66d74096d52fa0547cd35103c99066b1
|
data/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
1
|
# ruflet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ruflet` is the command-line package for creating, running, diagnosing, and
|
|
4
|
+
building Ruflet applications.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
gem install ruflet
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Create a project and run it:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
ruflet new my_app
|
|
16
|
+
cd my_app
|
|
17
|
+
bundle install
|
|
18
|
+
bundle exec ruflet run --web
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Generated projects include `ruflet_core` and `ruflet_server` as application
|
|
22
|
+
dependencies. Use `bundle exec ruflet` inside a project so commands run with
|
|
23
|
+
the versions declared in its `Gemfile`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ruflet new <appname>
|
|
29
|
+
ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
|
|
30
|
+
ruflet debug [scriptname|path]
|
|
31
|
+
ruflet devices
|
|
32
|
+
ruflet emulators
|
|
33
|
+
ruflet doctor
|
|
34
|
+
ruflet update [web|desktop|all] [--check] [--force]
|
|
35
|
+
ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self]
|
|
36
|
+
ruflet install [--device DEVICE_ID]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run `ruflet install` without `--device` to choose from a numbered list of
|
|
40
|
+
connected devices. Pass `--device DEVICE_ID` to skip the prompt.
|
|
41
|
+
|
|
42
|
+
Run `ruflet help <command>` for all options.
|
|
@@ -246,8 +246,9 @@ module Ruflet
|
|
|
246
246
|
|
|
247
247
|
puts " Installing Android packages: #{missing.join(', ')}"
|
|
248
248
|
env = sdkmanager_env(java)
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
# sdkmanager downloads sizeable platform/build-tool archives; stream so
|
|
250
|
+
# the install reports progress instead of appearing to hang.
|
|
251
|
+
system(env, sdkmanager, "--sdk_root=#{sdk_root}", *missing, out: $stdout, err: $stderr)
|
|
251
252
|
end
|
|
252
253
|
|
|
253
254
|
def android_package_installed?(sdk_root, package)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "find"
|
|
5
5
|
require "json"
|
|
6
|
+
require "open3"
|
|
6
7
|
require "pathname"
|
|
7
8
|
require "rbconfig"
|
|
8
9
|
require "uri"
|
|
@@ -28,7 +29,9 @@ module Ruflet
|
|
|
28
29
|
"lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
|
|
29
30
|
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
30
31
|
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
32
|
+
"rive" => { package: "flet_rive", alias: "ruflet_rive" },
|
|
31
33
|
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
34
|
+
"spinkit" => { package: "flet_spinkit", alias: "ruflet_spinkit" },
|
|
32
35
|
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
33
36
|
"webview" => { package: "flet_webview", alias: "ruflet_webview" }
|
|
34
37
|
}.freeze
|
|
@@ -169,6 +172,10 @@ module Ruflet
|
|
|
169
172
|
backend_url = configured_backend_url(config)
|
|
170
173
|
if self_contained
|
|
171
174
|
build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"] if backend_url
|
|
175
|
+
# Pin the embedded entry project so the runtime doesn't have to guess.
|
|
176
|
+
# Apps that bundle nested projects (e.g. ruflet_studio's standalone_apps,
|
|
177
|
+
# each with their own main.rb) otherwise trip auto-discovery.
|
|
178
|
+
build_args += ["--dart-define", "RUFLET_EMBEDDED_PROJECT=#{self_contained_project_name}"]
|
|
172
179
|
else
|
|
173
180
|
unless backend_url
|
|
174
181
|
warn "build config error: backend_url is required for server-driven builds"
|
|
@@ -204,8 +211,16 @@ module Ruflet
|
|
|
204
211
|
end
|
|
205
212
|
|
|
206
213
|
tools = ensure_flutter!("install", client_dir: client_dir)
|
|
207
|
-
|
|
214
|
+
discovery_env = unbundled_command_env(tools[:env])
|
|
215
|
+
device_id ||= select_install_device(
|
|
216
|
+
flutter: tools[:flutter],
|
|
217
|
+
env: discovery_env,
|
|
218
|
+
client_dir: client_dir
|
|
219
|
+
)
|
|
220
|
+
return 1 unless device_id
|
|
221
|
+
|
|
208
222
|
install_platform = install_platform_for_device(device_id)
|
|
223
|
+
command_env = install_tool_env(tools[:env], client_dir, platform: install_platform)
|
|
209
224
|
unless sync_built_outputs_for_install(client_dir, platform: install_platform, verbose: !!verbose)
|
|
210
225
|
warn "Could not find built app outputs under ./build"
|
|
211
226
|
warn "Run `ruflet build ...` first, then `ruflet install`."
|
|
@@ -243,6 +258,59 @@ module Ruflet
|
|
|
243
258
|
nil
|
|
244
259
|
end
|
|
245
260
|
|
|
261
|
+
def select_install_device(flutter:, env:, client_dir:, input: $stdin, output: $stdout)
|
|
262
|
+
devices = discover_install_devices(flutter: flutter, env: env, client_dir: client_dir)
|
|
263
|
+
if devices.empty?
|
|
264
|
+
warn "No supported devices are connected."
|
|
265
|
+
warn "Run `ruflet devices` to check device availability."
|
|
266
|
+
return nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
output.puts "Available devices:"
|
|
270
|
+
devices.each_with_index do |device, index|
|
|
271
|
+
details = [device["targetPlatform"], device["emulator"] ? "emulator" : "physical"].compact
|
|
272
|
+
output.puts " #{index + 1}) #{device.fetch("name", device["id"])} (#{details.join(", ")}) [#{device["id"]}]"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
loop do
|
|
276
|
+
output.print "Choose a device [1-#{devices.length}]: "
|
|
277
|
+
output.flush
|
|
278
|
+
choice = input.gets
|
|
279
|
+
unless choice
|
|
280
|
+
warn "Device selection cancelled. Use `--device DEVICE_ID` for noninteractive installs."
|
|
281
|
+
return nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
index = Integer(choice.strip, exception: false)
|
|
285
|
+
return devices[index - 1]["id"] if index && index.between?(1, devices.length)
|
|
286
|
+
|
|
287
|
+
output.puts "Enter a number from 1 to #{devices.length}."
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def discover_install_devices(flutter:, env:, client_dir:)
|
|
292
|
+
stdout, stderr, status = Open3.capture3(
|
|
293
|
+
env,
|
|
294
|
+
flutter,
|
|
295
|
+
"devices",
|
|
296
|
+
"--machine",
|
|
297
|
+
chdir: client_dir
|
|
298
|
+
)
|
|
299
|
+
unless status.success?
|
|
300
|
+
warn "Could not list Flutter devices."
|
|
301
|
+
warn stderr.strip unless stderr.to_s.strip.empty?
|
|
302
|
+
return []
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
devices = JSON.parse(stdout)
|
|
306
|
+
Array(devices).select do |device|
|
|
307
|
+
device.is_a?(Hash) && !device["id"].to_s.empty? && device["isSupported"] != false
|
|
308
|
+
end
|
|
309
|
+
rescue JSON::ParserError => e
|
|
310
|
+
warn "Could not parse Flutter device list: #{e.message}"
|
|
311
|
+
[]
|
|
312
|
+
end
|
|
313
|
+
|
|
246
314
|
def ensure_flutter_client_dir(verbose: false)
|
|
247
315
|
client_dir = detect_flutter_client_dir
|
|
248
316
|
return client_dir if client_dir
|
|
@@ -264,8 +332,9 @@ module Ruflet
|
|
|
264
332
|
apple_env
|
|
265
333
|
end
|
|
266
334
|
|
|
267
|
-
def install_tool_env(env, client_dir)
|
|
268
|
-
|
|
335
|
+
def install_tool_env(env, client_dir, platform: nil)
|
|
336
|
+
platform ||= inferred_install_platform
|
|
337
|
+
return build_tool_env(env, platform, client_dir) if platform
|
|
269
338
|
|
|
270
339
|
command_env = unbundled_command_env(env)
|
|
271
340
|
command_env["PATH"] = apple_build_path(command_env["PATH"])
|
|
@@ -998,6 +1067,23 @@ module Ruflet
|
|
|
998
1067
|
cmake_path = File.join(client_dir, "linux", "CMakeLists.txt")
|
|
999
1068
|
replace_in_file(cmake_path, /^set\(BINARY_NAME ".*"\)$/, %(set(BINARY_NAME "#{metadata[:package_name]}")))
|
|
1000
1069
|
replace_in_file(cmake_path, /^set\(APPLICATION_ID ".*"\)$/, %(set(APPLICATION_ID "#{metadata[:linux_application_id]}")))
|
|
1070
|
+
|
|
1071
|
+
# The GTK runner hardcodes the window title (header-bar and fallback
|
|
1072
|
+
# paths). Without this it shows the package name (e.g. "ruflet_client")
|
|
1073
|
+
# instead of the configured app name. Match the call, not the literal,
|
|
1074
|
+
# so re-runs stay idempotent.
|
|
1075
|
+
my_application_path = File.join(client_dir, "linux", "runner", "my_application.cc")
|
|
1076
|
+
title = c_string_escape(metadata[:display_name])
|
|
1077
|
+
replace_in_file(
|
|
1078
|
+
my_application_path,
|
|
1079
|
+
/gtk_header_bar_set_title\(header_bar, "[^"]*"\)/,
|
|
1080
|
+
%(gtk_header_bar_set_title(header_bar, "#{title}"))
|
|
1081
|
+
)
|
|
1082
|
+
replace_in_file(
|
|
1083
|
+
my_application_path,
|
|
1084
|
+
/gtk_window_set_title\(window, "[^"]*"\)/,
|
|
1085
|
+
%(gtk_window_set_title(window, "#{title}"))
|
|
1086
|
+
)
|
|
1001
1087
|
end
|
|
1002
1088
|
|
|
1003
1089
|
def apply_dart_metadata(client_dir, metadata)
|
|
@@ -1090,6 +1176,13 @@ module Ruflet
|
|
|
1090
1176
|
value.to_s.gsub('"', '""')
|
|
1091
1177
|
end
|
|
1092
1178
|
|
|
1179
|
+
# Escape a value for embedding inside a C string literal (used for the
|
|
1180
|
+
# GTK runner window title in my_application.cc). Block form avoids
|
|
1181
|
+
# backslash interpretation in the gsub replacement string.
|
|
1182
|
+
def c_string_escape(value)
|
|
1183
|
+
value.to_s.gsub(/[\\"]/) { |ch| "\\#{ch}" }
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1093
1186
|
def dart_single_quote_escape(value)
|
|
1094
1187
|
value.to_s.gsub("\\", "\\\\\\").gsub("'", "\\\\'")
|
|
1095
1188
|
end
|
|
@@ -1107,6 +1200,7 @@ module Ruflet
|
|
|
1107
1200
|
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
1108
1201
|
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
1109
1202
|
|
|
1203
|
+
sync_client_flet_packages(client_dir, extension_packages)
|
|
1110
1204
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
1111
1205
|
if File.file?(pubspec_path)
|
|
1112
1206
|
sync_client_extension_dependencies(pubspec_path, extension_packages)
|
|
@@ -1293,8 +1387,23 @@ module Ruflet
|
|
|
1293
1387
|
return if content.include?("<key>#{key}</key>")
|
|
1294
1388
|
|
|
1295
1389
|
entry = "\t<key>#{key}</key>\n\t#{value_xml}\n"
|
|
1296
|
-
|
|
1297
|
-
|
|
1390
|
+
|
|
1391
|
+
# Insert into the ROOT <dict> (opened right after <plist ...>), never the
|
|
1392
|
+
# first nested </dict>. Modern Flutter Info.plists nest a dict inside
|
|
1393
|
+
# UIApplicationSceneManifest, so subbing the first </dict> would bury
|
|
1394
|
+
# top-level keys (e.g. NS*UsageDescription) where iOS can't read them —
|
|
1395
|
+
# which crashes the app the moment a permission is requested.
|
|
1396
|
+
root_open = content.match(%r{<plist\b[^>]*>\s*<dict>}m)
|
|
1397
|
+
updated =
|
|
1398
|
+
if root_open
|
|
1399
|
+
insert_at = root_open.end(0)
|
|
1400
|
+
content[0...insert_at] + "\n#{entry}" + content[insert_at..]
|
|
1401
|
+
elsif (last = content.rindex("</dict>"))
|
|
1402
|
+
content[0...last] + entry + content[last..]
|
|
1403
|
+
else
|
|
1404
|
+
"#{content}\n#{entry}"
|
|
1405
|
+
end
|
|
1406
|
+
File.write(path, updated)
|
|
1298
1407
|
end
|
|
1299
1408
|
|
|
1300
1409
|
def remove_plist_entry(path, key)
|
|
@@ -1358,18 +1467,20 @@ module Ruflet
|
|
|
1358
1467
|
flutter = data["flutter"]
|
|
1359
1468
|
flutter = data["flutter"] = {} unless flutter.is_a?(Hash)
|
|
1360
1469
|
assets = Array(flutter["assets"]).map(&:to_s)
|
|
1470
|
+
project_asset_prefix = "assets/#{self_contained_project_name}/"
|
|
1471
|
+
assets.reject! { |asset| asset.start_with?(project_asset_prefix) }
|
|
1361
1472
|
|
|
1362
1473
|
if self_contained
|
|
1363
1474
|
dependencies["ruby_runtime"] = ruby_runtime_dependency(dependencies["ruby_runtime"])
|
|
1364
1475
|
assets.delete("assets/main.rb")
|
|
1365
1476
|
assets.delete("assets/ruby_project/")
|
|
1366
|
-
|
|
1367
|
-
|
|
1477
|
+
project_asset_relative_paths.each do |relative_path|
|
|
1478
|
+
assets << "#{project_asset_prefix}#{relative_path}"
|
|
1479
|
+
end
|
|
1368
1480
|
else
|
|
1369
1481
|
dependencies.delete("ruby_runtime")
|
|
1370
1482
|
assets.delete("assets/main.rb")
|
|
1371
1483
|
assets.delete("assets/ruby_project/")
|
|
1372
|
-
assets.delete("assets/#{self_contained_project_name}/")
|
|
1373
1484
|
end
|
|
1374
1485
|
|
|
1375
1486
|
flutter["assets"] = assets unless assets.empty?
|
|
@@ -1377,7 +1488,7 @@ module Ruflet
|
|
|
1377
1488
|
write_pubspec_yaml(pubspec_path, data)
|
|
1378
1489
|
end
|
|
1379
1490
|
|
|
1380
|
-
RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.
|
|
1491
|
+
RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.6"
|
|
1381
1492
|
|
|
1382
1493
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1383
1494
|
local_path = explicit_local_ruby_runtime_path || repo_checkout_ruby_runtime_path
|
|
@@ -1747,6 +1858,50 @@ module Ruflet
|
|
|
1747
1858
|
write_pubspec_yaml(path, data)
|
|
1748
1859
|
end
|
|
1749
1860
|
|
|
1861
|
+
def sync_client_flet_packages(client_dir, selected_packages)
|
|
1862
|
+
template_root =
|
|
1863
|
+
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
1864
|
+
Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
1865
|
+
end
|
|
1866
|
+
return unless template_root
|
|
1867
|
+
|
|
1868
|
+
source_root = File.join(template_root, "flet_packages")
|
|
1869
|
+
target_root = File.join(client_dir, "flet_packages")
|
|
1870
|
+
return unless Dir.exist?(source_root)
|
|
1871
|
+
return if File.expand_path(source_root) == File.expand_path(target_root)
|
|
1872
|
+
|
|
1873
|
+
# The repository's standalone client intentionally carries the full
|
|
1874
|
+
# local package catalog. Only generated/template-derived clients are
|
|
1875
|
+
# conditioned for a particular Ruflet application.
|
|
1876
|
+
return if standalone_ruflet_client_source?(client_dir, template_root)
|
|
1877
|
+
|
|
1878
|
+
known_packages = CLIENT_EXTENSION_MAP.values.map { |meta| meta.fetch(:package) }.uniq
|
|
1879
|
+
required_packages = (["flet"] + selected_packages).uniq
|
|
1880
|
+
|
|
1881
|
+
FileUtils.mkdir_p(target_root)
|
|
1882
|
+
required_packages.each do |package_name|
|
|
1883
|
+
source = File.join(source_root, package_name)
|
|
1884
|
+
target = File.join(target_root, package_name)
|
|
1885
|
+
next unless Dir.exist?(source)
|
|
1886
|
+
next if Dir.exist?(target)
|
|
1887
|
+
|
|
1888
|
+
FileUtils.cp_r(source, target)
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
(known_packages - selected_packages).each do |package_name|
|
|
1892
|
+
FileUtils.rm_rf(File.join(target_root, package_name))
|
|
1893
|
+
end
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
def standalone_ruflet_client_source?(client_dir, template_root)
|
|
1897
|
+
client_root = File.expand_path(client_dir)
|
|
1898
|
+
candidates = [
|
|
1899
|
+
File.expand_path("../../ruflet_client", template_root),
|
|
1900
|
+
File.expand_path("../../../../../ruflet_client", __dir__)
|
|
1901
|
+
]
|
|
1902
|
+
candidates.any? { |candidate| File.expand_path(candidate) == client_root }
|
|
1903
|
+
end
|
|
1904
|
+
|
|
1750
1905
|
def sync_client_extension_dependencies(path, selected_packages)
|
|
1751
1906
|
return if selected_packages.empty?
|
|
1752
1907
|
|
|
@@ -1755,14 +1910,30 @@ module Ruflet
|
|
|
1755
1910
|
|
|
1756
1911
|
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
1757
1912
|
deps = (data["dependencies"] || {}).dup
|
|
1913
|
+
if selected_packages.include?("flet_rive")
|
|
1914
|
+
deps.delete("rive")
|
|
1915
|
+
deps.delete("rive_native")
|
|
1916
|
+
end
|
|
1758
1917
|
selected_packages.each do |package_name|
|
|
1759
1918
|
deps[package_name] = template_deps[package_name] if template_deps.key?(package_name)
|
|
1760
1919
|
end
|
|
1761
1920
|
|
|
1762
1921
|
data["dependencies"] = deps
|
|
1922
|
+
sync_local_flet_dependency_override(data, deps["flet"])
|
|
1763
1923
|
write_pubspec_yaml(path, data)
|
|
1764
1924
|
end
|
|
1765
1925
|
|
|
1926
|
+
# Git-backed Flet extensions declare their own Flet source. When Ruflet
|
|
1927
|
+
# vendors the core engine locally, force every extension to resolve that
|
|
1928
|
+
# same copy instead of letting Pub reject the mixed sources.
|
|
1929
|
+
def sync_local_flet_dependency_override(data, flet_dependency)
|
|
1930
|
+
return unless flet_dependency.is_a?(Hash) && key_defined?(flet_dependency, "path")
|
|
1931
|
+
|
|
1932
|
+
overrides = data["dependency_overrides"]
|
|
1933
|
+
overrides = data["dependency_overrides"] = {} unless overrides.is_a?(Hash)
|
|
1934
|
+
overrides["flet"] = flet_dependency
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1766
1937
|
def template_client_pubspec_dependencies
|
|
1767
1938
|
template_root =
|
|
1768
1939
|
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
@@ -212,8 +212,11 @@ module Ruflet
|
|
|
212
212
|
def run_privileged_command(*command, verbose: false)
|
|
213
213
|
full = sudo_prefix + command
|
|
214
214
|
puts " $ #{full.join(' ')}"
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
# Package installs (apt/dnf/pacman...) routinely download hundreds of
|
|
216
|
+
# megabytes and take minutes. Always stream their output so the user
|
|
217
|
+
# can see progress instead of a frozen-looking prompt; `verbose` is
|
|
218
|
+
# left for callers that want to add their own detail elsewhere.
|
|
219
|
+
system(*full, out: $stdout, err: $stderr)
|
|
217
220
|
end
|
|
218
221
|
|
|
219
222
|
def sudo_prefix
|
|
@@ -56,8 +56,7 @@ module Ruflet
|
|
|
56
56
|
puts " Flutter: #{flutter_version_summary(tools)}"
|
|
57
57
|
environment_issues += android_environment_setup!(fix: !!fix, verbose: !!verbose)
|
|
58
58
|
ok = system(android_build_env(tools[:env]), tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
|
|
59
|
-
status =
|
|
60
|
-
status ||= ok ? 0 : 1
|
|
59
|
+
status = ok ? 0 : ($?&.exitstatus || 1)
|
|
61
60
|
if environment_issues.any?
|
|
62
61
|
warn "Unresolved environment issues: #{environment_issues.join('; ')}"
|
|
63
62
|
status = 1 if status.zero?
|
|
@@ -84,8 +84,12 @@ module Ruflet
|
|
|
84
84
|
File.write(fvmrc_path, "{\"flutter\":\"#{version}\"}\n")
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
# Installing a Flutter SDK through FVM downloads/extracts hundreds of
|
|
88
|
+
# megabytes. Stream FVM's output so the run does not look frozen.
|
|
89
|
+
puts " $ fvm install #{version}"
|
|
90
|
+
system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: $stdout, err: $stderr)
|
|
91
|
+
puts " $ fvm use --force #{version}"
|
|
92
|
+
system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: $stdout, err: $stderr)
|
|
89
93
|
|
|
90
94
|
flutter = flutter_bin_path(project_dir)
|
|
91
95
|
return nil unless File.executable?(flutter)
|
|
@@ -107,7 +111,8 @@ module Ruflet
|
|
|
107
111
|
end
|
|
108
112
|
return nil unless dart && File.executable?(dart)
|
|
109
113
|
|
|
110
|
-
|
|
114
|
+
puts " $ dart pub global activate fvm"
|
|
115
|
+
system(dart, "pub", "global", "activate", "fvm", out: $stdout, err: $stderr)
|
|
111
116
|
installed_fvm = File.join(pub_cache_bin_dir, fvm_executable_name)
|
|
112
117
|
return installed_fvm if File.executable?(installed_fvm)
|
|
113
118
|
|
|
@@ -148,10 +153,13 @@ module Ruflet
|
|
|
148
153
|
return sdk_root if File.executable?(flutter_bin)
|
|
149
154
|
|
|
150
155
|
FileUtils.mkdir_p(install_root)
|
|
156
|
+
puts " Installing Flutter #{release.fetch('version')} (#{host}) into #{install_root}"
|
|
151
157
|
Dir.mktmpdir("ruflet-flutter-sdk-") do |tmpdir|
|
|
152
158
|
archive_path = File.join(tmpdir, File.basename(archive))
|
|
153
|
-
download_file("#{RELEASES_BASE}/#{archive}", archive_path)
|
|
159
|
+
download_file("#{RELEASES_BASE}/#{archive}", archive_path, label: "Flutter SDK")
|
|
160
|
+
puts " Extracting #{File.basename(archive)} (this can take a minute)"
|
|
154
161
|
extract_archive(archive_path, install_root)
|
|
162
|
+
puts " Extracted Flutter SDK"
|
|
155
163
|
end
|
|
156
164
|
|
|
157
165
|
return sdk_root if File.executable?(flutter_bin)
|
|
@@ -366,7 +374,7 @@ module Ruflet
|
|
|
366
374
|
nil
|
|
367
375
|
end
|
|
368
376
|
|
|
369
|
-
def download_file(url, destination, limit: 5)
|
|
377
|
+
def download_file(url, destination, limit: 5, label: nil)
|
|
370
378
|
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
371
379
|
|
|
372
380
|
uri = URI(url)
|
|
@@ -376,10 +384,21 @@ module Ruflet
|
|
|
376
384
|
http.request(req) do |res|
|
|
377
385
|
case res
|
|
378
386
|
when Net::HTTPSuccess
|
|
379
|
-
|
|
387
|
+
name = label || File.basename(destination)
|
|
388
|
+
total = res["content-length"].to_i
|
|
389
|
+
downloaded = 0
|
|
390
|
+
marker = -1
|
|
391
|
+
File.open(destination, "wb") do |f|
|
|
392
|
+
res.read_body do |chunk|
|
|
393
|
+
f.write(chunk)
|
|
394
|
+
downloaded += chunk.bytesize
|
|
395
|
+
marker = report_download_progress(name, downloaded, total, marker)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
finish_download_progress(name, downloaded, total)
|
|
380
399
|
return destination
|
|
381
400
|
when Net::HTTPRedirection
|
|
382
|
-
return download_file(res["location"], destination, limit: limit - 1)
|
|
401
|
+
return download_file(res["location"], destination, limit: limit - 1, label: label)
|
|
383
402
|
else
|
|
384
403
|
raise "Download failed (#{res.code})"
|
|
385
404
|
end
|
|
@@ -389,6 +408,51 @@ module Ruflet
|
|
|
389
408
|
curl_download(url, destination)
|
|
390
409
|
end
|
|
391
410
|
|
|
411
|
+
# Net::HTTP gives no progress for a multi-hundred-MB SDK download, which
|
|
412
|
+
# looks like a hang. Report percentage on a TTY (rewritten in place) and
|
|
413
|
+
# at coarse steps when piped, so logs stay readable either way.
|
|
414
|
+
def report_download_progress(name, downloaded, total, marker)
|
|
415
|
+
if total.positive?
|
|
416
|
+
percent = downloaded * 100 / total
|
|
417
|
+
return marker if percent <= marker
|
|
418
|
+
|
|
419
|
+
line = " Downloading #{name}: #{percent}% (#{human_size(downloaded)} / #{human_size(total)})"
|
|
420
|
+
if $stdout.tty?
|
|
421
|
+
$stdout.print("\r#{line}")
|
|
422
|
+
elsif (percent % 10).zero?
|
|
423
|
+
puts line
|
|
424
|
+
end
|
|
425
|
+
percent
|
|
426
|
+
else
|
|
427
|
+
step = downloaded / (5 * 1024 * 1024)
|
|
428
|
+
return marker if step <= marker
|
|
429
|
+
|
|
430
|
+
line = " Downloading #{name}: #{human_size(downloaded)}"
|
|
431
|
+
$stdout.tty? ? $stdout.print("\r#{line}") : puts(line)
|
|
432
|
+
step
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def finish_download_progress(name, downloaded, total)
|
|
437
|
+
summary = total.positive? ? "#{human_size(downloaded)} / #{human_size(total)}" : human_size(downloaded)
|
|
438
|
+
if $stdout.tty?
|
|
439
|
+
$stdout.print("\r Downloaded #{name}: #{summary}\n")
|
|
440
|
+
else
|
|
441
|
+
puts " Downloaded #{name}: #{summary}"
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def human_size(bytes)
|
|
446
|
+
units = %w[B KB MB GB TB]
|
|
447
|
+
size = bytes.to_f
|
|
448
|
+
unit = units.shift
|
|
449
|
+
while size >= 1024 && units.any?
|
|
450
|
+
size /= 1024
|
|
451
|
+
unit = units.shift
|
|
452
|
+
end
|
|
453
|
+
format("%.1f %s", size, unit)
|
|
454
|
+
end
|
|
455
|
+
|
|
392
456
|
def extract_archive(archive, destination)
|
|
393
457
|
if archive.end_with?(".zip")
|
|
394
458
|
if windows_host?
|
|
@@ -25,6 +25,7 @@ module Ruflet
|
|
|
25
25
|
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
26
26
|
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
27
27
|
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
28
|
+
"spinkit" => { package: "flet_spinkit", alias: "ruflet_spinkit" },
|
|
28
29
|
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
29
30
|
"webview" => { package: "flet_webview", alias: "ruflet_webview" }
|
|
30
31
|
}.freeze
|
|
@@ -17,12 +17,18 @@ require "yaml"
|
|
|
17
17
|
module Ruflet
|
|
18
18
|
module CLI
|
|
19
19
|
module RunCommand
|
|
20
|
+
DEFAULT_BACKEND_PORTS = {
|
|
21
|
+
"web" => 8550,
|
|
22
|
+
"desktop" => 8560,
|
|
23
|
+
"mobile" => 8570
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
20
26
|
def command_run(args)
|
|
21
|
-
options = { target: "mobile", requested_port:
|
|
27
|
+
options = { target: "mobile", requested_port: nil }
|
|
22
28
|
parser = OptionParser.new do |o|
|
|
23
29
|
o.on("--web") { options[:target] = "web" }
|
|
24
30
|
o.on("--desktop") { options[:target] = "desktop" }
|
|
25
|
-
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
31
|
+
o.on("--port PORT", Integer, "Backend port (defaults: web 8550, desktop 8560, mobile 8570)") { |v| options[:requested_port] = v }
|
|
26
32
|
end
|
|
27
33
|
parser.parse!(args)
|
|
28
34
|
|
|
@@ -34,18 +40,33 @@ module Ruflet
|
|
|
34
40
|
return 1
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
requested_port = options[:requested_port] || default_backend_port(options[:target])
|
|
44
|
+
selected_port = resolve_backend_port(options[:target], requested_port: requested_port)
|
|
38
45
|
return 1 unless selected_port
|
|
39
46
|
env = {
|
|
40
47
|
"RUFLET_TARGET" => options[:target],
|
|
41
48
|
"RUFLET_SUPPRESS_SERVER_BANNER" => "1",
|
|
42
|
-
"RUFLET_PORT" => selected_port.to_s
|
|
49
|
+
"RUFLET_PORT" => selected_port.to_s,
|
|
50
|
+
"RUFLET_STRICT_PORT" => "1"
|
|
43
51
|
}
|
|
44
52
|
apply_local_ruflet_dev_overrides(env)
|
|
45
53
|
assets_dir = File.join(File.dirname(script_path), "assets")
|
|
46
54
|
env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
# Web: the Ruby backend serves the Flutter web client itself (same
|
|
57
|
+
# origin/port as /ws), so no separate static server or proxy is needed.
|
|
58
|
+
if options[:target] == "web"
|
|
59
|
+
web_dir = detect_web_client_dir
|
|
60
|
+
if web_dir
|
|
61
|
+
env["RUFLET_WEB_CLIENT_DIR"] = web_dir
|
|
62
|
+
else
|
|
63
|
+
warn "Ruflet web client unavailable for version #{ruflet_version}."
|
|
64
|
+
warn "Install the matching GitHub release client before running --web."
|
|
65
|
+
return 1
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
print_run_banner(target: options[:target], requested_port: requested_port, port: selected_port)
|
|
49
70
|
print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
|
|
50
71
|
|
|
51
72
|
gemfile_path = find_nearest_gemfile(Dir.pwd)
|
|
@@ -150,16 +171,14 @@ module Ruflet
|
|
|
150
171
|
|
|
151
172
|
def print_run_banner(target:, requested_port:, port:)
|
|
152
173
|
if port != requested_port.to_i
|
|
153
|
-
puts "
|
|
174
|
+
puts "Port #{requested_port} is busy; using #{port} for #{target}."
|
|
154
175
|
end
|
|
155
176
|
if target == "desktop"
|
|
156
177
|
puts "Ruflet desktop URL: http://localhost:#{port}"
|
|
157
178
|
elsif target == "mobile"
|
|
158
179
|
puts "Ruflet target: #{target}"
|
|
159
|
-
else
|
|
160
|
-
puts "Ruflet target: #{target}"
|
|
161
|
-
puts "Ruflet URL: http://localhost:#{port}"
|
|
162
180
|
end
|
|
181
|
+
# web: launch_web_client prints the single app URL after the server boots
|
|
163
182
|
end
|
|
164
183
|
|
|
165
184
|
def launch_target_client(target, port)
|
|
@@ -175,30 +194,19 @@ module Ruflet
|
|
|
175
194
|
end
|
|
176
195
|
end
|
|
177
196
|
|
|
197
|
+
# The Ruby backend (Ruflet::Server) serves the Flutter web client on its
|
|
198
|
+
# own port, so the client loads and opens its websocket on that same
|
|
199
|
+
# origin. The prebuilt web client derives its backend from Uri.base, so
|
|
200
|
+
# the public URL stays clean and dynamic port selection works naturally.
|
|
178
201
|
def launch_web_client(port)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
web_port = find_available_port(port + 1)
|
|
186
|
-
web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL)
|
|
187
|
-
Process.detach(web_pid)
|
|
188
|
-
wait_for_server_boot(web_port)
|
|
189
|
-
backend_url = "http://localhost:#{port}"
|
|
190
|
-
web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
|
|
191
|
-
browser_pid = open_in_browser_app_mode(web_url)
|
|
192
|
-
open_in_browser(web_url) if browser_pid.nil?
|
|
193
|
-
puts "Ruflet web client: #{web_url}"
|
|
194
|
-
puts "Ruflet backend ws: ws://localhost:#{port}/ws"
|
|
195
|
-
[web_pid, browser_pid].compact
|
|
196
|
-
rescue Errno::ENOENT
|
|
197
|
-
warn "python3 is required to host web client locally."
|
|
198
|
-
warn "Install Python 3 and rerun."
|
|
199
|
-
[]
|
|
202
|
+
url = "http://localhost:#{port}/"
|
|
203
|
+
browser_pid = open_in_browser_app_mode(url)
|
|
204
|
+
open_in_browser(url) if browser_pid.nil?
|
|
205
|
+
puts "Ruflet web app: #{url}"
|
|
206
|
+
[browser_pid].compact
|
|
200
207
|
rescue StandardError => e
|
|
201
|
-
warn "Failed to
|
|
208
|
+
warn "Failed to open web client: #{e.class}: #{e.message}"
|
|
209
|
+
warn "Open it manually: http://localhost:#{port}/"
|
|
202
210
|
[]
|
|
203
211
|
end
|
|
204
212
|
|
|
@@ -286,7 +294,7 @@ module Ruflet
|
|
|
286
294
|
end
|
|
287
295
|
|
|
288
296
|
def launch_desktop_client(url)
|
|
289
|
-
cmd =
|
|
297
|
+
cmd = detect_desktop_client_command(url)
|
|
290
298
|
unless cmd
|
|
291
299
|
warn "Desktop client executable not found."
|
|
292
300
|
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
@@ -307,136 +315,37 @@ module Ruflet
|
|
|
307
315
|
[]
|
|
308
316
|
end
|
|
309
317
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
return nil unless respond_to?(:ensure_flutter_client_dir, true)
|
|
313
|
-
return nil unless respond_to?(:prepare_flutter_client, true)
|
|
314
|
-
return nil unless respond_to?(:ensure_flutter!, true)
|
|
315
|
-
|
|
316
|
-
platform = host_platform_name
|
|
317
|
-
return nil unless %w[macos windows linux].include?(platform)
|
|
318
|
-
|
|
319
|
-
ensure_ruflet_build_assets(verbose: false) if respond_to?(:ensure_ruflet_build_assets, true)
|
|
320
|
-
client_dir = ensure_flutter_client_dir(verbose: false)
|
|
321
|
-
return nil unless client_dir
|
|
322
|
-
|
|
323
|
-
config = project_run_config
|
|
324
|
-
tools = ensure_flutter!("run", client_dir: client_dir)
|
|
325
|
-
env = build_tool_env(tools[:env], platform, client_dir)
|
|
326
|
-
return nil unless prepare_flutter_client(
|
|
327
|
-
client_dir,
|
|
328
|
-
platform: platform,
|
|
329
|
-
tools: tools,
|
|
330
|
-
config: config,
|
|
331
|
-
self_contained: false,
|
|
332
|
-
verbose: false
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
[
|
|
336
|
-
env,
|
|
337
|
-
tools[:flutter],
|
|
338
|
-
"run",
|
|
339
|
-
"-d",
|
|
340
|
-
platform,
|
|
341
|
-
"--target",
|
|
342
|
-
"lib/main.server.dart",
|
|
343
|
-
"--dart-define",
|
|
344
|
-
"RUFLET_BACKEND_URL=#{url}"
|
|
345
|
-
]
|
|
346
|
-
rescue StandardError => e
|
|
347
|
-
warn "Project desktop client setup failed: #{e.class}: #{e.message}"
|
|
348
|
-
nil
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def project_run_requires_managed_client?
|
|
352
|
-
extensions = Array(project_run_config["extensions"]).map { |value| normalize_run_extension_key(value) }.compact
|
|
353
|
-
protected_extensions =
|
|
354
|
-
if defined?(Ruflet::CLI::BuildCommand::SERVICE_EXTENSION_MAP)
|
|
355
|
-
Ruflet::CLI::BuildCommand::SERVICE_EXTENSION_MAP.values.flatten
|
|
356
|
-
else
|
|
357
|
-
[]
|
|
358
|
-
end
|
|
359
|
-
return true if (extensions - protected_extensions).any?
|
|
360
|
-
|
|
361
|
-
services =
|
|
362
|
-
if respond_to?(:load_service_definitions, true)
|
|
363
|
-
send(:load_service_definitions).keys
|
|
364
|
-
else
|
|
365
|
-
[]
|
|
366
|
-
end
|
|
367
|
-
return false if services.empty?
|
|
368
|
-
|
|
369
|
-
known_services =
|
|
370
|
-
if defined?(Ruflet::CLI::BuildCommand::DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
|
|
371
|
-
Ruflet::CLI::BuildCommand::DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys
|
|
372
|
-
else
|
|
373
|
-
[]
|
|
374
|
-
end
|
|
375
|
-
(services & known_services).any?
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def project_run_config
|
|
379
|
-
config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
|
|
380
|
-
return {} unless File.file?(config_path)
|
|
381
|
-
|
|
382
|
-
YAML.safe_load(File.read(config_path), aliases: true) || {}
|
|
383
|
-
rescue StandardError
|
|
384
|
-
{}
|
|
385
|
-
end
|
|
318
|
+
def detect_desktop_client_command(url)
|
|
319
|
+
root = configured_client_root
|
|
386
320
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return nil if key.empty?
|
|
321
|
+
command = desktop_client_command_from_root(root, url)
|
|
322
|
+
return command if command
|
|
390
323
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
key.gsub!(/\Aservice_/, "")
|
|
394
|
-
key
|
|
324
|
+
cached_root = ensure_prebuilt_client(desktop: true)
|
|
325
|
+
desktop_client_command_from_root(cached_root, url)
|
|
395
326
|
end
|
|
396
327
|
|
|
397
|
-
def
|
|
398
|
-
root = ENV["RUFLET_CLIENT_DIR"]
|
|
399
|
-
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
400
|
-
root = nil unless Dir.exist?(root)
|
|
401
|
-
root ||= ensure_prebuilt_client(desktop: true)
|
|
328
|
+
def desktop_client_command_from_root(root, url)
|
|
402
329
|
return nil unless root && Dir.exist?(root)
|
|
403
330
|
|
|
404
331
|
host_os = RbConfig::CONFIG["host_os"]
|
|
405
332
|
if host_os.match?(/darwin/i)
|
|
406
|
-
app_path =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
File.join(root, "desktop", "ruflet_client.app")
|
|
410
|
-
].find do |candidate|
|
|
411
|
-
bin = File.join(candidate, "Contents", "MacOS", "ruflet_client")
|
|
412
|
-
File.file?(bin) && File.executable?(bin) && ensure_macos_file_picker_entitlement(candidate)
|
|
413
|
-
end
|
|
414
|
-
return [File.join(app_path, "Contents", "MacOS", "ruflet_client"), url] if app_path
|
|
333
|
+
app_path = File.join(root, "desktop", "ruflet_client.app")
|
|
334
|
+
bin = File.join(app_path, "Contents", "MacOS", "ruflet_client")
|
|
335
|
+
return [bin, url] if File.file?(bin) && File.executable?(bin) && ensure_macos_file_picker_entitlement(app_path)
|
|
415
336
|
elsif host_os.match?(/mswin|mingw|cygwin/i)
|
|
416
|
-
exe = File.join(root, "
|
|
417
|
-
prebuilt = File.join(root, "desktop", "ruflet_client.exe")
|
|
418
|
-
exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
|
|
337
|
+
exe = File.join(root, "desktop", "ruflet_client.exe")
|
|
419
338
|
return [exe, url] if File.file?(exe)
|
|
420
339
|
else
|
|
421
|
-
direct = File.join(root, "
|
|
422
|
-
prebuilt_direct = File.join(root, "desktop", "ruflet_client")
|
|
423
|
-
direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
|
|
340
|
+
direct = File.join(root, "desktop", "ruflet_client")
|
|
424
341
|
return [direct, url] if File.file?(direct)
|
|
425
|
-
bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
|
|
426
|
-
if Dir.exist?(bundle_dir)
|
|
427
|
-
candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
|
|
428
|
-
.find { |path| File.file?(path) && File.executable?(path) }
|
|
429
|
-
return [candidate, url] if candidate
|
|
430
|
-
end
|
|
431
342
|
end
|
|
432
343
|
|
|
433
344
|
nil
|
|
434
345
|
end
|
|
435
346
|
|
|
436
347
|
def detect_web_client_dir
|
|
437
|
-
root =
|
|
438
|
-
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
439
|
-
root = nil unless Dir.exist?(root)
|
|
348
|
+
root = configured_client_root
|
|
440
349
|
root ||= ensure_prebuilt_client(web: true)
|
|
441
350
|
return nil unless root && Dir.exist?(root)
|
|
442
351
|
|
|
@@ -448,6 +357,14 @@ module Ruflet
|
|
|
448
357
|
nil
|
|
449
358
|
end
|
|
450
359
|
|
|
360
|
+
def configured_client_root
|
|
361
|
+
root = ENV["RUFLET_CLIENT_DIR"].to_s.strip
|
|
362
|
+
return nil if root.empty?
|
|
363
|
+
|
|
364
|
+
expanded = File.expand_path(root)
|
|
365
|
+
Dir.exist?(expanded) ? expanded : nil
|
|
366
|
+
end
|
|
367
|
+
|
|
451
368
|
def ensure_prebuilt_client(web: false, desktop: false, platform: nil, force: false)
|
|
452
369
|
platform ||= host_platform_name
|
|
453
370
|
return nil if platform.nil?
|
|
@@ -462,10 +379,12 @@ module Ruflet
|
|
|
462
379
|
return nil if desktop_asset.nil?
|
|
463
380
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
464
381
|
end
|
|
465
|
-
|
|
466
|
-
|
|
382
|
+
cached = prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
|
|
383
|
+
compatible_cache = cached && compatible_client_cache?(cache_root, platform: platform)
|
|
384
|
+
if !force && (wanted_assets.empty? || compatible_cache)
|
|
467
385
|
return cache_root
|
|
468
386
|
end
|
|
387
|
+
force = true if cached && !compatible_cache
|
|
469
388
|
|
|
470
389
|
release = fetch_release_for_version
|
|
471
390
|
return nil unless release
|
|
@@ -561,11 +480,33 @@ module Ruflet
|
|
|
561
480
|
end
|
|
562
481
|
|
|
563
482
|
def fetch_release_for_version
|
|
564
|
-
|
|
565
|
-
release_by_tag(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
483
|
+
client_release_tags.each do |tag|
|
|
484
|
+
release = release_by_tag(tag)
|
|
485
|
+
return release if release
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
nil
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def client_release_tags
|
|
492
|
+
channel = ENV["RUFLET_CLIENT_CHANNEL"].to_s.strip
|
|
493
|
+
unless channel.empty?
|
|
494
|
+
raise ArgumentError, "Invalid RUFLET_CLIENT_CHANNEL" unless channel.match?(/\A[A-Za-z0-9._-]+\z/)
|
|
495
|
+
|
|
496
|
+
return [channel]
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
["v#{ruflet_version}", ruflet_version]
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def compatible_client_cache?(root, platform:)
|
|
503
|
+
manifest = read_client_manifest(root)
|
|
504
|
+
return false unless manifest
|
|
505
|
+
|
|
506
|
+
manifest["schema"] == 1 &&
|
|
507
|
+
manifest["ruflet_version"] == ruflet_version &&
|
|
508
|
+
manifest["platform"] == platform &&
|
|
509
|
+
client_release_tags.include?(manifest["release_tag"])
|
|
569
510
|
end
|
|
570
511
|
|
|
571
512
|
def ruflet_version
|
|
@@ -575,10 +516,6 @@ module Ruflet
|
|
|
575
516
|
Ruflet::VERSION
|
|
576
517
|
end
|
|
577
518
|
|
|
578
|
-
def release_latest
|
|
579
|
-
github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/latest")
|
|
580
|
-
end
|
|
581
|
-
|
|
582
519
|
def release_by_tag(tag)
|
|
583
520
|
github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/tags/#{tag}")
|
|
584
521
|
rescue StandardError
|
|
@@ -748,19 +685,6 @@ module Ruflet
|
|
|
748
685
|
nil
|
|
749
686
|
end
|
|
750
687
|
|
|
751
|
-
def ensure_client_manifest(root, platform:)
|
|
752
|
-
return if read_client_manifest(root)
|
|
753
|
-
|
|
754
|
-
assets = []
|
|
755
|
-
assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if File.file?(File.join(root, "web", "index.html"))
|
|
756
|
-
if prebuilt_desktop_present?(root, platform: platform)
|
|
757
|
-
assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
|
|
758
|
-
end
|
|
759
|
-
return if assets.empty?
|
|
760
|
-
|
|
761
|
-
write_client_manifest(root, platform: platform, release: nil, assets: assets)
|
|
762
|
-
end
|
|
763
|
-
|
|
764
688
|
def write_client_manifest(root, platform:, release:, assets:)
|
|
765
689
|
FileUtils.mkdir_p(root)
|
|
766
690
|
payload = {
|
|
@@ -793,26 +717,24 @@ module Ruflet
|
|
|
793
717
|
port = start_port.to_i
|
|
794
718
|
|
|
795
719
|
max_attempts.times do
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
rescue Errno::EACCES, Errno::EPERM
|
|
800
|
-
probe = TCPServer.new("127.0.0.1", port)
|
|
801
|
-
end
|
|
802
|
-
probe.close
|
|
803
|
-
return port
|
|
804
|
-
rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
|
|
805
|
-
port += 1
|
|
806
|
-
end
|
|
720
|
+
return port if port_available?(port)
|
|
721
|
+
|
|
722
|
+
port += 1
|
|
807
723
|
end
|
|
808
724
|
|
|
809
|
-
|
|
725
|
+
nil
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def default_backend_port(target)
|
|
729
|
+
DEFAULT_BACKEND_PORTS.fetch(target.to_s, DEFAULT_BACKEND_PORTS.fetch("mobile"))
|
|
810
730
|
end
|
|
811
731
|
|
|
812
|
-
def resolve_backend_port(
|
|
732
|
+
def resolve_backend_port(target, requested_port: nil)
|
|
813
733
|
base = requested_port.to_i
|
|
814
|
-
base =
|
|
815
|
-
find_available_port(base)
|
|
734
|
+
base = default_backend_port(target) if base <= 0
|
|
735
|
+
selected = find_available_port(base)
|
|
736
|
+
warn "No available Ruflet port found starting at #{base}." unless selected
|
|
737
|
+
selected
|
|
816
738
|
end
|
|
817
739
|
|
|
818
740
|
def port_available?(port)
|
|
@@ -824,7 +746,7 @@ module Ruflet
|
|
|
824
746
|
probe = TCPServer.new("127.0.0.1", port)
|
|
825
747
|
end
|
|
826
748
|
true
|
|
827
|
-
rescue Errno::EADDRINUSE
|
|
749
|
+
rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
|
|
828
750
|
false
|
|
829
751
|
ensure
|
|
830
752
|
probe&.close
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -6,6 +6,13 @@ require "optparse"
|
|
|
6
6
|
# depend on the caller's locale for reading them.
|
|
7
7
|
Encoding.default_external = Encoding::UTF_8 if Encoding.default_external == Encoding::US_ASCII
|
|
8
8
|
|
|
9
|
+
# Provisioning (downloads, package installs, SDK extraction) can run for
|
|
10
|
+
# minutes. When stdout is not a TTY (CI logs, pipes, editor consoles) Ruby
|
|
11
|
+
# buffers it, so progress notes would only appear once the whole run finishes.
|
|
12
|
+
# Unbuffer both streams so the pipeline reports progress as it happens.
|
|
13
|
+
$stdout.sync = true
|
|
14
|
+
$stderr.sync = true
|
|
15
|
+
|
|
9
16
|
require_relative "version"
|
|
10
17
|
require_relative "cli/templates"
|
|
11
18
|
require_relative "cli/new_command"
|
data/lib/ruflet/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -49,7 +49,8 @@ files:
|
|
|
49
49
|
- lib/ruflet/version.rb
|
|
50
50
|
- lib/ruflet_cli.rb
|
|
51
51
|
homepage: https://github.com/AdamMusa/Ruflet
|
|
52
|
-
licenses:
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
53
54
|
metadata: {}
|
|
54
55
|
rdoc_options: []
|
|
55
56
|
require_paths:
|