]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: add Android Studio, and a per-user Android SDK
authorMax Vilimpoc <max@vilimpoc.org>
Mon, 28 Sep 2026 14:02:23 +0000 (16:02 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Mon, 28 Sep 2026 14:02:23 +0000 (16:02 +0200)
Two new steps, both x64-only: Studio has no Windows ARM64 build, and the
SDK's Windows build tools are x86/x64.

AndroidStudio installs Google.AndroidStudio (machine-wide; its NSIS
installer elevates itself) only when Studio is missing. It updates itself
in the app, and the package is published with UpgradeBehavior: deny and
RequireExplicitUpgrade, so winget would never upgrade it anyway. What the
check prevents is the other case: a Studio installed some other way - as on
the x64 laptop, where winget lists it only as "ARP\Machine\X64\Android
Studio" - is one winget cannot match to the package, so `winget install`
would run the installer over it. setup-windows-current.ps1 answers.

AndroidSdk sets up %LOCALAPPDATA%\Android\Sdk (or ANDROID_HOME's), which
that laptop never had - Studio was installed, its first-run wizard never
run: the command-line tools, platform-tools, and the newest stable
platform and build-tools, read from Google's package index at run time.
It accepts the SDK licences (the plan row says so), sets ANDROID_HOME,
puts the command-line tools on the user PATH, and points JAVA_HOME at
Studio's bundled JDK for a command-line gradlew when there is none.

The command-line tools are not what they were, and the step is built
around what they are now:

  - From 23.0, sdkmanager is deprecated: sdkmanager.bat is a shim over the
    new Android CLI (android.exe), which on first use installs itself into
    %USERPROFILE%\.android - ~90 MB in bin\, and a cli\ folder with its own
    Java runtime and a spool of usage metrics it uploads. No opt-out was
    found. The step calls the CLI directly, with its own
    "platforms/android-37.2" paths, so cmd never gets to split the old
    "platforms;android-37.2" at its semicolon.

  - CLI 1.0.16406183 crashes on the way out of every `sdk` command,
    0xC0000409 in ucrtbase, on this Windows 10 - in a console or not -
    after finishing the work, while --version and info exit 0. So its exit
    code is not trusted: each package asked for must have its package.xml
    afterwards. A crash with everything in place is a note; anything
    missing is a failure.

  - The archive calls itself cmdline-tools;23.0, a fixed version the CLI
    will never "update", so the step bootstraps and refreshes the tools
    itself from the index's cmdline-tools;latest (SHA-1 checked), only when
    the index has a newer revision.

Windows PowerShell 5.1's progress bar made the 148 MB download take about
ten minutes and five minutes of CPU; with it off, a whole first install
took 58 s and an unchanged re-run 4 s. BinSkim's 100+ MB download gets the
same fix. The tools unpack under an 8-character temp directory, so the
unpack is never deeper than the SDK path itself against MAX_PATH, and the
old copy is renamed aside before the new one moves in, so a failed delete
cannot leave the SDK without tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md
setup-windows-current.ps1
setup-windows-no-uac.ps1
setup-windows-steps.ps1
setup-windows.bat

index 55244d798cfd282fabd74bc9edb78bc3255e4e0f..e76e01720e62850f84ec00ca568e3110e06a5587 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,10 +6,11 @@ Windows development-box provisioning scripts.
 development environment: editors and shells, Python, the Visual Studio
 toolchain (including the Clang and Windows XP targeting toolsets), the Windows
 Driver Kit, Rust (rustup, with the stable MSVC toolchain for the host as the
 development environment: editors and shells, Python, the Visual Studio
 toolchain (including the Clang and Windows XP targeting toolsets), the Windows
 Driver Kit, Rust (rustup, with the stable MSVC toolchain for the host as the
-default), Android's `adb`, and a handful of analysis tools (Sysinternals,
-OpenCppCoverage, BinSkim, the Windows Performance Toolkit). It also sets the
-box up to be driven remotely: OpenSSH Server plus an rsync build for Windows,
-which is what makes a throwaway VM reachable from a Linux host.
+default), Android Studio and a per-user Android SDK, Android's `adb`, and a
+handful of analysis tools (Sysinternals, OpenCppCoverage, BinSkim, the Windows
+Performance Toolkit). It also sets the box up to be driven remotely: OpenSSH
+Server plus an rsync build for Windows, which is what makes a throwaway VM
+reachable from a Linux host.
 
 **Nothing happens until you have seen the plan.** It first lists every step it
 can take on this machine and lets you choose which ones to run — see
 
 **Nothing happens until you have seen the plan.** It first lists every step it
 can take on this machine and lets you choose which ones to run — see
@@ -27,7 +28,7 @@ C/C++ compiler. See
 | `setup-windows.bat` | Entry point. Shows the plan and lets you choose, then runs the winget installs, launches the elevated half and prints its log, then runs the non-elevated script. |
 | `setup-windows-plan.ps1` | The plan. Run by the batch file before anything else: lists every step for this machine, grouped and numbered, and takes your choice. Run it on its own with `-WhatIf` to preview; add `-HostArch ARM64 -Elevation NOLUA` to preview another kind of box. |
 | `setup-windows-steps.ps1` | The catalog the other four read: every step's name, what it does, and where it cannot run and why. Defines functions only; it is dot-sourced, never run. |
 | `setup-windows.bat` | Entry point. Shows the plan and lets you choose, then runs the winget installs, launches the elevated half and prints its log, then runs the non-elevated script. |
 | `setup-windows-plan.ps1` | The plan. Run by the batch file before anything else: lists every step for this machine, grouped and numbered, and takes your choice. Run it on its own with `-WhatIf` to preview; add `-HostArch ARM64 -Elevation NOLUA` to preview another kind of box. |
 | `setup-windows-steps.ps1` | The catalog the other four read: every step's name, what it does, and where it cannot run and why. Defines functions only; it is dot-sourced, never run. |
-| `setup-windows-current.ps1` | Asked by the batch file before the Claude Code and NASM installs: is what is on the box already current? Those are the two installs that would otherwise run in full every time — see [Re-running](#re-running). |
+| `setup-windows-current.ps1` | Asked by the batch file before the Claude Code, NASM and Android Studio installs: is what is on the box already current? Those are the installs that would otherwise run in full every time — see [Re-running](#re-running). |
 | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt; it lists its steps and asks first. |
 | `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt; it lists its steps and asks first. |
 | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
 | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt; it lists its steps and asks first. |
 | `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt; it lists its steps and asks first. |
 | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
@@ -66,12 +67,12 @@ C/C++ compiler. See
       [x]  2 Brave               Brave.Brave
       ...
     E  Elevated setup (setup-windows-with-uac.ps1) - one UAC prompt, runs in its own window
       [x]  2 Brave               Brave.Brave
       ...
     E  Elevated setup (setup-windows-with-uac.ps1) - one UAC prompt, runs in its own window
-      [x] 24 OpenSshClient       OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync's ssh.exe needs)
+      [x] 25 OpenSshClient       OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync's ssh.exe needs)
       ...
       ...
-      [x] 28 VisualStudio        Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)
+      [x] 29 VisualStudio        Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)
       ...
 
       ...
 
-     42 of 42 steps selected.
+     44 of 44 steps selected.
 
      Enter runs the selected steps. To change them, type step numbers or ranges
      (3 5-7), step names, or a group letter (U M E C) to switch them on or off;
 
      Enter runs the selected steps. To change them, type step numbers or ranges
      (3 5-7), step names, or a group letter (U M E C) to switch them on or off;
@@ -144,6 +145,7 @@ missing or out of date.
 | `ClaudeCode` | Anthropic's installer always downloads and reinstalls, so `setup-windows-current.ps1` asks first: is `%USERPROFILE%\.local\bin\claude.exe` the latest release — the version the installer's own endpoint names — and the build for this host? Only both skip it. |
 | `NASM` | Its per-user fallback install registers nothing, so winget cannot see it and would rerun the installer every time. `setup-windows-current.ps1` compares that `nasm.exe` with the version winget offers. A machine-wide NASM is left to winget. |
 | `Rustup` | Needs no help: rustup registers itself under the manifest's product code and rewrites its version when it updates itself, so winget sees what is really there. |
 | `ClaudeCode` | Anthropic's installer always downloads and reinstalls, so `setup-windows-current.ps1` asks first: is `%USERPROFILE%\.local\bin\claude.exe` the latest release — the version the installer's own endpoint names — and the build for this host? Only both skip it. |
 | `NASM` | Its per-user fallback install registers nothing, so winget cannot see it and would rerun the installer every time. `setup-windows-current.ps1` compares that `nasm.exe` with the version winget offers. A machine-wide NASM is left to winget. |
 | `Rustup` | Needs no help: rustup registers itself under the manifest's product code and rewrites its version when it updates itself, so winget sees what is really there. |
+| `AndroidStudio` | Installed only when it is missing. Studio updates itself in the app, and winget's package is marked never to upgrade it; the check keeps winget from installing over a Studio it cannot match to its package, as on a box where Studio was installed some other way. |
 | `WiX` | Skipped when 5.0.2 is already in the .NET global tool store. |
 | `OpenSshClient`, `OpenSshServer` | Installed only when the capability is not. |
 | `Rsync` | The release's `.sha256` is fetched first and compared with the hash recorded when the files were installed (`rsync-windows.sha256`). The zip is downloaded only when they differ or a file is missing. |
 | `WiX` | Skipped when 5.0.2 is already in the .NET global tool store. |
 | `OpenSshClient`, `OpenSshServer` | Installed only when the capability is not. |
 | `Rsync` | The release's `.sha256` is fetched first and compared with the hash recorded when the files were installed (`rsync-windows.sha256`). The zip is downloaded only when they differ or a file is missing. |
@@ -151,6 +153,7 @@ missing or out of date.
 | `Wdk`, `Wpt` | Skipped when the kit, or the toolkit, is already there. |
 | `BinSkim` | Asks NuGet for the newest stable version before downloading a package well over 100 MB; `nupkg-version.txt` beside the tool records what is installed. |
 | `RustToolchain` | `rustup toolchain install` updates stable when there is a newer one and otherwise reports it unchanged. |
 | `Wdk`, `Wpt` | Skipped when the kit, or the toolkit, is already there. |
 | `BinSkim` | Asks NuGet for the newest stable version before downloading a package well over 100 MB; `nupkg-version.txt` beside the tool records what is installed. |
 | `RustToolchain` | `rustup toolchain install` updates stable when there is a newer one and otherwise reports it unchanged. |
+| `AndroidSdk` | The command-line tools are re-downloaded only when Google's package index has a newer revision than the installed one; `android sdk install` updates the platform, build-tools and platform-tools only when they are out of date. A re-run with nothing new takes seconds. |
 | `Pillow` | `pip install` leaves an installed Pillow alone. It does not upgrade it either. |
 | the rest | Configuration: `PATH` entries are added only when missing, and settings are written with the values they already have. |
 
 | `Pillow` | `pip install` leaves an installed Pillow alone. It does not upgrade it either. |
 | the rest | Configuration: `PATH` entries are added only when missing, and settings are written with the values they already have. |
 
@@ -372,6 +375,33 @@ missing or out of date.
   emulation on ARM64, which every Windows on Arm has; the audit lists it as
   expected. It stays on ARM64 for the same reason `iperf3` does: it is bound by
   the link to the device, not by the CPU.
   emulation on ARM64, which every Windows on Arm has; the audit lists it as
   expected. It stays on ARM64 for the same reason `iperf3` does: it is bound by
   the link to the device, not by the CPU.
+- **Android Studio and the Android SDK** are separate steps. Studio
+  (`Google.AndroidStudio`) is machine-wide — its installer elevates itself — and
+  is installed only when missing, since it updates itself. The SDK is per-user,
+  in Studio's default `%LOCALAPPDATA%\Android\Sdk` (or wherever `ANDROID_HOME`
+  already points): the command-line tools, platform-tools, and the newest
+  stable platform and build-tools from Google's package index, read at run
+  time. That is what Studio's first-run wizard would set up, and it is enough
+  to build: Gradle fetches whatever else a project asks for. The step sets
+  `ANDROID_HOME`, puts the command-line tools on the user `PATH`, and points
+  `JAVA_HOME` at Studio's bundled JDK for a command-line `gradlew` when there is
+  no `JAVA_HOME` already. The SDK licences are accepted on your behalf — the
+  plan says so before anything runs. Both are x64-only: Studio has no ARM64
+  build, and the SDK's Windows build tools are x86/x64. Three things about
+  the current command-line tools (23.0), found when this step was written:
+  - `sdkmanager` is deprecated. It is now a shim over the new **Android CLI**
+    (`android.exe`), which on first use installs itself into
+    `%USERPROFILE%\.android` — about 90 MB in `bin\`, and a `cli\` folder with
+    its own Java runtime. It also keeps a spool of **usage metrics** there and
+    uploads them; no opt-out was found. The step calls the CLI directly.
+  - **CLI 1.0.16406183 crashes on the way out of every `sdk` command**
+    (`0xC0000409`) on this Windows 10, after the work is done. So the step does
+    not trust its exit code: each package must be present afterwards, and a
+    crash with everything in place is printed as a note, not a failure.
+  - Windows PowerShell 5.1's progress bar made the 148 MB command-line tools
+    download take about ten minutes; with it off, the whole first install took
+    under a minute. The BinSkim download now skips the progress bar for the
+    same reason.
 - The scripts were extracted from a native Windows project, so the component
   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
 - The scripts were extracted from a native Windows project, so the component
   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
@@ -455,6 +485,8 @@ duplicates it or that exists only as an x64 binary.**
 | VS Clang/LLVM component | `setup-windows-with-uac.ps1` | see below |
 | Windows Driver Kit | `setup-windows-with-uac.ps1` | see below |
 | BinSkim | `setup-windows-no-uac.ps1` | the NuGet package publishes `win-x64` only |
 | VS Clang/LLVM component | `setup-windows-with-uac.ps1` | see below |
 | Windows Driver Kit | `setup-windows-with-uac.ps1` | see below |
 | BinSkim | `setup-windows-no-uac.ps1` | the NuGet package publishes `win-x64` only |
+| Android Studio (`Google.AndroidStudio`) | `setup-windows.bat` | Google publishes no Windows ARM64 build |
+| Android SDK | `setup-windows-no-uac.ps1` | its Windows build tools are x86/x64 only, as is Android Studio |
 
 x64 keeps every one of them. The decision is made in one place,
 `setup-windows-steps.ps1` (`-X64Only`, with the reason): the plan shows each as
 
 x64 keeps every one of them. The decision is made in one place,
 `setup-windows-steps.ps1` (`-X64Only`, with the reason): the plan shows each as
@@ -528,8 +560,8 @@ What you end up with on ARM64:
 | **Native ARM64** | Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, Sysinternals, Claude Code, CMake, Ninja, the VS installer, **MSVC**, MSBuild, the SDK tools (`rc`, `signtool`), rustup and **Rust** (`stable-aarch64-pc-windows-msvc`), the in-box OpenSSH client, and the Windows Performance Toolkit. |
 | **Emulated** | `iperf3` (x64; network-bound), `adb` and `fastboot` (x86 — Google ships no other Windows build; bound by the USB/TCP link to the device), and the ADK *installer* (x64; the toolkit it lays down is native). |
 | **Emulated, minor** | `py.exe` (python.org ships the launcher shim as x86; it execs the native `python.exe`) and `vswhere.exe` (Microsoft ships x86 only; runs once). |
 | **Native ARM64** | Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, Sysinternals, Claude Code, CMake, Ninja, the VS installer, **MSVC**, MSBuild, the SDK tools (`rc`, `signtool`), rustup and **Rust** (`stable-aarch64-pc-windows-msvc`), the in-box OpenSSH client, and the Windows Performance Toolkit. |
 | **Emulated** | `iperf3` (x64; network-bound), `adb` and `fastboot` (x86 — Google ships no other Windows build; bound by the USB/TCP link to the device), and the ADK *installer* (x64; the toolkit it lays down is native). |
 | **Emulated, minor** | `py.exe` (python.org ships the launcher shim as x86; it execs the native `python.exe`) and `vswhere.exe` (Microsoft ships x86 only; runs once). |
-| **Not installed** | Brave, WinMerge, upstream LLVM, NASM, Android GPU Inspector, OpenCppCoverage, BinSkim, VS `clang-cl`, and the WDK — see the table above. |
-| **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, and the `v141` / Windows XP targeting toolset. |
+| **Not installed** | Brave, WinMerge, upstream LLVM, NASM, Android GPU Inspector, OpenCppCoverage, BinSkim, VS `clang-cl`, the WDK, and the Android SDK — see the table above. |
+| **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, Android Studio, and the `v141` / Windows XP targeting toolset. |
 
 ### The architecture audit
 
 
 ### The architecture audit
 
index be07ef7c821a1ddf7eb6033f60225f5a85f40223..b14a791cf37acd3e3a81e035bd9cfb5108296924 100644 (file)
@@ -1,16 +1,19 @@
 <#
   setup-windows-current.ps1 - is this step's program already current?
 
 <#
   setup-windows-current.ps1 - is this step's program already current?
 
-  setup-windows.bat asks this before the two installs that would otherwise run
-  in full on every re-run, because neither the installer nor winget can tell
-  that what is on the box is already the latest:
+  setup-windows.bat asks this before the installs that would otherwise run in
+  full on every re-run, because neither the installer nor winget can tell that
+  what is on the box is already the latest:
 
 
-      ClaudeCode   Anthropic's installer script always downloads the release
-                   (a couple of hundred MB) and reinstalls it; it has no "already
-                   current" check of its own.
-      NASM         its installer falls back to a per-user directory when it cannot
-                   write to Program Files, and registers nothing there - so winget
-                   cannot see it, and reruns the installer every time.
+      ClaudeCode     Anthropic's installer script always downloads the release
+                     (a couple of hundred MB) and reinstalls it; it has no
+                     "already current" check of its own.
+      NASM           its installer falls back to a per-user directory when it
+                     cannot write to Program Files, and registers nothing there -
+                     so winget cannot see it, and reruns the installer every time.
+      AndroidStudio  an install that did not come from winget is one winget
+                     cannot match to its package, so it would install over it.
+                     And Android Studio updates itself; winget never upgrades it.
 
   Exit code 0 means already current: skip the install. Anything else means go
   ahead as before - and that includes this script failing (no network, a version
 
   Exit code 0 means already current: skip the install. Anything else means go
   ahead as before - and that includes this script failing (no network, a version
@@ -22,7 +25,7 @@
   "Re-running" note lists how each one decides.
 #>
 param(
   "Re-running" note lists how each one decides.
 #>
 param(
-    [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM')] [string] $Step,
+    [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM', 'AndroidStudio')] [string] $Step,
     # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86.
     [string] $HostArch
 )
     # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86.
     [string] $HostArch
 )
@@ -109,10 +112,33 @@ function Test-NasmCurrent {
     $true
 }
 
     $true
 }
 
+function Test-AndroidStudioCurrent {
+    # Installed at all counts as current, because Android Studio keeps itself
+    # current: it updates in the app (Help > Check for Updates), and the winget
+    # package is published with UpgradeBehavior: deny and RequireExplicitUpgrade,
+    # so winget would not upgrade it anyway. The check matters for an install
+    # that did not come from winget - measured on the x64 laptop, where winget
+    # lists Studio only as "ARP\Machine\X64\Android Studio", unmatched to
+    # Google.AndroidStudio - since `winget install` would then run the installer
+    # over it. Looked for both ways: the Add/Remove Programs entry its installer
+    # writes, and studio64.exe where that installer puts it.
+    $arp = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
+                            'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
+                            'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
+           Where-Object { $_.DisplayName -eq 'Android Studio' }
+    $exe = @((Join-Path $env:ProgramFiles 'Android\Android Studio\bin\studio64.exe'),
+             (Join-Path $env:LOCALAPPDATA 'Programs\Android Studio\bin\studio64.exe')) | Where-Object { Test-Path $_ }
+    if (-not $arp -and -not $exe) { Say 'AndroidStudio: not installed yet.'; return $false }
+    $ver = @($arp)[0].DisplayVersion
+    Say "AndroidStudio: $(if ($ver) { $ver } else { 'an existing install' }) is already here, and it updates itself (Help > Check for Updates); not reinstalling."
+    $true
+}
+
 try {
     $current = switch ($Step) {
 try {
     $current = switch ($Step) {
-        'ClaudeCode' { Test-ClaudeCodeCurrent }
-        'NASM'       { Test-NasmCurrent }
+        'ClaudeCode'    { Test-ClaudeCodeCurrent }
+        'NASM'          { Test-NasmCurrent }
+        'AndroidStudio' { Test-AndroidStudioCurrent }
     }
 } catch {
     Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual."
     }
 } catch {
     Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual."
index c44bb1a66de8d2ce9cb74ca78cc4ac15bba30bc8..b048ec3770de382e61a87f14e6036b2959521090 100644 (file)
       half unpacks beside rsync.exe if it is there, the in-box one otherwise
     - Rust's stable MSVC toolchain for this host - x86_64 on x64, aarch64 on
       ARM64 - installed through rustup and made the default
       half unpacks beside rsync.exe if it is there, the in-box one otherwise
     - Rust's stable MSVC toolchain for this host - x86_64 on x64, aarch64 on
       ARM64 - installed through rustup and made the default
+    - The Android SDK, per-user in %LOCALAPPDATA%\Android\Sdk (x64 only): the
+      command-line tools, platform-tools and the newest stable platform and
+      build-tools, SDK licences accepted, ANDROID_HOME set - and JAVA_HOME, to
+      Android Studio's bundled JDK, when there is none
     - An architecture audit: the real PE machine type of every tool this box
       provisions, resolved the way a shell would. Informational, never fatal.
       Anything running emulated without a listed reason is called out.
     - An architecture audit: the real PE machine type of every tool this box
       provisions, resolved the way a shell would. Informational, never fatal.
       Anything running emulated without a listed reason is called out.
@@ -110,6 +114,13 @@ $RustHost   = switch ($HostArch) {
     default { $null }
 }
 
     default { $null }
 }
 
+# The Android SDK: where Android Studio puts it by default, unless ANDROID_HOME
+# already names one. "Latest" is read from Google's package index at run time,
+# the same index sdkmanager and Android Studio's SDK Manager read.
+$AndroidSdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
+$AndroidRepo    = 'https://dl.google.com/android/repository/'
+$AndroidIndex   = $AndroidRepo + 'repository2-3.xml'
+
 function Write-Step([string]$Msg) {
     Write-Host "`n==> $Msg" -ForegroundColor Cyan
 }
 function Write-Step([string]$Msg) {
     Write-Host "`n==> $Msg" -ForegroundColor Cyan
 }
