ruflet 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -1
- data/lib/ruflet/cli/android_sdk.rb +306 -0
- data/lib/ruflet/cli/build_command.rb +408 -30
- data/lib/ruflet/cli/environment_setup.rb +241 -0
- data/lib/ruflet/cli/extra_command.rb +15 -3
- data/lib/ruflet/cli/flutter_sdk.rb +87 -9
- data/lib/ruflet/cli/new_command.rb +41 -7
- data/lib/ruflet/cli/run_command.rb +106 -167
- data/lib/ruflet/cli.rb +13 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 653ba520f85466110b49be97b22029f8f1e30629edefcd3057def6ecbf20be2c
|
|
4
|
+
data.tar.gz: 66107e193cfd961763921dafcec2d67f21216f34daad33a22eeffbef279b1b0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73608863d7814a25d421f4a1b26bf07a231673b50679e3815eeb7e22df39c0da3c92bbdc696c207a36af38b427413d1d2ed6d61ba36d2720e8a0f444bb3ac3a7
|
|
7
|
+
data.tar.gz: 9a25b923371b68c7a8f51dbab32a30a71c0034033760c89cdf1773ffb9295707728107072f88523c9a5f7db4b43c5ded66d74096d52fa0547cd35103c99066b1
|
data/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
1
|
# ruflet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ruflet` is the command-line package for creating, running, diagnosing, and
|
|
4
|
+
building Ruflet applications.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
gem install ruflet
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Create a project and run it:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
ruflet new my_app
|
|
16
|
+
cd my_app
|
|
17
|
+
bundle install
|
|
18
|
+
bundle exec ruflet run --web
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Generated projects include `ruflet_core` and `ruflet_server` as application
|
|
22
|
+
dependencies. Use `bundle exec ruflet` inside a project so commands run with
|
|
23
|
+
the versions declared in its `Gemfile`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ruflet new <appname>
|
|
29
|
+
ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
|
|
30
|
+
ruflet debug [scriptname|path]
|
|
31
|
+
ruflet devices
|
|
32
|
+
ruflet emulators
|
|
33
|
+
ruflet doctor
|
|
34
|
+
ruflet update [web|desktop|all] [--check] [--force]
|
|
35
|
+
ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self]
|
|
36
|
+
ruflet install [--device DEVICE_ID]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run `ruflet install` without `--device` to choose from a numbered list of
|
|
40
|
+
connected devices. Pass `--device DEVICE_ID` to skip the prompt.
|
|
41
|
+
|
|
42
|
+
Run `ruflet help <command>` for all options.
|
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
# sdkmanager downloads sizeable platform/build-tool archives; stream so
|
|
250
|
+
# the install reports progress instead of appearing to hang.
|
|
251
|
+
system(env, sdkmanager, "--sdk_root=#{sdk_root}", *missing, out: $stdout, err: $stderr)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def android_package_installed?(sdk_root, package)
|
|
255
|
+
case package
|
|
256
|
+
when "platform-tools"
|
|
257
|
+
Dir.exist?(File.join(sdk_root, "platform-tools"))
|
|
258
|
+
when /\Aplatforms;(.+)\z/
|
|
259
|
+
Dir.exist?(File.join(sdk_root, "platforms", Regexp.last_match(1)))
|
|
260
|
+
when /\Abuild-tools;(.+)\z/
|
|
261
|
+
Dir.exist?(File.join(sdk_root, "build-tools", Regexp.last_match(1)))
|
|
262
|
+
else
|
|
263
|
+
false
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def accept_android_licenses(sdk_root, java, verbose: false)
|
|
268
|
+
sdkmanager = sdkmanager_bin(sdk_root)
|
|
269
|
+
return unless File.executable?(sdkmanager)
|
|
270
|
+
|
|
271
|
+
puts " Accepting Android SDK licenses"
|
|
272
|
+
env = sdkmanager_env(java)
|
|
273
|
+
Open3.popen2e(env, sdkmanager, "--sdk_root=#{sdk_root}", "--licenses") do |stdin, stdout, wait|
|
|
274
|
+
begin
|
|
275
|
+
stdin.write("y\n" * 64)
|
|
276
|
+
stdin.close
|
|
277
|
+
rescue Errno::EPIPE
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
stdout.each_line { |line| puts line if verbose }
|
|
281
|
+
wait.value
|
|
282
|
+
end
|
|
283
|
+
rescue StandardError => e
|
|
284
|
+
warn " License acceptance failed: #{e.class}: #{e.message}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def sdkmanager_env(java)
|
|
288
|
+
env = {}
|
|
289
|
+
java_home = detect_java_home
|
|
290
|
+
env["JAVA_HOME"] = java_home if java_home
|
|
291
|
+
if java && java[:path]
|
|
292
|
+
env["PATH"] = "#{File.dirname(java[:path])}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}"
|
|
293
|
+
end
|
|
294
|
+
env
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def jdk_install_hint
|
|
298
|
+
manager = system_package_manager
|
|
299
|
+
package = manager && JDK_PACKAGES[manager[:id]]
|
|
300
|
+
return "#{(sudo_prefix + manager[:install]).join(' ')} #{package}" if package
|
|
301
|
+
|
|
302
|
+
"install a JDK #{MINIMUM_JAVA_MAJOR}+ (e.g. Temurin: https://adoptium.net)"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|