Module: Everywhere::AndroidSdk
- Defined in:
- lib/everywhere/android_sdk.rb
Overview
Locates the Android toolchain — SDK, adb, emulator, sdkmanager, JDK — for
the builder, the emulator driver, every logs and every doctor.
The one rule everything here follows: resolve BY LOCATION, never off PATH.
Android Studio provisions the whole toolchain (SDK, platform-tools, the
emulator, system images, a bundled JDK) and exports none of it — no shell
profile is touched, so a machine that can build Android apps all day has no
adb, no sdkmanager and often no java on PATH at all. A which adb
here would report "missing" on a perfectly provisioned Mac. Fixed locations
under the SDK root are what's actually stable, the same way xcrun shields
the iOS builder from wherever Xcode put its internals.
Constant Summary collapse
- MINIMUM_JDK =
Android Gradle Plugin 8.x refuses to run on anything older, and the failure surfaces as an unreadable Gradle/Kotlin stacktrace rather than a version complaint — so we check before Gradle ever starts.
17- COMPILE_SDK =
The API level the shell template compiles against. Named here (not in the builder) because
every doctorhas to print the sdkmanager line for it long before a build is attempted. 35- STUDIO_JBR =
Android Studio's bundled JetBrains Runtime, per platform. Studio never exports it, so these paths are the only way to find a JDK on a machine whose owner installed Studio and nothing else.
[ "/Applications/Android Studio.app/Contents/jbr/Contents/Home", "~/Applications/Android Studio.app/Contents/jbr/Contents/Home", "/opt/android-studio/jbr", "/usr/local/android-studio/jbr", "~/android-studio/jbr", "C:/Program Files/Android/Android Studio/jbr" ].freeze
- CMDLINE_TOOLS_URL =
How to get command-line tools without already having them. The Studio route is first because it also accepts the licences, but the manual zip matters: it's the only answer for a machine with no Studio, and Studio's own SDK Tools list is exactly where this component was already unchecked.
"https://developer.android.com/studio#command-line-tools-only"
Class Method Summary collapse
-
.adb ⇒ Object
--- tools ---------------------------------------------------------------.
- .adb! ⇒ Object
- .avdmanager ⇒ Object
- .avdmanager! ⇒ Object
-
.avds ⇒ Object
Virtual devices, newest-created last.
- .avds! ⇒ Object
-
.cmdline_tool(name) ⇒ Object
sdkmanager and avdmanager are the one pair that moved: current installs put them in cmdline-tools/latest/bin, an SDK pinned to a specific command-line-tools release uses cmdline-tools/
/bin, and anything provisioned before 2021 still has the legacy tools/bin copy. -
.cmdline_tools? ⇒ Boolean
Whether this SDK can install anything at all.
-
.cmdline_tools_dir ⇒ Object
Where an unzipped command-line-tools release has to end up.
- .cmdline_tools_hint ⇒ Object
-
.create_avd_hint ⇒ Object
How to get a bootable AVD.
-
.default_sdk_roots ⇒ Object
Where the SDK lives on a default install, per platform.
- .emulator ⇒ Object
- .emulator! ⇒ Object
-
.expected_sdk_root ⇒ Object
The path named in "we couldn't find it" messages, so the user is told where we looked rather than left to guess.
-
.host_abi ⇒ Object
The system image ABI that runs at native speed on this host — an x86_64 image on Apple silicon boots, technically, at a speed nobody will wait for.
-
.host_tag ⇒ Object
The token Google's command-line-tools downloads are named with.
-
.install_hint(package) ⇒ Object
A ready-to-paste instruction for installing a missing SDK package.
- .installed_build_tools ⇒ Object
-
.installed_platforms ⇒ Object
Which platforms/build-tools are on disk, read straight off the filesystem rather than from
sdkmanager --list: that's a multi-second JVM start that also reaches out for the remote catalogue we don't care about, and it needs the command-line tools, which are exactly the component most likely to be missing on a Studio-only machine. -
.java_home ⇒ Object
Gradle needs a JDK.
-
.java_home!(minimum: MINIMUM_JDK) ⇒ Object
Like #java_home but also enforces the minimum: a JDK 11 that resolves cleanly still fails the build later, deep inside Gradle, so reject it here where the message can name the cause.
-
.java_version ⇒ Object
The JDK's major version, or nil if it can't be determined.
-
.macos? ⇒ Boolean
--- platform ------------------------------------------------------------.
- .platform!(api = COMPILE_SDK) ⇒ Object
-
.platform?(api = COMPILE_SDK) ⇒ Boolean
platform?(35)andplatform?("android-35")both work — callers spell it whichever way reads better where they are. -
.registry_java_home ⇒ Object
macOS keeps a registry of installed JDKs that java_home queries; -v 17 asks for "17 or newer", so a machine with only JDK 21 still answers.
- .sdk_package_dirs(kind) ⇒ Object
-
.sdk_root ⇒ Object
The SDK root, or nil.
- .sdk_root! ⇒ Object
-
.sdk_tool(subdir, name) ⇒ Object
A tool at its fixed spot under the SDK root, or nil.
- .sdkmanager ⇒ Object
- .sdkmanager! ⇒ Object
- .studio_java_home ⇒ Object
-
.tool_in(dir, name) ⇒ Object
Windows spells the same tools adb.exe and sdkmanager.bat; probing the suffixes here lets every caller ask for the plain Unix name.
- .windows? ⇒ Boolean
Class Method Details
.adb ⇒ Object
--- tools ---------------------------------------------------------------
92 |
# File 'lib/everywhere/android_sdk.rb', line 92 def adb = sdk_tool("platform-tools", "adb") |
.adb! ⇒ Object
95 96 97 |
# File 'lib/everywhere/android_sdk.rb', line 95 def adb! adb or UI.die!("no adb under #{sdk_root!} — #{install_hint("platform-tools")}") end |
.avdmanager ⇒ Object
104 |
# File 'lib/everywhere/android_sdk.rb', line 104 def avdmanager = cmdline_tool("avdmanager") |
.avdmanager! ⇒ Object
110 111 112 |
# File 'lib/everywhere/android_sdk.rb', line 110 def avdmanager! avdmanager or UI.die!("no avdmanager under #{sdk_root!} — #{cmdline_tools_hint}") end |
.avds ⇒ Object
Virtual devices, newest-created last. Asked of the emulator binary rather than globbed out of ~/.android/avd because the emulator also honours ANDROID_AVD_HOME and ANDROID_SDK_HOME — and an AVD it won't list is one we couldn't boot anyway. Studio's Device Manager writes to the same place, so a VM created in the GUI shows up here with no extra step.
299 300 301 302 303 304 305 306 307 308 |
# File 'lib/everywhere/android_sdk.rb', line 299 def avds exe = emulator or return [] out, status = Shellout.capture(exe, "-list-avds") return [] unless status&.success? # The emulator prints INFO/WARNING banners on the same stream; AVD names # can only contain [A-Za-z0-9._-], so anything with a space is chatter. out.lines.map(&:strip).reject(&:empty?).grep(/\A[\w.-]+\z/) end |
.avds! ⇒ Object
310 311 312 313 314 315 |
# File 'lib/everywhere/android_sdk.rb', line 310 def avds! found = avds return found if found.any? UI.die!("no Android virtual devices — #{create_avd_hint}") end |
.cmdline_tool(name) ⇒ Object
sdkmanager and avdmanager are the one pair that moved: current installs
put them in cmdline-tools/latest/bin, an SDK pinned to a specific
command-line-tools release uses cmdline-tools/every doctor can print a working install line on an older machine
instead of a path that doesn't exist.
163 164 165 166 167 168 169 170 171 172 |
# File 'lib/everywhere/android_sdk.rb', line 163 def cmdline_tool(name) root = sdk_root or return nil # Reverse-sorted so 12.0 beats 9.0 in the common case; "latest" is tried # first anyway, so this ordering only decides between pinned releases. versioned = Dir.glob(File.join(root, "cmdline-tools", "*", "bin")).sort.reverse dirs = [File.join(root, "cmdline-tools", "latest", "bin"), *versioned, File.join(root, "tools", "bin")] dirs.uniq.filter_map { |dir| tool_in(dir, name) }.first end |
.cmdline_tools? ⇒ Boolean
Whether this SDK can install anything at all. Worth asking on its own,
because it's the one component whose absence makes every other
remediation unquotable: without it there is no sdkmanager to run, and
the Phase 0 machine had a complete SDK, adb, the emulator, build-tools and
an AVD with no cmdline-tools anywhere — not in the SDK, not inside Android
Studio. So a missing one is an advisory, not a broken machine.
120 |
# File 'lib/everywhere/android_sdk.rb', line 120 def cmdline_tools? = !sdkmanager.nil? |
.cmdline_tools_dir ⇒ Object
Where an unzipped command-line-tools release has to end up. Named because both hints point at it, and because "unzip it somewhere in the SDK" is the instruction people get wrong — the tools look for their own jars relative to this exact directory.
139 140 141 |
# File 'lib/everywhere/android_sdk.rb', line 139 def cmdline_tools_dir File.join(sdk_root || expected_sdk_root, "cmdline-tools", "latest") end |
.cmdline_tools_hint ⇒ Object
128 129 130 131 132 133 |
# File 'lib/everywhere/android_sdk.rb', line 128 def cmdline_tools_hint "install \"Android SDK Command-line Tools (latest)\" in Android Studio → Settings → " \ "Languages & Frameworks → Android SDK → SDK Tools, or download " \ "commandlinetools-#{host_tag}-*_latest.zip from #{CMDLINE_TOOLS_URL} and unzip it into " \ "#{cmdline_tools_dir} (so its bin/ lands there)" end |
.create_avd_hint ⇒ Object
How to get a bootable AVD. The GUI route comes first because a machine with Studio installed almost always has a device defined already, and Device Manager also downloads the system image the CLI line assumes.
320 321 322 323 324 |
# File 'lib/everywhere/android_sdk.rb', line 320 def create_avd_hint "create one in Android Studio → Device Manager, or run: " \ "#{(avdmanager || "avdmanager").shellescape} create avd -n Pixel_API_#{COMPILE_SDK} " \ "-k \"system-images;android-#{COMPILE_SDK};google_apis;#{host_abi}\"" end |
.default_sdk_roots ⇒ Object
Where the SDK lives on a default install, per platform. Studio writes exactly one of these and records it in its own settings — never in the environment — which is why the fallback matters more here than it would for a toolchain with an installer that edits your profile.
55 56 57 58 59 60 61 62 63 |
# File 'lib/everywhere/android_sdk.rb', line 55 def default_sdk_roots if macos? ["~/Library/Android/sdk"] elsif windows? [ENV["LOCALAPPDATA"] && File.join(ENV["LOCALAPPDATA"], "Android", "Sdk")].compact else ["~/Android/Sdk"] end end |
.emulator ⇒ Object
93 |
# File 'lib/everywhere/android_sdk.rb', line 93 def emulator = sdk_tool("emulator", "emulator") |
.emulator! ⇒ Object
99 100 101 |
# File 'lib/everywhere/android_sdk.rb', line 99 def emulator! emulator or UI.die!("no Android emulator under #{sdk_root!} — #{install_hint("emulator")}") end |
.expected_sdk_root ⇒ Object
The path named in "we couldn't find it" messages, so the user is told where we looked rather than left to guess.
86 87 88 |
# File 'lib/everywhere/android_sdk.rb', line 86 def expected_sdk_root File.(default_sdk_roots.first || "~/Android/Sdk") end |
.host_abi ⇒ Object
The system image ABI that runs at native speed on this host — an x86_64 image on Apple silicon boots, technically, at a speed nobody will wait for.
328 |
# File 'lib/everywhere/android_sdk.rb', line 328 def host_abi = RUBY_PLATFORM.match?(/arm64|aarch64/) ? "arm64-v8a" : "x86_64" |
.host_tag ⇒ Object
The token Google's command-line-tools downloads are named with.
144 145 146 147 148 |
# File 'lib/everywhere/android_sdk.rb', line 144 def host_tag return "mac" if macos? windows? ? "win" : "linux" end |
.install_hint(package) ⇒ Object
A ready-to-paste instruction for installing a missing SDK package. When the SDK has no command-line tools at all there IS no command to paste, so say what to click in Studio instead — an honest instruction beats a copyable line that resolves to nothing. Phrased to read on its own, since callers append it after an em dash.
187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/everywhere/android_sdk.rb', line 187 def install_hint(package) if (manager = sdkmanager) "run: #{manager.shellescape} #{package.inspect}" else # The short form of cmdline_tools_hint: doctor prints the full version # once, above these lines, and repeating a URL and two absolute paths # on every missing package buries what each line is actually about. "install \"Android SDK Command-line Tools\" (Android Studio → SDK Tools, or unzip " \ "commandlinetools-#{host_tag}-*_latest.zip into #{cmdline_tools_dir}), " \ "then run: sdkmanager #{package.inspect}" end end |
.installed_build_tools ⇒ Object
265 |
# File 'lib/everywhere/android_sdk.rb', line 265 def installed_build_tools = sdk_package_dirs("build-tools") |
.installed_platforms ⇒ Object
Which platforms/build-tools are on disk, read straight off the filesystem
rather than from sdkmanager --list: that's a multi-second JVM start that
also reaches out for the remote catalogue we don't care about, and it
needs the command-line tools, which are exactly the component most likely
to be missing on a Studio-only machine.
264 |
# File 'lib/everywhere/android_sdk.rb', line 264 def installed_platforms = sdk_package_dirs("platforms") |
.java_home ⇒ Object
Gradle needs a JDK. JAVA_HOME wins because it's the one explicit choice on the machine; then macOS's own registry (which knows about JDKs installed by brew, Oracle or SDKMAN without any of them being on PATH); then Studio's bundled runtime, which is what a Studio-only machine actually has.
206 207 208 209 210 211 |
# File 'lib/everywhere/android_sdk.rb', line 206 def java_home explicit = ENV["JAVA_HOME"].to_s return File.(explicit) if !explicit.empty? && File.directory?(explicit) registry_java_home || studio_java_home end |
.java_home!(minimum: MINIMUM_JDK) ⇒ Object
Like #java_home but also enforces the minimum: a JDK 11 that resolves cleanly still fails the build later, deep inside Gradle, so reject it here where the message can name the cause.
216 217 218 219 220 221 222 223 224 225 |
# File 'lib/everywhere/android_sdk.rb', line 216 def java_home!(minimum: MINIMUM_JDK) home = java_home or UI.die!("no JDK — install Android Studio (it bundles one at #{STUDIO_JBR.first}) " \ "or set JAVA_HOME to a JDK #{minimum} or newer") version = java_version return home if version.nil? || version >= minimum UI.die!("#{home} is JDK #{version}; the Android Gradle Plugin needs #{minimum} or newer — " \ "set JAVA_HOME to a JDK #{minimum}+, or install Android Studio and use its bundled runtime") end |
.java_version ⇒ Object
The JDK's major version, or nil if it can't be determined. Read from the
release file the JDK ships rather than by running java -version: it's
a file read instead of a JVM start, and it works for a JDK that isn't on
PATH — which, per the rule at the top, is most of them. A JDK 8 reports
JAVA_VERSION="1.8.0_412", so this yields 1, which compares correctly
against MINIMUM_JDK without special-casing the old scheme.
233 234 235 236 237 238 239 240 241 |
# File 'lib/everywhere/android_sdk.rb', line 233 def java_version home = java_home or return nil release = File.join(home, "release") return File.read(release)[/^JAVA_VERSION="?(\d+)/, 1]&.to_i if File.file?(release) java = tool_in(File.join(home, "bin"), "java") or return nil out, status = Shellout.capture(java, "-version") out[/version "?(\d+)/, 1]&.to_i if status&.success? end |
.macos? ⇒ Boolean
--- platform ------------------------------------------------------------
46 |
# File 'lib/everywhere/android_sdk.rb', line 46 def macos? = RUBY_PLATFORM.include?("darwin") |
.platform!(api = COMPILE_SDK) ⇒ Object
283 284 285 286 287 288 289 290 |
# File 'lib/everywhere/android_sdk.rb', line 283 def platform!(api = COMPILE_SDK) return true if platform?(api) package = "platforms;android-#{api.to_s.delete_prefix("android-")}" UI.die!("the Android SDK has no #{package} (installed: " \ "#{installed_platforms.empty? ? "none" : installed_platforms.join(", ")}) — " \ "#{install_hint(package)}") end |
.platform?(api = COMPILE_SDK) ⇒ Boolean
platform?(35) and platform?("android-35") both work — callers spell it
whichever way reads better where they are.
278 279 280 281 |
# File 'lib/everywhere/android_sdk.rb', line 278 def platform?(api = COMPILE_SDK) name = api.to_s.start_with?("android-") ? api.to_s : "android-#{api}" installed_platforms.include?(name) end |
.registry_java_home ⇒ Object
macOS keeps a registry of installed JDKs that java_home queries; -v 17 asks for "17 or newer", so a machine with only JDK 21 still answers.
245 246 247 248 249 250 251 |
# File 'lib/everywhere/android_sdk.rb', line 245 def registry_java_home return nil unless macos? out, status = Shellout.capture("/usr/libexec/java_home", "-v", MINIMUM_JDK.to_s) home = out.to_s.strip home if status&.success? && !home.empty? && File.directory?(home) end |
.sdk_package_dirs(kind) ⇒ Object
267 268 269 270 271 272 273 274 |
# File 'lib/everywhere/android_sdk.rb', line 267 def sdk_package_dirs(kind) root = sdk_root or return [] Dir.glob(File.join(root, kind, "*")) .select { |dir| File.directory?(dir) } .map { |dir| File.basename(dir) } .sort end |
.sdk_root ⇒ Object
The SDK root, or nil. An explicit env var wins so CI images and secondary SDKs work without moving anything; ANDROID_HOME comes first because it's the spelling Studio and the docs use now (ANDROID_SDK_ROOT is deprecated but still all over CI). A var pointing at a directory that isn't there is skipped rather than fatal — a stale export shouldn't hide the SDK sitting in the default location.
71 72 73 74 75 76 |
# File 'lib/everywhere/android_sdk.rb', line 71 def sdk_root [ENV["ANDROID_HOME"], ENV["ANDROID_SDK_ROOT"], *default_sdk_roots] .reject { |dir| dir.to_s.empty? } .map { |dir| File.(dir) } .find { |dir| File.directory?(dir) } end |
.sdk_root! ⇒ Object
78 79 80 81 82 |
# File 'lib/everywhere/android_sdk.rb', line 78 def sdk_root! sdk_root or UI.die!("no Android SDK — install Android Studio (it provisions the SDK, adb, " \ "the emulator and a JDK in one go), or point ANDROID_HOME at an " \ "existing SDK; looked in #{expected_sdk_root}") end |
.sdk_tool(subdir, name) ⇒ Object
A tool at its fixed spot under the SDK root, or nil.
151 152 153 154 155 |
# File 'lib/everywhere/android_sdk.rb', line 151 def sdk_tool(subdir, name) root = sdk_root or return nil tool_in(File.join(root, subdir), name) end |
.sdkmanager ⇒ Object
103 |
# File 'lib/everywhere/android_sdk.rb', line 103 def sdkmanager = cmdline_tool("sdkmanager") |
.sdkmanager! ⇒ Object
106 107 108 |
# File 'lib/everywhere/android_sdk.rb', line 106 def sdkmanager! sdkmanager or UI.die!("no sdkmanager under #{sdk_root!} — #{cmdline_tools_hint}") end |
.studio_java_home ⇒ Object
253 254 255 |
# File 'lib/everywhere/android_sdk.rb', line 253 def studio_java_home STUDIO_JBR.map { |dir| File.(dir) }.find { |dir| File.directory?(dir) } end |
.tool_in(dir, name) ⇒ Object
Windows spells the same tools adb.exe and sdkmanager.bat; probing the suffixes here lets every caller ask for the plain Unix name.
176 177 178 179 180 |
# File 'lib/everywhere/android_sdk.rb', line 176 def tool_in(dir, name) [name, "#{name}.exe", "#{name}.bat"] .map { |file| File.join(dir, file) } .find { |path| File.file?(path) } end |
.windows? ⇒ Boolean
47 |
# File 'lib/everywhere/android_sdk.rb', line 47 def windows? = RUBY_PLATFORM.match?(/mswin|mingw|cygwin/) |