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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d273c23251a6550ce0d8fa89003051b98f4b92ade780a048bd0c56c41ca14c81
4
- data.tar.gz: e646f2912853da8f30a6b49b924c04cfb9a00fd8328f391f8a68bea0e7875fcb
3
+ metadata.gz: 653ba520f85466110b49be97b22029f8f1e30629edefcd3057def6ecbf20be2c
4
+ data.tar.gz: 66107e193cfd961763921dafcec2d67f21216f34daad33a22eeffbef279b1b0e
5
5
  SHA512:
6
- metadata.gz: d6084b2b22d1775ff0849ade08bbc0ac4cb67198816add2f9d02a4c6461fef156936b2e45cd69cde3c4d7f0cc3510e9c942c245d7c2d032def1a39d4d3620c24
7
- data.tar.gz: 7e73f5b93c8af40c22f7ff5c154bcf4bc40f87595ef7993144ebd7b7eccc7531fab2b72b7e0f19aa002ec2983a4b5e0d0655a5b8946c49b1a98d90185b8831c0
6
+ metadata.gz: 73608863d7814a25d421f4a1b26bf07a231673b50679e3815eeb7e22df39c0da3c92bbdc696c207a36af38b427413d1d2ed6d61ba36d2720e8a0f444bb3ac3a7
7
+ data.tar.gz: 9a25b923371b68c7a8f51dbab32a30a71c0034033760c89cdf1773ffb9295707728107072f88523c9a5f7db4b43c5ded66d74096d52fa0547cd35103c99066b1
data/README.md CHANGED
@@ -1,3 +1,42 @@
1
1
  # ruflet
2
2
 
3
- Part of Ruflet monorepo.
3
+ `ruflet` is the command-line package for creating, running, diagnosing, and
4
+ building Ruflet applications.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ gem install ruflet
10
+ ```
11
+
12
+ Create a project and run it:
13
+
14
+ ```bash
15
+ ruflet new my_app
16
+ cd my_app
17
+ bundle install
18
+ bundle exec ruflet run --web
19
+ ```
20
+
21
+ Generated projects include `ruflet_core` and `ruflet_server` as application
22
+ dependencies. Use `bundle exec ruflet` inside a project so commands run with
23
+ the versions declared in its `Gemfile`.
24
+
25
+ ## Commands
26
+
27
+ ```bash
28
+ ruflet new <appname>
29
+ ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
30
+ ruflet debug [scriptname|path]
31
+ ruflet devices
32
+ ruflet emulators
33
+ ruflet doctor
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