ruflet 0.0.13 → 0.0.15
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/android_sdk.rb +305 -0
- data/lib/ruflet/cli/build_command.rb +361 -5
- data/lib/ruflet/cli/environment_setup.rb +238 -0
- data/lib/ruflet/cli/extra_command.rb +14 -1
- data/lib/ruflet/cli/flutter_sdk.rb +20 -2
- data/lib/ruflet/cli/new_command.rb +40 -7
- data/lib/ruflet/cli/run_command.rb +48 -4
- data/lib/ruflet/cli.rb +6 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 629de90fa5d63db66ce7f96b69fcd83da284621d1bf8f3c179d2ea7b54f1591b
|
|
4
|
+
data.tar.gz: 4f91dfe6d31fe054b683f9838120fb6476501ac959e981eff19ef1809c880aa9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23a877adf84ca8bc119cb5452a480dd666e05f36fd6f3a1a03f75f51fa5b2ff028f07b5373b8c043a1acd43b49dc68f7885992bcb3596e279be07fed5751cb07
|
|
7
|
+
data.tar.gz: c66e78cc467a27803bbb52264444289f4a9126763c4aed1c53df6b945e1c08f0478fff277008a40c742750dde027a67c84a2209ff4d255b6b48483ffa6a21549
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
module Ruflet
|
|
9
|
+
module CLI
|
|
10
|
+
# Provisions the Android toolchain so `ruflet build apk|aab|android`
|
|
11
|
+
# works on a fresh machine:
|
|
12
|
+
#
|
|
13
|
+
# * a JDK (Gradle itself is bootstrapped by the project's Gradle
|
|
14
|
+
# wrapper, but the wrapper needs Java)
|
|
15
|
+
# * the Android SDK command-line tools
|
|
16
|
+
# * platform-tools / a platform / build-tools via sdkmanager
|
|
17
|
+
# * accepted SDK licenses
|
|
18
|
+
#
|
|
19
|
+
# An existing SDK (ANDROID_HOME, ANDROID_SDK_ROOT, or the standard
|
|
20
|
+
# per-OS install location) is always preferred; otherwise a Ruflet-managed
|
|
21
|
+
# SDK is created under ~/.ruflet/android-sdk.
|
|
22
|
+
module AndroidSdk
|
|
23
|
+
CMDLINE_TOOLS_VERSION = "11076708"
|
|
24
|
+
CMDLINE_TOOLS_BASE = "https://dl.google.com/android/repository".freeze
|
|
25
|
+
ANDROID_PLATFORM = "android-35"
|
|
26
|
+
ANDROID_BUILD_TOOLS = "35.0.0"
|
|
27
|
+
MINIMUM_JAVA_MAJOR = 17
|
|
28
|
+
|
|
29
|
+
JDK_PACKAGES = {
|
|
30
|
+
apt: "openjdk-17-jdk",
|
|
31
|
+
dnf: "java-17-openjdk-devel",
|
|
32
|
+
pacman: "jdk17-openjdk",
|
|
33
|
+
zypper: "java-17-openjdk-devel",
|
|
34
|
+
apk: "openjdk17",
|
|
35
|
+
brew: "openjdk@17",
|
|
36
|
+
winget: "EclipseAdoptium.Temurin.17.JDK",
|
|
37
|
+
choco: "temurin17"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def android_environment_setup!(fix: false, verbose: false)
|
|
41
|
+
issues = []
|
|
42
|
+
|
|
43
|
+
java = ensure_java!(fix: fix, verbose: verbose)
|
|
44
|
+
if java
|
|
45
|
+
puts " Java: #{java[:version]} (#{java[:path]})"
|
|
46
|
+
else
|
|
47
|
+
issues << "missing JDK #{MINIMUM_JAVA_MAJOR}+"
|
|
48
|
+
warn " Java: missing — Android builds need a JDK (Gradle wrapper requirement)."
|
|
49
|
+
warn " #{jdk_install_hint}"
|
|
50
|
+
return issues unless fix
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sdk_root = detect_android_sdk_root
|
|
54
|
+
if sdk_root
|
|
55
|
+
puts " Android SDK: #{sdk_root}"
|
|
56
|
+
elsif fix
|
|
57
|
+
unless java
|
|
58
|
+
warn " Android SDK: skipped (sdkmanager needs Java)"
|
|
59
|
+
return issues
|
|
60
|
+
end
|
|
61
|
+
sdk_root = install_managed_android_sdk(java, verbose: verbose)
|
|
62
|
+
unless sdk_root
|
|
63
|
+
issues << "Android SDK install failed"
|
|
64
|
+
warn " Android SDK: install failed"
|
|
65
|
+
return issues
|
|
66
|
+
end
|
|
67
|
+
puts " Android SDK: #{sdk_root} (installed)"
|
|
68
|
+
else
|
|
69
|
+
issues << "missing Android SDK"
|
|
70
|
+
warn " Android SDK: missing — run `ruflet doctor --fix` to install it under #{managed_android_sdk_root}."
|
|
71
|
+
return issues
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if fix && java
|
|
75
|
+
ensure_android_packages(sdk_root, java, verbose: verbose)
|
|
76
|
+
accept_android_licenses(sdk_root, java, verbose: verbose)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
issues
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Environment for invoking Flutter/Gradle Android builds.
|
|
83
|
+
def android_build_env(env)
|
|
84
|
+
merged = env.dup
|
|
85
|
+
sdk_root = detect_android_sdk_root
|
|
86
|
+
if sdk_root
|
|
87
|
+
merged["ANDROID_HOME"] ||= sdk_root
|
|
88
|
+
merged["ANDROID_SDK_ROOT"] ||= sdk_root
|
|
89
|
+
platform_tools = File.join(sdk_root, "platform-tools")
|
|
90
|
+
if Dir.exist?(platform_tools)
|
|
91
|
+
merged["PATH"] = "#{platform_tools}#{File::PATH_SEPARATOR}#{merged.fetch('PATH', ENV.fetch('PATH', ''))}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
java_home = detect_java_home
|
|
95
|
+
merged["JAVA_HOME"] ||= java_home if java_home
|
|
96
|
+
merged
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def detect_android_sdk_root
|
|
100
|
+
candidates = [
|
|
101
|
+
ENV["ANDROID_HOME"],
|
|
102
|
+
ENV["ANDROID_SDK_ROOT"],
|
|
103
|
+
default_android_sdk_location,
|
|
104
|
+
managed_android_sdk_root
|
|
105
|
+
]
|
|
106
|
+
candidates.compact.find { |root| android_sdk_present?(root) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def managed_android_sdk_root
|
|
110
|
+
File.join(Dir.home, ".ruflet", "android-sdk")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def android_sdk_present?(root)
|
|
116
|
+
return false if root.to_s.strip.empty?
|
|
117
|
+
|
|
118
|
+
Dir.exist?(File.join(root, "platform-tools")) ||
|
|
119
|
+
Dir.exist?(File.join(root, "cmdline-tools")) ||
|
|
120
|
+
Dir.exist?(File.join(root, "platforms"))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def default_android_sdk_location
|
|
124
|
+
if windows_host?
|
|
125
|
+
local = ENV["LOCALAPPDATA"].to_s
|
|
126
|
+
return File.join(local, "Android", "Sdk") unless local.empty?
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
elsif macos_host?
|
|
130
|
+
File.join(Dir.home, "Library", "Android", "sdk")
|
|
131
|
+
else
|
|
132
|
+
File.join(Dir.home, "Android", "Sdk")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ensure_java!(fix:, verbose: false)
|
|
137
|
+
java = current_java_info
|
|
138
|
+
return java if java && java[:major] >= MINIMUM_JAVA_MAJOR
|
|
139
|
+
|
|
140
|
+
return nil unless fix
|
|
141
|
+
|
|
142
|
+
manager = system_package_manager
|
|
143
|
+
package = manager && JDK_PACKAGES[manager[:id]]
|
|
144
|
+
return nil unless package
|
|
145
|
+
|
|
146
|
+
if manager[:id] == :winget
|
|
147
|
+
run_privileged_command(*manager[:install], package, verbose: verbose)
|
|
148
|
+
else
|
|
149
|
+
run_privileged_command(*manager[:install], package, verbose: verbose)
|
|
150
|
+
end
|
|
151
|
+
current_java_info
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def current_java_info
|
|
155
|
+
java = which_command("java") || bundled_java_candidate
|
|
156
|
+
return nil unless java
|
|
157
|
+
|
|
158
|
+
output, status = Open3.capture2e(java, "-version")
|
|
159
|
+
return nil unless status.success?
|
|
160
|
+
|
|
161
|
+
major = parse_java_major(output)
|
|
162
|
+
return nil unless major
|
|
163
|
+
|
|
164
|
+
{ path: java, major: major, version: output.lines.first.to_s.strip }
|
|
165
|
+
rescue StandardError
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def bundled_java_candidate
|
|
170
|
+
if macos_host?
|
|
171
|
+
brew_java = "/opt/homebrew/opt/openjdk@17/bin/java"
|
|
172
|
+
return brew_java if File.executable?(brew_java)
|
|
173
|
+
end
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_java_major(output)
|
|
178
|
+
match = output[/version "(\d+)(?:\.(\d+))?/, 1]
|
|
179
|
+
return nil unless match
|
|
180
|
+
|
|
181
|
+
major = match.to_i
|
|
182
|
+
# "1.8" style versions report the major in the second group.
|
|
183
|
+
major = output[/version "1\.(\d+)/, 1].to_i if major == 1
|
|
184
|
+
major
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def detect_java_home
|
|
188
|
+
if macos_host?
|
|
189
|
+
output, status = Open3.capture2e("/usr/libexec/java_home", "-v", MINIMUM_JAVA_MAJOR.to_s)
|
|
190
|
+
return output.strip if status.success? && !output.strip.empty?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
java = which_command("java")
|
|
194
|
+
return nil unless java
|
|
195
|
+
|
|
196
|
+
resolved = File.realpath(java) rescue java
|
|
197
|
+
home = File.expand_path("../..", resolved)
|
|
198
|
+
File.directory?(File.join(home, "bin")) ? home : nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def install_managed_android_sdk(java, verbose: false)
|
|
202
|
+
sdk_root = managed_android_sdk_root
|
|
203
|
+
tools_dir = File.join(sdk_root, "cmdline-tools", "latest")
|
|
204
|
+
return sdk_root if File.executable?(sdkmanager_bin(sdk_root))
|
|
205
|
+
|
|
206
|
+
archive_name = "commandlinetools-#{cmdline_tools_os}-#{CMDLINE_TOOLS_VERSION}_latest.zip"
|
|
207
|
+
FileUtils.mkdir_p(sdk_root)
|
|
208
|
+
Dir.mktmpdir("ruflet-android-") do |tmp|
|
|
209
|
+
archive = File.join(tmp, archive_name)
|
|
210
|
+
puts " Downloading Android command-line tools (#{archive_name})"
|
|
211
|
+
download_file("#{CMDLINE_TOOLS_BASE}/#{archive_name}", archive)
|
|
212
|
+
extract_archive(archive, tmp)
|
|
213
|
+
extracted = File.join(tmp, "cmdline-tools")
|
|
214
|
+
raise "cmdline-tools missing from archive" unless Dir.exist?(extracted)
|
|
215
|
+
|
|
216
|
+
FileUtils.rm_rf(tools_dir)
|
|
217
|
+
FileUtils.mkdir_p(File.dirname(tools_dir))
|
|
218
|
+
FileUtils.mv(extracted, tools_dir)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
File.executable?(sdkmanager_bin(sdk_root)) ? sdk_root : nil
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
warn " Android SDK download failed: #{e.class}: #{e.message}"
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def cmdline_tools_os
|
|
228
|
+
return "win" if windows_host?
|
|
229
|
+
return "mac" if macos_host?
|
|
230
|
+
|
|
231
|
+
"linux"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def sdkmanager_bin(sdk_root)
|
|
235
|
+
name = windows_host? ? "sdkmanager.bat" : "sdkmanager"
|
|
236
|
+
File.join(sdk_root, "cmdline-tools", "latest", "bin", name)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def ensure_android_packages(sdk_root, java, verbose: false)
|
|
240
|
+
sdkmanager = sdkmanager_bin(sdk_root)
|
|
241
|
+
return unless File.executable?(sdkmanager)
|
|
242
|
+
|
|
243
|
+
packages = ["platform-tools", "platforms;#{ANDROID_PLATFORM}", "build-tools;#{ANDROID_BUILD_TOOLS}"]
|
|
244
|
+
missing = packages.reject { |package| android_package_installed?(sdk_root, package) }
|
|
245
|
+
return if missing.empty?
|
|
246
|
+
|
|
247
|
+
puts " Installing Android packages: #{missing.join(', ')}"
|
|
248
|
+
env = sdkmanager_env(java)
|
|
249
|
+
output = verbose ? $stdout : File::NULL
|
|
250
|
+
system(env, sdkmanager, "--sdk_root=#{sdk_root}", *missing, out: output, err: $stderr)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def android_package_installed?(sdk_root, package)
|
|
254
|
+
case package
|
|
255
|
+
when "platform-tools"
|
|
256
|
+
Dir.exist?(File.join(sdk_root, "platform-tools"))
|
|
257
|
+
when /\Aplatforms;(.+)\z/
|
|
258
|
+
Dir.exist?(File.join(sdk_root, "platforms", Regexp.last_match(1)))
|
|
259
|
+
when /\Abuild-tools;(.+)\z/
|
|
260
|
+
Dir.exist?(File.join(sdk_root, "build-tools", Regexp.last_match(1)))
|
|
261
|
+
else
|
|
262
|
+
false
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def accept_android_licenses(sdk_root, java, verbose: false)
|
|
267
|
+
sdkmanager = sdkmanager_bin(sdk_root)
|
|
268
|
+
return unless File.executable?(sdkmanager)
|
|
269
|
+
|
|
270
|
+
puts " Accepting Android SDK licenses"
|
|
271
|
+
env = sdkmanager_env(java)
|
|
272
|
+
Open3.popen2e(env, sdkmanager, "--sdk_root=#{sdk_root}", "--licenses") do |stdin, stdout, wait|
|
|
273
|
+
begin
|
|
274
|
+
stdin.write("y\n" * 64)
|
|
275
|
+
stdin.close
|
|
276
|
+
rescue Errno::EPIPE
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
stdout.each_line { |line| puts line if verbose }
|
|
280
|
+
wait.value
|
|
281
|
+
end
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
warn " License acceptance failed: #{e.class}: #{e.message}"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def sdkmanager_env(java)
|
|
287
|
+
env = {}
|
|
288
|
+
java_home = detect_java_home
|
|
289
|
+
env["JAVA_HOME"] = java_home if java_home
|
|
290
|
+
if java && java[:path]
|
|
291
|
+
env["PATH"] = "#{File.dirname(java[:path])}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}"
|
|
292
|
+
end
|
|
293
|
+
env
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def jdk_install_hint
|
|
297
|
+
manager = system_package_manager
|
|
298
|
+
package = manager && JDK_PACKAGES[manager[:id]]
|
|
299
|
+
return "#{(sudo_prefix + manager[:install]).join(' ')} #{package}" if package
|
|
300
|
+
|
|
301
|
+
"install a JDK #{MINIMUM_JAVA_MAJOR}+ (e.g. Temurin: https://adoptium.net)"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -12,6 +12,8 @@ module Ruflet
|
|
|
12
12
|
module CLI
|
|
13
13
|
module BuildCommand
|
|
14
14
|
include FlutterSdk
|
|
15
|
+
include EnvironmentSetup
|
|
16
|
+
include AndroidSdk
|
|
15
17
|
CLIENT_EXTENSION_MAP = {
|
|
16
18
|
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
17
19
|
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
@@ -31,7 +33,99 @@ module Ruflet
|
|
|
31
33
|
"webview" => { package: "flet_webview", alias: "ruflet_webview" }
|
|
32
34
|
}.freeze
|
|
33
35
|
|
|
36
|
+
SERVICE_EXTENSION_MAP = {
|
|
37
|
+
"camera" => %w[camera permission_handler],
|
|
38
|
+
"microphone" => %w[audio_recorder permission_handler],
|
|
39
|
+
"location" => %w[geolocator permission_handler],
|
|
40
|
+
"motion" => %w[permission_handler]
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
DEFAULT_SERVICE_NATIVE_REQUIREMENTS = {
|
|
44
|
+
"camera" => {
|
|
45
|
+
android_permissions: ["android.permission.CAMERA"],
|
|
46
|
+
ios_info: {
|
|
47
|
+
"NSCameraUsageDescription" => "Camera access is required for camera experiences."
|
|
48
|
+
},
|
|
49
|
+
macos_info: {
|
|
50
|
+
"NSCameraUsageDescription" => "Camera access is required for camera experiences."
|
|
51
|
+
},
|
|
52
|
+
macos_entitlements: {
|
|
53
|
+
"com.apple.security.device.camera" => true
|
|
54
|
+
},
|
|
55
|
+
ios_permission_definitions: %w[
|
|
56
|
+
PERMISSION_CAMERA=1
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"microphone" => {
|
|
60
|
+
android_permissions: ["android.permission.RECORD_AUDIO"],
|
|
61
|
+
ios_info: {
|
|
62
|
+
"NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
|
|
63
|
+
},
|
|
64
|
+
macos_info: {
|
|
65
|
+
"NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
|
|
66
|
+
},
|
|
67
|
+
macos_entitlements: {
|
|
68
|
+
"com.apple.security.device.audio-input" => true
|
|
69
|
+
},
|
|
70
|
+
ios_permission_definitions: %w[
|
|
71
|
+
PERMISSION_MICROPHONE=1
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"motion" => {
|
|
75
|
+
ios_info: {
|
|
76
|
+
"NSMotionUsageDescription" => "Motion access is required for motion and sensor readings."
|
|
77
|
+
},
|
|
78
|
+
ios_permission_definitions: %w[
|
|
79
|
+
PERMISSION_SENSORS=1
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"location" => {
|
|
83
|
+
android_permissions: [
|
|
84
|
+
"android.permission.ACCESS_FINE_LOCATION",
|
|
85
|
+
"android.permission.ACCESS_COARSE_LOCATION"
|
|
86
|
+
],
|
|
87
|
+
ios_info: {
|
|
88
|
+
"NSLocationWhenInUseUsageDescription" => "Location access is required for location-aware experiences."
|
|
89
|
+
},
|
|
90
|
+
macos_info: {
|
|
91
|
+
"NSLocationUsageDescription" => "Location access is required for location-aware experiences."
|
|
92
|
+
},
|
|
93
|
+
macos_entitlements: {
|
|
94
|
+
"com.apple.security.personal-information.location" => true
|
|
95
|
+
},
|
|
96
|
+
ios_permission_definitions: %w[
|
|
97
|
+
PERMISSION_LOCATION=1
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}.freeze
|
|
101
|
+
|
|
34
102
|
def command_build(args)
|
|
103
|
+
with_project_build_lock { run_build_command(args) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Concurrent builds share build/client and corrupt each other's state
|
|
107
|
+
# (missing app.dill, Xcode build.db I/O errors). flock is released
|
|
108
|
+
# automatically when the process exits, so the lock cannot go stale.
|
|
109
|
+
def with_project_build_lock
|
|
110
|
+
lock_dir = File.join(Dir.pwd, "build")
|
|
111
|
+
FileUtils.mkdir_p(lock_dir)
|
|
112
|
+
lock_path = File.join(lock_dir, ".ruflet_build.lock")
|
|
113
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |file|
|
|
114
|
+
unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
115
|
+
owner = file.read.to_s.strip
|
|
116
|
+
warn "Another ruflet build is already running for this project#{owner.empty? ? '' : " (#{owner})"}."
|
|
117
|
+
warn "Concurrent builds share the same build directory and corrupt each other."
|
|
118
|
+
warn "Wait for it to finish, then retry."
|
|
119
|
+
return 1
|
|
120
|
+
end
|
|
121
|
+
file.truncate(0)
|
|
122
|
+
file.write("pid=#{Process.pid} started=#{Time.now}")
|
|
123
|
+
file.flush
|
|
124
|
+
yield
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run_build_command(args)
|
|
35
129
|
self_contained = args.delete("--self")
|
|
36
130
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
37
131
|
platform = (args.shift || "").downcase
|
|
@@ -159,6 +253,9 @@ module Ruflet
|
|
|
159
253
|
end
|
|
160
254
|
|
|
161
255
|
def build_tool_env(env, platform, client_dir = nil)
|
|
256
|
+
if %w[android apk aab appbundle].include?(platform)
|
|
257
|
+
return android_build_env(unbundled_command_env(env))
|
|
258
|
+
end
|
|
162
259
|
return env unless %w[ios macos].include?(platform)
|
|
163
260
|
|
|
164
261
|
apple_env = unbundled_command_env(env)
|
|
@@ -476,7 +573,20 @@ module Ruflet
|
|
|
476
573
|
end
|
|
477
574
|
|
|
478
575
|
def unbundled_command_env(env)
|
|
479
|
-
env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_") }
|
|
576
|
+
command_env = env.reject { |key, _value| key.start_with?("BUNDLE_") || key == "RUBYOPT" || key == "RUBYLIB" || key.start_with?("GEM_") }
|
|
577
|
+
ensure_utf8_locale(command_env)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# CocoaPods refuses to run in a non-UTF-8 terminal and Xcode project
|
|
581
|
+
# files contain UTF-8; guarantee a UTF-8 locale for child tools.
|
|
582
|
+
def ensure_utf8_locale(env)
|
|
583
|
+
locale = env["LC_ALL"].to_s
|
|
584
|
+
locale = env["LANG"].to_s if locale.empty?
|
|
585
|
+
return env if locale.downcase.include?("utf-8") || locale.downcase.include?("utf8")
|
|
586
|
+
|
|
587
|
+
env["LANG"] = "en_US.UTF-8"
|
|
588
|
+
env["LC_ALL"] = "en_US.UTF-8"
|
|
589
|
+
env
|
|
480
590
|
end
|
|
481
591
|
|
|
482
592
|
def run_external_command(env, *cmd, chdir:, unbundled: false)
|
|
@@ -989,8 +1099,11 @@ module Ruflet
|
|
|
989
1099
|
end
|
|
990
1100
|
|
|
991
1101
|
def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
|
|
992
|
-
|
|
993
|
-
|
|
1102
|
+
service_definitions = load_service_definitions(client_dir)
|
|
1103
|
+
service_extension_keys = service_definitions.keys.flat_map { |key| Array(SERVICE_EXTENSION_MAP[key]) }.uniq
|
|
1104
|
+
protected_extension_keys = SERVICE_EXTENSION_MAP.values.flatten.uniq
|
|
1105
|
+
configured_extension_keys = Array(config["extensions"]).map { |value| normalize_extension_key(value) }.compact
|
|
1106
|
+
extension_keys = ((configured_extension_keys - protected_extension_keys) | service_extension_keys).uniq
|
|
994
1107
|
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
995
1108
|
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
996
1109
|
|
|
@@ -1003,6 +1116,193 @@ module Ruflet
|
|
|
1003
1116
|
sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1004
1117
|
prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1005
1118
|
end
|
|
1119
|
+
apply_service_native_requirements(client_dir, service_definitions.keys, service_definitions)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def apply_service_native_requirements(client_dir, extension_keys, service_definitions = load_service_definitions(client_dir))
|
|
1123
|
+
service_requirements = service_native_requirements(service_definitions)
|
|
1124
|
+
stale_keys = service_requirements.keys - extension_keys
|
|
1125
|
+
remove_service_native_requirements(client_dir, stale_keys, service_requirements)
|
|
1126
|
+
|
|
1127
|
+
requirements = merge_service_native_requirements(extension_keys, service_requirements)
|
|
1128
|
+
sync_ios_permission_definitions(
|
|
1129
|
+
File.join(client_dir, "ios", "Podfile"),
|
|
1130
|
+
Array(requirements[:ios_permission_definitions])
|
|
1131
|
+
)
|
|
1132
|
+
return if requirements.empty?
|
|
1133
|
+
|
|
1134
|
+
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1135
|
+
Array(requirements[:android_permissions]).each do |permission|
|
|
1136
|
+
ensure_android_permission(android_manifest, permission)
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
ios_info = File.join(client_dir, "ios", "Runner", "Info.plist")
|
|
1140
|
+
Hash(requirements[:ios_info]).each do |key, value|
|
|
1141
|
+
ensure_plist_string(ios_info, key, value)
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
macos_info = File.join(client_dir, "macos", "Runner", "Info.plist")
|
|
1145
|
+
Hash(requirements[:macos_info]).each do |key, value|
|
|
1146
|
+
ensure_plist_string(macos_info, key, value)
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
%w[DebugProfile Release].each do |name|
|
|
1150
|
+
entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
|
|
1151
|
+
Hash(requirements[:macos_entitlements]).each do |key, value|
|
|
1152
|
+
ensure_plist_boolean(entitlements_path, key, value)
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
def remove_service_native_requirements(client_dir, extension_keys, service_requirements = service_native_requirements(load_service_definitions(client_dir)))
|
|
1158
|
+
requirements = merge_service_native_requirements(extension_keys, service_requirements)
|
|
1159
|
+
return if requirements.empty?
|
|
1160
|
+
|
|
1161
|
+
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
1162
|
+
Array(requirements[:android_permissions]).each do |permission|
|
|
1163
|
+
remove_android_permission(android_manifest, permission)
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
[File.join(client_dir, "ios", "Runner", "Info.plist"), File.join(client_dir, "macos", "Runner", "Info.plist")].each do |path|
|
|
1167
|
+
(Hash(requirements[:ios_info]).keys + Hash(requirements[:macos_info]).keys).uniq.each do |key|
|
|
1168
|
+
remove_plist_entry(path, key)
|
|
1169
|
+
end
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
%w[DebugProfile Release].each do |name|
|
|
1173
|
+
entitlements_path = File.join(client_dir, "macos", "Runner", "#{name}.entitlements")
|
|
1174
|
+
Hash(requirements[:macos_entitlements]).each_key do |key|
|
|
1175
|
+
remove_plist_entry(entitlements_path, key)
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def merge_service_native_requirements(extension_keys, service_requirements = DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
|
|
1181
|
+
extension_keys.each_with_object({}) do |key, memo|
|
|
1182
|
+
requirements = service_requirements[key]
|
|
1183
|
+
next unless requirements
|
|
1184
|
+
|
|
1185
|
+
memo[:android_permissions] ||= []
|
|
1186
|
+
memo[:android_permissions] |= Array(requirements[:android_permissions])
|
|
1187
|
+
memo[:ios_permission_definitions] ||= []
|
|
1188
|
+
memo[:ios_permission_definitions] |= Array(requirements[:ios_permission_definitions])
|
|
1189
|
+
%i[ios_info macos_info macos_entitlements].each do |section|
|
|
1190
|
+
memo[section] ||= {}
|
|
1191
|
+
memo[section].merge!(requirements[section] || {})
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
def load_service_definitions(client_dir = nil)
|
|
1197
|
+
path = services_config_path(client_dir)
|
|
1198
|
+
return {} unless path
|
|
1199
|
+
|
|
1200
|
+
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
1201
|
+
Array(data["services"]).each_with_object({}) do |entry, memo|
|
|
1202
|
+
name, metadata =
|
|
1203
|
+
if entry.is_a?(Hash)
|
|
1204
|
+
entry.first
|
|
1205
|
+
else
|
|
1206
|
+
[entry, {}]
|
|
1207
|
+
end
|
|
1208
|
+
key = normalize_extension_key(name)
|
|
1209
|
+
memo[key] = metadata.is_a?(Hash) ? metadata : {} if key
|
|
1210
|
+
end
|
|
1211
|
+
rescue Psych::Exception => e
|
|
1212
|
+
warn "Could not parse #{path}: #{e.message}"
|
|
1213
|
+
{}
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
def services_config_path(client_dir = nil)
|
|
1217
|
+
candidates = [ENV["RUFLET_SERVICES"], File.expand_path("services.yaml", Dir.pwd)]
|
|
1218
|
+
candidates << File.join(client_dir, "services.yaml") if client_dir
|
|
1219
|
+
candidates.compact.find { |path| File.file?(path) }
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def service_native_requirements(service_definitions)
|
|
1223
|
+
all_keys = (DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys | service_definitions.keys)
|
|
1224
|
+
all_keys.each_with_object({}) do |key, memo|
|
|
1225
|
+
defaults = DEFAULT_SERVICE_NATIVE_REQUIREMENTS[key] || {}
|
|
1226
|
+
metadata = service_definitions[key] || {}
|
|
1227
|
+
native = metadata["native"].is_a?(Hash) ? metadata["native"] : metadata
|
|
1228
|
+
memo[key] = {
|
|
1229
|
+
android_permissions: Array(native["android_permissions"] || defaults[:android_permissions]),
|
|
1230
|
+
ios_permission_definitions: Array(native["ios_permission_definitions"] || defaults[:ios_permission_definitions]),
|
|
1231
|
+
ios_info: native["ios_info"].is_a?(Hash) ? native["ios_info"] : (defaults[:ios_info] || {}),
|
|
1232
|
+
macos_info: native["macos_info"].is_a?(Hash) ? native["macos_info"] : (defaults[:macos_info] || {}),
|
|
1233
|
+
macos_entitlements: native["macos_entitlements"].is_a?(Hash) ? native["macos_entitlements"] : (defaults[:macos_entitlements] || {})
|
|
1234
|
+
}
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
def sync_ios_permission_definitions(path, definitions)
|
|
1239
|
+
return unless File.file?(path)
|
|
1240
|
+
|
|
1241
|
+
content = File.read(path)
|
|
1242
|
+
marker_pattern = %r{\n?\s*# BEGIN RUFLET PERMISSION DEFINITIONS.*?# END RUFLET PERMISSION DEFINITIONS\n?}m
|
|
1243
|
+
updated = content.gsub(marker_pattern, "\n")
|
|
1244
|
+
definitions = Array(definitions).map(&:to_s).reject(&:empty?).uniq.sort
|
|
1245
|
+
if definitions.any?
|
|
1246
|
+
block = <<~RUBY.chomp
|
|
1247
|
+
# BEGIN RUFLET PERMISSION DEFINITIONS
|
|
1248
|
+
target.build_configurations.each do |config|
|
|
1249
|
+
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
|
1250
|
+
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] += #{definitions.inspect}
|
|
1251
|
+
end
|
|
1252
|
+
# END RUFLET PERMISSION DEFINITIONS
|
|
1253
|
+
RUBY
|
|
1254
|
+
updated = updated.sub(
|
|
1255
|
+
/^(\s*)flutter_additional_ios_build_settings\(target\)\s*$/,
|
|
1256
|
+
"\\0\n\n#{block}"
|
|
1257
|
+
)
|
|
1258
|
+
end
|
|
1259
|
+
File.write(path, updated) unless updated == content
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
def ensure_android_permission(path, permission)
|
|
1263
|
+
return unless File.file?(path)
|
|
1264
|
+
|
|
1265
|
+
content = File.read(path)
|
|
1266
|
+
return if content.include?(permission)
|
|
1267
|
+
|
|
1268
|
+
permission_line = %( <uses-permission android:name="#{xml_escape(permission)}"/>\n)
|
|
1269
|
+
updated = content.sub(/(<manifest\b[^>]*>\s*)/m) { "#{Regexp.last_match(1)}#{permission_line}" }
|
|
1270
|
+
File.write(path, updated == content ? "#{permission_line}#{content}" : updated)
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
def remove_android_permission(path, permission)
|
|
1274
|
+
return unless File.file?(path)
|
|
1275
|
+
|
|
1276
|
+
content = File.read(path)
|
|
1277
|
+
updated = content.gsub(%r{^\s*<uses-permission\s+android:name="#{Regexp.escape(permission)}"\s*/>\s*\n?}, "")
|
|
1278
|
+
File.write(path, updated) unless updated == content
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
def ensure_plist_string(path, key, value)
|
|
1282
|
+
ensure_plist_entry(path, key, "<string>#{xml_escape(value)}</string>")
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
def ensure_plist_boolean(path, key, value)
|
|
1286
|
+
ensure_plist_entry(path, key, value ? "<true/>" : "<false/>")
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
def ensure_plist_entry(path, key, value_xml)
|
|
1290
|
+
return unless File.file?(path)
|
|
1291
|
+
|
|
1292
|
+
content = File.read(path)
|
|
1293
|
+
return if content.include?("<key>#{key}</key>")
|
|
1294
|
+
|
|
1295
|
+
entry = "\t<key>#{key}</key>\n\t#{value_xml}\n"
|
|
1296
|
+
updated = content.sub(%r{</dict>}, "#{entry}</dict>")
|
|
1297
|
+
File.write(path, updated == content ? "#{content}\n#{entry}" : updated)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def remove_plist_entry(path, key)
|
|
1301
|
+
return unless File.file?(path)
|
|
1302
|
+
|
|
1303
|
+
content = File.read(path)
|
|
1304
|
+
updated = content.gsub(%r{\s*<key>#{Regexp.escape(key)}</key>\s*<(?:string>.*?</string|true/|false/)>\s*}m, "\n")
|
|
1305
|
+
File.write(path, updated) unless updated == content
|
|
1006
1306
|
end
|
|
1007
1307
|
|
|
1008
1308
|
def clear_flutter_build_state(client_dir, verbose: false)
|
|
@@ -1077,11 +1377,46 @@ module Ruflet
|
|
|
1077
1377
|
write_pubspec_yaml(pubspec_path, data)
|
|
1078
1378
|
end
|
|
1079
1379
|
|
|
1380
|
+
RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.5"
|
|
1381
|
+
|
|
1080
1382
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1081
|
-
local_path = explicit_local_ruby_runtime_path
|
|
1383
|
+
local_path = explicit_local_ruby_runtime_path || repo_checkout_ruby_runtime_path
|
|
1082
1384
|
return { "path" => local_path } if local_path
|
|
1083
1385
|
|
|
1084
|
-
|
|
1386
|
+
template_dependency = template_client_pubspec_dependencies["ruby_runtime"]
|
|
1387
|
+
return template_dependency if usable_ruby_runtime_dependency?(template_dependency)
|
|
1388
|
+
|
|
1389
|
+
return current_dependency if usable_ruby_runtime_dependency?(current_dependency)
|
|
1390
|
+
|
|
1391
|
+
RUBY_RUNTIME_FALLBACK_REQUIREMENT
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
# In a ruflet repo checkout the plugin lives next to templates/; build
|
|
1395
|
+
# against it so framework changes are exercised without publishing.
|
|
1396
|
+
def repo_checkout_ruby_runtime_path
|
|
1397
|
+
template_root =
|
|
1398
|
+
if Ruflet::CLI.respond_to?(:resolve_ruflet_client_template_root, true)
|
|
1399
|
+
Ruflet::CLI.send(:resolve_ruflet_client_template_root)
|
|
1400
|
+
end
|
|
1401
|
+
return nil unless template_root
|
|
1402
|
+
|
|
1403
|
+
candidate = File.expand_path(File.join(template_root, "..", "..", "ruby_runtime"))
|
|
1404
|
+
File.file?(File.join(candidate, "pubspec.yaml")) ? candidate : nil
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
# Relative path dependencies only resolve from the directory the
|
|
1408
|
+
# template was authored in — never copy them into a user's client.
|
|
1409
|
+
def usable_ruby_runtime_dependency?(dependency)
|
|
1410
|
+
case dependency
|
|
1411
|
+
when nil then false
|
|
1412
|
+
when Hash
|
|
1413
|
+
path = dependency["path"] || dependency[:path]
|
|
1414
|
+
return true if path.nil? # hosted/git table form
|
|
1415
|
+
|
|
1416
|
+
Pathname.new(path.to_s).absolute? && File.file?(File.join(path, "pubspec.yaml"))
|
|
1417
|
+
else
|
|
1418
|
+
!dependency.to_s.strip.empty?
|
|
1419
|
+
end
|
|
1085
1420
|
end
|
|
1086
1421
|
|
|
1087
1422
|
def explicit_local_ruby_runtime_path
|
|
@@ -1122,6 +1457,27 @@ module Ruflet
|
|
|
1122
1457
|
FileUtils.cp(source, destination)
|
|
1123
1458
|
build_log(verbose, "refreshed template file #{relative_path}")
|
|
1124
1459
|
end
|
|
1460
|
+
|
|
1461
|
+
repair_legacy_self_contained_bootstrap(client_dir, verbose: verbose)
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
def repair_legacy_self_contained_bootstrap(client_dir, verbose: false)
|
|
1465
|
+
path = File.join(client_dir, "lib", "main.self.dart")
|
|
1466
|
+
return unless File.file?(path)
|
|
1467
|
+
|
|
1468
|
+
content = File.read(path)
|
|
1469
|
+
updated = content.gsub(
|
|
1470
|
+
/^\s*await RubyRuntime\.eval\("ENV\['RUFLET_DEBUG'\].*?\n/,
|
|
1471
|
+
""
|
|
1472
|
+
)
|
|
1473
|
+
updated = updated.gsub(
|
|
1474
|
+
/^\s*final digestLength = await RubyRuntime\.eval\(\n.*?^\s*\);\n\s*debugPrint\('Embedded Digest::SHA1 bytesize: \$digestLength'\);\n/m,
|
|
1475
|
+
""
|
|
1476
|
+
)
|
|
1477
|
+
return if updated == content
|
|
1478
|
+
|
|
1479
|
+
File.write(path, updated)
|
|
1480
|
+
build_log(verbose, "removed legacy pre-server RubyRuntime.eval diagnostics")
|
|
1125
1481
|
end
|
|
1126
1482
|
|
|
1127
1483
|
def write_pubspec_yaml(path, data)
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
output = verbose ? $stdout : File::NULL
|
|
216
|
+
system(*full, out: output, err: $stderr)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def sudo_prefix
|
|
220
|
+
return [] if windows_host? || macos_host?
|
|
221
|
+
return [] if Process.respond_to?(:uid) && Process.uid.zero?
|
|
222
|
+
return ["sudo"] if which_command("sudo")
|
|
223
|
+
|
|
224
|
+
[]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def unsupported_host_message
|
|
228
|
+
os = RbConfig::CONFIG["host_os"]
|
|
229
|
+
if os.match?(/linux/i) && machine_arch.match?(/arm|aarch64/)
|
|
230
|
+
"Flutter publishes no official Linux #{machine_arch} build; use an x64 machine " \
|
|
231
|
+
"or build Flutter from source (https://github.com/flutter/flutter)."
|
|
232
|
+
else
|
|
233
|
+
"unsupported host platform: #{os}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
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,14 @@ module Ruflet
|
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
puts " Flutter: #{flutter_version_summary(tools)}"
|
|
49
|
-
|
|
57
|
+
environment_issues += android_environment_setup!(fix: !!fix, verbose: !!verbose)
|
|
58
|
+
ok = system(android_build_env(tools[:env]), tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
|
|
50
59
|
status = $?.exitstatus if $?
|
|
51
60
|
status ||= ok ? 0 : 1
|
|
61
|
+
if environment_issues.any?
|
|
62
|
+
warn "Unresolved environment issues: #{environment_issues.join('; ')}"
|
|
63
|
+
status = 1 if status.zero?
|
|
64
|
+
end
|
|
52
65
|
status
|
|
53
66
|
end
|
|
54
67
|
|
|
@@ -289,6 +289,8 @@ module Ruflet
|
|
|
289
289
|
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
290
290
|
|
|
291
291
|
nil
|
|
292
|
+
rescue OpenSSL::SSL::SSLError
|
|
293
|
+
JSON.parse(curl_get(url, headers: ["User-Agent: ruflet-cli"]))
|
|
292
294
|
end
|
|
293
295
|
|
|
294
296
|
def pick_release(manifest, version: nil, revision: nil, channel: nil)
|
|
@@ -320,7 +322,10 @@ module Ruflet
|
|
|
320
322
|
if os.match?(/darwin/i)
|
|
321
323
|
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
322
324
|
end
|
|
323
|
-
|
|
325
|
+
if os.match?(/linux/i)
|
|
326
|
+
# Flutter publishes Linux archives for x64 only.
|
|
327
|
+
return machine_arch.match?(/arm|aarch64/) ? nil : "linux"
|
|
328
|
+
end
|
|
324
329
|
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
325
330
|
|
|
326
331
|
nil
|
|
@@ -380,6 +385,8 @@ module Ruflet
|
|
|
380
385
|
end
|
|
381
386
|
end
|
|
382
387
|
end
|
|
388
|
+
rescue OpenSSL::SSL::SSLError
|
|
389
|
+
curl_download(url, destination)
|
|
383
390
|
end
|
|
384
391
|
|
|
385
392
|
def extract_archive(archive, destination)
|
|
@@ -387,14 +394,25 @@ module Ruflet
|
|
|
387
394
|
if windows_host?
|
|
388
395
|
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
389
396
|
end
|
|
397
|
+
|
|
398
|
+
require_extract_tool!("unzip")
|
|
390
399
|
return system("unzip", "-oq", archive, "-d", destination)
|
|
391
400
|
end
|
|
392
401
|
|
|
393
402
|
if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
|
|
403
|
+
require_extract_tool!("tar")
|
|
404
|
+
require_extract_tool!("xz") if archive.end_with?(".tar.xz") && !windows_host?
|
|
394
405
|
return system("tar", "-xf", archive, "-C", destination)
|
|
395
406
|
end
|
|
396
407
|
|
|
397
|
-
|
|
408
|
+
raise "Unsupported archive format: #{File.basename(archive)}"
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def require_extract_tool!(name)
|
|
412
|
+
return if which_command(name)
|
|
413
|
+
|
|
414
|
+
raise "`#{name}` is required to extract the Flutter SDK but was not found. " \
|
|
415
|
+
"Run `ruflet doctor --fix` to install the missing system tools."
|
|
398
416
|
end
|
|
399
417
|
end
|
|
400
418
|
end
|
|
@@ -84,7 +84,20 @@ module Ruflet
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
cached_template = cached_ruflet_client_template_root
|
|
87
|
-
|
|
87
|
+
if Dir.exist?(cached_template)
|
|
88
|
+
return cached_template if cached_template_current?(cached_template)
|
|
89
|
+
|
|
90
|
+
# The cache was written by a different ruflet version; stale
|
|
91
|
+
# framework Dart breaks newer asset layouts. Refresh, but never
|
|
92
|
+
# block a build on the network: fall back to the stale copy loudly.
|
|
93
|
+
refreshed = download_ruflet_template(force: true)
|
|
94
|
+
return refreshed if refreshed && Dir.exist?(refreshed)
|
|
95
|
+
|
|
96
|
+
warn "ruflet: could not refresh the Flutter client template cache; " \
|
|
97
|
+
"using a stale copy from another ruflet version at #{cached_template}. " \
|
|
98
|
+
"Delete it or run with network access to update."
|
|
99
|
+
return cached_template
|
|
100
|
+
end
|
|
88
101
|
|
|
89
102
|
[
|
|
90
103
|
File.expand_path("../../../ruflet_client", __dir__),
|
|
@@ -112,6 +125,21 @@ module Ruflet
|
|
|
112
125
|
File.join(template_cache_root, "ruflet_flutter_template")
|
|
113
126
|
end
|
|
114
127
|
|
|
128
|
+
TEMPLATE_CACHE_STAMP = ".ruflet_template_version"
|
|
129
|
+
|
|
130
|
+
def cached_template_current?(target)
|
|
131
|
+
stamp = File.join(target, TEMPLATE_CACHE_STAMP)
|
|
132
|
+
File.file?(stamp) && File.read(stamp).strip == Ruflet::VERSION
|
|
133
|
+
rescue StandardError
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def write_template_cache_stamp(target)
|
|
138
|
+
File.write(File.join(target, TEMPLATE_CACHE_STAMP), Ruflet::VERSION)
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
115
143
|
def cached_ruby_runtime_root
|
|
116
144
|
File.join(cache_root, "ruby_runtime")
|
|
117
145
|
end
|
|
@@ -150,6 +178,7 @@ module Ruflet
|
|
|
150
178
|
FileUtils.cp_r(source, target)
|
|
151
179
|
end
|
|
152
180
|
|
|
181
|
+
write_template_cache_stamp(target)
|
|
153
182
|
target
|
|
154
183
|
rescue StandardError => e
|
|
155
184
|
warn "Failed to fetch Ruflet template: #{e.class}: #{e.message}"
|
|
@@ -195,12 +224,11 @@ module Ruflet
|
|
|
195
224
|
# Example: https://api.example.com
|
|
196
225
|
backend_url: ""
|
|
197
226
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
services: []
|
|
227
|
+
# Flutter extension packages included in the managed client.
|
|
228
|
+
# Permission-backed extensions such as camera, audio_recorder,
|
|
229
|
+
# geolocator, and permission_handler are activated by services.yaml
|
|
230
|
+
# and ignored here.
|
|
231
|
+
extensions: []
|
|
204
232
|
|
|
205
233
|
# Build assets configuration consumed by `ruflet build`.
|
|
206
234
|
# Paths are relative to this file unless absolute.
|
|
@@ -215,6 +243,11 @@ module Ruflet
|
|
|
215
243
|
icon_background: "#FFFFFF"
|
|
216
244
|
theme_color: "#FFFFFF"
|
|
217
245
|
YAML
|
|
246
|
+
|
|
247
|
+
File.write(File.join(root, "services.yaml"), <<~YAML)
|
|
248
|
+
# Protected device access requested by this app.
|
|
249
|
+
services: []
|
|
250
|
+
YAML
|
|
218
251
|
end
|
|
219
252
|
|
|
220
253
|
def copy_default_project_assets(root)
|
|
@@ -349,13 +349,30 @@ module Ruflet
|
|
|
349
349
|
end
|
|
350
350
|
|
|
351
351
|
def project_run_requires_managed_client?
|
|
352
|
-
|
|
352
|
+
extensions = Array(project_run_config["extensions"]).map { |value| normalize_run_extension_key(value) }.compact
|
|
353
|
+
protected_extensions =
|
|
354
|
+
if defined?(Ruflet::CLI::BuildCommand::SERVICE_EXTENSION_MAP)
|
|
355
|
+
Ruflet::CLI::BuildCommand::SERVICE_EXTENSION_MAP.values.flatten
|
|
356
|
+
else
|
|
357
|
+
[]
|
|
358
|
+
end
|
|
359
|
+
return true if (extensions - protected_extensions).any?
|
|
353
360
|
|
|
354
|
-
services =
|
|
361
|
+
services =
|
|
362
|
+
if respond_to?(:load_service_definitions, true)
|
|
363
|
+
send(:load_service_definitions).keys
|
|
364
|
+
else
|
|
365
|
+
[]
|
|
366
|
+
end
|
|
355
367
|
return false if services.empty?
|
|
356
368
|
|
|
357
|
-
|
|
358
|
-
|
|
369
|
+
known_services =
|
|
370
|
+
if defined?(Ruflet::CLI::BuildCommand::DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
|
|
371
|
+
Ruflet::CLI::BuildCommand::DEFAULT_SERVICE_NATIVE_REQUIREMENTS.keys
|
|
372
|
+
else
|
|
373
|
+
[]
|
|
374
|
+
end
|
|
375
|
+
(services & known_services).any?
|
|
359
376
|
end
|
|
360
377
|
|
|
361
378
|
def project_run_config
|
|
@@ -653,6 +670,8 @@ module Ruflet
|
|
|
653
670
|
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
654
671
|
|
|
655
672
|
raise "GitHub API failed (#{response.code})"
|
|
673
|
+
rescue OpenSSL::SSL::SSLError
|
|
674
|
+
JSON.parse(curl_get(url, headers: ["Accept: application/vnd.github+json", "User-Agent: ruflet-cli"]))
|
|
656
675
|
end
|
|
657
676
|
|
|
658
677
|
def download_file(url, destination, limit: 5)
|
|
@@ -674,6 +693,31 @@ module Ruflet
|
|
|
674
693
|
end
|
|
675
694
|
end
|
|
676
695
|
end
|
|
696
|
+
rescue OpenSSL::SSL::SSLError
|
|
697
|
+
curl_download(url, destination)
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def curl_get(url, headers: [])
|
|
701
|
+
args = ["curl", "-fsSL"]
|
|
702
|
+
headers.each do |header|
|
|
703
|
+
args += ["-H", header]
|
|
704
|
+
end
|
|
705
|
+
args << url
|
|
706
|
+
output = IO.popen(args, err: File::NULL, &:read)
|
|
707
|
+
return output if $?.success?
|
|
708
|
+
|
|
709
|
+
raise "curl failed while requesting #{url}"
|
|
710
|
+
rescue Errno::ENOENT
|
|
711
|
+
raise "Ruby SSL verification failed and curl is not available"
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def curl_download(url, destination)
|
|
715
|
+
ok = system("curl", "-fL", "--retry", "2", "--connect-timeout", "20", "-o", destination, url, out: File::NULL, err: File::NULL)
|
|
716
|
+
return destination if ok
|
|
717
|
+
|
|
718
|
+
raise "curl failed while downloading #{url}"
|
|
719
|
+
rescue Errno::ENOENT
|
|
720
|
+
raise "Ruby SSL verification failed and curl is not available"
|
|
677
721
|
end
|
|
678
722
|
|
|
679
723
|
def extract_archive(archive, destination)
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
|
+
# Project files (pbxproj, plists, embedded runtime sources) are UTF-8; never
|
|
6
|
+
# depend on the caller's locale for reading them.
|
|
7
|
+
Encoding.default_external = Encoding::UTF_8 if Encoding.default_external == Encoding::US_ASCII
|
|
8
|
+
|
|
5
9
|
require_relative "version"
|
|
6
10
|
require_relative "cli/templates"
|
|
7
11
|
require_relative "cli/new_command"
|
|
8
12
|
require_relative "cli/flutter_sdk"
|
|
13
|
+
require_relative "cli/environment_setup"
|
|
14
|
+
require_relative "cli/android_sdk"
|
|
9
15
|
require_relative "cli/run_command"
|
|
10
16
|
require_relative "cli/update_command"
|
|
11
17
|
require_relative "cli/build_command"
|
data/lib/ruflet/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -37,7 +37,9 @@ files:
|
|
|
37
37
|
- assets/splash.png
|
|
38
38
|
- bin/ruflet
|
|
39
39
|
- lib/ruflet/cli.rb
|
|
40
|
+
- lib/ruflet/cli/android_sdk.rb
|
|
40
41
|
- lib/ruflet/cli/build_command.rb
|
|
42
|
+
- lib/ruflet/cli/environment_setup.rb
|
|
41
43
|
- lib/ruflet/cli/extra_command.rb
|
|
42
44
|
- lib/ruflet/cli/flutter_sdk.rb
|
|
43
45
|
- lib/ruflet/cli/new_command.rb
|