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 new file mode 100644 index 00000000000..e8ba82ad766 --- /dev/null +++ b/.github/instructions/gradle.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: "**/*.gradle,**/*.gradle.kts" +--- + +# 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 +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} +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 + +Both files switch on `System.getenv('RUNNINGONCI')`. Azure DevOps exports the +`RunningOnCI` pipeline variable under this normalized environment-variable name. + +- **`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. `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). + +## When CI fails 401 on a Dependabot bump + +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 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 + +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. + +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. +- 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/dependency-repositories.gradle b/eng/gradle/dependency-repositories.gradle new file mode 100644 index 00000000000..a9daafecc52 --- /dev/null +++ b/eng/gradle/dependency-repositories.gradle @@ -0,0 +1,17 @@ +// 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. + +repositories { + if (System.getenv('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/mirror-dependencies.ps1 b/eng/gradle/mirror-dependencies.ps1 new file mode 100644 index 00000000000..a1c43f1b547 --- /dev/null +++ b/eng/gradle/mirror-dependencies.ps1 @@ -0,0 +1,224 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + 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), + 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/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 + 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 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 + `$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 + +.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(DefaultParameterSetName='Gradle')] +param( + [Parameter(Mandatory=$true, ParameterSetName='Gradle')] + [string] $ProjectDir = '.', + + [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 +$defaultGradleWrapper = if ($IsWindows -or $env:OS -eq 'Windows_NT') { + 'build-tools/gradle/gradlew.bat' +} else { + 'build-tools/gradle/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' + +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|HEAD) '(https://pkgs\.dev\.azure\.com/dnceng/[^']+)'" -AllMatches | + ForEach-Object { $_.Matches } | + ForEach-Object { $_.Groups[1].Value } | + Sort-Object -Unique + if ($urls.Count -eq 0) { return 0 } + $token = Get-AzDevOpsToken + $basicCredential = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$token")) + $headers = @{ Authorization = "Basic $basicCredential" } + $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 +} + +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" } + +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 new file mode 100644 index 00000000000..898391470d1 --- /dev/null +++ b/eng/gradle/plugin-repositories.gradle @@ -0,0 +1,32 @@ +// 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. +// +// 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. +// +// 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 { + if (System.getenv('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/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..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 @@ -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,16 +120,16 @@ 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") { - MetadataValues = "Version=1.3.3;Bind=false", + MetadataValues = $"Version=1.3.3;Bind=false;Repository={TestEnvironment.DotNetPublicMaven}", BinaryContent = () => [], }); 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/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..ce3501521db 100644 --- a/src/manifestmerger/settings.gradle +++ b/src/manifestmerger/settings.gradle @@ -1 +1,9 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} +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..820b2c40e49 100644 --- a/src/proguard-android/settings.gradle +++ b/src/proguard-android/settings.gradle @@ -1,8 +1,9 @@ +// 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 } +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..c4f158d7464 --- /dev/null +++ b/src/r8/settings.gradle @@ -0,0 +1,9 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} +dependencyResolutionManagement { + apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + +rootProject.name = 'r8' \ No newline at end of file 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..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,26 +1,9 @@ // 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 '8.7.0' 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..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 @@ -1 +1,10 @@ +// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle +pluginManagement { + apply from: "${rootDir}/../../../../../eng/gradle/plugin-repositories.gradle", to: pluginManagement +} +dependencyResolutionManagement { + apply from: "${rootDir}/../../../../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement +} + +rootProject.name = 'JavaLib' include ':library' 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",