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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c83f1ea707d8ab40ca6e5f8228fa7f73ce4bc2c4348f586b4c6f206288babb2
4
- data.tar.gz: d147a8eb4778f93958649e1fcabdfda7db65d85c5ca4b972e2fe38017533cfb2
3
+ metadata.gz: 699753a3740cce69a71d67b59cdcd5d8e60ee55d292c3f84b989b660f6e1935a
4
+ data.tar.gz: b8162cd3c319d7afcde7dcb3c50c6359d2c2596aec2acbbfef12368fe373132c
5
5
  SHA512:
6
- metadata.gz: 5a1b8036b2001449630a70ffcec1fdb319b7c12e5cdd43599d16e0fc12c1dd411f8d27a72172ea285452ff93d134014e56175f11508395b012503c96092b6fce
7
- data.tar.gz: 6d30c57bd94d4f264b81b5346c1160c4bcf75d14aa7b1e80dc6fab0d50cfbe0332421035b287dffad76e82718c28be71e3391004f640a5eecf40da0386aeedd0
6
+ metadata.gz: 12d669e64d6f397efe4c515fb258c147f243424a8b344f686e3787106086753e37af779e227dd4dc56ef6e3c80a5d68de15fab0c2b0ec95418585cd648e39478
7
+ data.tar.gz: c70d3335b5ddc83ca1d3f4d1f1b8d4bb91044ed56a667dbc45e8ab3c182ad5010d281801672307aaaaf63e35f83720c5a00b52370bb7153ee77345561c32c755
data/README.md CHANGED
@@ -1,3 +1,57 @@
1
1
  # ruflet
2
2
 
3
- Part of Ruflet monorepo.
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
- return client_dir if client_dir
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 name.start_with?("flet_")
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 { |line| line.match?(/^\s*#{Regexp.escape(extension_alias)}\.Extension\(\),\s*$/) }
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:(flet_[^/]+)/\1\.dart'\s+as ([a-zA-Z0-9_]+);$}m) do |package_name, import_alias|
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:(flet_[^/]+)/\1\.dart'\s+as ([a-zA-Z0-9_]+);\n}m) do |match|
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 package_name == "flet" || selected_aliases.include?(import_alias)
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_]+)\.Extension\(\),\s*$/) do |match|
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 = resolve_ruflet_client_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
- template_root = ensure_cached_ruflet_client_template!(force: true, verbose: !!verbose)
27
- unless template_root
28
- warn " Template: missing"
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
- # Prefer FVM when available so Flutter/Dart match a pinned SDK.
51
- fvm_tools = flutter_tools_via_fvm(client_dir: client_dir, auto_install: auto_install)
52
- return fvm_tools if fvm_tools
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
- return metadata.merge(source: :metadata) if metadata
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
- template_target = cached_ruflet_client_template_root
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
- return target if !force && Dir.exist?(target)
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 nil unless run_template_command(clone_cmd, verbose: verbose)
145
- return nil unless run_template_command(["git", "-C", repo_dir, "sparse-checkout", "set", "templates/ruflet_flutter_template"], verbose: verbose)
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 nil unless Dir.exist?(source)
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.cp_r(source, target)
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
- nil
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
- system(*cmd, out: output, err: verbose ? $stderr : File::NULL)
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
- if !force && (wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform))
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
- return cache_root
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 = fetch_release_for_version
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
- release_by_tag("v#{ruflet_version}") ||
472
- release_by_tag(ruflet_version) ||
473
- release_by_tag("prebuild") ||
474
- release_by_tag("prebuild-main") ||
475
- release_latest
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" => 1,
683
+ "schema" => 2,
601
684
  "ruflet_version" => ruflet_version,
602
685
  "platform" => platform,
603
- "release_tag" => release && release["tag_name"],
604
- "released_at" => release && release["published_at"],
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" => assets
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 if %w[version -v --version help -h --help].include?(command)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.18" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.19" unless const_defined?(:VERSION)
5
5
  end
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.18
4
+ version: 0.0.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa