From f0c8b2f90d7a0da109ac912f8d8760f41eb390e0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 13:29:59 +0000
Subject: [PATCH 1/8] Update Android SDK package pins
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
Configuration.props | 25 +++++++++++++------------
src/androidsdk/androidsdk.targets | 10 +++++++---
2 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/Configuration.props b/Configuration.props
index 16c7932f302..5a5fff0b53b 100644
--- a/Configuration.props
+++ b/Configuration.props
@@ -106,17 +106,18 @@
EE39AD5967E95C2A07F04DBCBDE96B1A0C916BA376096DB5D2F498B7727A5D1D
D230F13842F60F782A8645F9C813F8F845BF36089EA7289F28C48F17979313F1
45F4D63113E895EBDE0C90F194099A4676B6AC653BD28D54314A9E022BBC1A99
- 5673201E6F3869F418EEED3B5CB6C4BE7401502BD0AAE1B12A29D164D647A54E
- 7EC965280A073311C339E571CD5DE778B9975026CFCBE79F2B1CDCB1E15317EE
- 98B565CB657B012DAE6794CEFC0F66AE1EFB4690C699B78A614B4A6A3505B003
+ 7E601C04E7173754B3D051A87BB4C3A8243C65A9D7BFA9356ED6499FB8953FA3
+ 58DA8F215781CC0208743E88B65AE259780C55E7EB40E50373D377CF369232D6
+ 0877A1D048FE4A24EFE2EFF536CA4223F7ADEB58648BB81909D33C446918CFA8
+ E5885E2E59038C0778A85FC19F01DE7CE7C567C105553F7617B936B1E0E87B2B
130B80221ABDA282B96E3669CEF5DCCF0D916E988E08051F5DACCDD03360A1AC
46BDA18F0F3C28A55F88DA4BA05FEC21ABE3D9BCA0A78A8B18D444987B3DC817
3A78AD82A3F3E9E613E255618D2ACF1B3C4BDB24EF6E1F9341227CF6FC492A89
- 3029489D4B2F868DC5347AA27B1E52FB48D6D28ABD51278E3FBEC2EE73201EDF
- EDA85B9BFFD0926B7A01EA334A30AB41827F573CBDE8F7F97FCD8C5B38F039F6
- 58BFDF0572F06EC007A7287154CDC6B2EBFEA947D07A1E26A0F6B5E1A39E9191
- 9F2855BFE98E80728E61D45B0EA72BA22F64952C9330559FA4E2CD3698A32231
+ E79BFE36609EC8FBC4EA3928B514BD9CDDB508697B9F939EEF8E028AE2EC2C6D
+ E1136F23185B3D17A675D8C1E167A9B6D3AA95C3C6B834371967FC8ECC02A315
+ B93886AEEAA264E4CD0CC9AD57428DF8FCCB33F17E71392428F5DD221877A97E
+ 666312FA611C8751A4266EF0B3981F68A126E3C58225FB44415CED038301ECDA
B5C3FDA1F4B4931C30518D342E4AD5F7464945E0CDCED3538D4FF2E12F7BF201
7FD1610E16A69AC2E202278E82077DE134CE404FE099D872019F69F3A340ACDD
@@ -125,7 +126,7 @@
A3F91808DCE50C1717737DE90C18479ED3A78B147E06985247D138E7AB5123D0
68DB2690CB92E4EE5373AC9B792642C90717D8F417D83ECCEA48781171B3182A
- A853F452B8BA94933EB56ED4E0F622991F9D6DE6E0AC19923AA47414D79B9129
+ F63736E27B0D283298116DA8BCE2C024DE28C7E4081044B30716FCB6E0FA13F1
<_XAAndroidNdkRelease>28c
@@ -180,12 +181,12 @@
avdmanager.bat
$(AndroidSdkFullPath)\tools
$(AndroidToolPath)\bin
- 19.0
- 13114758_latest
+ 23.0
+ 16111833_latest
$(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)\bin
- 15004761
- 36.4.10
+ 16079175
+ 37.2.5
$(AndroidSdkFullPath)\emulator
emulator
emulator.exe
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index 4768da3cf37..bddf3d7abd3 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -104,11 +104,15 @@
-
- <_AndroidSdkPackage Include="commandlinetools-mac-$(CommandLineToolsVersion).zip" Condition=" '$(HostOS)' == 'Darwin' ">
+
+ <_AndroidSdkPackage Include="commandlinetools-mac_x86_64-$(CommandLineToolsVersion).zip" Condition=" '$(HostOS)' == 'Darwin' and '$(_IsArm64Apple)' != 'true' ">
$(XACmdlineToolsHashMacOS)
$(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)
+ <_AndroidSdkPackage Include="commandlinetools-mac_arm64-$(CommandLineToolsVersion).zip" Condition=" '$(_IsArm64Apple)' == 'true' ">
+ $(XACmdlineToolsHashMacOSArm64)
+ $(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)
+
<_AndroidSdkPackage Include="commandlinetools-linux-$(CommandLineToolsVersion).zip" Condition=" '$(HostOS)' == 'Linux' ">
$(XACmdlineToolsHashLinux)
$(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)
@@ -239,7 +243,7 @@
$(XAAndroidDocsHash)
$(AndroidSdkFullPath)\docs
- <_AndroidSdkPackage Include="source-37.0_r02.zip">
+ <_AndroidSdkPackage Include="source-37.1_r01.zip">
$(XAAndroidSourcesHash)
$(AndroidSdkFullPath)\sources\android-37
From 56f18cba13b1b6e22df656a684209dd6dc196bd8 Mon Sep 17 00:00:00 2001
From: Jonathan Peppers
Date: Wed, 19 Aug 2026 13:55:38 -0500
Subject: [PATCH 2/8] [androidsdk] Create licenses directory before marker
Command-line tools 23.0 no longer creates the licenses directory when --licenses is invoked, causing the marker Touch task to fail on all CI hosts.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
src/androidsdk/androidsdk.targets | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index bddf3d7abd3..0b609b49e51 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -494,6 +494,7 @@
EnvironmentVariables="JAVA_HOME=$(JavaSdkDirectory)"
IgnoreExitCode="true"
/>
+
From c7d920df2125a12b96adaf286498a6fc6d799782 Mon Sep 17 00:00:00 2001
From: Jonathan Peppers
Date: Wed, 19 Aug 2026 13:57:51 -0500
Subject: [PATCH 3/8] [androidsdk] Preserve sources for stable platforms
Keep separate source packages, hashes, and destinations for Android 37.0 and 37.1. Update the package refresh skill and its regression coverage so future stable source additions do not replace existing platform sources.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../update-androidsdk-packages/SKILL.md | 23 +++++++++++++------
.../evals/evals.json | 6 +++++
.../references/package-catalog.md | 4 ++--
Configuration.props | 3 ++-
src/androidsdk/androidsdk.targets | 8 +++++--
5 files changed, 32 insertions(+), 12 deletions(-)
diff --git a/.github/skills/update-androidsdk-packages/SKILL.md b/.github/skills/update-androidsdk-packages/SKILL.md
index eb782b62fb0..dd0797d6058 100644
--- a/.github/skills/update-androidsdk-packages/SKILL.md
+++ b/.github/skills/update-androidsdk-packages/SKILL.md
@@ -19,7 +19,7 @@ lives in two files:
Google republishes tool revisions on their own cadence; this skill brings those two files back in
sync with Google's *current stable* releases with a minimal, reviewable diff — matching the shape of PR #12371, which did exactly this (build-tools/platform-tools/cmdline-tools/cmake/emulator/sources/platform revisions all bumped, hashes recomputed, and per-arch macOS cmdline-tools support added when Apple Silicon archives showed up).
-## Two hard rules — read these before touching anything
+## Three hard rules — read these before touching anything
**1. Never touch the Android NDK.** `_XAAndroidNdkRelease`, `_XAAndroidNdkPkgRevision`, and every
`XAAndroidNdkHash*` property in `Configuration.props`, plus the `android-ndk-r$(_XAAndroidNdkRelease)-*`
@@ -44,6 +44,14 @@ nothing else in the catalog needed updating this run. Silence here is a bug: the
rule is that a human decides whether/when to onboard a new API level, and they can't decide on
something they were never told about.
+**3. Preserve the sources package for every shipped stable platform.** Source archives are additive,
+not a single "latest" package: `sources;android-37.0` and `sources;android-37.1` install into distinct
+SDK directories and both are needed when both platform levels are marked `IsLatestStable`. Never
+replace or remove an existing `source-NN.N_rMM.zip` entry when a new stable platform source appears.
+Keep one `_AndroidSdkPackage` entry and one version-specific hash property per stable platform level,
+and preserve the full API level in `Destination` (`sources\android-37.0` uses the historical
+`sources\android-37` directory; `sources\android-37.1` uses `sources\android-37.1`).
+
## Workflow
### 1. Read the current catalog
@@ -126,11 +134,12 @@ one shared macOS zip still covers both).
extension-level suffix too when Google has published one for an API level that already uses it
(e.g. `platform-34-ext7_r02` → `platform-34-ext12_r01`) — do not introduce an extension suffix for
an API level that never had one, or vice versa, without a clear reason from the manifest.
-- The `source-NN_r0M.zip` sources package and `XAAndroidSourcesHash` should track whichever API level
- is `IsLatestStable="true"` in `_PlatformPackage` (the `` uses the integer API level,
- e.g. `\sources\android-37`, even when the catalog entry is `platform-37.0_r01`). Update both the
- zip name/Destination and the hash
- together if the latest stable API level's source archive changed.
+- Keep a `source-NN.N_r0M.zip` package for every API level marked `IsLatestStable` in
+ `_PlatformPackage`. Treat a newly published stable source as an addition, not a replacement.
+ Give each archive a version-specific hash property such as `XAAndroidSourcesHash37_0`, and use
+ the platform's distinct SDK directory as `Destination` (`37.0` historically maps to
+ `\sources\android-37`; `37.1` maps to `\sources\android-37.1`). Update an existing entry in place
+ only when Google publishes a newer revision for that same API level.
### 5. Validate before finishing
@@ -154,7 +163,7 @@ dotnet build src/androidsdk/androidsdk.csproj --no-restore -v:minimal -t:_AddPla
Also check:
- **XML validity** — both edited files still parse (`dotnet build` will fail loudly on malformed XML, but a quick sanity check like `powershell -Command "[xml](Get-Content src/androidsdk/androidsdk.targets)"` catches issues faster).
- **Diff cleanliness** — `git status` and `git diff` should show changes *only* in `Configuration.props` and `src/androidsdk/androidsdk.targets`. This skill's scope is package pins, not the generated-package-xml template (`package.xml.in`) — if a routine refresh seems to require touching that file too, stop and flag it rather than including it, since automated runs of this skill (e.g. the `skill-runner` workflow) are only authorized to change the two files above. No stray temp files from hashing (the `sha256_of_url.cs` script cleans up after itself; double check if you downloaded anything manually instead).
-- **The two hard rules above** — diff the NDK properties and the `_PlatformPackage` item count/API-level set against `git diff` to confirm neither was touched/expanded.
+- **The three hard rules above** — diff the NDK properties and the `_PlatformPackage` item count/API-level set against `git diff`, then verify every `IsLatestStable` platform has its own sources package and destination.
- **Formatting** — match the existing tab indentation and column alignment in both files (several `_PlatformPackage`/`_AndroidSdkPackage` lines are hand-aligned with extra spaces before ``/`` — preserve that style rather than reformatting the whole block).
### 6. Summarize what changed
diff --git a/.github/skills/update-androidsdk-packages/evals/evals.json b/.github/skills/update-androidsdk-packages/evals/evals.json
index 2c129bea97d..f36256f4fa0 100644
--- a/.github/skills/update-androidsdk-packages/evals/evals.json
+++ b/.github/skills/update-androidsdk-packages/evals/evals.json
@@ -24,6 +24,12 @@
"prompt": "our CI machines are on an old command-line tools version (19.0 / 13114758_latest) and I keep seeing warnings that a newer cmdline-tools package is available with separate mac_arm64/mac_x86_64 downloads now — can you get us current, and check whether Android platform 36.1 has a newer revision published too?",
"expected_output": "CommandLineToolsFolder/CommandLineToolsVersion bumped to the current stable release, with the command-line tools _AndroidSdkPackage split into a mac_x86_64 item (gated on _IsArm64Apple != 'true') and a new mac_arm64 item (gated on _IsArm64Apple == 'true') backed by a new XACmdlineToolsHashMacOSArm64 property, following the existing emulator/system-image pattern for _IsArm64Apple. The existing platform-36.1_r01 _PlatformPackage entry is checked against the manifest and its revision/hash updated in place if a newer one exists (no new API level added). Validation runs the BootstrapTasks build before the androidsdk.csproj build.",
"assertions": []
+ },
+ {
+ "id": 5,
+ "prompt": "Both platform 37.0 and 37.1 are marked IsLatestStable in androidsdk.targets. Refresh the Android sources packages to the current revisions.",
+ "expected_output": "Both source-37.0 and source-37.1 _AndroidSdkPackage entries remain present with separate version-specific SHA-256 properties. The 37.0 package installs to sources/android-37 and the 37.1 package installs to sources/android-37.1. Updating 37.1 never removes or overwrites the 37.0 package.",
+ "assertions": []
}
]
}
diff --git a/.github/skills/update-androidsdk-packages/references/package-catalog.md b/.github/skills/update-androidsdk-packages/references/package-catalog.md
index 0b31b232d02..0817f260086 100644
--- a/.github/skills/update-androidsdk-packages/references/package-catalog.md
+++ b/.github/skills/update-androidsdk-packages/references/package-catalog.md
@@ -15,8 +15,8 @@ drift as the skill is used.
| API 29 system image | `sys-img/android` manifest, `path="system-images;android-29;default;{x86_64,arm64-v8a}"` | (fixed `x86_64-29_r08*`/`arm64-v8a-29_r08` filenames — check manifest for a newer `rNN` if refreshing) | `XASystemImageHashMacOSx64/MacOSArm64/Linux/Windows` | `{x86_64,arm64-v8a}-29_r08{-darwin,-linux,-windows,}.zip` under `sys-img/android/` |
| m2repository | `extras;android;m2repository` | (embedded in filename, e.g. `_r47`) | `XAAndroidM2RepositoryHash` | `android_m2repository_r47.zip`, host-agnostic |
| docs | `docs` | (embedded in filename, e.g. `-24_r01`) | `XAAndroidDocsHash` | `docs-24_r01.zip`, host-agnostic |
-| sources | `sources;android-NN` (tracks the latest stable platform) | (embedded in filename) | `XAAndroidSourcesHash` | `source-_r0M.zip`, `Destination` embeds the API level too |
-| platform APIs | `platforms;android-NN` | n/a — `_PlatformPackage` item's `Include` *is* the version string | `Hash` metadata per `_PlatformPackage` item | `_PlatformPackage` item group near the top of the file; one `IsLatestStable="true"` entry drives default install + the sources package above |
+| sources | `sources;android-NN.N` (one per stable shipped platform) | (embedded in filename) | Version-specific, e.g. `XAAndroidSourcesHash37_0` | One `source-NN.N_r0M.zip` entry per `IsLatestStable` platform; preserve each distinct `Destination` (`37.0` historically uses `sources\android-37`, while `37.1` uses `sources\android-37.1`) |
+| platform APIs | `platforms;android-NN` | n/a — `_PlatformPackage` item's `Include` *is* the version string | `Hash` metadata per `_PlatformPackage` item | `_PlatformPackage` item group near the top of the file; every `IsLatestStable="true"` entry drives default install and requires a corresponding sources package |
| **Android NDK — OUT OF SCOPE** | `ndk` | `_XAAndroidNdkRelease`, `_XAAndroidNdkPkgRevision` | `XAAndroidNdkHashMacOS/Linux/Windows` | `android-ndk-r$(_XAAndroidNdkRelease)-$(_NdkHostTag).zip` — **never edit as part of this skill** |
## Notes on Apple Silicon archives
diff --git a/Configuration.props b/Configuration.props
index 5a5fff0b53b..2c1f257ad6c 100644
--- a/Configuration.props
+++ b/Configuration.props
@@ -126,7 +126,8 @@
A3F91808DCE50C1717737DE90C18479ED3A78B147E06985247D138E7AB5123D0
68DB2690CB92E4EE5373AC9B792642C90717D8F417D83ECCEA48781171B3182A
- F63736E27B0D283298116DA8BCE2C024DE28C7E4081044B30716FCB6E0FA13F1
+ A853F452B8BA94933EB56ED4E0F622991F9D6DE6E0AC19923AA47414D79B9129
+ F63736E27B0D283298116DA8BCE2C024DE28C7E4081044B30716FCB6E0FA13F1
<_XAAndroidNdkRelease>28c
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index 0b609b49e51..5cf89596a9c 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -243,10 +243,14 @@
$(XAAndroidDocsHash)
$(AndroidSdkFullPath)\docs
- <_AndroidSdkPackage Include="source-37.1_r01.zip">
- $(XAAndroidSourcesHash)
+ <_AndroidSdkPackage Include="source-37.0_r02.zip">
+ $(XAAndroidSourcesHash37_0)
$(AndroidSdkFullPath)\sources\android-37
+ <_AndroidSdkPackage Include="source-37.1_r01.zip">
+ $(XAAndroidSourcesHash37_1)
+ $(AndroidSdkFullPath)\sources\android-37.1
+
@@ -480,25 +483,40 @@
BeforeTargets="Build"
DependsOnTargets="_ExtractAndroidSdkPackages"
Inputs="$(MSBuildThisFileFullPath);$(MSBuildProjectFullPath);..\..\Configuration.props"
- Outputs="$(_LicensesAcceptedFile)">
+ Outputs="$(_AndroidSdkLicenseFile);$(_LicensesAcceptedFile)">
-
+
+
+
-
+
From dc70072e7b8864fee05ab74dfe5f0d0c35c0b60c Mon Sep 17 00:00:00 2001
From: Jonathan Peppers
Date: Thu, 20 Aug 2026 10:37:12 -0500
Subject: [PATCH 5/8] [androidsdk] Reject stale license acceptance
Delete the existing Android SDK license before invoking the license tool so the postcondition proves the current invocation recreated it, even when the Android CLI's unreliable exit code must be ignored.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
src/androidsdk/androidsdk.targets | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index 29d5235bcf3..f225bd5182b 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -489,6 +489,7 @@
Text="Neither android nor sdkmanager was found in $(_SdkManagerDir)"
/>
+
Date: Thu, 20 Aug 2026 10:38:41 -0500
Subject: [PATCH 6/8] [androidsdk] Document Android CLI exit handling
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
src/androidsdk/androidsdk.targets | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index f225bd5182b..d84ef3a8769 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -490,6 +490,8 @@
/>
+
Date: Thu, 20 Aug 2026 15:36:39 -0500
Subject: [PATCH 7/8] [androidsdk] Harden SDK package refresh
Use stable emulator packages, align command-line tools across bootstrap and product installs, and make license acceptance deterministic and atomic.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e41ab6f-5ce8-4ee9-aea2-2dfb541e2592
---
.../update-androidsdk-packages/SKILL.md | 47 ++++++--
.../evals/evals.json | 14 ++-
.../references/package-catalog.md | 14 ++-
Configuration.props | 13 ++-
.../EnsureAndroidSdkLicense.cs | 106 ++++++++++++++++++
.../Feeds/AndroidManifestFeed_d18.0.xml | 9 +-
.../Xamarin.Installer.Common.props | 2 +-
src/androidsdk/androidsdk.targets | 64 ++++-------
8 files changed, 200 insertions(+), 69 deletions(-)
create mode 100644 build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/EnsureAndroidSdkLicense.cs
diff --git a/.github/skills/update-androidsdk-packages/SKILL.md b/.github/skills/update-androidsdk-packages/SKILL.md
index 9ad616e43a8..a6f455abb9c 100644
--- a/.github/skills/update-androidsdk-packages/SKILL.md
+++ b/.github/skills/update-androidsdk-packages/SKILL.md
@@ -19,7 +19,7 @@ lives in two files:
Google republishes tool revisions on their own cadence; this skill brings those two files back in
sync with Google's *current stable* releases with a minimal, reviewable diff — matching the shape of PR #12371, which did exactly this (build-tools/platform-tools/cmdline-tools/cmake/emulator/sources/platform revisions all bumped, hashes recomputed, and per-arch macOS cmdline-tools support added when Apple Silicon archives showed up).
-## Three hard rules — read these before touching anything
+## Seven hard rules — read these before touching anything
**1. Never touch the Android NDK.** `_XAAndroidNdkRelease`, `_XAAndroidNdkPkgRevision`, and every
`XAAndroidNdkHash*` property in `Configuration.props`, plus the `android-ndk-r$(_XAAndroidNdkRelease)-*`
@@ -52,6 +52,29 @@ Keep one `_AndroidSdkPackage` entry and one version-specific hash property per s
and preserve the full API level in `Destination` (`sources\android-37.0` uses the historical
`sources\android-37` directory; `sources\android-37.1` uses `sources\android-37.1`).
+**4. Stable means Google's stable channel.** Never select an emulator or other tool from a
+development, canary, beta, or preview channel merely because its revision sorts higher. Require
+`channel-0` for emulator updates. If a package's channel metadata and release labeling disagree,
+do not update it unattended; report the ambiguity instead.
+
+**5. Command-line tools are a coordinated product dependency.** Do not update only the bootstrap
+pins. A command-line-tools bump must also update
+`src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props` and the matching latest entry in
+`src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml`, including every published
+host/architecture archive. Confirm `CodeGenerator.targets` tracks the property file that supplies
+`AndroidCommandLineToolsVersion`. If the automated workflow is not authorized to change every
+required file, stop and report the coordinated update instead of opening a partial PR.
+
+**6. Never execute an unverified downloader to accept licenses.** License acceptance must not run
+the `android` bootstrapper, `sdkmanager`, or any payload fetched at execution time. Preserve all
+existing valid fingerprints, add the pinned expected fingerprint under a cross-process lock, write
+atomically, validate every line as a 40-character SHA-1 fingerprint, and create the acceptance
+marker only after validation succeeds.
+
+**7. Extraction outputs identify the exact archive.** Packages for different hosts or architectures
+may share a destination. Their incremental output stamp must include both archive identity and
+expected SHA-256; `source.properties` alone is not a safe extraction sentinel.
+
## Workflow
### 1. Read the current catalog
@@ -77,14 +100,11 @@ dotnet run .github/skills/update-androidsdk-packages/scripts/fetch_repo_package.
(These are C# file-based apps, matching the `ci_failures.cs` convention used by the `ci-status` skill — first run restores/builds, so allow a few extra seconds.)
-The script sorts matches by revision (newest first) and flags anything whose path/display-name
-looks like a preview build. **Treat that flag as a hint, not ground truth** — Google's
-`channelRef` metadata is not a reliable stable/preview signal by itself (some genuinely-stable
-packages carry a non-zero channel id, and freshly-promoted stable packages can briefly still show
-old channel numbers). Cross-check the display name and version string yourself: a real stable
-release reads like `36.0.1` or `28c`, not `37.0.0-rc1`, `2025.09.15-alpha01`, or anything with
-`beta`/`canary`/`preview` in it. When genuinely unsure whether a release is stable, prefer the
-previous confirmed-stable revision over guessing.
+The script sorts matches by revision (newest first) and reports each package's channel. Require
+`channel-0` for emulator updates and reject version/display names containing `alpha`, `beta`,
+`canary`, `dev`, `preview`, or `rc`. For other package families, a non-zero channel or conflicting
+metadata is ambiguous: keep the previous confirmed-stable revision and report it rather than
+guessing.
Reference `references/package-catalog.md` for the mapping between each `androidsdk.targets` entry,
its manifest `path`, and its `Configuration.props` properties — it documents the current package
@@ -140,6 +160,9 @@ one shared macOS zip still covers both).
the platform's distinct SDK directory as `Destination` (`37.0` historically maps to
`\sources\android-37`; `37.1` maps to `\sources\android-37.1`). Update an existing entry in place
only when Google publishes a newer revision for that same API level.
+- When command-line tools changes, update the shipped product version and feed entry described in
+ hard rule 5 in the same change. Do not leave bootstrap and product dependency versions split.
+- Keep archive/hash-specific extraction stamps intact when adding host or architecture variants.
### 5. Validate before finishing
@@ -162,9 +185,9 @@ dotnet build src/androidsdk/androidsdk.csproj --no-restore -v:minimal -t:_AddPla
Also check:
- **XML validity** — both edited files still parse (`dotnet build` will fail loudly on malformed XML, but a quick sanity check like `powershell -Command "[xml](Get-Content src/androidsdk/androidsdk.targets)"` catches issues faster).
-- **Diff cleanliness** — `git status` and `git diff` should show changes *only* in `Configuration.props` and `src/androidsdk/androidsdk.targets`. This skill's scope is package pins, not the generated-package-xml template (`package.xml.in`) — if a routine refresh seems to require touching that file too, stop and flag it rather than including it, since automated runs of this skill (e.g. the `skill-runner` workflow) are only authorized to change the two files above. No stray temp files from hashing (the `sha256_of_url.cs` script cleans up after itself; double check if you downloaded anything manually instead).
-- **The three hard rules above** — diff the NDK properties and the `_PlatformPackage` item count/API-level set against `git diff`, then verify every `IsLatestStable` platform has its own sources package and destination.
-- **Command-line tools compatibility** — when bumping command-line tools, test with `licenses/android-sdk-license` and `.licenses-accepted` absent. Newer releases may keep `sdkmanager` only as a deprecated compatibility shim; a successful `sdkmanager --licenses` exit does not prove that Gradle's license file was created. Confirm `_AcceptAndroidSdkLicenses` recreates both files, then run an Android Gradle project test or otherwise verify Gradle recognizes the Build Tools and platform licenses.
+- **Diff cleanliness** — no unrelated files or stray temp downloads. Routine families remain scoped to `Configuration.props` and `src/androidsdk/androidsdk.targets`; command-line-tools updates additionally require the two shipped-product files in hard rule 5. Never edit `package.xml.in` during a routine refresh.
+- **The seven hard rules above** — diff the NDK properties and the `_PlatformPackage` item count/API-level set, verify every `IsLatestStable` platform has its own sources package, verify selected releases are stable, and verify extraction outputs remain archive/hash-specific.
+- **Command-line tools compatibility** — test with `licenses/android-sdk-license` and `.licenses-accepted` absent, with a pre-existing unrelated valid fingerprint, and with malformed content. Confirm the expected pinned fingerprint is created, the unrelated fingerprint is preserved, malformed content fails, writes are atomic/locked, and Gradle recognizes the Build Tools and platform licenses. No license-acceptance path may execute a network-capable Android CLI.
- **Formatting** — match the existing tab indentation and column alignment in both files (several `_PlatformPackage`/`_AndroidSdkPackage` lines are hand-aligned with extra spaces before ``/`` — preserve that style rather than reformatting the whole block).
### 6. Summarize what changed
diff --git a/.github/skills/update-androidsdk-packages/evals/evals.json b/.github/skills/update-androidsdk-packages/evals/evals.json
index 56f8f288c93..1fd69286fb2 100644
--- a/.github/skills/update-androidsdk-packages/evals/evals.json
+++ b/.github/skills/update-androidsdk-packages/evals/evals.json
@@ -34,7 +34,19 @@
{
"id": 6,
"prompt": "Update command-line tools from 19.0 to a release where sdkmanager says --licenses is deprecated and exits successfully without creating the SDK license files.",
- "expected_output": "The package pins and hashes are updated, and license acceptance uses the replacement Android CLI when available while retaining compatibility with older sdkmanager releases. Validation starts without licenses/android-sdk-license or .licenses-accepted, proves both are recreated, and confirms Gradle accepts the pinned Build Tools and platform licenses.",
+ "expected_output": "Bootstrap pins, Xamarin.Installer.Common.props, and the installer manifest feed are updated together for every host/architecture. License acceptance invokes neither sdkmanager nor the network-capable Android CLI bootstrapper: it atomically adds the pinned expected fingerprint under a cross-process lock while preserving existing valid fingerprints and rejecting malformed files. Validation starts without either license file, then repeats with an unrelated valid fingerprint and malformed content, and confirms Gradle accepts the pinned Build Tools and platform licenses.",
+ "assertions": []
+ },
+ {
+ "id": 7,
+ "prompt": "The manifest lists emulator 37.2.5 on channel-2 and emulator 37.1.11 on channel-0. Refresh us to the newest emulator.",
+ "expected_output": "Emulator 37.1.11 is selected because channel-0 is required for emulator updates. The newer 37.2.5 development-channel package is explicitly rejected. All four stable host/architecture archives are hashed and pinned.",
+ "assertions": []
+ },
+ {
+ "id": 8,
+ "prompt": "Add a new macOS arm64 command-line-tools archive. It extracts to the same SDK directory as the x64 archive, so source.properties can stay as the shared output stamp.",
+ "expected_output": "The arm64 archive may share the destination, but source.properties is rejected as the sole incremental output. Extraction uses a stamp containing archive identity and expected SHA-256 so switching architectures or hashes forces replacement with the correct package.",
"assertions": []
}
]
diff --git a/.github/skills/update-androidsdk-packages/references/package-catalog.md b/.github/skills/update-androidsdk-packages/references/package-catalog.md
index 0817f260086..667fef1ab9b 100644
--- a/.github/skills/update-androidsdk-packages/references/package-catalog.md
+++ b/.github/skills/update-androidsdk-packages/references/package-catalog.md
@@ -9,7 +9,7 @@ drift as the skill is used.
|---|---|---|---|---|
| build-tools | `build-tools` | `XABuildToolsVersion`, `XABuildToolsFolder` | `XABuildToolsHashMacOS/Linux/Windows` | `build-tools_r$(XABuildToolsVersion)_{macosx,linux,windows}.zip` |
| platform-tools | `platform-tools` | `XAPlatformToolsVersion` | `XAPlatformToolsHashMacOS/Linux/Windows` | `platform-tools_r$(XAPlatformToolsVersion)-{darwin,linux,win}.zip` |
-| cmdline-tools | `cmdline-tools` | `CommandLineToolsFolder`, `CommandLineToolsVersion` | `XACmdlineToolsHashMacOS`, `XACmdlineToolsHashMacOSArm64`, `XACmdlineToolsHashLinux/Windows` | `commandlinetools-{mac_x86_64,mac_arm64,linux,win}-$(CommandLineToolsVersion).zip` — macOS is arch-split; other hosts are one zip |
+| cmdline-tools | `cmdline-tools` | `CommandLineToolsFolder`, `CommandLineToolsVersion`; shipped `AndroidCommandLineToolsVersion` in `src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props` | `XACmdlineToolsHashMacOS`, `XACmdlineToolsHashMacOSArm64`, `XACmdlineToolsHashLinux/Windows` | `commandlinetools-{mac_x86_64,mac_arm64,linux,win}-$(CommandLineToolsVersion).zip`; also update the latest entry in `src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml` |
| cmake | `cmake;` | `AndroidCmakeVersion` | `XACmakeHashMacOS/Linux/Windows` | `cmake-$(AndroidCmakeVersion)-{darwin,linux,windows}.zip` |
| emulator | `emulator` | `EmulatorVersion`, `EmulatorPkgRevision` | `XAEmulatorHashMacOSx64`, `XAEmulatorHashMacOSArm64`, `XAEmulatorHashLinux/Windows` | `emulator-{darwin_x64,darwin_aarch64,linux_x64,windows_x64}-$(EmulatorVersion).zip`; also drives a synthesized `package.xml` via `package.xml.in` |
| API 29 system image | `sys-img/android` manifest, `path="system-images;android-29;default;{x86_64,arm64-v8a}"` | (fixed `x86_64-29_r08*`/`arm64-v8a-29_r08` filenames — check manifest for a newer `rNN` if refreshing) | `XASystemImageHashMacOSx64/MacOSArm64/Linux/Windows` | `{x86_64,arm64-v8a}-29_r08{-darwin,-linux,-windows,}.zip` under `sys-img/android/` |
@@ -30,6 +30,18 @@ an `'$(_IsArm64Apple)' != 'true'` item for the existing x86_64/generic macOS arc
property. Don't add an arm64-specific branch speculatively for families where Google still ships
one universal/x86_64-only macOS archive.
+Every extraction output stamp must include the selected archive identity and SHA-256. Multiple
+architectures can intentionally share a destination, so `source.properties` alone must never be
+used as the incremental output.
+
+## Notes on stable channels and licenses
+
+Emulator updates must come from `channel-0`; a higher development-channel revision is not stable.
+Command-line-tools updates are incomplete unless bootstrap pins, shipped product versions, and feed
+archives all move together. License acceptance must remain deterministic and offline: preserve
+valid existing fingerprints and atomically add the pinned expected SHA-1 under a cross-process
+lock. Never invoke the command-line tools' `android` bootstrapper or another mutable downloader.
+
## Notes on platform extension levels
Some platform API levels ship as a numbered "extension" (e.g. `platform-34-ext12_r01`) rather than
diff --git a/Configuration.props b/Configuration.props
index 2c1f257ad6c..c364d4659d6 100644
--- a/Configuration.props
+++ b/Configuration.props
@@ -114,10 +114,10 @@
46BDA18F0F3C28A55F88DA4BA05FEC21ABE3D9BCA0A78A8B18D444987B3DC817
3A78AD82A3F3E9E613E255618D2ACF1B3C4BDB24EF6E1F9341227CF6FC492A89
- E79BFE36609EC8FBC4EA3928B514BD9CDDB508697B9F939EEF8E028AE2EC2C6D
- E1136F23185B3D17A675D8C1E167A9B6D3AA95C3C6B834371967FC8ECC02A315
- B93886AEEAA264E4CD0CC9AD57428DF8FCCB33F17E71392428F5DD221877A97E
- 666312FA611C8751A4266EF0B3981F68A126E3C58225FB44415CED038301ECDA
+ C1A3890F95B8868198918FAD05FFCA16FA20404D93547BA545FF5A5867EE7005
+ 22530DE9363F34EA945ECB5CAD74523ABD4B615F27F3C1A9899EFB183EA9E144
+ 95771E0AE431897B2A4BD2D97FA095F29A8B0624A7B216BAF529F9306161C266
+ 5FF441F3B12ACE9B13E9CF96FB0007D233967718652A8110705E995AC47BFEB7
B5C3FDA1F4B4931C30518D342E4AD5F7464945E0CDCED3538D4FF2E12F7BF201
7FD1610E16A69AC2E202278E82077DE134CE404FE099D872019F69F3A340ACDD
@@ -128,6 +128,7 @@
68DB2690CB92E4EE5373AC9B792642C90717D8F417D83ECCEA48781171B3182A
A853F452B8BA94933EB56ED4E0F622991F9D6DE6E0AC19923AA47414D79B9129
F63736E27B0D283298116DA8BCE2C024DE28C7E4081044B30716FCB6E0FA13F1
+ 24333f8a63b6825ea9c5514f83c2829b004d1fee
<_XAAndroidNdkRelease>28c
@@ -186,8 +187,8 @@
16111833_latest
$(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)\bin
- 16079175
- 37.2.5
+ 15917651
+ 37.1.11
$(AndroidSdkFullPath)\emulator
emulator
emulator.exe
diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/EnsureAndroidSdkLicense.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/EnsureAndroidSdkLicense.cs
new file mode 100644
index 00000000000..3a5bc5ebb77
--- /dev/null
+++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/EnsureAndroidSdkLicense.cs
@@ -0,0 +1,106 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tools.BootstrapTasks
+{
+ public sealed class EnsureAndroidSdkLicense : Task
+ {
+ [Required]
+ public string LicenseDirectory { get; set; } = "";
+
+ [Required]
+ public ITaskItem [] LicenseHashes { get; set; } = [];
+
+ public int LockTimeoutSeconds { get; set; } = 60;
+
+ public bool ValidateOnly { get; set; }
+
+ public override bool Execute ()
+ {
+ Directory.CreateDirectory (LicenseDirectory);
+
+ var licenseFile = Path.Combine (LicenseDirectory, "android-sdk-license");
+ var lockFile = Path.Combine (LicenseDirectory, ".android-sdk-license.lock");
+ using (AcquireLock (lockFile)) {
+ var licenses = new List ();
+ if (File.Exists (licenseFile)) {
+ foreach (var line in File.ReadAllLines (licenseFile)) {
+ var license = line.Trim ();
+ if (license.Length == 0)
+ continue;
+ if (!IsSha1 (license)) {
+ Log.LogError ($"Android SDK license file '{licenseFile}' contains invalid fingerprint '{license}'.");
+ return false;
+ }
+ if (!licenses.Contains (license, StringComparer.OrdinalIgnoreCase))
+ licenses.Add (license);
+ }
+ }
+
+ foreach (var item in LicenseHashes) {
+ var license = item.ItemSpec.Trim ();
+ if (!IsSha1 (license)) {
+ Log.LogError ($"Android SDK license fingerprint '{license}' is not a 40-character SHA-1 hash.");
+ return false;
+ }
+ if (ValidateOnly && !licenses.Contains (license, StringComparer.OrdinalIgnoreCase)) {
+ Log.LogError ($"Android SDK license file '{licenseFile}' does not contain expected fingerprint '{license}'.");
+ return false;
+ }
+ if (!ValidateOnly && !licenses.Contains (license, StringComparer.OrdinalIgnoreCase))
+ licenses.Add (license);
+ }
+
+ if (licenses.Count == 0) {
+ Log.LogError ("At least one Android SDK license fingerprint is required.");
+ return false;
+ }
+
+ if (!ValidateOnly)
+ WriteAtomically (licenseFile, licenses);
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+
+ FileStream AcquireLock (string lockFile)
+ {
+ var timer = Stopwatch.StartNew ();
+ while (true) {
+ try {
+ return new FileStream (lockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ } catch (IOException) when (timer.Elapsed < TimeSpan.FromSeconds (LockTimeoutSeconds)) {
+ Thread.Sleep (100);
+ }
+ }
+ }
+
+ static bool IsSha1 (string value)
+ {
+ return value.Length == 40 && value.All (character =>
+ (character >= '0' && character <= '9') ||
+ (character >= 'a' && character <= 'f') ||
+ (character >= 'A' && character <= 'F'));
+ }
+
+ static void WriteAtomically (string destination, IEnumerable lines)
+ {
+ var temporaryFile = Path.Combine (Path.GetDirectoryName (destination) ?? "", $".{Path.GetFileName (destination)}.{Guid.NewGuid ():N}.tmp");
+ try {
+ File.WriteAllText (temporaryFile, string.Concat (lines.Select (line => line + "\n")), new UTF8Encoding (encoderShouldEmitUTF8Identifier: false));
+ File.Move (temporaryFile, destination, overwrite: true);
+ } finally {
+ File.Delete (temporaryFile);
+ }
+ }
+ }
+}
diff --git a/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml b/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
index 33ca75a4e99..c0eadd4b78a 100644
--- a/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
+++ b/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
@@ -153,11 +153,12 @@
https://dl.google.com/android/repository/platform-tools_r33.0.2-windows.zip
-
+
- https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
- https://dl.google.com/android/repository/commandlinetools-mac-13114758_latest.zip
- https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-linux-16111833_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-mac_x86_64-16111833_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-mac_arm64-16111833_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-win-16111833_latest.zip
diff --git a/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props b/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
index 5bd946ce720..db89781e48e 100644
--- a/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
+++ b/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
@@ -2,7 +2,7 @@
37.0.0
- 19.0
+ 23.0
36.0.0
android-36
diff --git a/src/androidsdk/androidsdk.targets b/src/androidsdk/androidsdk.targets
index d84ef3a8769..9e631c49614 100644
--- a/src/androidsdk/androidsdk.targets
+++ b/src/androidsdk/androidsdk.targets
@@ -16,11 +16,6 @@
obj\$(Configuration)\
<_AndroidRepositoryUrl>https://dl.google.com/android/repository/
- <_SdkManagerDir>$(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)\bin
- <_SdkManagerPath Condition=" '$(HostOS)' == 'Windows' ">$(_SdkManagerDir)\sdkmanager.bat
- <_SdkManagerPath Condition=" '$(HostOS)' != 'Windows' ">$(_SdkManagerDir)\sdkmanager
- <_AndroidCliPath Condition=" '$(HostOS)' == 'Windows' ">$(_SdkManagerDir)\android.exe
- <_AndroidCliPath Condition=" '$(HostOS)' != 'Windows' ">$(_SdkManagerDir)\android
<_AndroidSdkLicenseFile>$(AndroidSdkFullPath)\licenses\android-sdk-license
<_LicensesAcceptedFile>$(AndroidSdkFullPath)\licenses\.licenses-accepted
@@ -56,6 +51,7 @@
+
+ Outputs="%(_AndroidSdkPackage.Destination)\.extracted-%(_AndroidSdkPackage.Identity)-%(_AndroidSdkPackage.Hash)">
<_StripComponents>%(_AndroidSdkPackage.StripComponents)
<_StripComponents Condition=" '$(_StripComponents)' == '' ">1
@@ -384,6 +381,7 @@
/>
+
-
-
-
-
-
+
+
+
+
From 2f8b985faaa55b27a9d58a251accde886012bb54 Mon Sep 17 00:00:00 2001
From: Jonathan Peppers
Date: Fri, 21 Aug 2026 08:08:25 -0500
Subject: [PATCH 8/8] [androidsdk] Keep installer command-line tools unchanged
Limit the command-line tools 23 update to the bootstrap SDK catalog.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e41ab6f-5ce8-4ee9-aea2-2dfb541e2592
---
.../Feeds/AndroidManifestFeed_d18.0.xml | 9 ++++-----
.../Xamarin.Installer.Common.props | 2 +-
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml b/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
index c0eadd4b78a..33ca75a4e99 100644
--- a/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
+++ b/src/Xamarin.Installer.AndroidSDK/Feeds/AndroidManifestFeed_d18.0.xml
@@ -153,12 +153,11 @@
https://dl.google.com/android/repository/platform-tools_r33.0.2-windows.zip
-
+
- https://dl.google.com/android/repository/commandlinetools-linux-16111833_latest.zip
- https://dl.google.com/android/repository/commandlinetools-mac_x86_64-16111833_latest.zip
- https://dl.google.com/android/repository/commandlinetools-mac_arm64-16111833_latest.zip
- https://dl.google.com/android/repository/commandlinetools-win-16111833_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-mac-13114758_latest.zip
+ https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip
diff --git a/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props b/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
index db89781e48e..5bd946ce720 100644
--- a/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
+++ b/src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Common.props
@@ -2,7 +2,7 @@
37.0.0
- 23.0
+ 19.0
36.0.0
android-36