ruflet 0.0.20 → 0.0.21
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/lib/ruflet/cli/build_command.rb +220 -13
- data/lib/ruflet/cli/new_command.rb +8 -2
- data/lib/ruflet/cli/run_command.rb +21 -10
- data/lib/ruflet/cli/update_command.rb +6 -6
- 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: 4f32ccf43fe430d641b8f1b72f05a048d6c654a8ec6c4e2ac4c02b6752d67e47
|
|
4
|
+
data.tar.gz: ef7244a5cc788bf2dbfbcf74731e86d4dd1bd61d0a0fc104dffecdfeab444850
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6618e3cbab937475c14990529be3e96786f21c62de32ddbc0b5d6ca0fcfb2809c1c70084904948881f575d2ad3b73641f2bca30dac64d0f3402e4f57f1e0846
|
|
7
|
+
data.tar.gz: 751bf5286756eab0da77e4be3f2abcafcec4b335ea379fb02628f8ca5af7016530d4052c8e595ab385bef5001ea594d31eadfb9f3fd71cada7aa362c8194b97e
|
|
@@ -73,7 +73,7 @@ module Ruflet
|
|
|
73
73
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
74
74
|
platform = (args.shift || "").downcase
|
|
75
75
|
if platform.empty?
|
|
76
|
-
warn "Usage: ruflet build <apk|android|ios|
|
|
76
|
+
warn "Usage: ruflet build <apk|android|aab|ios|ipa|web|macos|windows|linux> [--self] [--verbose]"
|
|
77
77
|
return 1
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -83,6 +83,11 @@ module Ruflet
|
|
|
83
83
|
return 1
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# `ipa` produces the uploadable archive; every other step — pods,
|
|
87
|
+
# signing, icons, package name — is the same as a plain iOS build.
|
|
88
|
+
requested_platform = platform
|
|
89
|
+
platform = "ios" if platform == "ipa"
|
|
90
|
+
|
|
86
91
|
# The embedded Ruby VM is a native plugin with no browser
|
|
87
92
|
# implementation, so a self-contained web build produces an app that
|
|
88
93
|
# cannot start. Say so rather than shipping one that hangs.
|
|
@@ -115,7 +120,7 @@ module Ruflet
|
|
|
115
120
|
return 1 unless ok
|
|
116
121
|
|
|
117
122
|
build_args = [*flutter_cmd, *args]
|
|
118
|
-
build_args << "--codesign" if ios_device_build_needs_codesign_flag?(
|
|
123
|
+
build_args << "--codesign" if ios_device_build_needs_codesign_flag?(requested_platform, build_args)
|
|
119
124
|
target_entrypoint = flutter_target_entrypoint(client_dir, self_contained: !!self_contained)
|
|
120
125
|
build_args += ["--target", target_entrypoint] if target_entrypoint
|
|
121
126
|
backend_url = configured_backend_url(config)
|
|
@@ -437,6 +442,8 @@ module Ruflet
|
|
|
437
442
|
return false unless validate_mobile_app_identity(metadata, platform: platform)
|
|
438
443
|
|
|
439
444
|
apply_native_service_permissions(client_dir, config)
|
|
445
|
+
apply_android_signing_config(client_dir, platform, verbose: !!verbose)
|
|
446
|
+
apply_ios_signing_team(client_dir, config) if %w[ios ipa macos].include?(platform.to_s)
|
|
440
447
|
configured = configure_client_runtime_mode(client_dir, self_contained: self_contained, verbose: verbose)
|
|
441
448
|
return false if configured == false
|
|
442
449
|
@ruflet_self_contained_build = self_contained
|
|
@@ -740,8 +747,11 @@ module Ruflet
|
|
|
740
747
|
if File.file?(services_path)
|
|
741
748
|
service_config = YAML.safe_load(read_text_file(services_path), aliases: true) || {}
|
|
742
749
|
if service_config["app"].is_a?(Hash)
|
|
743
|
-
|
|
744
|
-
|
|
750
|
+
# ruflet.yaml declares the app; services.yaml may still carry an
|
|
751
|
+
# identity from older projects, so it fills gaps rather than
|
|
752
|
+
# overriding what the project states.
|
|
753
|
+
declared = config["app"].is_a?(Hash) ? config["app"] : {}
|
|
754
|
+
config["app"] = service_config["app"].merge(declared)
|
|
745
755
|
end
|
|
746
756
|
config["services"] = service_config["services"] if service_config.key?("services")
|
|
747
757
|
end
|
|
@@ -1200,10 +1210,10 @@ module Ruflet
|
|
|
1200
1210
|
first_present(app["bundle_identifier"], config["bundle_identifier"], "#{organization}.#{package_name}")
|
|
1201
1211
|
)
|
|
1202
1212
|
identity_errors = []
|
|
1203
|
-
|
|
1213
|
+
named = first_present(app["name"], app["app_name"], app["display_name"])
|
|
1214
|
+
identity_errors << "app.name" if named.nil?
|
|
1204
1215
|
identity_errors << "app.package_name" if app["package_name"].to_s.strip.empty?
|
|
1205
|
-
identity_errors << "app.organization" if app["organization"].
|
|
1206
|
-
identity_errors << "services.yaml app section" unless config["_app_identity_source"] == "services.yaml"
|
|
1216
|
+
identity_errors << "app.organization" if first_present(app["organization"], app["org"]).nil?
|
|
1207
1217
|
|
|
1208
1218
|
{
|
|
1209
1219
|
package_name: package_name,
|
|
@@ -1237,8 +1247,8 @@ module Ruflet
|
|
|
1237
1247
|
errors = Array(metadata[:mobile_identity_errors])
|
|
1238
1248
|
return true if errors.empty?
|
|
1239
1249
|
|
|
1240
|
-
warn "build config error:
|
|
1241
|
-
warn "
|
|
1250
|
+
warn "build config error: ruflet.yaml must define #{errors.join(', ')}"
|
|
1251
|
+
warn "A mobile build needs app.name, app.package_name and app.organization."
|
|
1242
1252
|
false
|
|
1243
1253
|
end
|
|
1244
1254
|
|
|
@@ -1271,6 +1281,33 @@ module Ruflet
|
|
|
1271
1281
|
)
|
|
1272
1282
|
end
|
|
1273
1283
|
|
|
1284
|
+
# The signing team belongs to whoever ships the app, so it comes from the
|
|
1285
|
+
# project rather than being baked into the client.
|
|
1286
|
+
def apply_ios_signing_team(client_dir, config)
|
|
1287
|
+
team = ios_signing_team(config)
|
|
1288
|
+
pbxproj_path = File.join(client_dir, "ios", "Runner.xcodeproj", "project.pbxproj")
|
|
1289
|
+
return unless File.file?(pbxproj_path)
|
|
1290
|
+
|
|
1291
|
+
if team.to_s.strip.empty?
|
|
1292
|
+
build_note("No ios.team_id configured; Xcode will pick the signing team")
|
|
1293
|
+
replace_in_file(pbxproj_path, /DEVELOPMENT_TEAM = [^;]*;/, "DEVELOPMENT_TEAM = \"\";")
|
|
1294
|
+
return
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
replace_in_file(pbxproj_path, /DEVELOPMENT_TEAM = [^;]*;/, "DEVELOPMENT_TEAM = #{team};")
|
|
1298
|
+
build_note("iOS signing team set to #{team}")
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def ios_signing_team(config)
|
|
1302
|
+
app = config["app"].is_a?(Hash) ? config["app"] : {}
|
|
1303
|
+
first_present(
|
|
1304
|
+
platform_build_config(config, "ios")["team_id"],
|
|
1305
|
+
app["ios_team_id"],
|
|
1306
|
+
app["team_id"],
|
|
1307
|
+
ENV["RUFLET_IOS_TEAM_ID"]
|
|
1308
|
+
)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1274
1311
|
def apply_ios_metadata(client_dir, metadata)
|
|
1275
1312
|
info_plist_path = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1276
1313
|
replace_plist_value(info_plist_path, "CFBundleDisplayName", metadata[:display_name])
|
|
@@ -1508,17 +1545,109 @@ module Ruflet
|
|
|
1508
1545
|
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
1509
1546
|
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
1510
1547
|
|
|
1548
|
+
external = external_extension_entries(config)
|
|
1549
|
+
|
|
1511
1550
|
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
1512
1551
|
if File.file?(pubspec_path)
|
|
1513
1552
|
sync_client_extension_dependencies(pubspec_path, extension_packages)
|
|
1514
1553
|
prune_client_pubspec(pubspec_path, extension_packages)
|
|
1554
|
+
sync_external_extension_dependencies(pubspec_path, external)
|
|
1515
1555
|
end
|
|
1516
1556
|
client_entrypoint_paths(client_dir).each do |entrypoint|
|
|
1517
|
-
|
|
1518
|
-
|
|
1557
|
+
next unless File.file?(entrypoint)
|
|
1558
|
+
|
|
1559
|
+
sync_client_main_extensions(entrypoint, extension_aliases)
|
|
1560
|
+
prune_client_main(entrypoint, extension_aliases)
|
|
1561
|
+
sync_external_extension_registrations(entrypoint, external)
|
|
1562
|
+
end
|
|
1563
|
+
end
|
|
1564
|
+
|
|
1565
|
+
# An extension may name a package the template does not bundle, declared
|
|
1566
|
+
# with the source to fetch it from:
|
|
1567
|
+
#
|
|
1568
|
+
# extensions:
|
|
1569
|
+
# - charts
|
|
1570
|
+
# - my_package:
|
|
1571
|
+
# git:
|
|
1572
|
+
# url: https://github.com/owner/my_package
|
|
1573
|
+
# ref: main
|
|
1574
|
+
#
|
|
1575
|
+
# `branch` is accepted for `ref`, and `path` for a local checkout.
|
|
1576
|
+
def external_extension_entries(config)
|
|
1577
|
+
Array(config["extensions"]).filter_map do |entry|
|
|
1578
|
+
next unless entry.is_a?(Hash)
|
|
1579
|
+
next unless entry.size == 1
|
|
1580
|
+
|
|
1581
|
+
name, source = entry.first
|
|
1582
|
+
package = name.to_s.strip
|
|
1583
|
+
next if package.empty?
|
|
1584
|
+
|
|
1585
|
+
dependency = external_extension_dependency(source)
|
|
1586
|
+
next unless dependency
|
|
1587
|
+
|
|
1588
|
+
{ name: package, dependency: dependency }
|
|
1519
1589
|
end
|
|
1520
1590
|
end
|
|
1521
1591
|
|
|
1592
|
+
def external_extension_dependency(source)
|
|
1593
|
+
return nil unless source.is_a?(Hash)
|
|
1594
|
+
|
|
1595
|
+
normalized = source.each_with_object({}) { |(key, value), out| out[key.to_s] = value }
|
|
1596
|
+
git = normalized["git"] || normalized["github"] || normalized["repository"]
|
|
1597
|
+
git = normalized if git.nil? && normalized.key?("url")
|
|
1598
|
+
|
|
1599
|
+
if git.is_a?(String)
|
|
1600
|
+
return { "git" => git }
|
|
1601
|
+
elsif git.is_a?(Hash)
|
|
1602
|
+
git = git.each_with_object({}) { |(key, value), out| out[key.to_s] = value }
|
|
1603
|
+
url = git["url"].to_s.strip
|
|
1604
|
+
return nil if url.empty?
|
|
1605
|
+
|
|
1606
|
+
spec = { "url" => url }
|
|
1607
|
+
ref = (git["ref"] || git["branch"] || git["tag"]).to_s.strip
|
|
1608
|
+
spec["ref"] = ref unless ref.empty?
|
|
1609
|
+
path = git["path"].to_s.strip
|
|
1610
|
+
spec["path"] = path unless path.empty?
|
|
1611
|
+
return { "git" => spec }
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
local = normalized["path"]
|
|
1615
|
+
return { "path" => local.to_s } if local.is_a?(String) && !local.to_s.strip.empty?
|
|
1616
|
+
|
|
1617
|
+
nil
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
def sync_external_extension_dependencies(pubspec_path, entries)
|
|
1621
|
+
return if entries.empty?
|
|
1622
|
+
|
|
1623
|
+
data = YAML.safe_load(read_text_file(pubspec_path), aliases: true) || {}
|
|
1624
|
+
dependencies = (data["dependencies"] || {}).dup
|
|
1625
|
+
entries.each { |entry| dependencies[entry[:name]] = entry[:dependency] }
|
|
1626
|
+
data["dependencies"] = dependencies
|
|
1627
|
+
write_pubspec_yaml(pubspec_path, data)
|
|
1628
|
+
build_note("Added #{entries.map { |e| e[:name] }.join(', ')} from the extension configuration")
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
# Flet extension packages expose an Extension class from a library named
|
|
1632
|
+
# after the package, so the import and registration can be derived.
|
|
1633
|
+
def sync_external_extension_registrations(path, entries)
|
|
1634
|
+
return if entries.empty?
|
|
1635
|
+
|
|
1636
|
+
content = read_text_file(path)
|
|
1637
|
+
original = content.dup
|
|
1638
|
+
|
|
1639
|
+
entries.each do |entry|
|
|
1640
|
+
name = entry[:name]
|
|
1641
|
+
import_line = %(import 'package:#{name}/#{name}.dart' as #{name};\n)
|
|
1642
|
+
extension_line = " #{name}.Extension(),\n"
|
|
1643
|
+
|
|
1644
|
+
content = insert_missing_import(content, import_line) unless content.include?("package:#{name}/#{name}.dart")
|
|
1645
|
+
content = insert_missing_extension(content, extension_line) unless content.match?(/^\s*#{Regexp.escape(name)}\.Extension\(\),/)
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
write_text_file(path, content) unless content == original
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1522
1651
|
def configured_service_entries(config)
|
|
1523
1652
|
Array(config["services"]).filter_map do |entry|
|
|
1524
1653
|
case entry
|
|
@@ -1536,6 +1665,79 @@ module Ruflet
|
|
|
1536
1665
|
end
|
|
1537
1666
|
end
|
|
1538
1667
|
|
|
1668
|
+
ANDROID_SIGNING_KEYS = {
|
|
1669
|
+
"storeFile" => "RUFLET_ANDROID_KEYSTORE",
|
|
1670
|
+
"storePassword" => "RUFLET_ANDROID_KEYSTORE_PASSWORD",
|
|
1671
|
+
"keyAlias" => "RUFLET_ANDROID_KEY_ALIAS",
|
|
1672
|
+
"keyPassword" => "RUFLET_ANDROID_KEY_PASSWORD"
|
|
1673
|
+
}.freeze
|
|
1674
|
+
|
|
1675
|
+
# Release signing is read from the project, never from the managed client,
|
|
1676
|
+
# so nobody has to edit build/client by hand. Either keep an
|
|
1677
|
+
# android/key.properties beside the app, or set the RUFLET_ANDROID_*
|
|
1678
|
+
# variables. Without one of those, Gradle falls back to the debug key.
|
|
1679
|
+
def apply_android_signing_config(client_dir, platform, verbose: false)
|
|
1680
|
+
return unless %w[apk android aab appbundle].include?(platform.to_s)
|
|
1681
|
+
|
|
1682
|
+
android_dir = File.join(client_dir, "android")
|
|
1683
|
+
return unless Dir.exist?(android_dir)
|
|
1684
|
+
|
|
1685
|
+
destination = File.join(android_dir, "key.properties")
|
|
1686
|
+
properties = android_signing_properties
|
|
1687
|
+
if properties.empty?
|
|
1688
|
+
File.delete(destination) if File.file?(destination)
|
|
1689
|
+
build_note("No Android signing configured; the release build will use the debug key")
|
|
1690
|
+
return
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
missing = ANDROID_SIGNING_KEYS.keys - properties.keys
|
|
1694
|
+
unless missing.empty?
|
|
1695
|
+
warn "build config error: Android signing is missing #{missing.join(', ')}"
|
|
1696
|
+
warn "Set them in android/key.properties or the matching RUFLET_ANDROID_* variables."
|
|
1697
|
+
return
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
body = ANDROID_SIGNING_KEYS.keys.map { |key| "#{key}=#{properties.fetch(key)}" }.join("\n")
|
|
1701
|
+
write_text_file(destination, "#{body}\n")
|
|
1702
|
+
build_log(verbose, "wrote #{destination}")
|
|
1703
|
+
build_note("Android release signing configured from #{properties.fetch("_source")}")
|
|
1704
|
+
end
|
|
1705
|
+
|
|
1706
|
+
def android_signing_properties
|
|
1707
|
+
from_env = ANDROID_SIGNING_KEYS.each_with_object({}) do |(key, variable), out|
|
|
1708
|
+
value = ENV[variable].to_s.strip
|
|
1709
|
+
out[key] = value unless value.empty?
|
|
1710
|
+
end
|
|
1711
|
+
unless from_env.empty?
|
|
1712
|
+
from_env["storeFile"] = File.expand_path(from_env["storeFile"]) if from_env["storeFile"]
|
|
1713
|
+
return from_env.merge("_source" => "the RUFLET_ANDROID_* environment")
|
|
1714
|
+
end
|
|
1715
|
+
|
|
1716
|
+
source = project_android_key_properties_path
|
|
1717
|
+
return {} unless source
|
|
1718
|
+
|
|
1719
|
+
parsed = read_text_file(source).each_line.with_object({}) do |line, out|
|
|
1720
|
+
next if line.strip.empty? || line.strip.start_with?("#")
|
|
1721
|
+
|
|
1722
|
+
key, value = line.split("=", 2)
|
|
1723
|
+
out[key.to_s.strip] = value.to_s.strip unless value.nil?
|
|
1724
|
+
end
|
|
1725
|
+
return {} if parsed.empty?
|
|
1726
|
+
|
|
1727
|
+
# storeFile is written relative to the project; the client sits deeper.
|
|
1728
|
+
if parsed["storeFile"] && !Pathname.new(parsed["storeFile"]).absolute?
|
|
1729
|
+
parsed["storeFile"] = File.expand_path(parsed["storeFile"], File.dirname(source))
|
|
1730
|
+
end
|
|
1731
|
+
parsed.merge("_source" => source)
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def project_android_key_properties_path
|
|
1735
|
+
[
|
|
1736
|
+
File.join(Dir.pwd, "android", "key.properties"),
|
|
1737
|
+
File.join(Dir.pwd, "key.properties")
|
|
1738
|
+
].find { |path| File.file?(path) }
|
|
1739
|
+
end
|
|
1740
|
+
|
|
1539
1741
|
def apply_native_service_permissions(client_dir, config)
|
|
1540
1742
|
entries = configured_service_entries(config)
|
|
1541
1743
|
configured_extensions = Array(config["extensions"]).filter_map { |entry| normalize_extension_key(entry) }
|
|
@@ -1708,7 +1910,10 @@ module Ruflet
|
|
|
1708
1910
|
"lib/connection_probe_io.dart",
|
|
1709
1911
|
"lib/connection_probe_stub.dart",
|
|
1710
1912
|
"ios/Podfile",
|
|
1711
|
-
"windows/CMakeLists.txt"
|
|
1913
|
+
"windows/CMakeLists.txt",
|
|
1914
|
+
# Release signing lives here; an existing client would otherwise keep
|
|
1915
|
+
# signing release builds with the debug key.
|
|
1916
|
+
"android/app/build.gradle.kts"
|
|
1712
1917
|
]
|
|
1713
1918
|
|
|
1714
1919
|
managed_files.each do |relative_path|
|
|
@@ -1848,7 +2053,7 @@ module Ruflet
|
|
|
1848
2053
|
# app, so signing keys and local environment files must never be copied
|
|
1849
2054
|
# in even when a project keeps them beside its source.
|
|
1850
2055
|
SECRET_ASSET_EXTENSIONS = %w[.p8 .p12 .pem .key .jks .keystore .mobileprovision].freeze
|
|
1851
|
-
SECRET_ASSET_BASENAMES = %w[.env .netrc].freeze
|
|
2056
|
+
SECRET_ASSET_BASENAMES = %w[.env .netrc key.properties].freeze
|
|
1852
2057
|
|
|
1853
2058
|
def secret_project_asset?(relative)
|
|
1854
2059
|
basename = File.basename(relative)
|
|
@@ -2145,6 +2350,8 @@ module Ruflet
|
|
|
2145
2350
|
["build", "appbundle"]
|
|
2146
2351
|
when "ios"
|
|
2147
2352
|
["build", "ios"]
|
|
2353
|
+
when "ipa"
|
|
2354
|
+
["build", "ipa"]
|
|
2148
2355
|
when "web"
|
|
2149
2356
|
["build", "web"]
|
|
2150
2357
|
when "macos"
|
|
@@ -205,10 +205,16 @@ module Ruflet
|
|
|
205
205
|
git_revision(File.expand_path("../..", template_root))
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# Always record where the client came from. A template resolved from a
|
|
209
|
+
# local checkout may have no readable revision, and skipping the marker
|
|
210
|
+
# entirely left the copied client indistinguishable from one that was
|
|
211
|
+
# never stamped. "local" never matches an expected revision, so the client
|
|
212
|
+
# is refreshed rather than treated as current.
|
|
208
213
|
def write_client_template_revision(target, revision)
|
|
209
|
-
|
|
214
|
+
value = revision.to_s.strip
|
|
215
|
+
value = "local" if value.empty?
|
|
210
216
|
|
|
211
|
-
File.write(File.join(target, ".ruflet-template-revision"), "#{
|
|
217
|
+
File.write(File.join(target, ".ruflet-template-revision"), "#{value}\n")
|
|
212
218
|
end
|
|
213
219
|
|
|
214
220
|
def client_template_current?(target, template_root)
|
|
@@ -300,13 +300,12 @@ module Ruflet
|
|
|
300
300
|
end
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
-
# The backend serves the web client on its own port, so the client
|
|
304
|
-
# and opens its websocket
|
|
305
|
-
#
|
|
306
|
-
# it to select the websocket transport.
|
|
303
|
+
# The backend serves the web client on its own port, so the client reads
|
|
304
|
+
# the origin it was loaded from and opens its websocket there. No URL is
|
|
305
|
+
# passed in the query string: nothing in the client parses one.
|
|
307
306
|
def launch_web_client(port)
|
|
308
307
|
backend_url = "http://localhost:#{port}"
|
|
309
|
-
web_url = "#{backend_url}
|
|
308
|
+
web_url = "#{backend_url}/"
|
|
310
309
|
browser_pid = open_in_browser_app_mode(web_url)
|
|
311
310
|
open_in_browser(web_url) if browser_pid.nil?
|
|
312
311
|
puts "Ruflet web client: #{web_url}"
|
|
@@ -603,23 +602,35 @@ module Ruflet
|
|
|
603
602
|
nil
|
|
604
603
|
end
|
|
605
604
|
|
|
605
|
+
# This decides both "the download installed correctly" and "the cache is
|
|
606
|
+
# still usable, skip the network". It has to agree with what `ruflet run`
|
|
607
|
+
# will actually accept later, or a half-extracted cache reports success
|
|
608
|
+
# here and then fails to launch -- and, because a present cache short
|
|
609
|
+
# circuits the download, never repairs itself without --force.
|
|
606
610
|
def prebuilt_assets_present?(root, web:, desktop:, platform: nil)
|
|
607
|
-
ok_web = !web ||
|
|
611
|
+
ok_web = !web || built_web_client_dir?(File.join(root, "web"))
|
|
608
612
|
ok_desktop = !desktop || prebuilt_desktop_present?(root, platform: platform)
|
|
609
613
|
ok_web && ok_desktop
|
|
610
614
|
end
|
|
611
615
|
|
|
616
|
+
# The prebuilt client is whatever `ruflet build` produced from Ruflet
|
|
617
|
+
# Explorer, so its bundle and binary carry that project's app name --
|
|
618
|
+
# "Ruflet Explorer.app", not the old "ruflet_client.app". Match on shape
|
|
619
|
+
# the way detect_desktop_client_command does rather than on a fixed name.
|
|
612
620
|
def prebuilt_desktop_present?(root, platform: nil)
|
|
613
621
|
platform ||= host_platform_name
|
|
614
622
|
return false if platform.nil?
|
|
615
623
|
|
|
624
|
+
desktop = File.join(root, "desktop")
|
|
625
|
+
return false unless Dir.exist?(desktop)
|
|
626
|
+
|
|
616
627
|
case platform
|
|
617
628
|
when "macos"
|
|
618
|
-
|
|
629
|
+
Dir.glob(File.join(desktop, "*.app")).any? { |app_bundle| macos_app_executable(app_bundle) }
|
|
619
630
|
when "linux"
|
|
620
|
-
|
|
631
|
+
Dir.children(desktop).any? { |entry| executable_file?(File.join(desktop, entry)) }
|
|
621
632
|
when "windows"
|
|
622
|
-
|
|
633
|
+
Dir.glob(File.join(desktop, "*.exe")).any? { |path| File.file?(path) }
|
|
623
634
|
else
|
|
624
635
|
false
|
|
625
636
|
end
|
|
@@ -823,7 +834,7 @@ module Ruflet
|
|
|
823
834
|
return if read_client_manifest(root)
|
|
824
835
|
|
|
825
836
|
assets = []
|
|
826
|
-
assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if
|
|
837
|
+
assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if built_web_client_dir?(File.join(root, "web"))
|
|
827
838
|
if prebuilt_desktop_present?(root, platform: platform)
|
|
828
839
|
assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
|
|
829
840
|
end
|
|
@@ -108,12 +108,12 @@ module Ruflet
|
|
|
108
108
|
|
|
109
109
|
installed = true
|
|
110
110
|
targets.each do |target|
|
|
111
|
-
present =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
present = prebuilt_assets_present?(
|
|
112
|
+
root,
|
|
113
|
+
web: target == :web,
|
|
114
|
+
desktop: target == :desktop,
|
|
115
|
+
platform: platform
|
|
116
|
+
)
|
|
117
117
|
installed &&= present
|
|
118
118
|
puts "#{target}: #{present ? 'installed' : 'missing'}"
|
|
119
119
|
end
|
data/lib/ruflet/version.rb
CHANGED