ruflet 0.0.17 → 0.0.18

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.
@@ -1,306 +0,0 @@
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
@@ -1,241 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rbconfig"
4
-
5
- module Ruflet
6
- module CLI
7
- # Provisions the host so `ruflet build` can run: checks (and with
8
- # `ruflet doctor --fix`, installs) the system tools Flutter requires.
9
- #
10
- # Coverage:
11
- # Linux apt (Ubuntu, Debian, Kali, Mint), dnf (Fedora, RHEL),
12
- # pacman (Arch, Omarchy, Manjaro), zypper (openSUSE)
13
- # macOS Homebrew where available, plus Xcode Command Line Tools and
14
- # CocoaPods guidance
15
- # Windows winget or Chocolatey
16
- #
17
- # Anything that cannot be installed unattended (Xcode CLT, Visual Studio
18
- # Build Tools) is reported with the exact command the developer must run.
19
- module EnvironmentSetup
20
- Tool = Struct.new(:name, :probe, :purpose, :packages, :manual_hint, keyword_init: true)
21
-
22
- LINUX_PACKAGE_MANAGERS = [
23
- { id: :apt, probe: "apt-get", install: %w[apt-get install -y], update: %w[apt-get update] },
24
- { id: :dnf, probe: "dnf", install: %w[dnf install -y], update: nil },
25
- { id: :pacman, probe: "pacman", install: %w[pacman -S --noconfirm --needed], update: nil },
26
- { id: :zypper, probe: "zypper", install: %w[zypper --non-interactive install], update: nil },
27
- { id: :apk, probe: "apk", install: %w[apk add], update: nil }
28
- ].freeze
29
-
30
- def environment_setup!(fix: false, verbose: false)
31
- issues = []
32
- tools = required_system_tools
33
-
34
- unless flutter_host
35
- issues << unsupported_host_message
36
- warn " System: #{unsupported_host_message}"
37
- return issues
38
- end
39
-
40
- manager = system_package_manager
41
- missing = tools.reject { |tool| tool_present?(tool) }
42
-
43
- if missing.empty?
44
- puts " System tools: ok (#{tools.map(&:name).join(', ')})"
45
- return issues
46
- end
47
-
48
- puts " System tools missing: #{missing.map(&:name).join(', ')}"
49
-
50
- unless fix
51
- missing.each { |tool| warn " #{tool.name}: #{manual_hint_for(tool, manager)}" }
52
- warn " Run `ruflet doctor --fix` to install them."
53
- return missing.map { |tool| "missing #{tool.name}" }
54
- end
55
-
56
- auto, manual = missing.partition { |tool| installable?(tool, manager) }
57
-
58
- if auto.any?
59
- if manager
60
- install_system_packages(manager, auto, verbose: verbose)
61
- end
62
- still_missing = auto.reject { |tool| tool_present?(tool) }
63
- still_missing.each do |tool|
64
- issues << "missing #{tool.name}"
65
- warn " #{tool.name}: install failed — #{manual_hint_for(tool, manager)}"
66
- end
67
- installed = auto - still_missing
68
- puts " Installed: #{installed.map(&:name).join(', ')}" if installed.any?
69
- end
70
-
71
- manual.each do |tool|
72
- issues << "missing #{tool.name}"
73
- warn " #{tool.name}: requires a manual step — #{manual_hint_for(tool, manager)}"
74
- end
75
-
76
- issues
77
- end
78
-
79
- def required_system_tools
80
- case flutter_host
81
- when "linux" then linux_required_tools
82
- when "macos", "macos_arm64" then macos_required_tools
83
- when "windows" then windows_required_tools
84
- else []
85
- end
86
- end
87
-
88
- def system_package_manager
89
- if windows_host?
90
- return { id: :winget, install: %w[winget install --accept-source-agreements --accept-package-agreements -e --id] } if which_command("winget")
91
- return { id: :choco, install: %w[choco install -y] } if which_command("choco")
92
- return nil
93
- end
94
-
95
- if macos_host?
96
- return { id: :brew, install: %w[brew install] } if which_command("brew")
97
- return nil
98
- end
99
-
100
- LINUX_PACKAGE_MANAGERS.each do |manager|
101
- next unless which_command(manager[:probe])
102
-
103
- return manager
104
- end
105
- nil
106
- end
107
-
108
- private
109
-
110
- def macos_host?
111
- RbConfig::CONFIG["host_os"].match?(/darwin/i)
112
- end
113
-
114
- def linux_required_tools
115
- [
116
- Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
117
- packages: { apt: "git", dnf: "git", pacman: "git", zypper: "git", apk: "git" }),
118
- Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads",
119
- packages: { apt: "curl", dnf: "curl", pacman: "curl", zypper: "curl", apk: "curl" }),
120
- Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts",
121
- packages: { apt: "unzip", dnf: "unzip", pacman: "unzip", zypper: "unzip", apk: "unzip" }),
122
- Tool.new(name: "xz", probe: "xz", purpose: "extracting the Flutter SDK (.tar.xz)",
123
- packages: { apt: "xz-utils", dnf: "xz", pacman: "xz", zypper: "xz", apk: "xz" }),
124
- Tool.new(name: "zip", probe: "zip", purpose: "packaging app bundles",
125
- packages: { apt: "zip", dnf: "zip", pacman: "zip", zypper: "zip", apk: "zip" }),
126
- Tool.new(name: "clang", probe: "clang", purpose: "Linux desktop builds",
127
- packages: { apt: "clang", dnf: "clang", pacman: "clang", zypper: "clang", apk: "clang" }),
128
- Tool.new(name: "cmake", probe: "cmake", purpose: "Linux desktop builds",
129
- packages: { apt: "cmake", dnf: "cmake", pacman: "cmake", zypper: "cmake", apk: "cmake" }),
130
- Tool.new(name: "ninja", probe: "ninja", purpose: "Linux desktop builds",
131
- packages: { apt: "ninja-build", dnf: "ninja-build", pacman: "ninja", zypper: "ninja", apk: "ninja" }),
132
- Tool.new(name: "pkg-config", probe: "pkg-config", purpose: "Linux desktop builds",
133
- packages: { apt: "pkg-config", dnf: "pkgconf-pkg-config", pacman: "pkgconf", zypper: "pkg-config", apk: "pkgconf" }),
134
- Tool.new(name: "GTK 3 headers", probe: nil, purpose: "Linux desktop builds",
135
- packages: { apt: "libgtk-3-dev", dnf: "gtk3-devel", pacman: "gtk3", zypper: "gtk3-devel", apk: "gtk+3.0-dev" })
136
- ]
137
- end
138
-
139
- def macos_required_tools
140
- [
141
- Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
142
- packages: { brew: "git" },
143
- manual_hint: "install Xcode Command Line Tools: xcode-select --install"),
144
- Tool.new(name: "curl", probe: "curl", purpose: "SDK downloads", packages: { brew: "curl" }),
145
- Tool.new(name: "unzip", probe: "unzip", purpose: "extracting Flutter artifacts", packages: { brew: "unzip" }),
146
- Tool.new(name: "Xcode Command Line Tools", probe: nil, purpose: "macOS/iOS builds",
147
- packages: {},
148
- manual_hint: "run `xcode-select --install` (or install Xcode from the App Store)"),
149
- Tool.new(name: "CocoaPods", probe: "pod", purpose: "macOS/iOS plugin dependencies",
150
- packages: { brew: "cocoapods" },
151
- manual_hint: "run `brew install cocoapods` or `sudo gem install cocoapods`")
152
- ]
153
- end
154
-
155
- def windows_required_tools
156
- [
157
- Tool.new(name: "git", probe: "git", purpose: "required by the Flutter tool itself",
158
- packages: { winget: "Git.Git", choco: "git" }),
159
- Tool.new(name: "Visual Studio Build Tools", probe: nil, purpose: "Windows desktop builds",
160
- packages: {},
161
- manual_hint: "winget install --id Microsoft.VisualStudio.2022.BuildTools (select the " \
162
- "\"Desktop development with C++\" workload)")
163
- ]
164
- end
165
-
166
- def tool_present?(tool)
167
- case tool.name
168
- when "GTK 3 headers"
169
- return true if which_command("pkg-config") &&
170
- system("pkg-config", "--exists", "gtk+-3.0", out: File::NULL, err: File::NULL)
171
-
172
- false
173
- when "Xcode Command Line Tools"
174
- system("xcode-select", "-p", out: File::NULL, err: File::NULL)
175
- else
176
- tool.probe ? !which_command(tool.probe).nil? : false
177
- end
178
- end
179
-
180
- def installable?(tool, manager)
181
- return false unless manager
182
-
183
- tool.packages.key?(manager[:id])
184
- end
185
-
186
- def manual_hint_for(tool, manager)
187
- return tool.manual_hint if tool.manual_hint && !installable?(tool, manager)
188
- return "no supported package manager found; install #{tool.name} manually" unless manager
189
-
190
- package = tool.packages[manager[:id]]
191
- return tool.manual_hint || "install #{tool.name} manually" unless package
192
-
193
- "#{(sudo_prefix + manager[:install]).join(' ')} #{package}"
194
- end
195
-
196
- def install_system_packages(manager, tools, verbose: false)
197
- packages = tools.map { |tool| tool.packages[manager[:id]] }.compact.uniq
198
- return if packages.empty?
199
-
200
- if manager[:update]
201
- run_privileged_command(*manager[:update], verbose: verbose)
202
- end
203
-
204
- if manager[:id] == :winget
205
- # winget installs one id at a time.
206
- packages.each { |package| run_privileged_command(*manager[:install], package, verbose: verbose) }
207
- else
208
- run_privileged_command(*manager[:install], *packages, verbose: verbose)
209
- end
210
- end
211
-
212
- def run_privileged_command(*command, verbose: false)
213
- full = sudo_prefix + command
214
- puts " $ #{full.join(' ')}"
215
- # Package installs (apt/dnf/pacman...) routinely download hundreds of
216
- # megabytes and take minutes. Always stream their output so the user
217
- # can see progress instead of a frozen-looking prompt; `verbose` is
218
- # left for callers that want to add their own detail elsewhere.
219
- system(*full, out: $stdout, err: $stderr)
220
- end
221
-
222
- def sudo_prefix
223
- return [] if windows_host? || macos_host?
224
- return [] if Process.respond_to?(:uid) && Process.uid.zero?
225
- return ["sudo"] if which_command("sudo")
226
-
227
- []
228
- end
229
-
230
- def unsupported_host_message
231
- os = RbConfig::CONFIG["host_os"]
232
- if os.match?(/linux/i) && machine_arch.match?(/arm|aarch64/)
233
- "Flutter publishes no official Linux #{machine_arch} build; use an x64 machine " \
234
- "or build Flutter from source (https://github.com/flutter/flutter)."
235
- else
236
- "unsupported host platform: #{os}"
237
- end
238
- end
239
- end
240
- end
241
- end