ruflet 0.0.18 → 0.0.19
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 +55 -1
- data/lib/ruflet/cli/build_command.rb +42 -11
- data/lib/ruflet/cli/extra_command.rb +11 -8
- data/lib/ruflet/cli/flutter_sdk.rb +25 -4
- data/lib/ruflet/cli/new_command.rb +84 -15
- data/lib/ruflet/cli/run_command.rb +107 -14
- 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: 699753a3740cce69a71d67b59cdcd5d8e60ee55d292c3f84b989b660f6e1935a
|
|
4
|
+
data.tar.gz: b8162cd3c319d7afcde7dcb3c50c6359d2c2596aec2acbbfef12368fe373132c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12d669e64d6f397efe4c515fb258c147f243424a8b344f686e3787106086753e37af779e227dd4dc56ef6e3c80a5d68de15fab0c2b0ec95418585cd648e39478
|
|
7
|
+
data.tar.gz: c70d3335b5ddc83ca1d3f4d1f1b8d4bb91044ed56a667dbc45e8ab3c182ad5010d281801672307aaaaf63e35f83720c5a00b52370bb7153ee77345561c32c755
|
data/README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
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 [--fix]
|
|
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
|
+
Commands that create, diagnose, or build a Flutter client compare the cached
|
|
40
|
+
template revision with `AdamMusa/ruflet-template` on GitHub. When `main`
|
|
41
|
+
changes, Ruflet downloads the new template and refreshes its managed
|
|
42
|
+
`build/client` automatically. If GitHub is unavailable, Ruflet keeps using the
|
|
43
|
+
last complete cached template. Use `ruflet doctor --fix` to force a clean
|
|
44
|
+
template download.
|
|
45
|
+
|
|
46
|
+
Desktop and web runs use the completed `prebuild-main` client channel by
|
|
47
|
+
default. Ruflet checks the channel at most once every six hours and downloads
|
|
48
|
+
a new platform build only after every required prebuild job has finished.
|
|
49
|
+
Use `ruflet update --force` for an immediate refresh. Set
|
|
50
|
+
`RUFLET_CLIENT_CHANNEL=stable` to stay on versioned release assets,
|
|
51
|
+
`RUFLET_CLIENT_UPDATE_INTERVAL=0` to check on every run, or
|
|
52
|
+
`RUFLET_CLIENT_AUTO_UPDATE=0` to disable automatic client updates.
|
|
53
|
+
|
|
54
|
+
Run `ruflet install` without `--device` to choose from a numbered list of
|
|
55
|
+
connected devices. Pass `--device DEVICE_ID` to skip the prompt.
|
|
56
|
+
|
|
57
|
+
Run `ruflet help <command>` for all options.
|
|
@@ -14,7 +14,6 @@ module Ruflet
|
|
|
14
14
|
module BuildCommand
|
|
15
15
|
include FlutterSdk
|
|
16
16
|
CLIENT_EXTENSION_MAP = {
|
|
17
|
-
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
18
17
|
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
19
18
|
"audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
20
19
|
"camera" => { package: "flet_camera", alias: "ruflet_camera" },
|
|
@@ -175,13 +174,30 @@ module Ruflet
|
|
|
175
174
|
|
|
176
175
|
def ensure_flutter_client_dir(verbose: false)
|
|
177
176
|
client_dir = detect_flutter_client_dir
|
|
178
|
-
|
|
177
|
+
if client_dir
|
|
178
|
+
refresh_hidden_flutter_client_template(client_dir, verbose: verbose)
|
|
179
|
+
return client_dir
|
|
180
|
+
end
|
|
179
181
|
|
|
180
182
|
bootstrapped = bootstrap_flutter_client_template
|
|
181
183
|
build_log(verbose, "bootstrapped client template at #{bootstrapped}") if bootstrapped
|
|
182
184
|
bootstrapped
|
|
183
185
|
end
|
|
184
186
|
|
|
187
|
+
def refresh_hidden_flutter_client_template(client_dir, verbose: false)
|
|
188
|
+
return unless File.expand_path(client_dir) == File.expand_path(hidden_flutter_client_dir)
|
|
189
|
+
return unless File.file?(File.join(client_dir, ".metadata"))
|
|
190
|
+
return unless Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
191
|
+
|
|
192
|
+
template_root = Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
193
|
+
return unless template_root && Dir.exist?(template_root)
|
|
194
|
+
return if Ruflet::CLI.respond_to?(:client_template_current?, true) &&
|
|
195
|
+
Ruflet::CLI.send(:client_template_current?, client_dir, template_root)
|
|
196
|
+
|
|
197
|
+
Ruflet::CLI.send(:copy_ruflet_client_template, Dir.pwd)
|
|
198
|
+
build_log(verbose, "refreshed managed Flutter client from template #{template_root}")
|
|
199
|
+
end
|
|
200
|
+
|
|
185
201
|
def build_tool_env(env, platform, client_dir = nil)
|
|
186
202
|
return env unless %w[ios macos].include?(platform)
|
|
187
203
|
|
|
@@ -1213,7 +1229,8 @@ module Ruflet
|
|
|
1213
1229
|
"lib/connection_probe.dart",
|
|
1214
1230
|
"lib/connection_probe_io.dart",
|
|
1215
1231
|
"lib/connection_probe_stub.dart",
|
|
1216
|
-
"ios/Podfile"
|
|
1232
|
+
"ios/Podfile",
|
|
1233
|
+
"windows/CMakeLists.txt"
|
|
1217
1234
|
]
|
|
1218
1235
|
|
|
1219
1236
|
managed_files.each do |relative_path|
|
|
@@ -1388,10 +1405,10 @@ module Ruflet
|
|
|
1388
1405
|
def prune_client_pubspec(path, selected_packages)
|
|
1389
1406
|
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
1390
1407
|
deps = (data["dependencies"] || {}).dup
|
|
1408
|
+
optional_packages = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:package) }.uniq
|
|
1391
1409
|
|
|
1392
1410
|
deps.keys.each do |name|
|
|
1393
|
-
next unless
|
|
1394
|
-
next if name == "flet"
|
|
1411
|
+
next unless optional_packages.include?(name)
|
|
1395
1412
|
next if selected_packages.include?(name)
|
|
1396
1413
|
|
|
1397
1414
|
deps.delete(name)
|
|
@@ -1445,7 +1462,9 @@ module Ruflet
|
|
|
1445
1462
|
|
|
1446
1463
|
selected_aliases.each do |extension_alias|
|
|
1447
1464
|
import_line = template.lines.find { |line| line.match?(/\sas #{Regexp.escape(extension_alias)};\s*\z/) }
|
|
1448
|
-
extension_line = template.lines.find
|
|
1465
|
+
extension_line = template.lines.find do |line|
|
|
1466
|
+
line.match?(/^\s*#{Regexp.escape(extension_alias)}\.[A-Za-z0-9_]+\(\),\s*$/)
|
|
1467
|
+
end
|
|
1449
1468
|
|
|
1450
1469
|
content = insert_missing_import(content, import_line) if import_line && !content.include?(import_line)
|
|
1451
1470
|
content = insert_missing_extension(content, extension_line) if extension_line && !content.include?(extension_line)
|
|
@@ -1491,25 +1510,37 @@ module Ruflet
|
|
|
1491
1510
|
def prune_client_main(path, selected_aliases)
|
|
1492
1511
|
content = File.read(path)
|
|
1493
1512
|
alias_to_package = {}
|
|
1513
|
+
optional_aliases = CLIENT_EXTENSION_MAP.values.map { |entry| entry.fetch(:alias) }.uniq
|
|
1494
1514
|
|
|
1495
|
-
content.scan(%r{^import 'package:(
|
|
1515
|
+
content.scan(%r{^import 'package:([^/]+)/[^']+'\s+as ([a-zA-Z0-9_]+);$}m) do |package_name, import_alias|
|
|
1496
1516
|
alias_to_package[import_alias] = package_name
|
|
1497
1517
|
end
|
|
1518
|
+
content.scan(%r{^import '([^']+)'\s+as ([a-zA-Z0-9_]+);$}m) do |_source, import_alias|
|
|
1519
|
+
alias_to_package[import_alias] = :local if optional_aliases.include?(import_alias)
|
|
1520
|
+
end
|
|
1498
1521
|
|
|
1499
|
-
content = content.gsub(%r{^import 'package:(
|
|
1522
|
+
content = content.gsub(%r{^import 'package:([^/]+)/[^']+'\s+as ([a-zA-Z0-9_]+);\n}m) do |match|
|
|
1500
1523
|
package_name = Regexp.last_match(1)
|
|
1501
1524
|
import_alias = Regexp.last_match(2)
|
|
1502
|
-
if
|
|
1525
|
+
if !optional_aliases.include?(import_alias) || selected_aliases.include?(import_alias)
|
|
1526
|
+
match
|
|
1527
|
+
else
|
|
1528
|
+
""
|
|
1529
|
+
end
|
|
1530
|
+
end
|
|
1531
|
+
content = content.gsub(%r{^import '([^']+)'\s+as ([a-zA-Z0-9_]+);\n}m) do |match|
|
|
1532
|
+
import_alias = Regexp.last_match(2)
|
|
1533
|
+
if !optional_aliases.include?(import_alias) || selected_aliases.include?(import_alias)
|
|
1503
1534
|
match
|
|
1504
1535
|
else
|
|
1505
1536
|
""
|
|
1506
1537
|
end
|
|
1507
1538
|
end
|
|
1508
1539
|
|
|
1509
|
-
content = content.gsub(/^(\s*)([a-zA-Z0-9_]+)\.
|
|
1540
|
+
content = content.gsub(/^(\s*)([a-zA-Z0-9_]+)\.[A-Za-z0-9_]+\(\),\s*$/) do |match|
|
|
1510
1541
|
extension_alias = Regexp.last_match(2)
|
|
1511
1542
|
package_name = alias_to_package[extension_alias]
|
|
1512
|
-
if package_name.nil? || selected_aliases.include?(extension_alias)
|
|
1543
|
+
if package_name.nil? || !optional_aliases.include?(extension_alias) || selected_aliases.include?(extension_alias)
|
|
1513
1544
|
match
|
|
1514
1545
|
else
|
|
1515
1546
|
""
|
|
@@ -16,20 +16,23 @@ module Ruflet
|
|
|
16
16
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
17
17
|
fix = args.delete("--fix")
|
|
18
18
|
client_dir = detect_client_dir
|
|
19
|
-
template_root =
|
|
19
|
+
template_root =
|
|
20
|
+
if fix
|
|
21
|
+
ensure_cached_ruflet_client_template!(force: true, verbose: !!verbose)
|
|
22
|
+
else
|
|
23
|
+
resolve_ruflet_client_template_root
|
|
24
|
+
end
|
|
20
25
|
puts "Ruflet doctor"
|
|
21
26
|
puts " Ruby: #{RUBY_VERSION}"
|
|
22
27
|
puts " Flutter host target: #{flutter_host || 'unsupported'}"
|
|
23
28
|
if template_root
|
|
24
29
|
puts " Template: #{template_root}"
|
|
30
|
+
revision = cached_template_revision if respond_to?(:cached_template_revision, true)
|
|
31
|
+
puts " Template revision: #{revision}" if revision
|
|
25
32
|
elsif fix
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
warn "Failed to fetch the Ruflet Flutter template from GitHub."
|
|
30
|
-
return 1
|
|
31
|
-
end
|
|
32
|
-
puts " Template: #{template_root}"
|
|
33
|
+
warn " Template: missing"
|
|
34
|
+
warn "Failed to fetch the Ruflet Flutter template from GitHub."
|
|
35
|
+
return 1
|
|
33
36
|
else
|
|
34
37
|
warn " Template: missing"
|
|
35
38
|
warn "Run `ruflet doctor --fix` to fetch the Flutter template from GitHub."
|
|
@@ -47,9 +47,18 @@ module Ruflet
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
49
|
def flutter_tools(client_dir: nil, auto_install: true)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
desired = desired_flutter_spec(client_dir: client_dir)
|
|
51
|
+
|
|
52
|
+
# Environment and .fvmrc versions are intentional pins. Flutter's
|
|
53
|
+
# generated .metadata revision only records which SDK created the
|
|
54
|
+
# project; it must not force every build to download that old SDK.
|
|
55
|
+
if %i[env fvmrc].include?(desired[:source])
|
|
56
|
+
fvm_tools = flutter_tools_via_fvm(client_dir: client_dir, auto_install: auto_install)
|
|
57
|
+
return fvm_tools if fvm_tools
|
|
58
|
+
else
|
|
59
|
+
system_tools = flutter_tools_via_system
|
|
60
|
+
return system_tools if system_tools
|
|
61
|
+
end
|
|
53
62
|
|
|
54
63
|
managed_tools = flutter_tools_via_managed_sdk(client_dir: client_dir, auto_install: auto_install)
|
|
55
64
|
return managed_tools if managed_tools
|
|
@@ -57,6 +66,13 @@ module Ruflet
|
|
|
57
66
|
nil
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
def flutter_tools_via_system
|
|
70
|
+
flutter = which_command("flutter")
|
|
71
|
+
return nil unless flutter
|
|
72
|
+
|
|
73
|
+
tools_from_flutter_bin(flutter)
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
def flutter_tools_via_managed_sdk(client_dir: nil, auto_install: true)
|
|
61
77
|
return nil unless auto_install
|
|
62
78
|
|
|
@@ -199,7 +215,12 @@ module Ruflet
|
|
|
199
215
|
return { version: fvm, source: :fvmrc } if fvm
|
|
200
216
|
|
|
201
217
|
metadata = parse_flutter_metadata(find_flutter_metadata(client_dir))
|
|
202
|
-
|
|
218
|
+
if metadata
|
|
219
|
+
return {
|
|
220
|
+
channel: metadata[:channel] || DEFAULT_FLUTTER_CHANNEL,
|
|
221
|
+
source: :metadata
|
|
222
|
+
}
|
|
223
|
+
end
|
|
203
224
|
|
|
204
225
|
{ channel: DEFAULT_FLUTTER_CHANNEL, version: DEFAULT_FLUTTER_CHANNEL, source: :default }
|
|
205
226
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "open3"
|
|
4
5
|
require "tmpdir"
|
|
5
6
|
require "yaml"
|
|
6
7
|
|
|
@@ -8,10 +9,10 @@ module Ruflet
|
|
|
8
9
|
module CLI
|
|
9
10
|
module NewCommand
|
|
10
11
|
TEMPLATE_REPO_URL = ENV.fetch("RUFLET_TEMPLATE_REPO_URL", "https://github.com/AdamMusa/ruflet-template.git")
|
|
12
|
+
TEMPLATE_REPO_REF = ENV.fetch("RUFLET_TEMPLATE_REPO_REF", "main")
|
|
11
13
|
RUNTIME_REPO_URL = ENV.fetch("RUFLET_RUNTIME_REPO_URL", "https://github.com/AdamMusa/ruflet.git")
|
|
12
14
|
|
|
13
15
|
CLIENT_EXTENSION_MAP = {
|
|
14
|
-
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
15
16
|
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
16
17
|
"audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
17
18
|
"camera" => { package: "flet_camera", alias: "ruflet_camera" },
|
|
@@ -69,8 +70,10 @@ module Ruflet
|
|
|
69
70
|
|
|
70
71
|
target = hidden_flutter_client_dir(root)
|
|
71
72
|
FileUtils.mkdir_p(File.dirname(target))
|
|
73
|
+
FileUtils.rm_rf(target)
|
|
72
74
|
FileUtils.cp_r(template_root, target)
|
|
73
75
|
prune_client_template(target)
|
|
76
|
+
write_client_template_revision(target, installed_template_revision(template_root))
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def hidden_flutter_client_dir(root = Dir.pwd)
|
|
@@ -99,8 +102,6 @@ module Ruflet
|
|
|
99
102
|
|
|
100
103
|
def ensure_cached_ruflet_client_template!(force: false, verbose: false)
|
|
101
104
|
cached_template = cached_ruflet_client_template_root
|
|
102
|
-
return cached_template if !force && Dir.exist?(cached_template)
|
|
103
|
-
|
|
104
105
|
download_ruflet_template(force: force, verbose: verbose)
|
|
105
106
|
Dir.exist?(cached_template) ? cached_template : nil
|
|
106
107
|
end
|
|
@@ -113,6 +114,10 @@ module Ruflet
|
|
|
113
114
|
File.join(template_cache_root, "ruflet_flutter_template")
|
|
114
115
|
end
|
|
115
116
|
|
|
117
|
+
def cached_ruflet_client_template_revision_path
|
|
118
|
+
File.join(template_cache_root, "ruflet_flutter_template.revision")
|
|
119
|
+
end
|
|
120
|
+
|
|
116
121
|
def cached_ruby_runtime_root
|
|
117
122
|
File.join(cache_root, "ruby_runtime")
|
|
118
123
|
end
|
|
@@ -126,40 +131,104 @@ module Ruflet
|
|
|
126
131
|
end
|
|
127
132
|
|
|
128
133
|
def download_ruflet_assets(force: false, verbose: false)
|
|
129
|
-
|
|
130
|
-
return true if !force && Dir.exist?(template_target)
|
|
131
|
-
|
|
132
|
-
Dir.exist?(template_target) || download_ruflet_template(force: force, verbose: verbose)
|
|
134
|
+
!ensure_cached_ruflet_client_template!(force: force, verbose: verbose).nil?
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
def download_ruflet_template(force: false, verbose: false)
|
|
136
138
|
target = cached_ruflet_client_template_root
|
|
137
|
-
|
|
139
|
+
remote_revision = remote_template_revision(verbose: verbose)
|
|
140
|
+
current_revision = cached_template_revision
|
|
141
|
+
cache_available = valid_ruflet_template?(target)
|
|
142
|
+
if !force && cache_available && remote_revision && current_revision == remote_revision
|
|
143
|
+
return target
|
|
144
|
+
end
|
|
145
|
+
if !force && cache_available && remote_revision.nil?
|
|
146
|
+
build_log(verbose, "Unable to check the Ruflet template revision; using cached #{current_revision || 'template'}") if respond_to?(:build_log, true)
|
|
147
|
+
return target
|
|
148
|
+
end
|
|
138
149
|
|
|
139
150
|
FileUtils.mkdir_p(template_cache_root)
|
|
140
151
|
|
|
141
152
|
Dir.mktmpdir("ruflet-assets") do |tmp|
|
|
142
153
|
repo_dir = File.join(tmp, "Ruflet")
|
|
143
|
-
clone_cmd = ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", TEMPLATE_REPO_URL, repo_dir]
|
|
144
|
-
return
|
|
145
|
-
return
|
|
154
|
+
clone_cmd = ["git", "clone", "--depth", "1", "--branch", TEMPLATE_REPO_REF, "--filter=blob:none", "--sparse", TEMPLATE_REPO_URL, repo_dir]
|
|
155
|
+
return target_if_present(target) unless run_template_command(clone_cmd, verbose: verbose)
|
|
156
|
+
return target_if_present(target) unless run_template_command(["git", "-C", repo_dir, "sparse-checkout", "set", "templates/ruflet_flutter_template"], verbose: verbose)
|
|
146
157
|
|
|
147
158
|
source = File.join(repo_dir, "templates", "ruflet_flutter_template")
|
|
148
|
-
return
|
|
159
|
+
return target_if_present(target) unless valid_ruflet_template?(source)
|
|
149
160
|
|
|
161
|
+
fetched_revision = git_revision(repo_dir) || remote_revision
|
|
162
|
+
staged_target = File.join(tmp, "ruflet_flutter_template")
|
|
163
|
+
FileUtils.cp_r(source, staged_target)
|
|
150
164
|
FileUtils.rm_rf(target)
|
|
151
|
-
FileUtils.
|
|
165
|
+
FileUtils.mv(staged_target, target)
|
|
166
|
+
File.write(cached_ruflet_client_template_revision_path, "#{fetched_revision}\n") if fetched_revision
|
|
152
167
|
end
|
|
153
168
|
|
|
154
169
|
target
|
|
155
170
|
rescue StandardError => e
|
|
156
171
|
warn "Failed to fetch Ruflet template: #{e.class}: #{e.message}"
|
|
157
|
-
|
|
172
|
+
target_if_present(target)
|
|
158
173
|
end
|
|
159
174
|
|
|
160
175
|
def run_template_command(cmd, verbose: false)
|
|
161
176
|
output = verbose ? $stdout : File::NULL
|
|
162
|
-
|
|
177
|
+
env = { "GIT_TERMINAL_PROMPT" => "0" }
|
|
178
|
+
system(env, *cmd, out: output, err: verbose ? $stderr : File::NULL)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def remote_template_revision(verbose: false)
|
|
182
|
+
return @ruflet_template_remote_revision if defined?(@ruflet_template_remote_revision)
|
|
183
|
+
|
|
184
|
+
env = { "GIT_TERMINAL_PROMPT" => "0" }
|
|
185
|
+
stdout, stderr, status = Open3.capture3(env, "git", "ls-remote", TEMPLATE_REPO_URL, "refs/heads/#{TEMPLATE_REPO_REF}")
|
|
186
|
+
warn stderr unless status.success? || !verbose || stderr.empty?
|
|
187
|
+
@ruflet_template_remote_revision = status.success? ? stdout.split.first : nil
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
warn "Failed to check Ruflet template revision: #{e.message}" if verbose
|
|
190
|
+
@ruflet_template_remote_revision = nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def cached_template_revision
|
|
194
|
+
return nil unless File.file?(cached_ruflet_client_template_revision_path)
|
|
195
|
+
|
|
196
|
+
File.read(cached_ruflet_client_template_revision_path).strip.then { |value| value.empty? ? nil : value }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def installed_template_revision(template_root)
|
|
200
|
+
return cached_template_revision if File.expand_path(template_root) == File.expand_path(cached_ruflet_client_template_root)
|
|
201
|
+
|
|
202
|
+
git_revision(File.expand_path("../..", template_root))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def write_client_template_revision(target, revision)
|
|
206
|
+
return unless revision
|
|
207
|
+
|
|
208
|
+
File.write(File.join(target, ".ruflet-template-revision"), "#{revision}\n")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def client_template_current?(target, template_root)
|
|
212
|
+
expected = installed_template_revision(template_root)
|
|
213
|
+
return false unless expected
|
|
214
|
+
|
|
215
|
+
marker = File.join(target, ".ruflet-template-revision")
|
|
216
|
+
File.file?(marker) && File.read(marker).strip == expected
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def git_revision(repo_dir)
|
|
220
|
+
stdout, _stderr, status = Open3.capture3("git", "-C", repo_dir, "rev-parse", "HEAD")
|
|
221
|
+
status.success? ? stdout.strip : nil
|
|
222
|
+
rescue StandardError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def valid_ruflet_template?(path)
|
|
227
|
+
Dir.exist?(path) && File.file?(File.join(path, "pubspec.yaml")) && File.file?(File.join(path, "lib", "main.dart"))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def target_if_present(target)
|
|
231
|
+
valid_ruflet_template?(target) ? target : nil
|
|
163
232
|
end
|
|
164
233
|
|
|
165
234
|
def prune_client_template(target)
|
|
@@ -16,6 +16,9 @@ require "time"
|
|
|
16
16
|
module Ruflet
|
|
17
17
|
module CLI
|
|
18
18
|
module RunCommand
|
|
19
|
+
CLIENT_CHANNEL_MANIFEST = "ruflet_client-manifest.json"
|
|
20
|
+
DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
|
|
21
|
+
|
|
19
22
|
def command_run(args)
|
|
20
23
|
options = { target: "mobile", requested_port: 8550 }
|
|
21
24
|
parser = OptionParser.new do |o|
|
|
@@ -371,17 +374,29 @@ module Ruflet
|
|
|
371
374
|
return nil if desktop_asset.nil?
|
|
372
375
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
373
376
|
end
|
|
374
|
-
|
|
377
|
+
cache_ready = wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
|
|
378
|
+
release = nil
|
|
379
|
+
if !force && cache_ready
|
|
375
380
|
ensure_client_manifest(cache_root, platform: platform)
|
|
376
|
-
|
|
381
|
+
manifest = read_client_manifest(cache_root)
|
|
382
|
+
return cache_root unless client_update_due?(manifest)
|
|
383
|
+
|
|
384
|
+
release = fetch_release_for_version(wanted_assets: wanted_assets)
|
|
385
|
+
if release.nil? || client_release_current?(manifest, release, wanted_assets)
|
|
386
|
+
mark_client_update_checked(cache_root)
|
|
387
|
+
return cache_root
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
force = true
|
|
377
391
|
end
|
|
378
392
|
|
|
379
|
-
release
|
|
393
|
+
release ||= fetch_release_for_version(wanted_assets: wanted_assets)
|
|
380
394
|
return nil unless release
|
|
381
395
|
|
|
382
396
|
assets = release.fetch("assets", [])
|
|
383
397
|
asset_names = assets.map { |a| a["name"].to_s }
|
|
384
398
|
installed_assets = []
|
|
399
|
+
release_revision = client_release_revision(release)
|
|
385
400
|
Dir.mktmpdir("ruflet-prebuilt-") do |tmpdir|
|
|
386
401
|
wanted_assets.each do |wanted|
|
|
387
402
|
asset_name = wanted.fetch(:name)
|
|
@@ -408,7 +423,8 @@ module Ruflet
|
|
|
408
423
|
"kind" => wanted[:kind].to_s,
|
|
409
424
|
"platform" => wanted[:platform] || platform,
|
|
410
425
|
"asset_name" => resolved_name,
|
|
411
|
-
"download_url" => asset.fetch("browser_download_url")
|
|
426
|
+
"download_url" => asset.fetch("browser_download_url"),
|
|
427
|
+
"release_revision" => release_revision
|
|
412
428
|
}
|
|
413
429
|
end
|
|
414
430
|
end
|
|
@@ -467,12 +483,71 @@ module Ruflet
|
|
|
467
483
|
File.join(Dir.home, ".ruflet", "client", ruflet_version, platform.to_s)
|
|
468
484
|
end
|
|
469
485
|
|
|
470
|
-
def fetch_release_for_version
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
release_by_tag(
|
|
475
|
-
|
|
486
|
+
def fetch_release_for_version(wanted_assets: [])
|
|
487
|
+
releases = []
|
|
488
|
+
channel = client_release_channel
|
|
489
|
+
if channel != "stable"
|
|
490
|
+
rolling = release_by_tag(channel)
|
|
491
|
+
releases << rolling if rolling && rolling_release_complete?(rolling)
|
|
492
|
+
end
|
|
493
|
+
releases << release_by_tag("v#{ruflet_version}")
|
|
494
|
+
releases << release_by_tag(ruflet_version)
|
|
495
|
+
releases << release_latest
|
|
496
|
+
releases.compact.find { |release| release_has_wanted_assets?(release, wanted_assets) }
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def client_release_channel
|
|
500
|
+
value = ENV.fetch("RUFLET_CLIENT_CHANNEL", "prebuild-main").to_s.strip
|
|
501
|
+
value.empty? ? "prebuild-main" : value
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def rolling_release_complete?(release)
|
|
505
|
+
release.fetch("assets", []).any? { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def release_has_wanted_assets?(release, wanted_assets)
|
|
509
|
+
assets = release.fetch("assets", [])
|
|
510
|
+
wanted_assets.all? do |wanted|
|
|
511
|
+
assets.any? { |asset| asset["name"] == wanted[:name] } || fallback_release_asset(assets, wanted)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def client_release_revision(release)
|
|
516
|
+
marker = release.fetch("assets", []).find { |asset| asset["name"] == CLIENT_CHANNEL_MANIFEST }
|
|
517
|
+
source = marker || release
|
|
518
|
+
[source["id"], source["updated_at"] || source["published_at"], source["size"]].compact.join(":")
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def client_release_current?(manifest, release, wanted_assets)
|
|
522
|
+
return false unless manifest
|
|
523
|
+
|
|
524
|
+
revision = client_release_revision(release)
|
|
525
|
+
targets = Array(manifest["targets"])
|
|
526
|
+
wanted_assets.all? do |wanted|
|
|
527
|
+
targets.any? do |target|
|
|
528
|
+
target["kind"] == wanted[:kind].to_s &&
|
|
529
|
+
(wanted[:platform].nil? || target["platform"] == wanted[:platform]) &&
|
|
530
|
+
target["release_revision"] == revision
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def client_update_due?(manifest)
|
|
536
|
+
return false if ENV["RUFLET_CLIENT_AUTO_UPDATE"].to_s.match?(/\A(?:0|false|no|off)\z/i)
|
|
537
|
+
return true unless manifest
|
|
538
|
+
|
|
539
|
+
checked_at = manifest["checked_at"] || manifest["installed_at"]
|
|
540
|
+
return true if checked_at.to_s.empty?
|
|
541
|
+
|
|
542
|
+
Time.now.utc - Time.iso8601(checked_at) >= client_update_interval
|
|
543
|
+
rescue ArgumentError
|
|
544
|
+
true
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def client_update_interval
|
|
548
|
+
Integer(ENV.fetch("RUFLET_CLIENT_UPDATE_INTERVAL", DEFAULT_CLIENT_UPDATE_INTERVAL.to_s), 10).clamp(0, 7 * 24 * 60 * 60)
|
|
549
|
+
rescue ArgumentError
|
|
550
|
+
DEFAULT_CLIENT_UPDATE_INTERVAL
|
|
476
551
|
end
|
|
477
552
|
|
|
478
553
|
def ruflet_version
|
|
@@ -596,18 +671,36 @@ module Ruflet
|
|
|
596
671
|
|
|
597
672
|
def write_client_manifest(root, platform:, release:, assets:)
|
|
598
673
|
FileUtils.mkdir_p(root)
|
|
674
|
+
existing = read_client_manifest(root) || {}
|
|
675
|
+
installed_targets = Array(existing["targets"])
|
|
676
|
+
assets.each do |asset|
|
|
677
|
+
installed_targets.reject! do |target|
|
|
678
|
+
target["kind"] == asset["kind"] && target["platform"] == asset["platform"]
|
|
679
|
+
end
|
|
680
|
+
installed_targets << asset
|
|
681
|
+
end
|
|
599
682
|
payload = {
|
|
600
|
-
"schema" =>
|
|
683
|
+
"schema" => 2,
|
|
601
684
|
"ruflet_version" => ruflet_version,
|
|
602
685
|
"platform" => platform,
|
|
603
|
-
"release_tag" => release && release["tag_name"],
|
|
604
|
-
"
|
|
686
|
+
"release_tag" => release && release["tag_name"] || existing["release_tag"],
|
|
687
|
+
"release_revision" => release && client_release_revision(release) || existing["release_revision"],
|
|
688
|
+
"released_at" => release && release["published_at"] || existing["released_at"],
|
|
689
|
+
"checked_at" => Time.now.utc.iso8601,
|
|
605
690
|
"installed_at" => Time.now.utc.iso8601,
|
|
606
|
-
"targets" =>
|
|
691
|
+
"targets" => installed_targets
|
|
607
692
|
}
|
|
608
693
|
File.write(client_manifest_path(root), JSON.pretty_generate(payload))
|
|
609
694
|
end
|
|
610
695
|
|
|
696
|
+
def mark_client_update_checked(root)
|
|
697
|
+
manifest = read_client_manifest(root)
|
|
698
|
+
return unless manifest
|
|
699
|
+
|
|
700
|
+
manifest["checked_at"] = Time.now.utc.iso8601
|
|
701
|
+
File.write(client_manifest_path(root), JSON.pretty_generate(manifest))
|
|
702
|
+
end
|
|
703
|
+
|
|
611
704
|
def print_mobile_qr_hint(port: 8550)
|
|
612
705
|
host = best_lan_host
|
|
613
706
|
payload = "http://#{host}:#{port}"
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -73,7 +73,7 @@ module Ruflet
|
|
|
73
73
|
ruflet install [--device DEVICE_ID] [--verbose]
|
|
74
74
|
ruflet devices
|
|
75
75
|
ruflet emulators
|
|
76
|
-
ruflet doctor
|
|
76
|
+
ruflet doctor [--fix] [--verbose]
|
|
77
77
|
HELP
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -89,7 +89,7 @@ module Ruflet
|
|
|
89
89
|
private
|
|
90
90
|
|
|
91
91
|
def ensure_first_run_assets(command)
|
|
92
|
-
return
|
|
92
|
+
return unless %w[create new bootstrap init update debug build install doctor].include?(command)
|
|
93
93
|
return unless respond_to?(:download_ruflet_assets, true)
|
|
94
94
|
|
|
95
95
|
send(:download_ruflet_assets)
|
data/lib/ruflet/version.rb
CHANGED