@@ -221,6 +232,10 @@ function Install-BinSkim {
     # newest at the moment of the request.
     Write-Step 'BinSkim'
     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
     # newest at the moment of the request.
     Write-Step 'BinSkim'
     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+    # No progress bar: Windows PowerShell 5.1 redraws it per chunk, and on a
+    # download this size that, not the network, is the wait - see
+    # Install-AndroidSdk for the measurement. Local to this function.
+    $ProgressPreference = 'SilentlyContinue'
 
     $exe    = Join-Path $BinSkimDir 'BinSkim.exe'
     $marker = Join-Path $BinSkimDir 'nupkg-version.txt'
 
     $exe    = Join-Path $BinSkimDir 'BinSkim.exe'
     $marker = Join-Path $BinSkimDir 'nupkg-version.txt'
@@ -611,6 +626,239 @@ function Set-RustDefaultToolchain {
     }
 }
 
     }
 }
 
+function Set-UserEnvironmentVariable([string] $Name, [string] $Value) {
+    # HKCU, like Add-ToUserPath, so it outlives the script; written only when it
+    # differs, so a re-run changes nothing.
+    if ([Environment]::GetEnvironmentVariable($Name, 'User') -eq $Value) {
+        Write-Host "    $Name is already $Value"
+        return
+    }
+    [Environment]::SetEnvironmentVariable($Name, $Value, 'User')
+    Write-Host "    Set $Name = $Value (restart your shell to pick it up)."
+}
+
+function Get-AndroidRevision($Package) {
+    # A package's revision from the index, as a [version]: <major>, optional
+    # <minor> and <micro>. [int] of a missing element is 0.
+    $r = $Package.revision
+    [version]('{0}.{1}.{2}' -f [int]$r.major, [int]$r.minor, [int]$r.micro)
+}
+
+function ConvertTo-Revision([string] $Text) {
+    # "23.0" as the same three-part [version] the index gives, so the two
+    # compare. [version]'23.0' has no third part - it reads as -1 - and so
+    # counted as OLDER than 23.0.0: an installed, current copy looked stale.
+    $parts = @($Text.Trim() -split '\.' | Where-Object { $_ -ne '' })
+    while ($parts.Count -lt 3) { $parts += '0' }
+    [version]($parts[0..2] -join '.')
+}
+
+function Invoke-AndroidCli {
+    # The Android CLI (cmdline-tools\latest\bin\android.exe) against this SDK,
+    # its output shown as it comes - a first install downloads well over 100 MB
+    # and should not look stalled. Returns the exit code, $null if it would not
+    # start. EAP Continue for the call, as for the other native tools here.
+    #
+    # stdin is one empty line, so it is closed rather than the console: a
+    # prompt nobody expected gets "no" instead of an unattended run waiting on
+    # it forever.
+    param([string[]] $Arguments)
+    $cli = Join-Path $AndroidSdkRoot 'cmdline-tools\latest\bin\android.exe'
+    $prevEap = $ErrorActionPreference
+    $ErrorActionPreference = 'Continue'
+    try {
+        $global:LASTEXITCODE = $null
+        # Its stderr becomes error records under 2>&1 in Windows PowerShell,
+        # which would print as red "NativeCommandError" noise; back to plain
+        # lines first. The one JVM warning dropped is the bundled Java noting,
+        # on every call, a flag this Windows 10 does not support.
+        '' | & $cli "--sdk=$AndroidSdkRoot" @Arguments 2>&1 | ForEach-Object { "$_" } |
+            Where-Object { $_ -notmatch 'UseAllWindowsProcessorGroups flag is not supported' } | Out-Host
+        $LASTEXITCODE
+    } finally {
+        $ErrorActionPreference = $prevEap
+    }
+}
+
+function Install-AndroidSdk {
+    # The Android SDK for building apps, per-user, in Android Studio's default
+    # location (%LOCALAPPDATA%\Android\Sdk) unless ANDROID_HOME names another:
+    # the command-line tools, platform-tools, and the newest stable platform and
+    # build-tools. That is what Android Studio's first-run wizard would set up,
+    # done without the wizard - on the x64 laptop Studio was installed and that
+    # wizard had never run, so there was no SDK at all. Gradle fetches any other
+    # platform or build-tools a project asks for by itself, once the licences are
+    # accepted, so this is the floor rather than the whole set.
+    #
+    # "Newest stable" comes from Google's package index, read here each run:
+    # channel-0 (stable) and no <preview> revision, the platform by API level
+    # (android-37.2 beats android-37.1 beats android-36), build-tools by version.
+    #
+    # THE COMMAND-LINE TOOLS ARE NOT WHAT THEY WERE. From 23.0, sdkmanager is
+    # deprecated and sdkmanager.bat is a shim over the new Android CLI
+    # (cmdline-tools\latest\bin\android.exe, `android sdk install ...`), which on
+    # first use installs itself into %USERPROFILE%\.android (bin\android-cli.exe,
+    # about 90 MB, and cli\ - a bundled Java runtime, and a metrics spool). So
+    # the CLI is called directly here, with its own "platforms/android-37.2"
+    # package paths: no deprecated shim, and no cmd parsing to split the old
+    # "platforms;android-37.2" at its semicolon. `sdk install` installs a
+    # package or, if it is there, updates it to the latest - so it is also the
+    # re-run path, downloading nothing when everything is current.
+    #
+    # ITS EXIT CODE IS NOT TRUSTED. CLI 1.0.16406183 finishes every `sdk`
+    # command and then crashes on the way out, 0xC0000409 (a fail-fast in
+    # ucrtbase) - on the x64 laptop's Windows 10, every time, in a console or
+    # not, while `--version` and `info` exit 0. So success is judged by the
+    # result: each package asked for must have the package.xml the CLI writes
+    # once a package is complete. A nonzero exit with everything present is a
+    # note, not a failure; anything missing is a failure, whatever the exit.
+    #
+    # The command-line tools themselves are bootstrapped from the index's own
+    # "cmdline-tools;latest" archive (SHA-1 from the index), and replaced only
+    # when the index has a newer revision than the one in source.properties.
+    # The CLI cannot do that for them: the archive calls itself
+    # cmdline-tools;23.0, a fixed version, so it would never "update" it.
+    #
+    # The SDK licences are accepted on your behalf; the plan says so before
+    # anything runs. The CLI does it as it installs - sdkmanager --licenses is
+    # now a no-op - writing licenses\android-sdk-license. Gradle needs it to
+    # build, and to fetch what a project asks for.
+    #
+    # The CLI brings its own Java, so the SDK needs no JDK. Gradle does: when
+    # there is no JAVA_HOME, the one Android Studio bundles (jbr) becomes the
+    # user's, so a command-line gradlew works. An existing JAVA_HOME is never
+    # changed.
+    Write-Step "Android SDK in $AndroidSdkRoot"
+
+    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+    # No progress bar for the downloads. Windows PowerShell 5.1's
+    # Invoke-WebRequest redraws it per chunk, and that - not the network - is
+    # what a large download then waits on: measured on the x64 laptop, the
+    # 148 MB command-line tools took about ten minutes and five minutes of CPU
+    # with it on. Local to this function, so nothing else changes.
+    $ProgressPreference = 'SilentlyContinue'
+    [xml]$index = (Invoke-WebRequest -UseBasicParsing -Uri $AndroidIndex).Content
+    $stable = @($index.DocumentElement.remotePackage |
+                Where-Object { $_.channelRef.ref -eq 'channel-0' -and -not $_.revision.preview })
+
+    # --- the command-line tools: bootstrapped, and kept current, from the index ---
+    $ct      = @($stable | Where-Object { $_.path -eq 'cmdline-tools;latest' })[0]
+    if (-not $ct) { throw "No stable cmdline-tools;latest in $AndroidIndex." }
+    $ctWant  = Get-AndroidRevision $ct
+    $ctDir   = Join-Path $AndroidSdkRoot 'cmdline-tools\latest'
+    $ctHave  = $null
+    $props   = Join-Path $ctDir 'source.properties'
+    if (Test-Path $props) {
+        $m = [regex]::Match((Get-Content $props -Raw), '(?m)^Pkg\.Revision=(\d+(\.\d+){0,2})')
+        if ($m.Success) { $ctHave = ConvertTo-Revision $m.Groups[1].Value }
+    }
+    if ($ctHave -and $ctHave -ge $ctWant) {
+        Write-Host "    command-line tools $ctHave are current."
+    } else {
+        $archive = @($ct.archives.archive | Where-Object { $_.'host-os' -eq 'windows' })[0]
+        $sum     = $archive.complete.checksum
+        $sha1    = if ($sum -is [string]) { $sum } else { $sum.'#text' }
+        $url     = $AndroidRepo + $archive.complete.url
+        Write-Host "    command-line tools $(if ($ctHave) { "$ctHave -> " })$ctWant; downloading $($archive.complete.url)."
+        # PATH LENGTH. The archive nests deep - a guava jar in it sits well over
+        # 150 characters below its top - and Windows PowerShell 5.1 is bound by
+        # the 260-character MAX_PATH. So the temp directory's name is 8
+        # characters: %TEMP%\xxxxxxxx\cmdline-tools\... is then no deeper than
+        # %LOCALAPPDATA%\Android\Sdk\cmdline-tools\latest\..., and whenever the
+        # SDK's own path fits, unpacking it does too. (A 43-character name here
+        # made the unpack deeper than the SDK it was for.)
+        $tmp = Join-Path $env:TEMP ([guid]::NewGuid().ToString('N').Substring(0, 8))
+        New-Item -ItemType Directory -Force -Path $tmp | Out-Null
+        try {
+            $zip = Join-Path $tmp 'ct.zip'
+            Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $zip
+            $got = (Get-FileHash $zip -Algorithm SHA1).Hash.ToLower()
+            if ($sha1 -and $got -ne $sha1.ToLower()) { throw "SHA-1 mismatch for $url`: expected $sha1, got $got" }
+            Expand-Archive -Path $zip -DestinationPath $tmp -Force
+            # The archive holds a top-level cmdline-tools\ directory; the SDK
+            # layout wants it at cmdline-tools\latest. Replaced wholesale, as
+            # BinSkim is, so files dropped between releases do not linger - but
+            # the old copy is RENAMED aside first and deleted last. A rename is
+            # one operation whatever the depth below it; deleting walks every
+            # file and can trip over a path that is too long. Done this way, a
+            # delete that fails leaves a stray folder to report, not an SDK
+            # with no command-line tools in it. The aside name is six
+            # characters, like "latest", so the delete is no deeper either.
+            New-Item -ItemType Directory -Force -Path (Split-Path $ctDir) | Out-Null
+            $old = $null
+            if (Test-Path $ctDir) {
+                $old = Join-Path (Split-Path $ctDir) ('old' + [guid]::NewGuid().ToString('N').Substring(0, 3))
+                Rename-Item -Path $ctDir -NewName (Split-Path $old -Leaf)
+            }
+            Move-Item -Path (Join-Path $tmp 'cmdline-tools') -Destination $ctDir
+            if ($old) {
+                try { Remove-Item -Recurse -Force $old }
+                catch { Write-Warning "The new command-line tools are in place, but the old copy could not be removed: $old ($($_.Exception.Message))" }
+            }
+        } finally {
+            Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+        }
+    }
+
+    # --- the newest stable platform and build-tools, by the index ---
+    $platform = @($stable | ForEach-Object {
+                    $m = [regex]::Match($_.path, '^platforms;android-(\d+)(?:\.(\d+))?$')
+                    if ($m.Success) {
+                        [pscustomobject]@{ Path = $_.path; Api = [version]('{0}.{1}' -f $m.Groups[1].Value, [int]$m.Groups[2].Value) }
+                    }
+                } | Sort-Object Api)[-1].Path
+    $buildTools = @($stable | Where-Object { $_.path -match '^build-tools;\d+(\.\d+)*$' } |
+                    Sort-Object { [version]($_.path -replace '^build-tools;', '') })[-1].path
+    if (-not $platform -or -not $buildTools) { throw "Could not find a stable platform and build-tools in $AndroidIndex." }
+
+    # The index names packages "platforms;android-37.2"; the CLI, and the SDK's
+    # directories, use "platforms/android-37.2".
+    $packages = @('platform-tools', ($platform -replace ';', '/'), ($buildTools -replace ';', '/'))
+    Write-Host "    installing or updating: $($packages -join ', ')"
+    $rc = Invoke-AndroidCli -Arguments (@('sdk', 'install') + $packages)
+    $missing = @($packages | Where-Object { -not (Test-Path (Join-Path $AndroidSdkRoot (($_ -replace '/', '\') + '\package.xml'))) })
+    if ($missing) {
+        $why = if ($null -eq $rc) { 'it would not start' } else { "exit $rc" }
+        throw "android sdk install left out $($missing -join ', ') ($why)"
+    }
+    if ($null -eq $rc) {
+        Write-Warning 'The Android CLI would not start; the packages already installed were left as they were, not updated.'
+    } elseif ($rc -ne 0) {
+        Write-Host ("    note: the Android CLI exited {0} (0x{0:X8}) after finishing - its crash on the way out, not a failed" -f $rc) -ForegroundColor DarkGray
+        Write-Host '    install: every package asked for is in place.' -ForegroundColor DarkGray
+    }
+
+    # Report what is there now, from each package's own source.properties.
+    foreach ($pkg in $packages) {
+        $p = Join-Path $AndroidSdkRoot (($pkg -replace '/', '\') + '\source.properties')
+        $rev = if (Test-Path $p) { [regex]::Match((Get-Content $p -Raw), '(?m)^Pkg\.Revision=(.*)$').Groups[1].Value.Trim() } else { '?' }
+        Write-Host ('    {0,-28} {1}' -f $pkg, $rev)
+    }
+
+    Set-UserEnvironmentVariable 'ANDROID_HOME' $AndroidSdkRoot
+
+    # JAVA_HOME, for Gradle - only when there is none, and only to Android
+    # Studio's bundled JDK. Without Studio this just says what is missing.
+    if (-not [Environment]::GetEnvironmentVariable('JAVA_HOME', 'User') -and
+        -not [Environment]::GetEnvironmentVariable('JAVA_HOME', 'Machine')) {
+        $jbr = @((Join-Path $env:ProgramFiles 'Android\Android Studio\jbr'),
+                 (Join-Path $env:LOCALAPPDATA 'Programs\Android Studio\jbr') |
+                 Where-Object { Test-Path (Join-Path $_ 'bin\java.exe') })[0]
+        if ($jbr) {
+            Set-UserEnvironmentVariable 'JAVA_HOME' $jbr
+        } else {
+            Write-Warning 'No JAVA_HOME, and no Android Studio JDK to point it at: a command-line gradlew will need one. Install Android Studio (the AndroidStudio step) or a JDK.'
+        }
+    }
+
+    # The Android CLI (android), and the tools beside it (lint, d8, r8,
+    # apkanalyzer, the sdkmanager/avdmanager shims), in any shell.
+    # platform-tools is NOT added: the PlatformTools step already puts adb on the
+    # PATH, and two adb.exe on it would only make which one answers depend on
+    # the order.
+    Add-ToUserPath (Join-Path $ctDir 'bin')
+}
+
 function Get-PEMachine {
     # Architecture of a PE, read straight from the COFF header: the 2 bytes at
     # the e_lfanew offset + 4. Cheap, and it answers the only question that
 function Get-PEMachine {
     # Architecture of a PE, read straight from the COFF header: the 2 bytes at
     # the e_lfanew offset + 4. Cheap, and it answers the only question that
@@ -878,6 +1126,7 @@ $steps = @{
     NinjaPath     = { Set-NativeNinjaFirst }
     LlvmPath      = { Add-LlvmToUserPath }
     RustToolchain = { Set-RustDefaultToolchain }
     NinjaPath     = { Set-NativeNinjaFirst }
     LlvmPath      = { Add-LlvmToUserPath }
     RustToolchain = { Set-RustDefaultToolchain }
+    AndroidSdk    = { Install-AndroidSdk }
     ArchAudit     = { Invoke-ArchAudit }
 }
 
     ArchAudit     = { Invoke-ArchAudit }
 }
 
index 02c1515f502af33d06deb8d9e5a387e8261a95ef..4ff5786224f015d303c721417d3e5c99a8d29de4 100644 (file)
@@ -122,6 +122,8 @@ function Get-SetupCatalog {
         New-SetupStep CMake               M bat -Admin -Package Kitware.CMake -Text 'standalone, so a plain cmake works in any shell'
         New-SetupStep AndroidGPUInspector M bat -Admin -Package Google.AndroidGPUInspector `
             -X64Only 'x64-only build, and it profiles Android devices, not this box'
         New-SetupStep CMake               M bat -Admin -Package Kitware.CMake -Text 'standalone, so a plain cmake works in any shell'
         New-SetupStep AndroidGPUInspector M bat -Admin -Package Google.AndroidGPUInspector `
             -X64Only 'x64-only build, and it profiles Android devices, not this box'
+        New-SetupStep AndroidStudio       M bat -Admin -Package Google.AndroidStudio -Text 'only if missing; it updates itself' `
+            -X64Only 'Google publishes no Windows ARM64 build of Android Studio'
         New-SetupStep OpenCppCoverage     M bat -Admin -Package OpenCppCoverage.OpenCppCoverage -Text 'native line coverage' `
             -X64Only 'it steps x86/x64 instructions, so it cannot cover an ARM64 binary'
         New-SetupStep WiX                 M bat -Needs DotNetSdk `
         New-SetupStep OpenCppCoverage     M bat -Admin -Package OpenCppCoverage.OpenCppCoverage -Text 'native line coverage' `
             -X64Only 'it steps x86/x64 instructions, so it cannot cover an ARM64 binary'
         New-SetupStep WiX                 M bat -Needs DotNetSdk `
@@ -153,6 +155,9 @@ function Get-SetupCatalog {
         New-SetupStep LlvmPath            C user -Needs LLVM -Text 'upstream LLVM on the user PATH' `
             -X64Only 'LLVM is not installed on ARM64'
         New-SetupStep RustToolchain       C user -Needs Rustup, VisualStudio -Text $rust
         New-SetupStep LlvmPath            C user -Needs LLVM -Text 'upstream LLVM on the user PATH' `
             -X64Only 'LLVM is not installed on ARM64'
         New-SetupStep RustToolchain       C user -Needs Rustup, VisualStudio -Text $rust
+        New-SetupStep AndroidSdk          C user -Needs AndroidStudio `
+            -Text 'Android SDK, per-user: latest platform + build-tools; accepts the SDK licences' `
+            -X64Only 'its Windows build tools are x86/x64 only, as is Android Studio'
         New-SetupStep ArchAudit           C user -Text 'architecture audit: native or emulated, for every tool (read-only)'
 
         # --- setup-windows.bat, after both halves ---
         New-SetupStep ArchAudit           C user -Text 'architecture audit: native or emulated, for every tool (read-only)'
 
         # --- setup-windows.bat, after both halves ---
index 7445de5aa8ef4e6abe25a5898eec3cb421d8bdc9..23f2ff9b812536c1191b69f8ed57134441aab753 100644 (file)
@@ -95,6 +95,8 @@ if "%ELEV%"=="NOLUA" (
 @rem   VS Clang/LLVM component          with-uac  see $ClangComponents\r
 @rem   Windows Driver Kit               with-uac  see the WDK step\r
 @rem   BinSkim                          no-uac    NuGet publishes win-x64 only\r
 @rem   VS Clang/LLVM component          with-uac  see $ClangComponents\r
 @rem   Windows Driver Kit               with-uac  see the WDK step\r
 @rem   BinSkim                          no-uac    NuGet publishes win-x64 only\r
+@rem   Google.AndroidStudio             here      no Windows ARM64 build\r
+@rem   Android SDK                      no-uac    Windows build tools are x86/x64 only\r
 @rem\r
 @rem The decision itself is made in ONE place, setup-windows-steps.ps1, which\r
 @rem marks each of these -X64Only with its reason. The plan shows them as [-]\r
 @rem\r
 @rem The decision itself is made in ONE place, setup-windows-steps.ps1, which\r
 @rem marks each of these -X64Only with its reason. The plan shows them as [-]\r
@@ -378,6 +380,27 @@ call :winget DotNetSdk Microsoft.DotNet.SDK.10
 call :winget CMake Kitware.CMake\r
 call :winget AndroidGPUInspector Google.AndroidGPUInspector\r
 \r
 call :winget CMake Kitware.CMake\r
 call :winget AndroidGPUInspector Google.AndroidGPUInspector\r
 \r
+@rem --- Android Studio ---\r
+@rem\r
+@rem Machine-wide: the manifest's NSIS installer elevates itself and installs to\r
+@rem %ProgramFiles%\Android\Android Studio. x64 only - there is no ARM64 build -\r
+@rem so the plan keeps it off ARM64.\r
+@rem\r
+@rem Installed only when it is missing, which setup-windows-current.ps1 decides.\r
+@rem Android Studio updates itself, in the app, and the winget package is published\r
+@rem with UpgradeBehavior: deny and RequireExplicitUpgrade, so winget would never\r
+@rem upgrade it anyway. What the check prevents is the other case: a Studio that\r
+@rem was installed some other way - as on the x64 laptop, where winget lists it\r
+@rem only by its Add/Remove Programs key - is one winget cannot match to the\r
+@rem package, so `winget install` would run the installer over it.\r
+@rem\r
+@rem It is also where the per-user Android SDK step gets its JDK: Studio bundles\r
+@rem one (jbr), and the SDK's sdkmanager and Gradle both need Java.\r
+call :want AndroidStudio || goto :after_studio\r
+"%PS64%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-current.ps1" -Step AndroidStudio -HostArch "%HOST_ARCH%"\r
+if not "%ERRORLEVEL%"=="0" call :winget AndroidStudio Google.AndroidStudio\r
+:after_studio\r
+\r
 @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
 @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
 @rem which it reads). The installer elevates via UAC.\r
 @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
 @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
 @rem which it reads). The installer elevates via UAC.\r
@@ -687,9 +710,9 @@ if "%IS_ARM64%"=="1" (
     echo [setup-windows]                 compiler by design; clang-cl and upstream LLVM are x64-box only.\r
     echo [setup-windows]                 Rust stable-aarch64-pc-windows-msvc, native, linking through MSVC.\r
     echo [setup-windows]   emulated    : iperf3 ^(x64^) and adb ^(x86^), both bound by their link, not the CPU.\r
     echo [setup-windows]                 compiler by design; clang-cl and upstream LLVM are x64-box only.\r
     echo [setup-windows]                 Rust stable-aarch64-pc-windows-msvc, native, linking through MSVC.\r
     echo [setup-windows]   emulated    : iperf3 ^(x64^) and adb ^(x86^), both bound by their link, not the CPU.\r
-    echo [setup-windows]   not here    : Brave, WinMerge, LLVM, NASM, AGI, OpenCppCoverage, BinSkim\r
-    echo [setup-windows]                 and the WDK - dropped on ARM64; the plan above says why.\r
-    echo [setup-windows]   unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune,\r
+    echo [setup-windows]   not here    : Brave, WinMerge, LLVM, NASM, AGI, OpenCppCoverage, BinSkim,\r
+    echo [setup-windows]                 the WDK and the Android SDK - dropped on ARM64; the plan above says why.\r
+    echo [setup-windows]   unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune, Android Studio,\r
     echo [setup-windows]                 and the v141 / Windows XP targeting toolset.\r
 )\r
 @rem The list above is what this script PROVIDES on ARM64, not necessarily what\r
     echo [setup-windows]                 and the v141 / Windows XP targeting toolset.\r
 )\r
 @rem The list above is what this script PROVIDES on ARM64, not necessarily what\r