ruflet 0.0.14 → 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 +229 -22
- 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 +16 -2
- data/lib/ruflet/cli/new_command.rb +40 -7
- data/lib/ruflet/cli/run_command.rb +21 -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,8 +33,30 @@ module Ruflet
|
|
|
31
33
|
"webview" => { package: "flet_webview", alias: "ruflet_webview" }
|
|
32
34
|
}.freeze
|
|
33
35
|
|
|
34
|
-
|
|
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" => {
|
|
36
60
|
android_permissions: ["android.permission.RECORD_AUDIO"],
|
|
37
61
|
ios_info: {
|
|
38
62
|
"NSMicrophoneUsageDescription" => "Microphone access is required for audio recording."
|
|
@@ -42,14 +66,20 @@ module Ruflet
|
|
|
42
66
|
},
|
|
43
67
|
macos_entitlements: {
|
|
44
68
|
"com.apple.security.device.audio-input" => true
|
|
45
|
-
}
|
|
69
|
+
},
|
|
70
|
+
ios_permission_definitions: %w[
|
|
71
|
+
PERMISSION_MICROPHONE=1
|
|
72
|
+
]
|
|
46
73
|
},
|
|
47
|
-
"
|
|
74
|
+
"motion" => {
|
|
48
75
|
ios_info: {
|
|
49
|
-
"NSMotionUsageDescription" => "Motion access is required for
|
|
50
|
-
}
|
|
76
|
+
"NSMotionUsageDescription" => "Motion access is required for motion and sensor readings."
|
|
77
|
+
},
|
|
78
|
+
ios_permission_definitions: %w[
|
|
79
|
+
PERMISSION_SENSORS=1
|
|
80
|
+
]
|
|
51
81
|
},
|
|
52
|
-
"
|
|
82
|
+
"location" => {
|
|
53
83
|
android_permissions: [
|
|
54
84
|
"android.permission.ACCESS_FINE_LOCATION",
|
|
55
85
|
"android.permission.ACCESS_COARSE_LOCATION"
|
|
@@ -62,11 +92,40 @@ module Ruflet
|
|
|
62
92
|
},
|
|
63
93
|
macos_entitlements: {
|
|
64
94
|
"com.apple.security.personal-information.location" => true
|
|
65
|
-
}
|
|
95
|
+
},
|
|
96
|
+
ios_permission_definitions: %w[
|
|
97
|
+
PERMISSION_LOCATION=1
|
|
98
|
+
]
|
|
66
99
|
}
|
|
67
100
|
}.freeze
|
|
68
101
|
|
|
69
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)
|
|
70
129
|
self_contained = args.delete("--self")
|
|
71
130
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
72
131
|
platform = (args.shift || "").downcase
|
|
@@ -194,6 +253,9 @@ module Ruflet
|
|
|
194
253
|
end
|
|
195
254
|
|
|
196
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
|
|
197
259
|
return env unless %w[ios macos].include?(platform)
|
|
198
260
|
|
|
199
261
|
apple_env = unbundled_command_env(env)
|
|
@@ -511,7 +573,20 @@ module Ruflet
|
|
|
511
573
|
end
|
|
512
574
|
|
|
513
575
|
def unbundled_command_env(env)
|
|
514
|
-
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
|
|
515
590
|
end
|
|
516
591
|
|
|
517
592
|
def run_external_command(env, *cmd, chdir:, unbundled: false)
|
|
@@ -1024,8 +1099,11 @@ module Ruflet
|
|
|
1024
1099
|
end
|
|
1025
1100
|
|
|
1026
1101
|
def apply_service_extension_config(client_dir, config = {}, self_contained: @ruflet_self_contained_build)
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
|
1029
1107
|
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
1030
1108
|
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
1031
1109
|
|
|
@@ -1038,14 +1116,19 @@ module Ruflet
|
|
|
1038
1116
|
sync_client_main_extensions(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1039
1117
|
prune_client_main(entrypoint, extension_aliases) if File.file?(entrypoint)
|
|
1040
1118
|
end
|
|
1041
|
-
apply_service_native_requirements(client_dir,
|
|
1119
|
+
apply_service_native_requirements(client_dir, service_definitions.keys, service_definitions)
|
|
1042
1120
|
end
|
|
1043
1121
|
|
|
1044
|
-
def apply_service_native_requirements(client_dir, extension_keys)
|
|
1045
|
-
|
|
1046
|
-
|
|
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)
|
|
1047
1126
|
|
|
1048
|
-
requirements = merge_service_native_requirements(extension_keys)
|
|
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
|
+
)
|
|
1049
1132
|
return if requirements.empty?
|
|
1050
1133
|
|
|
1051
1134
|
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
@@ -1071,8 +1154,8 @@ module Ruflet
|
|
|
1071
1154
|
end
|
|
1072
1155
|
end
|
|
1073
1156
|
|
|
1074
|
-
def remove_service_native_requirements(client_dir, extension_keys)
|
|
1075
|
-
requirements = merge_service_native_requirements(extension_keys)
|
|
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)
|
|
1076
1159
|
return if requirements.empty?
|
|
1077
1160
|
|
|
1078
1161
|
android_manifest = File.join(client_dir, "android", "app", "src", "main", "AndroidManifest.xml")
|
|
@@ -1094,13 +1177,15 @@ module Ruflet
|
|
|
1094
1177
|
end
|
|
1095
1178
|
end
|
|
1096
1179
|
|
|
1097
|
-
def merge_service_native_requirements(extension_keys)
|
|
1180
|
+
def merge_service_native_requirements(extension_keys, service_requirements = DEFAULT_SERVICE_NATIVE_REQUIREMENTS)
|
|
1098
1181
|
extension_keys.each_with_object({}) do |key, memo|
|
|
1099
|
-
requirements =
|
|
1182
|
+
requirements = service_requirements[key]
|
|
1100
1183
|
next unless requirements
|
|
1101
1184
|
|
|
1102
1185
|
memo[:android_permissions] ||= []
|
|
1103
1186
|
memo[:android_permissions] |= Array(requirements[:android_permissions])
|
|
1187
|
+
memo[:ios_permission_definitions] ||= []
|
|
1188
|
+
memo[:ios_permission_definitions] |= Array(requirements[:ios_permission_definitions])
|
|
1104
1189
|
%i[ios_info macos_info macos_entitlements].each do |section|
|
|
1105
1190
|
memo[section] ||= {}
|
|
1106
1191
|
memo[section].merge!(requirements[section] || {})
|
|
@@ -1108,6 +1193,72 @@ module Ruflet
|
|
|
1108
1193
|
end
|
|
1109
1194
|
end
|
|
1110
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
|
+
|
|
1111
1262
|
def ensure_android_permission(path, permission)
|
|
1112
1263
|
return unless File.file?(path)
|
|
1113
1264
|
|
|
@@ -1226,11 +1377,46 @@ module Ruflet
|
|
|
1226
1377
|
write_pubspec_yaml(pubspec_path, data)
|
|
1227
1378
|
end
|
|
1228
1379
|
|
|
1380
|
+
RUBY_RUNTIME_FALLBACK_REQUIREMENT = "^0.0.5"
|
|
1381
|
+
|
|
1229
1382
|
def ruby_runtime_dependency(current_dependency = nil)
|
|
1230
|
-
local_path = explicit_local_ruby_runtime_path
|
|
1383
|
+
local_path = explicit_local_ruby_runtime_path || repo_checkout_ruby_runtime_path
|
|
1231
1384
|
return { "path" => local_path } if local_path
|
|
1232
1385
|
|
|
1233
|
-
|
|
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
|
|
1234
1420
|
end
|
|
1235
1421
|
|
|
1236
1422
|
def explicit_local_ruby_runtime_path
|
|
@@ -1271,6 +1457,27 @@ module Ruflet
|
|
|
1271
1457
|
FileUtils.cp(source, destination)
|
|
1272
1458
|
build_log(verbose, "refreshed template file #{relative_path}")
|
|
1273
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")
|
|
1274
1481
|
end
|
|
1275
1482
|
|
|
1276
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
|
|
|
@@ -322,7 +322,10 @@ module Ruflet
|
|
|
322
322
|
if os.match?(/darwin/i)
|
|
323
323
|
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
324
324
|
end
|
|
325
|
-
|
|
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
|
|
326
329
|
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
327
330
|
|
|
328
331
|
nil
|
|
@@ -391,14 +394,25 @@ module Ruflet
|
|
|
391
394
|
if windows_host?
|
|
392
395
|
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
393
396
|
end
|
|
397
|
+
|
|
398
|
+
require_extract_tool!("unzip")
|
|
394
399
|
return system("unzip", "-oq", archive, "-d", destination)
|
|
395
400
|
end
|
|
396
401
|
|
|
397
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?
|
|
398
405
|
return system("tar", "-xf", archive, "-C", destination)
|
|
399
406
|
end
|
|
400
407
|
|
|
401
|
-
|
|
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."
|
|
402
416
|
end
|
|
403
417
|
end
|
|
404
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
|
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
|