From 358a156fd55b7d899d133af81cb767cac4ffe804 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 02:04:07 -0500 Subject: [PATCH 1/6] Centralize Gradle Maven repositories via shared eng/gradle/repositories.gradle (#11711) All Gradle projects in this repo (`src/manifestmerger`, `src/r8`, `src/proguard-android`) previously declared their own ad-hoc mix of `mavenCentral()`, `google()`, `jcenter()`, and `kotlin.bintray.com` repositories. That's a maintenance hazard and blocks any future CFSClean network-isolation work (see https://aka.ms/1es/netiso/CFS), which requires all Maven dependencies to flow through the dnceng `dotnet-public-maven` Azure Artifacts feed. ## Approach A single shared file `eng/gradle/repositories.gradle` is now the only place repository URLs are declared. Each `settings.gradle` applies it twice via `apply from: ..., to: ` to populate both `pluginManagement.repositories` and `dependencyResolutionManagement.repositories`. The `build.gradle` files no longer declare any `repositories {}` block. The shared file switches on `System.getenv('RunningOnCI')`: - **`RunningOnCI=true`** (set by `build-tools/automation/yaml-templates/variables.yaml` in our AzDO pipeline): the dnceng `dotnet-public-maven` feed, plus the anonymous AzureArtifacts feed for the credprovider Gradle plugin. - **unset** (local builds, Dependabot, GitHub Actions): `google()` + `mavenCentral()` + `gradlePluginPortal()` so contributors and Dependabot need no credentials. Adapted from dotnet/maui ([72cc860](https://github.com/dotnet/maui/commit/72cc860ec998477cf07f160ea7f49e695d99f3c3)). ## Dependabot workflow This preserves Dependabot for the Gradle ecosystem (`/src/r8/`, `/src/manifestmerger/` in `.github/dependabot.yml`): 1. Dependabot opens a PR against public repos -> sees the latest upstream version. 2. CI runs with `RunningOnCI=true`. If the new version isn't cached in the dnceng feed yet, CI fails 401. 3. A maintainer runs `$env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/ build` locally; the artifacts-credprovider plugin device-flow-logs-in once, the feed proxies + caches the package, and anonymous reads work from then on. 4. Re-run CI -> green. No PR edit required. CI itself reads the feed **anonymously** -- no PAT secret or pipeline auth setup is required. ## Notes for reviewers - All three projects (`manifestmerger`, `r8`, `proguard-android`) were verified to build through both code paths (`RunningOnCI` set and unset) on Windows. - `proguard-android/build.gradle` keeps the modern `plugins { id 'com.android.application' version '8.7.0' }` DSL; AGP is resolvable from both `gradlePluginPortal()` locally and `dotnet-public-maven` in CI. - `.github/instructions/gradle.instructions.md` (scoped via frontmatter to `**/*.gradle`) documents the pattern so Copilot picks it up when editing Gradle files. - The credprovider plugin is declared unconditionally in each `settings.gradle` because Gradle disallows wrapping `plugins {}` in `if (...)`. It is a no-op on the local path (no AzDO repos for it to authenticate). --- .github/instructions/gradle.instructions.md | 49 +++++++++++++++ eng/gradle/dependency-repositories.gradle | 23 +++++++ eng/gradle/plugin-repositories.gradle | 68 +++++++++++++++++++++ src/manifestmerger/build.gradle | 7 --- src/manifestmerger/settings.gradle | 13 ++++ src/proguard-android/build.gradle | 5 -- src/proguard-android/settings.gradle | 16 +++-- src/r8/build.gradle | 5 -- src/r8/settings.gradle | 14 +++++ 9 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 .github/instructions/gradle.instructions.md create mode 100644 eng/gradle/dependency-repositories.gradle create mode 100644 eng/gradle/plugin-repositories.gradle create mode 100644 src/r8/settings.gradle diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md new file mode 100644 index 00000000000..7233969afe9 --- /dev/null +++ b/.github/instructions/gradle.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**/*.gradle" +--- + +# Gradle conventions + +All `src/*` Gradle projects share two repo config files: **`eng/gradle/plugin-repositories.gradle`** (for `pluginManagement.repositories`) and **`eng/gradle/dependency-repositories.gradle`** (for `dependencyResolutionManagement.repositories`). Never hard-code Maven URLs (`mavenCentral()`, `google()`, `pkgs.dev.azure.com/...`, etc.) in `build.gradle`/`settings.gradle`. + +## settings.gradle template + +```groovy +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} +plugins { + id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +} +dependencyResolutionManagement { + apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} +rootProject.name = '' +``` + +`build.gradle` files must not declare their own `repositories { ... }`. + +## CI vs local + +Both files switch on `System.getenv('RunningOnCI')` (or `RUNNINGONCI` — AzDO uppercases env vars on Linux/macOS agents): + +- **`RunningOnCI=true`** (Azure DevOps, set in `build-tools/automation/yaml-templates/variables.yaml`) → dnceng `dotnet-public-maven` feed (CFSClean isolation, https://aka.ms/1es/netiso/CFS). Anonymous read of cached packages. +- **unset** (local, Dependabot, GitHub Actions) → `google()` + `mavenCentral()` + `gradlePluginPortal()` for plugins, `google()` + `mavenCentral()` for deps. No credentials needed. + +Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI=true ...` (bash). + +## When CI fails 401 on a Dependabot bump + +The new package isn't cached in the feed yet. One-time setup, then ingest: + +1. `iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"` (or the `.sh` equivalent) +2. `$env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/ build` — sign in via the device-flow prompt; the feed proxies + caches the package. +3. Re-run CI on the Dependabot PR. No PR edit needed. + +The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`). + +## Don'ts + +- Don't hard-code Maven repo URLs in `build.gradle` / `settings.gradle`; use the shared file. +- Don't wrap `plugins {}` in `if (...)` — Gradle rejects it. +- Don't use modern `plugins { id 'com.android.application' version '...' }` DSL without confirming the plugin is in `dotnet-public-maven`; prefer `buildscript { ... } / apply plugin: '...'` when in doubt. \ No newline at end of file diff --git a/eng/gradle/dependency-repositories.gradle b/eng/gradle/dependency-repositories.gradle new file mode 100644 index 00000000000..ac491c649c2 --- /dev/null +++ b/eng/gradle/dependency-repositories.gradle @@ -0,0 +1,23 @@ +// Shared Maven repository list for project DEPENDENCY resolution +// (dependencyResolutionManagement.repositories) across every settings.gradle +// in this repo. See plugin-repositories.gradle for plugin resolution. +// +// Switches on RunningOnCI for the same CFSClean reasons described there. +// AzureArtifacts is intentionally NOT included here — it only hosts the +// credprovider plugin, so listing it in this scope would add a 404 round-trip +// to every dependency lookup. + +repositories { + // AzDO uppercases pipeline variables when exporting them as env vars on + // Linux/macOS agents, so check both spellings. + def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI') + if (runningOnCI == 'true') { + maven { + url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' + name = 'dotnet-public-maven' + } + } else { + google() + mavenCentral() + } +} diff --git a/eng/gradle/plugin-repositories.gradle b/eng/gradle/plugin-repositories.gradle new file mode 100644 index 00000000000..bca6736b23f --- /dev/null +++ b/eng/gradle/plugin-repositories.gradle @@ -0,0 +1,68 @@ +// Shared Maven repository list for PLUGIN resolution (pluginManagement.repositories) +// across every settings.gradle in this repo. See plugin-repositories.gradle's +// sibling, dependency-repositories.gradle, for project dependency resolution. +// +// In our Azure DevOps CI pipeline (RunningOnCI=true), plugins resolve through +// the dnceng Azure Artifacts feed (dotnet-public-maven) for CFSClean network +// isolation compliance (https://aka.ms/1es/netiso/CFS). Locally and from +// GitHub Actions (e.g. Dependabot), the standard Gradle Plugin Portal is used. +// +// AzureArtifacts (anonymous public feed) is always included because every +// settings.gradle loads the artifacts-credprovider plugin from there. +// +// The dnceng feed proxies public sources. Once any package has been pulled +// through the feed (an authenticated request), it is cached and anonymous +// reads work forever after. CI therefore does NOT need credentials — it just +// reads anonymously from packages already cached in the feed. +// +// =================== TESTING / INGESTING LOCALLY =================== +// +// To exercise the CI code path locally (or to ingest a new package that +// Dependabot brought in but isn't yet cached in the feed): +// +// 1. Install the Azure Artifacts credential provider (one-time): +// +// PowerShell: iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }" +// bash: wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash +// +// 2. Flip the switch and run the gradle build that needs the package: +// +// PowerShell: $env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/r8 build +// bash: RunningOnCI=true ./build-tools/gradle/gradlew --project-dir src/r8 build +// +// On first authenticated request, you'll get a device-flow login prompt +// pointing at https://aka.ms/devicelogin — sign in with your Microsoft +// account. The credprovider caches the token; the feed caches the +// package; future CI runs read it anonymously and pass. +// +// =================== WORKFLOW FOR DEPENDABOT PRs =================== +// +// 1. Dependabot opens a PR bumping a Gradle dep (uses public repos, so it +// always sees the latest upstream version). +// 2. CI runs with RunningOnCI=true, hits the feed, and fails with 401 if +// the new package version isn't ingested yet. +// 3. A maintainer follows the steps above to ingest the package, then +// re-runs CI. No PR edit is required. + +repositories { + // Anonymous public Azure Artifacts feed that hosts the + // artifacts-credprovider Gradle plugin (loaded by every settings.gradle). + maven { + url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' + name = 'AzureArtifacts' + } + + // AzDO uppercases pipeline variables when exporting them as env vars on + // Linux/macOS agents, so check both spellings. + def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI') + if (runningOnCI == 'true') { + maven { + url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' + name = 'dotnet-public-maven' + } + } else { + google() + mavenCentral() + gradlePluginPortal() + } +} diff --git a/src/manifestmerger/build.gradle b/src/manifestmerger/build.gradle index 1c22cc978fd..278dd70cdd0 100644 --- a/src/manifestmerger/build.gradle +++ b/src/manifestmerger/build.gradle @@ -10,13 +10,6 @@ java { targetCompatibility = ext.javaTargetVer } -repositories { - maven { url 'https://maven.google.com' } - mavenCentral() - maven { url 'https://kotlin.bintray.com/kotlinx' } - jcenter() -} - dependencies { // https://mvnrepository.com/artifact/com.android.tools.build/manifest-merger implementation 'com.android.tools.build:manifest-merger:31.12.2' diff --git a/src/manifestmerger/settings.gradle b/src/manifestmerger/settings.gradle index 6a4458f7677..483063983cd 100644 --- a/src/manifestmerger/settings.gradle +++ b/src/manifestmerger/settings.gradle @@ -1 +1,14 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} + +plugins { + id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +} + +dependencyResolutionManagement { + apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + rootProject.name = 'manifestmerger' \ No newline at end of file diff --git a/src/proguard-android/build.gradle b/src/proguard-android/build.gradle index b168ffa79e6..9be3b1b1e74 100644 --- a/src/proguard-android/build.gradle +++ b/src/proguard-android/build.gradle @@ -2,11 +2,6 @@ plugins { id 'com.android.application' version '8.7.0' } -repositories { - google() - mavenCentral() -} - android { namespace 'com.microsoft.proguard.android' // Setting the minimum we support at the moment, might not matter diff --git a/src/proguard-android/settings.gradle b/src/proguard-android/settings.gradle index 958c11d72b5..f1d5c0e6326 100644 --- a/src/proguard-android/settings.gradle +++ b/src/proguard-android/settings.gradle @@ -1,8 +1,14 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } + +plugins { + id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +} + +dependencyResolutionManagement { + apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + rootProject.name = 'proguard-android' \ No newline at end of file diff --git a/src/r8/build.gradle b/src/r8/build.gradle index cd77405206d..c1f1a24d932 100644 --- a/src/r8/build.gradle +++ b/src/r8/build.gradle @@ -9,11 +9,6 @@ java { targetCompatibility = ext.javaTargetVer } -repositories { - google() - mavenCentral() -} - dependencies { implementation 'com.android.tools:r8:8.11.18' } diff --git a/src/r8/settings.gradle b/src/r8/settings.gradle new file mode 100644 index 00000000000..0d56e34025f --- /dev/null +++ b/src/r8/settings.gradle @@ -0,0 +1,14 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} + +plugins { + id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +} + +dependencyResolutionManagement { + apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + +rootProject.name = 'r8' \ No newline at end of file From c5b89a12b7ee93b72a2e8a56dd04991e623d0b7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:46:51 +0200 Subject: [PATCH 2/6] Bump com.android.application from 8.7.0 to 9.2.1 in /src/proguard-android (#11717) Bumps com.android.application from 8.7.0 to 9.2.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.android.application&package-manager=gradle&previous-version=8.7.0&new-version=9.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- .github/dependabot.yml | 10 +- .github/instructions/gradle.instructions.md | 19 ++- .../gradle/wrapper/gradle-wrapper.properties | 2 +- eng/gradle/mirror-dependencies.ps1 | 141 ++++++++++++++++++ eng/gradle/plugin-repositories.gradle | 33 +--- src/proguard-android/build.gradle | 2 +- ...ndroid.LibraryProjectZip-LibBinding.csproj | 2 +- .../java/JavaLib/build.gradle | 22 +-- .../java/JavaLib/library/build.gradle | 31 +++- .../library/src/main/AndroidManifest.xml | 2 +- .../java/JavaLib/settings.gradle | 14 ++ 11 files changed, 209 insertions(+), 69 deletions(-) create mode 100644 eng/gradle/mirror-dependencies.ps1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d278e747639..11fa44700b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,11 +2,11 @@ version: 2 updates: - package-ecosystem: "gradle" - directory: "/src/r8/" - schedule: - interval: "weekly" - - package-ecosystem: "gradle" - directory: "/src/manifestmerger/" + directories: + - "/src/r8" + - "/src/manifestmerger" + - "/src/proguard-android" + - "/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib" schedule: interval: "weekly" - package-ecosystem: "gitsubmodule" diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 7233969afe9..28629fe2e73 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -34,13 +34,22 @@ Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI ## When CI fails 401 on a Dependabot bump -The new package isn't cached in the feed yet. One-time setup, then ingest: +The new package isn't cached in the dnceng `dotnet-public-maven` feed yet. CI agents only do anonymous reads, so someone has to authenticate once locally to make the feed pull the package (and its transitive deps) from upstream. -1. `iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"` (or the `.sh` equivalent) -2. `$env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/ build` — sign in via the device-flow prompt; the feed proxies + caches the package. -3. Re-run CI on the Dependabot PR. No PR edit needed. +Use the helper script — it runs the build, parses any 401 URLs out of the log, re-fetches each one with an Azure DevOps bearer token (so the feed mirrors it), and loops until the build succeeds: -The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`). +```powershell +az login # one-time, corp account with MFA satisfied + +pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -ProjectDir ` + -Task ` + -AndroidHome # required for any com.android.* project +``` + +The mirror must run in the project that actually needs the new package — a sibling project's build won't trigger a mirror for someone else's deps. Typical convergence is 2-5 iterations as the resolver walks the dep graph breadth-first. + +After it succeeds, just re-run the failed CI job. No PR edits needed — the packages are now anonymous-readable forever. ## Don'ts diff --git a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties index d6e308a6378..221c4f98228 100644 --- a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/eng/gradle/mirror-dependencies.ps1 b/eng/gradle/mirror-dependencies.ps1 new file mode 100644 index 00000000000..20a225c9d21 --- /dev/null +++ b/eng/gradle/mirror-dependencies.ps1 @@ -0,0 +1,141 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Mirrors a gradle project's dependencies into the dnceng dotnet-public-maven + Azure Artifacts feed so CI can resolve them anonymously. + +.DESCRIPTION + When Dependabot bumps a gradle dependency (or its transitive graph changes), + CI fails with 401 errors because the new package(s) haven't been pulled + from upstream into the dnceng feed yet. CI agents only do anonymous reads, + so a developer has to authenticate locally once to seed the feed. + + This script does that by running the requested gradle build in a loop: + 1. Run gradle with RunningOnCI=true so it points at the dnceng feed. + 2. Parse any 'Could not GET' URLs out of the build log. + 3. Re-fetch each failing URL with an Azure DevOps OAuth bearer token + (obtained via `az account get-access-token`). The feed's upstream + connector then pulls the package and caches it for anonymous reads. + 4. Repeat until the build succeeds or no more 401s appear. + + After the loop converges, no PR edits are needed — just re-run the failing + CI job, since the packages are now anonymous-readable. + +.PARAMETER ProjectDir + Path to the gradle project (the one containing the failing dependency). + Mirroring must run in the project that actually requires the package; + a sibling project's build won't trigger a mirror for someone else's deps. + +.PARAMETER Task + Gradle task(s) to run. Should be one that resolves the new dependency + graph (e.g. 'assembleDebug', 'build', 'extractProguardFiles'). + +.PARAMETER AndroidHome + Optional path to the Android SDK. Required when the gradle build needs it + (any project using the com.android.* plugins). Defaults to the value of + `$env:ANDROID_HOME` if set. + +.PARAMETER MaxIterations + Cap on build/mirror cycles. Default 15. Typical convergence is 2-5 + iterations as the resolver walks the dep graph breadth-first. + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -ProjectDir tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib ` + -Task assembleDebug ` + -AndroidHome D:\android-toolchain\sdk + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 -ProjectDir src/proguard-android -Task extractProguardFiles +#> +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string] $ProjectDir, + + [Parameter(Mandatory=$true)] + [string] $Task, + + [string] $AndroidHome = $env:ANDROID_HOME, + + [int] $MaxIterations = 15 +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../..') | Select-Object -ExpandProperty Path +$projectDirAbs = Resolve-Path (Join-Path $repoRoot $ProjectDir) -ErrorAction Stop | Select-Object -ExpandProperty Path +$gradlew = if ($IsWindows -or $env:OS -eq 'Windows_NT') { + Join-Path $repoRoot 'build-tools/gradle/gradlew.bat' +} else { + Join-Path $repoRoot 'build-tools/gradle/gradlew' +} +if (-not (Test-Path $gradlew)) { throw "gradlew not found at $gradlew" } + +# Azure DevOps resource id — same for every AzDO tenant. +$azDevOpsResource = '499b84ac-1321-427f-aa17-267ca6975798' + +function Get-AzDevOpsToken { + $token = az account get-access-token --resource $azDevOpsResource --query accessToken -o tsv 2>$null + if ([string]::IsNullOrEmpty($token)) { + throw "Could not get an Azure DevOps access token. Run 'az login' first." + } + return $token +} + +function Invoke-Mirror($logPath) { + $urls = Select-String -Path $logPath -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | + ForEach-Object { $_.Matches } | + ForEach-Object { $_.Value -replace "^Could not GET '", "" -replace "'$", "" } | + Sort-Object -Unique + if ($urls.Count -eq 0) { return 0 } + $token = Get-AzDevOpsToken + $headers = @{ Authorization = "Bearer $token" } + $ok = 0; $fail = 0 + foreach ($u in $urls) { + try { + $r = Invoke-WebRequest -Uri $u -Headers $headers -SkipHttpErrorCheck -ErrorAction Stop + if ($r.StatusCode -eq 200) { $ok++ } else { $fail++; Write-Host " $($r.StatusCode) $u" -ForegroundColor Yellow } + } catch { + $fail++ + Write-Host " ERR $u : $_" -ForegroundColor Yellow + } + } + Write-Host " -> mirrored OK=$ok, not-found=$fail (of $($urls.Count))" -ForegroundColor Cyan + return $urls.Count +} + +Write-Host "Repo root: $repoRoot" +Write-Host "Project: $projectDirAbs" +Write-Host "Task: $Task" +if ($AndroidHome) { Write-Host "ANDROID_HOME: $AndroidHome" } + +# Verify az is available and authenticated up front so we fail fast. +Get-AzDevOpsToken | Out-Null + +if ($AndroidHome) { $env:ANDROID_HOME = $AndroidHome } +$env:RunningOnCI = 'true' + +Push-Location $projectDirAbs +try { + for ($i = 1; $i -le $MaxIterations; $i++) { + Write-Host "`n=== iteration $i ===" -ForegroundColor Green + $log = Join-Path ([IO.Path]::GetTempPath()) "gradle-mirror-iter-$i.log" + & $gradlew $Task --no-daemon --refresh-dependencies *>&1 | Tee-Object -FilePath $log | Out-Null + if (Select-String -Path $log -Pattern 'BUILD SUCCESSFUL' -SimpleMatch -Quiet) { + Write-Host "`nBUILD SUCCESSFUL after $i iteration(s). The feed now has the packages CI needs." -ForegroundColor Green + return + } + $count = Invoke-Mirror $log + if ($count -eq 0) { + Write-Host "`nGradle failed but no 401s to mirror — see $log" -ForegroundColor Red + Get-Content $log -Tail 30 + exit 1 + } + } + Write-Host "`nExhausted $MaxIterations iterations without success. Last log:" -ForegroundColor Red + Get-Content $log -Tail 30 + exit 1 +} +finally { + Pop-Location +} diff --git a/eng/gradle/plugin-repositories.gradle b/eng/gradle/plugin-repositories.gradle index bca6736b23f..5763cacff65 100644 --- a/eng/gradle/plugin-repositories.gradle +++ b/eng/gradle/plugin-repositories.gradle @@ -15,34 +15,11 @@ // reads work forever after. CI therefore does NOT need credentials — it just // reads anonymously from packages already cached in the feed. // -// =================== TESTING / INGESTING LOCALLY =================== -// -// To exercise the CI code path locally (or to ingest a new package that -// Dependabot brought in but isn't yet cached in the feed): -// -// 1. Install the Azure Artifacts credential provider (one-time): -// -// PowerShell: iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }" -// bash: wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash -// -// 2. Flip the switch and run the gradle build that needs the package: -// -// PowerShell: $env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/r8 build -// bash: RunningOnCI=true ./build-tools/gradle/gradlew --project-dir src/r8 build -// -// On first authenticated request, you'll get a device-flow login prompt -// pointing at https://aka.ms/devicelogin — sign in with your Microsoft -// account. The credprovider caches the token; the feed caches the -// package; future CI runs read it anonymously and pass. -// -// =================== WORKFLOW FOR DEPENDABOT PRs =================== -// -// 1. Dependabot opens a PR bumping a Gradle dep (uses public repos, so it -// always sees the latest upstream version). -// 2. CI runs with RunningOnCI=true, hits the feed, and fails with 401 if -// the new package version isn't ingested yet. -// 3. A maintainer follows the steps above to ingest the package, then -// re-runs CI. No PR edit is required. +// When a Dependabot PR's CI fails with 401 because a new package isn't yet +// cached in the feed, run the helper script described in +// .github/instructions/gradle.instructions.md (TL;DR: `az login` once, then +// `pwsh ./eng/gradle/mirror-dependencies.ps1 -ProjectDir -Task `). +// After it succeeds, just re-run the failed CI job — no PR edit is needed. repositories { // Anonymous public Azure Artifacts feed that hosts the diff --git a/src/proguard-android/build.gradle b/src/proguard-android/build.gradle index 9be3b1b1e74..e70b9fa5fe7 100644 --- a/src/proguard-android/build.gradle +++ b/src/proguard-android/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.android.application' version '8.7.0' + id 'com.android.application' version '9.2.1' } android { diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/Xamarin.Android.LibraryProjectZip-LibBinding.csproj b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/Xamarin.Android.LibraryProjectZip-LibBinding.csproj index b0ef446b485..0290e186e4b 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/Xamarin.Android.LibraryProjectZip-LibBinding.csproj +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/Xamarin.Android.LibraryProjectZip-LibBinding.csproj @@ -22,7 +22,7 @@ - + Jars\classes.jar diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle index 8a2011238e7..3e75fba8418 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle @@ -1,26 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - mavenCentral() - } +plugins { + id 'com.android.library' version '9.2.1' apply false } task clean(type: Delete) { - delete rootProject.buildDir + delete rootProject.layout.buildDirectory } diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle index b5cb8374ce7..9527ba58cdf 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle @@ -1,21 +1,21 @@ -apply plugin: 'com.android.library' +plugins { + id 'com.android.library' +} android { - compileSdkVersion 25 + namespace 'com.example.javalib' + compileSdk 35 defaultConfig { - minSdkVersion 19 - targetSdkVersion 25 + minSdk 21 + targetSdk 35 versionCode 1 versionName "1.0" - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } @@ -23,3 +23,18 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } + +// Extract classes.jar from the AAR to a stable path that the binding +// project (../../../Xamarin.Android.LibraryProjectZip-LibBinding.csproj) +// can reference without depending on AGP intermediates/ layout, which +// Google reorganizes between major AGP versions. +tasks.register('extractClassesJar', Copy) { + dependsOn 'assembleDebug' + def aar = layout.buildDirectory.file('outputs/aar/library-debug.aar') + from({ zipTree(aar) }) { + include 'classes.jar' + } + into layout.buildDirectory.dir('libs') +} + +tasks.matching { it.name == 'assembleDebug' }.configureEach { finalizedBy 'extractClassesJar' } diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml index a2fb60b67f5..0a0938ae37e 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ - + diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle index d8f14a134bf..571862a24e5 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle @@ -1 +1,15 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../../../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} + +plugins { + id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +} + +dependencyResolutionManagement { + apply from: "${rootDir}/../../../../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + +rootProject.name = 'JavaLib' include ':library' From 6dccd76dceef534e3fdb8c514428a9e5455f7f58 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 13 Jul 2026 16:54:16 -0500 Subject: [PATCH 3/6] [build] Restrict Maven authentication to mirror seeding (#12055) PR #11711 centralized Gradle repository configuration, but applying the Azure Artifacts credential provider during ordinary builds caused authentication dialogs for local developers. CI does not need that provider because it reads packages already cached in `dotnet-public-maven` anonymously. This keeps repository selection and authentication as separate controls: - `RUNNINGONCI=true` selects `dotnet-public-maven` for Azure DevOps builds. - `ANDROID_MIRROR_MAVEN_DEPENDENCIES=true` loads the credential provider only while `mirror-dependencies.ps1` seeds uncached packages. - Local builds use Google, Maven Central, and the Gradle Plugin Portal without loading Azure authentication code. The credential provider is loaded through an applied script so it can be gated dynamically. Gradle cannot resolve a plugin by ID from that script's isolated `buildscript` classpath, so this context requires its implementation class. - [x] Useful description of *why the change is necessary*. - [x] Links to issues fixed: Follow-up to #11711 - [x] Unit tests: Configured all four Gradle projects in local, anonymous CI, and authenticated mirror-helper modes. --- .github/instructions/gradle.instructions.md | 16 ++++++++++------ eng/gradle/credential-provider.gradle | 17 +++++++++++++++++ eng/gradle/dependency-repositories.gradle | 10 ++-------- eng/gradle/mirror-dependencies.ps1 | 5 +++-- eng/gradle/plugin-repositories.gradle | 17 ++--------------- src/manifestmerger/settings.gradle | 4 ++-- src/proguard-android/settings.gradle | 4 ++-- src/r8/settings.gradle | 4 ++-- .../java/JavaLib/settings.gradle | 4 ++-- 9 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 eng/gradle/credential-provider.gradle diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 28629fe2e73..b702b46d390 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -12,8 +12,8 @@ All `src/*` Gradle projects share two repo config files: **`eng/gradle/plugin-re pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -plugins { - id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { + apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" } dependencyResolutionManagement { apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement @@ -25,12 +25,17 @@ rootProject.name = '' ## CI vs local -Both files switch on `System.getenv('RunningOnCI')` (or `RUNNINGONCI` — AzDO uppercases env vars on Linux/macOS agents): +Both files switch on `System.getenv('RUNNINGONCI')`. Azure DevOps exports the +`RunningOnCI` pipeline variable under this normalized environment-variable name. -- **`RunningOnCI=true`** (Azure DevOps, set in `build-tools/automation/yaml-templates/variables.yaml`) → dnceng `dotnet-public-maven` feed (CFSClean isolation, https://aka.ms/1es/netiso/CFS). Anonymous read of cached packages. +- **`RUNNINGONCI=true`** (Azure DevOps, sourced from `RunningOnCI` in `build-tools/automation/yaml-templates/variables.yaml`) → dnceng `dotnet-public-maven` feed (CFSClean isolation, https://aka.ms/1es/netiso/CFS). Anonymous read of cached packages. - **unset** (local, Dependabot, GitHub Actions) → `google()` + `mavenCentral()` + `gradlePluginPortal()` for plugins, `google()` + `mavenCentral()` for deps. No credentials needed. -Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI=true ...` (bash). +CI reads cached packages from the mirror anonymously. The Azure Artifacts +credential provider is loaded only when `ANDROID_MIRROR_MAVEN_DEPENDENCIES=true`; +`mirror-dependencies.ps1` sets this while seeding uncached packages. + +Test the CI path locally: `$env:RUNNINGONCI='true'` (PowerShell) or `RUNNINGONCI=true ...` (bash). ## When CI fails 401 on a Dependabot bump @@ -54,5 +59,4 @@ After it succeeds, just re-run the failed CI job. No PR edits needed — the pac ## Don'ts - Don't hard-code Maven repo URLs in `build.gradle` / `settings.gradle`; use the shared file. -- Don't wrap `plugins {}` in `if (...)` — Gradle rejects it. - Don't use modern `plugins { id 'com.android.application' version '...' }` DSL without confirming the plugin is in `dotnet-public-maven`; prefer `buildscript { ... } / apply plugin: '...'` when in doubt. \ No newline at end of file diff --git a/eng/gradle/credential-provider.gradle b/eng/gradle/credential-provider.gradle new file mode 100644 index 00000000000..32f6fa95e8b --- /dev/null +++ b/eng/gradle/credential-provider.gradle @@ -0,0 +1,17 @@ +// The dependency-mirroring helper uses this plugin to authenticate with the +// Maven mirror. Regular local and CI builds do not apply this script. +buildscript { + repositories { + maven { + url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' + name = 'AzureArtifacts' + } + } + dependencies { + classpath 'com.microsoft.azure:artifacts-gradle-credprovider:1.1.1' + } +} + +// Plugins loaded through an applied script's buildscript classpath cannot be +// resolved by ID; Gradle requires the implementation class in this context. +apply plugin: com.microsoft.azure.artifacts.credprovider.gradle.GradleCredentialProviderPlugin diff --git a/eng/gradle/dependency-repositories.gradle b/eng/gradle/dependency-repositories.gradle index ac491c649c2..a9daafecc52 100644 --- a/eng/gradle/dependency-repositories.gradle +++ b/eng/gradle/dependency-repositories.gradle @@ -2,16 +2,10 @@ // (dependencyResolutionManagement.repositories) across every settings.gradle // in this repo. See plugin-repositories.gradle for plugin resolution. // -// Switches on RunningOnCI for the same CFSClean reasons described there. -// AzureArtifacts is intentionally NOT included here — it only hosts the -// credprovider plugin, so listing it in this scope would add a 404 round-trip -// to every dependency lookup. +// Switches on RUNNINGONCI for the same CFSClean reasons described there. repositories { - // AzDO uppercases pipeline variables when exporting them as env vars on - // Linux/macOS agents, so check both spellings. - def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI') - if (runningOnCI == 'true') { + if (System.getenv('RUNNINGONCI') == 'true') { maven { url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' name = 'dotnet-public-maven' diff --git a/eng/gradle/mirror-dependencies.ps1 b/eng/gradle/mirror-dependencies.ps1 index 20a225c9d21..dedf0abbdfc 100644 --- a/eng/gradle/mirror-dependencies.ps1 +++ b/eng/gradle/mirror-dependencies.ps1 @@ -11,7 +11,7 @@ so a developer has to authenticate locally once to seed the feed. This script does that by running the requested gradle build in a loop: - 1. Run gradle with RunningOnCI=true so it points at the dnceng feed. + 1. Run gradle with RUNNINGONCI=true so it points at the dnceng feed. 2. Parse any 'Could not GET' URLs out of the build log. 3. Re-fetch each failing URL with an Azure DevOps OAuth bearer token (obtained via `az account get-access-token`). The feed's upstream @@ -113,7 +113,8 @@ if ($AndroidHome) { Write-Host "ANDROID_HOME: $AndroidHome" } Get-AzDevOpsToken | Out-Null if ($AndroidHome) { $env:ANDROID_HOME = $AndroidHome } -$env:RunningOnCI = 'true' +$env:RUNNINGONCI = 'true' +$env:ANDROID_MIRROR_MAVEN_DEPENDENCIES = 'true' Push-Location $projectDirAbs try { diff --git a/eng/gradle/plugin-repositories.gradle b/eng/gradle/plugin-repositories.gradle index 5763cacff65..898391470d1 100644 --- a/eng/gradle/plugin-repositories.gradle +++ b/eng/gradle/plugin-repositories.gradle @@ -2,14 +2,11 @@ // across every settings.gradle in this repo. See plugin-repositories.gradle's // sibling, dependency-repositories.gradle, for project dependency resolution. // -// In our Azure DevOps CI pipeline (RunningOnCI=true), plugins resolve through +// In our Azure DevOps CI pipeline (RUNNINGONCI=true), plugins resolve through // the dnceng Azure Artifacts feed (dotnet-public-maven) for CFSClean network // isolation compliance (https://aka.ms/1es/netiso/CFS). Locally and from // GitHub Actions (e.g. Dependabot), the standard Gradle Plugin Portal is used. // -// AzureArtifacts (anonymous public feed) is always included because every -// settings.gradle loads the artifacts-credprovider plugin from there. -// // The dnceng feed proxies public sources. Once any package has been pulled // through the feed (an authenticated request), it is cached and anonymous // reads work forever after. CI therefore does NOT need credentials — it just @@ -22,17 +19,7 @@ // After it succeeds, just re-run the failed CI job — no PR edit is needed. repositories { - // Anonymous public Azure Artifacts feed that hosts the - // artifacts-credprovider Gradle plugin (loaded by every settings.gradle). - maven { - url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' - name = 'AzureArtifacts' - } - - // AzDO uppercases pipeline variables when exporting them as env vars on - // Linux/macOS agents, so check both spellings. - def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI') - if (runningOnCI == 'true') { + if (System.getenv('RUNNINGONCI') == 'true') { maven { url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' name = 'dotnet-public-maven' diff --git a/src/manifestmerger/settings.gradle b/src/manifestmerger/settings.gradle index 483063983cd..47d043fb305 100644 --- a/src/manifestmerger/settings.gradle +++ b/src/manifestmerger/settings.gradle @@ -3,8 +3,8 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -plugins { - id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { + apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" } dependencyResolutionManagement { diff --git a/src/proguard-android/settings.gradle b/src/proguard-android/settings.gradle index f1d5c0e6326..8a8d63d070d 100644 --- a/src/proguard-android/settings.gradle +++ b/src/proguard-android/settings.gradle @@ -3,8 +3,8 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -plugins { - id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { + apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" } dependencyResolutionManagement { diff --git a/src/r8/settings.gradle b/src/r8/settings.gradle index 0d56e34025f..39e8674b5d8 100644 --- a/src/r8/settings.gradle +++ b/src/r8/settings.gradle @@ -3,8 +3,8 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -plugins { - id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { + apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" } dependencyResolutionManagement { diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle index 571862a24e5..0f5b891e40c 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle @@ -3,8 +3,8 @@ pluginManagement { apply from: "${rootDir}/../../../../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -plugins { - id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1' +if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { + apply from: "${rootDir}/../../../../../eng/gradle/credential-provider.gradle" } dependencyResolutionManagement { From cbda5f4a28e4d5e362abfcb0f19c689db5a3f0ea Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 07:54:01 -0500 Subject: [PATCH 4/6] [tests] Route Maven and Gradle resolution through the dnceng mirror (#12199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CFSClean reports test jobs that resolve Maven dependencies and Gradle plugins directly from public services. CI agents are network-isolated and can only reach the anonymous `dotnet-public-maven` Azure Artifacts feed, which mirrors both Maven Central and Google Maven. This routes every test Maven and Gradle resolution through that feed. Crucially, it does so **unconditionally** rather than only when `RUNNINGONCI` is set — a CI-only code path means a local run exercises different URLs than CI, so a test can pass locally and then fail in the pipeline. Making local and CI identical is the same principle applied to the mirror-seeding tool below. - `Xamarin.ProjectTools` exposes a single `TestEnvironment.DotNetPublicMaven` constant. Test download URLs and `` metadata are built from it directly, replacing the old `GetTestDownloadUrl`/`GetMavenRepository` helpers and their table of public-repository prefixes. A missing prefix in that table used to fail silently on CI only. - `AndroidGradleProject` writes a `settings.gradle.kts` that applies the shared `eng/gradle` repository scripts, and copies the repository Gradle wrapper instead of downloading an alternate distribution. - Java.Interop `java-source-utils` no longer declares `mavenCentral()` independently, and parallel Kotlin Gradle builds are isolated so they don't race. `eng/gradle/mirror-dependencies.ps1` now runs Gradle exactly the way CI does — anonymously, with the configuration cache enabled — instead of loading an Azure Artifacts credential-provider plugin behind an env var. That plugin forced the seeding run to differ from the real run, so lazily-resolved Kotlin and lint classpaths resolved differently and the feed was seeded with the wrong packages. Authentication moved out of Gradle entirely: 401 URLs are re-fetched over plain HTTP with an Azure DevOps OAuth token, which makes the feed's upstream connector cache them for anonymous reads. `eng/gradle/credential-provider.gradle` is removed. A `-MavenArtifact` mode seeds coordinates directly for tests that don't use Gradle. - `TestEnvironment.IsWindows`/`IsMacOS`/`IsLinux` are annotated `[SupportedOSPlatformGuard]` so CA1416 can see through them. The properties were always correct at runtime; the analyzer just couldn't recognize an arbitrary `bool` as a platform guard. Annotating the shared helper fixes every current and future caller instead of rewriting call sites. - `Xamarin.Google.Android.InstallReferrer` is bumped to 2.2.0.8 to match Facebook SDK 18.3.0, which requires `com.android.installreferrer:2.2`. - Built `Xamarin.ProjectTools`, `Xamarin.Android.Build.Tests`, and `MSBuildDeviceIntegration` — 0 errors. - All 17 `MavenDownloadTests` pass locally, including the three that download through the mirror. Because the mirror is now used locally too, this exercises the same URLs CI does. - Verified every artifact referenced by a test resolves on the feed (`HEAD` → 200), so nothing 401s on CI. - Routing all downloads through a repository URL would leave the `Repository="Central"`/`"Google"` shorthands unexercised, so that mapping is extracted to `MavenDownload.GetKnownRepository` and covered by new tests that need no network and therefore run under CI isolation. - Passed 106 Java.Interop Maven tests; `java-source-utils` resolves through the feed. - `GradleFBProj` passes on device after the InstallReferrer bump. - Full pipeline green apart from the pre-existing, unrelated `JnienvArrayMarshaling.GetObjectArray` JNI peer-registration flake. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 70 ++++++++-- eng/gradle/credential-provider.gradle | 17 --- eng/gradle/mirror-dependencies.ps1 | 124 +++++++++++++++--- .../Tasks/MavenDownload.cs | 18 ++- .../AndroidGradleProjectTests.cs | 3 +- .../BindingBuildTest.cs | 21 +-- .../Xamarin.Android.Build.Tests/BuildTest.cs | 20 +-- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 2 +- .../BuildWithLibraryTests.cs | 4 +- .../IncrementalBuildTest.cs | 2 +- .../Tasks/MavenDownloadTests.cs | 64 +++++++-- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 6 +- .../Android/AndroidGradleProject.cs | 64 ++++++--- .../Common/TestEnvironment.cs | 25 +++- src/manifestmerger/settings.gradle | 5 - src/proguard-android/settings.gradle | 5 - src/r8/settings.gradle | 5 - .../java/JavaLib/settings.gradle | 5 - .../Tests/InstallAndRunTests.cs | 12 +- 19 files changed, 340 insertions(+), 132 deletions(-) delete mode 100644 eng/gradle/credential-provider.gradle diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index b702b46d390..e8ba82ad766 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -1,5 +1,5 @@ --- -applyTo: "**/*.gradle" +applyTo: "**/*.gradle,**/*.gradle.kts" --- # Gradle conventions @@ -9,18 +9,34 @@ All `src/*` Gradle projects share two repo config files: **`eng/gradle/plugin-re ## settings.gradle template ```groovy +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } -if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { - apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" -} dependencyResolutionManagement { apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement } rootProject.name = '' ``` +Adjust the `../..` depth to reach the repo root from that project; it is not +always two levels (e.g. `external/Java.Interop/tools/java-source-utils` uses +four). + +Kotlin DSL (`settings.gradle.kts`) applies the same two Groovy files, but passes +the receiver as `to = this`: + +```kotlin +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply(from = "$rootDir/../../eng/gradle/plugin-repositories.gradle", to = this) +} +dependencyResolutionManagement { + apply(from = "$rootDir/../../eng/gradle/dependency-repositories.gradle", to = this) +} +rootProject.name = "" +``` + `build.gradle` files must not declare their own `repositories { ... }`. ## CI vs local @@ -31,9 +47,9 @@ Both files switch on `System.getenv('RUNNINGONCI')`. Azure DevOps exports the - **`RUNNINGONCI=true`** (Azure DevOps, sourced from `RunningOnCI` in `build-tools/automation/yaml-templates/variables.yaml`) → dnceng `dotnet-public-maven` feed (CFSClean isolation, https://aka.ms/1es/netiso/CFS). Anonymous read of cached packages. - **unset** (local, Dependabot, GitHub Actions) → `google()` + `mavenCentral()` + `gradlePluginPortal()` for plugins, `google()` + `mavenCentral()` for deps. No credentials needed. -CI reads cached packages from the mirror anonymously. The Azure Artifacts -credential provider is loaded only when `ANDROID_MIRROR_MAVEN_DEPENDENCIES=true`; -`mirror-dependencies.ps1` sets this while seeding uncached packages. +CI reads cached packages from the mirror anonymously. `mirror-dependencies.ps1` +runs the same anonymous Gradle resolution, then seeds each missing URL with an +authenticated HTTP request until the build succeeds. Test the CI path locally: `$env:RUNNINGONCI='true'` (PowerShell) or `RUNNINGONCI=true ...` (bash). @@ -41,7 +57,7 @@ Test the CI path locally: `$env:RUNNINGONCI='true'` (PowerShell) or `RUNNINGONCI The new package isn't cached in the dnceng `dotnet-public-maven` feed yet. CI agents only do anonymous reads, so someone has to authenticate once locally to make the feed pull the package (and its transitive deps) from upstream. -Use the helper script — it runs the build, parses any 401 URLs out of the log, re-fetches each one with an Azure DevOps bearer token (so the feed mirrors it), and loops until the build succeeds: +Use the helper script — it runs the build, parses any 401 URLs out of the log, re-fetches each one with an Azure DevOps OAuth token using Basic authentication (so the feed mirrors it), and loops until the build succeeds: ```powershell az login # one-time, corp account with MFA satisfied @@ -56,7 +72,43 @@ The mirror must run in the project that actually needs the new package — a sib After it succeeds, just re-run the failed CI job. No PR edits needed — the packages are now anonymous-readable forever. +Tests that resolve Maven files without Gradle can seed coordinates directly: + +```powershell +pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -MavenArtifact 'androidx.core:core:1.12.0' +``` + +This attempts the coordinate's POM, JAR, AAR, and Gradle module metadata. Append +the exact filename as a fourth segment for a nonstandard payload. + +## Tests + +Tests must not reach the public internet on CI; everything routes through the +mirror. Two mechanisms in `Xamarin.ProjectTools` handle this, and both apply +unconditionally — local runs hit the same URLs as CI, so a package the mirror +lacks fails everywhere instead of only on CI: + +- **Generated Gradle projects** — `AndroidGradleProject` writes a + `settings.gradle.kts` that applies the same two shared config files by + absolute path, and copies the repository wrapper from `build-tools/gradle` + instead of running `gradle init`. Don't reintroduce `google()` / + `mavenCentral()` into generated projects, and don't let a generated project + download its own Gradle distribution on CI. +- **Non-Gradle Maven downloads** — use `TestEnvironment.DotNetPublicMaven` as + the base URL, both for `WebContent` on a `BuildItem` and for `Repository` + metadata on an ``. Don't write a `repo1.maven.org` or + `maven.google.com` URL into a test, and don't use the `"Central"` / `"Google"` + shorthands there — those are covered without network by + `MavenDownloadTests.KnownRepositoryShorthand`. + +When a test needs a coordinate the feed hasn't cached, seed it with +`-MavenArtifact` above rather than pointing the test at a public repository. + ## Don'ts - Don't hard-code Maven repo URLs in `build.gradle` / `settings.gradle`; use the shared file. -- Don't use modern `plugins { id 'com.android.application' version '...' }` DSL without confirming the plugin is in `dotnet-public-maven`; prefer `buildscript { ... } / apply plugin: '...'` when in doubt. \ No newline at end of file +- Don't use modern `plugins { id 'com.android.application' version '...' }` DSL without confirming the plugin is in `dotnet-public-maven`; prefer `buildscript { ... } / apply plugin: '...'` when in doubt. +- Don't add a Gradle credential provider or any authenticated repository to a + build. CI resolves anonymously; authentication belongs only in + `mirror-dependencies.ps1`, which seeds the feed over plain HTTP. \ No newline at end of file diff --git a/eng/gradle/credential-provider.gradle b/eng/gradle/credential-provider.gradle deleted file mode 100644 index 32f6fa95e8b..00000000000 --- a/eng/gradle/credential-provider.gradle +++ /dev/null @@ -1,17 +0,0 @@ -// The dependency-mirroring helper uses this plugin to authenticate with the -// Maven mirror. Regular local and CI builds do not apply this script. -buildscript { - repositories { - maven { - url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' - name = 'AzureArtifacts' - } - } - dependencies { - classpath 'com.microsoft.azure:artifacts-gradle-credprovider:1.1.1' - } -} - -// Plugins loaded through an applied script's buildscript classpath cannot be -// resolved by ID; Gradle requires the implementation class in this context. -apply plugin: com.microsoft.azure.artifacts.credprovider.gradle.GradleCredentialProviderPlugin diff --git a/eng/gradle/mirror-dependencies.ps1 b/eng/gradle/mirror-dependencies.ps1 index dedf0abbdfc..a1c43f1b547 100644 --- a/eng/gradle/mirror-dependencies.ps1 +++ b/eng/gradle/mirror-dependencies.ps1 @@ -1,8 +1,8 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Mirrors a gradle project's dependencies into the dnceng dotnet-public-maven - Azure Artifacts feed so CI can resolve them anonymously. + Mirrors Gradle dependencies or explicit Maven artifacts into the dnceng + dotnet-public-maven Azure Artifacts feed so CI can resolve them anonymously. .DESCRIPTION When Dependabot bumps a gradle dependency (or its transitive graph changes), @@ -12,10 +12,11 @@ This script does that by running the requested gradle build in a loop: 1. Run gradle with RUNNINGONCI=true so it points at the dnceng feed. - 2. Parse any 'Could not GET' URLs out of the build log. - 3. Re-fetch each failing URL with an Azure DevOps OAuth bearer token - (obtained via `az account get-access-token`). The feed's upstream - connector then pulls the package and caches it for anonymous reads. + 2. Parse any 'Could not GET/HEAD' URLs out of the build log. + 3. Re-fetch each failing URL with an Azure DevOps OAuth token using Basic + authentication (obtained via `az account get-access-token`). The + feed's upstream connector then pulls the package and caches it for + anonymous reads. 4. Repeat until the build succeeds or no more 401s appear. After the loop converges, no PR edits are needed — just re-run the failing @@ -30,6 +31,18 @@ Gradle task(s) to run. Should be one that resolves the new dependency graph (e.g. 'assembleDebug', 'build', 'extractProguardFiles'). +.PARAMETER MavenArtifact + Maven coordinates to mirror directly, for tests that do not use Gradle. + Each value is group:artifact:version, which attempts the POM, JAR, AAR, and + Gradle module metadata files. Append an exact filename as a fourth segment + when a test requests a nonstandard payload. + +.PARAMETER GradleWrapper + Optional path to the Gradle wrapper used by CI for this project, relative + to the repository root or absolute. Defaults to build-tools/gradle/gradlew. + Use this when a subproject has its own wrapper so Gradle resolves the same + dependency variants that CI requests. + .PARAMETER AndroidHome Optional path to the Android SDK. Required when the gradle build needs it (any project using the com.android.* plugins). Defaults to the value of @@ -47,29 +60,55 @@ .EXAMPLE pwsh ./eng/gradle/mirror-dependencies.ps1 -ProjectDir src/proguard-android -Task extractProguardFiles + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -ProjectDir external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle ` + -Task classes ` + -GradleWrapper external/Java.Interop/build-tools/gradle/gradlew.bat + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -MavenArtifact 'androidx.core:core:1.12.0', ` + 'com.facebook.react:react-android:0.76.1:react-android-0.76.1.module' #> -[CmdletBinding()] +[CmdletBinding(DefaultParameterSetName='Gradle')] param( - [Parameter(Mandatory=$true)] - [string] $ProjectDir, + [Parameter(Mandatory=$true, ParameterSetName='Gradle')] + [string] $ProjectDir = '.', - [Parameter(Mandatory=$true)] + [Parameter(Mandatory=$true, ParameterSetName='Gradle')] [string] $Task, + [Parameter(Mandatory=$true, ParameterSetName='MavenArtifact')] + [string[]] $MavenArtifact, + + [Parameter(ParameterSetName='Gradle')] + [string] $GradleWrapper, + + [Parameter(ParameterSetName='Gradle')] [string] $AndroidHome = $env:ANDROID_HOME, + [Parameter(ParameterSetName='Gradle')] [int] $MaxIterations = 15 ) $ErrorActionPreference = 'Stop' $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../..') | Select-Object -ExpandProperty Path $projectDirAbs = Resolve-Path (Join-Path $repoRoot $ProjectDir) -ErrorAction Stop | Select-Object -ExpandProperty Path -$gradlew = if ($IsWindows -or $env:OS -eq 'Windows_NT') { - Join-Path $repoRoot 'build-tools/gradle/gradlew.bat' +$defaultGradleWrapper = if ($IsWindows -or $env:OS -eq 'Windows_NT') { + 'build-tools/gradle/gradlew.bat' } else { - Join-Path $repoRoot 'build-tools/gradle/gradlew' + 'build-tools/gradle/gradlew' } -if (-not (Test-Path $gradlew)) { throw "gradlew not found at $gradlew" } +$gradleWrapperPath = if ([string]::IsNullOrEmpty($GradleWrapper)) { + Join-Path $repoRoot $defaultGradleWrapper +} elseif ([IO.Path]::IsPathRooted($GradleWrapper)) { + $GradleWrapper +} else { + Join-Path $repoRoot $GradleWrapper +} +$gradlew = Resolve-Path $gradleWrapperPath -ErrorAction Stop | Select-Object -ExpandProperty Path # Azure DevOps resource id — same for every AzDO tenant. $azDevOpsResource = '499b84ac-1321-427f-aa17-267ca6975798' @@ -83,13 +122,14 @@ function Get-AzDevOpsToken { } function Invoke-Mirror($logPath) { - $urls = Select-String -Path $logPath -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | + $urls = Select-String -Path $logPath -Pattern "Could not (?:GET|HEAD) '(https://pkgs\.dev\.azure\.com/dnceng/[^']+)'" -AllMatches | ForEach-Object { $_.Matches } | - ForEach-Object { $_.Value -replace "^Could not GET '", "" -replace "'$", "" } | + ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique if ($urls.Count -eq 0) { return 0 } $token = Get-AzDevOpsToken - $headers = @{ Authorization = "Bearer $token" } + $basicCredential = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$token")) + $headers = @{ Authorization = "Basic $basicCredential" } $ok = 0; $fail = 0 foreach ($u in $urls) { try { @@ -104,17 +144,59 @@ function Invoke-Mirror($logPath) { return $urls.Count } +function Get-MavenArtifactUrls($artifacts) { + $feedBaseUrl = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' + foreach ($artifact in $artifacts) { + $parts = $artifact.Split(':', 4) + if ($parts.Count -lt 3) { + throw "Invalid Maven artifact '$artifact'. Expected group:artifact:version[:filename]." + } + $group = $parts[0].Replace('.', '/') + $name = $parts[1] + $version = $parts[2] + $filenames = if ($parts.Count -eq 4) { + @($parts[3]) + } else { + @( + "$name-$version.pom" + "$name-$version.jar" + "$name-$version.aar" + "$name-$version.module" + ) + } + foreach ($filename in $filenames) { + "$feedBaseUrl/$group/$name/$version/$filename" + } + } +} + +# Verify az is available and authenticated up front so we fail fast. +Get-AzDevOpsToken | Out-Null + +if ($PSCmdlet.ParameterSetName -eq 'MavenArtifact') { + Write-Host "Mirroring Maven artifacts directly:" + $MavenArtifact | ForEach-Object { Write-Host " $_" } + $log = Join-Path ([IO.Path]::GetTempPath()) 'maven-artifact-mirror.log' + try { + Get-MavenArtifactUrls $MavenArtifact | + ForEach-Object { "Could not GET '$_'" } | + Set-Content $log + Invoke-Mirror $log | Out-Null + } + finally { + Remove-Item $log -ErrorAction SilentlyContinue + } + return +} + Write-Host "Repo root: $repoRoot" Write-Host "Project: $projectDirAbs" Write-Host "Task: $Task" +Write-Host "Gradle: $gradlew" if ($AndroidHome) { Write-Host "ANDROID_HOME: $AndroidHome" } -# Verify az is available and authenticated up front so we fail fast. -Get-AzDevOpsToken | Out-Null - if ($AndroidHome) { $env:ANDROID_HOME = $AndroidHome } $env:RUNNINGONCI = 'true' -$env:ANDROID_MIRROR_MAVEN_DEPENDENCIES = 'true' Push-Location $projectDirAbs try { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs b/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs index 0521682daf6..740a1a22fb9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/MavenDownload.cs @@ -136,16 +136,24 @@ public async override System.Threading.Tasks.Task RunTaskAsync () return result; } - CachedMavenRepository? GetRepository (ITaskItem item) - { - var type = item.GetMetadataOrDefault ("Repository", "Central"); - - var repo = type.ToLowerInvariant () switch { + /// + /// Maps the well-known Repository metadata shorthands to their repositories. + /// Returns if is not a known shorthand, + /// in which case it is treated as a repository URL. + /// + internal static MavenRepository? GetKnownRepository (string type) => + type.ToLowerInvariant () switch { "central" => MavenRepository.Central, "google" => MavenRepository.Google, _ => null }; + CachedMavenRepository? GetRepository (ITaskItem item) + { + var type = item.GetMetadataOrDefault ("Repository", "Central"); + + var repo = GetKnownRepository (type); + if (repo is null && type.StartsWith ("http", StringComparison.OrdinalIgnoreCase)) { using var hasher = SHA256.Create (); var hash = hasher.ComputeHash (Encoding.UTF8.GetBytes (type)); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs index 3f548e1e595..966d7cb5d96 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs @@ -43,6 +43,7 @@ public void GradleTestTearDown () public void BuildApp () { var gradleProject = AndroidGradleProject.CreateDefault (GradleTestProjectDir, isApplication: true); + FileAssert.Exists (Path.Combine (GradleTestProjectDir, TestEnvironment.IsWindows ? "gradlew.bat" : "gradlew")); var moduleName = gradleProject.Modules.First ().Name; var proj = new XamarinAndroidApplicationProject { @@ -405,7 +406,7 @@ namespace = ""{gradleModule.PackageName}"" dependencies {{ implementation(""androidx.appcompat:appcompat:1.6.1"") implementation(""com.google.android.material:material:1.11.0"") - implementation(""com.facebook.android:facebook-android-sdk:latest.release"") + implementation(""com.facebook.android:facebook-android-sdk:18.3.0"") }} "; gradleModule.JavaSources.Add (new AndroidItem.AndroidJavaSource ("FacebookSdk.java") { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs index f970281c0e2..6827ea1edf3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs @@ -178,7 +178,7 @@ public void BuildAarBindingLibraryStandalone (string classParser) } }; proj.Jars.Add (new AndroidItem.AndroidLibrary ("Jars\\material-menu-1.1.0.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" }); proj.AndroidClassParser = classParser; using (var b = CreateDllBuilder ()) { @@ -195,7 +195,7 @@ public void BuildAarBindigLibraryWithNuGetPackageOfJar (string classParser) IsRelease = true, }; proj.Jars.Add (new AndroidItem.LibraryProjectZip ("Jars\\android-crop-1.0.1.aar") { - WebContent = "https://repo1.maven.org/maven2/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" }); proj.MetadataXml = @" @@ -575,7 +575,7 @@ public void DesignTimeBuild (string classParser) AndroidClassParser = classParser }; proj.Jars.Add (new AndroidItem.LibraryProjectZip ("Jars\\material-menu-1.1.0.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" }); using (var b = CreateDllBuilder ()) { Assert.IsTrue (b.DesignTimeBuild (proj), "design-time build should have succeeded."); @@ -765,7 +765,7 @@ public void LibraryProjectZipWithLint () AndroidClassParser = "class-parse", Jars = { new AndroidItem.LibraryProjectZip ("fragment-1.2.2.aar") { - WebContent = "https://maven.google.com/androidx/fragment/fragment/1.2.2/fragment-1.2.2.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/androidx/fragment/fragment/1.2.2/fragment-1.2.2.aar" } }, MetadataXml = @"" @@ -805,7 +805,7 @@ public void CheckDuplicateJavaLibraries () // repackaged.jar new AndroidItem.AndroidLibrary ("emoji2-1.4.0.aar") { MetadataValues = "Bind=false", - WebContent = "https://maven.google.com/androidx/emoji2/emoji2/1.4.0/emoji2-1.4.0.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/androidx/emoji2/emoji2/1.4.0/emoji2-1.4.0.aar", }, }, }; @@ -820,7 +820,7 @@ public void CheckDuplicateJavaLibraries () // repackaged.jar new AndroidItem.AndroidLibrary ("connect-client-1.1.0-alpha07.aar") { MetadataValues = "Bind=false", - WebContent = "https://maven.google.com/androidx/health/connect/connect-client/1.1.0-alpha07/connect-client-1.1.0-alpha07.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/androidx/health/connect/connect-client/1.1.0-alpha07/connect-client-1.1.0-alpha07.aar", }, }, }; @@ -844,6 +844,7 @@ public void AndroidMavenLibrary () // Test that downloads .jar from Maven and successfully binds it var item = new BuildItem ("AndroidMavenLibrary", "com.google.auto.value:auto-value-annotations"); item.Metadata.Add ("Version", "1.10.4"); + item.Metadata.Add ("Repository", TestEnvironment.DotNetPublicMaven); var proj = new XamarinAndroidBindingProject { Jars = { item } @@ -865,7 +866,7 @@ public void AndroidMavenLibrary_FailsDueToUnverifiedDependency () // var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); item.Metadata.Add ("Version", "1.9.0"); - item.Metadata.Add ("Repository", "Google"); + item.Metadata.Add ("Repository", TestEnvironment.DotNetPublicMaven); var proj = new XamarinAndroidBindingProject { Jars = { item } @@ -887,7 +888,7 @@ public void AndroidMavenLibrary_IgnoreDependencyVerification () // var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); item.Metadata.Add ("Version", "1.9.0"); - item.Metadata.Add ("Repository", "Google"); + item.Metadata.Add ("Repository", TestEnvironment.DotNetPublicMaven); item.Metadata.Add ("VerifyDependencies", "false"); item.Metadata.Add ("Bind", "false"); @@ -909,7 +910,7 @@ public void AndroidMavenLibrary_AllDependenciesAreVerified () // var item = new BuildItem ("AndroidMavenLibrary", "androidx.core:core"); item.Metadata.Add ("Version", "1.9.0"); - item.Metadata.Add ("Repository", "Google"); + item.Metadata.Add ("Repository", TestEnvironment.DotNetPublicMaven); item.Metadata.Add ("Bind", "false"); // Dependency fulfilled by @@ -921,7 +922,7 @@ public void AndroidMavenLibrary_AllDependenciesAreVerified () // Dependency fulfilled by var annotations_experimental_androidlib = new BuildItem ("AndroidMavenLibrary", "androidx.annotation:annotation-experimental"); annotations_experimental_androidlib.Metadata.Add ("Version", "1.3.0"); - annotations_experimental_androidlib.Metadata.Add ("Repository", "Google"); + annotations_experimental_androidlib.Metadata.Add ("Repository", TestEnvironment.DotNetPublicMaven); annotations_experimental_androidlib.Metadata.Add ("Bind", "false"); annotations_experimental_androidlib.Metadata.Add ("VerifyDependencies", "false"); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index a895295d526..c4342d6cdda 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -61,7 +61,7 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo TextContent = () => "", }, new AndroidItem.AndroidLibrary ("material-menu-1.1.0.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" }, } }; @@ -494,13 +494,13 @@ public void AarContentExtraction () { var aar = new AndroidItem.AndroidAarLibrary ("Jars\\android-crop-1.0.1.aar") { // https://mvnrepository.com/artifact/com.soundcloud.android/android-crop/1.0.1 - WebContent = "https://repo1.maven.org/maven2/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" }; var proj = new XamarinAndroidApplicationProject () { OtherBuildItems = { aar, new AndroidItem.AndroidAarLibrary ("fragment-1.2.2.aar") { - WebContent = "https://maven.google.com/androidx/fragment/fragment/1.2.2/fragment-1.2.2.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/androidx/fragment/fragment/1.2.2/fragment-1.2.2.aar" } }, }; @@ -953,19 +953,19 @@ public override void OnReceive(Context context, Intent intent) { } }); } proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "okio-1.13.0.jar") { - WebContent = "https://repo1.maven.org/maven2/com/squareup/okio/okio/1.13.0/okio-1.13.0.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/squareup/okio/okio/1.13.0/okio-1.13.0.jar" }); proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "okhttp-3.8.0.jar") { - WebContent = "https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/3.8.0/okhttp-3.8.0.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/squareup/okhttp3/okhttp/3.8.0/okhttp-3.8.0.jar" }); proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "retrofit-2.3.0.jar") { - WebContent = "https://repo1.maven.org/maven2/com/squareup/retrofit2/retrofit/2.3.0/retrofit-2.3.0.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/squareup/retrofit2/retrofit/2.3.0/retrofit-2.3.0.jar" }); proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "converter-gson-2.3.0.jar") { - WebContent = "https://repo1.maven.org/maven2/com/squareup/retrofit2/converter-gson/2.3.0/converter-gson-2.3.0.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/squareup/retrofit2/converter-gson/2.3.0/converter-gson-2.3.0.jar" }); proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "gson-2.7.jar") { - WebContent = "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.7/gson-2.7.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/google/code/gson/gson/2.7/gson-2.7.jar" }); /* The source is simple: * @@ -1355,10 +1355,10 @@ public void KotlinServiceLoader ([Values ("apk", "aab")] string packageFormat) // Disable fast deployment for aabs because it is not currently compatible and so gives an XA0119 build error. proj.EmbedAssembliesIntoApk = true; proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "kotlinx-coroutines-android-1.3.2.jar") { - WebContent = "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.3.2/kotlinx-coroutines-android-1.3.2.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.3.2/kotlinx-coroutines-android-1.3.2.jar" }); proj.OtherBuildItems.Add (new BuildItem ("AndroidJavaLibrary", "gson-2.7.jar") { - WebContent = "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.7/gson-2.7.jar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/google/code/gson/gson/2.7/gson-2.7.jar" }); using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "build should have succeeded."); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 9ab18fc3307..94cc8fa27a6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -711,7 +711,7 @@ public void SkipConvertResourcesCases () var target = "ConvertResourcesCases"; var proj = new XamarinFormsAndroidApplicationProject (); proj.OtherBuildItems.Add (new BuildItem ("AndroidAarLibrary", "Jars\\material-menu-1.1.0.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar" }); using (var b = CreateApkBuilder ()) { b.Verbosity = LoggerVerbosity.Detailed; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index fb558936ae2..910854fa543 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -132,10 +132,10 @@ public Foo () BinaryContent = () => ResourceData.JavaSourceJarTestJar, }, new AndroidItem.AndroidLibrary ("sub\\directory\\bar.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", }, new AndroidItem.AndroidLibrary ("sub\\directory\\baz.aar") { - WebContent = "https://repo1.maven.org/maven2/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar", MetadataValues = "Bind=false", }, new AndroidItem.AndroidJavaSource ("JavaSourceTestExtension.java") { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index c2b61a40093..b8b2b491fe5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -870,7 +870,7 @@ public void ResolveLibraryProjectImports () // Add a new AAR file to the project var aar = new AndroidItem.AndroidAarLibrary ("Jars\\android-crop-1.0.1.aar") { - WebContent = "https://repo1.maven.org/maven2/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/soundcloud/android/android-crop/1.0.1/android-crop-1.0.1.aar" }; proj.OtherBuildItems.Add (aar); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs index 55d81bb69c3..3bce446f18c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/MavenDownloadTests.cs @@ -7,6 +7,7 @@ using Microsoft.Build.Utilities; using NUnit.Framework; using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; using Task = System.Threading.Tasks.Task; namespace Xamarin.Android.Build.Tests; @@ -75,6 +76,9 @@ public async Task UnknownRepository () [Test] public async Task UnknownArtifact () { + if (TestEnvironment.IsRunningOnCI) + Assert.Ignore ("The CI mirror returns 401 for uncached artifacts instead of Maven Central's 404."); + var engine = new MockBuildEngine (TestContext.Out, new List ()); var task = new MavenDownload { BuildEngine = engine, @@ -91,6 +95,9 @@ public async Task UnknownArtifact () [Test] public async Task UnknownPom () { + if (TestEnvironment.IsRunningOnCI) + Assert.Ignore ("The CI mirror returns 401 for uncached artifacts instead of Maven Central's 404."); + var temp_cache_dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); try { @@ -103,7 +110,10 @@ public async Task UnknownPom () // Create the dummy jar so we bypass that step and try to download the dummy pom var dummy_jar = Path.Combine (temp_cache_dir, "central", "com.example", "dummy", "1.0.0", "dummy-1.0.0.jar"); - Directory.CreateDirectory (Path.GetDirectoryName (dummy_jar)!); + var dummy_jar_directory = Path.GetDirectoryName (dummy_jar); + if (dummy_jar_directory is null) + throw new InvalidOperationException ($"Could not determine the directory for '{dummy_jar}'."); + Directory.CreateDirectory (dummy_jar_directory); using (File.Create (dummy_jar)) { } @@ -126,7 +136,7 @@ public async Task MavenCentralSuccess () var task = new MavenDownload { BuildEngine = engine, MavenCacheDirectory = temp_cache_dir, - AndroidMavenLibraries = [CreateMavenTaskItem ("com.google.auto.value:auto-value-annotations", "1.10.4")], + AndroidMavenLibraries = [CreateMavenTaskItem ("com.google.auto.value:auto-value-annotations", "1.10.4", TestEnvironment.DotNetPublicMaven)], }; await task.RunTaskAsync (); @@ -134,10 +144,14 @@ public async Task MavenCentralSuccess () Assert.AreEqual (0, engine.Errors.Count); Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries?.Length); - var output_item = task.ResolvedAndroidMavenLibraries! [0]; + var output_items = task.ResolvedAndroidMavenLibraries; + if (output_items is null) + throw new InvalidOperationException ("MavenDownload did not produce resolved libraries."); + var output_item = output_items [0]; Assert.AreEqual ("com.google.auto.value:auto-value-annotations:1.10.4", output_item.GetMetadata ("JavaArtifact")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "central", "com.google.auto.value", "auto-value-annotations", "1.10.4", "auto-value-annotations-1.10.4.pom"), output_item.GetMetadata ("Manifest")); + Assert.That (output_item.GetMetadata ("Manifest"), Does.StartWith (temp_cache_dir)); + Assert.That (output_item.GetMetadata ("Manifest"), Does.EndWith (Path.Combine ("com.google.auto.value", "auto-value-annotations", "1.10.4", "auto-value-annotations-1.10.4.pom"))); } finally { DeleteTempDirectory (temp_cache_dir); } @@ -153,7 +167,7 @@ public async Task MavenGoogleSuccess () var task = new MavenDownload { BuildEngine = engine, MavenCacheDirectory = temp_cache_dir, - AndroidMavenLibraries = [CreateMavenTaskItem ("androidx.core:core", "1.12.0", "Google")], + AndroidMavenLibraries = [CreateMavenTaskItem ("androidx.core:core", "1.12.0", TestEnvironment.DotNetPublicMaven)], }; await task.RunTaskAsync (); @@ -161,10 +175,14 @@ public async Task MavenGoogleSuccess () Assert.AreEqual (0, engine.Errors.Count); Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries?.Length); - var output_item = task.ResolvedAndroidMavenLibraries! [0]; + var output_items = task.ResolvedAndroidMavenLibraries; + if (output_items is null) + throw new InvalidOperationException ("MavenDownload did not produce resolved libraries."); + var output_item = output_items [0]; Assert.AreEqual ("androidx.core:core:1.12.0", output_item.GetMetadata ("JavaArtifact")); - Assert.AreEqual (Path.Combine (temp_cache_dir, "google", "androidx.core", "core", "1.12.0", "core-1.12.0.pom"), output_item.GetMetadata ("Manifest")); + Assert.That (output_item.GetMetadata ("Manifest"), Does.StartWith (temp_cache_dir)); + Assert.That (output_item.GetMetadata ("Manifest"), Does.EndWith (Path.Combine ("androidx.core", "core", "1.12.0", "core-1.12.0.pom"))); } finally { DeleteTempDirectory (temp_cache_dir); } @@ -182,7 +200,7 @@ public async Task ArtifactFilenameOverride () var task = new MavenDownload { BuildEngine = engine, MavenCacheDirectory = temp_cache_dir, - AndroidMavenLibraries = [CreateMavenTaskItem ("com.facebook.react:react-android", "0.76.1", artifactFilename: "react-android-0.76.1.module")], + AndroidMavenLibraries = [CreateMavenTaskItem ("com.facebook.react:react-android", "0.76.1", TestEnvironment.DotNetPublicMaven, artifactFilename: "react-android-0.76.1.module")], }; await task.RunTaskAsync (); @@ -190,16 +208,42 @@ public async Task ArtifactFilenameOverride () Assert.AreEqual (0, engine.Errors.Count); Assert.AreEqual (1, task.ResolvedAndroidMavenLibraries?.Length); - var output_item = task.ResolvedAndroidMavenLibraries! [0]; + var output_items = task.ResolvedAndroidMavenLibraries; + if (output_items is null) + throw new InvalidOperationException ("MavenDownload did not produce resolved libraries."); + var output_item = output_items [0]; Assert.AreEqual ("com.facebook.react:react-android:0.76.1", output_item.GetMetadata ("JavaArtifact")); Assert.True (output_item.ItemSpec.EndsWith (Path.Combine ("0.76.1", "react-android-0.76.1.module"), StringComparison.OrdinalIgnoreCase)); - Assert.AreEqual (Path.Combine (temp_cache_dir, "central", "com.facebook.react", "react-android", "0.76.1", "react-android-0.76.1.pom"), output_item.GetMetadata ("Manifest")); + Assert.That (output_item.GetMetadata ("Manifest"), Does.StartWith (temp_cache_dir)); + Assert.That (output_item.GetMetadata ("Manifest"), Does.EndWith (Path.Combine ("com.facebook.react", "react-android", "0.76.1", "react-android-0.76.1.pom"))); } finally { DeleteTempDirectory (temp_cache_dir); } } + // The tests below route every download through TestEnvironment.DotNetPublicMaven, so the + // "Central"/"Google" shorthands are never exercised there. Cover them directly instead -- + // this needs no network, so it also runs under CI's network isolation. + [TestCase ("Central", "central")] + [TestCase ("central", "central")] + [TestCase ("Google", "google")] + [TestCase ("google", "google")] + public void KnownRepositoryShorthand (string metadata, string expectedName) + { + var repository = MavenDownload.GetKnownRepository (metadata); + + Assert.IsNotNull (repository); + Assert.AreEqual (expectedName, repository?.Name); + } + + [TestCase ("bad-repo")] + [TestCase ("https://repo1.maven.org/maven2/")] + public void UnknownRepositoryShorthand (string metadata) + { + Assert.IsNull (MavenDownload.GetKnownRepository (metadata)); + } + ITaskItem CreateMavenTaskItem (string name, string? version, string? repository = null, string? artifactFilename = null) { var item = new TaskItem (name); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 5a2e4963605..fc93e59b0e0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -89,7 +89,7 @@ public void DotNetPack (string dotnetVersion, string platform, Version apiLevel) BinaryContent = () => ResourceData.JavaSourceJarTestJar, }, new AndroidItem.AndroidLibrary ("sub\\directory\\bar.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", }, new AndroidItem.AndroidJavaSource ("JavaSourceTest.java") { Encoding = Encoding.ASCII, @@ -120,12 +120,12 @@ public String Say (String quote) { BinaryContent = () => [], }); proj.OtherBuildItems.Add (new AndroidItem.LibraryProjectZip ("..\\baz.aar") { - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", MetadataValues = "Bind=false", }); proj.OtherBuildItems.Add (new AndroidItem.AndroidLibrary (default (Func)) { Update = () => "nopack.aar", - WebContent = "https://repo1.maven.org/maven2/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", + WebContent = $"{TestEnvironment.DotNetPublicMaven}/com/balysv/material-menu/1.1.0/material-menu-1.1.0.aar", MetadataValues = "Pack=false;Bind=false", }); proj.OtherBuildItems.Add (new AndroidItem.AndroidMavenLibrary ("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm") { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs index 09b177270ac..2ea2e388dd6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs @@ -13,8 +13,6 @@ public class AndroidGradleProject public string BuildFilePath => Path.Combine (ProjectDirectory, "build.gradle.kts"); - GradleCLI gradleCLI = new GradleCLI (); - public AndroidGradleProject (string directory) { ProjectDirectory = directory; @@ -23,15 +21,49 @@ public AndroidGradleProject (string directory) public void Create () { Directory.CreateDirectory (ProjectDirectory); - gradleCLI.Init (ProjectDirectory); + CopyGradleWrapper (); var settingsFile = Path.Combine (ProjectDirectory, "settings.gradle.kts"); - File.WriteAllText (settingsFile, settings_gradle_kts_content); + File.WriteAllText (settingsFile, GetSettingsGradleKtsContent ()); File.WriteAllText (BuildFilePath, build_gradle_kts_content); foreach (var module in Modules) { module.Create (); File.AppendAllText (settingsFile, $"{Environment.NewLine}include(\":{module.Name}\")"); } - File.AppendAllText (Path.Combine (ProjectDirectory, "gradle.properties"), "android.useAndroidX=true"); + File.WriteAllText (Path.Combine (ProjectDirectory, "gradle.properties"), """ +# Exercise Gradle configuration-cache compatibility. +org.gradle.configuration-cache=true +# Build independent modules concurrently. +org.gradle.parallel=true +# Reuse task outputs across test builds. +org.gradle.caching=true +# Required by the AndroidX dependencies used by generated modules. +android.useAndroidX=true +"""); + } + + /// + /// Copies the repository wrapper so generated projects do not depend on gradle init or a CI distribution download. + /// + void CopyGradleWrapper () + { + var sourceDirectory = Path.Combine (XABuildPaths.TopDirectory, "build-tools", "gradle"); + var destinationWrapperDirectory = Path.Combine (ProjectDirectory, "gradle", "wrapper"); + Directory.CreateDirectory (destinationWrapperDirectory); + + CopyFile ("gradlew", ProjectDirectory); + CopyFile ("gradlew.bat", ProjectDirectory); + CopyFile (Path.Combine ("gradle", "wrapper", "gradle-wrapper.jar"), destinationWrapperDirectory); + CopyFile (Path.Combine ("gradle", "wrapper", "gradle-wrapper.properties"), destinationWrapperDirectory); + + void CopyFile (string relativePath, string destinationDirectory) + { + var source = Path.Combine (sourceDirectory, relativePath); + var destination = Path.Combine (destinationDirectory, Path.GetFileName (relativePath)); + File.Copy (source, destination, overwrite: true); + if (!TestEnvironment.IsWindows) { + File.SetUnixFileMode (destination, File.GetUnixFileMode (source)); + } + } } public static AndroidGradleProject CreateDefault (string projectDir, bool isApplication = false) @@ -54,23 +86,21 @@ public static AndroidGradleProject CreateDefault (string projectDir, bool isAppl id(""com.android.library"") version ""8.5.0"" apply false } "; - const string settings_gradle_kts_content = -@" + string GetSettingsGradleKtsContent () + { + var gradleConfigurationDirectory = Path.Combine (XABuildPaths.TopDirectory, "eng", "gradle").Replace ('\\', '/'); + + return $$""" +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } + apply(from = "{{gradleConfigurationDirectory}}/plugin-repositories.gradle", to = this) } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } + apply(from = "{{gradleConfigurationDirectory}}/dependency-repositories.gradle", to = this) } -"; +"""; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/TestEnvironment.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/TestEnvironment.cs index 639b9e5db8f..a5450607a8d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/TestEnvironment.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/TestEnvironment.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Xamarin.Android.Tools; namespace Xamarin.ProjectTools @@ -20,6 +21,18 @@ namespace Xamarin.ProjectTools /// public static class TestEnvironment { + /// + /// The dnceng dotnet-public-maven feed, which mirrors both Maven Central and + /// Google Maven and is readable anonymously. + /// + /// + /// Tests download through this feed unconditionally, including locally, so that a local + /// run exercises exactly the same URLs as CI. CI agents are network-isolated and can only + /// reach this feed, so an artifact the mirror does not have must fail everywhere rather + /// than only on CI. + /// + public const string DotNetPublicMaven = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1"; + [DllImport ("libc")] static extern int uname (IntPtr buf); @@ -43,6 +56,12 @@ static bool IsDarwin () /// /// Gets a value indicating whether the current platform is Windows. /// + /// + /// teaches the platform-compatibility + /// analyzer (CA1416) that this property is a windows guard, so callers can use it + /// instead of . + /// + [SupportedOSPlatformGuard ("windows")] public static bool IsWindows { get { return Environment.OSVersion.Platform == PlatformID.Win32NT; @@ -52,6 +71,7 @@ public static bool IsWindows { /// /// Gets a value indicating whether the current platform is macOS. /// + [SupportedOSPlatformGuard ("macos")] public static bool IsMacOS { get { return IsDarwin (); @@ -61,12 +81,16 @@ public static bool IsMacOS { /// /// Gets a value indicating whether the current platform is Linux. /// + [SupportedOSPlatformGuard ("linux")] public static bool IsLinux { get { return !IsWindows && !IsMacOS; } } + public static bool IsRunningOnCI => + string.Equals (Environment.GetEnvironmentVariable ("RUNNINGONCI"), "true", StringComparison.OrdinalIgnoreCase); + /// /// The MonoAndroid reference assemblies directory within a local build tree, e.g. bin/Debug/lib/packs/Microsoft.Android.Ref.34/34.99.0/ref/net8.0/
/// If a local build tree can not be found, or if it is empty, this will return the system installation location instead:
@@ -198,4 +222,3 @@ static Version ParseVersion (string path) public static bool IsUsingJdk11 => AndroidSdkResolver.GetJavaSdkVersionString ().Contains ("11.0"); } } - diff --git a/src/manifestmerger/settings.gradle b/src/manifestmerger/settings.gradle index 47d043fb305..ce3501521db 100644 --- a/src/manifestmerger/settings.gradle +++ b/src/manifestmerger/settings.gradle @@ -2,11 +2,6 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } - -if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { - apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" -} - dependencyResolutionManagement { apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement } diff --git a/src/proguard-android/settings.gradle b/src/proguard-android/settings.gradle index 8a8d63d070d..820b2c40e49 100644 --- a/src/proguard-android/settings.gradle +++ b/src/proguard-android/settings.gradle @@ -2,11 +2,6 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } - -if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { - apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" -} - dependencyResolutionManagement { apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement } diff --git a/src/r8/settings.gradle b/src/r8/settings.gradle index 39e8674b5d8..c4f158d7464 100644 --- a/src/r8/settings.gradle +++ b/src/r8/settings.gradle @@ -2,11 +2,6 @@ pluginManagement { apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } - -if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { - apply from: "${rootDir}/../../eng/gradle/credential-provider.gradle" -} - dependencyResolutionManagement { apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement } diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle index 0f5b891e40c..d36a773edd5 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle @@ -2,11 +2,6 @@ pluginManagement { apply from: "${rootDir}/../../../../../eng/gradle/plugin-repositories.gradle", to: pluginManagement } - -if (System.getenv('ANDROID_MIRROR_MAVEN_DEPENDENCIES') == 'true') { - apply from: "${rootDir}/../../../../../eng/gradle/credential-provider.gradle" -} - dependencyResolutionManagement { apply from: "${rootDir}/../../../../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement } diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 054d3ab75dc..94500fd35ab 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1619,6 +1619,7 @@ public void MicrosoftIntune ([Values (false, true)] bool isRelease) [Test] public void GradleFBProj ([Values (false, true)] bool isRelease) { + const string facebookVersion = "18.3.0"; var moduleName = "Library"; var gradleTestProjectDir = Path.Combine (Root, "temp", "gradle", TestName); var gradleModule = new AndroidGradleModule (Path.Combine (gradleTestProjectDir, moduleName)); @@ -1637,7 +1638,7 @@ namespace = ""{gradleModule.PackageName}"" dependencies {{ implementation(""androidx.appcompat:appcompat:1.7.0"") implementation(""com.google.android.material:material:1.11.0"") - implementation(""com.facebook.android:facebook-android-sdk:17.0.2"") + implementation(""com.facebook.android:facebook-android-sdk:{facebookVersion}"") }} "; gradleModule.JavaSources.Add (new AndroidItem.AndroidJavaSource ("FacebookSdk.java") { @@ -1682,14 +1683,16 @@ public static void logEvent(String eventName) {{ }, new BuildItem ("AndroidMavenLibrary", "com.facebook.android:facebook-core") { Metadata = { - { "Version", "17.0.2" }, + { "Version", facebookVersion }, { "Bind", "false" }, + { "Repository", TestEnvironment.DotNetPublicMaven }, }, }, new BuildItem ("AndroidMavenLibrary", "com.facebook.android:facebook-bolts") { Metadata = { - { "Version", "17.0.2" }, + { "Version", facebookVersion }, { "Bind", "false" }, + { "Repository", TestEnvironment.DotNetPublicMaven }, }, }, }, @@ -1708,7 +1711,8 @@ public static void logEvent(String eventName) {{ }, new Package { Id = "Xamarin.Google.Android.InstallReferrer", - Version = "1.1.2.6", + // Facebook SDK 18.3.0 requires com.android.installreferrer:installreferrer:2.2. + Version = "2.2.0.8", }, new Package { Id = "Xamarin.AndroidX.Core.Core.Ktx", From 2e3c7ac153a25ea5da1ad6375482cb47666dcd3d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 13 Aug 2026 13:08:58 -0500 Subject: [PATCH 5/6] [tests] Route XASdk Maven dependency through public feed (#12368) The original Kotlin artifact is now mirrored through dotnet-public-maven, so keep the existing package while routing it through the approved feed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/XASdkTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index fc93e59b0e0..f5286cb46f8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -129,7 +129,7 @@ public String Say (String quote) { MetadataValues = "Pack=false;Bind=false", }); proj.OtherBuildItems.Add (new AndroidItem.AndroidMavenLibrary ("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm") { - MetadataValues = "Version=1.3.3;Bind=false", + MetadataValues = $"Version=1.3.3;Bind=false;Repository={TestEnvironment.DotNetPublicMaven}", BinaryContent = () => [], }); From 830e1a0540c3f3974658bdf36c9f17b7a7c7643a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 14 Aug 2026 14:59:12 -0500 Subject: [PATCH 6/6] [release/10.0.1xx] Preserve Gradle 8 compatibility Keep the release branch on its existing Gradle 8.12 wrapper and AGP 8.7 while retaining the public Maven repository routing from the backported mirror series. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- build-tools/gradle/gradle/wrapper/gradle-wrapper.properties | 2 +- src/proguard-android/build.gradle | 2 +- .../java/JavaLib/build.gradle | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties index 221c4f98228..d6e308a6378 100644 --- a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/proguard-android/build.gradle b/src/proguard-android/build.gradle index e70b9fa5fe7..9be3b1b1e74 100644 --- a/src/proguard-android/build.gradle +++ b/src/proguard-android/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.android.application' version '9.2.1' + id 'com.android.application' version '8.7.0' } android { diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle index 3e75fba8418..ee6ee439a57 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle @@ -1,10 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.library' version '9.2.1' apply false + id 'com.android.library' version '8.7.0' apply false } task clean(type: Delete) { delete rootProject.layout.buildDirectory } -