ruflet 0.0.14 → 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 +306 -0
- data/lib/ruflet/cli/build_command.rb +408 -30
- data/lib/ruflet/cli/environment_setup.rb +241 -0
- data/lib/ruflet/cli/extra_command.rb +15 -3
- data/lib/ruflet/cli/flutter_sdk.rb +87 -9
- data/lib/ruflet/cli/new_command.rb +41 -7
- data/lib/ruflet/cli/run_command.rb +106 -167
- data/lib/ruflet/cli.rb +13 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +5 -2
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Ruflet
|
|
6
|
+
module CLI
|
|
7
|
+
# Provisions the host so `ruflet build` can run: checks (and with
|
|
8
|
+
# `ruflet doctor --fix`, installs) the system tools Flutter requires.
|
|
9
|
+
#
|
|
10
|
+
# Coverage:
|
|
11
|
+
# Linux apt (Ubuntu, Debian, Kali, Mint), dnf (Fedora, RHEL),
|
|
12
|
+
# pacman (Arch, Omarchy, Manjaro), zypper (openSUSE)
|
|
13
|
+
# macOS Homebrew where available, plus Xcode Command Line Tools and
|
|
14
|
+
# CocoaPods guidance
|
|
15
|
+
# Windows winget or Chocolatey
|
|
16
|
+
#
|
|
17
|
+
# Anything that cannot be installed unattended (Xcode CLT, Visual Studio
|
|
18
|
+
# Build Tools) is reported with the exact command the developer must run.
|
|
19
|
+
module EnvironmentSetup
|
|
20
|
+
Tool = Struct.new(:name, :probe, :purpose, :packages, :manual_hint, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
LINUX_PACKAGE_MANAGERS = [
|
|
23
|
+
{ id: :apt, probe: "apt-get", install: %w[apt-get install -y], update: %w[apt-get update] },
|
|
24
|
+
{ id: :dnf, probe: "dnf", install: %w[dnf install -y], update: nil },
|
|
25
|
+
{ id: :pacman, probe: "pacman", install: %w[pacman -S --noconfirm --needed], update: nil },
|
|
26
|
+
{ id: :zypper, probe: "zypper", install: %w[zypper --non-interactive install], update: nil },
|
|
27
|
+
{ id: :apk, probe: "apk", install: %w[apk add], update: nil }
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def environment_setup!(fix: false, verbose: false)
|
|
31
|
+
issues = []
|
|
32
|
+
tools = required_system_tools
|
|
33
|
+
|
|
34
|
+
unless flutter_host
|
|
35
|
+
issues << unsupported_host_message
|
|
36
|
+
warn " System: #{unsupported_host_message}"
|
|
37
|
+
return issues
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
manager = system_package_manager
|
|
41
|
+
missing = tools.reject { |tool| tool_present?(tool) }
|
|
42
|
+
|
|
43
|
+
if missing.empty?
|
|
44
|
+
puts " System tools: ok (#{tools.map(&:name).join(', ')})"
|
|
45
|
+
return issues
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts " System tools missing: #{missing.map(&:name).join(', ')}"
|
|
49
|
+
|
|
50
|
+
unless fix
|
|
51
|
+
missing.each { |tool| warn " #{tool.name}: #{manual_hint_for(tool, manager)}" }
|
|
52
|
+
warn " Run `ruflet doctor --fix` to install them."
|
|
53
|
+
return missing.map { |tool| "missing #{tool.name}" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
auto, manual = missing.partition { |tool| installable?(tool, manager) }
|
|
57
|
+
|
|
58
|
+
if auto.any?
|
|
59
|
+
if manager
|
|
60
|
+
install_system_packages(manager, auto, verbose: verbose)
|
|
61
|
+
end
|
|
62
|
+
still_missing = auto.reject { |tool| tool_present?(tool) }
|
|
63
|
+
still_missing.each do |tool|
|
|
64
|
+
issues << "missing #{tool.name}"
|
|
65
|
+
warn " #{tool.name}: install failed — #{manual_hint_for(tool, manager)}"
|
|
66
|
+
end
|
|
67
|
+
installed = auto - still_missing
|
|
68
|
+
puts " Installed: #{installed.map(&:name).join(', ')}" if installed.any?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
manual.each do |tool|
|
|
72
|
+
issues << "missing #{tool.name}"
|
|
73
|
+
warn " #{tool.name}: requires a manual step — #{manual_hint_for(tool, manager)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
issues
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def required_system_tools
|
|
80
|
+
case flutter_host
|
|
81
|
+
when "linux" then linux_required_tools
|
|
82
|
+
when "macos", "macos_arm64" then macos_required_tools
|
|
83
|
+
when "windows" then windows_required_tools
|
|
84
|
+
else []
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def system_package_manager
|
|
89
|
+
if windows_host?
|
|
90
|
+
return { id: :winget, install: %w[winget install --accept-source-agreements --accept-package-agreements -e --id] } if which_command("winget")
|
|
91
|
+
return { id: :choco, install: %w[choco install -y] } if which_command("choco")
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if macos_host?
|
|
96
|
+
return { id: :brew, install: %w[brew install] } if which_command("brew")
|
|
97
|
+
return nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
LINUX_PACKAGE_MANAGERS.each do |manager|
|
|
101
|
+
next unless which_command(manager[:probe])
|
|
102
|
+
|
|
103
|
+
return manager
|
|
104
|
+
end
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def macos_host?
|
|
111
|
+
RbConfig::CONFIG["host_os"].match?(/darwin/i)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def linux_required_tools
|
|
115
|
+
[
|
|
116
|
+
Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
|
|
117
|
+
packages: { apt: "git", dnf: "git", pacman: "git", zypper: "git", apk: "git" }),
|
|
118
|
+
Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads",
|
|
119
|
+
packages: { apt: "curl", dnf: "curl", pacman: "curl", zypper: "curl", apk: "curl" }),
|
|
120
|
+
Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts",
|
|
121
|
+
packages: { apt: "unzip", dnf: "unzip", pacman: "unzip", zypper: "unzip", apk: "unzip" }),
|
|
122
|
+
Tool.new(name: "xz", probe: "xz", purpose: "extracting the Flutter SDK (.tar.xz)",
|
|
123
|
+
packages: { apt: "xz-utils", dnf: "xz", pacman: "xz", zypper: "xz", apk: "xz" }),
|
|
124
|
+
Tool.new(name: "zip", probe: "zip", purpose: "packaging app bundles",
|
|
125
|
+
packages: { apt: "zip", dnf: "zip", pacman: "zip", zypper: "zip", apk: "zip" }),
|
|
126
|
+
Tool.new(name: "clang", probe: "clang", purpose: "Linux desktop builds",
|
|
127
|
+
packages: { apt: "clang", dnf: "clang", pacman: "clang", zypper: "clang", apk: "clang" }),
|
|
128
|
+
Tool.new(name: "cmake", probe: "cmake", purpose: "Linux desktop builds",
|
|
129
|
+
packages: { apt: "cmake", dnf: "cmake", pacman: "cmake", zypper: "cmake", apk: "cmake" }),
|
|
130
|
+
Tool.new(name: "ninja", probe: "ninja", purpose: "Linux desktop builds",
|
|
131
|
+
packages: { apt: "ninja-build", dnf: "ninja-build", pacman: "ninja", zypper: "ninja", apk: "ninja" }),
|
|
132
|
+
Tool.new(name: "pkg-config", probe: "pkg-config", purpose: "Linux desktop builds",
|
|
133
|
+
packages: { apt: "pkg-config", dnf: "pkgconf-pkg-config", pacman: "pkgconf", zypper: "pkg-config", apk: "pkgconf" }),
|
|
134
|
+
Tool.new(name: "GTK 3 headers", probe: nil, purpose: "Linux desktop builds",
|
|
135
|
+
packages: { apt: "libgtk-3-dev", dnf: "gtk3-devel", pacman: "gtk3", zypper: "gtk3-devel", apk: "gtk+3.0-dev" })
|
|
136
|
+
]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def macos_required_tools
|
|
140
|
+
[
|
|
141
|
+
Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
|
|
142
|
+
packages: { brew: "git" },
|
|
143
|
+
manual_hint: "install Xcode Command Line Tools: xcode-select --install"),
|
|
144
|
+
Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads", packages: { brew: "curl" }),
|
|
145
|
+
Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts", packages: { brew: "unzip" }),
|
|
146
|
+
Tool.new(name: "Xcode Command Line Tools", probe: nil, purpose: "macOS/iOS builds",
|
|
147
|
+
packages: {},
|
|
148
|
+
manual_hint: "run `xcode-select --install` (or install Xcode from the App Store)"),
|
|
149
|
+
Tool.new(name: "CocoaPods", probe: "pod", purpose: "macOS/iOS plugin dependencies",
|
|
150
|
+
packages: { brew: "cocoapods" },
|
|
151
|
+
manual_hint: "run `brew install cocoapods` or `sudo gem install cocoapods`")
|
|
152
|
+
]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def windows_required_tools
|
|
156
|
+
[
|
|
157
|
+
Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
|
|
158
|
+
packages: { winget: "Git.Git", choco: "git" }),
|
|
159
|
+
Tool.new(name: "Visual Studio Build Tools", probe: nil, purpose: "Windows desktop builds",
|
|
160
|
+
packages: {},
|
|
161
|
+
manual_hint: "winget install --id Microsoft.VisualStudio.2022.BuildTools (select the " \
|
|
162
|
+
"\"Desktop development with C++\" workload)")
|
|
163
|
+
]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def tool_present?(tool)
|
|
167
|
+
case tool.name
|
|
168
|
+
when "GTK 3 headers"
|
|
169
|
+
return true if which_command("pkg-config") &&
|
|
170
|
+
system("pkg-config", "--exists", "gtk+-3.0", out: File::NULL, err: File::NULL)
|
|
171
|
+
|
|
172
|
+
false
|
|
173
|
+
when "Xcode Command Line Tools"
|
|
174
|
+
system("xcode-select", "-p", out: File::NULL, err: File::NULL)
|
|
175
|
+
else
|
|
176
|
+
tool.probe ? !which_command(tool.probe).nil? : false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def installable?(tool, manager)
|
|
181
|
+
return false unless manager
|
|
182
|
+
|
|
183
|
+
tool.packages.key?(manager[:id])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def manual_hint_for(tool, manager)
|
|
187
|
+
return tool.manual_hint if tool.manual_hint && !installable?(tool, manager)
|
|
188
|
+
return "no supported package manager found; install #{tool.name} manually" unless manager
|
|
189
|
+
|
|
190
|
+
package = tool.packages[manager[:id]]
|
|
191
|
+
return tool.manual_hint || "install #{tool.name} manually" unless package
|
|
192
|
+
|
|
193
|
+
"#{(sudo_prefix + manager[:install]).join(' ')} #{package}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def install_system_packages(manager, tools, verbose: false)
|
|
197
|
+
packages = tools.map { |tool| tool.packages[manager[:id]] }.compact.uniq
|
|
198
|
+
return if packages.empty?
|
|
199
|
+
|
|
200
|
+
if manager[:update]
|
|
201
|
+
run_privileged_command(*manager[:update], verbose: verbose)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if manager[:id] == :winget
|
|
205
|
+
# winget installs one id at a time.
|
|
206
|
+
packages.each { |package| run_privileged_command(*manager[:install], package, verbose: verbose) }
|
|
207
|
+
else
|
|
208
|
+
run_privileged_command(*manager[:install], *packages, verbose: verbose)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def run_privileged_command(*command, verbose: false)
|
|
213
|
+
full = sudo_prefix + command
|
|
214
|
+
puts " $ #{full.join(' ')}"
|
|
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)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def sudo_prefix
|
|
223
|
+
return [] if windows_host? || macos_host?
|
|
224
|
+
return [] if Process.respond_to?(:uid) && Process.uid.zero?
|
|
225
|
+
return ["sudo"] if which_command("sudo")
|
|
226
|
+
|
|
227
|
+
[]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def unsupported_host_message
|
|
231
|
+
os = RbConfig::CONFIG["host_os"]
|
|
232
|
+
if os.match?(/linux/i) && machine_arch.match?(/arm|aarch64/)
|
|
233
|
+
"Flutter publishes no official Linux #{machine_arch} build; use an x64 machine " \
|
|
234
|
+
"or build Flutter from source (https://github.com/flutter/flutter)."
|
|
235
|
+
else
|
|
236
|
+
"unsupported host platform: #{os}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -6,6 +6,8 @@ module Ruflet
|
|
|
6
6
|
module CLI
|
|
7
7
|
module ExtraCommand
|
|
8
8
|
include FlutterSdk
|
|
9
|
+
include EnvironmentSetup
|
|
10
|
+
include AndroidSdk
|
|
9
11
|
include NewCommand
|
|
10
12
|
|
|
11
13
|
def command_create(args)
|
|
@@ -20,6 +22,12 @@ module Ruflet
|
|
|
20
22
|
puts "Ruflet doctor"
|
|
21
23
|
puts " Ruby: #{RUBY_VERSION}"
|
|
22
24
|
puts " Flutter host target: #{flutter_host || 'unsupported'}"
|
|
25
|
+
|
|
26
|
+
# System prerequisites first: Flutter cannot be installed (or even
|
|
27
|
+
# extracted) without them.
|
|
28
|
+
environment_issues = environment_setup!(fix: !!fix, verbose: !!verbose)
|
|
29
|
+
return 1 unless flutter_host
|
|
30
|
+
|
|
23
31
|
if template_root
|
|
24
32
|
puts " Template: #{template_root}"
|
|
25
33
|
elsif fix
|
|
@@ -46,9 +54,13 @@ module Ruflet
|
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
puts " Flutter: #{flutter_version_summary(tools)}"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
status
|
|
57
|
+
environment_issues += android_environment_setup!(fix: !!fix, verbose: !!verbose)
|
|
58
|
+
ok = system(android_build_env(tools[:env]), tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
|
|
59
|
+
status = ok ? 0 : ($?&.exitstatus || 1)
|
|
60
|
+
if environment_issues.any?
|
|
61
|
+
warn "Unresolved environment issues: #{environment_issues.join('; ')}"
|
|
62
|
+
status = 1 if status.zero?
|
|
63
|
+
end
|
|
52
64
|
status
|
|
53
65
|
end
|
|
54
66
|
|
|
@@ -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)
|
|
@@ -322,7 +330,10 @@ module Ruflet
|
|
|
322
330
|
if os.match?(/darwin/i)
|
|
323
331
|
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
324
332
|
end
|
|
325
|
-
|
|
333
|
+
if os.match?(/linux/i)
|
|
334
|
+
# Flutter publishes Linux archives for x64 only.
|
|
335
|
+
return machine_arch.match?(/arm|aarch64/) ? nil : "linux"
|
|
336
|
+
end
|
|
326
337
|
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
327
338
|
|
|
328
339
|
nil
|
|
@@ -363,7 +374,7 @@ module Ruflet
|
|
|
363
374
|
nil
|
|
364
375
|
end
|
|
365
376
|
|
|
366
|
-
def download_file(url, destination, limit: 5)
|
|
377
|
+
def download_file(url, destination, limit: 5, label: nil)
|
|
367
378
|
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
368
379
|
|
|
369
380
|
uri = URI(url)
|
|
@@ -373,10 +384,21 @@ module Ruflet
|
|
|
373
384
|
http.request(req) do |res|
|
|
374
385
|
case res
|
|
375
386
|
when Net::HTTPSuccess
|
|
376
|
-
|
|
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)
|
|
377
399
|
return destination
|
|
378
400
|
when Net::HTTPRedirection
|
|
379
|
-
return download_file(res["location"], destination, limit: limit - 1)
|
|
401
|
+
return download_file(res["location"], destination, limit: limit - 1, label: label)
|
|
380
402
|
else
|
|
381
403
|
raise "Download failed (#{res.code})"
|
|
382
404
|
end
|
|
@@ -386,19 +408,75 @@ module Ruflet
|
|
|
386
408
|
curl_download(url, destination)
|
|
387
409
|
end
|
|
388
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
|
+
|
|
389
456
|
def extract_archive(archive, destination)
|
|
390
457
|
if archive.end_with?(".zip")
|
|
391
458
|
if windows_host?
|
|
392
459
|
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
393
460
|
end
|
|
461
|
+
|
|
462
|
+
require_extract_tool!("unzip")
|
|
394
463
|
return system("unzip", "-oq", archive, "-d", destination)
|
|
395
464
|
end
|
|
396
465
|
|
|
397
466
|
if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
|
|
467
|
+
require_extract_tool!("tar")
|
|
468
|
+
require_extract_tool!("xz") if archive.end_with?(".tar.xz") && !windows_host?
|
|
398
469
|
return system("tar", "-xf", archive, "-C", destination)
|
|
399
470
|
end
|
|
400
471
|
|
|
401
|
-
|
|
472
|
+
raise "Unsupported archive format: #{File.basename(archive)}"
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def require_extract_tool!(name)
|
|
476
|
+
return if which_command(name)
|
|
477
|
+
|
|
478
|
+
raise "`#{name}` is required to extract the Flutter SDK but was not found. " \
|
|
479
|
+
"Run `ruflet doctor --fix` to install the missing system tools."
|
|
402
480
|
end
|
|
403
481
|
end
|
|
404
482
|
end
|
|
@@ -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
|
|
@@ -84,7 +85,20 @@ module Ruflet
|
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
cached_template = cached_ruflet_client_template_root
|
|
87
|
-
|
|
88
|
+
if Dir.exist?(cached_template)
|
|
89
|
+
return cached_template if cached_template_current?(cached_template)
|
|
90
|
+
|
|
91
|
+
# The cache was written by a different ruflet version; stale
|
|
92
|
+
# framework Dart breaks newer asset layouts. Refresh, but never
|
|
93
|
+
# block a build on the network: fall back to the stale copy loudly.
|
|
94
|
+
refreshed = download_ruflet_template(force: true)
|
|
95
|
+
return refreshed if refreshed && Dir.exist?(refreshed)
|
|
96
|
+
|
|
97
|
+
warn "ruflet: could not refresh the Flutter client template cache; " \
|
|
98
|
+
"using a stale copy from another ruflet version at #{cached_template}. " \
|
|
99
|
+
"Delete it or run with network access to update."
|
|
100
|
+
return cached_template
|
|
101
|
+
end
|
|
88
102
|
|
|
89
103
|
[
|
|
90
104
|
File.expand_path("../../../ruflet_client", __dir__),
|
|
@@ -112,6 +126,21 @@ module Ruflet
|
|
|
112
126
|
File.join(template_cache_root, "ruflet_flutter_template")
|
|
113
127
|
end
|
|
114
128
|
|
|
129
|
+
TEMPLATE_CACHE_STAMP = ".ruflet_template_version"
|
|
130
|
+
|
|
131
|
+
def cached_template_current?(target)
|
|
132
|
+
stamp = File.join(target, TEMPLATE_CACHE_STAMP)
|
|
133
|
+
File.file?(stamp) && File.read(stamp).strip == Ruflet::VERSION
|
|
134
|
+
rescue StandardError
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def write_template_cache_stamp(target)
|
|
139
|
+
File.write(File.join(target, TEMPLATE_CACHE_STAMP), Ruflet::VERSION)
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
115
144
|
def cached_ruby_runtime_root
|
|
116
145
|
File.join(cache_root, "ruby_runtime")
|
|
117
146
|
end
|
|
@@ -150,6 +179,7 @@ module Ruflet
|
|
|
150
179
|
FileUtils.cp_r(source, target)
|
|
151
180
|
end
|
|
152
181
|
|
|
182
|
+
write_template_cache_stamp(target)
|
|
153
183
|
target
|
|
154
184
|
rescue StandardError => e
|
|
155
185
|
warn "Failed to fetch Ruflet template: #{e.class}: #{e.message}"
|
|
@@ -195,12 +225,11 @@ module Ruflet
|
|
|
195
225
|
# Example: https://api.example.com
|
|
196
226
|
backend_url: ""
|
|
197
227
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
services: []
|
|
228
|
+
# Flutter extension packages included in the managed client.
|
|
229
|
+
# Permission-backed extensions such as camera, audio_recorder,
|
|
230
|
+
# geolocator, and permission_handler are activated by services.yaml
|
|
231
|
+
# and ignored here.
|
|
232
|
+
extensions: []
|
|
204
233
|
|
|
205
234
|
# Build assets configuration consumed by `ruflet build`.
|
|
206
235
|
# Paths are relative to this file unless absolute.
|
|
@@ -215,6 +244,11 @@ module Ruflet
|
|
|
215
244
|
icon_background: "#FFFFFF"
|
|
216
245
|
theme_color: "#FFFFFF"
|
|
217
246
|
YAML
|
|
247
|
+
|
|
248
|
+
File.write(File.join(root, "services.yaml"), <<~YAML)
|
|
249
|
+
# Protected device access requested by this app.
|
|
250
|
+
services: []
|
|
251
|
+
YAML
|
|
218
252
|
end
|
|
219
253
|
|
|
220
254
|
def copy_default_project_assets(root)
|