diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bd864df90d1..14000ed8eb8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,6 +18,17 @@ - **Run tests:** `dotnet-local.cmd test bin/TestDebug/net9.0/Xamarin.Android.Build.Tests.dll --filter Name~TestName` - **Device tests:** `dotnet-local.cmd test bin/TestDebug/MSBuildDeviceIntegration/net9.0/MSBuildDeviceIntegration.dll` +### external/xamarin-android-tools/ + +The in-tree Android tools libraries include SDK/JDK discovery (`AndroidSdkInfo`, `JdkInfo`, SDK manifest parsing, `AdbRunner`, `EmulatorRunner`) and MSBuild task infrastructure (`AndroidTask`, `AndroidToolTask`, `AsyncTask`, `Files`, `ProcessUtils`, `FileUtil`, `MemoryStreamPool`). They target `netstandard2.0` and/or modern .NET, so verify API availability across all target frameworks before using newer BCL APIs. Prefer `ANDROID_HOME` for new Android SDK environment handling; `ANDROID_SDK_ROOT` is deprecated and should only remain for compatibility. + +Useful focused checks: +```sh +dotnet build external/xamarin-android-tools/Xamarin.Android.Tools.sln +dotnet test external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj +dotnet test external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj +``` + ## Critical Rules **Never use `git commit --amend`:** Always create new commits. The user will squash or fixup as needed. diff --git a/.github/skills/android-reviewer/SKILL.md b/.github/skills/android-reviewer/SKILL.md index 9dcb224930d..6ed99c3a413 100644 --- a/.github/skills/android-reviewer/SKILL.md +++ b/.github/skills/android-reviewer/SKILL.md @@ -9,7 +9,7 @@ description: >- # Android PR Reviewer -Review PRs against guidelines distilled from past reviews by senior maintainers of dotnet/android. +Review PRs against guidelines distilled from past reviews by senior maintainers of dotnet/android, including the in-tree shared tooling under `external/xamarin-android-tools/`. ## Review Mindset @@ -70,7 +70,7 @@ Based on the file types identified in step 2, read the appropriate rule files fr **Conditionally load based on changed file types:** - `references/csharp-rules.md` — When any `.cs` files changed. Covers nullable, async, error handling, performance, and code organization. -- `references/msbuild-rules.md` — When `.targets`, `.props`, `.projitems`, or `.csproj` files changed, or when MSBuild task C# files changed (e.g., files under `src/Xamarin.Android.Build.Tasks/`). +- `references/msbuild-rules.md` — When `.targets`, `.props`, `.projitems`, or `.csproj` files changed, or when MSBuild task C# files changed (e.g., files under `src/Xamarin.Android.Build.Tasks/` or `external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/`). - `references/native-rules.md` — When `.c`, `.cpp`, `.h`, or `.hpp` files changed. Covers memory management, C++ best practices, symbol visibility, and platform-specific code. - `references/interop-rules.md` — When both C# and native files changed, when the diff contains P/Invoke or JNI interop code (e.g., `DllImport`, `[Register]` attribute changes, `JNIEnv` calls, `[MarshalAs]`, `[StructLayout]`, `JniObjectReference`, `JniPeerMembers`, `JniTransition`), or when files under `external/Java.Interop/`, `src/Mono.Android/`, or `src/native/` changed. - `references/testing-rules.md` — When test files changed (e.g., files under `tests/`, `**/Tests/`, or test project directories). diff --git a/.github/skills/android-reviewer/references/csharp-rules.md b/.github/skills/android-reviewer/references/csharp-rules.md index 029c54a2b88..6bb4bb76bb9 100644 --- a/.github/skills/android-reviewer/references/csharp-rules.md +++ b/.github/skills/android-reviewer/references/csharp-rules.md @@ -4,6 +4,15 @@ General C# guidance applicable to any .NET repository. --- +## Target Framework Compatibility + +| Check | What to look for | +|-------|-----------------| +| **Oldest TFM must compile** | Code under `external/xamarin-android-tools/` may target `netstandard2.0` and modern .NET. Verify every API and overload against the oldest target framework; common traps include cancellation-token overloads such as `HttpContent.ReadAsStringAsync(CancellationToken)`, modern `ProcessStartInfo.ArgumentList` usage without the existing fallback helpers, and newer language/BCL features that need `#if` guards or polyfills. | +| **Prefer existing compatibility helpers** | Use repository helpers such as `ProcessUtils`, `FileUtil`, and nullable extension methods instead of direct modern-BCL calls when they provide `netstandard2.0` fallbacks or better annotations. | + +--- + ## Nullable Reference Types | Check | What to look for | @@ -53,6 +62,8 @@ General C# guidance applicable to any .NET repository. |-------|-----------------| | **Avoid unnecessary allocations** | Don't create intermediate collections when LINQ chaining or a single list would do. Char arrays for `string.Split()` should be `static readonly` fields. | | **ArrayPool for large buffers** | Buffers ≥ 1 KB should use `ArrayPool.Shared.Rent()` with `try`/`finally` return. Large allocations go to the LOH and are expensive to GC. | +| **Use existing pools for repeated streams/buffers** | In Android tools code, prefer existing pooling helpers such as `MemoryStreamPool`/`ObjectPool` for repeated temporary streams or buffers instead of allocating new instances in hot paths. If mutable buffers are reused with single-caller assumptions, document the thread-safety invariant. | +| **Static `HttpClient`** | `HttpClient` instances should be `static readonly` fields, not per-instance objects that are repeatedly constructed or disposed. Per-instance clients can exhaust sockets; inject clients only when there is a real caller requirement. | | **`HashSet.Add()` already handles duplicates** | Calling `.Contains()` before `.Add()` does the hash lookup twice. Just call `.Add()`. (Postmortem `#41`) | | **Don't wrap a value in an interpolated string** | `$"{someString}"` creates an unnecessary `string.Format` call when `someString` is already a string. (Postmortem `#42`) | | **Consider allocations when choosing types** | `Stopwatch` is heap-allocated; `DateTime`/`ValueStopwatch` is a struct. On hot paths or startup, prefer value types. (Postmortem `#39`) | diff --git a/.github/skills/android-reviewer/references/repo-conventions.md b/.github/skills/android-reviewer/references/repo-conventions.md index 3317d22d9b1..df66a83a8f4 100644 --- a/.github/skills/android-reviewer/references/repo-conventions.md +++ b/.github/skills/android-reviewer/references/repo-conventions.md @@ -40,10 +40,13 @@ and merge conflicts. | Check | What to look for | |-------|-----------------| | **Use existing utilities** | Check `MonoAndroidHelper`, `FileUtil`, `PathUtil`, `ITaskItemExtensions`, and other utilities before writing new helpers. Duplicating existing logic is the most expensive AI pattern. | +| **Use Android tools utilities** | In `external/xamarin-android-tools/`, process execution should go through `ProcessUtils`, file extraction/download/checksum/path helpers through `FileUtil`, and repeated buffers/streams through `ObjectPool` or `MemoryStreamPool` where applicable. | | **`Log.LogDebugMessage` for diagnostics** | Use `Log.LogDebugMessage(…)` for verbose/debug output, not `Console.WriteLine` or `Debug.WriteLine`. Don't spam logcat with messages that fire on every type lookup miss. (Postmortem `#9`) | +| **Android tools logger delegate** | SDK/JDK discovery helpers in `external/xamarin-android-tools/` commonly use `Action? logger` (see `AndroidSdkInfo.DefaultConsoleLogger`) so callers can route diagnostics into MSBuild or IDE logs. Don't replace this with `Console.WriteLine` or `Debug.WriteLine`. | | **Return `IReadOnlyList`** | Public methods should return `IReadOnlyList` or `IReadOnlyCollection` instead of mutable `List`. | | **Prefer C# pattern matching** | Use `is`, `switch` expressions, and property patterns instead of `if`/`else` type-check chains. | | **Structured args, not string interpolation** | Process arguments should be `IEnumerable` or use `ArgumentList`, not a single interpolated string. | +| **Android SDK environment variables** | In `external/xamarin-android-tools/`, use `EnvironmentVariableNames.AndroidHome`/`ANDROID_HOME` for new SDK-root behavior. `ANDROID_SDK_ROOT` is deprecated by Android and should only be read for backward compatibility. | | **Method names must reflect behavior** | If `CreateFoo()` sometimes returns an existing instance, rename it `GetOrCreateFoo()` or `GetFoo()`. (Postmortem `#4`) | | **Choose collision-proof names** | Types and constants that could collide with user code or Android concepts need disambiguating prefixes (e.g., `__Xamarin.Android.Resource.Designer` with a `__` prefix). (Postmortem `#2`) | | **Don't assume transitive assembly references** | An assembly containing an `Activity` subclass does not necessarily reference `Mono.Android.dll` directly — the reference may be transitive. Skipping assemblies based on direct reference checks can break user code. (Postmortem `#64`) | @@ -93,6 +96,7 @@ and merge conflicts. | Check | What to look for | |-------|-----------------| | **XmlReader over LINQ XML** | For forward-only XML parsing (manifests, config files), prefer `XmlReader` — it's streaming and allocation-free. `XElement`/`XDocument` builds a full DOM tree. | +| **SDK manifest parsing stays streaming** | Android SDK repository manifests can be large; keep `external/xamarin-android-tools/` manifest parsing on streaming `XmlReader`-style paths unless there is measured evidence a DOM is acceptable. | | **p/invoke over process spawn** | For single syscalls like `chmod`, use `[DllImport("libc")]` instead of spawning a child process. Process creation is orders of magnitude more expensive. | | **Use `Files.CopyIfStringChanged()`** | Don't write to a file if the content hasn't changed — it breaks incremental builds by updating timestamps. (Postmortem `#53`) | | **Don't remove caches without measurement** | If a cache (like `TypeDefinitionCache`) had a measured perf win, removing it requires proving the replacement provides equivalent caching. (Postmortem `#57`) | diff --git a/.github/skills/android-reviewer/references/security-rules.md b/.github/skills/android-reviewer/references/security-rules.md index f8e255103f1..1c672d71584 100644 --- a/.github/skills/android-reviewer/references/security-rules.md +++ b/.github/skills/android-reviewer/references/security-rules.md @@ -11,6 +11,7 @@ I/O, archives, or process execution. |-------|-----------------| | **Zip Slip protection** | Archive extraction must validate that every entry path, after `Path.GetFullPath()`, resolves under the destination directory. Never use `ZipFile.ExtractToDirectory()` for untrusted archives without entry-by-entry validation. | | **Path traversal** | `StartsWith()` checks on paths must normalize with `Path.GetFullPath()` first. A path like `C:\Program Files\..\Users\evil` bypasses naive prefix checks. Also check for directory boundary issues (`C:\Program FilesX` matching `C:\Program Files`). | +| **Mandatory checksum verification** | Downloads or archive installs in Android tools code must not proceed unverified when checksum/hash data is expected but missing or mismatched. Fail closed with an actionable error. | --- diff --git a/.github/skills/android-reviewer/references/testing-rules.md b/.github/skills/android-reviewer/references/testing-rules.md index 7f93f6f97cc..2f36333a36a 100644 --- a/.github/skills/android-reviewer/references/testing-rules.md +++ b/.github/skills/android-reviewer/references/testing-rules.md @@ -16,6 +16,7 @@ Guidance for test code. The repo-specific conventions (e.g., `BaseTest`, | **Test assertions must be specific** | `Assert.IsNotNull(result)` or `Assert.IsTrue(success)` don't tell you what went wrong. Prefer `Assert.AreEqual(expected, actual)` or NUnit constraints (`Assert.That` with `Does.Contain`, `Is.EqualTo`, etc.) for richer failure messages. | | **Deterministic test data** | Tests should not depend on system locale, timezone, or current date. Use explicit `CultureInfo.InvariantCulture` and hardcoded dates when testing formatting. | | **Test edge cases** | Empty collections, null inputs, boundary values, concurrent calls, and very large inputs should all be considered. If the PR only tests the happy path, suggest edge cases. | +| **Android tools SDK/JDK fixtures** | Tests under `external/xamarin-android-tools/tests/` commonly build isolated fake SDK/JDK layouts and platform-specific tool scripts (`.bat` on Windows, shell scripts on Unix). Keep these fixtures self-contained and cleaned up in setup/teardown rather than depending on the developer machine's installed SDK/JDK. | | **Generator tests must include Invoker types** | Tests for generated binding code (under `external/Java.Interop/tools/generator/` and related test projects) should verify both the interface/class output and the `*Invoker` type behavior. Invoker codegen has historically had subtle bugs with default interface methods and virtual dispatch. | | **JVM-dependent tests** | Tests that require a running JVM must be in projects that configure the JVM environment (e.g., `Java.Interop-Tests`). Verify that test classes requiring a JVM are not placed in unit-test-only projects, where they will silently skip or fail with obscure errors. | | **Expected codegen output tests** | Generator tests that compare against expected output files should be updated when the expected format changes. Stale expected-output files cause spurious test failures that mask real regressions. | diff --git a/.gitmodules b/.gitmodules index ae4a5871b6e..ed056635dec 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,10 +22,6 @@ path = external/robin-map url = https://github.com/xamarin/robin-map branch = master -[submodule "external/xamarin-android-tools"] - path = external/xamarin-android-tools - url = https://github.com/dotnet/android-tools - branch = main [submodule "external/xxHash"] path = external/xxHash url = https://github.com/Cyan4973/xxHash.git diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index 436dc31d184..c7835968275 100644 --- a/build-tools/automation/azure-pipelines-public.yaml +++ b/build-tools/automation/azure-pipelines-public.yaml @@ -429,6 +429,20 @@ stages: - ImageOverride -equals ACES_VM_SharedPool_Tahoe os: macOS +# Android Tools Tests Stage +- template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self + parameters: + windowsPool: + name: $(NetCorePublicPoolName) + demands: + - ImageOverride -equals $(WindowsPoolImageNetCorePublic) + os: windows + macPool: + name: AcesShared + demands: + - ImageOverride -equals ACES_VM_SharedPool_Tahoe + os: macOS + # MAUI Tests Stage - stage: maui_tests displayName: MAUI Tests diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml index d7406c1f7c0..92cfa0b653a 100644 --- a/build-tools/automation/azure-pipelines.yaml +++ b/build-tools/automation/azure-pipelines.yaml @@ -110,6 +110,17 @@ extends: image: $(WindowsPoolImage1ESPT) os: windows + - template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self + parameters: + # The internal Xamarin.Android pipeline uses 1ES Pipeline Templates, which + # require Windows jobs to run on a 1ES-hosted pool. macOS is fine on the + # hosted Azure Pipelines pool as long as `os: macOS` is set (which is now + # the template default). + windowsPool: + name: MAUI-1ESPT + image: $(WindowsPoolImage1ESPT) + os: windows + - stage: maui_tests displayName: MAUI Tests dependsOn: mac_build diff --git a/build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml b/build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml new file mode 100644 index 00000000000..f3bc29bed8c --- /dev/null +++ b/build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml @@ -0,0 +1,135 @@ +# xamarin-android-tools tests stage +# +# Mirrors the build + test jobs from the upstream dotnet/android-tools +# azure-pipelines.yaml so that the Xamarin.Android.Tools build + unit tests +# continue to run as part of dotnet/android CI now that android-tools has been +# merged in-tree under external/xamarin-android-tools/. +# +# The upstream pipeline also packed a NuGet and published build artifacts; +# those are intentionally dropped here (the binaries aren't consumed from this +# stage -- dotnet/android's own build produces the shipping assemblies). Only +# logs + test results are surfaced. +# +# Referenced from both: +# - build-tools/automation/azure-pipelines.yaml (official / 1ES) +# - build-tools/automation/azure-pipelines-public.yaml (public PR validation) + +parameters: +- name: stageName + type: string + default: xamarin_android_tools_tests +- name: dependsOn + type: object + default: [] +- name: condition + type: string + default: succeeded() +- name: windowsPool + type: object + # NOTE: This default is a Microsoft-hosted Azure Pipelines pool, which is NOT + # allowed by 1ES Pipeline Templates in Official mode. Callers that extend the + # 1ES Official template (e.g. build-tools/automation/azure-pipelines.yaml) must + # override this with a 1ES-hosted pool like `MAUI-1ESPT`. + default: + name: Azure Pipelines + vmImage: $(HostedWinImage) + os: windows +- name: macPool + type: object + default: + name: Azure Pipelines + vmImage: $(HostedMacImage) + os: macOS + +stages: +- stage: ${{ parameters.stageName }} + displayName: Android Tools Tests + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + variables: + DotNetCoreVersion: 10.0.x + XatSourceDirectory: $(System.DefaultWorkingDirectory)/external/xamarin-android-tools + # When built in-tree, external/xamarin-android-tools.override.props imports + # dotnet/android's root Directory.Build.props, which pins + # DotNetTargetFrameworkVersion=11.0 and sets AndroidToolsDisableMultiTargeting=true + # (so the product build gets a single netstandard2.0 assembly). Upstream + # dotnet/android-tools CI instead builds standalone: multi-targeting + # netstandard2.0 + net10.0, with the test projects running on net10.0. Its + # ProcessUtils tests assert ProcessStartInfo.ArgumentList, which only exists on + # the net10.0 (NET5_0_OR_GREATER) build -- the netstandard2.0 build takes the + # string-Arguments path. To reproduce upstream's green test configuration we + # re-enable multi-targeting and force net10.0 via these global properties. + XatBuildProperties: -p:AndroidToolsDisableMultiTargeting=false -p:DotNetTargetFrameworkVersion=10.0 + jobs: + + # Check - "Xamarin.Android (Android Tools Tests Windows - .NET)" + - job: xamarin_android_tools_windows + displayName: Windows - .NET + pool: ${{ parameters.windowsPool }} + timeoutInMinutes: 30 + workspace: + clean: all + steps: + - checkout: self + clean: true + + - task: UseDotNet@2 + displayName: Use .NET Core $(DotNetCoreVersion) + inputs: + version: $(DotNetCoreVersion) + + - task: DotNetCoreCLI@2 + displayName: Build Xamarin.Android.Tools.sln + inputs: + command: build + projects: $(XatSourceDirectory)/Xamarin.Android.Tools.sln + arguments: -c Debug $(XatBuildProperties) -bl:$(Build.ArtifactStagingDirectory)/build-windows.binlog + + - task: DotNetCoreCLI@2 + displayName: Run Tests + inputs: + command: test + projects: $(XatSourceDirectory)/bin/TestDebug/**/*-Tests.dll + + - task: PublishBuildArtifacts@1 + displayName: Upload build logs + condition: always() + inputs: + pathToPublish: $(Build.ArtifactStagingDirectory) + artifactName: android-tools-logs-windows + + # Check - "Xamarin.Android (Android Tools Tests Mac - .NET)" + - job: xamarin_android_tools_mac + displayName: Mac - .NET + pool: ${{ parameters.macPool }} + timeoutInMinutes: 30 + workspace: + clean: all + steps: + - checkout: self + clean: true + + - task: UseDotNet@2 + displayName: Use .NET Core $(DotNetCoreVersion) + inputs: + version: $(DotNetCoreVersion) + + - task: DotNetCoreCLI@2 + displayName: Build Xamarin.Android.Tools.sln + inputs: + command: build + projects: $(XatSourceDirectory)/Xamarin.Android.Tools.sln + arguments: -c Debug $(XatBuildProperties) -bl:$(Build.ArtifactStagingDirectory)/build-mac.binlog + + - task: DotNetCoreCLI@2 + displayName: Run Tests + inputs: + command: test + projects: $(XatSourceDirectory)/bin/TestDebug/**/*-Tests.dll + + - task: PublishBuildArtifacts@1 + displayName: Upload build logs + condition: always() + inputs: + pathToPublish: $(Build.ArtifactStagingDirectory) + artifactName: android-tools-logs-mac diff --git a/build-tools/scripts/XAVersionInfo.targets b/build-tools/scripts/XAVersionInfo.targets index 5e2889a3fc7..b1ab9737fa0 100644 --- a/build-tools/scripts/XAVersionInfo.targets +++ b/build-tools/scripts/XAVersionInfo.targets @@ -10,31 +10,11 @@ - - - - - - <_SubmoduleBranchInfo Include="external/xamarin-android-tools"> - _BuildInfo_XamarinAndroidToolsCommit - - - - - - - - - - + + + + + + Debug + + true + 10.0 + net$(DotNetTargetFrameworkVersion) + true + + + + + obj\ + $(BaseIntermediateOutputPath)\$(Configuration) + $(MSBuildThisFileDirectory)bin\Build$(Configuration)\ + $(MSBuildThisFileDirectory)bin\$(Configuration)\ + $(MSBuildThisFileDirectory)bin\Test$(Configuration)\ + + diff --git a/external/xamarin-android-tools/Directory.Build.targets b/external/xamarin-android-tools/Directory.Build.targets new file mode 100644 index 00000000000..71eb37507a4 --- /dev/null +++ b/external/xamarin-android-tools/Directory.Build.targets @@ -0,0 +1,34 @@ + + + + + + false + main + + + + + $(GitSemVerMajor).$(GitSemVerMinor).$(GitSemVerPatch) + $(Version)$(PackageVersionSuffix) + $(Version); git-rev-head:$(GitCommit); git-branch:$(GitBranch) + + + + + + + + + + + + + + + + + diff --git a/external/xamarin-android-tools/GitInfo.txt b/external/xamarin-android-tools/GitInfo.txt new file mode 100644 index 00000000000..9f8e9b69a33 --- /dev/null +++ b/external/xamarin-android-tools/GitInfo.txt @@ -0,0 +1 @@ +1.0 \ No newline at end of file diff --git a/external/xamarin-android-tools/LICENSE b/external/xamarin-android-tools/LICENSE new file mode 100644 index 00000000000..67f20e2e241 --- /dev/null +++ b/external/xamarin-android-tools/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/xamarin-android-tools/Localize/LocProject.json b/external/xamarin-android-tools/Localize/LocProject.json new file mode 100644 index 00000000000..688ef4e4de2 --- /dev/null +++ b/external/xamarin-android-tools/Localize/LocProject.json @@ -0,0 +1,19 @@ +{ + "Projects": [ + { + "LanguageSet": "VS_Main_Languages", + "LocItems": [ + { + "CopyOption": "LangIDOnName", + "SourceFile": ".\\src\\Microsoft.Android.Build.BaseTasks\\Properties\\Resources.resx", + "OutputPath": ".\\src\\Microsoft.Android.Build.BaseTasks\\Properties" + }, + { + "CopyOption": "LangIDOnName", + "SourceFile": ".\\src\\Xamarin.Android.Tools.AndroidSdk\\Properties\\Resources.resx", + "OutputPath": ".\\src\\Xamarin.Android.Tools.AndroidSdk\\Properties" + } + ] + } + ] +} diff --git a/external/xamarin-android-tools/Localize/loc/cs/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/cs/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..28b0748c7c7 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/cs/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/cs/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/cs/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..2009686d61b --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/cs/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/de/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/de/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..a5826587b77 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/de/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/de/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/de/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..b67ca37e0fd --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/de/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/es/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/es/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..0c034721451 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/es/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/es/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/es/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..d6f46f207a7 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/es/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/fr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/fr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..27a2d9f4d92 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/fr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/fr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/fr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..bb99c91de22 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/fr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/it/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/it/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..575b45b2e64 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/it/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/it/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/it/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..79552708bb5 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/it/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ja/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ja/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..25f613fa090 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ja/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ja/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ja/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..36cd2b97ea8 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ja/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ko/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ko/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..66ce2f97708 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ko/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ko/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ko/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..9122014fba2 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ko/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/pl/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/pl/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..6b74fae0f63 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/pl/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/pl/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/pl/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..6f9623d4fc6 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/pl/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/pt-BR/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/pt-BR/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..d1d92b68236 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/pt-BR/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/pt-BR/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/pt-BR/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..450eced92ce --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/pt-BR/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ru/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ru/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..5d08dd83572 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ru/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/ru/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/ru/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..ff13dd21c69 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/ru/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/tr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/tr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..61a1f604f90 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/tr/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/tr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/tr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..623f1e866e8 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/tr/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..9de1389fddf --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..ca010195273 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/zh-Hans/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..c6a55b9d67c --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx.lcl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl b/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl new file mode 100644 index 00000000000..473420a0d31 --- /dev/null +++ b/external/xamarin-android-tools/Localize/loc/zh-Hant/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx.lcl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/Makefile b/external/xamarin-android-tools/Makefile new file mode 100644 index 00000000000..865b03118eb --- /dev/null +++ b/external/xamarin-android-tools/Makefile @@ -0,0 +1,17 @@ +CONFIGURATION := Debug +OS := $(shell uname) +V ?= 0 + +include build-tools/scripts/msbuild.mk + +all: + $(MSBUILD) $(MSBUILD_FLAGS) Xamarin.Android.Tools.sln + +clean: + -$(MSBUILD) $(MSBUILD_FLAGS) /t:Clean Xamarin.Android.Tools.sln + +run-all-tests: + dotnet test -l "console;verbosity=detailed" -l trx \ + tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj + dotnet test -l "console;verbosity=detailed" -l trx \ + tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj diff --git a/external/xamarin-android-tools/NuGet.config b/external/xamarin-android-tools/NuGet.config new file mode 100644 index 00000000000..de2f5d6506e --- /dev/null +++ b/external/xamarin-android-tools/NuGet.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/external/xamarin-android-tools/README.md b/external/xamarin-android-tools/README.md new file mode 100644 index 00000000000..93efde718ba --- /dev/null +++ b/external/xamarin-android-tools/README.md @@ -0,0 +1,86 @@ +# android-tools +[![Build Status](https://dev.azure.com/devdiv/DevDiv/_apis/build/status%2FXamarin%2FAndroid%2Fandroid-tools?branchName=main)](https://dev.azure.com/devdiv/DevDiv/_build/latest?definitionId=22338&branchName=main) + +**android-tools** is a repo to easily share code between the +[xamarin-android][android] repo and the .NET for Android commercial tooling, +such as IDE extensions, without requiring that the IDE extensions +submodule the entire **android** repo, which is gigantic. + +[android]: https://github.com/xamarin/xamarin-android + +# Build Requirements + +**-android-tools** requires .NET 6 or later. + +# Build Configuration + +The default `make all` target accepts the following optional +**make**(1) variables: + + * `$(CONFIGURATION)`: The configuration to build. + Possible values include `Debug` and `Release`. + The default value is `Debug`. + * `$(V)`: Controls build verbosity. When set to a non-zero value, + The build is built with `/v:diag` logging. + +# Build + +To build **android-tools**: + + dotnet build Xamarin.Android.Tools.sln + +Alternatively run `make`: + + make + +# Tests + +To run the unit tests: + + dotnet test tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj -l "console;verbosity=detailed" + +# Build Output Directory Structure + +There are two configurations, `Debug` and `Release`, controlled by the +`$(Configuration)` MSBuild property or the `$(CONFIGURATION)` make variable. + +The `bin\$(Configuration)` directory, e.g. `bin\Debug`, contains +*redistributable* artifacts. The `bin\Test$(Configuration)` directory, +e.g. `bin\TestDebug`, contains unit tests and related files. + +* `bin\$(Configuration)`: redistributable build artifacts. +* `bin\Test$(Configuration)`: Unit tests and related files. + +# Distribution + +Package versioning follows [Semantic Versioning 2.0.0](https://semver.org/). +The major version in the `nuget.version` file should be updated when a breaking change is introduced. +The minor version should be updated when new functionality is added. +The patch version will be automatically determined by the number of commits since the last version change. + +Xamarin.Android.Tools.AndroidSdk nupkg files are produced for every build which occurrs on [Azure Devops](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=22338). +To download one of these packages, navigate to the build you are interested in and click on the `Artifacts` button. + +Alternatively, "unofficial" releases are currently hosted on the [Xamarin.Android](https://dev.azure.com/xamarin/public/_packaging?_a=feed&feed=Xamarin.Android) feed. +Add the feed to your project's `NuGet.config` to reference these packages: + +```xml + + + + + +``` + +# Mailing Lists + +To discuss this project, and participate in the design, we use the +[android-devel@lists.xamarin.com](http://lists.xamarin.com/mailman/listinfo/android-devel) mailing list. + +# Coding Guidelines + +We use [Mono's Coding Guidelines](http://www.mono-project.com/community/contributing/coding-guidelines/). + +# Reporting Bugs + +We use [GitHub](https://github.com/dotnet/android-tools/issues) to track issues. diff --git a/external/xamarin-android-tools/Xamarin.Android.Tools.sln b/external/xamarin-android-tools/Xamarin.Android.Tools.sln new file mode 100644 index 00000000000..d4618ff7b63 --- /dev/null +++ b/external/xamarin-android-tools/Xamarin.Android.Tools.sln @@ -0,0 +1,59 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30709.64 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Tools.AndroidSdk", "src\Xamarin.Android.Tools.AndroidSdk\Xamarin.Android.Tools.AndroidSdk.csproj", "{E34BCFA0-CAA4-412C-AA1C-75DB8D67D157}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Tools.AndroidSdk-Tests", "tests\Xamarin.Android.Tools.AndroidSdk-Tests\Xamarin.Android.Tools.AndroidSdk-Tests.csproj", "{1E5501E8-49C1-4659-838D-CC9720C5208F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Build.BaseTasks", "src\Microsoft.Android.Build.BaseTasks\Microsoft.Android.Build.BaseTasks.csproj", "{C8E51A26-F7A5-4B81-B0BC-F9A4BFB43D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Android.Build.BaseTasks-Tests", "tests\Microsoft.Android.Build.BaseTasks-Tests\Microsoft.Android.Build.BaseTasks-Tests.csproj", "{4F5FD01C-B5A6-4F9F-91CE-3E78B3F942AC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{19FE1B44-DB71-4F97-A07E-085888690DAE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ls-jdks", "tools\ls-jdks\ls-jdks.csproj", "{3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Android.Tools.Benchmarks", "tests\Xamarin.Android.Tools.Benchmarks\Xamarin.Android.Tools.Benchmarks.csproj", "{F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E34BCFA0-CAA4-412C-AA1C-75DB8D67D157}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E34BCFA0-CAA4-412C-AA1C-75DB8D67D157}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E34BCFA0-CAA4-412C-AA1C-75DB8D67D157}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E34BCFA0-CAA4-412C-AA1C-75DB8D67D157}.Release|Any CPU.Build.0 = Release|Any CPU + {1E5501E8-49C1-4659-838D-CC9720C5208F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E5501E8-49C1-4659-838D-CC9720C5208F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E5501E8-49C1-4659-838D-CC9720C5208F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E5501E8-49C1-4659-838D-CC9720C5208F}.Release|Any CPU.Build.0 = Release|Any CPU + {C8E51A26-F7A5-4B81-B0BC-F9A4BFB43D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8E51A26-F7A5-4B81-B0BC-F9A4BFB43D38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8E51A26-F7A5-4B81-B0BC-F9A4BFB43D38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8E51A26-F7A5-4B81-B0BC-F9A4BFB43D38}.Release|Any CPU.Build.0 = Release|Any CPU + {4F5FD01C-B5A6-4F9F-91CE-3E78B3F942AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F5FD01C-B5A6-4F9F-91CE-3E78B3F942AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F5FD01C-B5A6-4F9F-91CE-3E78B3F942AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F5FD01C-B5A6-4F9F-91CE-3E78B3F942AC}.Release|Any CPU.Build.0 = Release|Any CPU + {3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D}.Release|Any CPU.Build.0 = Release|Any CPU + {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA1CD771-F00B-4DE8-93EE-7690D81F6A5A} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3D2ADF77-62C7-4E0E-AB29-BDD266B7D56D} = {19FE1B44-DB71-4F97-A07E-085888690DAE} + EndGlobalSection +EndGlobal diff --git a/external/xamarin-android-tools/build-tools/scripts/msbuild.mk b/external/xamarin-android-tools/build-tools/scripts/msbuild.mk new file mode 100644 index 00000000000..6201e263b88 --- /dev/null +++ b/external/xamarin-android-tools/build-tools/scripts/msbuild.mk @@ -0,0 +1,50 @@ +# +# MSBuild Abstraction. +# +# Makefile targets which need to invoke MSBuild should use `$(MSBUILD)`, +# not some specific MSBuild program such as `xbuild` or `msbuild`. +# +# Typical use will also include `$(MSBUILD_FLAGS)`, which provides the +# Configuration and logging verbosity, as per $(CONFIGURATION) and $(V): +# +# $(MSBUILD) $(MSBUILD_FLAGS) path/to/Project.csproj +# +# Inputs: +# +# $(CONFIGURATION): Build configuration name, e.g. Debug or Release +# $(MSBUILD): The MSBuild program to use. +# $(MSBUILD_ARGS): Extra arguments to pass to $(MSBUILD); embedded into $(MSBUILD_FLAGS) +# $(OS): Operating system; used to determine `pkg-config` location +# $(V): Build verbosity +# +# Outputs: +# +# $(MSBUILD): The MSBuild program to use. Defaults to `xbuild` unless overridden. +# $(MSBUILD_FLAGS): Additional MSBuild flags; contains $(CONFIGURATION), $(V), $(MSBUILD_ARGS). + +MSBUILD = dotnet build +MSBUILD_FLAGS = /p:Configuration=$(CONFIGURATION) $(MSBUILD_ARGS) + +ifeq ($(OS),Darwin) +_PKG_CONFIG = /Library/Frameworks/Mono.framework/Commands/pkg-config +else # $(OS) != Darwin +_PKG_CONFIG = pkg-config +endif # $(OS) == Darwin + +ifneq ($(V),0) +MSBUILD_FLAGS += /v:diag +endif # $(V) != 0 + +ifeq ($(MSBUILD),dotnet build) +USE_MSBUILD = 1 +endif # $(MSBUILD) == msbuild + +ifeq ($(USE_MSBUILD),1) +else # $(MSBUILD) != 1 +_CSC_EMITS_PDB := $(shell if $(_PKG_CONFIG) --atleast-version=4.9 mono ; then echo Pdb; fi ) +ifeq ($(_CSC_EMITS_PDB),Pdb) +MSBUILD_FLAGS += /p:_DebugFileExt=.pdb +else # $(_CSC_EMITS_PDB) == '' +MSBUILD_FLAGS += /p:_DebugFileExt=.mdb +endif # $(_CSC_EMITS_PDB) == Pdb +endif # $(USE_MSBUILD) == 1 diff --git a/external/xamarin-android-tools/global.json b/external/xamarin-android-tools/global.json new file mode 100644 index 00000000000..03e7d36e420 --- /dev/null +++ b/external/xamarin-android-tools/global.json @@ -0,0 +1,5 @@ +{ + "msbuild-sdks": { + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.26072.1" + } +} \ No newline at end of file diff --git a/external/xamarin-android-tools/nuget.version b/external/xamarin-android-tools/nuget.version new file mode 100644 index 00000000000..b123147e2a1 --- /dev/null +++ b/external/xamarin-android-tools/nuget.version @@ -0,0 +1 @@ +1.1 \ No newline at end of file diff --git a/external/xamarin-android-tools/product.snk b/external/xamarin-android-tools/product.snk new file mode 100644 index 00000000000..8c04e53be9d Binary files /dev/null and b/external/xamarin-android-tools/product.snk differ diff --git a/external/xamarin-android-tools/scripts/benchmarks.ps1 b/external/xamarin-android-tools/scripts/benchmarks.ps1 new file mode 100644 index 00000000000..9c413e77f9c --- /dev/null +++ b/external/xamarin-android-tools/scripts/benchmarks.ps1 @@ -0,0 +1,37 @@ +<# +.SYNOPSIS + Runs the Files hash benchmarks and produces a markdown report. + +.DESCRIPTION + Builds and runs Xamarin.Android.Tools.Benchmarks in Release mode, + then copies the GitHub-flavored markdown results to + tests/Xamarin.Android.Tools.Benchmarks/README.md. +#> + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot +$projectDir = Join-Path (Join-Path $repoRoot 'tests') 'Xamarin.Android.Tools.Benchmarks' +$csproj = Join-Path $projectDir 'Xamarin.Android.Tools.Benchmarks.csproj' +$artifactsDir = Join-Path $projectDir 'BenchmarkDotNet.Artifacts' +$readme = Join-Path $projectDir 'README.md' + +Write-Host "Building benchmarks in Release..." +dotnet build $csproj -c Release --nologo -v quiet +if ($LASTEXITCODE -ne 0) { throw "Build failed." } + +Write-Host "Running benchmarks..." +dotnet run --project $csproj -c Release --no-build -- --filter '*' --exporters github --artifacts $artifactsDir +if ($LASTEXITCODE -ne 0) { throw "Benchmarks failed." } + +# Find the generated markdown report +$mdFile = Get-ChildItem -Path (Join-Path $artifactsDir 'results') -Filter '*-report-github.md' | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -First 1 +if (-not $mdFile) { throw "No markdown report found in $artifactsDir\results" } + +Copy-Item $mdFile.FullName $readme -Force +Write-Host "Results written to: $readme" + +# Clean up artifacts +Remove-Item $artifactsDir -Recurse -Force +Write-Host "Cleaned up BenchmarkDotNet.Artifacts." diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRidAbiHelper.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRidAbiHelper.cs new file mode 100644 index 00000000000..009181afffd --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRidAbiHelper.cs @@ -0,0 +1,111 @@ +// https://github.com/xamarin/xamarin-android/blob/34acbbae6795854cc4e9f8eb7167ab011e0266b4/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs#L251 + +using System; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Build.Tasks +{ + public static class AndroidRidAbiHelper + { + static readonly string[] ValidAbis = new[]{ + "arm64-v8a", + "armeabi-v7a", + "x86", + "x86_64", + }; + + public static string? GetNativeLibraryAbi (string lib) + { + if (string.IsNullOrEmpty (lib)) + return null; + lib = lib.Replace ('\\', Path.DirectorySeparatorChar); + + // The topmost directory the .so file is contained within + var dir = Directory.GetParent (lib); + var dirName = dir.Name.ToLowerInvariant (); + if (dirName.StartsWith ("interpreter-", StringComparison.Ordinal)) { + dirName = dirName.Substring ("interpreter-".Length); + } + if (ValidAbis.Contains (dirName)) { + return dirName; + } + + // Look for a directory with a RID as a name, such as: + // android-arm64/libfoo.so + var abi = RuntimeIdentifierToAbi (dirName); + if (!string.IsNullOrEmpty (abi)) + return abi; + + // Try one directory higher, such as: + // packages/sqlitepclraw.lib.e_sqlite3.android/1.1.11/runtimes/android-arm64/native/libe_sqlite3.so + abi = RuntimeIdentifierToAbi (dir.Parent.Name.ToLowerInvariant ()); + if (!string.IsNullOrEmpty (abi)) + return abi; + + return null; + } + + public static string? GetNativeLibraryAbi (ITaskItem lib) + { + // If Abi is explicitly specified, simply return it. + var lib_abi = lib.GetMetadata ("Abi"); + + if (!string.IsNullOrWhiteSpace (lib_abi)) + return lib_abi; + + // Try to figure out what type of abi this is from the path + // First, try nominal "Link" path. + var link = lib.GetMetadata ("Link"); + if (!string.IsNullOrWhiteSpace (link)) { + lib_abi = GetNativeLibraryAbi (link); + } + + // Check for a RuntimeIdentifier + var rid = lib.GetMetadata ("RuntimeIdentifier"); + if (!string.IsNullOrWhiteSpace (rid)) { + lib_abi = RuntimeIdentifierToAbi (rid); + } + + if (!string.IsNullOrWhiteSpace (lib_abi)) + return lib_abi; + + // If not resolved, use ItemSpec + return GetNativeLibraryAbi (lib.ItemSpec); + } + + /// + /// Converts .NET 5 RIDs to Android ABIs or an empty string if no match. + /// + /// Known RIDs: + /// "android.21-arm64" -> "arm64-v8a" + /// "android.21-arm" -> "armeabi-v7a" + /// "android.21-x86" -> "x86" + /// "android.21-x64" -> "x86_64" + /// "android-arm64" -> "arm64-v8a" + /// "android-arm" -> "armeabi-v7a" + /// "android-x86" -> "x86" + /// "android-x64" -> "x86_64" + /// + public static string RuntimeIdentifierToAbi (string runtimeIdentifier) + { + if (string.IsNullOrEmpty (runtimeIdentifier) || !runtimeIdentifier.StartsWith ("android", StringComparison.OrdinalIgnoreCase)) { + return ""; + } + if (runtimeIdentifier.EndsWith ("-arm64", StringComparison.OrdinalIgnoreCase)) { + return "arm64-v8a"; + } + if (runtimeIdentifier.EndsWith ("-arm", StringComparison.OrdinalIgnoreCase)) { + return "armeabi-v7a"; + } + if (runtimeIdentifier.EndsWith ("-x86", StringComparison.OrdinalIgnoreCase)) { + return "x86"; + } + if (runtimeIdentifier.EndsWith ("-x64", StringComparison.OrdinalIgnoreCase)) { + return "x86_64"; + } + return ""; + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRunToolTask.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRunToolTask.cs new file mode 100644 index 00000000000..64fd1ac5ffc --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidRunToolTask.cs @@ -0,0 +1,76 @@ +// https://github.com/xamarin/xamarin-android/blob/0134c2fb20f2f20127b24ef49177d9fe8226efdb/src/Xamarin.Android.Build.Tasks/Tasks/AndroidToolTask.cs#L9 + +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Build.Tasks +{ + public abstract class AndroidRunToolTask : AndroidToolTask + { + protected static bool IsWindows = Path.DirectorySeparatorChar == '\\'; + + protected abstract string DefaultErrorCode { get; } + + protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance) + { + base.LogEventsFromTextOutput (singleLine, messageImportance); + + if (messageImportance != StandardErrorLoggingImportance) + return; + + Log.LogFromStandardError (DefaultErrorCode, singleLine); + } + + protected virtual Regex ErrorRegex { + get { return AndroidErrorRegex; } + } + + /* This gets pre-pended to any filenames that we get from error strings */ + protected string? BaseDirectory { get; set; } + + // Aapt errors looks like this: + // res\layout\main.axml:7: error: No resource identifier found for attribute 'id2' in package 'android' (TaskId:22) + // Resources/values/theme.xml(2): error APT0000: Error retrieving parent for item: No resource found that matches the given name '@android:style/Theme.AppCompat'. + // Resources/values/theme.xml:2: error APT0000: Error retrieving parent for item: No resource found that matches the given name '@android:style/Theme.AppCompat'. + // res/drawable/foo-bar.jpg: Invalid file name: must contain only [a-z0-9_.] + // Warnings can be like this + // aapt2 W 09-17 18:15:27 98796 12879433 ApkAssets.cpp:138] resources.arsc in APK 'android.jar' is compressed. + // Look for them and convert them to MSBuild compatible errors. + static Regex? androidErrorRegex; + public static Regex AndroidErrorRegex { + get { + if (androidErrorRegex == null) + androidErrorRegex = new Regex (@" +^ +( # start optional path followed by `:` + (? + (?.+[\\/][^:\(]+) + ( + ([:](?[\d ]+)) + | + (\((?[\d ]+)\)) + )? + ) + \s* + : +)? +( # optional warning|error|aapt2\sW|aapt2.exe\sW: + \s* + (?(warning|error|aapt2\sW|aapt2.exe\sW)[^:]*)\s* + : +)? +\s* +(?.*) +$ +", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); + return androidErrorRegex; + } + } + + protected static string QuoteString (string value) + { + return string.Format ("\"{0}\"", value); + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs new file mode 100644 index 00000000000..9acb7b8b056 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs @@ -0,0 +1,36 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L10 + +using System; +using System.IO; +using Microsoft.Build.Utilities; + +namespace Microsoft.Android.Build.Tasks +{ + // We use this task to ensure that no unhandled exceptions + // escape our tasks which would cause an MSB4018 + public abstract class AndroidTask : Task + { + public abstract string TaskPrefix { get; } + + protected string WorkingDirectory { get; private set; } + + public AndroidTask () + { + WorkingDirectory = Directory.GetCurrentDirectory(); + } + + public override bool Execute () + { + try { + return RunTask (); + } catch (Exception ex) { + Log.LogUnhandledException (TaskPrefix, ex); + return false; + } + } + + public abstract bool RunTask (); + + protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs new file mode 100644 index 00000000000..5e9cfe831e4 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs @@ -0,0 +1,51 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L75 + +using System; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Android.Build.Tasks +{ + public abstract class AndroidToolTask : ToolTask + { + public abstract string TaskPrefix { get; } + + protected string WorkingDirectory { get; private set; } + + StringBuilder toolOutput = new StringBuilder (); + + public AndroidToolTask () + { + WorkingDirectory = Directory.GetCurrentDirectory(); + } + + public override bool Execute () + { + try { + bool taskResult = RunTask (); + if (!taskResult && !string.IsNullOrEmpty (toolOutput.ToString ())) { + Log.LogUnhandledToolError (TaskPrefix, toolOutput.ToString ().Trim ()); + } + toolOutput.Clear (); + return taskResult; + } catch (Exception ex) { + Log.LogUnhandledException (TaskPrefix, ex); + return false; + } + } + + protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance) + { + base.LogEventsFromTextOutput (singleLine, messageImportance); + toolOutput.AppendLine (singleLine); + } + + // Most ToolTask's do not override Execute and + // just expect the base to be called + public virtual bool RunTask () => base.Execute (); + + protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs new file mode 100644 index 00000000000..5ff29a9275c --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs @@ -0,0 +1,437 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L27 +// https://github.com/xamarin/Xamarin.Build.AsyncTask/blob/db4ce14dacfef47435c238b1b681c124e60ea1a0/Xamarin.Build.AsyncTask/AsyncTask.cs + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using System.Threading; +using static System.Threading.Tasks.TaskExtensions; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// Base class for tasks that need long-running cancellable asynchronous tasks + /// that don't block the UI thread in the IDE. + /// + public abstract class AsyncTask : Task, ICancelableTask + { + public abstract string TaskPrefix { get; } + + readonly CancellationTokenSource cts = new CancellationTokenSource (); + readonly Queue logMessageQueue = new Queue (); + readonly Queue warningMessageQueue = new Queue (); + readonly Queue errorMessageQueue = new Queue (); + readonly Queue customMessageQueue = new Queue (); + readonly Queue telemetryMessageQueue = new Queue (); + readonly ManualResetEvent logDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent errorDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent warningDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent customDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent telemetryDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent taskCancelled = new ManualResetEvent (false); + readonly ManualResetEvent completed = new ManualResetEvent (false); + bool isRunning = true; + object eventlock = new object (); + int uiThreadId = 0; + + /// + /// Indicates if the task will yield the node during tool execution. + /// + public bool YieldDuringToolExecution { get; set; } + + /// + /// The cancellation token to notify the cancellation requests + /// + public CancellationToken CancellationToken => cts.Token; + + /// + /// Gets the current working directory for the task, which is captured at task + /// creation time from . + /// + protected string WorkingDirectory { get; private set; } + + [Obsolete ("Do not use the Log.LogXXXX from within your Async task as it will Lock the Visual Studio UI. Use the this.LogXXXX methods instead.")] + private new TaskLoggingHelper Log => base.Log; + + /// + /// Initializes the task. + /// + protected AsyncTask () + { + YieldDuringToolExecution = false; + WorkingDirectory = Directory.GetCurrentDirectory (); + uiThreadId = Thread.CurrentThread.ManagedThreadId; + } + + public void Cancel () + => taskCancelled.Set (); + + protected void Complete (System.Threading.Tasks.Task task) + { + if (task.Exception != null) { + var ex = task.Exception.GetBaseException (); + this.LogUnhandledException (TaskPrefix, ex); + } + Complete (); + } + + public void Complete () + => completed.Set (); + + public void LogDebugTaskItems (string message, string [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item); + } + + public void LogDebugTaskItems (string message, ITaskItem [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item.ItemSpec); + } + + public void LogTelemetry (string eventName, IDictionary properties) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogTelemetry (eventName, properties); + return; +#pragma warning restore 618 + } + + var data = new TelemetryEventArgs () { + EventName = eventName, + Properties = properties + }; + EnqueueMessage (telemetryMessageQueue, data, telemetryDataAvailable); + } + + public void LogMessage (string message) + => LogMessage (message, importance: MessageImportance.Normal); + + public void LogMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs)); + + public void LogDebugMessage (string message) + => LogMessage (message, importance: MessageImportance.Low); + + public void LogDebugMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs), importance: MessageImportance.Low); + + public void LogMessage (string message, MessageImportance importance = MessageImportance.Normal) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogMessage (importance, message); + return; +#pragma warning restore 618 + } + + var data = new BuildMessageEventArgs ( + message: message, + helpKeyword: null, + senderName: null, + importance: importance + ); + EnqueueMessage (logMessageQueue, data, logDataAvailable); + } + + public void LogError (string message) + => LogCodedError (code: null, message: message, file: null, lineNumber: 0); + + public void LogError (string message, params object [] messageArgs) + => LogCodedError (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedError (string? code, string message) + => LogCodedError (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedError (string? code, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedError (string? code, string? file, int lineNumber, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedError (string? code, string message, string? file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + + var data = new BuildErrorEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (errorMessageQueue, data, errorDataAvailable); + } + + public void LogWarning (string message) + => LogCodedWarning (code: null, message: message, file: null, lineNumber: 0); + + public void LogWarning (string message, params object [] messageArgs) + => LogCodedWarning (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedWarning (string? code, string message) + => LogCodedWarning (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedWarning (string? code, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedWarning (string? code, string? file, int lineNumber, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedWarning (string? code, string message, string? file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogWarning ( + subcategory: null, + warningCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + var data = new BuildWarningEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (warningMessageQueue, data, warningDataAvailable); + } + + public void LogCustomBuildEvent (CustomBuildEventArgs e) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { + BuildEngine.LogCustomEvent (e); + return; + } + EnqueueMessage (customMessageQueue, e, customDataAvailable); + } + + bool ExecuteWaitForCompletion () + { + WaitForCompletion (); +#pragma warning disable 618 + return !Log.HasLoggedErrors; +#pragma warning restore 618 + } + + void EnqueueMessage (Queue queue, object item, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + queue.Enqueue (item); + lock (eventlock) { + if (isRunning) + resetEvent.Set (); + } + } + } + + void LogInternal (Queue queue, Action action, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + while (queue.Count > 0) { + var args = (T) queue.Dequeue (); + action (args); + } + resetEvent.Reset (); + } + } + + protected void Yield () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Yield (); + } + + protected void Reacquire () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Reacquire (); + } + + protected void WaitForCompletion () + { + WaitHandle [] handles = new WaitHandle [] { + logDataAvailable, + errorDataAvailable, + warningDataAvailable, + customDataAvailable, + telemetryDataAvailable, + taskCancelled, + completed, + }; + try { + while (isRunning) { + var index = (WaitHandleIndex) System.Threading.WaitHandle.WaitAny (handles, TimeSpan.FromMilliseconds (10)); + switch (index) { + case WaitHandleIndex.LogDataAvailable: + LogInternal (logMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogMessage (e.Importance, e.Message); +#pragma warning restore 618 + }, logDataAvailable); + break; + case WaitHandleIndex.ErrorDataAvailable: + LogInternal (errorMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, errorDataAvailable); + break; + case WaitHandleIndex.WarningDataAvailable: + LogInternal (warningMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogWarning (subcategory: null, + warningCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, warningDataAvailable); + break; + case WaitHandleIndex.CustomDataAvailable: + LogInternal (customMessageQueue, (e) => { + BuildEngine.LogCustomEvent (e); + }, customDataAvailable); + break; + case WaitHandleIndex.TelemetryDataAvailable: + LogInternal (telemetryMessageQueue, (e) => { + BuildEngine5.LogTelemetry (e.EventName, e.Properties); + }, telemetryDataAvailable); + break; + case WaitHandleIndex.TaskCancelled: + Cancel (); + cts.Cancel (); + isRunning = false; + break; + case WaitHandleIndex.Completed: + isRunning = false; + break; + } + } + + } finally { + + } + } + + public override bool Execute () + { + try { + return RunTask (); + } catch (Exception ex) { + this.LogUnhandledException (TaskPrefix, ex); + return false; + } + } + + /// + /// Typically `RunTaskAsync` will be the preferred method to override, + /// however this method can be overridden instead for Tasks that will + /// run quickly and do not need to be asynchronous. + /// + public virtual bool RunTask () + { + Yield (); + try { + this.RunTask (() => RunTaskAsync ()) + .Unwrap () + .ContinueWith (Complete); + + // This blocks on Execute, until Complete is called + return ExecuteWaitForCompletion (); + } finally { + Reacquire (); + } + } + + /// + /// Override this method for simplicity of AsyncTask usage: + /// + /// + /// Yield / Reacquire is handled for you + /// + /// + /// RunTaskAsync is already on a background thread + /// + /// + /// + public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask; + + protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); + + private enum WaitHandleIndex + { + LogDataAvailable, + ErrorDataAvailable, + WarningDataAvailable, + CustomDataAvailable, + TelemetryDataAvailable, + TaskCancelled, + Completed, + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs new file mode 100644 index 00000000000..6b643ee7d61 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs @@ -0,0 +1,152 @@ +// https://github.com/xamarin/xamarin-android/blob/83854738b8e01747f9536f426fe17ad784cc2081/src/Xamarin.Android.Build.Tasks/Utilities/AsyncTaskExtensions.cs + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Android.Build.Tasks +{ + public static class AsyncTaskExtensions + { + /// + /// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll + /// + public static Task WhenAll (this AsyncTask asyncTask, IEnumerable source, Action body) => + asyncTask.WhenAll (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll + /// + public static Task WhenAll(this AsyncTask asyncTask, IEnumerable source, Action body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) + { + var scheduler = GetTaskScheduler (maxConcurrencyLevel); + var tasks = new List (); + foreach (var s in source) { + tasks.Add (Task.Factory.StartNew (() => { + try { + body (s); + } catch (Exception exc) { + LogErrorAndCancel (asyncTask, exc); + } + }, asyncTask.CancellationToken, creationOptions, scheduler)); + } + return Task.WhenAll (tasks); + } + + /// + /// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll + /// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject) + /// + public static Task WhenAllWithLock (this AsyncTask asyncTask, IEnumerable source, Action body) => + asyncTask.WhenAllWithLock (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll + /// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject) + /// + public static Task WhenAllWithLock (this AsyncTask asyncTask, IEnumerable source, Action body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) + { + var scheduler = GetTaskScheduler (maxConcurrencyLevel); + var lockObject = new object (); + var tasks = new List (); + foreach (var s in source) { + tasks.Add (Task.Factory.StartNew (() => { + try { + body (s, lockObject); + } catch (Exception exc) { + LogErrorAndCancel (asyncTask, exc); + } + }, asyncTask.CancellationToken, creationOptions, scheduler)); + } + return Task.WhenAll (tasks); + } + + /// + /// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling. + /// + public static ParallelLoopResult ParallelForEach (this AsyncTask asyncTask, IEnumerable source, Action body) => + asyncTask.ParallelForEach (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling. + /// + public static ParallelLoopResult ParallelForEach (this AsyncTask asyncTask, IEnumerable source, Action body, int maxConcurrencyLevel) + { + var options = ParallelOptions (asyncTask, maxConcurrencyLevel); + return Parallel.ForEach (source, options, s => { + try { + body (s); + } catch (Exception exc) { + LogErrorAndCancel (asyncTask, exc); + } + }); + } + + /// + /// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling. + /// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject) + /// + public static ParallelLoopResult ParallelForEachWithLock (this AsyncTask asyncTask, IEnumerable source, Action body) => + asyncTask.ParallelForEachWithLock (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling. + /// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject) + /// + public static ParallelLoopResult ParallelForEachWithLock (this AsyncTask asyncTask, IEnumerable source, Action body, int maxConcurrencyLevel) + { + var options = ParallelOptions (asyncTask, maxConcurrencyLevel); + var lockObject = new object (); + return Parallel.ForEach (source, options, s => { + try { + body (s, lockObject); + } catch (Exception exc) { + LogErrorAndCancel (asyncTask, exc); + } + }); + } + + static ParallelOptions ParallelOptions (AsyncTask asyncTask, int maxConcurrencyLevel) => new ParallelOptions { + CancellationToken = asyncTask.CancellationToken, + TaskScheduler = GetTaskScheduler (maxConcurrencyLevel), + }; + + static TaskScheduler GetTaskScheduler (int maxConcurrencyLevel) + { + var pair = new ConcurrentExclusiveSchedulerPair (TaskScheduler.Default, maxConcurrencyLevel); + return pair.ConcurrentScheduler; + } + + static int DefaultMaxConcurrencyLevel => Math.Max (1, Environment.ProcessorCount - 1); + + static void LogErrorAndCancel (AsyncTask asyncTask, Exception exc) + { + asyncTask.LogCodedError ("XA0000", Properties.Resources.XA0000_Exception, exc); + asyncTask.Cancel (); + } + + /// + /// Calls Task.Factory.StartNew() with a proper CancellationToken, TaskScheduler, and TaskCreationOptions.LongRunning. + /// + public static Task RunTask (this AsyncTask asyncTask, Action body) => + asyncTask.RunTask (body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Calls Task.Factory.StartNew() with a proper CancellationToken + /// + public static Task RunTask (this AsyncTask asyncTask, Action body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) => + Task.Factory.StartNew (body, asyncTask.CancellationToken, creationOptions, GetTaskScheduler (maxConcurrencyLevel)); + + /// + /// Calls Task.Factory.StartNew() with a proper CancellationToken, TaskScheduler, and TaskCreationOptions.LongRunning. + /// + public static Task RunTask (this AsyncTask asyncTask, Func body) => + asyncTask.RunTask (body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel); + + /// + /// Calls Task.Factory.StartNew() with a proper CancellationToken. + /// + public static Task RunTask (this AsyncTask asyncTask, Func body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) => + Task.Factory.StartNew (body, asyncTask.CancellationToken, creationOptions, GetTaskScheduler (maxConcurrencyLevel)); + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.Table.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.Table.cs new file mode 100644 index 00000000000..20f94499bbf --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.Table.cs @@ -0,0 +1,538 @@ +// https://github.com/xamarin/java.interop/blob/7d197f17a0f9d73854522d6e1d68deafbcdbcaf6/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/Crc64.Table.cs + +namespace Microsoft.Android.Build.Tasks +{ + partial class Crc64Helper + { + static readonly ulong[,] table = { + { + 0x0000000000000000, 0x7ad870c830358979, 0xf5b0e190606b12f2, 0x8f689158505e9b8b, + 0xc038e5739841b68f, 0xbae095bba8743ff6, 0x358804e3f82aa47d, 0x4f50742bc81f2d04, + 0xab28ecb46814fe75, 0xd1f09c7c5821770c, 0x5e980d24087fec87, 0x24407dec384a65fe, + 0x6b1009c7f05548fa, 0x11c8790fc060c183, 0x9ea0e857903e5a08, 0xe478989fa00bd371, + 0x7d08ff3b88be6f81, 0x07d08ff3b88be6f8, 0x88b81eabe8d57d73, 0xf2606e63d8e0f40a, + 0xbd301a4810ffd90e, 0xc7e86a8020ca5077, 0x4880fbd87094cbfc, 0x32588b1040a14285, + 0xd620138fe0aa91f4, 0xacf86347d09f188d, 0x2390f21f80c18306, 0x594882d7b0f40a7f, + 0x1618f6fc78eb277b, 0x6cc0863448deae02, 0xe3a8176c18803589, 0x997067a428b5bcf0, + 0xfa11fe77117cdf02, 0x80c98ebf2149567b, 0x0fa11fe77117cdf0, 0x75796f2f41224489, + 0x3a291b04893d698d, 0x40f16bccb908e0f4, 0xcf99fa94e9567b7f, 0xb5418a5cd963f206, + 0x513912c379682177, 0x2be1620b495da80e, 0xa489f35319033385, 0xde51839b2936bafc, + 0x9101f7b0e12997f8, 0xebd98778d11c1e81, 0x64b116208142850a, 0x1e6966e8b1770c73, + 0x8719014c99c2b083, 0xfdc17184a9f739fa, 0x72a9e0dcf9a9a271, 0x08719014c99c2b08, + 0x4721e43f0183060c, 0x3df994f731b68f75, 0xb29105af61e814fe, 0xc849756751dd9d87, + 0x2c31edf8f1d64ef6, 0x56e99d30c1e3c78f, 0xd9810c6891bd5c04, 0xa3597ca0a188d57d, + 0xec09088b6997f879, 0x96d1784359a27100, 0x19b9e91b09fcea8b, 0x636199d339c963f2, + 0xdf7adabd7a6e2d6f, 0xa5a2aa754a5ba416, 0x2aca3b2d1a053f9d, 0x50124be52a30b6e4, + 0x1f423fcee22f9be0, 0x659a4f06d21a1299, 0xeaf2de5e82448912, 0x902aae96b271006b, + 0x74523609127ad31a, 0x0e8a46c1224f5a63, 0x81e2d7997211c1e8, 0xfb3aa75142244891, + 0xb46ad37a8a3b6595, 0xceb2a3b2ba0eecec, 0x41da32eaea507767, 0x3b024222da65fe1e, + 0xa2722586f2d042ee, 0xd8aa554ec2e5cb97, 0x57c2c41692bb501c, 0x2d1ab4dea28ed965, + 0x624ac0f56a91f461, 0x1892b03d5aa47d18, 0x97fa21650afae693, 0xed2251ad3acf6fea, + 0x095ac9329ac4bc9b, 0x7382b9faaaf135e2, 0xfcea28a2faafae69, 0x8632586aca9a2710, + 0xc9622c4102850a14, 0xb3ba5c8932b0836d, 0x3cd2cdd162ee18e6, 0x460abd1952db919f, + 0x256b24ca6b12f26d, 0x5fb354025b277b14, 0xd0dbc55a0b79e09f, 0xaa03b5923b4c69e6, + 0xe553c1b9f35344e2, 0x9f8bb171c366cd9b, 0x10e3202993385610, 0x6a3b50e1a30ddf69, + 0x8e43c87e03060c18, 0xf49bb8b633338561, 0x7bf329ee636d1eea, 0x012b592653589793, + 0x4e7b2d0d9b47ba97, 0x34a35dc5ab7233ee, 0xbbcbcc9dfb2ca865, 0xc113bc55cb19211c, + 0x5863dbf1e3ac9dec, 0x22bbab39d3991495, 0xadd33a6183c78f1e, 0xd70b4aa9b3f20667, + 0x985b3e827bed2b63, 0xe2834e4a4bd8a21a, 0x6debdf121b863991, 0x1733afda2bb3b0e8, + 0xf34b37458bb86399, 0x8993478dbb8deae0, 0x06fbd6d5ebd3716b, 0x7c23a61ddbe6f812, + 0x3373d23613f9d516, 0x49aba2fe23cc5c6f, 0xc6c333a67392c7e4, 0xbc1b436e43a74e9d, + 0x95ac9329ac4bc9b5, 0xef74e3e19c7e40cc, 0x601c72b9cc20db47, 0x1ac40271fc15523e, + 0x5594765a340a7f3a, 0x2f4c0692043ff643, 0xa02497ca54616dc8, 0xdafce7026454e4b1, + 0x3e847f9dc45f37c0, 0x445c0f55f46abeb9, 0xcb349e0da4342532, 0xb1eceec59401ac4b, + 0xfebc9aee5c1e814f, 0x8464ea266c2b0836, 0x0b0c7b7e3c7593bd, 0x71d40bb60c401ac4, + 0xe8a46c1224f5a634, 0x927c1cda14c02f4d, 0x1d148d82449eb4c6, 0x67ccfd4a74ab3dbf, + 0x289c8961bcb410bb, 0x5244f9a98c8199c2, 0xdd2c68f1dcdf0249, 0xa7f41839ecea8b30, + 0x438c80a64ce15841, 0x3954f06e7cd4d138, 0xb63c61362c8a4ab3, 0xcce411fe1cbfc3ca, + 0x83b465d5d4a0eece, 0xf96c151de49567b7, 0x76048445b4cbfc3c, 0x0cdcf48d84fe7545, + 0x6fbd6d5ebd3716b7, 0x15651d968d029fce, 0x9a0d8ccedd5c0445, 0xe0d5fc06ed698d3c, + 0xaf85882d2576a038, 0xd55df8e515432941, 0x5a3569bd451db2ca, 0x20ed197575283bb3, + 0xc49581ead523e8c2, 0xbe4df122e51661bb, 0x3125607ab548fa30, 0x4bfd10b2857d7349, + 0x04ad64994d625e4d, 0x7e7514517d57d734, 0xf11d85092d094cbf, 0x8bc5f5c11d3cc5c6, + 0x12b5926535897936, 0x686de2ad05bcf04f, 0xe70573f555e26bc4, 0x9ddd033d65d7e2bd, + 0xd28d7716adc8cfb9, 0xa85507de9dfd46c0, 0x273d9686cda3dd4b, 0x5de5e64efd965432, + 0xb99d7ed15d9d8743, 0xc3450e196da80e3a, 0x4c2d9f413df695b1, 0x36f5ef890dc31cc8, + 0x79a59ba2c5dc31cc, 0x037deb6af5e9b8b5, 0x8c157a32a5b7233e, 0xf6cd0afa9582aa47, + 0x4ad64994d625e4da, 0x300e395ce6106da3, 0xbf66a804b64ef628, 0xc5bed8cc867b7f51, + 0x8aeeace74e645255, 0xf036dc2f7e51db2c, 0x7f5e4d772e0f40a7, 0x05863dbf1e3ac9de, + 0xe1fea520be311aaf, 0x9b26d5e88e0493d6, 0x144e44b0de5a085d, 0x6e963478ee6f8124, + 0x21c640532670ac20, 0x5b1e309b16452559, 0xd476a1c3461bbed2, 0xaeaed10b762e37ab, + 0x37deb6af5e9b8b5b, 0x4d06c6676eae0222, 0xc26e573f3ef099a9, 0xb8b627f70ec510d0, + 0xf7e653dcc6da3dd4, 0x8d3e2314f6efb4ad, 0x0256b24ca6b12f26, 0x788ec2849684a65f, + 0x9cf65a1b368f752e, 0xe62e2ad306bafc57, 0x6946bb8b56e467dc, 0x139ecb4366d1eea5, + 0x5ccebf68aecec3a1, 0x2616cfa09efb4ad8, 0xa97e5ef8cea5d153, 0xd3a62e30fe90582a, + 0xb0c7b7e3c7593bd8, 0xca1fc72bf76cb2a1, 0x45775673a732292a, 0x3faf26bb9707a053, + 0x70ff52905f188d57, 0x0a2722586f2d042e, 0x854fb3003f739fa5, 0xff97c3c80f4616dc, + 0x1bef5b57af4dc5ad, 0x61372b9f9f784cd4, 0xee5fbac7cf26d75f, 0x9487ca0fff135e26, + 0xdbd7be24370c7322, 0xa10fceec0739fa5b, 0x2e675fb4576761d0, 0x54bf2f7c6752e8a9, + 0xcdcf48d84fe75459, 0xb71738107fd2dd20, 0x387fa9482f8c46ab, 0x42a7d9801fb9cfd2, + 0x0df7adabd7a6e2d6, 0x772fdd63e7936baf, 0xf8474c3bb7cdf024, 0x829f3cf387f8795d, + 0x66e7a46c27f3aa2c, 0x1c3fd4a417c62355, 0x935745fc4798b8de, 0xe98f353477ad31a7, + 0xa6df411fbfb21ca3, 0xdc0731d78f8795da, 0x536fa08fdfd90e51, 0x29b7d047efec8728 + }, + { + 0x0000000000000000, 0x89e99ffd73bddf69, 0x388a19a9bfec2db9, 0xb1638654cc51f2d0, + 0x711433537fd85b72, 0xf8fdacae0c65841b, 0x499e2afac03476cb, 0xc077b507b389a9a2, + 0xe22866a6ffb0b6e4, 0x6bc1f95b8c0d698d, 0xdaa27f0f405c9b5d, 0x534be0f233e14434, + 0x933c55f58068ed96, 0x1ad5ca08f3d532ff, 0xabb64c5c3f84c02f, 0x225fd3a14c391f46, + 0xef09eb1ea7f6fea3, 0x66e074e3d44b21ca, 0xd783f2b7181ad31a, 0x5e6a6d4a6ba70c73, + 0x9e1dd84dd82ea5d1, 0x17f447b0ab937ab8, 0xa697c1e467c28868, 0x2f7e5e19147f5701, + 0x0d218db858464847, 0x84c812452bfb972e, 0x35ab9411e7aa65fe, 0xbc420bec9417ba97, + 0x7c35beeb279e1335, 0xf5dc21165423cc5c, 0x44bfa74298723e8c, 0xcd5638bfebcfe1e5, + 0xf54af06e177a6e2d, 0x7ca36f9364c7b144, 0xcdc0e9c7a8964394, 0x4429763adb2b9cfd, + 0x845ec33d68a2355f, 0x0db75cc01b1fea36, 0xbcd4da94d74e18e6, 0x353d4569a4f3c78f, + 0x176296c8e8cad8c9, 0x9e8b09359b7707a0, 0x2fe88f615726f570, 0xa601109c249b2a19, + 0x6676a59b971283bb, 0xef9f3a66e4af5cd2, 0x5efcbc3228feae02, 0xd71523cf5b43716b, + 0x1a431b70b08c908e, 0x93aa848dc3314fe7, 0x22c902d90f60bd37, 0xab209d247cdd625e, + 0x6b572823cf54cbfc, 0xe2beb7debce91495, 0x53dd318a70b8e645, 0xda34ae770305392c, + 0xf86b7dd64f3c266a, 0x7182e22b3c81f903, 0xc0e1647ff0d00bd3, 0x4908fb82836dd4ba, + 0x897f4e8530e47d18, 0x0096d1784359a271, 0xb1f5572c8f0850a1, 0x381cc8d1fcb58fc8, + 0xc1ccc68f76634f31, 0x4825597205de9058, 0xf946df26c98f6288, 0x70af40dbba32bde1, + 0xb0d8f5dc09bb1443, 0x39316a217a06cb2a, 0x8852ec75b65739fa, 0x01bb7388c5eae693, + 0x23e4a02989d3f9d5, 0xaa0d3fd4fa6e26bc, 0x1b6eb980363fd46c, 0x9287267d45820b05, + 0x52f0937af60ba2a7, 0xdb190c8785b67dce, 0x6a7a8ad349e78f1e, 0xe393152e3a5a5077, + 0x2ec52d91d195b192, 0xa72cb26ca2286efb, 0x164f34386e799c2b, 0x9fa6abc51dc44342, + 0x5fd11ec2ae4deae0, 0xd638813fddf03589, 0x675b076b11a1c759, 0xeeb29896621c1830, + 0xcced4b372e250776, 0x4504d4ca5d98d81f, 0xf467529e91c92acf, 0x7d8ecd63e274f5a6, + 0xbdf9786451fd5c04, 0x3410e7992240836d, 0x857361cdee1171bd, 0x0c9afe309dacaed4, + 0x348636e16119211c, 0xbd6fa91c12a4fe75, 0x0c0c2f48def50ca5, 0x85e5b0b5ad48d3cc, + 0x459205b21ec17a6e, 0xcc7b9a4f6d7ca507, 0x7d181c1ba12d57d7, 0xf4f183e6d29088be, + 0xd6ae50479ea997f8, 0x5f47cfbaed144891, 0xee2449ee2145ba41, 0x67cdd61352f86528, + 0xa7ba6314e171cc8a, 0x2e53fce992cc13e3, 0x9f307abd5e9de133, 0x16d9e5402d203e5a, + 0xdb8fddffc6efdfbf, 0x52664202b55200d6, 0xe305c4567903f206, 0x6aec5bab0abe2d6f, + 0xaa9beeacb93784cd, 0x23727151ca8a5ba4, 0x9211f70506dba974, 0x1bf868f87566761d, + 0x39a7bb59395f695b, 0xb04e24a44ae2b632, 0x012da2f086b344e2, 0x88c43d0df50e9b8b, + 0x48b3880a46873229, 0xc15a17f7353aed40, 0x703991a3f96b1f90, 0xf9d00e5e8ad6c0f9, + 0xa8c0ab4db4510d09, 0x212934b0c7ecd260, 0x904ab2e40bbd20b0, 0x19a32d197800ffd9, + 0xd9d4981ecb89567b, 0x503d07e3b8348912, 0xe15e81b774657bc2, 0x68b71e4a07d8a4ab, + 0x4ae8cdeb4be1bbed, 0xc3015216385c6484, 0x7262d442f40d9654, 0xfb8b4bbf87b0493d, + 0x3bfcfeb83439e09f, 0xb215614547843ff6, 0x0376e7118bd5cd26, 0x8a9f78ecf868124f, + 0x47c9405313a7f3aa, 0xce20dfae601a2cc3, 0x7f4359faac4bde13, 0xf6aac607dff6017a, + 0x36dd73006c7fa8d8, 0xbf34ecfd1fc277b1, 0x0e576aa9d3938561, 0x87bef554a02e5a08, + 0xa5e126f5ec17454e, 0x2c08b9089faa9a27, 0x9d6b3f5c53fb68f7, 0x1482a0a12046b79e, + 0xd4f515a693cf1e3c, 0x5d1c8a5be072c155, 0xec7f0c0f2c233385, 0x659693f25f9eecec, + 0x5d8a5b23a32b6324, 0xd463c4ded096bc4d, 0x6500428a1cc74e9d, 0xece9dd776f7a91f4, + 0x2c9e6870dcf33856, 0xa577f78daf4ee73f, 0x141471d9631f15ef, 0x9dfdee2410a2ca86, + 0xbfa23d855c9bd5c0, 0x364ba2782f260aa9, 0x8728242ce377f879, 0x0ec1bbd190ca2710, + 0xceb60ed623438eb2, 0x475f912b50fe51db, 0xf63c177f9cafa30b, 0x7fd58882ef127c62, + 0xb283b03d04dd9d87, 0x3b6a2fc0776042ee, 0x8a09a994bb31b03e, 0x03e03669c88c6f57, + 0xc397836e7b05c6f5, 0x4a7e1c9308b8199c, 0xfb1d9ac7c4e9eb4c, 0x72f4053ab7543425, + 0x50abd69bfb6d2b63, 0xd942496688d0f40a, 0x6821cf32448106da, 0xe1c850cf373cd9b3, + 0x21bfe5c884b57011, 0xa8567a35f708af78, 0x1935fc613b595da8, 0x90dc639c48e482c1, + 0x690c6dc2c2324238, 0xe0e5f23fb18f9d51, 0x5186746b7dde6f81, 0xd86feb960e63b0e8, + 0x18185e91bdea194a, 0x91f1c16cce57c623, 0x20924738020634f3, 0xa97bd8c571bbeb9a, + 0x8b240b643d82f4dc, 0x02cd94994e3f2bb5, 0xb3ae12cd826ed965, 0x3a478d30f1d3060c, + 0xfa303837425aafae, 0x73d9a7ca31e770c7, 0xc2ba219efdb68217, 0x4b53be638e0b5d7e, + 0x860586dc65c4bc9b, 0x0fec1921167963f2, 0xbe8f9f75da289122, 0x37660088a9954e4b, + 0xf711b58f1a1ce7e9, 0x7ef82a7269a13880, 0xcf9bac26a5f0ca50, 0x467233dbd64d1539, + 0x642de07a9a740a7f, 0xedc47f87e9c9d516, 0x5ca7f9d3259827c6, 0xd54e662e5625f8af, + 0x1539d329e5ac510d, 0x9cd04cd496118e64, 0x2db3ca805a407cb4, 0xa45a557d29fda3dd, + 0x9c469dacd5482c15, 0x15af0251a6f5f37c, 0xa4cc84056aa401ac, 0x2d251bf81919dec5, + 0xed52aeffaa907767, 0x64bb3102d92da80e, 0xd5d8b756157c5ade, 0x5c3128ab66c185b7, + 0x7e6efb0a2af89af1, 0xf78764f759454598, 0x46e4e2a39514b748, 0xcf0d7d5ee6a96821, + 0x0f7ac8595520c183, 0x869357a4269d1eea, 0x37f0d1f0eaccec3a, 0xbe194e0d99713353, + 0x734f76b272bed2b6, 0xfaa6e94f01030ddf, 0x4bc56f1bcd52ff0f, 0xc22cf0e6beef2066, + 0x025b45e10d6689c4, 0x8bb2da1c7edb56ad, 0x3ad15c48b28aa47d, 0xb338c3b5c1377b14, + 0x916710148d0e6452, 0x188e8fe9feb3bb3b, 0xa9ed09bd32e249eb, 0x20049640415f9682, + 0xe0732347f2d63f20, 0x699abcba816be049, 0xd8f93aee4d3a1299, 0x5110a5133e87cdf0 + }, + { + 0x0000000000000000, 0xf4125129ce4038be, 0xc37d8400c417e217, 0x376fd5290a57daa9, + 0xada22e52d0b85745, 0x59b07f7b1ef86ffb, 0x6edfaa5214afb552, 0x9acdfb7bdaef8dec, + 0x701d7af6f9e73de1, 0x840f2bdf37a7055f, 0xb360fef63df0dff6, 0x4772afdff3b0e748, + 0xddbf54a4295f6aa4, 0x29ad058de71f521a, 0x1ec2d0a4ed4888b3, 0xead0818d2308b00d, + 0xe03af5edf3ce7bc2, 0x1428a4c43d8e437c, 0x234771ed37d999d5, 0xd75520c4f999a16b, + 0x4d98dbbf23762c87, 0xb98a8a96ed361439, 0x8ee55fbfe761ce90, 0x7af70e962921f62e, + 0x90278f1b0a294623, 0x6435de32c4697e9d, 0x535a0b1bce3ea434, 0xa7485a32007e9c8a, + 0x3d85a149da911166, 0xc997f06014d129d8, 0xfef825491e86f371, 0x0aea7460d0c6cbcf, + 0xeb2ccd88bf0b64ef, 0x1f3e9ca1714b5c51, 0x285149887b1c86f8, 0xdc4318a1b55cbe46, + 0x468ee3da6fb333aa, 0xb29cb2f3a1f30b14, 0x85f367daaba4d1bd, 0x71e136f365e4e903, + 0x9b31b77e46ec590e, 0x6f23e65788ac61b0, 0x584c337e82fbbb19, 0xac5e62574cbb83a7, + 0x3693992c96540e4b, 0xc281c805581436f5, 0xf5ee1d2c5243ec5c, 0x01fc4c059c03d4e2, + 0x0b1638654cc51f2d, 0xff04694c82852793, 0xc86bbc6588d2fd3a, 0x3c79ed4c4692c584, + 0xa6b416379c7d4868, 0x52a6471e523d70d6, 0x65c99237586aaa7f, 0x91dbc31e962a92c1, + 0x7b0b4293b52222cc, 0x8f1913ba7b621a72, 0xb876c6937135c0db, 0x4c6497babf75f865, + 0xd6a96cc1659a7589, 0x22bb3de8abda4d37, 0x15d4e8c1a18d979e, 0xe1c6b9e86fcdaf20, + 0xfd00bd4226815ab5, 0x0912ec6be8c1620b, 0x3e7d3942e296b8a2, 0xca6f686b2cd6801c, + 0x50a29310f6390df0, 0xa4b0c2393879354e, 0x93df1710322eefe7, 0x67cd4639fc6ed759, + 0x8d1dc7b4df666754, 0x790f969d11265fea, 0x4e6043b41b718543, 0xba72129dd531bdfd, + 0x20bfe9e60fde3011, 0xd4adb8cfc19e08af, 0xe3c26de6cbc9d206, 0x17d03ccf0589eab8, + 0x1d3a48afd54f2177, 0xe92819861b0f19c9, 0xde47ccaf1158c360, 0x2a559d86df18fbde, + 0xb09866fd05f77632, 0x448a37d4cbb74e8c, 0x73e5e2fdc1e09425, 0x87f7b3d40fa0ac9b, + 0x6d2732592ca81c96, 0x99356370e2e82428, 0xae5ab659e8bffe81, 0x5a48e77026ffc63f, + 0xc0851c0bfc104bd3, 0x34974d223250736d, 0x03f8980b3807a9c4, 0xf7eac922f647917a, + 0x162c70ca998a3e5a, 0xe23e21e357ca06e4, 0xd551f4ca5d9ddc4d, 0x2143a5e393dde4f3, + 0xbb8e5e984932691f, 0x4f9c0fb1877251a1, 0x78f3da988d258b08, 0x8ce18bb14365b3b6, + 0x66310a3c606d03bb, 0x92235b15ae2d3b05, 0xa54c8e3ca47ae1ac, 0x515edf156a3ad912, + 0xcb93246eb0d554fe, 0x3f8175477e956c40, 0x08eea06e74c2b6e9, 0xfcfcf147ba828e57, + 0xf61685276a444598, 0x0204d40ea4047d26, 0x356b0127ae53a78f, 0xc179500e60139f31, + 0x5bb4ab75bafc12dd, 0xafa6fa5c74bc2a63, 0x98c92f757eebf0ca, 0x6cdb7e5cb0abc874, + 0x860bffd193a37879, 0x7219aef85de340c7, 0x45767bd157b49a6e, 0xb1642af899f4a2d0, + 0x2ba9d183431b2f3c, 0xdfbb80aa8d5b1782, 0xe8d45583870ccd2b, 0x1cc604aa494cf595, + 0xd1585cd715952601, 0x254a0dfedbd51ebf, 0x1225d8d7d182c416, 0xe63789fe1fc2fca8, + 0x7cfa7285c52d7144, 0x88e823ac0b6d49fa, 0xbf87f685013a9353, 0x4b95a7accf7aabed, + 0xa1452621ec721be0, 0x555777082232235e, 0x6238a2212865f9f7, 0x962af308e625c149, + 0x0ce708733cca4ca5, 0xf8f5595af28a741b, 0xcf9a8c73f8ddaeb2, 0x3b88dd5a369d960c, + 0x3162a93ae65b5dc3, 0xc570f813281b657d, 0xf21f2d3a224cbfd4, 0x060d7c13ec0c876a, + 0x9cc0876836e30a86, 0x68d2d641f8a33238, 0x5fbd0368f2f4e891, 0xabaf52413cb4d02f, + 0x417fd3cc1fbc6022, 0xb56d82e5d1fc589c, 0x820257ccdbab8235, 0x761006e515ebba8b, + 0xecddfd9ecf043767, 0x18cfacb701440fd9, 0x2fa0799e0b13d570, 0xdbb228b7c553edce, + 0x3a74915faa9e42ee, 0xce66c07664de7a50, 0xf909155f6e89a0f9, 0x0d1b4476a0c99847, + 0x97d6bf0d7a2615ab, 0x63c4ee24b4662d15, 0x54ab3b0dbe31f7bc, 0xa0b96a247071cf02, + 0x4a69eba953797f0f, 0xbe7bba809d3947b1, 0x89146fa9976e9d18, 0x7d063e80592ea5a6, + 0xe7cbc5fb83c1284a, 0x13d994d24d8110f4, 0x24b641fb47d6ca5d, 0xd0a410d28996f2e3, + 0xda4e64b25950392c, 0x2e5c359b97100192, 0x1933e0b29d47db3b, 0xed21b19b5307e385, + 0x77ec4ae089e86e69, 0x83fe1bc947a856d7, 0xb491cee04dff8c7e, 0x40839fc983bfb4c0, + 0xaa531e44a0b704cd, 0x5e414f6d6ef73c73, 0x692e9a4464a0e6da, 0x9d3ccb6daae0de64, + 0x07f13016700f5388, 0xf3e3613fbe4f6b36, 0xc48cb416b418b19f, 0x309ee53f7a588921, + 0x2c58e19533147cb4, 0xd84ab0bcfd54440a, 0xef256595f7039ea3, 0x1b3734bc3943a61d, + 0x81facfc7e3ac2bf1, 0x75e89eee2dec134f, 0x42874bc727bbc9e6, 0xb6951aeee9fbf158, + 0x5c459b63caf34155, 0xa857ca4a04b379eb, 0x9f381f630ee4a342, 0x6b2a4e4ac0a49bfc, + 0xf1e7b5311a4b1610, 0x05f5e418d40b2eae, 0x329a3131de5cf407, 0xc6886018101cccb9, + 0xcc621478c0da0776, 0x387045510e9a3fc8, 0x0f1f907804cde561, 0xfb0dc151ca8ddddf, + 0x61c03a2a10625033, 0x95d26b03de22688d, 0xa2bdbe2ad475b224, 0x56afef031a358a9a, + 0xbc7f6e8e393d3a97, 0x486d3fa7f77d0229, 0x7f02ea8efd2ad880, 0x8b10bba7336ae03e, + 0x11dd40dce9856dd2, 0xe5cf11f527c5556c, 0xd2a0c4dc2d928fc5, 0x26b295f5e3d2b77b, + 0xc7742c1d8c1f185b, 0x33667d34425f20e5, 0x0409a81d4808fa4c, 0xf01bf9348648c2f2, + 0x6ad6024f5ca74f1e, 0x9ec4536692e777a0, 0xa9ab864f98b0ad09, 0x5db9d76656f095b7, + 0xb76956eb75f825ba, 0x437b07c2bbb81d04, 0x7414d2ebb1efc7ad, 0x800683c27fafff13, + 0x1acb78b9a54072ff, 0xeed929906b004a41, 0xd9b6fcb9615790e8, 0x2da4ad90af17a856, + 0x274ed9f07fd16399, 0xd35c88d9b1915b27, 0xe4335df0bbc6818e, 0x10210cd97586b930, + 0x8aecf7a2af6934dc, 0x7efea68b61290c62, 0x499173a26b7ed6cb, 0xbd83228ba53eee75, + 0x5753a30686365e78, 0xa341f22f487666c6, 0x942e27064221bc6f, 0x603c762f8c6184d1, + 0xfaf18d54568e093d, 0x0ee3dc7d98ce3183, 0x398c09549299eb2a, 0xcd9e587d5cd9d394 + }, + { + 0x0000000000000000, 0x8ce168638c796306, 0x329bf69440655567, 0xbe7a9ef7cc1c3661, + 0x6537ed2880caaace, 0xe9d6854b0cb3c9c8, 0x57ac1bbcc0afffa9, 0xdb4d73df4cd69caf, + 0xca6fda510195559c, 0x468eb2328dec369a, 0xf8f42cc541f000fb, 0x741544a6cd8963fd, + 0xaf583779815fff52, 0x23b95f1a0d269c54, 0x9dc3c1edc13aaa35, 0x1122a98e4d43c933, + 0xbf8692f15bbd3853, 0x3367fa92d7c45b55, 0x8d1d64651bd86d34, 0x01fc0c0697a10e32, + 0xdab17fd9db77929d, 0x565017ba570ef19b, 0xe82a894d9b12c7fa, 0x64cbe12e176ba4fc, + 0x75e948a05a286dcf, 0xf90820c3d6510ec9, 0x4772be341a4d38a8, 0xcb93d65796345bae, + 0x10dea588dae2c701, 0x9c3fcdeb569ba407, 0x2245531c9a879266, 0xaea43b7f16fef160, + 0x545403b1efede3cd, 0xd8b56bd2639480cb, 0x66cff525af88b6aa, 0xea2e9d4623f1d5ac, + 0x3163ee996f274903, 0xbd8286fae35e2a05, 0x03f8180d2f421c64, 0x8f19706ea33b7f62, + 0x9e3bd9e0ee78b651, 0x12dab1836201d557, 0xaca02f74ae1de336, 0x2041471722648030, + 0xfb0c34c86eb21c9f, 0x77ed5cabe2cb7f99, 0xc997c25c2ed749f8, 0x4576aa3fa2ae2afe, + 0xebd29140b450db9e, 0x6733f9233829b898, 0xd94967d4f4358ef9, 0x55a80fb7784cedff, + 0x8ee57c68349a7150, 0x0204140bb8e31256, 0xbc7e8afc74ff2437, 0x309fe29ff8864731, + 0x21bd4b11b5c58e02, 0xad5c237239bced04, 0x1326bd85f5a0db65, 0x9fc7d5e679d9b863, + 0x448aa639350f24cc, 0xc86bce5ab97647ca, 0x761150ad756a71ab, 0xfaf038cef91312ad, + 0xa8a80763dfdbc79a, 0x24496f0053a2a49c, 0x9a33f1f79fbe92fd, 0x16d2999413c7f1fb, + 0xcd9fea4b5f116d54, 0x417e8228d3680e52, 0xff041cdf1f743833, 0x73e574bc930d5b35, + 0x62c7dd32de4e9206, 0xee26b5515237f100, 0x505c2ba69e2bc761, 0xdcbd43c51252a467, + 0x07f0301a5e8438c8, 0x8b115879d2fd5bce, 0x356bc68e1ee16daf, 0xb98aaeed92980ea9, + 0x172e95928466ffc9, 0x9bcffdf1081f9ccf, 0x25b56306c403aaae, 0xa9540b65487ac9a8, + 0x721978ba04ac5507, 0xfef810d988d53601, 0x40828e2e44c90060, 0xcc63e64dc8b06366, + 0xdd414fc385f3aa55, 0x51a027a0098ac953, 0xefdab957c596ff32, 0x633bd13449ef9c34, + 0xb876a2eb0539009b, 0x3497ca888940639d, 0x8aed547f455c55fc, 0x060c3c1cc92536fa, + 0xfcfc04d230362457, 0x701d6cb1bc4f4751, 0xce67f24670537130, 0x42869a25fc2a1236, + 0x99cbe9fab0fc8e99, 0x152a81993c85ed9f, 0xab501f6ef099dbfe, 0x27b1770d7ce0b8f8, + 0x3693de8331a371cb, 0xba72b6e0bdda12cd, 0x0408281771c624ac, 0x88e94074fdbf47aa, + 0x53a433abb169db05, 0xdf455bc83d10b803, 0x613fc53ff10c8e62, 0xeddead5c7d75ed64, + 0x437a96236b8b1c04, 0xcf9bfe40e7f27f02, 0x71e160b72bee4963, 0xfd0008d4a7972a65, + 0x264d7b0beb41b6ca, 0xaaac13686738d5cc, 0x14d68d9fab24e3ad, 0x9837e5fc275d80ab, + 0x89154c726a1e4998, 0x05f42411e6672a9e, 0xbb8ebae62a7b1cff, 0x376fd285a6027ff9, + 0xec22a15aead4e356, 0x60c3c93966ad8050, 0xdeb957ceaab1b631, 0x52583fad26c8d537, + 0x7a092894e7201c5f, 0xf6e840f76b597f59, 0x4892de00a7454938, 0xc473b6632b3c2a3e, + 0x1f3ec5bc67eab691, 0x93dfaddfeb93d597, 0x2da53328278fe3f6, 0xa1445b4babf680f0, + 0xb066f2c5e6b549c3, 0x3c879aa66acc2ac5, 0x82fd0451a6d01ca4, 0x0e1c6c322aa97fa2, + 0xd5511fed667fe30d, 0x59b0778eea06800b, 0xe7cae979261ab66a, 0x6b2b811aaa63d56c, + 0xc58fba65bc9d240c, 0x496ed20630e4470a, 0xf7144cf1fcf8716b, 0x7bf524927081126d, + 0xa0b8574d3c578ec2, 0x2c593f2eb02eedc4, 0x9223a1d97c32dba5, 0x1ec2c9baf04bb8a3, + 0x0fe06034bd087190, 0x8301085731711296, 0x3d7b96a0fd6d24f7, 0xb19afec3711447f1, + 0x6ad78d1c3dc2db5e, 0xe636e57fb1bbb858, 0x584c7b887da78e39, 0xd4ad13ebf1deed3f, + 0x2e5d2b2508cdff92, 0xa2bc434684b49c94, 0x1cc6ddb148a8aaf5, 0x9027b5d2c4d1c9f3, + 0x4b6ac60d8807555c, 0xc78bae6e047e365a, 0x79f13099c862003b, 0xf51058fa441b633d, + 0xe432f1740958aa0e, 0x68d399178521c908, 0xd6a907e0493dff69, 0x5a486f83c5449c6f, + 0x81051c5c899200c0, 0x0de4743f05eb63c6, 0xb39eeac8c9f755a7, 0x3f7f82ab458e36a1, + 0x91dbb9d45370c7c1, 0x1d3ad1b7df09a4c7, 0xa3404f40131592a6, 0x2fa127239f6cf1a0, + 0xf4ec54fcd3ba6d0f, 0x780d3c9f5fc30e09, 0xc677a26893df3868, 0x4a96ca0b1fa65b6e, + 0x5bb4638552e5925d, 0xd7550be6de9cf15b, 0x692f95111280c73a, 0xe5cefd729ef9a43c, + 0x3e838eadd22f3893, 0xb262e6ce5e565b95, 0x0c187839924a6df4, 0x80f9105a1e330ef2, + 0xd2a12ff738fbdbc5, 0x5e404794b482b8c3, 0xe03ad963789e8ea2, 0x6cdbb100f4e7eda4, + 0xb796c2dfb831710b, 0x3b77aabc3448120d, 0x850d344bf854246c, 0x09ec5c28742d476a, + 0x18cef5a6396e8e59, 0x942f9dc5b517ed5f, 0x2a550332790bdb3e, 0xa6b46b51f572b838, + 0x7df9188eb9a42497, 0xf11870ed35dd4791, 0x4f62ee1af9c171f0, 0xc383867975b812f6, + 0x6d27bd066346e396, 0xe1c6d565ef3f8090, 0x5fbc4b922323b6f1, 0xd35d23f1af5ad5f7, + 0x0810502ee38c4958, 0x84f1384d6ff52a5e, 0x3a8ba6baa3e91c3f, 0xb66aced92f907f39, + 0xa748675762d3b60a, 0x2ba90f34eeaad50c, 0x95d391c322b6e36d, 0x1932f9a0aecf806b, + 0xc27f8a7fe2191cc4, 0x4e9ee21c6e607fc2, 0xf0e47ceba27c49a3, 0x7c0514882e052aa5, + 0x86f52c46d7163808, 0x0a1444255b6f5b0e, 0xb46edad297736d6f, 0x388fb2b11b0a0e69, + 0xe3c2c16e57dc92c6, 0x6f23a90ddba5f1c0, 0xd15937fa17b9c7a1, 0x5db85f999bc0a4a7, + 0x4c9af617d6836d94, 0xc07b9e745afa0e92, 0x7e01008396e638f3, 0xf2e068e01a9f5bf5, + 0x29ad1b3f5649c75a, 0xa54c735cda30a45c, 0x1b36edab162c923d, 0x97d785c89a55f13b, + 0x3973beb78cab005b, 0xb592d6d400d2635d, 0x0be84823ccce553c, 0x8709204040b7363a, + 0x5c44539f0c61aa95, 0xd0a53bfc8018c993, 0x6edfa50b4c04fff2, 0xe23ecd68c07d9cf4, + 0xf31c64e68d3e55c7, 0x7ffd0c85014736c1, 0xc1879272cd5b00a0, 0x4d66fa11412263a6, + 0x962b89ce0df4ff09, 0x1acae1ad818d9c0f, 0xa4b07f5a4d91aa6e, 0x28511739c1e8c968 + }, + { + 0x0000000000000000, 0x3504e58b9ba6dd1e, 0x6a09cb17374dba3c, 0x5f0d2e9caceb6722, + 0xd413962e6e9b7478, 0xe11773a5f53da966, 0xbe1a5d3959d6ce44, 0x8b1eb8b2c270135a, + 0x837e0a0f85a17b9b, 0xb67aef841e07a685, 0xe977c118b2ecc1a7, 0xdc732493294a1cb9, + 0x576d9c21eb3a0fe3, 0x626979aa709cd2fd, 0x3d645736dc77b5df, 0x0860b2bd47d168c1, + 0x2da5324c53d5645d, 0x18a1d7c7c873b943, 0x47acf95b6498de61, 0x72a81cd0ff3e037f, + 0xf9b6a4623d4e1025, 0xccb241e9a6e8cd3b, 0x93bf6f750a03aa19, 0xa6bb8afe91a57707, + 0xaedb3843d6741fc6, 0x9bdfddc84dd2c2d8, 0xc4d2f354e139a5fa, 0xf1d616df7a9f78e4, + 0x7ac8ae6db8ef6bbe, 0x4fcc4be62349b6a0, 0x10c1657a8fa2d182, 0x25c580f114040c9c, + 0x5b4a6498a7aac8ba, 0x6e4e81133c0c15a4, 0x3143af8f90e77286, 0x04474a040b41af98, + 0x8f59f2b6c931bcc2, 0xba5d173d529761dc, 0xe55039a1fe7c06fe, 0xd054dc2a65dadbe0, + 0xd8346e97220bb321, 0xed308b1cb9ad6e3f, 0xb23da5801546091d, 0x8739400b8ee0d403, + 0x0c27f8b94c90c759, 0x39231d32d7361a47, 0x662e33ae7bdd7d65, 0x532ad625e07ba07b, + 0x76ef56d4f47face7, 0x43ebb35f6fd971f9, 0x1ce69dc3c33216db, 0x29e278485894cbc5, + 0xa2fcc0fa9ae4d89f, 0x97f8257101420581, 0xc8f50bedada962a3, 0xfdf1ee66360fbfbd, + 0xf5915cdb71ded77c, 0xc095b950ea780a62, 0x9f9897cc46936d40, 0xaa9c7247dd35b05e, + 0x2182caf51f45a304, 0x14862f7e84e37e1a, 0x4b8b01e228081938, 0x7e8fe469b3aec426, + 0xb694c9314f559174, 0x83902cbad4f34c6a, 0xdc9d022678182b48, 0xe999e7ade3bef656, + 0x62875f1f21cee50c, 0x5783ba94ba683812, 0x088e940816835f30, 0x3d8a71838d25822e, + 0x35eac33ecaf4eaef, 0x00ee26b5515237f1, 0x5fe30829fdb950d3, 0x6ae7eda2661f8dcd, + 0xe1f95510a46f9e97, 0xd4fdb09b3fc94389, 0x8bf09e07932224ab, 0xbef47b8c0884f9b5, + 0x9b31fb7d1c80f529, 0xae351ef687262837, 0xf138306a2bcd4f15, 0xc43cd5e1b06b920b, + 0x4f226d53721b8151, 0x7a2688d8e9bd5c4f, 0x252ba64445563b6d, 0x102f43cfdef0e673, + 0x184ff17299218eb2, 0x2d4b14f9028753ac, 0x72463a65ae6c348e, 0x4742dfee35cae990, + 0xcc5c675cf7bafaca, 0xf95882d76c1c27d4, 0xa655ac4bc0f740f6, 0x935149c05b519de8, + 0xeddeada9e8ff59ce, 0xd8da4822735984d0, 0x87d766bedfb2e3f2, 0xb2d3833544143eec, + 0x39cd3b8786642db6, 0x0cc9de0c1dc2f0a8, 0x53c4f090b129978a, 0x66c0151b2a8f4a94, + 0x6ea0a7a66d5e2255, 0x5ba4422df6f8ff4b, 0x04a96cb15a139869, 0x31ad893ac1b54577, + 0xbab3318803c5562d, 0x8fb7d40398638b33, 0xd0bafa9f3488ec11, 0xe5be1f14af2e310f, + 0xc07b9fe5bb2a3d93, 0xf57f7a6e208ce08d, 0xaa7254f28c6787af, 0x9f76b17917c15ab1, + 0x146809cbd5b149eb, 0x216cec404e1794f5, 0x7e61c2dce2fcf3d7, 0x4b652757795a2ec9, + 0x430595ea3e8b4608, 0x76017061a52d9b16, 0x290c5efd09c6fc34, 0x1c08bb769260212a, + 0x971603c450103270, 0xa212e64fcbb6ef6e, 0xfd1fc8d3675d884c, 0xc81b2d58fcfb5552, + 0x4670b431c63cb183, 0x737451ba5d9a6c9d, 0x2c797f26f1710bbf, 0x197d9aad6ad7d6a1, + 0x9263221fa8a7c5fb, 0xa767c794330118e5, 0xf86ae9089fea7fc7, 0xcd6e0c83044ca2d9, + 0xc50ebe3e439dca18, 0xf00a5bb5d83b1706, 0xaf07752974d07024, 0x9a0390a2ef76ad3a, + 0x111d28102d06be60, 0x2419cd9bb6a0637e, 0x7b14e3071a4b045c, 0x4e10068c81edd942, + 0x6bd5867d95e9d5de, 0x5ed163f60e4f08c0, 0x01dc4d6aa2a46fe2, 0x34d8a8e13902b2fc, + 0xbfc61053fb72a1a6, 0x8ac2f5d860d47cb8, 0xd5cfdb44cc3f1b9a, 0xe0cb3ecf5799c684, + 0xe8ab8c721048ae45, 0xddaf69f98bee735b, 0x82a2476527051479, 0xb7a6a2eebca3c967, + 0x3cb81a5c7ed3da3d, 0x09bcffd7e5750723, 0x56b1d14b499e6001, 0x63b534c0d238bd1f, + 0x1d3ad0a961967939, 0x283e3522fa30a427, 0x77331bbe56dbc305, 0x4237fe35cd7d1e1b, + 0xc92946870f0d0d41, 0xfc2da30c94abd05f, 0xa3208d903840b77d, 0x9624681ba3e66a63, + 0x9e44daa6e43702a2, 0xab403f2d7f91dfbc, 0xf44d11b1d37ab89e, 0xc149f43a48dc6580, + 0x4a574c888aac76da, 0x7f53a903110aabc4, 0x205e879fbde1cce6, 0x155a6214264711f8, + 0x309fe2e532431d64, 0x059b076ea9e5c07a, 0x5a9629f2050ea758, 0x6f92cc799ea87a46, + 0xe48c74cb5cd8691c, 0xd1889140c77eb402, 0x8e85bfdc6b95d320, 0xbb815a57f0330e3e, + 0xb3e1e8eab7e266ff, 0x86e50d612c44bbe1, 0xd9e823fd80afdcc3, 0xececc6761b0901dd, + 0x67f27ec4d9791287, 0x52f69b4f42dfcf99, 0x0dfbb5d3ee34a8bb, 0x38ff5058759275a5, + 0xf0e47d00896920f7, 0xc5e0988b12cffde9, 0x9aedb617be249acb, 0xafe9539c258247d5, + 0x24f7eb2ee7f2548f, 0x11f30ea57c548991, 0x4efe2039d0bfeeb3, 0x7bfac5b24b1933ad, + 0x739a770f0cc85b6c, 0x469e9284976e8672, 0x1993bc183b85e150, 0x2c975993a0233c4e, + 0xa789e12162532f14, 0x928d04aaf9f5f20a, 0xcd802a36551e9528, 0xf884cfbdceb84836, + 0xdd414f4cdabc44aa, 0xe845aac7411a99b4, 0xb748845bedf1fe96, 0x824c61d076572388, + 0x0952d962b42730d2, 0x3c563ce92f81edcc, 0x635b1275836a8aee, 0x565ff7fe18cc57f0, + 0x5e3f45435f1d3f31, 0x6b3ba0c8c4bbe22f, 0x34368e546850850d, 0x01326bdff3f65813, + 0x8a2cd36d31864b49, 0xbf2836e6aa209657, 0xe025187a06cbf175, 0xd521fdf19d6d2c6b, + 0xabae19982ec3e84d, 0x9eaafc13b5653553, 0xc1a7d28f198e5271, 0xf4a3370482288f6f, + 0x7fbd8fb640589c35, 0x4ab96a3ddbfe412b, 0x15b444a177152609, 0x20b0a12aecb3fb17, + 0x28d01397ab6293d6, 0x1dd4f61c30c44ec8, 0x42d9d8809c2f29ea, 0x77dd3d0b0789f4f4, + 0xfcc385b9c5f9e7ae, 0xc9c760325e5f3ab0, 0x96ca4eaef2b45d92, 0xa3ceab256912808c, + 0x860b2bd47d168c10, 0xb30fce5fe6b0510e, 0xec02e0c34a5b362c, 0xd9060548d1fdeb32, + 0x5218bdfa138df868, 0x671c5871882b2576, 0x381176ed24c04254, 0x0d159366bf669f4a, + 0x057521dbf8b7f78b, 0x3071c45063112a95, 0x6f7ceacccffa4db7, 0x5a780f47545c90a9, + 0xd166b7f5962c83f3, 0xe462527e0d8a5eed, 0xbb6f7ce2a16139cf, 0x8e6b99693ac7e4d1 + }, + { + 0x0000000000000000, 0xe39d1389931b9354, 0xec6301407ea0b5c3, 0x0ffe12c9edbb2697, + 0xf39f24d3a5d6f8ed, 0x1002375a36cd6bb9, 0x1ffc2593db764d2e, 0xfc61361a486dde7a, + 0xcc676ff4133a62b1, 0x2ffa7c7d8021f1e5, 0x20046eb46d9ad772, 0xc3997d3dfe814426, + 0x3ff84b27b6ec9a5c, 0xdc6558ae25f70908, 0xd39b4a67c84c2f9f, 0x300659ee5b57bccb, + 0xb397f9bb7ee35609, 0x500aea32edf8c55d, 0x5ff4f8fb0043e3ca, 0xbc69eb729358709e, + 0x4008dd68db35aee4, 0xa395cee1482e3db0, 0xac6bdc28a5951b27, 0x4ff6cfa1368e8873, + 0x7ff0964f6dd934b8, 0x9c6d85c6fec2a7ec, 0x9393970f1379817b, 0x700e84868062122f, + 0x8c6fb29cc80fcc55, 0x6ff2a1155b145f01, 0x600cb3dcb6af7996, 0x8391a05525b4eac2, + 0x4c76d525a5513f79, 0xafebc6ac364aac2d, 0xa015d465dbf18aba, 0x4388c7ec48ea19ee, + 0xbfe9f1f60087c794, 0x5c74e27f939c54c0, 0x538af0b67e277257, 0xb017e33fed3ce103, + 0x8011bad1b66b5dc8, 0x638ca9582570ce9c, 0x6c72bb91c8cbe80b, 0x8fefa8185bd07b5f, + 0x738e9e0213bda525, 0x90138d8b80a63671, 0x9fed9f426d1d10e6, 0x7c708ccbfe0683b2, + 0xffe12c9edbb26970, 0x1c7c3f1748a9fa24, 0x13822ddea512dcb3, 0xf01f3e5736094fe7, + 0x0c7e084d7e64919d, 0xefe31bc4ed7f02c9, 0xe01d090d00c4245e, 0x03801a8493dfb70a, + 0x3386436ac8880bc1, 0xd01b50e35b939895, 0xdfe5422ab628be02, 0x3c7851a325332d56, + 0xc01967b96d5ef32c, 0x23847430fe456078, 0x2c7a66f913fe46ef, 0xcfe7757080e5d5bb, + 0x98edaa4b4aa27ef2, 0x7b70b9c2d9b9eda6, 0x748eab0b3402cb31, 0x9713b882a7195865, + 0x6b728e98ef74861f, 0x88ef9d117c6f154b, 0x87118fd891d433dc, 0x648c9c5102cfa088, + 0x548ac5bf59981c43, 0xb717d636ca838f17, 0xb8e9c4ff2738a980, 0x5b74d776b4233ad4, + 0xa715e16cfc4ee4ae, 0x4488f2e56f5577fa, 0x4b76e02c82ee516d, 0xa8ebf3a511f5c239, + 0x2b7a53f0344128fb, 0xc8e74079a75abbaf, 0xc71952b04ae19d38, 0x24844139d9fa0e6c, + 0xd8e577239197d016, 0x3b7864aa028c4342, 0x34867663ef3765d5, 0xd71b65ea7c2cf681, + 0xe71d3c04277b4a4a, 0x04802f8db460d91e, 0x0b7e3d4459dbff89, 0xe8e32ecdcac06cdd, + 0x148218d782adb2a7, 0xf71f0b5e11b621f3, 0xf8e11997fc0d0764, 0x1b7c0a1e6f169430, + 0xd49b7f6eeff3418b, 0x37066ce77ce8d2df, 0x38f87e2e9153f448, 0xdb656da70248671c, + 0x27045bbd4a25b966, 0xc4994834d93e2a32, 0xcb675afd34850ca5, 0x28fa4974a79e9ff1, + 0x18fc109afcc9233a, 0xfb6103136fd2b06e, 0xf49f11da826996f9, 0x17020253117205ad, + 0xeb633449591fdbd7, 0x08fe27c0ca044883, 0x0700350927bf6e14, 0xe49d2680b4a4fd40, + 0x670c86d591101782, 0x8491955c020b84d6, 0x8b6f8795efb0a241, 0x68f2941c7cab3115, + 0x9493a20634c6ef6f, 0x770eb18fa7dd7c3b, 0x78f0a3464a665aac, 0x9b6db0cfd97dc9f8, + 0xab6be921822a7533, 0x48f6faa81131e667, 0x4708e861fc8ac0f0, 0xa495fbe86f9153a4, + 0x58f4cdf227fc8dde, 0xbb69de7bb4e71e8a, 0xb497ccb2595c381d, 0x570adf3bca47ab49, + 0x1a8272c5cdd36e8f, 0xf91f614c5ec8fddb, 0xf6e17385b373db4c, 0x157c600c20684818, + 0xe91d561668059662, 0x0a80459ffb1e0536, 0x057e575616a523a1, 0xe6e344df85beb0f5, + 0xd6e51d31dee90c3e, 0x35780eb84df29f6a, 0x3a861c71a049b9fd, 0xd91b0ff833522aa9, + 0x257a39e27b3ff4d3, 0xc6e72a6be8246787, 0xc91938a2059f4110, 0x2a842b2b9684d244, + 0xa9158b7eb3303886, 0x4a8898f7202babd2, 0x45768a3ecd908d45, 0xa6eb99b75e8b1e11, + 0x5a8aafad16e6c06b, 0xb917bc2485fd533f, 0xb6e9aeed684675a8, 0x5574bd64fb5de6fc, + 0x6572e48aa00a5a37, 0x86eff7033311c963, 0x8911e5cadeaaeff4, 0x6a8cf6434db17ca0, + 0x96edc05905dca2da, 0x7570d3d096c7318e, 0x7a8ec1197b7c1719, 0x9913d290e867844d, + 0x56f4a7e0688251f6, 0xb569b469fb99c2a2, 0xba97a6a01622e435, 0x590ab52985397761, + 0xa56b8333cd54a91b, 0x46f690ba5e4f3a4f, 0x49088273b3f41cd8, 0xaa9591fa20ef8f8c, + 0x9a93c8147bb83347, 0x790edb9de8a3a013, 0x76f0c95405188684, 0x956ddadd960315d0, + 0x690cecc7de6ecbaa, 0x8a91ff4e4d7558fe, 0x856fed87a0ce7e69, 0x66f2fe0e33d5ed3d, + 0xe5635e5b166107ff, 0x06fe4dd2857a94ab, 0x09005f1b68c1b23c, 0xea9d4c92fbda2168, + 0x16fc7a88b3b7ff12, 0xf561690120ac6c46, 0xfa9f7bc8cd174ad1, 0x190268415e0cd985, + 0x290431af055b654e, 0xca9922269640f61a, 0xc56730ef7bfbd08d, 0x26fa2366e8e043d9, + 0xda9b157ca08d9da3, 0x390606f533960ef7, 0x36f8143cde2d2860, 0xd56507b54d36bb34, + 0x826fd88e8771107d, 0x61f2cb07146a8329, 0x6e0cd9cef9d1a5be, 0x8d91ca476aca36ea, + 0x71f0fc5d22a7e890, 0x926defd4b1bc7bc4, 0x9d93fd1d5c075d53, 0x7e0eee94cf1cce07, + 0x4e08b77a944b72cc, 0xad95a4f30750e198, 0xa26bb63aeaebc70f, 0x41f6a5b379f0545b, + 0xbd9793a9319d8a21, 0x5e0a8020a2861975, 0x51f492e94f3d3fe2, 0xb2698160dc26acb6, + 0x31f82135f9924674, 0xd26532bc6a89d520, 0xdd9b20758732f3b7, 0x3e0633fc142960e3, + 0xc26705e65c44be99, 0x21fa166fcf5f2dcd, 0x2e0404a622e40b5a, 0xcd99172fb1ff980e, + 0xfd9f4ec1eaa824c5, 0x1e025d4879b3b791, 0x11fc4f8194089106, 0xf2615c0807130252, + 0x0e006a124f7edc28, 0xed9d799bdc654f7c, 0xe2636b5231de69eb, 0x01fe78dba2c5fabf, + 0xce190dab22202f04, 0x2d841e22b13bbc50, 0x227a0ceb5c809ac7, 0xc1e71f62cf9b0993, + 0x3d86297887f6d7e9, 0xde1b3af114ed44bd, 0xd1e52838f956622a, 0x32783bb16a4df17e, + 0x027e625f311a4db5, 0xe1e371d6a201dee1, 0xee1d631f4fbaf876, 0x0d807096dca16b22, + 0xf1e1468c94ccb558, 0x127c550507d7260c, 0x1d8247ccea6c009b, 0xfe1f5445797793cf, + 0x7d8ef4105cc3790d, 0x9e13e799cfd8ea59, 0x91edf5502263ccce, 0x7270e6d9b1785f9a, + 0x8e11d0c3f91581e0, 0x6d8cc34a6a0e12b4, 0x6272d18387b53423, 0x81efc20a14aea777, + 0xb1e99be44ff91bbc, 0x5274886ddce288e8, 0x5d8a9aa43159ae7f, 0xbe17892da2423d2b, + 0x4276bf37ea2fe351, 0xa1ebacbe79347005, 0xae15be77948f5692, 0x4d88adfe0794c5c6 + }, + { + 0x0000000000000000, 0x62a95de6e302eff2, 0xc552bbcdc605dfe4, 0xa7fbe62b25073016, + 0xa1fc51c8d49c2ca3, 0xc3550c2e379ec351, 0x64aeea051299f347, 0x0607b7e3f19b1cb5, + 0x68a185c2f1afca2d, 0x0a08d82412ad25df, 0xadf33e0f37aa15c9, 0xcf5a63e9d4a8fa3b, + 0xc95dd40a2533e68e, 0xabf489ecc631097c, 0x0c0f6fc7e336396a, 0x6ea632210034d698, + 0xd1430b85e35f945a, 0xb3ea5663005d7ba8, 0x1411b048255a4bbe, 0x76b8edaec658a44c, + 0x70bf5a4d37c3b8f9, 0x121607abd4c1570b, 0xb5ede180f1c6671d, 0xd744bc6612c488ef, + 0xb9e28e4712f05e77, 0xdb4bd3a1f1f2b185, 0x7cb0358ad4f58193, 0x1e19686c37f76e61, + 0x181edf8fc66c72d4, 0x7ab78269256e9d26, 0xdd4c64420069ad30, 0xbfe539a4e36b42c2, + 0x89df31589e28bbdf, 0xeb766cbe7d2a542d, 0x4c8d8a95582d643b, 0x2e24d773bb2f8bc9, + 0x282360904ab4977c, 0x4a8a3d76a9b6788e, 0xed71db5d8cb14898, 0x8fd886bb6fb3a76a, + 0xe17eb49a6f8771f2, 0x83d7e97c8c859e00, 0x242c0f57a982ae16, 0x468552b14a8041e4, + 0x4082e552bb1b5d51, 0x222bb8b45819b2a3, 0x85d05e9f7d1e82b5, 0xe77903799e1c6d47, + 0x589c3add7d772f85, 0x3a35673b9e75c077, 0x9dce8110bb72f061, 0xff67dcf658701f93, + 0xf9606b15a9eb0326, 0x9bc936f34ae9ecd4, 0x3c32d0d86feedcc2, 0x5e9b8d3e8cec3330, + 0x303dbf1f8cd8e5a8, 0x5294e2f96fda0a5a, 0xf56f04d24add3a4c, 0x97c65934a9dfd5be, + 0x91c1eed75844c90b, 0xf368b331bb4626f9, 0x5493551a9e4116ef, 0x363a08fc7d43f91d, + 0x38e744e264c6e4d5, 0x5a4e190487c40b27, 0xfdb5ff2fa2c33b31, 0x9f1ca2c941c1d4c3, + 0x991b152ab05ac876, 0xfbb248cc53582784, 0x5c49aee7765f1792, 0x3ee0f301955df860, + 0x5046c12095692ef8, 0x32ef9cc6766bc10a, 0x95147aed536cf11c, 0xf7bd270bb06e1eee, + 0xf1ba90e841f5025b, 0x9313cd0ea2f7eda9, 0x34e82b2587f0ddbf, 0x564176c364f2324d, + 0xe9a44f678799708f, 0x8b0d1281649b9f7d, 0x2cf6f4aa419caf6b, 0x4e5fa94ca29e4099, + 0x48581eaf53055c2c, 0x2af14349b007b3de, 0x8d0aa562950083c8, 0xefa3f88476026c3a, + 0x8105caa57636baa2, 0xe3ac974395345550, 0x44577168b0336546, 0x26fe2c8e53318ab4, + 0x20f99b6da2aa9601, 0x4250c68b41a879f3, 0xe5ab20a064af49e5, 0x87027d4687ada617, + 0xb13875bafaee5f0a, 0xd391285c19ecb0f8, 0x746ace773ceb80ee, 0x16c39391dfe96f1c, + 0x10c424722e7273a9, 0x726d7994cd709c5b, 0xd5969fbfe877ac4d, 0xb73fc2590b7543bf, + 0xd999f0780b419527, 0xbb30ad9ee8437ad5, 0x1ccb4bb5cd444ac3, 0x7e6216532e46a531, + 0x7865a1b0dfddb984, 0x1accfc563cdf5676, 0xbd371a7d19d86660, 0xdf9e479bfada8992, + 0x607b7e3f19b1cb50, 0x02d223d9fab324a2, 0xa529c5f2dfb414b4, 0xc78098143cb6fb46, + 0xc1872ff7cd2de7f3, 0xa32e72112e2f0801, 0x04d5943a0b283817, 0x667cc9dce82ad7e5, + 0x08dafbfde81e017d, 0x6a73a61b0b1cee8f, 0xcd8840302e1bde99, 0xaf211dd6cd19316b, + 0xa926aa353c822dde, 0xcb8ff7d3df80c22c, 0x6c7411f8fa87f23a, 0x0edd4c1e19851dc8, + 0x71ce89c4c98dc9aa, 0x1367d4222a8f2658, 0xb49c32090f88164e, 0xd6356fefec8af9bc, + 0xd032d80c1d11e509, 0xb29b85eafe130afb, 0x156063c1db143aed, 0x77c93e273816d51f, + 0x196f0c0638220387, 0x7bc651e0db20ec75, 0xdc3db7cbfe27dc63, 0xbe94ea2d1d253391, + 0xb8935dceecbe2f24, 0xda3a00280fbcc0d6, 0x7dc1e6032abbf0c0, 0x1f68bbe5c9b91f32, + 0xa08d82412ad25df0, 0xc224dfa7c9d0b202, 0x65df398cecd78214, 0x0776646a0fd56de6, + 0x0171d389fe4e7153, 0x63d88e6f1d4c9ea1, 0xc4236844384baeb7, 0xa68a35a2db494145, + 0xc82c0783db7d97dd, 0xaa855a65387f782f, 0x0d7ebc4e1d784839, 0x6fd7e1a8fe7aa7cb, + 0x69d0564b0fe1bb7e, 0x0b790badece3548c, 0xac82ed86c9e4649a, 0xce2bb0602ae68b68, + 0xf811b89c57a57275, 0x9ab8e57ab4a79d87, 0x3d43035191a0ad91, 0x5fea5eb772a24263, + 0x59ede95483395ed6, 0x3b44b4b2603bb124, 0x9cbf5299453c8132, 0xfe160f7fa63e6ec0, + 0x90b03d5ea60ab858, 0xf21960b8450857aa, 0x55e28693600f67bc, 0x374bdb75830d884e, + 0x314c6c96729694fb, 0x53e5317091947b09, 0xf41ed75bb4934b1f, 0x96b78abd5791a4ed, + 0x2952b319b4fae62f, 0x4bfbeeff57f809dd, 0xec0008d472ff39cb, 0x8ea9553291fdd639, + 0x88aee2d16066ca8c, 0xea07bf378364257e, 0x4dfc591ca6631568, 0x2f5504fa4561fa9a, + 0x41f336db45552c02, 0x235a6b3da657c3f0, 0x84a18d168350f3e6, 0xe608d0f060521c14, + 0xe00f671391c900a1, 0x82a63af572cbef53, 0x255ddcde57ccdf45, 0x47f48138b4ce30b7, + 0x4929cd26ad4b2d7f, 0x2b8090c04e49c28d, 0x8c7b76eb6b4ef29b, 0xeed22b0d884c1d69, + 0xe8d59cee79d701dc, 0x8a7cc1089ad5ee2e, 0x2d872723bfd2de38, 0x4f2e7ac55cd031ca, + 0x218848e45ce4e752, 0x43211502bfe608a0, 0xe4daf3299ae138b6, 0x8673aecf79e3d744, + 0x8074192c8878cbf1, 0xe2dd44ca6b7a2403, 0x4526a2e14e7d1415, 0x278fff07ad7ffbe7, + 0x986ac6a34e14b925, 0xfac39b45ad1656d7, 0x5d387d6e881166c1, 0x3f9120886b138933, + 0x3996976b9a889586, 0x5b3fca8d798a7a74, 0xfcc42ca65c8d4a62, 0x9e6d7140bf8fa590, + 0xf0cb4361bfbb7308, 0x92621e875cb99cfa, 0x3599f8ac79beacec, 0x5730a54a9abc431e, + 0x513712a96b275fab, 0x339e4f4f8825b059, 0x9465a964ad22804f, 0xf6ccf4824e206fbd, + 0xc0f6fc7e336396a0, 0xa25fa198d0617952, 0x05a447b3f5664944, 0x670d1a551664a6b6, + 0x610aadb6e7ffba03, 0x03a3f05004fd55f1, 0xa458167b21fa65e7, 0xc6f14b9dc2f88a15, + 0xa85779bcc2cc5c8d, 0xcafe245a21ceb37f, 0x6d05c27104c98369, 0x0fac9f97e7cb6c9b, + 0x09ab28741650702e, 0x6b027592f5529fdc, 0xccf993b9d055afca, 0xae50ce5f33574038, + 0x11b5f7fbd03c02fa, 0x731caa1d333eed08, 0xd4e74c361639dd1e, 0xb64e11d0f53b32ec, + 0xb049a63304a02e59, 0xd2e0fbd5e7a2c1ab, 0x751b1dfec2a5f1bd, 0x17b2401821a71e4f, + 0x791472392193c8d7, 0x1bbd2fdfc2912725, 0xbc46c9f4e7961733, 0xdeef94120494f8c1, + 0xd8e823f1f50fe474, 0xba417e17160d0b86, 0x1dba983c330a3b90, 0x7f13c5dad008d462 + }, + { + 0x0000000000000000, 0x381d0015c96f4444, 0x703a002b92de8888, 0x4827003e5bb1cccc, + 0xe074005725bd1110, 0xd8690042ecd25554, 0x904e007cb7639998, 0xa85300697e0cdddc, + 0xebb126fd13edb14b, 0xd3ac26e8da82f50f, 0x9b8b26d6813339c3, 0xa39626c3485c7d87, + 0x0bc526aa3650a05b, 0x33d826bfff3fe41f, 0x7bff2681a48e28d3, 0x43e226946de16c97, + 0xfc3b6ba97f4cf1fd, 0xc4266bbcb623b5b9, 0x8c016b82ed927975, 0xb41c6b9724fd3d31, + 0x1c4f6bfe5af1e0ed, 0x24526beb939ea4a9, 0x6c756bd5c82f6865, 0x54686bc001402c21, + 0x178a4d546ca140b6, 0x2f974d41a5ce04f2, 0x67b04d7ffe7fc83e, 0x5fad4d6a37108c7a, + 0xf7fe4d03491c51a6, 0xcfe34d16807315e2, 0x87c44d28dbc2d92e, 0xbfd94d3d12ad9d6a, + 0xd32ff101a60e7091, 0xeb32f1146f6134d5, 0xa315f12a34d0f819, 0x9b08f13ffdbfbc5d, + 0x335bf15683b36181, 0x0b46f1434adc25c5, 0x4361f17d116de909, 0x7b7cf168d802ad4d, + 0x389ed7fcb5e3c1da, 0x0083d7e97c8c859e, 0x48a4d7d7273d4952, 0x70b9d7c2ee520d16, + 0xd8ead7ab905ed0ca, 0xe0f7d7be5931948e, 0xa8d0d78002805842, 0x90cdd795cbef1c06, + 0x2f149aa8d942816c, 0x17099abd102dc528, 0x5f2e9a834b9c09e4, 0x67339a9682f34da0, + 0xcf609afffcff907c, 0xf77d9aea3590d438, 0xbf5a9ad46e2118f4, 0x87479ac1a74e5cb0, + 0xc4a5bc55caaf3027, 0xfcb8bc4003c07463, 0xb49fbc7e5871b8af, 0x8c82bc6b911efceb, + 0x24d1bc02ef122137, 0x1cccbc17267d6573, 0x54ebbc297dcca9bf, 0x6cf6bc3cb4a3edfb, + 0x8d06c450148b7249, 0xb51bc445dde4360d, 0xfd3cc47b8655fac1, 0xc521c46e4f3abe85, + 0x6d72c40731366359, 0x556fc412f859271d, 0x1d48c42ca3e8ebd1, 0x2555c4396a87af95, + 0x66b7e2ad0766c302, 0x5eaae2b8ce098746, 0x168de28695b84b8a, 0x2e90e2935cd70fce, + 0x86c3e2fa22dbd212, 0xbedee2efebb49656, 0xf6f9e2d1b0055a9a, 0xcee4e2c4796a1ede, + 0x713daff96bc783b4, 0x4920afeca2a8c7f0, 0x0107afd2f9190b3c, 0x391aafc730764f78, + 0x9149afae4e7a92a4, 0xa954afbb8715d6e0, 0xe173af85dca41a2c, 0xd96eaf9015cb5e68, + 0x9a8c8904782a32ff, 0xa2918911b14576bb, 0xeab6892feaf4ba77, 0xd2ab893a239bfe33, + 0x7af889535d9723ef, 0x42e5894694f867ab, 0x0ac28978cf49ab67, 0x32df896d0626ef23, + 0x5e293551b28502d8, 0x663435447bea469c, 0x2e13357a205b8a50, 0x160e356fe934ce14, + 0xbe5d3506973813c8, 0x864035135e57578c, 0xce67352d05e69b40, 0xf67a3538cc89df04, + 0xb59813aca168b393, 0x8d8513b96807f7d7, 0xc5a2138733b63b1b, 0xfdbf1392fad97f5f, + 0x55ec13fb84d5a283, 0x6df113ee4dbae6c7, 0x25d613d0160b2a0b, 0x1dcb13c5df646e4f, + 0xa2125ef8cdc9f325, 0x9a0f5eed04a6b761, 0xd2285ed35f177bad, 0xea355ec696783fe9, + 0x42665eafe874e235, 0x7a7b5eba211ba671, 0x325c5e847aaa6abd, 0x0a415e91b3c52ef9, + 0x49a37805de24426e, 0x71be7810174b062a, 0x3999782e4cfacae6, 0x0184783b85958ea2, + 0xa9d77852fb99537e, 0x91ca784732f6173a, 0xd9ed78796947dbf6, 0xe1f0786ca0289fb2, + 0x3154aef3718177f9, 0x0949aee6b8ee33bd, 0x416eaed8e35fff71, 0x7973aecd2a30bb35, + 0xd120aea4543c66e9, 0xe93daeb19d5322ad, 0xa11aae8fc6e2ee61, 0x9907ae9a0f8daa25, + 0xdae5880e626cc6b2, 0xe2f8881bab0382f6, 0xaadf8825f0b24e3a, 0x92c2883039dd0a7e, + 0x3a91885947d1d7a2, 0x028c884c8ebe93e6, 0x4aab8872d50f5f2a, 0x72b688671c601b6e, + 0xcd6fc55a0ecd8604, 0xf572c54fc7a2c240, 0xbd55c5719c130e8c, 0x8548c564557c4ac8, + 0x2d1bc50d2b709714, 0x1506c518e21fd350, 0x5d21c526b9ae1f9c, 0x653cc53370c15bd8, + 0x26dee3a71d20374f, 0x1ec3e3b2d44f730b, 0x56e4e38c8ffebfc7, 0x6ef9e3994691fb83, + 0xc6aae3f0389d265f, 0xfeb7e3e5f1f2621b, 0xb690e3dbaa43aed7, 0x8e8de3ce632cea93, + 0xe27b5ff2d78f0768, 0xda665fe71ee0432c, 0x92415fd945518fe0, 0xaa5c5fcc8c3ecba4, + 0x020f5fa5f2321678, 0x3a125fb03b5d523c, 0x72355f8e60ec9ef0, 0x4a285f9ba983dab4, + 0x09ca790fc462b623, 0x31d7791a0d0df267, 0x79f0792456bc3eab, 0x41ed79319fd37aef, + 0xe9be7958e1dfa733, 0xd1a3794d28b0e377, 0x9984797373012fbb, 0xa1997966ba6e6bff, + 0x1e40345ba8c3f695, 0x265d344e61acb2d1, 0x6e7a34703a1d7e1d, 0x56673465f3723a59, + 0xfe34340c8d7ee785, 0xc62934194411a3c1, 0x8e0e34271fa06f0d, 0xb6133432d6cf2b49, + 0xf5f112a6bb2e47de, 0xcdec12b37241039a, 0x85cb128d29f0cf56, 0xbdd61298e09f8b12, + 0x158512f19e9356ce, 0x2d9812e457fc128a, 0x65bf12da0c4dde46, 0x5da212cfc5229a02, + 0xbc526aa3650a05b0, 0x844f6ab6ac6541f4, 0xcc686a88f7d48d38, 0xf4756a9d3ebbc97c, + 0x5c266af440b714a0, 0x643b6ae189d850e4, 0x2c1c6adfd2699c28, 0x14016aca1b06d86c, + 0x57e34c5e76e7b4fb, 0x6ffe4c4bbf88f0bf, 0x27d94c75e4393c73, 0x1fc44c602d567837, + 0xb7974c09535aa5eb, 0x8f8a4c1c9a35e1af, 0xc7ad4c22c1842d63, 0xffb04c3708eb6927, + 0x4069010a1a46f44d, 0x7874011fd329b009, 0x3053012188987cc5, 0x084e013441f73881, + 0xa01d015d3ffbe55d, 0x98000148f694a119, 0xd0270176ad256dd5, 0xe83a0163644a2991, + 0xabd827f709ab4506, 0x93c527e2c0c40142, 0xdbe227dc9b75cd8e, 0xe3ff27c9521a89ca, + 0x4bac27a02c165416, 0x73b127b5e5791052, 0x3b96278bbec8dc9e, 0x038b279e77a798da, + 0x6f7d9ba2c3047521, 0x57609bb70a6b3165, 0x1f479b8951dafda9, 0x275a9b9c98b5b9ed, + 0x8f099bf5e6b96431, 0xb7149be02fd62075, 0xff339bde7467ecb9, 0xc72e9bcbbd08a8fd, + 0x84ccbd5fd0e9c46a, 0xbcd1bd4a1986802e, 0xf4f6bd7442374ce2, 0xccebbd618b5808a6, + 0x64b8bd08f554d57a, 0x5ca5bd1d3c3b913e, 0x1482bd23678a5df2, 0x2c9fbd36aee519b6, + 0x9346f00bbc4884dc, 0xab5bf01e7527c098, 0xe37cf0202e960c54, 0xdb61f035e7f94810, + 0x7332f05c99f595cc, 0x4b2ff049509ad188, 0x0308f0770b2b1d44, 0x3b15f062c2445900, + 0x78f7d6f6afa53597, 0x40ead6e366ca71d3, 0x08cdd6dd3d7bbd1f, 0x30d0d6c8f414f95b, + 0x9883d6a18a182487, 0xa09ed6b4437760c3, 0xe8b9d68a18c6ac0f, 0xd0a4d69fd1a9e84b + }, + }; + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.cs new file mode 100644 index 00000000000..84dd209dbfb --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64.cs @@ -0,0 +1,65 @@ +/* + * Originally ported from: https://github.com/gityf/crc/blob/8045f50ba6e4193d4ee5d2539025fef26e613c9f/crc/crc64.c + * + * Copyright (c) 2012, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. */ + +// https://github.com/xamarin/java.interop/blob/7d197f17a0f9d73854522d6e1d68deafbcdbcaf6/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/Crc64.cs + +using System; +using System.Security.Cryptography; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// CRC64 variant: crc-64-jones 64-bit + /// * Poly: 0xad93d23594c935a9 + /// + /// Changes beyond initial implementation: + /// * Starting Value: ulong.MaxValue + /// * XOR length in HashFinal() + /// * Using spliced table for faster processing + /// + public partial class Crc64 : HashAlgorithm + { + ulong crc = ulong.MaxValue; + ulong length = 0; + + public override void Initialize () + { + crc = ulong.MaxValue; + length = 0; + } + + protected override unsafe void HashCore (byte [] array, int ibStart, int cbSize) + { + Crc64Helper.HashCore (array, ibStart, cbSize, ref crc, ref length); + } + + protected override byte [] HashFinal () => BitConverter.GetBytes (crc ^ length); + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64Helper.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64Helper.cs new file mode 100644 index 00000000000..decab0d019e --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Crc64Helper.cs @@ -0,0 +1,93 @@ +/* + * Originally ported from: https://github.com/gityf/crc/blob/8045f50ba6e4193d4ee5d2539025fef26e613c9f/crc/crc64.c + * + * Copyright (c) 2012, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. */ + +// https://github.com/xamarin/java.interop/blob/7d197f17a0f9d73854522d6e1d68deafbcdbcaf6/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/Crc64Helper.cs + +using System; +using System.Security.Cryptography; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// CRC64 variant: crc-64-jones 64-bit + /// * Poly: 0xad93d23594c935a9 + /// + /// Changes beyond initial implementation: + /// * Starting Value: ulong.MaxValue + /// * XOR length in HashFinal() + /// * Using spliced table for faster processing + /// + internal static partial class Crc64Helper + { + + internal static byte [] Compute (byte [] array) + { + ulong crc = ulong.MaxValue; + ulong length = 0; + + HashCore (array, 0, array.Length, ref crc, ref length); + + return BitConverter.GetBytes (crc ^ length); + } + + internal static unsafe void HashCore (byte [] array, int ibStart, int cbSize, ref ulong crc, ref ulong length) + { + int len = cbSize; + int idx = ibStart; + + fixed (ulong* tptr = table) { + fixed (byte* aptr = array) { + while (len >= 8) { + crc ^= *((ulong*) (aptr + idx)); + crc = + tptr [7 * 256 + (crc & 0xff)] ^ + tptr [6 * 256 + ((crc >> 8) & 0xff)] ^ + tptr [5 * 256 + ((crc >> 16) & 0xff)] ^ + tptr [4 * 256 + ((crc >> 24) & 0xff)] ^ + tptr [3 * 256 + ((crc >> 32) & 0xff)] ^ + tptr [2 * 256 + ((crc >> 40) & 0xff)] ^ + tptr [1 * 256 + ((crc >> 48) & 0xff)] ^ + tptr [0 * 256 + (crc >> 56)]; + idx += 8; + len -= 8; + } + + while (len > 0) { + crc = tptr [0 * 256 + ((crc ^ aptr [idx]) & 0xff)] ^ (crc >> 8); + idx++; + len--; + } + } + } + + length += (ulong) cbSize; + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Files.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Files.cs new file mode 100644 index 00000000000..bdc464185fd --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Files.cs @@ -0,0 +1,674 @@ +// https://github.com/xamarin/xamarin-android/blob/34acbbae6795854cc4e9f8eb7167ab011e0266b4/src/Xamarin.Android.Build.Tasks/Utilities/Files.cs +// https://github.com/xamarin/xamarin-android/blob/34acbbae6795854cc4e9f8eb7167ab011e0266b4/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs#L409 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using Xamarin.Tools.Zip; +using Microsoft.Build.Utilities; +using System.Threading; +using System.Runtime.InteropServices; +using System.Collections; + +namespace Microsoft.Android.Build.Tasks +{ + public static class Files + { + const int ERROR_ACCESS_DENIED = -2147024891; + const int ERROR_SHARING_VIOLATION = -2147024864; + + const int DEFAULT_FILE_WRITE_RETRY_ATTEMPTS = 10; + + const int DEFAULT_FILE_WRITE_RETRY_DELAY_MS = 1000; + + // NOTE: System.IO.Hashing.Crc64 produces different output than the Crc64 class in this repo + const int CRC64_SIZE_IN_BYTES = 8; + + static int fileWriteRetry = -1; + static int fileWriteRetryDelay = -1; + + /// + /// Windows has a MAX_PATH limit of 260 characters + /// See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + /// + public const int MaxPath = 260; + + /// + /// On Windows, we can opt into a long path with this prefix + /// + public const string LongPathPrefix = @"\\?\"; + + public static readonly Encoding UTF8withoutBOM = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false); + readonly static byte[] Utf8Preamble = Encoding.UTF8.GetPreamble (); + + /// + /// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS to + /// see if a custom value for the number of times to retry writing a file has been + /// set. + /// + /// The value of DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS or the default of DEFAULT_FILE_WRITE_RETRY_ATTEMPTS + public static int GetFileWriteRetryAttempts () + { + if (fileWriteRetry == -1) { + var retryVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS"); + if (string.IsNullOrEmpty (retryVariable) || !int.TryParse (retryVariable, out fileWriteRetry)) + fileWriteRetry = DEFAULT_FILE_WRITE_RETRY_ATTEMPTS; + } + return fileWriteRetry; + } + + /// + /// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS to + /// see if a custom value for the delay between trying to write a file has been + /// set. + /// + /// The value of DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS or the default of DEFAULT_FILE_WRITE_RETRY_DELAY_MS + public static int GetFileWriteRetryDelay () + { + if (fileWriteRetryDelay == -1) { + var delayVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS"); + if (string.IsNullOrEmpty (delayVariable) || !int.TryParse (delayVariable, out fileWriteRetryDelay)) + fileWriteRetryDelay = DEFAULT_FILE_WRITE_RETRY_DELAY_MS; + } + return fileWriteRetryDelay; + } + /// + /// Converts a full path to a \\?\ prefixed path that works on all Windows machines when over 260 characters + /// NOTE: requires a *full path*, use sparingly + /// + public static string ToLongPath (string fullPath) + { + // On non-Windows platforms, return the path unchanged + if (Path.DirectorySeparatorChar != '\\') { + return fullPath; + } + return LongPathPrefix + fullPath; + } + + public static void SetWriteable (string source, bool checkExists = true) + { + if (checkExists && !File.Exists (source)) + return; + + var attributes = File.GetAttributes (source); + if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + File.SetAttributes (source, attributes & ~FileAttributes.ReadOnly); + } + + public static void SetDirectoryWriteable (string directory) + { + if (!Directory.Exists (directory)) + return; + + var dirInfo = new DirectoryInfo (directory); + if ((dirInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + dirInfo.Attributes &= ~FileAttributes.ReadOnly; + + foreach (var dir in Directory.EnumerateDirectories (directory, "*", SearchOption.AllDirectories)) { + dirInfo = new DirectoryInfo (dir); + if ((dirInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + dirInfo.Attributes &= ~FileAttributes.ReadOnly; + } + + foreach (var file in Directory.EnumerateFiles (directory, "*", SearchOption.AllDirectories)) { + Files.SetWriteable (Path.GetFullPath (file)); + } + } + + public static bool Archive (string target, Action archiver) + { + string newTarget = target + ".new"; + + archiver (newTarget); + + bool changed = CopyIfChanged (newTarget, target); + + try { + File.Delete (newTarget); + } catch { + } + + return changed; + } + + public static bool ArchiveZipUpdate (string target, Action archiver) + { + var lastWrite = File.Exists (target) ? File.GetLastWriteTimeUtc (target) : DateTime.MinValue; + archiver (target); + return lastWrite < File.GetLastWriteTimeUtc (target); + } + + public static bool ArchiveZip (string target, Action archiver) + { + string newTarget = target + ".new"; + + archiver (newTarget); + + bool changed = CopyIfZipChanged (newTarget, target); + + try { + File.Delete (newTarget); + } catch { + } + + return changed; + } + + public static bool CopyIfChanged (string source, string destination) + { + int retryCount = 0; + int attempts = GetFileWriteRetryAttempts (); + int delay = GetFileWriteRetryDelay (); + while (retryCount <= attempts) { + try { + return CopyIfChangedOnce (source, destination); + } catch (Exception e) { + switch (e) { + case UnauthorizedAccessException: + case IOException: + int code = Marshal.GetHRForException (e); + if ((code != ERROR_ACCESS_DENIED && code != ERROR_SHARING_VIOLATION) || retryCount == attempts) { + throw; + }; + break; + default: + throw; + } + } + retryCount++; + Thread.Sleep (delay); + } + return false; + } + + public static bool CopyIfChangedOnce (string source, string destination) + { + if (HasFileChanged (source, destination)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + if (!Directory.Exists (source)) { + if (File.Exists (destination)) { + SetWriteable (destination, checkExists: false); + File.Delete (destination); + } + File.Copy (source, destination, overwrite: true); + SetWriteable (destination, checkExists: false); + File.SetLastWriteTimeUtc (destination, DateTime.UtcNow); + return true; + } + } + + return false; + } + + public static bool CopyIfStringChanged (string contents, string destination) + { + //NOTE: this is not optimal since it allocates a byte[]. We can improve this down the road with Span or System.Buffers. + var bytes = Encoding.UTF8.GetBytes (contents); + return CopyIfBytesChanged (bytes, destination); + } + + public static bool CopyIfBytesChanged (byte[] bytes, string destination) + { + if (HasBytesChanged (bytes, destination)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + if (File.Exists (destination)) { + SetWriteable (destination, checkExists: false); + File.Delete (destination); + } + File.WriteAllBytes (destination, bytes); + return true; + } + return false; + } + + public static bool CopyIfStreamChanged (Stream stream, string destination) + { + int retryCount = 0; + int attempts = GetFileWriteRetryAttempts (); + int delay = GetFileWriteRetryDelay (); + while (retryCount <= attempts) { + try { + return CopyIfStreamChangedOnce (stream, destination); + } catch (Exception e) { + switch (e) { + case UnauthorizedAccessException: + case IOException: + int code = Marshal.GetHRForException (e); + if ((code != ERROR_ACCESS_DENIED && code != ERROR_SHARING_VIOLATION) || retryCount >= attempts) { + throw; + }; + break; + default: + throw; + } + } + retryCount++; + Thread.Sleep (delay); + } + return false; + } + + public static bool CopyIfStreamChangedOnce (Stream stream, string destination) + { + if (HasStreamChanged (stream, destination)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + if (File.Exists (destination)) { + SetWriteable (destination, checkExists: false); + File.Delete (destination); + } + using (var fileStream = File.Create (destination)) { + stream.Position = 0; //HasStreamChanged read to the end + stream.CopyTo (fileStream); + } + return true; + } + return false; + } + + public static bool CopyIfZipChanged (Stream source, string destination) + { + if (HasZipChanged (source, destination, out _)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + source.Position = 0; + using (var f = File.Create (destination)) { + source.CopyTo (f); + } + File.SetLastWriteTimeUtc (destination, DateTime.UtcNow); + return true; + }/* else + Console.WriteLine ("Skipping copying {0}, unchanged", Path.GetFileName (destination));*/ + + return false; + } + + public static bool CopyIfZipChanged (string source, string destination) + { + if (HasZipChanged (source, destination, out _)) { + var directory = Path.GetDirectoryName (destination); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + File.Copy (source, destination, true); + File.SetLastWriteTimeUtc (destination, DateTime.UtcNow); + return true; + } + + return false; + } + + public static bool HasZipChanged (Stream source, string destination, out string? hash) + { + hash = null; + + string? src_hash = hash = HashZip (source); + + if (!File.Exists (destination)) + return true; + + string? dst_hash = HashZip (destination); + + if (src_hash == null || dst_hash == null) + return true; + + return src_hash != dst_hash; + } + + public static bool HasZipChanged (string source, string destination, out string? hash) + { + hash = null; + if (!File.Exists (source)) + return true; + + string? src_hash = hash = HashZip (source); + + if (!File.Exists (destination)) + return true; + + string? dst_hash = HashZip (destination); + + if (src_hash == null || dst_hash == null) + return true; + + return src_hash != dst_hash; + } + + // This is for if the file contents have changed. Often we have to + // regenerate a file, but we don't want to update it if hasn't changed + // so that incremental build is as efficient as possible + public static bool HasFileChanged (string source, string destination) + { + // If either are missing, that's definitely a change + if (!File.Exists (source) || !File.Exists (destination)) + return true; + + var src_hash = HashFile (source); + var dst_hash = HashFile (destination); + + // If the hashes don't match, then the file has changed + if (src_hash != dst_hash) + return true; + + return false; + } + + public static bool HasStreamChanged (Stream source, string destination) + { + //If destination is missing, that's definitely a change + if (!File.Exists (destination)) + return true; + + var src_hash = HashStream (source); + var dst_hash = HashFile (destination); + + // If the hashes don't match, then the file has changed + if (src_hash != dst_hash) + return true; + + return false; + } + + public static bool HasBytesChanged (byte [] bytes, string destination) + { + //If destination is missing, that's definitely a change + if (!File.Exists (destination)) + return true; + + var src_hash = HashBytes (bytes); + var dst_hash = HashFile (destination); + + // If the hashes don't match, then the file has changed + if (src_hash != dst_hash) + return true; + + return false; + } + + static string? HashZip (Stream stream) + { + string hashes = String.Empty; + + try { + using (var zip = ZipArchive.Open (stream)) { + foreach (var item in zip) { + hashes += String.Format ("{0}{1}", item.FullName, item.CRC); + } + } + } catch { + return null; + } + return hashes; + } + + static string? HashZip (string filename) + { + string hashes = String.Empty; + + try { + // check cache + if (File.Exists (filename + ".hash")) + return File.ReadAllText (filename + ".hash"); + + using (var zip = ReadZipFile (filename)) { + foreach (var item in zip) { + hashes += String.Format ("{0}{1}", item.FullName, item.CRC); + } + } + } catch { + return null; + } + return hashes; + } + + public static ZipArchive ReadZipFile (string filename, bool strictConsistencyChecks = false) + { + return ZipArchive.Open (filename, FileMode.Open, strictConsistencyChecks: strictConsistencyChecks); + } + + public static bool ZipAny (string filename, Func filter) + { + using (var zip = ReadZipFile (filename)) { + return zip.Any (filter); + } + } + + [Obsolete ("Use the overload that accepts a TaskLoggingHelper parameter.")] + public static bool ExtractAll (ZipArchive zip, string destination, Action? progressCallback, + Func? modifyCallback, Func? deleteCallback, Func? skipCallback) + { + return ExtractAll (zip, destination, progressCallback, modifyCallback, deleteCallback, skipCallback, log: null); + } + + public static bool ExtractAll (ZipArchive zip, string destination, Action? progressCallback = null, Func? modifyCallback = null, + Func? deleteCallback = null, Func? skipCallback = null, TaskLoggingHelper? log = null) + { + int i = 0; + int total = (int)zip.EntryCount; + bool updated = false; + var files = new HashSet (); + var memoryStream = MemoryStreamPool.Shared.Rent (); + var fullDestination = Path.GetFullPath (destination + Path.DirectorySeparatorChar); + try { + foreach (var entry in zip) { + progressCallback?.Invoke (i++, total); + if (entry.IsDirectory) + continue; + if (entry.FullName.Contains ("/__MACOSX/") || + entry.FullName.EndsWith ("/__MACOSX", StringComparison.OrdinalIgnoreCase) || + string.Equals (entry.FullName, ".DS_Store", StringComparison.OrdinalIgnoreCase) || + entry.FullName.EndsWith ("/.DS_Store", StringComparison.OrdinalIgnoreCase)) + continue; + if (skipCallback != null && skipCallback (entry.FullName)) + continue; + var fullName = modifyCallback?.Invoke (entry.FullName) ?? entry.FullName; + var outfile = Path.GetFullPath (Path.Combine (destination, fullName)); + if (!outfile.StartsWith (fullDestination, StringComparison.OrdinalIgnoreCase)) { + log?.LogDebugMessage ($"Skipping zip entry \"{entry.FullName}\" (resolved as \"{fullName}\") because it would extract outside the destination directory: \"{outfile}\"."); + continue; + } + files.Add (outfile); + memoryStream.SetLength (0); //Reuse the stream + entry.Extract (memoryStream); + try { + updated |= CopyIfStreamChanged (memoryStream, outfile); + } catch (PathTooLongException) { + throw new PathTooLongException ($"Could not extract \"{fullName}\" to \"{outfile}\". Path is too long."); + } + } + } finally { + MemoryStreamPool.Shared.Return (memoryStream); + } + if (Directory.Exists (destination)) { + foreach (var file in Directory.GetFiles (destination, "*", SearchOption.AllDirectories)) { + var outfile = Path.GetFullPath (file); + if (outfile.Contains ("/__MACOSX/") || + outfile.EndsWith (".flat", StringComparison.OrdinalIgnoreCase) || + outfile.EndsWith ("files.cache", StringComparison.OrdinalIgnoreCase) || + outfile.EndsWith ("__AndroidLibraryProjects__.zip", StringComparison.OrdinalIgnoreCase) || + outfile.EndsWith ("/__MACOSX", StringComparison.OrdinalIgnoreCase) || + outfile.EndsWith ("/.DS_Store", StringComparison.OrdinalIgnoreCase)) + continue; + if (!files.Contains (outfile) && (deleteCallback?.Invoke (outfile) ?? true)) { + File.Delete (outfile); + updated = true; + } + } + } + return updated; + } + + /// + /// Callback that can be used in combination with ExtractAll for extracting .aar files + /// + public static bool ShouldSkipEntryInAar (string entryFullName) + { + // AAR files may contain other jars not needed for compilation + // See: https://developer.android.com/studio/projects/android-library.html#aar-contents + if (!entryFullName.EndsWith (".jar", StringComparison.OrdinalIgnoreCase)) + return false; + if (entryFullName == "classes.jar" || + entryFullName.StartsWith ("libs/", StringComparison.OrdinalIgnoreCase) || + entryFullName.StartsWith ("libs\\", StringComparison.OrdinalIgnoreCase)) + return false; + // This could be `lint.jar` or `api.jar`, etc. + return true; + } + + public static string HashString (string s) + { + var bytes = Encoding.UTF8.GetBytes (s); + return HashBytes (bytes); + } + + public static string HashBytes (byte [] bytes) + { + Span hash = stackalloc byte[CRC64_SIZE_IN_BYTES]; + // NOTE: System.IO.Hashing.Crc64 produces different output than the Crc64 class in this repo + System.IO.Hashing.Crc64.Hash (bytes, hash); + XorLength (hash, (ulong) bytes.Length); + return ToHexString (hash); + } + + public static string HashFile (string filename) + { + // NOTE: System.IO.Hashing.Crc64 produces different output than the Crc64 class in this repo + var hasher = new System.IO.Hashing.Crc64 (); + long length; + using (var file = File.OpenRead (filename)) { + hasher.Append (file); + length = file.Length; + } + Span hash = stackalloc byte[CRC64_SIZE_IN_BYTES]; + hasher.GetCurrentHash (hash); + XorLength (hash, (ulong) length); + return ToHexString (hash); + } + + public static string HashFile (string filename, HashAlgorithm hashAlg) + { + using (var file = File.OpenRead (filename)) { + byte[] hash = hashAlg.ComputeHash (file); + return ToHexString (hash); + } + } + + public static string HashStream (Stream stream) + { + stream.Position = 0; + // NOTE: System.IO.Hashing.Crc64 produces different output than the Crc64 class in this repo + var hasher = new System.IO.Hashing.Crc64 (); + hasher.Append (stream); + Span hash = stackalloc byte[CRC64_SIZE_IN_BYTES]; + hasher.GetCurrentHash (hash); + XorLength (hash, (ulong) stream.Length); + return ToHexString (hash); + } + + /// XOR the data length into the hash to avoid collisions on zero-filled inputs. + static void XorLength (Span hash, ulong length) + { + ref var crc = ref Unsafe.As (ref hash [0]); + crc ^= length; + } + + public static string ToHexString (byte[] hash) + { + if (hash == null) + throw new ArgumentNullException (nameof (hash)); + return ToHexString ((ReadOnlySpan) hash); + } + + public static string ToHexString (ReadOnlySpan hash) + { + const int MaxStackCharLength = 128; + int charLength = hash.Length * 2; + Span chars = charLength <= MaxStackCharLength + ? stackalloc char[charLength] + : new char[charLength]; + for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { + byte b = hash [i]; + chars [j] = GetHexValue (b / 16); + chars [j + 1] = GetHexValue (b % 16); + } + return ((ReadOnlySpan) chars).ToString (); + } + + static char GetHexValue (int i) => (char) (i < 10 ? i + 48 : i - 10 + 65); + + public static void DeleteFile (string filename, object log) + { + try { + File.Delete (filename); + } catch (Exception ex) { + if (log is TaskLoggingHelper helper) { + helper.LogErrorFromException (ex); + } + } + } + + const uint ppdb_signature = 0x424a5342; + + public static bool IsPortablePdb (string filename) + { + try { + using (var fs = new FileStream (filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { + using (var br = new BinaryReader (fs)) { + return br.ReadUInt32 () == ppdb_signature; + } + } + } + catch { + return false; + } + } + + /// + /// Open a file given its path and remove the 3 bytes UTF-8 BOM if there is one + /// + public static void CleanBOM (string filePath) + { + if (string.IsNullOrEmpty (filePath) || !File.Exists (filePath)) + return; + + string? temp = null; + try { + using (var input = File.OpenRead (filePath)) { + // Check if the file actually has a BOM + for (int i = 0; i < Utf8Preamble.Length; i++) { + var next = input.ReadByte (); + if (next == -1) + return; + if (Utf8Preamble [i] != (byte) next) + return; + } + + temp = Path.GetTempFileName (); + using (var stream = File.OpenWrite (temp)) + input.CopyTo (stream); + } + + Files.SetWriteable (filePath); + File.Delete (filePath); + File.Copy (temp, filePath); + } finally { + if (temp != null) { + File.Delete (temp); + } + } + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/LinePreservedXmlWriter.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/LinePreservedXmlWriter.cs new file mode 100644 index 00000000000..d2cd82e1d39 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/LinePreservedXmlWriter.cs @@ -0,0 +1,146 @@ +// https://github.com/xamarin/xamarin-android/blob/2aea0af1da5c46924dd00587701b6c91391d62f8/src/Xamarin.Android.Build.Tasks/Utilities/LinePreservedXmlWriter.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.XPath; + +namespace Microsoft.Android.Build.Tasks +{ + public class LinePreservedXmlWriter : XmlTextWriter + { + LinePreservedTextWriter tw; + + static LinePreservedTextWriter GetLinePreservedTextWriter (TextWriter w) + { + var tw = w as LinePreservedTextWriter; + if (tw == null) + tw = new LinePreservedTextWriter (w); + return tw; + } + + public LinePreservedXmlWriter (TextWriter w) + : this (GetLinePreservedTextWriter (w)) + { + } + + internal LinePreservedXmlWriter (LinePreservedTextWriter w) + : base (w) + { + this.tw = w; + } + + XPathNavigator? nav; + + public override void WriteNode (XPathNavigator navigator, bool defattr) + { + XPathNavigator? bak = nav; + this.nav = navigator; + IXmlLineInfo? li = navigator as IXmlLineInfo; + if (li != null) + tw.ProceedTo (li.LineNumber, li.LinePosition); + base.WriteNode (navigator, defattr); + this.nav = bak; + } + + public override void WriteStartAttribute (string prefix, string localName, string namespaceUri) + { + if (nav != null) + Proceed (nav as IXmlLineInfo); + base.WriteStartAttribute (prefix, localName, namespaceUri); + } + + public override void WriteStartElement (string prefix, string localName, string namespaceUri) + { + if (nav != null) + Proceed (nav as IXmlLineInfo); + base.WriteStartElement (prefix, localName, namespaceUri); + } + + void Proceed (IXmlLineInfo? li) + { + if (li == null || !li.HasLineInfo ()) + return; + tw.ProceedTo (li.LineNumber, li.LinePosition); + } + } + + class LinePreservedTextWriter : TextWriter + { + TextWriter w; + int line = 1; + + public LinePreservedTextWriter (TextWriter w) + { + this.w = w; + } + + public override System.Text.Encoding Encoding { + get { return Encoding.Unicode; } + } + + public void ProceedTo (int line, int column) + { + if (line <= 0) + return; + bool wrote = this.line < line; + while (this.line < line) + WriteLine (); + if (wrote) + Write (new string (' ', column)); + } + + public override void Close () + { + w.Close (); + } + + public override void Flush () + { + w.Flush (); + } + + public override void Write (char value) + { + w.Write (value); + if (value == '\n') + line++; + } + + public override void Write (char[] buffer, int index, int count) + { + w.Write (buffer, index, count); + int next = index; + while (next < index + count) { + int idx = Array.IndexOf (buffer, '\n', next, count + (index - next)); + if (idx < 0) + break; + line++; + next = idx + 1; + } + } + + public override void Write (string value) + { + w.Write (value); + int next = 0; + while (next < value.Length) { + int idx = value.IndexOf ('\n', next); + if (idx < 0) + break; + line++; + next = idx + 1; + } + } + + public override void WriteLine () + { + w.WriteLine (); + line++; + } + } +} + diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs new file mode 100644 index 00000000000..dc1f55a17bd --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs @@ -0,0 +1,331 @@ +// https://github.com/xamarin/xamarin-android/blob/eed430e4dc442ee98046fb13956ef49f29ce7b40/src/Xamarin.Android.Build.Tasks/Utilities/MSBuildExtensions.cs + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Text.RegularExpressions; + +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Build.Tasks +{ + public static class MSBuildExtensions + { + public static void LogDebugMessage (this TaskLoggingHelper log, string message, params object[] messageArgs) + { + log.LogMessage (MessageImportance.Low, message, messageArgs); + } + + public static void LogTaskItems (this TaskLoggingHelper log, string message, ITaskItem[] items) + { + log.LogMessage (message); + + if (items == null) + return; + + foreach (var item in items) + log.LogMessage (" {0}", item.ItemSpec); + } + + public static void LogTaskItems (this TaskLoggingHelper log, string message, params string[] items) + { + log.LogMessage (message); + + if (items == null) + return; + + foreach (var item in items) + log.LogMessage (" {0}", item); + } + + public static void LogDebugTaskItems (this TaskLoggingHelper log, string message, ITaskItem[] items, bool logMetadata = false) + { + log.LogMessage (MessageImportance.Low, message); + + if (items == null) + return; + + foreach (var item in items) { + log.LogMessage (MessageImportance.Low, " {0}", item.ItemSpec); + if (!logMetadata || item.MetadataCount <= 0) + continue; + foreach (string name in item.MetadataNames) + log.LogMessage (MessageImportance.Low, $" {name} = {item.GetMetadata (name)}"); + } + } + + public static void LogDebugTaskItems (this TaskLoggingHelper log, string message, params string[] items) + { + log.LogMessage (MessageImportance.Low, message); + + if (items == null) + return; + + foreach (var item in items) + log.LogMessage (MessageImportance.Low, " {0}", item); + } + + // looking for: mandroid: warning XA9000: message... + static readonly Regex Message = new Regex ( + @"^(?[^: ]+)\s*:\s*(?warning|error) (?[^:]+): (?.*)"); + + public static void LogFromStandardError (this TaskLoggingHelper log, string defaultErrorCode, string message) + { + if (string.IsNullOrEmpty (message)) + return; + + var m = Message.Match (message); + if (!m.Success) { + if (message.IndexOf ("error:", StringComparison.InvariantCultureIgnoreCase) != -1) { + log.LogCodedError (defaultErrorCode, message); + } else { + log.LogMessage (null, defaultErrorCode, null, null, 0, 0, 0, 0, MessageImportance.Low, message); + } + return; + } + + string subcategory = m.Groups ["source"].Value; + string type = m.Groups ["type"].Value; + string code = m.Groups ["code"].Value; + string msg = m.Groups ["message"].Value; + + if (string.IsNullOrEmpty (code)) + code = defaultErrorCode; + + if (type == "warning") + log.LogWarning (subcategory, code, string.Empty, string.Empty, 0, 0, 0, 0, "{0}", msg); + else + log.LogError (subcategory, code, string.Empty, string.Empty, 0, 0, 0, 0, "{0}", msg); + } + + public static void LogDebugTaskItemsAndLogical (this TaskLoggingHelper log, string message, ITaskItem[] items) + { + log.LogMessage (MessageImportance.Low, message); + + if (items == null) + return; + + foreach (var item in items) { + log.LogMessage (MessageImportance.Low, " {0}", item.ItemSpec); + log.LogMessage (MessageImportance.Low, " [{0}]", item.GetMetadata ("LogicalName")); + } + } + + public static void LogCodedError (this TaskLoggingHelper log, string code, string message, params object[] messageArgs) + { + log.LogError (string.Empty, code, string.Empty, string.Empty, 0, 0, 0, 0, message, messageArgs); + } + + public static void LogCodedError (this TaskLoggingHelper log, string code, string file, int lineNumber, string message, params object[] messageArgs) + { + log.LogError (string.Empty, code, string.Empty, file, lineNumber, 0, 0, 0, message, messageArgs); + } + + public static void LogCodedWarning (this TaskLoggingHelper log, string code, string message, params object [] messageArgs) + { + log.LogWarning (string.Empty, code, string.Empty, string.Empty, 0, 0, 0, 0, message, messageArgs); + } + + public static void LogCodedWarning (this TaskLoggingHelper log, string code, string file, int lineNumber, string message, params object [] messageArgs) + { + log.LogWarning (string.Empty, code, string.Empty, file, lineNumber, 0, 0, 0, message, messageArgs); + } + + /// + /// Logs a coded error from a node in an XML document + /// + /// An element that implements IXmlLineInfo + public static void LogErrorForXmlNode (this TaskLoggingHelper log, string code, string file, object node, string message, params object [] messageArgs) + { + int lineNumber = 0; + int columnNumber = 0; + var lineInfo = node as IXmlLineInfo; + if (lineInfo != null && lineInfo.HasLineInfo ()) { + lineNumber = lineInfo.LineNumber; + columnNumber = lineInfo.LinePosition; + } + log.LogError ( + subcategory: string.Empty, + errorCode: code, + helpKeyword: string.Empty, + file: file, + lineNumber: lineNumber, + columnNumber: columnNumber, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + messageArgs: messageArgs + ); + } + + /// + /// Logs a coded warning from a node in an XML document + /// + /// An element that implements IXmlLineInfo + public static void LogWarningForXmlNode (this TaskLoggingHelper log, string code, string file, object node, string message, params object [] messageArgs) + { + int lineNumber = 0; + int columnNumber = 0; + var lineInfo = node as IXmlLineInfo; + if (lineInfo != null && lineInfo.HasLineInfo ()) { + lineNumber = lineInfo.LineNumber; + columnNumber = lineInfo.LinePosition; + } + log.LogWarning ( + subcategory: string.Empty, + warningCode: code, + helpKeyword: string.Empty, + file: file, + lineNumber: lineNumber, + columnNumber: columnNumber, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + messageArgs: messageArgs + ); + } + + public static Action CreateTaskLogger (this Task task) + { + Action logger = (level, value) => { + switch (level) { + case TraceLevel.Error: + task.Log.LogError ("{0}", value); + break; + case TraceLevel.Warning: + task.Log.LogWarning ("{0}", value); + break; + default: + task.Log.LogDebugMessage ("{0}", value); + break; + } + }; + return logger; + } + + public static Action CreateTaskLogger (this AsyncTask task) + { + Action logger = (level, value) => { + switch (level) { + case TraceLevel.Error: + task.LogError (value); + break; + case TraceLevel.Warning: + task.LogWarning (value); + break; + default: + task.LogDebugMessage (value); + break; + } + }; + return logger; + } + + + public static IEnumerable Concat (params ITaskItem[][] values) + { + if (values == null) + yield break; + foreach (ITaskItem[] v in values) { + if (v == null) + continue; + foreach (ITaskItem t in v) + yield return t; + } + } + + public static string FixupResourceFilename (string file, string resourceDir, Dictionary resourceNameCaseMap) + { + var targetfile = file; + if (resourceDir != null && targetfile.StartsWith (resourceDir, StringComparison.InvariantCultureIgnoreCase)) { + targetfile = file.Substring (resourceDir.Length).TrimStart (Path.DirectorySeparatorChar); + if (resourceNameCaseMap.TryGetValue (targetfile, out string temp)) + targetfile = temp; + targetfile = Path.Combine ("Resources", targetfile); + } + return targetfile; + } + + public static void FixupResourceFilenameAndLogCodedError (this TaskLoggingHelper log, string code, string message, string file, string resourceDir, Dictionary resourceNameCaseMap) + { + var targetfile = FixupResourceFilename (file, resourceDir, resourceNameCaseMap); + log.LogCodedError (code, file: targetfile, lineNumber: 0, message: message); + } + + public static void FixupResourceFilenameAndLogCodedWarning (this TaskLoggingHelper log, string code, string message, string file, string resourceDir, Dictionary resourceNameCaseMap) + { + var targetfile = FixupResourceFilename (file, resourceDir, resourceNameCaseMap); + log.LogCodedWarning (code, file: targetfile, lineNumber: 0, message: message); + } + + /// + /// Sets the default value for %(DestinationSubPath) if it is not already set + /// + public static void SetDestinationSubPath (this ITaskItem assembly) + { + if (string.IsNullOrEmpty (assembly.GetMetadata ("DestinationSubPath"))) { + var directory = assembly.GetMetadata ("DestinationSubDirectory"); + var path = Path.Combine (directory, Path.GetFileName (assembly.ItemSpec)); + assembly.SetMetadata ("DestinationSubPath", path); + } + } + + static readonly string AssemblyLocation = typeof (MSBuildExtensions).Assembly.Location; + + /// + /// IBuildEngine4.RegisterTaskObject, but adds the current assembly path into the key + /// The `key` should be unique to a project unless it is a global item. + /// Ideally the key should be the full path of a file in the project directory structure. + /// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate + /// a project specific key if needed. + /// + public static void RegisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, object value, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection = false) => + engine.RegisterTaskObject ((AssemblyLocation, key), value, lifetime, allowEarlyCollection); + + /// + /// IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key + /// The `key` should be unique to a project unless it is a global item. + /// Ideally the key should be the full path of a file in the project directory structure. + /// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate + /// a project specific key if needed. + /// + public static object GetRegisteredTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) => + engine.GetRegisteredTaskObject ((AssemblyLocation, key), lifetime); + + /// + /// Generic version of IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key + /// The `key` should be unique to a project unless it is a global item. + /// Ideally the key should be the full path of a file in the project directory structure. + /// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate + /// a project specific key if needed. + /// + public static T? GetRegisteredTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) + where T : class => + engine.GetRegisteredTaskObject ((AssemblyLocation, key), lifetime) as T; + + + /// + /// IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key + /// The `key` should be unique to a project unless it is a global item. + /// Ideally the key should be the full path of a file in the project directory structure. + /// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate + /// a project specific key if needed. + /// + public static object UnregisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) => + engine.UnregisterTaskObject ((AssemblyLocation, key), lifetime); + + /// + /// Generic version of IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key. + /// The `key` should be unique to a project unless it is a global item. + /// Ideally the key should be the full path of a file in the project directory structure. + /// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate + /// a project specific key if needed. + /// + public static T? UnregisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) + where T : class => + engine.UnregisterTaskObject ((AssemblyLocation, key), lifetime) as T; + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems new file mode 100644 index 00000000000..e5ccd74facc --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems @@ -0,0 +1,24 @@ + + + + + + + 18.7.1 + 10.0.4 + 3.3.0 + 7.1.0-final.1.21458.1 + + + + + + + + + + + + + + diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MemoryStreamPool.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MemoryStreamPool.cs new file mode 100644 index 00000000000..2a8174a5f5d --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/MemoryStreamPool.cs @@ -0,0 +1,105 @@ +// https://github.com/xamarin/xamarin-android/blob/c9e5f58d2fa92283917ae4e8f9c370adfd40b908/src/Xamarin.Android.Build.Tasks/Utilities/MemoryStreamPool.cs + +using System.IO; +using System.Text; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// A class for pooling and reusing MemoryStream objects. + /// + /// Based on: + /// https://docs.microsoft.com/dotnet/standard/collections/thread-safe/how-to-create-an-object-pool + /// https://docs.microsoft.com/dotnet/api/system.buffers.arraypool-1 + /// + public class MemoryStreamPool : ObjectPool + { + public static readonly Encoding UTF8withoutBOM = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false); + + /// + /// Static instance across the entire process. Use this most of the time. + /// + public static readonly MemoryStreamPool Shared = new MemoryStreamPool (); + + public MemoryStreamPool () : base (() => new MemoryStream ()) { } + + public override void Return (MemoryStream stream) + { + // We want to throw here before base.Return() if it was disposed + stream.SetLength (0); + base.Return (stream); + } + + /// + /// Creates a StreamWriter that uses the underlying MemoryStreamPool. Calling Dispose() will Return() the MemoryStream. + /// By default uses MonoAndroidHelper.UTF8withoutBOM for the encoding. + /// + public StreamWriter CreateStreamWriter () => CreateStreamWriter (Files.UTF8withoutBOM); + + /// + /// Creates a StreamWriter that uses the underlying MemoryStreamPool. Calling Dispose() will Return() the MemoryStream. + /// + public StreamWriter CreateStreamWriter (Encoding encoding) => new ReturningStreamWriter (this, Rent (), encoding); + + /// + /// Creates a BinaryWriter that uses the underlying MemoryStreamPool. Calling Dispose() will Return() the MemoryStream. + /// By default uses MonoAndroidHelper.UTF8withoutBOM for the encoding. + /// + public BinaryWriter CreateBinaryWriter () => CreateBinaryWriter (Files.UTF8withoutBOM); + + /// + /// Creates a BinaryWriter that uses the underlying MemoryStreamPool. Calling Dispose() will Return() the MemoryStream. + /// + public BinaryWriter CreateBinaryWriter (Encoding encoding) => new ReturningBinaryWriter (this, Rent (), encoding); + + class ReturningStreamWriter : StreamWriter + { + readonly MemoryStreamPool pool; + readonly MemoryStream stream; + bool returned; + + public ReturningStreamWriter (MemoryStreamPool pool, MemoryStream stream, Encoding encoding) + : base (stream, encoding, bufferSize: 8 * 1024, leaveOpen: true) + { + this.pool = pool; + this.stream = stream; + } + + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + + //NOTE: Dispose() can be called multiple times + if (disposing && !returned) { + returned = true; + pool.Return (stream); + } + } + } + + class ReturningBinaryWriter : BinaryWriter + { + readonly MemoryStreamPool pool; + readonly MemoryStream stream; + bool returned; + + public ReturningBinaryWriter (MemoryStreamPool pool, MemoryStream stream, Encoding encoding) + : base (stream, encoding, leaveOpen: true) + { + this.pool = pool; + this.stream = stream; + } + + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + + //NOTE: Dispose() can be called multiple times + if (disposing && !returned) { + returned = true; + pool.Return (stream); + } + } + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj new file mode 100644 index 00000000000..6a23bfb7999 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj @@ -0,0 +1,34 @@ + + + + + + netstandard2.0 + Microsoft.Android.Build.Tasks + true + true + ..\..\product.snk + $(VendorPrefix)Microsoft.Android.Build.BaseTasks$(VendorSuffix) + latest + enable + nullable + + + + + True + True + Resources.resx + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/ObjectPool.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/ObjectPool.cs new file mode 100644 index 00000000000..91018c17bd1 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/ObjectPool.cs @@ -0,0 +1,39 @@ +// https://github.com/xamarin/xamarin-android/blob/87ee20c59a09ba4314f4226e1c77ee2331c3f09f/src/Xamarin.Android.Build.Tasks/Utilities/ObjectPool.cs + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// A class for pooling and reusing objects. See MemoryStreamPool. + /// + /// Based on: + /// https://docs.microsoft.com/dotnet/standard/collections/thread-safe/how-to-create-an-object-pool + /// https://docs.microsoft.com/dotnet/api/system.buffers.arraypool-1 + /// + public class ObjectPool + { + readonly ConcurrentBag bag = new ConcurrentBag(); + readonly Func generator; + + public ObjectPool (Func generator) + { + if (generator == null) + throw new ArgumentNullException (nameof (generator)); + this.generator = generator; + } + + public virtual T Rent () + { + if (bag.TryTake (out T item)) + return item; + return generator (); + } + + public virtual void Return (T item) + { + bag.Add (item); + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.Designer.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..1795a033042 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Android.Build.Tasks.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Android.Build.Tasks.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unhandled exception: {0}. + /// + internal static string XA0000_Exception { + get { + return ResourceManager.GetString("XA0000_Exception", resourceCulture); + } + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.cs.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.cs.resx new file mode 100644 index 00000000000..59e52f6cd5f --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.cs.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Neošetřená výjimka: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.de.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.de.resx new file mode 100644 index 00000000000..bebc92ad434 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.de.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ausnahmefehler: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.es.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.es.resx new file mode 100644 index 00000000000..846b2488fb9 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.es.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Excepción no controlada: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.fr.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.fr.resx new file mode 100644 index 00000000000..0a514e6b51c --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.fr.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Exception non prise en charge{0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.it.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.it.resx new file mode 100644 index 00000000000..8a6d245903a --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.it.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Eccezione non gestita: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ja.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ja.resx new file mode 100644 index 00000000000..329f20e9980 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ja.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ハンドルされない例外: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ko.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ko.resx new file mode 100644 index 00000000000..b0a8db624d4 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ko.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 처리되지 않은 예외: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pl.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pl.resx new file mode 100644 index 00000000000..a31add32af6 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pl.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nieobsługiwany wyjątek: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pt-BR.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pt-BR.resx new file mode 100644 index 00000000000..79ad207a084 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.pt-BR.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Exceção não tratada: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx new file mode 100644 index 00000000000..c8a29d55556 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.resx @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unhandled exception: {0} + {0} - The exception message of the associated exception + + diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ru.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ru.resx new file mode 100644 index 00000000000..18b77ef1a2c --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.ru.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Необработанное исключение: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.tr.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.tr.resx new file mode 100644 index 00000000000..934bb43f582 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.tr.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + İşlenmeyen özel durum: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hans.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hans.resx new file mode 100644 index 00000000000..ee9e25027e2 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hans.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 未处理的异常: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hant.resx b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hant.resx new file mode 100644 index 00000000000..f7abee57c3b --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/Properties/Resources.zh-Hant.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 未處理的例外狀況: {0} + {0} - The exception message of the associated exception + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs new file mode 100644 index 00000000000..06a34fe9a96 --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs @@ -0,0 +1,114 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Utilities/UnhandledExceptionLogger.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Android.Build.Tasks +{ + public static class UnhandledExceptionLogger + { + public static void LogUnhandledException (this AsyncTask task, string prefix, Exception ex) + { + LogUnhandledException ((code, message) => task.LogCodedError (code, message), prefix, ex); + } + + public static void LogUnhandledException (this TaskLoggingHelper log, string prefix, Exception ex) + { + LogUnhandledException ((code, message) => log.LogCodedError (code, message), prefix, ex); + } + + static void LogUnhandledException (Action logCodedError, string prefix, Exception ex) + { + prefix = "XA" + prefix; + + // Some ordering is necessary here to ensure exceptions are before their base exceptions + if (ex is NullReferenceException) + logCodedError (prefix + "7001", ex.ToString ()); + else if (ex is ArgumentOutOfRangeException) // ArgumentException + logCodedError (prefix + "7002", ex.ToString ()); + else if (ex is ArgumentNullException) // ArgumentException + logCodedError (prefix + "7003", ex.ToString ()); + else if (ex is ArgumentException) + logCodedError (prefix + "7004", ex.ToString ()); + else if (ex is FormatException) + logCodedError (prefix + "7005", ex.ToString ()); + else if (ex is IndexOutOfRangeException) + logCodedError (prefix + "7006", ex.ToString ()); + else if (ex is InvalidCastException) + logCodedError (prefix + "7007", ex.ToString ()); + else if (ex is ObjectDisposedException) // InvalidOperationException + logCodedError (prefix + "7008", ex.ToString ()); + else if (ex is InvalidOperationException) + logCodedError (prefix + "7009", ex.ToString ()); + else if (ex is InvalidProgramException) + logCodedError (prefix + "7010", ex.ToString ()); + else if (ex is KeyNotFoundException) + logCodedError (prefix + "7011", ex.ToString ()); + else if (ex is TaskCanceledException) // OperationCanceledException + logCodedError (prefix + "7012", ex.ToString ()); + else if (ex is OperationCanceledException) + logCodedError (prefix + "7013", ex.ToString ()); + else if (ex is OutOfMemoryException) + logCodedError (prefix + "7014", ex.ToString ()); + else if (ex is NotSupportedException) + logCodedError (prefix + "7015", ex.ToString ()); + else if (ex is StackOverflowException) + logCodedError (prefix + "7016", ex.ToString ()); + else if (ex is TimeoutException) + logCodedError (prefix + "7017", ex.ToString ()); + else if (ex is TypeInitializationException) + logCodedError (prefix + "7018", ex.ToString ()); + else if (ex is UnauthorizedAccessException uaex) + logCodedError (prefix + "7019", GetFileLockedExceptionMessage (uaex)); + else if (ex is ApplicationException) + logCodedError (prefix + "7020", ex.ToString ()); + else if (ex is KeyNotFoundException) + logCodedError (prefix + "7021", ex.ToString ()); + else if (ex is PathTooLongException) // IOException + logCodedError (prefix + "7022", ex.ToString ()); + else if (ex is DirectoryNotFoundException) // IOException + logCodedError (prefix + "7023", ex.ToString ()); + else if (ex is DriveNotFoundException) // IOException + logCodedError (prefix + "7025", ex.ToString ()); + else if (ex is EndOfStreamException) // IOException + logCodedError (prefix + "7026", ex.ToString ()); + else if (ex is FileLoadException) // IOException + logCodedError (prefix + "7027", ex.ToString ()); + else if (ex is FileNotFoundException) // IOException + logCodedError (prefix + "7028", ex.ToString ()); + else if (ex is IOException ioex) + logCodedError (prefix + "7024", GetFileLockedExceptionMessage (ioex)); + else + logCodedError (prefix + "7000", ex.ToString ()); + } + + static string GetFileLockedExceptionMessage (Exception ex) + { + // If we find a file path in the message, and the file exists, check if it's locked + // en-US message is: + // The process cannot access the file 'D:\temp\tmpw5mhqp.tmp' because it is being used by another process. + var matches = Regex.Matches (ex.Message, @"'([^']+)'"); + for (int i = 0; i < matches.Count; ++i) { + string path = matches [i].Groups [1].Value; + if (!File.Exists (path)) { + continue; + } + string processes = LockCheck.GetLockedFileMessage (path); + if (string.IsNullOrEmpty (processes)) { + continue; + } + return $"{processes}.{Environment.NewLine}{ex.ToString ()}"; + } + return ex.ToString (); + } + + public static void LogUnhandledToolError (this TaskLoggingHelper log, string prefix, string toolOutput) + { + log.LogCodedError ($"XA{prefix}0000", toolOutput); + } + } +} diff --git a/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/XDocumentExtensions.cs b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/XDocumentExtensions.cs new file mode 100644 index 00000000000..c3979a786ac --- /dev/null +++ b/external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/XDocumentExtensions.cs @@ -0,0 +1,56 @@ +// https://github.com/xamarin/xamarin-android/blob/72bb66856814e0b64e02b21be66a6dc03e1ffcb6/src/Xamarin.Android.Build.Tasks/Utilities/XDocumentExtensions.cs + +using System.Linq; +using System.Xml.XPath; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Android.Build.Tasks +{ + public static class XDocumentExtensions + { + const string PathsElementName = "Paths"; + + public static ITaskItem[] GetPathsAsTaskItems (this XDocument doc, params string[] paths) + { + var e = doc.Elements (PathsElementName); + foreach (var p in paths) + e = e.Elements (p); + return e.Select (ToTaskItem).ToArray (); + } + + static ITaskItem ToTaskItem (XElement element) + { + var taskItem = new TaskItem (element.Value); + foreach (var attribute in element.Attributes ()) { + taskItem.SetMetadata (attribute.Name.LocalName, attribute.Value); + } + return taskItem; + } + + public static string[] GetPaths (this XDocument doc, params string[] paths) + { + var e = doc.Elements (PathsElementName); + foreach (var p in paths) + e = e.Elements (p); + return e.Select (p => p.Value).ToArray (); + } + + public static string ToFullString (this XElement element) + { + return element.ToString (SaveOptions.DisableFormatting); + } + + public static bool SaveIfChanged (this XDocument document, string fileName) + { + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) + using (var xw = new LinePreservedXmlWriter (sw)) { + xw.WriteNode (document.CreateNavigator (), false); + xw.Flush (); + return Files.CopyIfStreamChanged (sw.BaseStream, fileName); + } + } + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs new file mode 100644 index 00000000000..87ed37a34eb --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs @@ -0,0 +1,361 @@ +using System; +using System.Linq; +using System.Xml; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Text.RegularExpressions; +using System.Text; +using System.IO; + +namespace Xamarin.Android.Tools +{ + public class AndroidAppManifest + { + AndroidVersions versions; + XDocument doc; + XElement manifest, application, usesSdk; + + static readonly XNamespace aNS = "http://schemas.android.com/apk/res/android"; + static readonly XName aName = aNS + "name"; + + public static XNamespace AndroidXNamespace => aNS; + public static XName NameXName => aName; + + public XDocument Document => doc; + + AndroidAppManifest (AndroidVersions versions, XDocument doc) + { + if (versions == null) + throw new ArgumentNullException (nameof (versions)); + if (doc == null) + throw new ArgumentNullException (nameof (doc)); + this.versions = versions; + this.doc = doc; + + if (doc.Root is null || doc.Root.Name != "manifest") + throw new ArgumentException ("App manifest does not have 'manifest' root element", nameof (doc)); + + manifest = doc.Root; + + if (manifest.Element ("application") is XElement app) + application = app; + else + manifest.Add (application = new XElement ("application")); + + if (manifest.Element ("uses-sdk") is XElement uses) + usesSdk = uses; + else + usesSdk = new XElement ("uses-sdk"); + } + + public static string CanonicalizePackageName (string packageNameOrAssemblyName) + { + if (packageNameOrAssemblyName == null) + throw new ArgumentNullException ("packageNameOrAssemblyName"); + if (string.IsNullOrEmpty (packageNameOrAssemblyName = packageNameOrAssemblyName.Trim ())) + throw new ArgumentException ("Must specify a package name or assembly name", "packageNameOrAssemblyName"); + + string[] packageParts = packageNameOrAssemblyName.Split (new[]{'.'}, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < packageParts.Length; ++i) { + packageParts [i] = Regex.Replace (packageParts [i], "[^A-Za-z0-9_]", "_"); + if (char.IsDigit (packageParts [i], 0) || packageParts [i][0] == '_') + packageParts [i] = "x" + packageParts [i]; + } + return packageParts.Length == 1 + ? packageParts [0] + "." + packageParts [0] + : string.Join (".", packageParts); + } + + public static AndroidAppManifest Create (string packageName, string appLabel, AndroidVersions versions) + { + return new AndroidAppManifest (versions, XDocument.Parse ( + @" + + + +")) { + PackageName = packageName, + ApplicationLabel = appLabel, + }; + } + + public static AndroidAppManifest Load (string filename, AndroidVersions versions) + { + if (filename == null) + throw new ArgumentNullException (nameof (filename)); + if (versions == null) + throw new ArgumentNullException (nameof (versions)); + + return Load (XDocument.Load (filename), versions); + } + + public static AndroidAppManifest Load (XDocument doc, AndroidVersions versions) + { + if (doc == null) + throw new ArgumentNullException (nameof (doc)); + if (versions == null) + throw new ArgumentNullException (nameof (versions)); + + return new AndroidAppManifest (versions, doc); + } + + public void Write (XmlWriter writer) + { + // Make sure that if the XML element does not have any attributes (i.e. minSdkVersion + // and targetSdkVersion), do NOT write it into the output. This is to avoid issues like + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1874249/ + if (usesSdk.HasAttributes && usesSdk.Parent == null) + manifest.Add (usesSdk); + else if (!usesSdk.HasAttributes && usesSdk.Parent != null) + usesSdk.Remove (); + + doc.Save (writer); + } + + public void WriteToFile (string fileName) + { + var xmlSettings = new XmlWriterSettings () { + Encoding = Encoding.UTF8, + CloseOutput = false, + Indent = true, + IndentChars = "\t", + NewLineChars = "\n", + }; + + var tempFile = FileUtil.GetTempFilenameForWrite (fileName); + bool success = false; + try { + using (var writer = XmlTextWriter.Create (tempFile, xmlSettings)) { + Write (writer); + } + FileUtil.SystemRename (tempFile, fileName); + success = true; + } finally { + if (!success) { + try { + File.Delete (tempFile); + } catch { + //the original exception is more important than this one + } + } + } + } + + static string? NullIfEmpty (string? value) + { + return string.IsNullOrEmpty (value) ? null : value; + } + + public string? PackageName { + get { return (string?) manifest.Attribute ("package"); } + set { manifest.SetAttributeValue ("package", NullIfEmpty (value)); } + } + + public string? ApplicationLabel { + get { return (string?) application.Attribute (aNS + "label"); } + set { application.SetAttributeValue (aNS + "label", NullIfEmpty (value)); } + } + + public string? ApplicationIcon { + get { return (string?) application.Attribute (aNS + "icon"); } + set { application.SetAttributeValue (aNS + "icon", NullIfEmpty (value)); } + } + + public string? ApplicationRoundIcon { + get { return (string?) application.Attribute (aNS + "roundIcon"); } + set { application.SetAttributeValue (aNS + "roundIcon", NullIfEmpty (value)); } + } + + public string? ApplicationTheme { + get { return (string?) application.Attribute (aNS + "theme"); } + set { application.SetAttributeValue (aNS + "theme", NullIfEmpty (value)); } + } + + public string? VersionName { + get { return (string?) manifest.Attribute (aNS + "versionName"); } + set { manifest.SetAttributeValue (aNS + "versionName", NullIfEmpty (value)); } + } + + public string? VersionCode { + get { return (string?) manifest.Attribute (aNS + "versionCode"); } + set { manifest.SetAttributeValue (aNS + "versionCode", NullIfEmpty (value)); } + } + + public string? InstallLocation { + get { return (string?) manifest.Attribute (aNS + "installLocation"); } + set { manifest.SetAttributeValue (aNS + "installLocation", NullIfEmpty (value)); } + } + + public int? MinSdkVersion { + get { return ParseSdkVersion (usesSdk.Attribute (aNS + "minSdkVersion")); } + set { usesSdk.SetAttributeValue (aNS + "minSdkVersion", value == null ? null : value.ToString ()); } + } + + public int? TargetSdkVersion { + get { return ParseSdkVersion (usesSdk.Attribute (aNS + "targetSdkVersion")); } + set { usesSdk.SetAttributeValue (aNS + "targetSdkVersion", value == null ? null : value.ToString ()); } + } + + int? ParseSdkVersion (XAttribute? attribute) + { + var version = (string?) attribute; + if (version == null || string.IsNullOrEmpty (version)) + return null; + int vn; + if (!int.TryParse (version, out vn)) { + int? apiLevel = versions.GetApiLevelFromId (version); + if (apiLevel.HasValue) + return apiLevel.Value; + return versions.MaxStableVersion?.ApiLevel; + } + return vn; + } + + public IEnumerable AndroidPermissions { + get { + foreach (var el in manifest.Elements ("uses-permission")) { + var name = (string?) el.Attribute (aName); + if (name == null) + continue; + var lastDot = name.LastIndexOf ('.'); + if (lastDot >= 0) + yield return name.Substring (lastDot + 1); + } + } + } + + public IEnumerable AndroidPermissionsQualified { + get { + foreach (var el in manifest.Elements ("uses-permission")) { + var name = (string?) el.Attribute (aName); + if (name != null) + yield return name; + } + } + } + + public bool? Debuggable { + get { return (bool?) application.Attribute (aNS + "debuggable"); } + set { application.SetAttributeValue (aNS + "debuggable", value); } + } + + public void SetAndroidPermissions (IEnumerable permissions) + { + var newPerms = new HashSet (permissions.Select (FullyQualifyPermission)); + var current = new HashSet (AndroidPermissionsQualified); + AddAndroidPermissions (newPerms.Except (current)); + RemoveAndroidPermissions (current.Except (newPerms)); + } + + void AddAndroidPermissions (IEnumerable permissions) + { + var newElements = permissions.Select (p => new XElement ("uses-permission", new XAttribute (aName, p))); + + var lastPerm = manifest.Elements ("uses-permission").LastOrDefault (); + if (lastPerm != null) { + foreach (var el in newElements) { + lastPerm.AddAfterSelf (el); + lastPerm = el; + } + } else { + var parentNode = (XNode?) manifest.Element ("application") ?? manifest.LastNode; + foreach (var el in newElements) + parentNode!.AddBeforeSelf (el); + } + } + + string FullyQualifyPermission (string permission) + { + //if already qualified, don't mess with it + if (permission.IndexOf ('.') > -1) + return permission; + + switch (permission) { + case "READ_HISTORY_BOOKMARKS": + case "WRITE_HISTORY_BOOKMARKS": + return string.Format ("com.android.browser.permission.{0}", permission); + default: + return string.Format ("android.permission.{0}", permission); + } + } + + void RemoveAndroidPermissions (IEnumerable permissions) + { + var perms = new HashSet (permissions); + var list = manifest.Elements ("uses-permission") + .Where (el => { + var name = (string?) el.Attribute (aName); + return name != null && perms.Contains (name); + }) + .ToList (); + foreach (var el in list) + el.Remove (); + } + + [Obsolete ("Use GetLaunchableFastdevActivityName or GetLaunchableUserActivityName")] + public string? GetLaunchableActivityName () + { + return GetLaunchableFastDevActivityName (); + } + + /// Gets an activity that can be used to initialize the override directory for fastdev. + [Obsolete ("This should not be needed anymore; Activity execution is not part of installation.")] + public string? GetLaunchableFastDevActivityName () + { + string? first = null; + foreach (var a in GetLaunchableActivities ()) { + var name = (string?) a.Attribute (aName); + //prefer the fastdev launcher, it's quicker + if (name == "mono.android.__FastDevLauncher") { + return name; + } + //else just use the first other launchable activity + if (first == null) { + first = name; + } + } + + return string.IsNullOrEmpty (first)? null : first; + } + + // We add a fake launchable activity for FastDev, but we don't want + // to launch that one when the user does Run or Debug + public string? GetLaunchableUserActivityName () + { + return GetLaunchableActivities () + .Select (a => (string?) a.Attribute (aName)) + .FirstOrDefault (name => !string.IsNullOrEmpty (name) && name != "mono.android.__FastDevLauncher"); + } + + IEnumerable GetLaunchableActivities () + { + var activities = application.Elements ("activity"); + var aliases = application.Elements ("activity-alias"); + foreach (var activity in activities.Union (aliases)) { + foreach (var filter in activity.Elements ("intent-filter")) { + foreach (var category in filter.Elements ("category")) + if (category != null && (string?)category.Attribute (aName) == "android.intent.category.LAUNCHER") + yield return activity; + } + } + } + + public IEnumerable GetAllActivityNames () + { + foreach (var activity in application.Elements ("activity")) { + var activityName = (string?) activity.Attribute (aName); + if (activityName != null && activityName != "mono.android.__FastDevLauncher") + yield return activityName; + } + } + + public IEnumerable GetLaunchableActivityNames () + { + return GetLaunchableActivities () + .Select (a => (string?) a.Attribute (aName)) + .Where (name => !string.IsNullOrEmpty (name) && name != "mono.android.__FastDevLauncher") + .Select (name => name!); + } + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs new file mode 100644 index 00000000000..1e843a01828 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Xamarin.Android.Tools +{ + public class AndroidSdkInfo + { + AndroidSdkBase sdk; + + public AndroidSdkInfo (Action? logger = null, string? androidSdkPath = null, string? androidNdkPath = null, string? javaSdkPath = null) + { + logger = logger ?? DefaultConsoleLogger; + + sdk = CreateSdk (logger); + sdk.Initialize (androidSdkPath, androidNdkPath, javaSdkPath); + + // shouldn't happen, in that sdk.Initialize() should throw instead + if (string.IsNullOrEmpty (AndroidSdkPath)) + throw new InvalidOperationException ($"Could not determine Android SDK location. Please provide `{nameof (androidSdkPath)}`."); + if (string.IsNullOrEmpty (JavaSdkPath)) + throw new InvalidOperationException ($"Could not determine Java SDK location. Please provide `{nameof (javaSdkPath)}`."); + } + + static AndroidSdkBase CreateSdk (Action logger) + { + return OS.IsWindows + ? (AndroidSdkBase) new AndroidSdkWindows (logger) + : (AndroidSdkBase) new AndroidSdkUnix (logger); + } + + public IEnumerable GetBuildToolsPaths (string preferredBuildToolsVersion) + { + if (!string.IsNullOrEmpty (preferredBuildToolsVersion)) { + var preferredDir = Path.Combine (AndroidSdkPath, "build-tools", preferredBuildToolsVersion); + if (Directory.Exists (preferredDir)) + return new[] { preferredDir }.Concat (GetBuildToolsPaths ().Where (p => p!= preferredDir)); + } + return GetBuildToolsPaths (); + } + + public IEnumerable GetBuildToolsPaths () + { + var buildTools = Path.Combine (AndroidSdkPath, "build-tools"); + if (Directory.Exists (buildTools)) { + var sorted = SortedSubdirectoriesByVersion (buildTools); + + foreach (var d in sorted) + yield return d; + + var preview = Directory.EnumerateDirectories (buildTools) + .Where(x => TryParseVersion (Path.GetFileName (x)) == null) + .Select(x => x); + + foreach (var d in preview) + yield return d; + } + var ptPath = Path.Combine (AndroidSdkPath, "platform-tools"); + if (Directory.Exists (ptPath)) + yield return ptPath; + } + + static IEnumerable SortedSubdirectoriesByVersion (string dir) + { + return from p in Directory.EnumerateDirectories (dir) + let version = TryParseVersion (Path.GetFileName (p)) + where version != null + orderby version descending + select p; + } + + static Version? TryParseVersion (string v) + { + if (Version.TryParse (v, out var version)) + return version; + return null; + } + + public IEnumerable GetInstalledPlatformVersions (AndroidVersions versions) + { + if (versions == null) + throw new ArgumentNullException (nameof (versions)); + return versions.InstalledBindingVersions + .Where (p => TryGetPlatformDirectoryFromApiLevel (p.Id, versions) != null) ; + } + + public string GetPlatformDirectory (int apiLevel) + { + return GetPlatformDirectoryFromId (apiLevel.ToString ()); + } + + public string GetPlatformDirectoryFromId (string id) + { + return Path.Combine (AndroidSdkPath, "platforms", "android-" + id); + } + + public string? TryGetPlatformDirectoryFromApiLevel (string idOrApiLevel, AndroidVersions versions) + { + var id = versions.GetIdFromApiLevel (idOrApiLevel); + if (id == null) + return null; + + string? dir = GetPlatformDirectoryFromId (id); + + if (Directory.Exists (dir)) + return dir; + + // Only fall back to major API level if we weren't explicitly requesting a minor version. + // For example, if "36.1" was requested but android-36.1 doesn't exist, don't fall back to android-36 + // because the minor version may have APIs that the major version doesn't have. + // See: https://github.com/dotnet/android/issues/10720 + if (Version.TryParse (id, out var version) && version.Minor != 0) { + return null; + } + + var level = versions.GetApiLevelFromId (id); + dir = level.HasValue ? GetPlatformDirectory (level.Value) : null; + if (dir != null && Directory.Exists (dir)) + return dir; + + // Starting with API 37, Google's SDK Manager installs platforms to + // "android-37.0" instead of "android-37". Try the "{major}.0" fallback. + // See: https://github.com/dotnet/android-tools/issues/319 + if (int.TryParse (id, out _)) { + dir = GetPlatformDirectoryFromId (id + ".0"); + if (Directory.Exists (dir)) + return dir; + } + + return null; + } + + public bool IsPlatformInstalled (int apiLevel) + { + return apiLevel != 0 && + (Directory.Exists (GetPlatformDirectory (apiLevel)) || + Directory.Exists (GetPlatformDirectoryFromId (apiLevel + ".0"))); + } + + public string? AndroidNdkPath { + get { return sdk.AndroidNdkPath; } + } + + public string AndroidSdkPath { + get { return sdk.AndroidSdkPath!; } + } + + public string [] AllAndroidSdkPaths { + get { + return sdk.AllAndroidSdks ?? new string [0]; + } + } + + public string JavaSdkPath { + get { return sdk.JavaSdkPath!; } + } + + public string AndroidNdkHostPlatform { + get { return sdk.NdkHostPlatform; } + } + + public static void SetPreferredAndroidNdkPath (string path, Action? logger = null) + { + logger = logger ?? DefaultConsoleLogger; + + var sdk = CreateSdk (logger); + sdk.SetPreferredAndroidNdkPath(path); + } + + internal static void DefaultConsoleLogger (TraceLevel level, string message) + { + switch (level) { + case TraceLevel.Error: + Console.Error.WriteLine (message); + break; + default: + Console.WriteLine ($"[{level}] {message}"); + break; + } + } + + public static void SetPreferredAndroidSdkPath (string path, Action? logger = null) + { + logger = logger ?? DefaultConsoleLogger; + + var sdk = CreateSdk (logger); + sdk.SetPreferredAndroidSdkPath (path); + } + + public static void SetPreferredJavaSdkPath (string path, Action? logger = null) + { + logger = logger ?? DefaultConsoleLogger; + + var sdk = CreateSdk (logger); + sdk.SetPreferredJavaSdkPath (path); + } + + public static void DetectAndSetPreferredJavaSdkPathToLatest (Action? logger = null) + { + if (OS.IsWindows) + throw new NotImplementedException ("Windows is not supported at this time."); + + logger = logger ?? DefaultConsoleLogger; + + var latestJdk = JdkInfo.GetSupportedJdkInfos (logger).FirstOrDefault (); + if (latestJdk == null) + throw new NotSupportedException ("No Microsoft OpenJDK could be found. Please re-run the Visual Studio installer or manually specify the JDK path in settings."); + + var sdk = CreateSdk (logger); + sdk.SetPreferredJavaSdkPath (latestJdk.HomePath); + } + + public string? TryGetCommandLineToolsPath () + { + return GetCommandLineToolsPaths ("latest").FirstOrDefault (); + } + + public IEnumerable GetCommandLineToolsPaths (string preferredCommandLineToolsVersion) + { + if (!string.IsNullOrEmpty (preferredCommandLineToolsVersion)) { + var preferredDir = Path.Combine (AndroidSdkPath, "cmdline-tools", preferredCommandLineToolsVersion); + if (Directory.Exists (preferredDir)) + return new[] { preferredDir }.Concat (GetCommandLineToolsPaths ().Where (p => p != preferredDir)); + } + return GetCommandLineToolsPaths (); + } + + public IEnumerable GetCommandLineToolsPaths () + { + var cmdlineToolsDir = Path.Combine (AndroidSdkPath, "cmdline-tools"); + if (Directory.Exists (cmdlineToolsDir)) { + var latestDir = Path.Combine (cmdlineToolsDir, "latest"); + if (Directory.Exists (latestDir)) + yield return latestDir; + foreach (var d in SortedSubdirectoriesByVersion (cmdlineToolsDir)) { + var version = Path.GetFileName (d); + if (version == "latest") + continue; + yield return d; + } + } + var toolsDir = Path.Combine (AndroidSdkPath, "tools"); + if (Directory.Exists (toolsDir)) { + yield return toolsDir; + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidTargetArch.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidTargetArch.cs new file mode 100644 index 00000000000..a54c57b8f3b --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidTargetArch.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.RegularExpressions; +using System.IO; + +namespace Xamarin.Android.Tools +{ + [Flags] + public enum AndroidTargetArch + { + None = 0, + Arm = 1, + X86 = 2, + Mips = 4, + Arm64 = 8, + X86_64 = 16, + Other = 0x10000 // hope it's not too optimistic + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs new file mode 100644 index 00000000000..31c95ab2f96 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Linq; + +namespace Xamarin.Android.Tools +{ + public class AndroidVersion + { + // Android API Level. *Usually* corresponds to $(AndroidSdkPath)/platforms/android-$(ApiLevel)/android.jar + public int ApiLevel { get; private set; } + + // Android API Level; includes "minor" version bumps, e.g. Android 16 QPR2 is "36.1" while ApiLevel=36 + public Version VersionCodeFull { get; private set; } + + // Android API Level ID. == ApiLevel on stable versions, will be e.g. `N` for previews: $(AndroidSdkPath)/platforms/android-N/android.jar + public string Id { get; private set; } + + // Name of an Android release, e.g. "Oreo" + public string? CodeName { get; private set; } + + // Android version number, e.g. 8.0 + public string OSVersion { get; private set; } + + // Xamarin.Android $(TargetFrameworkVersion) value, e.g. 8.0 + public Version TargetFrameworkVersion { get; private set; } + + // TargetFrameworkVersion *with* a leading `v`, e.g. "v8.0" + public string FrameworkVersion { get; private set; } + + // Is this API level stable? Should be False for non-numeric Id values. + public bool Stable { get; private set; } + + internal HashSet Ids { get; } = new (); + + // Alternate Ids for a given API level. Allows for historical mapping, e.g. API-11 has alternate ID 'H'. + internal string[]? AlternateIds { + set { + if (value is { Length: > 0 }) + Ids.UnionWith (value); + } + } + + public AndroidVersion (int apiLevel, string osVersion, string? codeName = null, string? id = null, bool stable = true) + : this (new Version (apiLevel, 0), osVersion, codeName, id, stable) + { + } + + public AndroidVersion (Version versionCodeFull, string osVersion, string? codeName = null, string? id = null, bool stable = true) + { + if (versionCodeFull == null) + throw new ArgumentNullException (nameof (versionCodeFull)); + if (osVersion == null) + throw new ArgumentNullException (nameof (osVersion)); + + ApiLevel = versionCodeFull.Major; + VersionCodeFull = versionCodeFull; + Id = id ?? (versionCodeFull.Minor != 0 ? versionCodeFull.ToString () : ApiLevel.ToString ()); + CodeName = codeName; + OSVersion = osVersion; + TargetFrameworkVersion = Version.Parse (osVersion); + FrameworkVersion = "v" + osVersion; + Stable = stable; + + Ids.Add (ApiLevel.ToString ()); + Ids.Add (VersionCodeFull.ToString ()); + Ids.Add (Id); + } + + public override string ToString () + { + return $"(AndroidVersion: ApiLevel={ApiLevel} VersionCodeFull={VersionCodeFull} Id={Id} OSVersion={OSVersion} CodeName='{CodeName}' TargetFrameworkVersion={TargetFrameworkVersion} Stable={Stable})"; + } + + public static AndroidVersion Load (Stream stream) + { + var doc = XDocument.Load (stream); + return Load (doc); + } + + public static AndroidVersion Load (string uri) + { + var doc = XDocument.Load (uri); + return Load (doc); + } + + // Example: + // + // 26 + // 26 + // Oreo + // v8.0 + // True + // + static AndroidVersion Load (XDocument doc) + { + var id = (string?) doc.Root?.Element ("Id") ?? throw new InvalidOperationException ("Missing Id element"); + var level = (int?) doc.Root?.Element ("Level") ?? throw new InvalidOperationException ("Missing Level element"); + var name = (string?) doc.Root?.Element ("Name") ?? throw new InvalidOperationException ("Missing Name element"); + var version = (string?) doc.Root?.Element ("Version") ?? throw new InvalidOperationException ("Missing Version element"); + var stable = (bool?) doc.Root?.Element ("Stable") ?? throw new InvalidOperationException ("Missing Stable element"); + var versionCodeFull = (string?) doc.Root?.Element ("VersionCodeFull"); + + var fullLevel = string.IsNullOrWhiteSpace (versionCodeFull) + ? new Version (level, 0) + : Version.Parse (versionCodeFull); + + return new AndroidVersion (fullLevel, version.TrimStart ('v'), name, id, stable); + } + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs new file mode 100644 index 00000000000..734aa1cf656 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Xamarin.Android.Tools +{ + public class AndroidVersions + { + List installedVersions = new List (); + + public IReadOnlyList FrameworkDirectories { get; } + public AndroidVersion? MaxStableVersion { get; private set; } + public AndroidVersion? MinStableVersion { get; private set; } + + public IReadOnlyList InstalledBindingVersions { get; private set; } + + public AndroidVersions (IEnumerable frameworkDirectories) + { + if (frameworkDirectories == null) + throw new ArgumentNullException (nameof (frameworkDirectories)); + + var dirs = new List (); + + foreach (var d in frameworkDirectories) { + if (!Directory.Exists (d)) + throw new ArgumentException ($"`{d}` must be a directory!", nameof (frameworkDirectories)); + + var dp = d.TrimEnd (Path.DirectorySeparatorChar); + var dn = Path.GetFileName (dp); + // In "normal" use, `dp` will contain e.g. `...\MonoAndroid\v1.0`. + // We want the `MonoAndroid` dir, not the versioned dir. + var p = dn.StartsWith ("v", StringComparison.Ordinal) ? (Path.GetDirectoryName (dp) ?? "") : dp; + dirs.Add (Path.GetFullPath (p)); + } + + dirs = dirs.Distinct (StringComparer.OrdinalIgnoreCase) + .ToList (); + + FrameworkDirectories = new ReadOnlyCollection (dirs); + + var versions = dirs.SelectMany (d => Directory.EnumerateFiles (d, "AndroidApiInfo.xml", SearchOption.AllDirectories)) + .Select (file => AndroidVersion.Load (file)); + + LoadVersions (versions); + + InstalledBindingVersions = new ReadOnlyCollection (installedVersions); + } + + public AndroidVersions (IEnumerable versions) + { + if (versions == null) + throw new ArgumentNullException (nameof (versions)); + + FrameworkDirectories = new ReadOnlyCollection (new string [0]); + + LoadVersions (versions); + + InstalledBindingVersions = new ReadOnlyCollection (installedVersions); + } + + void LoadVersions (IEnumerable versions) + { + foreach (var version in versions) { + installedVersions.Add (version); + if (!version.Stable) + continue; + if (MaxStableVersion == null || (MaxStableVersion.TargetFrameworkVersion < version.TargetFrameworkVersion)) { + MaxStableVersion = version; + } + if (MinStableVersion == null || (MinStableVersion.TargetFrameworkVersion > version.TargetFrameworkVersion)) { + MinStableVersion = version; + } + } + installedVersions.Sort ((x, y) => { + return x.VersionCodeFull.CompareTo (y.VersionCodeFull); + }); + } + + public int? GetApiLevelFromFrameworkVersion (string frameworkVersion) + { + return installedVersions.FirstOrDefault (v => MatchesFrameworkVersion (v, frameworkVersion))?.ApiLevel ?? + KnownVersions.FirstOrDefault (v => MatchesFrameworkVersion (v, frameworkVersion))?.ApiLevel; + } + + static bool MatchesFrameworkVersion (AndroidVersion version, string frameworkVersion) + { + return version.FrameworkVersion == frameworkVersion || + version.OSVersion == frameworkVersion; + } + + public int? GetApiLevelFromId (string id) + { + return installedVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ?? + KnownVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ?? + (Version.TryParse (id, out var versionCodeFull) ? (int?) versionCodeFull.Major : default (int?)) ?? + (int.TryParse (id, out int apiLevel) ? apiLevel : default (int?)); + } + + static bool MatchesId (AndroidVersion version, string id) + { + return version.Ids.Contains (id); + } + + public string? GetIdFromApiLevel (int apiLevel) + { + return installedVersions.FirstOrDefault (v => v.ApiLevel == apiLevel)?.Id ?? + KnownVersions.FirstOrDefault (v => v.ApiLevel == apiLevel)?.Id ?? + apiLevel.ToString (); + } + + public string? GetIdFromVersionCodeFull (Version versionCodeFull) + { + return installedVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ?? + KnownVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ?? + versionCodeFull.ToString (); + } + + // Sometimes, e.g. when new API levels are introduced, the "API level" is a letter, not a number, + // e.g. 'API-H' for API-11, 'API-O' for API-26, etc. + public string? GetIdFromApiLevel (string apiLevel) + { + if (int.TryParse (apiLevel, out var platform)) + return GetIdFromApiLevel (platform); + if (Version.TryParse (apiLevel, out var versionCodeFull)) + return GetIdFromVersionCodeFull (versionCodeFull); + return installedVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ?? + KnownVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ?? + apiLevel; + } + + public string? GetIdFromFrameworkVersion (string frameworkVersion) + { + return installedVersions.FirstOrDefault (v => MatchesFrameworkVersion (v, frameworkVersion))?.Id ?? + KnownVersions.FirstOrDefault (v => MatchesFrameworkVersion (v, frameworkVersion))?.Id; + } + + public string? GetFrameworkVersionFromApiLevel (int apiLevel) + { + return installedVersions.FirstOrDefault (v => v.ApiLevel == apiLevel)?.FrameworkVersion ?? + KnownVersions.FirstOrDefault (v => v.ApiLevel == apiLevel)?.FrameworkVersion; + } + + public string? GetFrameworkVersionFromId (string id) + { + return installedVersions.FirstOrDefault (v => MatchesId (v, id))?.FrameworkVersion ?? + KnownVersions.FirstOrDefault (v => MatchesId (v, id))?.FrameworkVersion; + } + + public static readonly AndroidVersion [] KnownVersions = new [] { + new AndroidVersion (4, "1.6", "Donut"), + new AndroidVersion (5, "2.0", "Eclair"), + new AndroidVersion (6, "2.0.1", "Eclair"), + new AndroidVersion (7, "2.1", "Eclair"), + new AndroidVersion (8, "2.2", "Froyo"), + new AndroidVersion (10, "2.3", "Gingerbread"), + new AndroidVersion (11, "3.0", "Honeycomb") { + AlternateIds = new[]{ "H" }, + }, + new AndroidVersion (12, "3.1", "Honeycomb"), + new AndroidVersion (13, "3.2", "Honeycomb"), + new AndroidVersion (14, "4.0", "Ice Cream Sandwich"), + new AndroidVersion (15, "4.0.3", "Ice Cream Sandwich"), + new AndroidVersion (16, "4.1", "Jelly Bean"), + new AndroidVersion (17, "4.2", "Jelly Bean"), + new AndroidVersion (18, "4.3", "Jelly Bean"), + new AndroidVersion (19, "4.4", "Kit Kat"), + new AndroidVersion (20, "4.4.87", "Kit Kat + Wear support"), + new AndroidVersion (21, "5.0", "Lollipop") { + AlternateIds = new[]{ "L" }, + }, + new AndroidVersion (22, "5.1", "Lollipop"), + new AndroidVersion (23, "6.0", "Marshmallow") { + AlternateIds = new[]{ "M" }, + }, + new AndroidVersion (24, "7.0", "Nougat") { + AlternateIds = new[]{ "N" }, + }, + new AndroidVersion (25, "7.1", "Nougat"), + new AndroidVersion (26, "8.0", "Oreo") { + AlternateIds = new[]{ "O" }, + }, + new AndroidVersion (27, "8.1", "Oreo"), + new AndroidVersion (28, "9.0", "Pie") { + AlternateIds = new[]{ "P" }, + }, + new AndroidVersion (29, "10.0", "Android10") { + AlternateIds = new[]{ "Q" }, + }, + new AndroidVersion (30, "11.0") { + AlternateIds = new[]{ "R" }, + }, + new AndroidVersion (31, "12.0") { + AlternateIds = new[]{ "S" }, + }, + new AndroidVersion (32, "12.1") { + AlternateIds = new[]{ "Sv2" }, + }, + new AndroidVersion (33, "13.0") { + AlternateIds = new[]{ "T" }, + }, + new AndroidVersion (34, "14.0") { + AlternateIds = new[]{ "U", "UpsideDownCake", "Upside Down Cake" }, + }, + new AndroidVersion (35, "15.0") { + AlternateIds = new[]{ "V", "VanillaIceCream", "Vanilla Ice Cream" }, + }, + new AndroidVersion (36, "16.0") { + AlternateIds = new[]{ "Baklava" }, + }, + new AndroidVersion (new Version ("36.1"), "16.0") { + AlternateIds = new[]{ "Canary" }, + }, + new AndroidVersion (new Version ("37.0"), "17.0") { + AlternateIds = new[]{ "CinnamonBun", "Cinnamon Bun" }, + }, + }; + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/DownloadUtils.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/DownloadUtils.cs new file mode 100644 index 00000000000..2fc56cb4a41 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/DownloadUtils.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ + /// + /// Shared helpers for downloading files, verifying checksums, and extracting archives. + /// + static class DownloadUtils + { + const int BufferSize = 81920; + const long BytesPerMB = 1024 * 1024; + static readonly char[] WhitespaceChars = [' ', '\t', '\n', '\r']; + + static Task ReadAsStreamAsync (HttpContent content, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + return content.ReadAsStreamAsync (cancellationToken); +#else + return content.ReadAsStreamAsync (); +#endif + } + + internal static Task ReadAsStringAsync (HttpContent content, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + return content.ReadAsStringAsync (cancellationToken); +#else + return content.ReadAsStringAsync (); +#endif + } + + /// Downloads a file from the given URL with optional progress reporting. + public static async Task DownloadFileAsync (HttpClient client, string url, string destinationPath, long expectedSize, IProgress<(double percent, string message)>? progress, CancellationToken cancellationToken) + { + using var response = await client.GetAsync (url, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait (false); + response.EnsureSuccessStatusCode (); + + var totalBytes = response.Content.Headers.ContentLength ?? expectedSize; + + using var contentStream = await ReadAsStreamAsync (response.Content, cancellationToken).ConfigureAwait (false); + + var dirPath = Path.GetDirectoryName (destinationPath); + if (!string.IsNullOrEmpty (dirPath)) + Directory.CreateDirectory (dirPath); + + using var fileStream = new FileStream (destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, useAsync: true); + + var buffer = ArrayPool.Shared.Rent (BufferSize); + try { + long totalRead = 0; + int bytesRead; + + while ((bytesRead = await contentStream.ReadAsync (buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false)) > 0) { + await fileStream.WriteAsync (buffer, 0, bytesRead, cancellationToken).ConfigureAwait (false); + totalRead += bytesRead; + + if (progress is not null && totalBytes > 0) { + var pct = (double) totalRead / totalBytes * 100; + progress.Report ((pct, $"Downloaded {totalRead / BytesPerMB} MB / {totalBytes / BytesPerMB} MB")); + } + } + } + finally { + ArrayPool.Shared.Return (buffer); + } + } + + /// Verifies a file's hash against an expected value using the specified algorithm. + public static void VerifyChecksum (string filePath, string expectedChecksum, ChecksumType checksumType = ChecksumType.Sha256) + { + using var hasher = CreateHashAlgorithm (checksumType); + using var stream = File.OpenRead (filePath); + + var actual = ComputeHashString (hasher, stream); + + if (!string.Equals (actual, expectedChecksum, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException ($"Checksum verification failed. Expected: {expectedChecksum}, Actual: {actual}"); + } + + /// Computes the hash of the given bytes and returns it as a lowercase hex string. + internal static string ComputeHashString (ChecksumType checksumType, byte[] data) + { + using var hasher = CreateHashAlgorithm (checksumType); + var hash = hasher.ComputeHash (data); + return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + } + + static HashAlgorithm CreateHashAlgorithm (ChecksumType checksumType) => checksumType switch { + ChecksumType.Sha256 => (HashAlgorithm) SHA256.Create (), + ChecksumType.Sha1 => SHA1.Create (), + _ => throw new NotSupportedException ($"Unsupported checksum type: '{checksumType}'."), + }; + + static string ComputeHashString (HashAlgorithm hasher, Stream stream) + { + var hash = hasher.ComputeHash (stream); + return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + } + + /// Extracts a ZIP archive with Zip Slip protection. + public static void ExtractZipSafe (string archivePath, string destinationPath, CancellationToken cancellationToken) + { + using var archive = ZipFile.OpenRead (archivePath); + var fullExtractRoot = Path.GetFullPath (destinationPath); + + foreach (var entry in archive.Entries) { + cancellationToken.ThrowIfCancellationRequested (); + + if (string.IsNullOrEmpty (entry.Name)) + continue; + + var destinationFile = Path.GetFullPath (Path.Combine (fullExtractRoot, entry.FullName)); + + // Zip Slip protection + if (!FileUtil.IsUnderDirectory (destinationFile, fullExtractRoot)) { + throw new InvalidOperationException ($"Archive entry '{entry.FullName}' would extract outside target directory."); + } + + var entryDir = Path.GetDirectoryName (destinationFile); + if (!string.IsNullOrEmpty (entryDir)) + Directory.CreateDirectory (entryDir); + + entry.ExtractToFile (destinationFile, overwrite: true); + } + } + + /// Extracts a tar.gz archive using the system tar command. + public static async Task ExtractTarGzAsync (string archivePath, string destinationPath, Action logger, CancellationToken cancellationToken) + { + var psi = ProcessUtils.CreateProcessStartInfo ("/usr/bin/tar", "-xzf", archivePath, "-C", destinationPath); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, stdout: stdout, stderr: stderr, cancellationToken).ConfigureAwait (false); + + if (exitCode != 0) { + var errorOutput = stderr.ToString (); + logger (TraceLevel.Error, $"tar extraction failed (exit code {exitCode}): {errorOutput}"); + throw new IOException ($"Failed to extract archive '{archivePath}': {errorOutput}"); + } + } + + /// Fetches a SHA-256 checksum from a remote URL, returning null on failure. + public static async Task FetchChecksumAsync (HttpClient httpClient, string checksumUrl, string label, Action logger, CancellationToken cancellationToken) + { + try { + using var response = await httpClient.GetAsync (checksumUrl, cancellationToken).ConfigureAwait (false); + response.EnsureSuccessStatusCode (); + var content = await ReadAsStringAsync (response.Content, cancellationToken).ConfigureAwait (false); + var checksum = ParseChecksumFile (content); + logger (TraceLevel.Verbose, $"{label}: checksum={checksum}"); + return checksum; + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) { + logger (TraceLevel.Warning, $"Could not fetch checksum for {label}: {ex.Message}"); + return null; + } + } + + /// Parses "hash filename" or just "hash" from .sha256sum.txt content. + public static string? ParseChecksumFile (string content) + { + if (string.IsNullOrWhiteSpace (content)) + return null; + + var trimmed = content.Trim (); + var end = trimmed.IndexOfAny (WhitespaceChars); + return end >= 0 ? trimmed.Substring (0, end) : trimmed; + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/EnvironmentVariableNames.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/EnvironmentVariableNames.cs new file mode 100644 index 00000000000..1ea607f0582 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/EnvironmentVariableNames.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools +{ + /// + /// Constants for environment variable names used by Android SDK tooling. + /// See https://developer.android.com/tools/variables#envar + /// + internal static class EnvironmentVariableNames + { + /// + /// The preferred variable for the Android SDK root directory. + /// + public const string AndroidHome = "ANDROID_HOME"; + + /// + /// Deprecated — use instead. + /// Retained for reading existing environment configurations. + /// + [System.Obsolete ("ANDROID_SDK_ROOT is deprecated. Use ANDROID_HOME instead.")] + public const string AndroidSdkRoot = "ANDROID_SDK_ROOT"; + + /// + /// The JDK installation directory. + /// + public const string JavaHome = "JAVA_HOME"; + + /// + /// Internal/override JDK path. Takes precedence over JAVA_HOME when set. + /// + public const string JiJavaHome = "JI_JAVA_HOME"; + + /// + /// Executable search paths. + /// + public const string Path = "PATH"; + + /// + /// Executable file extensions (Windows). + /// + public const string PathExt = "PATHEXT"; + + /// + /// Overrides the default location for Android user-specific data + /// (AVDs, preferences, etc.). Defaults to $HOME/.android. + /// + public const string AndroidUserHome = "ANDROID_USER_HOME"; + + /// + /// Overrides the AVD storage directory. Takes precedence over + /// /avd. + /// + public const string AndroidAvdHome = "ANDROID_AVD_HOME"; + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs new file mode 100644 index 00000000000..f324fbd6be4 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs @@ -0,0 +1,274 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ + class FileUtil + { + public static string GetTempFilenameForWrite (string fileName) + { + return Path.GetDirectoryName (fileName) + Path.DirectorySeparatorChar + ".#" + Path.GetFileName (fileName); + } + + //From MonoDevelop.Core.FileService + public static void SystemRename (string sourceFile, string destFile) + { + //FIXME: use the atomic System.IO.File.Replace on NTFS + if (OS.IsWindows) { + string? wtmp = null; + if (File.Exists (destFile)) { + do { + wtmp = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + } while (File.Exists (wtmp)); + + File.Move (destFile, wtmp); + } + try { + File.Move (sourceFile, destFile); + } + catch { + try { + if (wtmp != null) + File.Move (wtmp, destFile); + } + catch { + wtmp = null; + } + throw; + } + finally { + if (wtmp != null) { + try { + File.Delete (wtmp); + } + catch { } + } + } + } + else { + rename (sourceFile, destFile); + } + } + + /// Deletes a file if it exists, logging any failure instead of throwing. + internal static void TryDeleteFile (string path, Action logger) + { + if (!File.Exists (path)) + return; + try { File.Delete (path); } + catch (Exception ex) { logger (TraceLevel.Warning, $"Could not delete '{path}': {ex.Message}"); } + } + + /// Recursively deletes a directory if it exists, logging any failure instead of throwing. + internal static void TryDeleteDirectory (string path, string label, Action logger) + { + if (!Directory.Exists (path)) + return; + try { Directory.Delete (path, recursive: true); } + catch (Exception ex) { logger (TraceLevel.Warning, $"Could not clean up {label} at '{path}': {ex.Message}"); } + } + + /// Moves a directory to the target path, backing up any existing directory and restoring on failure. + internal static void MoveWithRollback (string sourcePath, string targetPath, Action logger) + { + string? backupPath = null; + if (Directory.Exists (targetPath)) { + backupPath = targetPath + $".old-{Guid.NewGuid ():N}"; + Directory.Move (targetPath, backupPath); + } + + var parentDir = Path.GetDirectoryName (targetPath); + if (!string.IsNullOrEmpty (parentDir)) + Directory.CreateDirectory (parentDir); + + try { + Directory.Move (sourcePath, targetPath); + } + catch (Exception ex) { + logger (TraceLevel.Error, $"Failed to move to '{targetPath}': {ex.Message}"); + if (backupPath is not null && Directory.Exists (backupPath)) { + try { + if (Directory.Exists (targetPath)) + Directory.Delete (targetPath, recursive: true); + Directory.Move (backupPath, targetPath); + logger (TraceLevel.Warning, $"Restored previous directory from backup '{backupPath}'."); + } + catch (Exception restoreEx) { + logger (TraceLevel.Error, $"Failed to restore from backup: {restoreEx.Message}"); + } + } + throw; + } + + // Delete backup only after move and caller validation succeed + if (backupPath is not null) + TryDeleteDirectory (backupPath, "old backup", logger); + } + + /// + /// Extracts a zip archive to a temp directory, locates the expected top-level folder, + /// and moves it to the target path with rollback support. + /// + internal static void ExtractAndInstall (string archivePath, string targetPath, string expectedTopDir, Action logger, CancellationToken cancellationToken) + { + var tempExtractDir = Path.Combine (Path.GetTempPath (), $"extract-{Guid.NewGuid ()}"); + try { + Directory.CreateDirectory (tempExtractDir); + DownloadUtils.ExtractZipSafe (archivePath, tempExtractDir, cancellationToken); + + var extractedDir = Path.Combine (tempExtractDir, expectedTopDir); + if (!Directory.Exists (extractedDir)) { + var dirs = Directory.GetDirectories (tempExtractDir); + extractedDir = dirs.Length == 1 ? dirs[0] : tempExtractDir; + } + + MoveWithRollback (extractedDir, targetPath, logger); + logger (TraceLevel.Info, $"Installed to '{targetPath}'."); + } + finally { + TryDeleteDirectory (tempExtractDir, "temp extract dir", logger); + } + } + + /// Deletes a backup created by MoveWithRollback. Call after validation succeeds. + internal static void CommitMove (string targetPath, Action logger) + { + // Find and clean up any leftover backup directories + var parentDir = Path.GetDirectoryName (targetPath); + if (string.IsNullOrEmpty (parentDir) || !Directory.Exists (parentDir)) + return; + + var dirName = Path.GetFileName (targetPath); + foreach (var dir in Directory.GetDirectories (parentDir, $"{dirName}.old-*")) { + TryDeleteDirectory (dir, "old backup", logger); + } + } + + /// Checks if the target path is writable by probing write access on the nearest existing ancestor. + /// + /// Follows the same pattern as dotnet/sdk WorkloadInstallerFactory.CanWriteToDotnetRoot: + /// probe with File.Create + DeleteOnClose, only catch UnauthorizedAccessException. + /// See https://github.com/dotnet/sdk/blob/db01067a9c4b67dc1806956393ec63b032032166/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallerFactory.cs + /// + internal static bool IsTargetPathWritable (string targetPath, Action logger) + { + if (string.IsNullOrEmpty (targetPath)) + return false; + + try { + targetPath = Path.GetFullPath (targetPath); + } + catch { + return false; + } + + try { + // Walk up to the nearest existing ancestor directory + var testDir = targetPath; + while (!string.IsNullOrEmpty (testDir) && !Directory.Exists (testDir)) + testDir = Path.GetDirectoryName (testDir); + + if (string.IsNullOrEmpty (testDir)) + return false; + + var testFile = Path.Combine (testDir, Path.GetRandomFileName ()); + using (File.Create (testFile, 1, FileOptions.DeleteOnClose)) { } + return true; + } + catch (UnauthorizedAccessException) { + logger (TraceLevel.Warning, $"Target path '{targetPath}' is not writable."); + return false; + } + } + + /// Checks if a path is under a given directory. + internal static bool IsUnderDirectory (string path, string directory) + { + if (string.IsNullOrEmpty (directory) || string.IsNullOrEmpty (path)) + return false; + if (path.Equals (directory, StringComparison.OrdinalIgnoreCase)) + return true; + return path.StartsWith (directory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); + } + + // Returns .msi (Windows), .pkg (macOS), or null (Linux) + internal static string? GetInstallerExtension () + { + if (OS.IsWindows) return ".msi"; + if (OS.IsMac) return ".pkg"; + return null; + } + + internal static string GetArchiveExtension () + { + return OS.IsWindows ? ".zip" : ".tar.gz"; + } + + + /// + /// Sets Unix file permissions. Uses File.SetUnixFileMode on net7.0+ (see + /// https://learn.microsoft.com/dotnet/api/system.io.file.setunixfilemode), + /// falls back to libc P/Invoke on netstandard2.0. + /// + internal static bool Chmod (string path, int mode) + { + if (OS.IsWindows) + return true; // No-op on Windows + + try { +#if NET7_0_OR_GREATER + // Managed API avoids P/Invoke overhead and works on all .NET 7+ Unix platforms. + // See https://learn.microsoft.com/dotnet/api/system.io.file.setunixfilemode + if (!OperatingSystem.IsWindows ()) { + File.SetUnixFileMode (path, (UnixFileMode) mode); + return true; + } + return true; +#else + return chmod (path, mode) == 0; +#endif + } + catch { + return false; + } + } + + /// + /// Sets executable permissions on all files in the bin/ subdirectory. + /// Uses File.SetUnixFileMode on net7.0+, falls back to chmod process on netstandard2.0. + /// + internal static async Task SetExecutablePermissionsAsync (string directory, Action logger, CancellationToken cancellationToken = default) + { + var binDir = Path.Combine (directory, "bin"); + if (!Directory.Exists (binDir)) + return; + + foreach (var file in Directory.GetFiles (binDir)) { + cancellationToken.ThrowIfCancellationRequested (); + if (!Chmod (file, 0x1ED)) { // 0755 C# does not have octal literals + // Managed chmod failed, fall back to process + var psi = ProcessUtils.CreateProcessStartInfo ("chmod", "+x", file); + int exitCode = await ProcessUtils.StartProcess (psi, null, null, cancellationToken) + .ConfigureAwait (false); + if (exitCode != 0) { + logger (TraceLevel.Error, $"Failed to set executable permission on '{file}' (exit code {exitCode})."); + throw new InvalidOperationException ($"chmod failed for '{file}' with exit code {exitCode}."); + } + } + } + } + + [DllImport ("libc", SetLastError=true)] + static extern int rename (string old, string @new); + +#if !NET7_0_OR_GREATER + [DllImport ("libc", SetLastError = true)] + static extern int chmod (string pathname, int mode); +#endif + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/IsExternalInit.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/IsExternalInit.cs new file mode 100644 index 00000000000..4f2414e663d --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/IsExternalInit.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + // Polyfill for netstandard2.0 to support C# 9+ records and init-only properties. + internal static class IsExternalInit { } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs new file mode 100644 index 00000000000..f103880483c --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs @@ -0,0 +1,509 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using System.Xml.Linq; + +using Xamarin.Android.Tools.AndroidSdk.Properties; + +namespace Xamarin.Android.Tools +{ + public class JdkInfo { + + public string HomePath {get;} + + public string? Locator {get;} + + public string JarPath {get;} + public string JavaPath {get;} + public string JavacPath {get;} + public string JdkJvmPath {get;} + public ReadOnlyCollection IncludePath {get;} + + public Version? Version => javaVersion.Value; + public string? Vendor { + get { + if (GetJavaSettingsPropertyValue ("java.vendor", out string? vendor)) + return vendor; + return null; + } + } + + public ReadOnlyDictionary ReleaseProperties {get;} + public IEnumerable JavaSettingsPropertyKeys => javaProperties.Value.Keys; + + Lazy>> javaProperties; + Lazy javaVersion; + + Action logger; + + public JdkInfo (string homePath) + : this (homePath, null, null) + { + } + + public JdkInfo (string homePath, string? locator = null, Action? logger = null) + { + if (homePath == null) + throw new ArgumentNullException (nameof (homePath)); + if (!Directory.Exists (homePath)) + throw new ArgumentException ("Not a directory", nameof (homePath)); + + HomePath = homePath; + Locator = locator; + this.logger = logger ?? AndroidSdkInfo.DefaultConsoleLogger; + + var binPath = Path.Combine (HomePath, "bin"); + JarPath = RequireExecutableInDirectory (binPath, "jar"); + JavaPath = RequireExecutableInDirectory (binPath, "java"); + JavacPath = RequireExecutableInDirectory (binPath, "javac"); + + string? jdkJvmPath = GetJdkJvmPath (); + + ValidateFile ("jvm", jdkJvmPath); + + JdkJvmPath = jdkJvmPath; + + var includes = new List (); + var jdkInclude = Path.Combine (HomePath, "include"); + + if (Directory.Exists (jdkInclude)) { + includes.Add (jdkInclude); + includes.AddRange (Directory.GetDirectories (jdkInclude)); + } + + + ReleaseProperties = GetReleaseProperties(); + + IncludePath = new ReadOnlyCollection (includes); + + javaProperties = new Lazy>> ( + () => GetJavaProperties (this.logger), + LazyThreadSafetyMode.ExecutionAndPublication); + javaVersion = new Lazy (GetJavaVersion, LazyThreadSafetyMode.ExecutionAndPublication); + } + + public JdkInfo (string homePath, string locator) + : this (homePath, locator, null) + { + } + + public override string ToString() + { + return $"JdkInfo(Version={Version}, Vendor=\"{Vendor}\", HomePath=\"{HomePath}\", Locator=\"{Locator}\")"; + } + + public bool GetJavaSettingsPropertyValues (string key, [NotNullWhen (true)] out IEnumerable? value) + { + value = null; + var props = javaProperties.Value; + if (props.TryGetValue (key, out var v)) { + value = v; + return true; + } + return false; + } + + public bool GetJavaSettingsPropertyValue (string key, [NotNullWhen (true)] out string? value) + { + value = null; + var props = javaProperties.Value; + if (props.TryGetValue (key, out var v)) { + if (v.Count > 1) + throw new InvalidOperationException ($"Requested to get one string value when property `{key}` contains `{v.Count}` values."); + value = v [0]; + return true; + } + return false; + } + + string? GetJdkJvmPath () + { + string jreDir = Path.Combine (HomePath, "jre"); + string libDir = Path.Combine (HomePath, "lib"); + + if (OS.IsMac) { + return FindLibrariesInDirectory (jreDir, "jli").FirstOrDefault () ?? + FindLibrariesInDirectory (libDir, "jli").FirstOrDefault (); + } + if (OS.IsWindows) { + string binServerDir = Path.Combine (HomePath, "bin", "server"); + return FindLibrariesInDirectory (jreDir, "jvm").FirstOrDefault () ?? + FindLibrariesInDirectory (binServerDir, "jvm").FirstOrDefault (); + } + return FindLibrariesInDirectory (jreDir, "jvm").FirstOrDefault () ?? + FindLibrariesInDirectory (libDir, "jvm").FirstOrDefault (); + } + + static IEnumerable FindLibrariesInDirectory (string dir, string libraryName) + { + if (!Directory.Exists (dir)) + return Enumerable.Empty (); + var library = string.Format (OS.NativeLibraryFormat, libraryName); + return Directory.EnumerateFiles (dir, library, SearchOption.AllDirectories); + } + + void ValidateFile (string name, [NotNull]string? path) + { + if (path == null || !File.Exists (path)) + throw new ArgumentException ($"Could not find required file `{name}` within `{HomePath}`; is this a valid JDK?", "homePath"); + } + + string RequireExecutableInDirectory (string binPath, string fileName) + { + var file = ProcessUtils.FindExecutablesInDirectory (binPath, fileName).FirstOrDefault (); + + ValidateFile (fileName, file); + + return file; + } + + static Regex NonDigitMatcher = new Regex (@"[^\d]", RegexOptions.Compiled | RegexOptions.CultureInvariant); + + Version? GetJavaVersion () + { + string? version = null; + if (ReleaseProperties.TryGetValue ("JAVA_VERSION", out version) && !string.IsNullOrEmpty (version)) { + version = GetParsableVersion (version); + if (ReleaseProperties.TryGetValue ("BUILD_NUMBER", out var build) && !string.IsNullOrEmpty (build)) + version += "." + build; + } + else if (GetJavaSettingsPropertyValue ("java.version", out version) && !string.IsNullOrEmpty (version)) { + version = GetParsableVersion (version); + } + if (string.IsNullOrEmpty (version)) + throw new NotSupportedException ("Could not determine Java version."); + var normalizedVersion = NonDigitMatcher.Replace (version, "."); + var versionParts = normalizedVersion.Split (new[]{"."}, StringSplitOptions.RemoveEmptyEntries); + + try { + if (versionParts.Length < 2) + return null; + if (versionParts.Length == 2) + return new Version (major: int.Parse (versionParts [0]), minor: int.Parse (versionParts [1])); + if (versionParts.Length == 3) + return new Version (major: int.Parse (versionParts [0]), minor: int.Parse (versionParts [1]), build: int.Parse (versionParts [2])); + // We just ignore elements 4+ + return new Version (major: int.Parse (versionParts [0]), minor: int.Parse (versionParts [1]), build: int.Parse (versionParts [2]), revision: int.Parse (versionParts [3])); + } + catch (Exception) { + return null; + } + } + + static string GetParsableVersion (string version) + { + if (!version.Contains (".")) + version += ".0"; + return version; + } + + ReadOnlyDictionary GetReleaseProperties () + { + var props = new Dictionary (); + var releasePath = Path.Combine (HomePath, "release"); + if (!File.Exists (releasePath)) + return new ReadOnlyDictionary(props); + + using (var release = File.OpenText (releasePath)) { + string? line; + while ((line = release.ReadLine ()) != null) { + line = line.Trim (); + const string PropertyDelim = "="; + int delim = line.IndexOf (PropertyDelim, StringComparison.Ordinal); + if (delim < 0) { + props [line] = ""; + continue; + } + string key = line.Substring (0, delim).Trim (); + string value = line.Substring (delim + PropertyDelim.Length).Trim (); + if (value.StartsWith ("\"", StringComparison.Ordinal) && value.EndsWith ("\"", StringComparison.Ordinal)) { + value = value.Substring (1, value.Length - 2); + } + props [key] = value; + } + } + return new ReadOnlyDictionary(props); + } + + Dictionary> GetJavaProperties (Action logger) + { + return GetJavaProperties ( + logger, + ProcessUtils.FindExecutablesInDirectory (Path.Combine (HomePath, "bin"), "java").First ()); + } + + static bool AnySystemJavasInstalled () + { + if (OS.IsMac) { + string path = Path.Combine (Path.DirectorySeparatorChar + "System", "Library", "Java", "JavaVirtualMachines"); + if (!Directory.Exists (path)) { + return false; + } + + string[] dirs = Directory.GetDirectories (path); + if (dirs == null || dirs.Length == 0) { + return false; + } + } + + return true; + } + + static Dictionary> GetJavaProperties (Action logger, string java) + { + var javaProps = new ProcessStartInfo { + FileName = java, + Arguments = "-XshowSettings:properties -version", + }; + + var props = new Dictionary> (); + string? curKey = null; + bool foundPS = false; + var output = new StringBuilder (); + + if (!AnySystemJavasInstalled () && (java == "/usr/bin/java" || java == "java")) + return props; + + const string PropertySettings = "Property settings:"; + + try { + ProcessUtils.Exec (javaProps, (o, e) => { + const string ContinuedValuePrefix = " "; + const string NewValuePrefix = " "; + const string NameValueDelim = " = "; + lock (output) { + output.AppendLine (e.Data); + } + if (string.IsNullOrEmpty (e.Data)) + return; + if (e.Data.StartsWith (PropertySettings, StringComparison.Ordinal)) { + foundPS = true; + return; + } + if (!foundPS) { + return; + } + if (e.Data.StartsWith (ContinuedValuePrefix, StringComparison.Ordinal)) { + if (curKey == null) { + logger (TraceLevel.Error, $"No Java property previously seen for continued value `{e.Data}`."); + return; + } + props [curKey].Add (e.Data.Substring (ContinuedValuePrefix.Length)); + return; + } + if (e.Data.StartsWith (NewValuePrefix, StringComparison.Ordinal)) { + var delim = e.Data.IndexOf (NameValueDelim, StringComparison.Ordinal); + if (delim <= 0) + return; + curKey = e.Data.Substring (NewValuePrefix.Length, delim - NewValuePrefix.Length); + var value = e.Data.Substring (delim + NameValueDelim.Length); + List? values; + if (!props.TryGetValue (curKey!, out values)) + props.Add (curKey, values = new List ()); + values.Add (value); + } + }); + } + catch (Exception e) { + logger (TraceLevel.Error, $"Error retrieving Java properties by running `{javaProps.FileName} {javaProps.Arguments}`: {e.Message}"); + logger (TraceLevel.Verbose, e.ToString ()); + return props; + } + + if (!foundPS) { + logger (TraceLevel.Warning, $"No Java properties found; did not find `{PropertySettings}` in `{java} -XshowSettings:properties -version` output: ```{output.ToString ()}```"); + } + + return props; + } + + // Keep ordering in sync w/ GetSupportedJdkInfos + public static IEnumerable GetKnownSystemJdkInfos (Action? logger = null) + { + logger = logger ?? AndroidSdkInfo.DefaultConsoleLogger; + + return GetEnvironmentVariableJdks (EnvironmentVariableNames.JiJavaHome, logger) + .Concat (JdkLocations.GetPreferredJdks (logger)) + .Concat (XAPrepareJdkLocations.GetXAPrepareJdks (logger)) + .Concat (MicrosoftOpenJdkLocations.GetMicrosoftOpenJdks (logger)) + .Concat (EclipseAdoptiumJdkLocations.GetEclipseAdoptiumJdks (logger)) + .Concat (AzulJdkLocations.GetAzulJdks (logger)) + .Concat (OracleJdkLocations.GetOracleJdks (logger)) + .Concat (VSAndroidJdkLocations.GetVSAndroidJdks (logger)) + .Concat (MicrosoftDistJdkLocations.GetMicrosoftDistJdks (logger)) + .Concat (GetEnvironmentVariableJdks (EnvironmentVariableNames.JavaHome, logger)) + .Concat (GetPathEnvironmentJdks (logger)) + .Concat (GetLibexecJdks (logger)) + .Concat (GetJavaAlternativesJdks (logger)) + ; + } + + // Keep ordering in sync w/ GetKnownSystemJdkInfos + public static IEnumerable GetSupportedJdkInfos (Action? logger = null) + { + logger = logger ?? AndroidSdkInfo.DefaultConsoleLogger; + + return MicrosoftOpenJdkLocations.GetMicrosoftOpenJdks (logger) + .Concat (EclipseAdoptiumJdkLocations.GetEclipseAdoptiumJdks (logger)) + .Concat (MicrosoftDistJdkLocations.GetMicrosoftDistJdks (logger)) + ; + } + + internal static JdkInfo? TryGetJdkInfo (string path, Action logger, string locator) + { + JdkInfo? jdk = null; + try { + jdk = new JdkInfo (path, locator); + } + catch (Exception e) { + logger (TraceLevel.Warning, string.Format (Resources.InvalidJdkDirectory_path_locator_message, path, locator, e.Message)); + logger (TraceLevel.Verbose, e.ToString ()); + } + return jdk; + } + + static IEnumerable GetEnvironmentVariableJdks (string envVar, Action logger) + { + var java_home = Environment.GetEnvironmentVariable (envVar); + if (string.IsNullOrEmpty (java_home)) + yield break; + var jdk = TryGetJdkInfo (java_home, logger, $"${envVar}"); + if (jdk != null) + yield return jdk; + } + + // macOS + static IEnumerable GetLibexecJdks (Action logger) + { + return GetLibexecJdkPaths (logger) + .Distinct () + .Select (p => TryGetJdkInfo (p, logger, "`/usr/libexec/java_home -X`")) + .Where (jdk => jdk != null) + .Select (jdk => jdk!) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetLibexecJdkPaths (Action logger) + { + if (!OS.IsMac) { + yield break; + } + + var java_home = Path.GetFullPath ("/usr/libexec/java_home"); + if (!File.Exists (java_home)) { + yield break; + } + var jhp = new ProcessStartInfo { + FileName = java_home, + Arguments = "-X", + }; + var xml = new StringBuilder (); + ProcessUtils.Exec (jhp, (o, e) => { + if (string.IsNullOrEmpty (e.Data)) + return; + xml.Append (e.Data); + }, includeStderr: false); + + XElement plist; + try { + plist = XElement.Parse (xml.ToString ()); + } catch (XmlException e) { + logger (TraceLevel.Warning, string.Format (Resources.InvalidXmlLibExecJdk_path_args_message, jhp.FileName, jhp.Arguments, e.Message)); + yield break; + } + foreach (var info in plist.Elements ("array").Elements ("dict")) { + var JVMHomePath = (XNode?) info.Elements ("key").FirstOrDefault (e => e.Value == "JVMHomePath"); + if (JVMHomePath == null) + continue; + while (JVMHomePath.NextNode!.NodeType != XmlNodeType.Element) + JVMHomePath = JVMHomePath.NextNode!; + var strElement = (XElement) JVMHomePath.NextNode!; + var path = strElement.Value; + yield return path; + } + } + + // Linux; Ubuntu & Derivatives + static IEnumerable GetJavaAlternativesJdks (Action logger) + { + return GetJavaAlternativesJdkPaths () + .Distinct () + .Select (p => TryGetJdkInfo (p, logger, "`/usr/sbin/update-java-alternatives -l`")) + .Where (jdk => jdk != null) + .Select (jdk => jdk!); + } + + static IEnumerable GetJavaAlternativesJdkPaths () + { + if (!OS.IsLinux) { + return Enumerable.Empty (); + } + + var alternatives = Path.GetFullPath ("/usr/sbin/update-java-alternatives"); + if (!File.Exists (alternatives)) + return Enumerable.Empty (); + + var psi = new ProcessStartInfo { + FileName = alternatives, + Arguments = "-l", + }; + var paths = new List (); + ProcessUtils.Exec (psi, (o, e) => { + if (string.IsNullOrWhiteSpace (e.Data)) + return; + // Example line: + // java-1.8.0-openjdk-amd64 1081 /usr/lib/jvm/java-1.8.0-openjdk-amd64 + var columns = e.Data.Split (new[]{ ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (columns.Length <= 2) + return; + paths.Add (columns [2]); + }); + return paths; + } + + // Last-ditch fallback! + static IEnumerable GetPathEnvironmentJdks (Action logger) + { + return GetPathEnvironmentJdkPaths (logger) + .Select (p => TryGetJdkInfo (p, logger, "$PATH")) + .Where (jdk => jdk != null) + .Select (jdk => jdk!); + } + + static IEnumerable GetPathEnvironmentJdkPaths (Action logger) + { + foreach (var java in ProcessUtils.FindExecutablesInPath ("java")) { + var props = GetJavaProperties (logger, java); + if (props.TryGetValue ("java.home", out var java_homes)) { + var java_home = java_homes [0]; + // `java -XshowSettings:properties -version 2>&1 | grep java.home` ends with `/jre` on macOS. + // We need the parent dir so we can properly lookup the `include` directories + if (java_home.EndsWith ("jre", StringComparison.OrdinalIgnoreCase)) { + java_home = Path.GetDirectoryName (java_home) ?? ""; + } + yield return java_home; + } + } + } + } + + class JdkInfoVersionComparer : IComparer + { + public static readonly IComparer Default = new JdkInfoVersionComparer (); + + public int Compare ([AllowNull]JdkInfo x, [AllowNull]JdkInfo y) + { + if (x?.Version != null && y?.Version != null) + return x.Version.CompareTo (y.Version); + return 0; + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs new file mode 100644 index 00000000000..74b344dffe4 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs @@ -0,0 +1,336 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ + /// + /// Provides JDK installation capabilities using the Microsoft Build of OpenJDK. + /// Downloads from https://aka.ms/download-jdk with SHA-256 verification. + /// See https://www.microsoft.com/openjdk for more information. + /// + public class JdkInstaller : IDisposable + { + const string DownloadUrlBase = "https://aka.ms/download-jdk"; + + /// Gets the recommended JDK major version for .NET Android development. + public static int RecommendedMajorVersion => 21; + + /// Gets the supported JDK major versions available for installation. + public static IReadOnlyList SupportedVersions { get; } = [ RecommendedMajorVersion ]; + + static readonly IProgress NullProgress = new Progress (); + + static readonly HttpClient httpClient = new(); + readonly Action logger; + + public JdkInstaller (Action? logger = null) + { + this.logger = logger ?? AndroidSdkInfo.DefaultConsoleLogger; + } + + public void Dispose () { } + + /// Discovers available Microsoft OpenJDK versions for the current platform. + public async Task> DiscoverAsync (CancellationToken cancellationToken = default) + { + var results = new List (); + + foreach (var version in SupportedVersions) { + cancellationToken.ThrowIfCancellationRequested (); + try { + var info = BuildVersionInfo (version); + + // Verify the download URL is valid with a HEAD request + using var request = new HttpRequestMessage (HttpMethod.Head, info.DownloadUrl); + using var response = await httpClient.SendAsync (request, cancellationToken).ConfigureAwait (false); + + if (!response.IsSuccessStatusCode) { + logger (TraceLevel.Warning, $"JDK {version} not available: HEAD {info.DownloadUrl} returned {(int) response.StatusCode}"); + continue; + } + + if (response.Content.Headers.ContentLength.HasValue) + info.Size = response.Content.Headers.ContentLength.Value; + + if (response.RequestMessage?.RequestUri is not null) + info.ResolvedUrl = response.RequestMessage.RequestUri.ToString (); + + info.Checksum = await DownloadUtils.FetchChecksumAsync (httpClient, info.ChecksumUrl, $"JDK {version}", logger, cancellationToken).ConfigureAwait (false); + if (string.IsNullOrEmpty (info.Checksum)) + logger (TraceLevel.Warning, $"Could not fetch checksum for JDK {version}, integrity verification will be skipped."); + + results.Add (info); + logger (TraceLevel.Info, $"Discovered {info.DisplayName} (size={info.Size})"); + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) { + logger (TraceLevel.Warning, $"Failed to discover JDK {version}: {ex.Message}"); + logger (TraceLevel.Verbose, ex.ToString ()); + } + } + + return results.AsReadOnly (); + } + + /// + /// Downloads and installs a Microsoft OpenJDK. + /// When running elevated, uses the platform installer (.msi/.pkg) which chooses its own install location; + /// is ignored in that case. When non-elevated, extracts to . + /// + public async Task InstallAsync (int majorVersion, string targetPath, IProgress? progress = null, CancellationToken cancellationToken = default) + { + progress ??= NullProgress; + + if (!SupportedVersions.Contains (majorVersion)) + throw new ArgumentException ($"JDK version {majorVersion} is not supported. Supported versions: {string.Join (", ", SupportedVersions)}", nameof (majorVersion)); + + if (string.IsNullOrEmpty (targetPath)) + throw new ArgumentNullException (nameof (targetPath)); + + // When elevated and a platform installer is available, use it and let the installer handle paths + if (ProcessUtils.IsElevated () && FileUtil.GetInstallerExtension () is not null) { + logger (TraceLevel.Info, "Running elevated — using platform installer (.msi/.pkg)."); + await InstallWithPlatformInstallerAsync (majorVersion, progress, cancellationToken).ConfigureAwait (false); + return; + } + + if (!FileUtil.IsTargetPathWritable (targetPath, logger)) { + logger (TraceLevel.Error, $"Target path '{targetPath}' is not writable or is in a restricted location."); + throw new ArgumentException ($"Target path '{targetPath}' is not writable or is in a restricted location.", nameof (targetPath)); + } + + var versionInfo = BuildVersionInfo (majorVersion); + + // Fetch checksum - required for supply-chain integrity + var checksum = await DownloadUtils.FetchChecksumAsync (httpClient, versionInfo.ChecksumUrl, $"JDK {majorVersion}", logger, cancellationToken).ConfigureAwait (false); + if (string.IsNullOrEmpty (checksum)) + throw new InvalidOperationException ($"Failed to fetch SHA-256 checksum for JDK {majorVersion}. Cannot verify download integrity."); + versionInfo.Checksum = checksum; + + var tempArchivePath = Path.Combine (Path.GetTempPath (), $"microsoft-jdk-{majorVersion}-{Guid.NewGuid ()}{FileUtil.GetArchiveExtension ()}"); + + try { + // Download + logger (TraceLevel.Info, $"Downloading Microsoft OpenJDK {majorVersion} from {versionInfo.DownloadUrl}"); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Downloading, 0, $"Downloading Microsoft OpenJDK {majorVersion}...")); + await DownloadUtils.DownloadFileAsync (httpClient, versionInfo.DownloadUrl, tempArchivePath, versionInfo.Size, + new Progress<(double pct, string msg)> (p => progress.Report (new JdkInstallProgress (JdkInstallPhase.Downloading, p.pct, p.msg))), + cancellationToken).ConfigureAwait (false); + logger (TraceLevel.Info, $"Download complete: {tempArchivePath}"); + + // Verify checksum + progress.Report (new JdkInstallProgress (JdkInstallPhase.Verifying, 0, "Verifying SHA-256 checksum...")); + DownloadUtils.VerifyChecksum (tempArchivePath, versionInfo.Checksum!); + logger (TraceLevel.Info, "Checksum verified."); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Verifying, 100, "Checksum verified.")); + + // Extract + logger (TraceLevel.Info, $"Extracting JDK to {targetPath}"); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Extracting, 0, "Extracting JDK...")); + await ExtractArchiveAsync (tempArchivePath, targetPath, cancellationToken).ConfigureAwait (false); + logger (TraceLevel.Info, "Extraction complete."); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Extracting, 100, "Extraction complete.")); + + // Validate + progress.Report (new JdkInstallProgress (JdkInstallPhase.Validating, 0, "Validating installation...")); + if (!IsValid (targetPath)) { + logger (TraceLevel.Error, $"JDK installation at '{targetPath}' failed validation."); + FileUtil.TryDeleteDirectory (targetPath, "invalid installation", logger); + throw new InvalidOperationException ($"JDK installation at '{targetPath}' failed validation. The extracted files may be corrupted."); + } + + // Validation passed — commit the move by cleaning up any backup + FileUtil.CommitMove (targetPath, logger); + logger (TraceLevel.Info, $"Microsoft OpenJDK {majorVersion} installed successfully at {targetPath}"); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Validating, 100, "Validation complete.")); + + progress.Report (new JdkInstallProgress (JdkInstallPhase.Complete, 100, $"Microsoft OpenJDK {majorVersion} installed successfully.")); + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) when (ex is not ArgumentException and not ArgumentNullException) { + logger (TraceLevel.Error, $"JDK installation failed: {ex.Message}"); + logger (TraceLevel.Verbose, ex.ToString ()); + throw; + } + finally { + FileUtil.TryDeleteFile (tempArchivePath, logger); + } + } + + /// Validates whether the path contains a valid JDK installation. + public bool IsValid (string jdkPath) + { + if (string.IsNullOrEmpty (jdkPath) || !Directory.Exists (jdkPath)) + return false; + + try { + var jdk = new JdkInfo (jdkPath, logger: logger); + return jdk.Version is not null; + } + catch (Exception ex) { + logger (TraceLevel.Warning, $"JDK validation failed for '{jdkPath}': {ex.Message}"); + logger (TraceLevel.Verbose, ex.ToString ()); + return false; + } + } + + async Task InstallWithPlatformInstallerAsync (int majorVersion, IProgress progress, CancellationToken cancellationToken) + { + var installerExt = FileUtil.GetInstallerExtension ()!; + var info = BuildVersionInfo (majorVersion, installerExt); + + // Fetch checksum before download for supply-chain integrity + var checksum = await DownloadUtils.FetchChecksumAsync (httpClient, info.ChecksumUrl, $"JDK {majorVersion} installer", logger, cancellationToken).ConfigureAwait (false); + if (string.IsNullOrEmpty (checksum)) + throw new InvalidOperationException ($"Failed to fetch SHA-256 checksum for JDK {majorVersion} installer. Cannot verify download integrity."); + info.Checksum = checksum; + + var tempInstallerPath = Path.Combine (Path.GetTempPath (), $"microsoft-jdk-{majorVersion}-{Guid.NewGuid ()}{installerExt}"); + + try { + // Download installer + logger (TraceLevel.Info, $"Downloading installer from {info.DownloadUrl}"); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Downloading, 0, $"Downloading Microsoft OpenJDK {majorVersion} installer...")); + await DownloadUtils.DownloadFileAsync (httpClient, info.DownloadUrl, tempInstallerPath, info.Size, + new Progress<(double pct, string msg)> (p => progress.Report (new JdkInstallProgress (JdkInstallPhase.Downloading, p.pct, p.msg))), + cancellationToken).ConfigureAwait (false); + + progress.Report (new JdkInstallProgress (JdkInstallPhase.Verifying, 0, "Verifying SHA-256 checksum...")); + DownloadUtils.VerifyChecksum (tempInstallerPath, info.Checksum!); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Verifying, 100, "Checksum verified.")); + + // Run the installer silently + progress.Report (new JdkInstallProgress (JdkInstallPhase.Extracting, 0, "Running platform installer...")); + await RunPlatformInstallerAsync (tempInstallerPath, cancellationToken).ConfigureAwait (false); + + logger (TraceLevel.Info, $"Microsoft OpenJDK {majorVersion} installed successfully via platform installer."); + progress.Report (new JdkInstallProgress (JdkInstallPhase.Complete, 100, $"Microsoft OpenJDK {majorVersion} installed successfully.")); + } + finally { + FileUtil.TryDeleteFile (tempInstallerPath, logger); + } + } + + async Task RunPlatformInstallerAsync (string installerPath, CancellationToken cancellationToken) + { + var psi = OS.IsWindows + ? ProcessUtils.CreateProcessStartInfo ("msiexec", "/i", installerPath, "/quiet", "/norestart") + : ProcessUtils.CreateProcessStartInfo ("/usr/sbin/installer", "-pkg", installerPath, "-target", "/"); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, stdout: stdout, stderr: stderr, cancellationToken).ConfigureAwait (false); + + if (exitCode != 0) { + var errorOutput = stderr.ToString (); + logger (TraceLevel.Error, $"Installer failed (exit code {exitCode}): {errorOutput}"); + throw new InvalidOperationException ($"Platform installer failed with exit code {exitCode}: {errorOutput}"); + } + } + + /// Removes a JDK installation at the specified path. + public bool Remove (string jdkPath) + { + if (string.IsNullOrEmpty (jdkPath) || !Directory.Exists (jdkPath)) + return false; + + try { + Directory.Delete (jdkPath, recursive: true); + logger (TraceLevel.Info, $"Removed JDK at '{jdkPath}'."); + return true; + } + catch (Exception ex) { + logger (TraceLevel.Error, $"Failed to remove JDK at '{jdkPath}': {ex.Message}"); + logger (TraceLevel.Verbose, ex.ToString ()); + return false; + } + } + + static JdkVersionInfo BuildVersionInfo (int majorVersion, string? extensionOverride = null) + { + var os = GetMicrosoftOpenJDKOSName (); + var arch = GetArchitectureName (); + var ext = extensionOverride ?? FileUtil.GetArchiveExtension (); + + var filename = $"microsoft-jdk-{majorVersion}-{os}-{arch}{ext}"; + var downloadUrl = $"{DownloadUrlBase}/{filename}"; + var checksumUrl = $"{downloadUrl}.sha256sum.txt"; + var displayName = extensionOverride is not null + ? $"Microsoft OpenJDK {majorVersion} ({ext})" + : $"Microsoft OpenJDK {majorVersion}"; + + return new JdkVersionInfo ( + majorVersion: majorVersion, + displayName: displayName, + downloadUrl: downloadUrl, + checksumUrl: checksumUrl + ); + } + + async Task ExtractArchiveAsync (string archivePath, string targetPath, CancellationToken cancellationToken) + { + var targetParent = Path.GetDirectoryName (targetPath); + if (string.IsNullOrEmpty (targetParent)) + targetParent = Path.GetTempPath (); + else + Directory.CreateDirectory (targetParent); + var tempExtractPath = Path.Combine (targetParent, $".jdk-extract-{Guid.NewGuid ()}"); + + try { + Directory.CreateDirectory (tempExtractPath); + + if (OS.IsWindows) + DownloadUtils.ExtractZipSafe (archivePath, tempExtractPath, cancellationToken); + else + await DownloadUtils.ExtractTarGzAsync (archivePath, tempExtractPath, logger, cancellationToken).ConfigureAwait (false); + + // Find the actual JDK root (archives contain a single top-level directory) + var extractedDirs = Directory.GetDirectories (tempExtractPath); + var jdkRoot = extractedDirs.Length == 1 ? extractedDirs [0] : tempExtractPath; + + // On macOS, the JDK is inside Contents/Home + if (OS.IsMac) { + var contentsHome = Path.Combine (jdkRoot, "Contents", "Home"); + if (Directory.Exists (contentsHome)) + jdkRoot = contentsHome; + } + + FileUtil.MoveWithRollback (jdkRoot, targetPath, logger); + } + finally { + FileUtil.TryDeleteDirectory (tempExtractPath, "temp extract directory", logger); + } + } + + static string GetMicrosoftOpenJDKOSName () + { + if (OS.IsMac) return "macOS"; + if (OS.IsWindows) return "windows"; + if (OS.IsLinux) return "linux"; + throw new PlatformNotSupportedException ("Unsupported platform"); + } + + static string GetArchitectureName () + { + return RuntimeInformation.OSArchitecture switch { + Architecture.X64 => "x64", + Architecture.Arm64 => "aarch64", + _ => throw new PlatformNotSupportedException ($"Unsupported architecture: {RuntimeInformation.OSArchitecture}"), + }; + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/AzulJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/AzulJdkLocations.cs new file mode 100644 index 00000000000..13d10c1ffab --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/AzulJdkLocations.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class AzulJdkLocations : JdkLocations { + + internal static IEnumerable GetAzulJdks (Action logger) + { + return GetMacOSSystemJdks ("zulu-*.jdk", logger) + .Concat (GetWindowsFileSystemJdks (Path.Combine ("Zulu", "zulu-*"), logger)) + .Concat (GetWindowsRegistryJdks (logger, @"SOFTWARE\Azul Systems\Zulu", "zulu-*", null, "InstallationPath")) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/EclipseAdoptiumJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/EclipseAdoptiumJdkLocations.cs new file mode 100644 index 00000000000..d9d9b75c814 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/EclipseAdoptiumJdkLocations.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class EclipseAdoptiumJdkLocations : JdkLocations { + + internal static IEnumerable GetEclipseAdoptiumJdks (Action logger) + { + return GetMacOSSystemJdks ("temurin-*.jdk", logger) + .Concat (GetMacOSSystemJdks ("adoptopenjdk-*.jdk", logger)) + .Concat (GetWindowsFileSystemJdks (Path.Combine ("AdoptOpenJDK", "jdk-*"), logger)) + .Concat (GetWindowsRegistryJdks (logger, @"SOFTWARE\AdoptOpenJDK\JDK", "*", @"hotspot\MSI", "Path")) + .Concat (GetWindowsFileSystemJdks (Path.Combine ("Eclipse Foundation", "jdk-*"), logger)) + .Concat (GetWindowsRegistryJdks (logger, @"SOFTWARE\Eclipse Foundation\JDK", "*", @"hotspot\MSI", "Path")) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.MacOS.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.MacOS.cs new file mode 100644 index 00000000000..9eb85154ec0 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.MacOS.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Xamarin.Android.Tools { + + partial class JdkLocations { + + static IEnumerable GetUnixPreferredJdks (Action logger) + { + return GetUnixConfiguredJdkPaths (logger) + .Select (p => JdkInfo.TryGetJdkInfo (p, logger, "monodroid-config.xml")) + .Where (jdk => jdk != null) + .Select (jdk => jdk!) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetUnixConfiguredJdkPaths (Action logger) + { + var config = AndroidSdkUnix.GetUnixConfigFile (logger); + + foreach (var java_sdk in config.Elements ("java-sdk")) { + var path = (string?) java_sdk.Attribute ("path"); + if (path != null && !string.IsNullOrEmpty (path)) { + yield return path; + } + } + } + + const string MacOSJavaVirtualMachinesRoot = "/Library/Java/JavaVirtualMachines"; + + protected static IEnumerable GetMacOSUserFileSystemJdks (Action logger) + { + if (!OS.IsMac) { + return Array.Empty (); + } + + // Search ~/Library/Android/*jdk*/ (matches microsoft-21.jdk, jdk-21, etc.) + var libraryAndroidRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Android"); + if (!Directory.Exists (libraryAndroidRoot)) { + return Array.Empty (); + } + + IEnumerable dirs; + try { + dirs = Directory.EnumerateDirectories (libraryAndroidRoot, "*jdk*"); + } + catch (IOException) { + return Array.Empty (); + } + + var toHome = Path.Combine ("Contents", "Home"); + var paths = new List (); + foreach (var dir in dirs) { + // Check for macOS .jdk bundle structure (Contents/Home) + var bundleHome = Path.Combine (dir, toHome); + if (Directory.Exists (bundleHome)) { + paths.Add (bundleHome); + } else { + // Flat JDK structure - let TryGetJdkInfo validate + paths.Add (dir); + } + } + + return FromPaths (paths, logger, "~/Library/Android/*jdk*/"); + } + + protected static IEnumerable GetMacOSSystemJdks (string pattern, Action logger, string? locator = null) + { + if (!OS.IsMac) { + return Array.Empty(); + } + locator = locator ?? Path.Combine (MacOSJavaVirtualMachinesRoot, pattern); + return FromPaths (GetMacOSSystemJvmPaths (pattern), logger, locator); + } + + static IEnumerable GetMacOSSystemJvmPaths (string pattern) + { + var root = MacOSJavaVirtualMachinesRoot; + var toHome = Path.Combine ("Contents", "Home"); + var jdks = AppDomain.CurrentDomain.GetData ($"GetMacOSMicrosoftJdkPaths jdks override! {typeof (JdkInfo).AssemblyQualifiedName}") + ?.ToString (); + if (jdks != null) { + root = jdks; + toHome = ""; + pattern = "*"; + } + if (!Directory.Exists (root)) { + yield break; + } + foreach (var dir in Directory.EnumerateDirectories (root, pattern)) { + var home = Path.Combine (dir, toHome); + if (!Directory.Exists (home)) + continue; + yield return home; + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.Windows.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.Windows.cs new file mode 100644 index 00000000000..1eb77c2c1e4 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.Windows.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Xamarin.Android.Tools { + + partial class JdkLocations { + + internal static string? GetWindowsPreferredJdkPath () + { + var wow = RegistryEx.Wow64.Key32; + var regKey = AndroidSdkWindows.GetMDRegistryKey (); + if (RegistryEx.CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, regKey, AndroidSdkWindows.MDREG_JAVA_SDK, wow, "bin", "java.exe")) + return RegistryEx.GetValueString (RegistryEx.CurrentUser, regKey, AndroidSdkWindows.MDREG_JAVA_SDK, wow); + return null; + + } + + protected static IEnumerable GetWindowsFileSystemJdks (string pattern, Action logger, string? locator = null) + { + if (!OS.IsWindows) { + yield break; + } + + var roots = new List() { + // "Compatibility" with existing codebases; should probably be avoided… + Environment.ExpandEnvironmentVariables ("%ProgramW6432%"), + Environment.GetFolderPath (Environment.SpecialFolder.ProgramFiles), + Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), + }; + foreach (var root in roots) { + if (!Directory.Exists (root)) + continue; + IEnumerable homes; + try { + homes = Directory.EnumerateDirectories (root, pattern); + } + catch (IOException) { + continue; + } + foreach (var home in homes) { + if (!File.Exists (Path.Combine (home, "bin", "java.exe"))) + continue; + var loc = locator ?? $"{pattern} via {root}"; + var jdk = JdkInfo.TryGetJdkInfo (home, logger, loc); + if (jdk == null) + continue; + yield return jdk; + } + } + } + + protected static IEnumerable GetWindowsUserFileSystemJdks (Action logger) + { + if (!OS.IsWindows) { + yield break; + } + + var root = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android"); + if (!Directory.Exists (root)) { + yield break; + } + + IEnumerable homes; + try { + // Match any folder containing "jdk" (jdk-21, microsoft-21.jdk, jdk-21.0.8.1-hotspot, etc.) + homes = Directory.EnumerateDirectories (root, "*jdk*"); + } + catch (IOException) { + yield break; + } + + foreach (var home in homes) { + var jdk = JdkInfo.TryGetJdkInfo (home, logger, @"%LocalAppData%\Android\*jdk*"); + if (jdk == null) + continue; + yield return jdk; + } + } + + protected static IEnumerable GetWindowsRegistryJdks ( + Action logger, + string parentKey, + string childKeyGlob, + string? grandChildKey, + string valueName, + string? locator = null) + { + if (!OS.IsWindows) { + yield break; + } + + var regex = ToRegex (childKeyGlob); + var paths = new List<(Version version, string path)> (); + var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; + var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }; + foreach (var root in roots) + foreach (var wow in wows) { + foreach (var subkeyName in RegistryEx.EnumerateSubkeys (root, parentKey, wow)) { + if (!regex.Match (subkeyName).Success) { + continue; + } + var key = $@"{parentKey}\{subkeyName}" + + (grandChildKey == null ? "" : "\\" + grandChildKey); + var path = RegistryEx.GetValueString (root, key, valueName, wow); + if (path == null) { + continue; + } + var jdk = JdkInfo.TryGetJdkInfo (path, logger, locator ?? $"Windows Registry @ {parentKey}"); + if (jdk == null) { + continue; + } + yield return jdk; + } + } + } + + static Regex ToRegex (string glob) + { + var r = new StringBuilder (); + foreach (char c in glob) { + switch (c) { + case '*': + r.Append (".*"); + break; + case '?': + r.Append ("."); + break; + default: + r.Append (Regex.Escape (c.ToString ())); + break; + } + } + return new Regex (r.ToString (), RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.cs new file mode 100644 index 00000000000..9f87523eb80 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Xamarin.Android.Tools { + + partial class JdkLocations { + + internal static IEnumerable GetPreferredJdks (Action logger) + { + if (OS.IsWindows) { + var path = GetWindowsPreferredJdkPath (); + var jdk = path == null ? null : JdkInfo.TryGetJdkInfo (path, logger, "Windows Registry"); + if (jdk != null) + yield return jdk; + } + foreach (var jdk in GetUnixPreferredJdks (logger)) { + yield return jdk; + } + } + + protected static IEnumerable FromPaths (IEnumerable paths, Action logger, string locator) + { + return paths + .Select (p => JdkInfo.TryGetJdkInfo (p, logger, locator)) + .Where (jdk => jdk != null) + .Select (jdk => jdk!); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftDistJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftDistJdkLocations.cs new file mode 100644 index 00000000000..5e9ae60e2ed --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftDistJdkLocations.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class MicrosoftDistJdkLocations : JdkLocations { + + internal static IEnumerable GetMicrosoftDistJdks (Action logger) + { + return FromPaths (GetMacOSMicrosoftDistJdkPaths (), logger, "$HOME/Library/Developer/Xamarin/jdk") + .Concat (GetWindowsFileSystemJdks (Path.Combine ("Android", "jdk", "microsoft_dist_openjdk_*"), logger, locator: "legacy microsoft_dist_openjdk")) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetMacOSMicrosoftDistJdkPaths () + { + if (!OS.IsMac) { + return Array.Empty (); + } + + var jdks = AppDomain.CurrentDomain.GetData ($"GetMacOSMicrosoftJdkPaths jdks override! {typeof (JdkInfo).AssemblyQualifiedName}") + ?.ToString (); + if (jdks == null) { + var home = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); + jdks = Path.Combine (home, "Library", "Developer", "Xamarin", "jdk"); + } + if (!Directory.Exists (jdks)) + return Enumerable.Empty (); + + return Directory.EnumerateDirectories (jdks); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftOpenJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftOpenJdkLocations.cs new file mode 100644 index 00000000000..d9ee1736fad --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftOpenJdkLocations.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class MicrosoftOpenJdkLocations : JdkLocations { + + internal static IEnumerable GetMicrosoftOpenJdks (Action logger) + { + return GetMacOSSystemJdks ("microsoft-*.jdk", logger) + .Concat (GetMacOSUserFileSystemJdks (logger)) + .Concat (GetWindowsFileSystemJdks (Path.Combine ("Android", "openjdk", "jdk-*"), logger)) + .Concat (GetWindowsFileSystemJdks (Path.Combine ("Microsoft", "jdk-*"), logger)) + .Concat (GetWindowsRegistryJdks (logger, @"SOFTWARE\Microsoft\JDK", "*", @"hotspot\MSI", "Path")) + .Concat (GetWindowsUserFileSystemJdks (logger)) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/OracleJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/OracleJdkLocations.cs new file mode 100644 index 00000000000..a49f2ec7ee3 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/OracleJdkLocations.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class OracleJdkLocations : JdkLocations { + + internal static IEnumerable GetOracleJdks (Action logger) + { + if (!OS.IsWindows) { + yield break; + } + foreach (var path in GetOracleJdkPaths ()) { + var jdk = JdkInfo.TryGetJdkInfo (path, logger, "Oracle Registry"); + if (jdk == null) { + continue; + } + yield return jdk; + } + } + + static IEnumerable GetOracleJdkPaths () + { + string subkey = @"SOFTWARE\JavaSoft\Java Development Kit"; + + foreach (var wow64 in new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }) { + var currentVersion = RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey, "CurrentVersion", wow64); + + if (!string.IsNullOrEmpty (currentVersion)) { + + if (RegistryEx.CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64, "bin", "java.exe")) + yield return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64) ?? ""; + } + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/VSAndroidJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/VSAndroidJdkLocations.cs new file mode 100644 index 00000000000..2c5dcc931ad --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/VSAndroidJdkLocations.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class VSAndroidJdkLocations : JdkLocations { + + internal static IEnumerable GetVSAndroidJdks (Action logger) + { + if (!OS.IsWindows) { + yield break; + } + var root = RegistryEx.LocalMachine; + var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }; + var subKey = @"SOFTWARE\Microsoft\VisualStudio\Android"; + var valueName = "JavaHome"; + + foreach (var wow in wows) { + if (!RegistryEx.CheckRegistryKeyForExecutable (root, subKey, valueName, wow, "bin", "java.exe")) { + continue; + } + var path = RegistryEx.GetValueString (root, subKey, valueName, wow) ?? ""; + if (string.IsNullOrEmpty (path)) { + continue; + } + var jdk = JdkInfo.TryGetJdkInfo (path, logger, subKey); + if (jdk == null) { + continue; + } + yield return jdk; + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/XAPrepareJdkLocations.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/XAPrepareJdkLocations.cs new file mode 100644 index 00000000000..80c1ed8bc30 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Jdks/XAPrepareJdkLocations.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools { + + class XAPrepareJdkLocations : JdkLocations { + + internal static IEnumerable GetXAPrepareJdks (Action logger) + { + return FromPaths (GetXAPrepareJdkPaths (), logger, "android-toolchain"); + } + + static IEnumerable GetXAPrepareJdkPaths () + { + var androidToolchainDir = Path.Combine (Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "android-toolchain"); + if (!Directory.Exists (androidToolchainDir)) { + return Array.Empty (); + } + return Directory.EnumerateDirectories (androidToolchainDir, "jdk*"); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs new file mode 100644 index 00000000000..a14941ee5f1 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents an Android device or emulator from 'adb devices -l' output. +/// Mirrors the metadata produced by dotnet/android's GetAvailableAndroidDevices task. +/// +public class AdbDeviceInfo +{ + /// + /// Serial number of the device (e.g., "emulator-5554", "0A041FDD400327"). + /// For non-running emulators, this is the AVD name. + /// + public string Serial { get; set; } = string.Empty; + + /// + /// Human-friendly description of the device (e.g., "Pixel 7 API 35", "Pixel 6 Pro"). + /// + public string Description { get; set; } = string.Empty; + + /// + /// Device type: Device or Emulator. + /// + public AdbDeviceType Type { get; set; } + + /// + /// Device status: Online, Offline, Unauthorized, NoPermissions, NotRunning, Unknown. + /// + public AdbDeviceStatus Status { get; set; } + + /// + /// AVD name for emulators (e.g., "pixel_7_api_35"). Null for physical devices. + /// + public string? AvdName { get; set; } + + /// + /// Device model from adb properties (e.g., "Pixel_6_Pro"). + /// + public string? Model { get; set; } + + /// + /// Product name from adb properties (e.g., "raven"). + /// + public string? Product { get; set; } + + /// + /// Device code name from adb properties (e.g., "raven"). + /// + public string? Device { get; set; } + + /// + /// Transport ID from adb properties. + /// + public string? TransportId { get; set; } + + /// + /// Whether this device is an emulator. + /// + public bool IsEmulator => Type == AdbDeviceType.Emulator; +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceStatus.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceStatus.cs new file mode 100644 index 00000000000..18b51a4917e --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceStatus.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents the status of an Android device. +/// +public enum AdbDeviceStatus +{ + Online, + Offline, + Unauthorized, + NoPermissions, + NotRunning, + Unknown +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceType.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceType.cs new file mode 100644 index 00000000000..7662d1b3052 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceType.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents the type of an Android device. +/// +public enum AdbDeviceType +{ + Device, + Emulator +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs new file mode 100644 index 00000000000..1ea65cb4050 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents a hardware device profile (e.g., "pixel_7", "Nexus 5X") from avdmanager list device --compact. +/// +public record AvdDeviceProfile (string Id); diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs new file mode 100644 index 00000000000..6116e6cf88c --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +public record AvdInfo (string Name, string? DeviceProfile = null, string? Path = null); diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs new file mode 100644 index 00000000000..698e1a0e9da --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Xamarin.Android.Tools; + +/// +/// Options for booting an Android emulator. +/// +public record EmulatorBootOptions +{ + public TimeSpan BootTimeout { get; init; } = TimeSpan.FromSeconds (300); + public List? AdditionalArgs { get; init; } + public bool ColdBoot { get; init; } + public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds (500); +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootResult.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootResult.cs new file mode 100644 index 00000000000..e73cb939402 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootResult.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Classifies the reason an emulator boot operation failed. +/// +public enum EmulatorBootErrorKind +{ + /// No error — the boot succeeded. + None, + + /// The emulator process could not be launched (e.g., binary not found, AVD missing). + LaunchFailed, + + /// The emulator launched but did not finish booting within the allowed timeout. + Timeout, + + /// The boot was cancelled via . + Cancelled, + + /// An unexpected error occurred. + Unknown, +} + +/// +/// Result of an emulator boot operation. +/// +public record EmulatorBootResult +{ + public bool Success { get; init; } + public string? Serial { get; init; } + public string? ErrorMessage { get; init; } + + /// + /// Structured error classification. Consumers should switch on this value + /// instead of parsing strings. + /// + public EmulatorBootErrorKind ErrorKind { get; init; } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallPhase.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallPhase.cs new file mode 100644 index 00000000000..fff2af6164c --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallPhase.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools +{ + /// + /// Phases of JDK installation. + /// + public enum JdkInstallPhase + { + Downloading, + Verifying, + Extracting, + Validating, + Complete + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallProgress.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallProgress.cs new file mode 100644 index 00000000000..16a3e01683b --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkInstallProgress.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools +{ + /// + /// Progress information for JDK installation. + /// + public record JdkInstallProgress (JdkInstallPhase Phase, double PercentComplete, string? Message = null); +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkVersionInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkVersionInfo.cs new file mode 100644 index 00000000000..0003cc7e37a --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Jdk/JdkVersionInfo.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools +{ + /// + /// Represents information about an available JDK version from Microsoft OpenJDK. + /// + public class JdkVersionInfo + { + /// Major version number (e.g. 17, 21). + public int MajorVersion { get; } + + /// Display name for the version (e.g. "Microsoft OpenJDK 17"). + public string DisplayName { get; } + + /// Download URL for the current platform. + public string DownloadUrl { get; } + + /// URL for the SHA-256 checksum file. + public string ChecksumUrl { get; } + + /// Expected file size in bytes, or 0 if unknown. + public long Size { get; internal set; } + + /// SHA-256 checksum for download verification, if fetched. + public string? Checksum { get; internal set; } + + /// The actual download URL after following redirects (reveals the specific version). + public string? ResolvedUrl { get; internal set; } + + public JdkVersionInfo (int majorVersion, string displayName, string downloadUrl, string checksumUrl, long size = 0, string? checksum = null) + { + MajorVersion = majorVersion; + DisplayName = displayName; + DownloadUrl = downloadUrl; + ChecksumUrl = checksumUrl; + Size = size; + Checksum = checksum; + } + + public override string ToString () => DisplayName; + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/ChecksumType.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/ChecksumType.cs new file mode 100644 index 00000000000..411c1901b25 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/ChecksumType.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +public enum ChecksumType +{ + Sha1, + Sha256, +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapPhase.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapPhase.cs new file mode 100644 index 00000000000..d2a1f0195b0 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapPhase.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Phases of the SDK bootstrap operation. +/// +public enum SdkBootstrapPhase +{ + /// Reading the manifest feed. + ReadingManifest, + /// Downloading the command-line tools archive. + Downloading, + /// Verifying the downloaded archive checksum. + Verifying, + /// Extracting the archive. + Extracting, + /// Bootstrap completed successfully. + Complete +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapProgress.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapProgress.cs new file mode 100644 index 00000000000..3fe59a4d4b5 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkBootstrapProgress.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// Progress information for SDK bootstrap operations. +public record SdkBootstrapProgress (SdkBootstrapPhase Phase, int PercentComplete = -1, string Message = ""); diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkLicense.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkLicense.cs new file mode 100644 index 00000000000..7519c4822ff --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkLicense.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +public record SdkLicense (string Id, string Text); diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkManifestComponent.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkManifestComponent.cs new file mode 100644 index 00000000000..9fc5892bbb6 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkManifestComponent.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +internal record SdkManifestComponent +{ + public string ElementName { get; set; } = ""; + public string Revision { get; set; } = ""; + public string? Path { get; set; } + public string? FilesystemPath { get; set; } + public string? Description { get; set; } + public string? DownloadUrl { get; set; } + public long Size { get; set; } + public string? Checksum { get; set; } + public ChecksumType ChecksumType { get; set; } = ChecksumType.Sha1; + public bool IsObsolete { get; set; } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkPackage.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkPackage.cs new file mode 100644 index 00000000000..01fd5c63730 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Models/Sdk/SdkPackage.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// Information about an SDK package as reported by the sdkmanager CLI. +public record SdkPackage (string Path, string? Version = null, string? Description = null, bool IsInstalled = false); + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/NullableAttributes.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/NullableAttributes.cs new file mode 100644 index 00000000000..ba4e92f0106 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/NullableAttributes.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute (bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute (bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage (AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute (string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage (AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute (bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/OS.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/OS.cs new file mode 100644 index 00000000000..66a0b8fe6c0 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/OS.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.IO; +using System.Linq; +using System.Text; + +namespace Xamarin.Android.Tools +{ + public class OS + { + public readonly static bool IsWindows; + public readonly static bool IsMac; + public readonly static bool IsLinux; + + internal readonly static string? ProgramFilesX86; + + internal readonly static string NativeLibraryFormat = "{0}"; + + static OS () + { + IsWindows = Path.DirectorySeparatorChar == '\\'; + IsMac = !IsWindows && IsRunningOnMac (); + IsLinux = !IsWindows && !IsMac; + + if (IsWindows) { + ProgramFilesX86 = GetProgramFilesX86 (); + } + + if (IsWindows) + NativeLibraryFormat = "{0}.dll"; + if (IsMac) + NativeLibraryFormat = "lib{0}.dylib"; + if (IsLinux) + NativeLibraryFormat = "lib{0}.so"; + } + + //From Managed.Windows.Forms/XplatUI + static bool IsRunningOnMac () + { + IntPtr buf = IntPtr.Zero; + try { + buf = Marshal.AllocHGlobal (8192); + // This is a hacktastic way of getting sysname from uname () + if (uname (buf) == 0) { + string? os = System.Runtime.InteropServices.Marshal.PtrToStringAnsi (buf); + if (os == "Darwin") + return true; + } + } catch { + } finally { + if (buf != IntPtr.Zero) + System.Runtime.InteropServices.Marshal.FreeHGlobal (buf); + } + return false; + } + + [DllImport ("libc")] + static extern int uname (IntPtr buf); + + static string GetProgramFilesX86 () + { + //SpecialFolder.ProgramFilesX86 is broken on 32-bit WinXP + if (IntPtr.Size == 8) { + return Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86); + } else { + return Environment.GetFolderPath (Environment.SpecialFolder.ProgramFiles); + } + } + + internal static string GetXamarinAndroidCacheDir () + { + if (IsMac) { + var home = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); + return Path.Combine (home, "Library", "Caches", "Xamarin.Android"); + } else if (IsWindows) { + var localAppData = Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData); + return Path.Combine (localAppData, "Xamarin.Android", "Cache"); + } else { + var xdgCacheHome = Environment.GetEnvironmentVariable ("XDG_CACHE_HOME"); + if (string.IsNullOrEmpty (xdgCacheHome)) { + var home = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); + xdgCacheHome = Path.Combine (home, ".cache"); + } + return Path.Combine (xdgCacheHome, "Xamarin.Android"); + } + } + } + + public static class KernelEx { + [DllImport ("kernel32.dll", CharSet = CharSet.Auto)] + static extern int GetLongPathName ( + [MarshalAs (UnmanagedType.LPTStr)] string path, + [MarshalAs (UnmanagedType.LPTStr)] StringBuilder longPath, + int longPathLength + ); + + public static string GetLongPathName (string path) + { + StringBuilder sb = new StringBuilder (255); + GetLongPathName (path, sb, sb.Capacity); + return sb.ToString (); + } + + [DllImport ("kernel32.dll", CharSet = CharSet.Auto)] + static extern int GetShortPathName ( + [MarshalAs (UnmanagedType.LPTStr)] string path, + [MarshalAs (UnmanagedType.LPTStr)] StringBuilder shortPath, + int shortPathLength + ); + + public static string GetShortPathName (string path) + { + StringBuilder sb = new StringBuilder (255); + GetShortPathName (path, sb, sb.Capacity); + return sb.ToString (); + } + } + + internal static class RegistryEx + { + const string ADVAPI = "advapi32.dll"; + + public static UIntPtr CurrentUser = (UIntPtr)0x80000001; + public static UIntPtr LocalMachine = (UIntPtr)0x80000002; + + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegOpenKeyEx (UIntPtr hKey, string subKey, uint reserved, uint sam, out UIntPtr phkResult); + + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegQueryValueExW (UIntPtr hKey, string lpValueName, int lpReserved, out uint lpType, + StringBuilder lpData, ref uint lpcbData); + + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegSetValueExW (UIntPtr hKey, string lpValueName, int lpReserved, + uint dwType, string data, uint cbData); + + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegSetValueExW (UIntPtr hKey, string lpValueName, int lpReserved, + uint dwType, IntPtr data, uint cbData); + + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegCreateKeyEx (UIntPtr hKey, string subKey, uint reserved, string? @class, uint options, + uint samDesired, IntPtr lpSecurityAttributes, out UIntPtr phkResult, out Disposition lpdwDisposition); + + // https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regenumkeyexw + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegEnumKeyExW ( + UIntPtr hKey, + uint dwIndex, + [Out] char[] lpName, + ref uint lpcchName, + IntPtr lpReserved, + IntPtr lpClass, + IntPtr lpcchClass, + IntPtr lpftLastWriteTime + ); + + // https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryinfokeyw + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegQueryInfoKey ( + UIntPtr hKey, + IntPtr lpClass, + IntPtr lpcchClass, + IntPtr lpReserved, + out uint lpcSubkey, + out uint lpcchMaxSubkeyLen, + IntPtr lpcchMaxClassLen, + IntPtr lpcValues, + IntPtr lpcchMaxValueNameLen, + IntPtr lpcbMaxValueLen, + IntPtr lpSecurityDescriptor, + IntPtr lpftLastWriteTime + ); + + [DllImport ("advapi32.dll", SetLastError = true)] + static extern int RegCloseKey (UIntPtr hKey); + + internal static bool CheckRegistryKeyForExecutable (UIntPtr key, string subkey, string valueName, RegistryEx.Wow64 wow64, string subdir, string exe) + { + try { + var path = AndroidSdkBase.NullIfEmpty (RegistryEx.GetValueString (key, subkey, valueName, wow64)); + + if (path == null) { + return false; + } + + if (!ProcessUtils.FindExecutablesInDirectory (Path.Combine (path, subdir), exe).Any ()) { + return false; + } + + return true; + } catch (Exception) { + return false; + } + } + + public static IEnumerable EnumerateSubkeys (UIntPtr key, string subkey, Wow64 wow64) + { + UIntPtr regKeyHandle; + uint sam = (uint)Rights.Read + (uint)wow64; + int r = RegOpenKeyEx (key, subkey, 0, sam, out regKeyHandle); + if (r != 0) { + yield break; + } + try { + r = RegQueryInfoKey ( + hKey: regKeyHandle, + lpClass: IntPtr.Zero, + lpcchClass: IntPtr.Zero, + lpReserved: IntPtr.Zero, + lpcSubkey: out uint cSubkeys, + lpcchMaxSubkeyLen: out uint cchMaxSubkeyLen, + lpcchMaxClassLen: IntPtr.Zero, + lpcValues: IntPtr.Zero, + lpcchMaxValueNameLen: IntPtr.Zero, + lpcbMaxValueLen: IntPtr.Zero, + lpSecurityDescriptor: IntPtr.Zero, + lpftLastWriteTime: IntPtr.Zero + ); + if (r != 0) { + yield break; + } + var name = new char [cchMaxSubkeyLen+1]; + for (uint i = 0; i < cSubkeys; ++i) { + var nameLen = (uint) name.Length; + r = RegEnumKeyExW ( + hKey: regKeyHandle, + dwIndex: i, + lpName: name, + lpcchName: ref nameLen, + lpReserved: IntPtr.Zero, + lpClass: IntPtr.Zero, + lpcchClass: IntPtr.Zero, + lpftLastWriteTime: IntPtr.Zero + ); + if (r != 0) { + continue; + } + yield return new string (name, 0, (int) nameLen); + } + } + finally { + RegCloseKey (regKeyHandle); + } + } + + public static string? GetValueString (UIntPtr key, string subkey, string valueName, Wow64 wow64) + { + UIntPtr regKeyHandle; + uint sam = (uint)Rights.QueryValue + (uint)wow64; + if (RegOpenKeyEx (key, subkey, 0, sam, out regKeyHandle) != 0) + return null; + + try { + uint type; + var sb = new StringBuilder (2048); + uint cbData = (uint) sb.Capacity; + if (RegQueryValueExW (regKeyHandle, valueName, 0, out type, sb, ref cbData) == 0) { + return sb.ToString (); + } + return null; + } finally { + RegCloseKey (regKeyHandle); + } + } + + public static void SetValueString (UIntPtr key, string subkey, string valueName, string value, Wow64 wow64) + { + UIntPtr regKeyHandle; + uint sam = (uint)(Rights.CreateSubKey | Rights.SetValue) + (uint)wow64; + uint options = (uint) Options.NonVolatile; + Disposition disposition; + if (RegCreateKeyEx (key, subkey, 0, "", options, sam, IntPtr.Zero, out regKeyHandle, out disposition) != 0) { + throw new Exception ("Could not open or create key"); + } + + try { + uint type = (uint)ValueType.String; + uint lenBytesPlusNull = ((uint)value.Length + 1) * 2; + var result = RegSetValueExW (regKeyHandle, valueName, 0, type, value, lenBytesPlusNull); + if (result != 0) + throw new Exception (string.Format ("Error {0} setting registry key '{1}{2}@{3}'='{4}'", + result, key, subkey, valueName, value)); + } finally { + RegCloseKey (regKeyHandle); + } + } + + [Flags] + enum Rights : uint + { + None = 0, + QueryValue = 0x0001, + SetValue = 0x0002, + CreateSubKey = 0x0004, + EnumerateSubKey = 0x0008, + Notify = 0x0010, + Read = _StandardRead | QueryValue | EnumerateSubKey | Notify, + _StandardRead = 0x20000, + } + + enum Options + { + BackupRestore = 0x00000004, + CreateLink = 0x00000002, + NonVolatile = 0x00000000, + Volatile = 0x00000001, + } + + public enum Wow64 : uint + { + Key64 = 0x0100, + Key32 = 0x0200, + } + + enum ValueType : uint + { + None = 0, //REG_NONE + String = 1, //REG_SZ + UnexpandedString = 2, //REG_EXPAND_SZ + Binary = 3, //REG_BINARY + DWord = 4, //REG_DWORD + DWordLittleEndian = 4, //REG_DWORD_LITTLE_ENDIAN + DWordBigEndian = 5, //REG_DWORD_BIG_ENDIAN + Link = 6, //REG_LINK + MultiString = 7, //REG_MULTI_SZ + ResourceList = 8, //REG_RESOURCE_LIST + FullResourceDescriptor = 9, //REG_FULL_RESOURCE_DESCRIPTOR + ResourceRequirementsList = 10, //REG_RESOURCE_REQUIREMENTS_LIST + QWord = 11, //REG_QWORD + QWordLittleEndian = 11, //REG_QWORD_LITTLE_ENDIAN + } + + enum Disposition : uint + { + CreatedNewKey = 0x00000001, + OpenedExistingKey = 0x00000002, + } + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs new file mode 100644 index 00000000000..d2ab46f02db --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#if !NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Xamarin.Android.Tools +{ + public static class ProcessUtils + { + static string[] ExecutableFileExtensions; + + static ProcessUtils () + { + var pathExt = Environment.GetEnvironmentVariable (EnvironmentVariableNames.PathExt); + var pathExts = pathExt?.Split (new char [] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries) ?? new string [0]; + + ExecutableFileExtensions = pathExts; + } + + /// Backward-compatible overload matching the original shipped API (without environmentVariables). +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + public static Task StartProcess (ProcessStartInfo psi, TextWriter? stdout, TextWriter? stderr, CancellationToken cancellationToken, Action? onStarted = null) +#pragma warning restore RS0027 + { + return StartProcess (psi, stdout, stderr, cancellationToken, null, onStarted); + } + + /// Convenience overload accepting environmentVariables without requiring onStarted. + public static Task StartProcess (ProcessStartInfo psi, TextWriter? stdout, TextWriter? stderr, CancellationToken cancellationToken, IDictionary? environmentVariables) + { + return StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables, null); + } + + public static async Task StartProcess (ProcessStartInfo psi, TextWriter? stdout, TextWriter? stderr, CancellationToken cancellationToken, IDictionary? environmentVariables, Action? onStarted) + { + cancellationToken.ThrowIfCancellationRequested (); + psi.UseShellExecute = false; + psi.RedirectStandardOutput |= stdout != null; + psi.RedirectStandardError |= stderr != null; + + if (environmentVariables != null) { + foreach (var kvp in environmentVariables) + psi.EnvironmentVariables[kvp.Key] = kvp.Value; + } + + var process = new Process { + StartInfo = psi, + EnableRaisingEvents = true, + }; + + Task output = Task.FromResult (true); + Task error = Task.FromResult (true); + Task exit = WaitForExitAsync (process); + using (process) { + process.Start (); + if (onStarted != null) + onStarted (process); + + // If the token is cancelled while we're running, kill the process. + // Otherwise once we finish the Task.WhenAll we can remove this registration + // as there is no longer any need to Kill the process. + // + // We wrap `stdout` and `stderr` in syncronized wrappers for safety in case they + // end up writing to the same buffer, or they are the same object. + using (cancellationToken.Register (() => KillProcess (process))) { + if (psi.RedirectStandardOutput) + output = ReadStreamAsync (process.StandardOutput, TextWriter.Synchronized (stdout!)); + + if (psi.RedirectStandardError) + error = ReadStreamAsync (process.StandardError, TextWriter.Synchronized (stderr!)); + + await Task.WhenAll (new [] { output, error, exit }).ConfigureAwait (false); + } + // If we invoke 'KillProcess' our output, error and exit tasks will all complete normally. + // To protected against passing the user incomplete data we have to call + // `cancellationToken.ThrowIfCancellationRequested ()` here. + cancellationToken.ThrowIfCancellationRequested (); + return process.ExitCode; + } + } + + static void KillProcess (Process p) + { + try { + p.Kill (); + } catch (InvalidOperationException) { + // If the process has already exited this could happen + } + } + + static Task WaitForExitAsync (Process process) + { + var exitDone = new TaskCompletionSource (); + process.Exited += (o, e) => exitDone.TrySetResult (true); + return exitDone.Task; + } + + static async Task ReadStreamAsync (StreamReader stream, TextWriter destination) + { + int read; + var buffer = new char [4096]; + while ((read = await stream.ReadAsync (buffer, 0, buffer.Length).ConfigureAwait (false)) > 0) + destination.Write (buffer, 0, read); + } + + /// + /// Executes an Android Sdk tool and returns a result. The result is based on a function of the command output. + /// + public static Task ExecuteToolAsync (string exe, Func result, CancellationToken token, Action? onStarted = null) + { + var tcs = new TaskCompletionSource (); + + var log = new StringWriter (); + var error = new StringWriter (); + + var psi = new ProcessStartInfo (exe); + psi.CreateNoWindow = true; + psi.RedirectStandardInput = onStarted != null; + + var processTask = ProcessUtils.StartProcess (psi, log, error, token, null, onStarted); + var exeName = Path.GetFileName (exe); + + processTask.ContinueWith (t => { + var output = log.ToString (); + var errorOutput = error.ToString (); + log.Dispose (); + error.Dispose (); + + if (t.IsCanceled) { + tcs.TrySetCanceled (); + return; + } + + if (t.IsFaulted) { + tcs.TrySetException (t.Exception?.Flatten ()?.InnerException ?? t.Exception!); + return; + } + + if (t.Result == 0) { + tcs.TrySetResult (result != null ? result (output) : default (TResult)!); + } else { + var errorMessage = !string.IsNullOrEmpty (errorOutput) ? errorOutput : output; + errorMessage = string.IsNullOrEmpty (errorMessage) + ? $"`{exeName}` returned non-zero exit code" + : $"{t.Result} : {errorMessage}"; + + tcs.TrySetException (new InvalidOperationException (errorMessage)); + } + }, TaskContinuationOptions.ExecuteSynchronously); + + return tcs.Task; + } + + internal static void Exec (ProcessStartInfo processStartInfo, DataReceivedEventHandler output, bool includeStderr = true) + { + processStartInfo.UseShellExecute = false; + processStartInfo.RedirectStandardInput = false; + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + processStartInfo.CreateNoWindow = true; + processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; + + var p = new Process () { + StartInfo = processStartInfo, + }; + p.OutputDataReceived += output; + if (includeStderr) { + p.ErrorDataReceived += output; + } + + using (p) { + p.Start (); + p.BeginOutputReadLine (); + p.BeginErrorReadLine (); + p.WaitForExit (); + } + } + + /// + /// Creates a with the given filename and arguments. + /// On .NET 5+ uses to avoid shell-escaping issues; + /// on older frameworks falls back to a single string. + /// + public static ProcessStartInfo CreateProcessStartInfo (string fileName, params string[] args) + { + var psi = new ProcessStartInfo { + FileName = fileName, + UseShellExecute = false, + CreateNoWindow = true, + }; +#if NET5_0_OR_GREATER + foreach (var arg in args) + psi.ArgumentList.Add (arg); +#else + psi.Arguments = JoinArguments (args); +#endif + return psi; + } + +#if !NET5_0_OR_GREATER + static string JoinArguments (string[] args) + { + var sb = new StringBuilder (); + for (int i = 0; i < args.Length; i++) { + if (i > 0) + sb.Append (' '); + sb.Append ('"').Append (args [i]).Append ('"'); + } + return sb.ToString (); + } +#endif + + /// + /// Throws when is non-zero. + /// Includes stderr/stdout context in the message when available. + /// + internal static void ThrowIfFailed (int exitCode, string command, string? stderr = null, string? stdout = null) + { + if (exitCode == 0) + return; + + var message = $"'{command}' failed with exit code {exitCode}."; + + if (stderr is { Length: > 0 }) + message += $" stderr:{Environment.NewLine}{stderr.Trim ()}"; + if (stdout is { Length: > 0 }) + message += $" stdout:{Environment.NewLine}{stdout.Trim ()}"; + + throw new InvalidOperationException (message); + } + + /// + /// Overload that accepts directly so callers don't need to call ToString(). + /// + internal static void ThrowIfFailed (int exitCode, string command, StringWriter? stderr = null, StringWriter? stdout = null) + { + ThrowIfFailed (exitCode, command, stderr?.ToString (), stdout?.ToString ()); + } + + /// + /// Searches for a cmdline-tools binary in the SDK. + /// Prefers the "latest" symlink, then the highest versioned directory. + /// + /// Root path to the Android SDK. + /// Tool binary name without extension (e.g., "avdmanager"). + /// File extension including the dot (e.g., ".bat") or empty string for no extension. + /// Optional logger for diagnostic messages. + internal static string? FindCmdlineTool (string sdkPath, string toolName, string extension, Action? logger = null) + { + var cmdlineToolsDir = Path.Combine (sdkPath, "cmdline-tools"); + + if (Directory.Exists (cmdlineToolsDir)) { + // Prefer "latest" symlink first — it's the SDK's own recommended default + var latestPath = Path.Combine (cmdlineToolsDir, "latest", "bin", toolName + extension); + if (File.Exists (latestPath)) + return latestPath; + + try { + var subdirs = new List<(string name, Version version, bool isPreRelease)> (); + foreach (var dir in Directory.GetDirectories (cmdlineToolsDir)) { + var name = Path.GetFileName (dir); + if (string.IsNullOrEmpty (name) || name == "latest") + continue; + // Strip pre-release suffixes (e.g., "5.0-rc1" → "5.0") before parsing + var versionStr = name; + var dashIndex = name.IndexOf ('-'); + var isPreRelease = dashIndex >= 0; + if (isPreRelease) + versionStr = name.Substring (0, dashIndex); + Version.TryParse (versionStr, out var v); + subdirs.Add ((name, v ?? new Version (0, 0), isPreRelease)); + } + // Sort by version descending, then prefer stable (non-prerelease) over prerelease + subdirs.Sort ((a, b) => { + var cmp = b.version.CompareTo (a.version); + if (cmp != 0) return cmp; + if (a.isPreRelease != b.isPreRelease) + return a.isPreRelease ? 1 : -1; // stable first + return string.Compare (a.name, b.name, StringComparison.Ordinal); + }); + + foreach (var (name, _, _) in subdirs) { + var toolPath = Path.Combine (cmdlineToolsDir, name, "bin", toolName + extension); + if (File.Exists (toolPath)) + return toolPath; + } + } catch (IOException ex) { + logger?.Invoke (TraceLevel.Warning, $"FindCmdlineTool: IO error enumerating {cmdlineToolsDir}: {ex.Message}"); + } catch (UnauthorizedAccessException ex) { + logger?.Invoke (TraceLevel.Warning, $"FindCmdlineTool: Permission denied on {cmdlineToolsDir}: {ex.Message}"); + } + } + + return null; + } + + internal static IEnumerable FindExecutablesInPath (string executable) + { + var path = Environment.GetEnvironmentVariable (EnvironmentVariableNames.Path) ?? ""; + var pathDirs = path.Split (new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var dir in pathDirs) { + foreach (var exe in FindExecutablesInDirectory (dir, executable)) { + yield return exe; + } + } + } + + internal static IEnumerable FindExecutablesInDirectory (string dir, string executable) + { + if (!Directory.Exists (dir)) + yield break; + foreach (var exe in ExecutableFiles (executable)) { + string exePath; + try { + exePath = Path.Combine (dir, exe); + } catch (ArgumentException) { + continue; + } + if (File.Exists (exePath)) + yield return exePath; + } + } + + internal static IEnumerable ExecutableFiles (string executable) + { + if (ExecutableFileExtensions == null || ExecutableFileExtensions.Length == 0) { + yield return executable; + yield break; + } + + foreach (var ext in ExecutableFileExtensions) + yield return Path.ChangeExtension (executable, ext); + yield return executable; + } + + /// Checks if running as Administrator (Windows) or root (macOS/Linux). + public static bool IsElevated () + { +#if NET5_0_OR_GREATER + return Environment.IsPrivilegedProcess; +#else + if (OS.IsWindows) + return IsUserAnAdmin (); + return geteuid () == 0; +#endif + } + +#if !NET5_0_OR_GREATER + [DllImport ("shell32.dll")] + [return: MarshalAs (UnmanagedType.Bool)] + static extern bool IsUserAnAdmin (); + + [DllImport ("libc", SetLastError = true)] + static extern uint geteuid (); +#endif + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..05a6dfb830e --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo ( + "Xamarin.Android.Tools.AndroidSdk-Tests, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" + + "6cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2" + + "814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" + + "d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" + + "2c9733db" +)] diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.Designer.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..9013f19feef --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Xamarin.Android.Tools.AndroidSdk.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Xamarin.Android.Tools.AndroidSdk.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An exception occurred while validating the Java SDK installation in '{0}' that was found while searching the paths from '{1}'. Ensure that the Android section of the Visual Studio options has a valid Java SDK directory configured. To use a custom SDK path for a command line build, set the 'JavaSdkDirectory' MSBuild property to the custom path. Exception: {2}. + /// + internal static string InvalidJdkDirectory_path_locator_message { + get { + return ResourceManager.GetString("InvalidJdkDirectory_path_locator_message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception occurred while reading configuration file '{0}'. Exception: {1}. + /// + internal static string InvalidMonodroidConfigFile_path_message { + get { + return ResourceManager.GetString("InvalidMonodroidConfigFile_path_message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception occurred while reading the output of '{0} {1}'. Exception: {2}. + /// + internal static string InvalidXmlLibExecJdk_path_args_message { + get { + return ResourceManager.GetString("InvalidXmlLibExecJdk_path_args_message", resourceCulture); + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.cs.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.cs.resx new file mode 100644 index 00000000000..42470c39056 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.cs.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Při ověřování instalace sady Java SDK v {0}, která se našla při hledání cest z {1} došlo k výjimce. Ujistěte se, že v části Android v možnostech Visual Studio je nakonfigurovaný platný adresář sady Java SDK. Pokud chcete pro sestavení z příkazového řádku použít vlastní cestu sady SDK, nastavte vlastnost MSBuild JavaSdkDirectory na vlastní cestu. Výjimka: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Při čtení konfiguračního souboru {0} došlo k výjimce. Výjimka: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Při čtení výstupu {0} {1} došlo k výjimce. Výjimka: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.de.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.de.resx new file mode 100644 index 00000000000..cc440d66307 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.de.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ausnahme beim Überprüfen der Java SDK-Installation in '{0}', die beim Durchsuchen der Pfade von '{1}' gefunden wurde. Stellen Sie sicher, dass für den Android-Abschnitt der Visual Studio-Optionen ein gültiges Java SDK-Verzeichnis konfiguriert ist. Um einen benutzerdefinierten SDK-Pfad für einen Befehlszeilenbuild zu verwenden, legen Sie die MSBuild-Eigenschaft "JavaSdkDirectory" auf den benutzerdefinierten Pfad fest. Ausnahme: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Ausnahme beim Lesen der Konfigurationsdatei '{0}'. Ausnahme: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Ausnahme beim Lesen der Ausgabe von "{0} {1}". Ausnahme: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.es.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.es.resx new file mode 100644 index 00000000000..dea39840b2f --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.es.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Se ha producido una excepción al validar la instalación del SDK de Java en "{0}" que se ha encontrado al buscar las rutas desde "{1}". Asegúrese de que la sección de Android de las opciones de Visual Studio tiene configurado un directorio de Java SDK válido. Para utilizar una ruta de acceso al SDK personalizada para una compilación de línea de comandos, establezca la propiedad MSBuild 'JavaSdkDirectory' en la ruta personalizada. Excepción: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Se ha producido una excepción al leer el archivo de configuración "{0}". Excepción: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Se ha producido una excepción al leer la salida de "{0} {1}". Excepción: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.fr.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.fr.resx new file mode 100644 index 00000000000..64106e94733 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.fr.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Une exception s’est produite lors de la validation de l’installation du Kit de développement logiciel (SDK) Java dans '{0}' qui a été trouvée lors de la recherche dans les chemins d’accès à partir de '{1}'. Vérifiez que la section Android des options Visual Studio a un répertoire Java SDK valide configuré. Pour utiliser un chemin de kit SDK personnalisé pour une build de ligne de commande, définissez la propriété MSBuild 'JavaSdkDirectory' sur le chemin personnalisé. Exception : {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Une exception s’est produite lors de la lecture du fichier de configuration '{0}'. Exception : {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Une exception s’est produite lors de la lecture de la sortie de '{0} {1}'. Exception : {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.it.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.it.resx new file mode 100644 index 00000000000..82b70004437 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.it.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Si è verificata un'eccezione durante la convalida dell'installazione di Java SDK in '{0}' trovata durante la ricerca nei percorsi da '{1}'. Assicurarsi che nella sezione Android delle opzioni di Visual Studio sia configurata una directory java SDK valida. Per usare un percorso SDK personalizzato per una compilazione della riga di comando, impostare la proprietà MSBuild 'JavaSdkDirectory' sul percorso personalizzato. Eccezione: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Si è verificata un'eccezione durante la lettura del file di configurazione '{0}'. Eccezione: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Eccezione durante la lettura dell'output di '{0} {1}'. Eccezione: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ja.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ja.resx new file mode 100644 index 00000000000..caac05f6558 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ja.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{1}' からのパスの検索中に見つかった '{0}' の Java SDK インストールを検証中に例外が発生しました。Visual Studio オプションの Android セクションに、有効な Java SDK ディレクトリが構成されていることを確認してください。コマンド ライン ビルドにカスタム SDK パスを使用するには、'JavaSdkDirectory' MSBuild プロパティにそのカスタム パスを設定します。例外: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + 構成ファイル '{0}' の読み取り中に例外が発生しました。例外: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + '{0} {1}' の出力の読み取り中に例外が発生しました。例外: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ko.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ko.resx new file mode 100644 index 00000000000..11d77439612 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ko.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{1}'에서 경로를 검색하는 동안 발견된 '{0}'의 Java SDK 설치의 유효성을 검사하는 동안 예외가 발생했습니다. Visual Studio 옵션의 Android 섹션에 유효한 Java SDK 디렉터리가 구성되어 있는지 확인하세요. 명령줄 빌드에 사용자 지정 SDK 경로를 사용하려면 'JavaSdkDirectory' MSBuild 속성을 사용자 지정 경로로 설정하세요. 예외: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + 구성 파일 '{0}'을(를) 읽는 동안 예외가 발생했습니다. 예외: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + '{0} {1}'의 출력을 읽는 동안 예외가 발생했습니다. 예외: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pl.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pl.resx new file mode 100644 index 00000000000..9f9c5dacf45 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pl.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Wystąpił wyjątek podczas sprawdzania poprawności instalacji zestawu Java SDK w lokalizacji „{0}”, która została znaleziona podczas przeszukiwania ścieżek z lokalizacji „{1}”. Upewnij się, że sekcja opcji programu Visual Studio dla systemu Android ma skonfigurowany prawidłowy katalog zestawu Java SDK. Aby użyć niestandardowej ścieżki zestawu SDK dla kompilacji wiersza polecenia, ustaw ścieżkę niestandardową właściwości „JavaSdkDirectory” programu MSBuild. Wyjątek: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Wystąpił wyjątek podczas odczytywania pliku konfiguracji „{0}”. Wyjątek: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Wystąpił wyjątek podczas odczytywania danych wyjściowych elementu „{0} {1}„. Wyjątek: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pt-BR.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pt-BR.resx new file mode 100644 index 00000000000..4a2b67a15e2 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.pt-BR.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ocorreu uma exceção ao validar a instalação do Java SDK em '{0}' que foi encontrada ao pesquisar os caminhos de '{1}'. Certifique-se de que a seção Android das opções do Visual Studio tenha um diretório Java SDK válido configurado. Para usar um caminho SDK personalizado para uma compilação de linha de comando, defina a propriedade MSBuild 'JavaSdkDirectory' para o caminho personalizado. Exceção: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Ocorreu uma exceção ao ler o arquivo de configuração '{0}'. Exceção: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Ocorreu uma exceção ao ler a saída de '{0} {1}'. Exceção: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx new file mode 100644 index 00000000000..7b047617793 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An exception occurred while validating the Java SDK installation in '{0}' that was found while searching the paths from '{1}'. Ensure that the Android section of the Visual Studio options has a valid Java SDK directory configured. To use a custom SDK path for a command line build, set the 'JavaSdkDirectory' MSBuild property to the custom path. Exception: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + An exception occurred while reading configuration file '{0}'. Exception: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + An exception occurred while reading the output of '{0} {1}'. Exception: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ru.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ru.resx new file mode 100644 index 00000000000..9be590b3588 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.ru.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Возникло исключение при проверке установки Java SDK в "{0}", обнаруженном при поиске путей из "{1}". Настройте допустимый каталог Java SDK в разделе Android параметров Visual Studio. Чтобы использовать пользовательский путь SDK для сборки командной строки, задайте настраиваемый путь для свойства MSBuild "JavaSdkDirectory". Исключение: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + Возникло исключение при чтении файла конфигурации "{0}". Исключение: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + Возникло исключение при чтении выходных данных "{0} {1}". Исключение: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.tr.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.tr.resx new file mode 100644 index 00000000000..8156b187b07 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.tr.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{1}' yolu aranıp '{0}' içindeki Java SDK yüklemesi doğrulanırken bir istisna oluştu. Visual Studio seçeneklerinin Android bölümünün yapılandırılmış geçerli bir Java SDK dizinine sahip olduğundan emin olun. Komut satırı derlemesi için özel bir SDK yolu kullanmak üzere 'JavaSdkDirectory' MSBuild özelliğini özel yola ayarlayın. Özel durum: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + ‘{0}’ Yapılandırma dosyası okunurken özel durum oluştu. Özel durum: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + '{0} {1}' çıktısı okunurken özel durum oluştu. Özel durum: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hans.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hans.resx new file mode 100644 index 00000000000..c452ceb9a05 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hans.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 验证 "{0}" 中的 Java SDK 安装时发生异常,该安装是在从 "{1}" 搜索路径时找到的。确保 Visual Studio 选项的 Android 部分配置了有效的 Java SDK 目录。若要对命令行生成使用自定义 SDK 路径,请将 "JavaSdkDirectory" MSBuild 属性设置为自定义路径。异常: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + 读取配置文件 "{0}" 时发生异常。异常: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + 读取 "{0} {1}" 的输出时发生异常。异常: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hant.resx b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hant.resx new file mode 100644 index 00000000000..c3a204d4434 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Properties/Resources.zh-Hant.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 驗證 '{0}' 中的 JAVA SDK 安裝時發生例外狀況,該安裝是在從 '{1}' 搜尋路徑時找到的。請確認 Visual Studio 選項的 Android 區段已設定有效的 JAVA SDK 目錄。若要針對命令列組建使用自訂 SDK 路徑,請將 'JAVASdkDirectory' MSBuild 屬性設定為自訂路徑。例外狀況: {2} + +{0} - The path of the invalid installation +{1} - a "contextual" name for where {0} came from: `Preferred Registry` (Windows Registry), `OpenJDK`, `$JAVA_HOME` (environment variable), etc. +{2} - The exception message of the associated exception. + + + 讀取設定檔 '{0}' 時發生例外狀況。例外狀況: {1} + +{0} - The path of the file being read. +{1} - The exception message of the associated exception. + + + 讀取 '{0} {1}' 的輸出時發生例外狀況。例外狀況: {2} + + \ No newline at end of file diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Shipped.txt b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..47e8e682995 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -0,0 +1,131 @@ +#nullable enable +Xamarin.Android.Tools.AndroidAppManifest +Xamarin.Android.Tools.AndroidAppManifest.AndroidPermissions.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.AndroidPermissionsQualified.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.ApplicationIcon.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationIcon.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationLabel.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationLabel.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationRoundIcon.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationRoundIcon.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationTheme.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationTheme.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Debuggable.get -> bool? +Xamarin.Android.Tools.AndroidAppManifest.Debuggable.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Document.get -> System.Xml.Linq.XDocument! +Xamarin.Android.Tools.AndroidAppManifest.GetAllActivityNames() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableActivityNames() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableFastDevActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableUserActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.InstallLocation.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.InstallLocation.set -> void +Xamarin.Android.Tools.AndroidAppManifest.MinSdkVersion.get -> int? +Xamarin.Android.Tools.AndroidAppManifest.MinSdkVersion.set -> void +Xamarin.Android.Tools.AndroidAppManifest.PackageName.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.PackageName.set -> void +Xamarin.Android.Tools.AndroidAppManifest.SetAndroidPermissions(System.Collections.Generic.IEnumerable! permissions) -> void +Xamarin.Android.Tools.AndroidAppManifest.TargetSdkVersion.get -> int? +Xamarin.Android.Tools.AndroidAppManifest.TargetSdkVersion.set -> void +Xamarin.Android.Tools.AndroidAppManifest.VersionCode.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.VersionCode.set -> void +Xamarin.Android.Tools.AndroidAppManifest.VersionName.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.VersionName.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Write(System.Xml.XmlWriter! writer) -> void +Xamarin.Android.Tools.AndroidAppManifest.WriteToFile(string! fileName) -> void +Xamarin.Android.Tools.AndroidSdkInfo +Xamarin.Android.Tools.AndroidSdkInfo.AllAndroidSdkPaths.get -> string![]! +Xamarin.Android.Tools.AndroidSdkInfo.AndroidNdkHostPlatform.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.AndroidNdkPath.get -> string? +Xamarin.Android.Tools.AndroidSdkInfo.AndroidSdkInfo(System.Action? logger = null, string? androidSdkPath = null, string? androidNdkPath = null, string? javaSdkPath = null) -> void +Xamarin.Android.Tools.AndroidSdkInfo.AndroidSdkPath.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.GetBuildToolsPaths() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetBuildToolsPaths(string! preferredBuildToolsVersion) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetCommandLineToolsPaths() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetCommandLineToolsPaths(string! preferredCommandLineToolsVersion) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetInstalledPlatformVersions(Xamarin.Android.Tools.AndroidVersions! versions) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetPlatformDirectory(int apiLevel) -> string! +Xamarin.Android.Tools.AndroidSdkInfo.GetPlatformDirectoryFromId(string! id) -> string! +Xamarin.Android.Tools.AndroidSdkInfo.IsPlatformInstalled(int apiLevel) -> bool +Xamarin.Android.Tools.AndroidSdkInfo.JavaSdkPath.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.TryGetCommandLineToolsPath() -> string? +Xamarin.Android.Tools.AndroidSdkInfo.TryGetPlatformDirectoryFromApiLevel(string! idOrApiLevel, Xamarin.Android.Tools.AndroidVersions! versions) -> string? +Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Arm = 1 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Arm64 = 8 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Mips = 4 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.None = 0 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Other = 65536 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.X86 = 2 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.X86_64 = 16 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidVersion +Xamarin.Android.Tools.AndroidVersion.AndroidVersion(System.Version! versionCodeFull, string! osVersion, string? codeName = null, string? id = null, bool stable = true) -> void +Xamarin.Android.Tools.AndroidVersion.AndroidVersion(int apiLevel, string! osVersion, string? codeName = null, string? id = null, bool stable = true) -> void +Xamarin.Android.Tools.AndroidVersion.ApiLevel.get -> int +Xamarin.Android.Tools.AndroidVersion.CodeName.get -> string? +Xamarin.Android.Tools.AndroidVersion.FrameworkVersion.get -> string! +Xamarin.Android.Tools.AndroidVersion.Id.get -> string! +Xamarin.Android.Tools.AndroidVersion.OSVersion.get -> string! +Xamarin.Android.Tools.AndroidVersion.Stable.get -> bool +Xamarin.Android.Tools.AndroidVersion.TargetFrameworkVersion.get -> System.Version! +Xamarin.Android.Tools.AndroidVersion.VersionCodeFull.get -> System.Version! +Xamarin.Android.Tools.AndroidVersions +Xamarin.Android.Tools.AndroidVersions.AndroidVersions(System.Collections.Generic.IEnumerable! versions) -> void +Xamarin.Android.Tools.AndroidVersions.AndroidVersions(System.Collections.Generic.IEnumerable! frameworkDirectories) -> void +Xamarin.Android.Tools.AndroidVersions.FrameworkDirectories.get -> System.Collections.Generic.IReadOnlyList! +Xamarin.Android.Tools.AndroidVersions.GetApiLevelFromFrameworkVersion(string! frameworkVersion) -> int? +Xamarin.Android.Tools.AndroidVersions.GetApiLevelFromId(string! id) -> int? +Xamarin.Android.Tools.AndroidVersions.GetFrameworkVersionFromApiLevel(int apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetFrameworkVersionFromId(string! id) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromApiLevel(int apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromApiLevel(string! apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromFrameworkVersion(string! frameworkVersion) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromVersionCodeFull(System.Version! versionCodeFull) -> string? +Xamarin.Android.Tools.AndroidVersions.InstalledBindingVersions.get -> System.Collections.Generic.IReadOnlyList! +Xamarin.Android.Tools.AndroidVersions.MaxStableVersion.get -> Xamarin.Android.Tools.AndroidVersion? +Xamarin.Android.Tools.AndroidVersions.MinStableVersion.get -> Xamarin.Android.Tools.AndroidVersion? +Xamarin.Android.Tools.JdkInfo +Xamarin.Android.Tools.JdkInfo.GetJavaSettingsPropertyValue(string! key, out string? value) -> bool +Xamarin.Android.Tools.JdkInfo.GetJavaSettingsPropertyValues(string! key, out System.Collections.Generic.IEnumerable? value) -> bool +Xamarin.Android.Tools.JdkInfo.HomePath.get -> string! +Xamarin.Android.Tools.JdkInfo.IncludePath.get -> System.Collections.ObjectModel.ReadOnlyCollection! +Xamarin.Android.Tools.JdkInfo.JarPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JavaPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JavaSettingsPropertyKeys.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.JdkInfo.JavacPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath) -> void +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath, string! locator) -> void +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath, string? locator = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.JdkInfo.JdkJvmPath.get -> string! +Xamarin.Android.Tools.JdkInfo.Locator.get -> string? +Xamarin.Android.Tools.JdkInfo.ReleaseProperties.get -> System.Collections.ObjectModel.ReadOnlyDictionary! +Xamarin.Android.Tools.JdkInfo.Vendor.get -> string? +Xamarin.Android.Tools.JdkInfo.Version.get -> System.Version? +Xamarin.Android.Tools.KernelEx +Xamarin.Android.Tools.OS +Xamarin.Android.Tools.OS.OS() -> void +Xamarin.Android.Tools.ProcessUtils +override Xamarin.Android.Tools.AndroidVersion.ToString() -> string! +override Xamarin.Android.Tools.JdkInfo.ToString() -> string! +static Xamarin.Android.Tools.AndroidAppManifest.AndroidXNamespace.get -> System.Xml.Linq.XNamespace! +static Xamarin.Android.Tools.AndroidAppManifest.CanonicalizePackageName(string! packageNameOrAssemblyName) -> string! +static Xamarin.Android.Tools.AndroidAppManifest.Create(string! packageName, string! appLabel, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.Load(System.Xml.Linq.XDocument! doc, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.Load(string! filename, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.NameXName.get -> System.Xml.Linq.XName! +static Xamarin.Android.Tools.AndroidSdkInfo.DetectAndSetPreferredJavaSdkPathToLatest(System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredAndroidNdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredAndroidSdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredJavaSdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidVersion.Load(System.IO.Stream! stream) -> Xamarin.Android.Tools.AndroidVersion! +static Xamarin.Android.Tools.AndroidVersion.Load(string! uri) -> Xamarin.Android.Tools.AndroidVersion! +static Xamarin.Android.Tools.JdkInfo.GetKnownSystemJdkInfos(System.Action? logger = null) -> System.Collections.Generic.IEnumerable! +static Xamarin.Android.Tools.JdkInfo.GetSupportedJdkInfos(System.Action? logger = null) -> System.Collections.Generic.IEnumerable! +static Xamarin.Android.Tools.KernelEx.GetLongPathName(string! path) -> string! +static Xamarin.Android.Tools.KernelEx.GetShortPathName(string! path) -> string! +static Xamarin.Android.Tools.ProcessUtils.ExecuteToolAsync(string! exe, System.Func! result, System.Threading.CancellationToken token, System.Action? onStarted = null) -> System.Threading.Tasks.Task! +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Action? onStarted = null) -> System.Threading.Tasks.Task! +static readonly Xamarin.Android.Tools.AndroidVersions.KnownVersions -> Xamarin.Android.Tools.AndroidVersion![]! +static readonly Xamarin.Android.Tools.OS.IsLinux -> bool +static readonly Xamarin.Android.Tools.OS.IsMac -> bool +static readonly Xamarin.Android.Tools.OS.IsWindows -> bool diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..21b4baba56a --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -0,0 +1,319 @@ +#nullable enable +Xamarin.Android.Tools.AdbDeviceInfo +Xamarin.Android.Tools.AdbDeviceInfo.AdbDeviceInfo() -> void +Xamarin.Android.Tools.AdbDeviceInfo.AvdName.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.AvdName.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Description.get -> string! +Xamarin.Android.Tools.AdbDeviceInfo.Description.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Device.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Device.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.IsEmulator.get -> bool +Xamarin.Android.Tools.AdbDeviceInfo.Model.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Model.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Product.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Product.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Serial.get -> string! +Xamarin.Android.Tools.AdbDeviceInfo.Serial.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Status.get -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceInfo.Status.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.TransportId.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.TransportId.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Type.get -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceInfo.Type.set -> void +Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.NoPermissions = 3 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.NotRunning = 4 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Offline = 1 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Online = 0 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Unauthorized = 2 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Unknown = 5 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceType.Device = 0 -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceType.Emulator = 1 -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbRunner +Xamarin.Android.Tools.AdbRunner.AdbRunner(string! adbPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +virtual Xamarin.Android.Tools.AdbRunner.ListDevicesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AdbRunner.StopEmulatorAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AdbRunner.WaitForDeviceAsync(string? serial = null, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.ChecksumType.Sha1 = 0 -> Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.ChecksumType.Sha256 = 1 -> Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Complete = 4 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Downloading = 0 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Extracting = 2 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Validating = 3 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Verifying = 1 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallProgress +Xamarin.Android.Tools.JdkInstallProgress.JdkInstallProgress(Xamarin.Android.Tools.JdkInstallPhase Phase, double PercentComplete, string? Message = null) -> void +Xamarin.Android.Tools.JdkInstallProgress.Message.get -> string? +Xamarin.Android.Tools.JdkInstallProgress.Message.init -> void +Xamarin.Android.Tools.JdkInstallProgress.PercentComplete.get -> double +Xamarin.Android.Tools.JdkInstallProgress.PercentComplete.init -> void +Xamarin.Android.Tools.JdkInstallProgress.Phase.get -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallProgress.Phase.init -> void +Xamarin.Android.Tools.JdkInstaller +Xamarin.Android.Tools.JdkInstaller.DiscoverAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.JdkInstaller.Dispose() -> void +Xamarin.Android.Tools.JdkInstaller.InstallAsync(int majorVersion, string! targetPath, System.IProgress? progress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.JdkInstaller.IsValid(string! jdkPath) -> bool +Xamarin.Android.Tools.JdkInstaller.JdkInstaller(System.Action? logger = null) -> void +Xamarin.Android.Tools.JdkInstaller.Remove(string! jdkPath) -> bool +Xamarin.Android.Tools.JdkVersionInfo +Xamarin.Android.Tools.JdkVersionInfo.Checksum.get -> string? +Xamarin.Android.Tools.JdkVersionInfo.ChecksumUrl.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.DisplayName.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.DownloadUrl.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.JdkVersionInfo(int majorVersion, string! displayName, string! downloadUrl, string! checksumUrl, long size = 0, string? checksum = null) -> void +Xamarin.Android.Tools.JdkVersionInfo.MajorVersion.get -> int +Xamarin.Android.Tools.JdkVersionInfo.ResolvedUrl.get -> string? +Xamarin.Android.Tools.JdkVersionInfo.Size.get -> long +Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Complete = 4 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Downloading = 1 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Extracting = 3 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.ReadingManifest = 0 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Verifying = 2 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapProgress +Xamarin.Android.Tools.SdkBootstrapProgress.Message.get -> string! +Xamarin.Android.Tools.SdkBootstrapProgress.Message.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.PercentComplete.get -> int +Xamarin.Android.Tools.SdkBootstrapProgress.PercentComplete.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.Phase.get -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapProgress.Phase.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.SdkBootstrapProgress(Xamarin.Android.Tools.SdkBootstrapPhase Phase, int PercentComplete = -1, string! Message = "") -> void +Xamarin.Android.Tools.SdkLicense +Xamarin.Android.Tools.SdkLicense.Id.get -> string! +Xamarin.Android.Tools.SdkLicense.Id.init -> void +Xamarin.Android.Tools.SdkLicense.SdkLicense(string! Id, string! Text) -> void +Xamarin.Android.Tools.SdkLicense.Text.get -> string! +Xamarin.Android.Tools.SdkLicense.Text.init -> void +Xamarin.Android.Tools.SdkManager +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync() -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Collections.Generic.IEnumerable! licenseIds) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Collections.Generic.IEnumerable! licenseIds, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AndroidSdkPath.get -> string? +Xamarin.Android.Tools.SdkManager.AndroidSdkPath.set -> void +Xamarin.Android.Tools.SdkManager.AreLicensesAccepted() -> bool +Xamarin.Android.Tools.SdkManager.BootstrapAsync(string! targetPath, System.IProgress? progress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.Dispose() -> void +Xamarin.Android.Tools.SdkManager.FindSdkManagerPath() -> string? +Xamarin.Android.Tools.SdkManager.GetPendingLicensesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.SdkManager.InstallAsync(System.Collections.Generic.IEnumerable! packages, bool acceptLicenses = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.JavaSdkPath.get -> string? +Xamarin.Android.Tools.SdkManager.JavaSdkPath.set -> void +Xamarin.Android.Tools.SdkManager.ListAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Installed, System.Collections.Generic.IReadOnlyList! Available)>! +Xamarin.Android.Tools.SdkManager.ManifestFeedUrl.get -> string! +Xamarin.Android.Tools.SdkManager.ManifestFeedUrl.set -> void +Xamarin.Android.Tools.SdkManager.SdkManager(System.Action? logger = null) -> void +Xamarin.Android.Tools.SdkManager.UninstallAsync(System.Collections.Generic.IEnumerable! packages, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.UpdateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkPackage +Xamarin.Android.Tools.SdkPackage.Description.get -> string? +Xamarin.Android.Tools.SdkPackage.Description.init -> void +Xamarin.Android.Tools.SdkPackage.IsInstalled.get -> bool +Xamarin.Android.Tools.SdkPackage.IsInstalled.init -> void +Xamarin.Android.Tools.SdkPackage.Path.get -> string! +Xamarin.Android.Tools.SdkPackage.Path.init -> void +Xamarin.Android.Tools.SdkPackage.SdkPackage(string! Path, string? Version = null, string? Description = null, bool IsInstalled = false) -> void +Xamarin.Android.Tools.SdkPackage.Version.get -> string? +Xamarin.Android.Tools.SdkPackage.Version.init -> void +const Xamarin.Android.Tools.SdkManager.DefaultManifestFeedUrl = "https://aka.ms/AndroidManifestFeed/d18-0" -> string! +override Xamarin.Android.Tools.JdkVersionInfo.ToString() -> string! +static Xamarin.Android.Tools.AdbRunner.BuildDeviceDescription(Xamarin.Android.Tools.AdbDeviceInfo! device, System.Action? logger = null) -> string! +static Xamarin.Android.Tools.AdbRunner.FormatDisplayName(string! avdName) -> string! +static Xamarin.Android.Tools.AdbRunner.MapAdbStateToStatus(string! adbState) -> Xamarin.Android.Tools.AdbDeviceStatus +static Xamarin.Android.Tools.AdbRunner.MergeDevicesAndEmulators(System.Collections.Generic.IReadOnlyList! adbDevices, System.Collections.Generic.IReadOnlyList! availableEmulators, System.Action? logger = null) -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.AdbRunner.ParseAdbDevicesOutput(System.Collections.Generic.IEnumerable! lines) -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.JdkInstaller.RecommendedMajorVersion.get -> int +static Xamarin.Android.Tools.JdkInstaller.SupportedVersions.get -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.ProcessUtils.CreateProcessStartInfo(string! fileName, params string![]! args) -> System.Diagnostics.ProcessStartInfo! +static Xamarin.Android.Tools.ProcessUtils.IsElevated() -> bool +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Collections.Generic.IDictionary? environmentVariables, System.Action? onStarted) -> System.Threading.Tasks.Task! +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Collections.Generic.IDictionary? environmentVariables) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdInfo +Xamarin.Android.Tools.AvdInfo.AvdInfo(string! Name, string? DeviceProfile = null, string? Path = null) -> void +Xamarin.Android.Tools.AvdInfo.DeviceProfile.get -> string? +Xamarin.Android.Tools.AvdInfo.DeviceProfile.init -> void +Xamarin.Android.Tools.AvdInfo.Name.get -> string! +Xamarin.Android.Tools.AvdInfo.Name.init -> void +Xamarin.Android.Tools.AvdInfo.Path.get -> string? +Xamarin.Android.Tools.AvdInfo.Path.init -> void +Xamarin.Android.Tools.AvdManagerRunner +Xamarin.Android.Tools.AvdManagerRunner.AvdManagerRunner(string! avdManagerPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.AvdManagerRunner.GetOrCreateAvdAsync(string! name, string! systemImage, string? deviceProfile = null, bool force = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.DeleteAvdAsync(string! name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListAvdsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.GetShellPropertyAsync(string! serial, string! propertyName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RunShellCommandAsync(string! serial, string! command, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RunShellCommandAsync(string! serial, string! command, string![]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.EmulatorBootOptions +Xamarin.Android.Tools.EmulatorBootOptions.AdditionalArgs.get -> System.Collections.Generic.List? +Xamarin.Android.Tools.EmulatorBootOptions.AdditionalArgs.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.BootTimeout.get -> System.TimeSpan +Xamarin.Android.Tools.EmulatorBootOptions.BootTimeout.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.ColdBoot.get -> bool +Xamarin.Android.Tools.EmulatorBootOptions.ColdBoot.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.PollInterval.get -> System.TimeSpan +Xamarin.Android.Tools.EmulatorBootOptions.PollInterval.init -> void +Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.None = 0 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.LaunchFailed = 1 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Timeout = 2 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Cancelled = 3 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Unknown = 4 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootResult +Xamarin.Android.Tools.EmulatorBootResult.ErrorKind.get -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootResult.ErrorKind.init -> void +Xamarin.Android.Tools.EmulatorBootResult.ErrorMessage.get -> string? +Xamarin.Android.Tools.EmulatorBootResult.ErrorMessage.init -> void +Xamarin.Android.Tools.EmulatorBootResult.Serial.get -> string? +Xamarin.Android.Tools.EmulatorBootResult.Serial.init -> void +Xamarin.Android.Tools.EmulatorBootResult.Success.get -> bool +Xamarin.Android.Tools.EmulatorBootResult.Success.init -> void +Xamarin.Android.Tools.EmulatorRunner +Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.AdbRunner! adbRunner, Xamarin.Android.Tools.EmulatorBootOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! +Xamarin.Android.Tools.AdbPortRule +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortSpec! Remote, Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortRule.Local.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Local.init -> void +Xamarin.Android.Tools.AdbPortRule.Remote.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Remote.init -> void +Xamarin.Android.Tools.AdbPortSpec +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbProtocol Protocol, int Port) -> void +Xamarin.Android.Tools.AdbPortSpec.Port.get -> int +Xamarin.Android.Tools.AdbPortSpec.Port.init -> void +Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void +Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! +Xamarin.Android.Tools.AdbProtocol + +Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol +override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AvdDeviceProfile +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void +override Xamarin.Android.Tools.AdbPortRule.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AdbPortRule.GetHashCode() -> int +override Xamarin.Android.Tools.AdbPortRule.ToString() -> string! +override Xamarin.Android.Tools.AdbPortSpec.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AdbPortSpec.GetHashCode() -> int +override Xamarin.Android.Tools.AvdDeviceProfile.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AvdDeviceProfile.GetHashCode() -> int +override Xamarin.Android.Tools.AvdDeviceProfile.ToString() -> string! +override Xamarin.Android.Tools.AvdInfo.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AvdInfo.GetHashCode() -> int +override Xamarin.Android.Tools.AvdInfo.ToString() -> string! +override Xamarin.Android.Tools.EmulatorBootOptions.Equals(object? obj) -> bool +override Xamarin.Android.Tools.EmulatorBootOptions.GetHashCode() -> int +override Xamarin.Android.Tools.EmulatorBootOptions.ToString() -> string! +override Xamarin.Android.Tools.EmulatorBootResult.Equals(object? obj) -> bool +override Xamarin.Android.Tools.EmulatorBootResult.GetHashCode() -> int +override Xamarin.Android.Tools.EmulatorBootResult.ToString() -> string! +override Xamarin.Android.Tools.JdkInstallProgress.Equals(object? obj) -> bool +override Xamarin.Android.Tools.JdkInstallProgress.GetHashCode() -> int +override Xamarin.Android.Tools.JdkInstallProgress.ToString() -> string! +override Xamarin.Android.Tools.SdkBootstrapProgress.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkBootstrapProgress.GetHashCode() -> int +override Xamarin.Android.Tools.SdkBootstrapProgress.ToString() -> string! +override Xamarin.Android.Tools.SdkLicense.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkLicense.GetHashCode() -> int +override Xamarin.Android.Tools.SdkLicense.ToString() -> string! +override Xamarin.Android.Tools.SdkPackage.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkPackage.GetHashCode() -> int +override Xamarin.Android.Tools.SdkPackage.ToString() -> string! +static Xamarin.Android.Tools.AdbPortRule.operator !=(Xamarin.Android.Tools.AdbPortRule? left, Xamarin.Android.Tools.AdbPortRule? right) -> bool +static Xamarin.Android.Tools.AdbPortRule.operator ==(Xamarin.Android.Tools.AdbPortRule? left, Xamarin.Android.Tools.AdbPortRule? right) -> bool +static Xamarin.Android.Tools.AdbPortSpec.operator !=(Xamarin.Android.Tools.AdbPortSpec? left, Xamarin.Android.Tools.AdbPortSpec? right) -> bool +static Xamarin.Android.Tools.AdbPortSpec.operator ==(Xamarin.Android.Tools.AdbPortSpec? left, Xamarin.Android.Tools.AdbPortSpec? right) -> bool +static Xamarin.Android.Tools.AvdDeviceProfile.operator !=(Xamarin.Android.Tools.AvdDeviceProfile? left, Xamarin.Android.Tools.AvdDeviceProfile? right) -> bool +static Xamarin.Android.Tools.AvdDeviceProfile.operator ==(Xamarin.Android.Tools.AvdDeviceProfile? left, Xamarin.Android.Tools.AvdDeviceProfile? right) -> bool +static Xamarin.Android.Tools.AvdInfo.operator !=(Xamarin.Android.Tools.AvdInfo? left, Xamarin.Android.Tools.AvdInfo? right) -> bool +static Xamarin.Android.Tools.AvdInfo.operator ==(Xamarin.Android.Tools.AvdInfo? left, Xamarin.Android.Tools.AvdInfo? right) -> bool +static Xamarin.Android.Tools.EmulatorBootOptions.operator !=(Xamarin.Android.Tools.EmulatorBootOptions? left, Xamarin.Android.Tools.EmulatorBootOptions? right) -> bool +static Xamarin.Android.Tools.EmulatorBootOptions.operator ==(Xamarin.Android.Tools.EmulatorBootOptions? left, Xamarin.Android.Tools.EmulatorBootOptions? right) -> bool +static Xamarin.Android.Tools.EmulatorBootResult.operator !=(Xamarin.Android.Tools.EmulatorBootResult? left, Xamarin.Android.Tools.EmulatorBootResult? right) -> bool +static Xamarin.Android.Tools.EmulatorBootResult.operator ==(Xamarin.Android.Tools.EmulatorBootResult? left, Xamarin.Android.Tools.EmulatorBootResult? right) -> bool +static Xamarin.Android.Tools.JdkInstallProgress.operator !=(Xamarin.Android.Tools.JdkInstallProgress? left, Xamarin.Android.Tools.JdkInstallProgress? right) -> bool +static Xamarin.Android.Tools.JdkInstallProgress.operator ==(Xamarin.Android.Tools.JdkInstallProgress? left, Xamarin.Android.Tools.JdkInstallProgress? right) -> bool +static Xamarin.Android.Tools.SdkBootstrapProgress.operator !=(Xamarin.Android.Tools.SdkBootstrapProgress? left, Xamarin.Android.Tools.SdkBootstrapProgress? right) -> bool +static Xamarin.Android.Tools.SdkBootstrapProgress.operator ==(Xamarin.Android.Tools.SdkBootstrapProgress? left, Xamarin.Android.Tools.SdkBootstrapProgress? right) -> bool +static Xamarin.Android.Tools.SdkLicense.operator !=(Xamarin.Android.Tools.SdkLicense? left, Xamarin.Android.Tools.SdkLicense? right) -> bool +static Xamarin.Android.Tools.SdkLicense.operator ==(Xamarin.Android.Tools.SdkLicense? left, Xamarin.Android.Tools.SdkLicense? right) -> bool +static Xamarin.Android.Tools.SdkPackage.operator !=(Xamarin.Android.Tools.SdkPackage? left, Xamarin.Android.Tools.SdkPackage? right) -> bool +static Xamarin.Android.Tools.SdkPackage.operator ==(Xamarin.Android.Tools.SdkPackage? left, Xamarin.Android.Tools.SdkPackage? right) -> bool +virtual Xamarin.Android.Tools.AdbPortRule.$() -> Xamarin.Android.Tools.AdbPortRule! +virtual Xamarin.Android.Tools.AdbPortRule.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AdbPortRule.Equals(Xamarin.Android.Tools.AdbPortRule? other) -> bool +virtual Xamarin.Android.Tools.AdbPortRule.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AdbPortSpec.$() -> Xamarin.Android.Tools.AdbPortSpec! +virtual Xamarin.Android.Tools.AdbPortSpec.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AdbPortSpec.Equals(Xamarin.Android.Tools.AdbPortSpec? other) -> bool +virtual Xamarin.Android.Tools.AdbPortSpec.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AvdDeviceProfile.$() -> Xamarin.Android.Tools.AvdDeviceProfile! +virtual Xamarin.Android.Tools.AvdDeviceProfile.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AvdDeviceProfile.Equals(Xamarin.Android.Tools.AvdDeviceProfile? other) -> bool +virtual Xamarin.Android.Tools.AvdDeviceProfile.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AvdInfo.$() -> Xamarin.Android.Tools.AvdInfo! +virtual Xamarin.Android.Tools.AvdInfo.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AvdInfo.Equals(Xamarin.Android.Tools.AvdInfo? other) -> bool +virtual Xamarin.Android.Tools.AvdInfo.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.EmulatorBootOptions.$() -> Xamarin.Android.Tools.EmulatorBootOptions! +virtual Xamarin.Android.Tools.EmulatorBootOptions.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.EmulatorBootOptions.Equals(Xamarin.Android.Tools.EmulatorBootOptions? other) -> bool +virtual Xamarin.Android.Tools.EmulatorBootOptions.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.EmulatorBootResult.$() -> Xamarin.Android.Tools.EmulatorBootResult! +virtual Xamarin.Android.Tools.EmulatorBootResult.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.EmulatorBootResult.Equals(Xamarin.Android.Tools.EmulatorBootResult? other) -> bool +virtual Xamarin.Android.Tools.EmulatorBootResult.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.JdkInstallProgress.$() -> Xamarin.Android.Tools.JdkInstallProgress! +virtual Xamarin.Android.Tools.JdkInstallProgress.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.JdkInstallProgress.Equals(Xamarin.Android.Tools.JdkInstallProgress? other) -> bool +virtual Xamarin.Android.Tools.JdkInstallProgress.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkBootstrapProgress.$() -> Xamarin.Android.Tools.SdkBootstrapProgress! +virtual Xamarin.Android.Tools.SdkBootstrapProgress.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkBootstrapProgress.Equals(Xamarin.Android.Tools.SdkBootstrapProgress? other) -> bool +virtual Xamarin.Android.Tools.SdkBootstrapProgress.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkLicense.$() -> Xamarin.Android.Tools.SdkLicense! +virtual Xamarin.Android.Tools.SdkLicense.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkLicense.Equals(Xamarin.Android.Tools.SdkLicense? other) -> bool +virtual Xamarin.Android.Tools.SdkLicense.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkPackage.$() -> Xamarin.Android.Tools.SdkPackage! +virtual Xamarin.Android.Tools.SdkPackage.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkPackage.Equals(Xamarin.Android.Tools.SdkPackage? other) -> bool +virtual Xamarin.Android.Tools.SdkPackage.PrintMembers(System.Text.StringBuilder! builder) -> bool +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortRule! original) -> void +Xamarin.Android.Tools.AdbPortRule.Deconstruct(out Xamarin.Android.Tools.AdbPortSpec! Remote, out Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbPortSpec! original) -> void +Xamarin.Android.Tools.AdbPortSpec.Deconstruct(out Xamarin.Android.Tools.AdbProtocol Protocol, out int Port) -> void +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(Xamarin.Android.Tools.AvdDeviceProfile! original) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Deconstruct(out string! Id) -> void +Xamarin.Android.Tools.AvdInfo.AvdInfo(Xamarin.Android.Tools.AvdInfo! original) -> void +Xamarin.Android.Tools.AvdInfo.Deconstruct(out string! Name, out string? DeviceProfile, out string? Path) -> void +Xamarin.Android.Tools.EmulatorBootOptions.EmulatorBootOptions() -> void +Xamarin.Android.Tools.EmulatorBootOptions.EmulatorBootOptions(Xamarin.Android.Tools.EmulatorBootOptions! original) -> void +Xamarin.Android.Tools.EmulatorBootResult.EmulatorBootResult() -> void +Xamarin.Android.Tools.EmulatorBootResult.EmulatorBootResult(Xamarin.Android.Tools.EmulatorBootResult! original) -> void +Xamarin.Android.Tools.JdkInstallProgress.Deconstruct(out Xamarin.Android.Tools.JdkInstallPhase Phase, out double PercentComplete, out string? Message) -> void +Xamarin.Android.Tools.JdkInstallProgress.JdkInstallProgress(Xamarin.Android.Tools.JdkInstallProgress! original) -> void +Xamarin.Android.Tools.SdkBootstrapProgress.Deconstruct(out Xamarin.Android.Tools.SdkBootstrapPhase Phase, out int PercentComplete, out string! Message) -> void +Xamarin.Android.Tools.SdkBootstrapProgress.SdkBootstrapProgress(Xamarin.Android.Tools.SdkBootstrapProgress! original) -> void +Xamarin.Android.Tools.SdkLicense.Deconstruct(out string! Id, out string! Text) -> void +Xamarin.Android.Tools.SdkLicense.SdkLicense(Xamarin.Android.Tools.SdkLicense! original) -> void +Xamarin.Android.Tools.SdkPackage.Deconstruct(out string! Path, out string? Version, out string? Description, out bool IsInstalled) -> void +Xamarin.Android.Tools.SdkPackage.SdkPackage(Xamarin.Android.Tools.SdkPackage! original) -> void diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..47e8e682995 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1,131 @@ +#nullable enable +Xamarin.Android.Tools.AndroidAppManifest +Xamarin.Android.Tools.AndroidAppManifest.AndroidPermissions.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.AndroidPermissionsQualified.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.ApplicationIcon.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationIcon.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationLabel.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationLabel.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationRoundIcon.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationRoundIcon.set -> void +Xamarin.Android.Tools.AndroidAppManifest.ApplicationTheme.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.ApplicationTheme.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Debuggable.get -> bool? +Xamarin.Android.Tools.AndroidAppManifest.Debuggable.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Document.get -> System.Xml.Linq.XDocument! +Xamarin.Android.Tools.AndroidAppManifest.GetAllActivityNames() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableActivityNames() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableFastDevActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.GetLaunchableUserActivityName() -> string? +Xamarin.Android.Tools.AndroidAppManifest.InstallLocation.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.InstallLocation.set -> void +Xamarin.Android.Tools.AndroidAppManifest.MinSdkVersion.get -> int? +Xamarin.Android.Tools.AndroidAppManifest.MinSdkVersion.set -> void +Xamarin.Android.Tools.AndroidAppManifest.PackageName.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.PackageName.set -> void +Xamarin.Android.Tools.AndroidAppManifest.SetAndroidPermissions(System.Collections.Generic.IEnumerable! permissions) -> void +Xamarin.Android.Tools.AndroidAppManifest.TargetSdkVersion.get -> int? +Xamarin.Android.Tools.AndroidAppManifest.TargetSdkVersion.set -> void +Xamarin.Android.Tools.AndroidAppManifest.VersionCode.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.VersionCode.set -> void +Xamarin.Android.Tools.AndroidAppManifest.VersionName.get -> string? +Xamarin.Android.Tools.AndroidAppManifest.VersionName.set -> void +Xamarin.Android.Tools.AndroidAppManifest.Write(System.Xml.XmlWriter! writer) -> void +Xamarin.Android.Tools.AndroidAppManifest.WriteToFile(string! fileName) -> void +Xamarin.Android.Tools.AndroidSdkInfo +Xamarin.Android.Tools.AndroidSdkInfo.AllAndroidSdkPaths.get -> string![]! +Xamarin.Android.Tools.AndroidSdkInfo.AndroidNdkHostPlatform.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.AndroidNdkPath.get -> string? +Xamarin.Android.Tools.AndroidSdkInfo.AndroidSdkInfo(System.Action? logger = null, string? androidSdkPath = null, string? androidNdkPath = null, string? javaSdkPath = null) -> void +Xamarin.Android.Tools.AndroidSdkInfo.AndroidSdkPath.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.GetBuildToolsPaths() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetBuildToolsPaths(string! preferredBuildToolsVersion) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetCommandLineToolsPaths() -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetCommandLineToolsPaths(string! preferredCommandLineToolsVersion) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetInstalledPlatformVersions(Xamarin.Android.Tools.AndroidVersions! versions) -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.AndroidSdkInfo.GetPlatformDirectory(int apiLevel) -> string! +Xamarin.Android.Tools.AndroidSdkInfo.GetPlatformDirectoryFromId(string! id) -> string! +Xamarin.Android.Tools.AndroidSdkInfo.IsPlatformInstalled(int apiLevel) -> bool +Xamarin.Android.Tools.AndroidSdkInfo.JavaSdkPath.get -> string! +Xamarin.Android.Tools.AndroidSdkInfo.TryGetCommandLineToolsPath() -> string? +Xamarin.Android.Tools.AndroidSdkInfo.TryGetPlatformDirectoryFromApiLevel(string! idOrApiLevel, Xamarin.Android.Tools.AndroidVersions! versions) -> string? +Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Arm = 1 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Arm64 = 8 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Mips = 4 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.None = 0 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.Other = 65536 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.X86 = 2 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidTargetArch.X86_64 = 16 -> Xamarin.Android.Tools.AndroidTargetArch +Xamarin.Android.Tools.AndroidVersion +Xamarin.Android.Tools.AndroidVersion.AndroidVersion(System.Version! versionCodeFull, string! osVersion, string? codeName = null, string? id = null, bool stable = true) -> void +Xamarin.Android.Tools.AndroidVersion.AndroidVersion(int apiLevel, string! osVersion, string? codeName = null, string? id = null, bool stable = true) -> void +Xamarin.Android.Tools.AndroidVersion.ApiLevel.get -> int +Xamarin.Android.Tools.AndroidVersion.CodeName.get -> string? +Xamarin.Android.Tools.AndroidVersion.FrameworkVersion.get -> string! +Xamarin.Android.Tools.AndroidVersion.Id.get -> string! +Xamarin.Android.Tools.AndroidVersion.OSVersion.get -> string! +Xamarin.Android.Tools.AndroidVersion.Stable.get -> bool +Xamarin.Android.Tools.AndroidVersion.TargetFrameworkVersion.get -> System.Version! +Xamarin.Android.Tools.AndroidVersion.VersionCodeFull.get -> System.Version! +Xamarin.Android.Tools.AndroidVersions +Xamarin.Android.Tools.AndroidVersions.AndroidVersions(System.Collections.Generic.IEnumerable! versions) -> void +Xamarin.Android.Tools.AndroidVersions.AndroidVersions(System.Collections.Generic.IEnumerable! frameworkDirectories) -> void +Xamarin.Android.Tools.AndroidVersions.FrameworkDirectories.get -> System.Collections.Generic.IReadOnlyList! +Xamarin.Android.Tools.AndroidVersions.GetApiLevelFromFrameworkVersion(string! frameworkVersion) -> int? +Xamarin.Android.Tools.AndroidVersions.GetApiLevelFromId(string! id) -> int? +Xamarin.Android.Tools.AndroidVersions.GetFrameworkVersionFromApiLevel(int apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetFrameworkVersionFromId(string! id) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromApiLevel(int apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromApiLevel(string! apiLevel) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromFrameworkVersion(string! frameworkVersion) -> string? +Xamarin.Android.Tools.AndroidVersions.GetIdFromVersionCodeFull(System.Version! versionCodeFull) -> string? +Xamarin.Android.Tools.AndroidVersions.InstalledBindingVersions.get -> System.Collections.Generic.IReadOnlyList! +Xamarin.Android.Tools.AndroidVersions.MaxStableVersion.get -> Xamarin.Android.Tools.AndroidVersion? +Xamarin.Android.Tools.AndroidVersions.MinStableVersion.get -> Xamarin.Android.Tools.AndroidVersion? +Xamarin.Android.Tools.JdkInfo +Xamarin.Android.Tools.JdkInfo.GetJavaSettingsPropertyValue(string! key, out string? value) -> bool +Xamarin.Android.Tools.JdkInfo.GetJavaSettingsPropertyValues(string! key, out System.Collections.Generic.IEnumerable? value) -> bool +Xamarin.Android.Tools.JdkInfo.HomePath.get -> string! +Xamarin.Android.Tools.JdkInfo.IncludePath.get -> System.Collections.ObjectModel.ReadOnlyCollection! +Xamarin.Android.Tools.JdkInfo.JarPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JavaPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JavaSettingsPropertyKeys.get -> System.Collections.Generic.IEnumerable! +Xamarin.Android.Tools.JdkInfo.JavacPath.get -> string! +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath) -> void +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath, string! locator) -> void +Xamarin.Android.Tools.JdkInfo.JdkInfo(string! homePath, string? locator = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.JdkInfo.JdkJvmPath.get -> string! +Xamarin.Android.Tools.JdkInfo.Locator.get -> string? +Xamarin.Android.Tools.JdkInfo.ReleaseProperties.get -> System.Collections.ObjectModel.ReadOnlyDictionary! +Xamarin.Android.Tools.JdkInfo.Vendor.get -> string? +Xamarin.Android.Tools.JdkInfo.Version.get -> System.Version? +Xamarin.Android.Tools.KernelEx +Xamarin.Android.Tools.OS +Xamarin.Android.Tools.OS.OS() -> void +Xamarin.Android.Tools.ProcessUtils +override Xamarin.Android.Tools.AndroidVersion.ToString() -> string! +override Xamarin.Android.Tools.JdkInfo.ToString() -> string! +static Xamarin.Android.Tools.AndroidAppManifest.AndroidXNamespace.get -> System.Xml.Linq.XNamespace! +static Xamarin.Android.Tools.AndroidAppManifest.CanonicalizePackageName(string! packageNameOrAssemblyName) -> string! +static Xamarin.Android.Tools.AndroidAppManifest.Create(string! packageName, string! appLabel, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.Load(System.Xml.Linq.XDocument! doc, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.Load(string! filename, Xamarin.Android.Tools.AndroidVersions! versions) -> Xamarin.Android.Tools.AndroidAppManifest! +static Xamarin.Android.Tools.AndroidAppManifest.NameXName.get -> System.Xml.Linq.XName! +static Xamarin.Android.Tools.AndroidSdkInfo.DetectAndSetPreferredJavaSdkPathToLatest(System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredAndroidNdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredAndroidSdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidSdkInfo.SetPreferredJavaSdkPath(string! path, System.Action? logger = null) -> void +static Xamarin.Android.Tools.AndroidVersion.Load(System.IO.Stream! stream) -> Xamarin.Android.Tools.AndroidVersion! +static Xamarin.Android.Tools.AndroidVersion.Load(string! uri) -> Xamarin.Android.Tools.AndroidVersion! +static Xamarin.Android.Tools.JdkInfo.GetKnownSystemJdkInfos(System.Action? logger = null) -> System.Collections.Generic.IEnumerable! +static Xamarin.Android.Tools.JdkInfo.GetSupportedJdkInfos(System.Action? logger = null) -> System.Collections.Generic.IEnumerable! +static Xamarin.Android.Tools.KernelEx.GetLongPathName(string! path) -> string! +static Xamarin.Android.Tools.KernelEx.GetShortPathName(string! path) -> string! +static Xamarin.Android.Tools.ProcessUtils.ExecuteToolAsync(string! exe, System.Func! result, System.Threading.CancellationToken token, System.Action? onStarted = null) -> System.Threading.Tasks.Task! +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Action? onStarted = null) -> System.Threading.Tasks.Task! +static readonly Xamarin.Android.Tools.AndroidVersions.KnownVersions -> Xamarin.Android.Tools.AndroidVersion![]! +static readonly Xamarin.Android.Tools.OS.IsLinux -> bool +static readonly Xamarin.Android.Tools.OS.IsMac -> bool +static readonly Xamarin.Android.Tools.OS.IsWindows -> bool diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..21b4baba56a --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,319 @@ +#nullable enable +Xamarin.Android.Tools.AdbDeviceInfo +Xamarin.Android.Tools.AdbDeviceInfo.AdbDeviceInfo() -> void +Xamarin.Android.Tools.AdbDeviceInfo.AvdName.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.AvdName.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Description.get -> string! +Xamarin.Android.Tools.AdbDeviceInfo.Description.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Device.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Device.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.IsEmulator.get -> bool +Xamarin.Android.Tools.AdbDeviceInfo.Model.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Model.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Product.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.Product.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Serial.get -> string! +Xamarin.Android.Tools.AdbDeviceInfo.Serial.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Status.get -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceInfo.Status.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.TransportId.get -> string? +Xamarin.Android.Tools.AdbDeviceInfo.TransportId.set -> void +Xamarin.Android.Tools.AdbDeviceInfo.Type.get -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceInfo.Type.set -> void +Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.NoPermissions = 3 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.NotRunning = 4 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Offline = 1 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Online = 0 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Unauthorized = 2 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceStatus.Unknown = 5 -> Xamarin.Android.Tools.AdbDeviceStatus +Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceType.Device = 0 -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbDeviceType.Emulator = 1 -> Xamarin.Android.Tools.AdbDeviceType +Xamarin.Android.Tools.AdbRunner +Xamarin.Android.Tools.AdbRunner.AdbRunner(string! adbPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +virtual Xamarin.Android.Tools.AdbRunner.ListDevicesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AdbRunner.StopEmulatorAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AdbRunner.WaitForDeviceAsync(string? serial = null, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.ChecksumType.Sha1 = 0 -> Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.ChecksumType.Sha256 = 1 -> Xamarin.Android.Tools.ChecksumType +Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Complete = 4 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Downloading = 0 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Extracting = 2 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Validating = 3 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallPhase.Verifying = 1 -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallProgress +Xamarin.Android.Tools.JdkInstallProgress.JdkInstallProgress(Xamarin.Android.Tools.JdkInstallPhase Phase, double PercentComplete, string? Message = null) -> void +Xamarin.Android.Tools.JdkInstallProgress.Message.get -> string? +Xamarin.Android.Tools.JdkInstallProgress.Message.init -> void +Xamarin.Android.Tools.JdkInstallProgress.PercentComplete.get -> double +Xamarin.Android.Tools.JdkInstallProgress.PercentComplete.init -> void +Xamarin.Android.Tools.JdkInstallProgress.Phase.get -> Xamarin.Android.Tools.JdkInstallPhase +Xamarin.Android.Tools.JdkInstallProgress.Phase.init -> void +Xamarin.Android.Tools.JdkInstaller +Xamarin.Android.Tools.JdkInstaller.DiscoverAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.JdkInstaller.Dispose() -> void +Xamarin.Android.Tools.JdkInstaller.InstallAsync(int majorVersion, string! targetPath, System.IProgress? progress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.JdkInstaller.IsValid(string! jdkPath) -> bool +Xamarin.Android.Tools.JdkInstaller.JdkInstaller(System.Action? logger = null) -> void +Xamarin.Android.Tools.JdkInstaller.Remove(string! jdkPath) -> bool +Xamarin.Android.Tools.JdkVersionInfo +Xamarin.Android.Tools.JdkVersionInfo.Checksum.get -> string? +Xamarin.Android.Tools.JdkVersionInfo.ChecksumUrl.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.DisplayName.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.DownloadUrl.get -> string! +Xamarin.Android.Tools.JdkVersionInfo.JdkVersionInfo(int majorVersion, string! displayName, string! downloadUrl, string! checksumUrl, long size = 0, string? checksum = null) -> void +Xamarin.Android.Tools.JdkVersionInfo.MajorVersion.get -> int +Xamarin.Android.Tools.JdkVersionInfo.ResolvedUrl.get -> string? +Xamarin.Android.Tools.JdkVersionInfo.Size.get -> long +Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Complete = 4 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Downloading = 1 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Extracting = 3 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.ReadingManifest = 0 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapPhase.Verifying = 2 -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapProgress +Xamarin.Android.Tools.SdkBootstrapProgress.Message.get -> string! +Xamarin.Android.Tools.SdkBootstrapProgress.Message.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.PercentComplete.get -> int +Xamarin.Android.Tools.SdkBootstrapProgress.PercentComplete.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.Phase.get -> Xamarin.Android.Tools.SdkBootstrapPhase +Xamarin.Android.Tools.SdkBootstrapProgress.Phase.init -> void +Xamarin.Android.Tools.SdkBootstrapProgress.SdkBootstrapProgress(Xamarin.Android.Tools.SdkBootstrapPhase Phase, int PercentComplete = -1, string! Message = "") -> void +Xamarin.Android.Tools.SdkLicense +Xamarin.Android.Tools.SdkLicense.Id.get -> string! +Xamarin.Android.Tools.SdkLicense.Id.init -> void +Xamarin.Android.Tools.SdkLicense.SdkLicense(string! Id, string! Text) -> void +Xamarin.Android.Tools.SdkLicense.Text.get -> string! +Xamarin.Android.Tools.SdkLicense.Text.init -> void +Xamarin.Android.Tools.SdkManager +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync() -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Collections.Generic.IEnumerable! licenseIds) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Collections.Generic.IEnumerable! licenseIds, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AcceptLicensesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.AndroidSdkPath.get -> string? +Xamarin.Android.Tools.SdkManager.AndroidSdkPath.set -> void +Xamarin.Android.Tools.SdkManager.AreLicensesAccepted() -> bool +Xamarin.Android.Tools.SdkManager.BootstrapAsync(string! targetPath, System.IProgress? progress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.Dispose() -> void +Xamarin.Android.Tools.SdkManager.FindSdkManagerPath() -> string? +Xamarin.Android.Tools.SdkManager.GetPendingLicensesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.SdkManager.InstallAsync(System.Collections.Generic.IEnumerable! packages, bool acceptLicenses = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.JavaSdkPath.get -> string? +Xamarin.Android.Tools.SdkManager.JavaSdkPath.set -> void +Xamarin.Android.Tools.SdkManager.ListAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Installed, System.Collections.Generic.IReadOnlyList! Available)>! +Xamarin.Android.Tools.SdkManager.ManifestFeedUrl.get -> string! +Xamarin.Android.Tools.SdkManager.ManifestFeedUrl.set -> void +Xamarin.Android.Tools.SdkManager.SdkManager(System.Action? logger = null) -> void +Xamarin.Android.Tools.SdkManager.UninstallAsync(System.Collections.Generic.IEnumerable! packages, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkManager.UpdateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.SdkPackage +Xamarin.Android.Tools.SdkPackage.Description.get -> string? +Xamarin.Android.Tools.SdkPackage.Description.init -> void +Xamarin.Android.Tools.SdkPackage.IsInstalled.get -> bool +Xamarin.Android.Tools.SdkPackage.IsInstalled.init -> void +Xamarin.Android.Tools.SdkPackage.Path.get -> string! +Xamarin.Android.Tools.SdkPackage.Path.init -> void +Xamarin.Android.Tools.SdkPackage.SdkPackage(string! Path, string? Version = null, string? Description = null, bool IsInstalled = false) -> void +Xamarin.Android.Tools.SdkPackage.Version.get -> string? +Xamarin.Android.Tools.SdkPackage.Version.init -> void +const Xamarin.Android.Tools.SdkManager.DefaultManifestFeedUrl = "https://aka.ms/AndroidManifestFeed/d18-0" -> string! +override Xamarin.Android.Tools.JdkVersionInfo.ToString() -> string! +static Xamarin.Android.Tools.AdbRunner.BuildDeviceDescription(Xamarin.Android.Tools.AdbDeviceInfo! device, System.Action? logger = null) -> string! +static Xamarin.Android.Tools.AdbRunner.FormatDisplayName(string! avdName) -> string! +static Xamarin.Android.Tools.AdbRunner.MapAdbStateToStatus(string! adbState) -> Xamarin.Android.Tools.AdbDeviceStatus +static Xamarin.Android.Tools.AdbRunner.MergeDevicesAndEmulators(System.Collections.Generic.IReadOnlyList! adbDevices, System.Collections.Generic.IReadOnlyList! availableEmulators, System.Action? logger = null) -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.AdbRunner.ParseAdbDevicesOutput(System.Collections.Generic.IEnumerable! lines) -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.JdkInstaller.RecommendedMajorVersion.get -> int +static Xamarin.Android.Tools.JdkInstaller.SupportedVersions.get -> System.Collections.Generic.IReadOnlyList! +static Xamarin.Android.Tools.ProcessUtils.CreateProcessStartInfo(string! fileName, params string![]! args) -> System.Diagnostics.ProcessStartInfo! +static Xamarin.Android.Tools.ProcessUtils.IsElevated() -> bool +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Collections.Generic.IDictionary? environmentVariables, System.Action? onStarted) -> System.Threading.Tasks.Task! +static Xamarin.Android.Tools.ProcessUtils.StartProcess(System.Diagnostics.ProcessStartInfo! psi, System.IO.TextWriter? stdout, System.IO.TextWriter? stderr, System.Threading.CancellationToken cancellationToken, System.Collections.Generic.IDictionary? environmentVariables) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdInfo +Xamarin.Android.Tools.AvdInfo.AvdInfo(string! Name, string? DeviceProfile = null, string? Path = null) -> void +Xamarin.Android.Tools.AvdInfo.DeviceProfile.get -> string? +Xamarin.Android.Tools.AvdInfo.DeviceProfile.init -> void +Xamarin.Android.Tools.AvdInfo.Name.get -> string! +Xamarin.Android.Tools.AvdInfo.Name.init -> void +Xamarin.Android.Tools.AvdInfo.Path.get -> string? +Xamarin.Android.Tools.AvdInfo.Path.init -> void +Xamarin.Android.Tools.AvdManagerRunner +Xamarin.Android.Tools.AvdManagerRunner.AvdManagerRunner(string! avdManagerPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.AvdManagerRunner.GetOrCreateAvdAsync(string! name, string! systemImage, string? deviceProfile = null, bool force = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.DeleteAvdAsync(string! name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListAvdsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.GetShellPropertyAsync(string! serial, string! propertyName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RunShellCommandAsync(string! serial, string! command, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RunShellCommandAsync(string! serial, string! command, string![]! args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.EmulatorBootOptions +Xamarin.Android.Tools.EmulatorBootOptions.AdditionalArgs.get -> System.Collections.Generic.List? +Xamarin.Android.Tools.EmulatorBootOptions.AdditionalArgs.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.BootTimeout.get -> System.TimeSpan +Xamarin.Android.Tools.EmulatorBootOptions.BootTimeout.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.ColdBoot.get -> bool +Xamarin.Android.Tools.EmulatorBootOptions.ColdBoot.init -> void +Xamarin.Android.Tools.EmulatorBootOptions.PollInterval.get -> System.TimeSpan +Xamarin.Android.Tools.EmulatorBootOptions.PollInterval.init -> void +Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.None = 0 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.LaunchFailed = 1 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Timeout = 2 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Cancelled = 3 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootErrorKind.Unknown = 4 -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootResult +Xamarin.Android.Tools.EmulatorBootResult.ErrorKind.get -> Xamarin.Android.Tools.EmulatorBootErrorKind +Xamarin.Android.Tools.EmulatorBootResult.ErrorKind.init -> void +Xamarin.Android.Tools.EmulatorBootResult.ErrorMessage.get -> string? +Xamarin.Android.Tools.EmulatorBootResult.ErrorMessage.init -> void +Xamarin.Android.Tools.EmulatorBootResult.Serial.get -> string? +Xamarin.Android.Tools.EmulatorBootResult.Serial.init -> void +Xamarin.Android.Tools.EmulatorBootResult.Success.get -> bool +Xamarin.Android.Tools.EmulatorBootResult.Success.init -> void +Xamarin.Android.Tools.EmulatorRunner +Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.AdbRunner! adbRunner, Xamarin.Android.Tools.EmulatorBootOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void +Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! +Xamarin.Android.Tools.AdbPortRule +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortSpec! Remote, Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortRule.Local.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Local.init -> void +Xamarin.Android.Tools.AdbPortRule.Remote.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Remote.init -> void +Xamarin.Android.Tools.AdbPortSpec +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbProtocol Protocol, int Port) -> void +Xamarin.Android.Tools.AdbPortSpec.Port.get -> int +Xamarin.Android.Tools.AdbPortSpec.Port.init -> void +Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void +Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! +Xamarin.Android.Tools.AdbProtocol + +Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol +override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AvdDeviceProfile +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void +override Xamarin.Android.Tools.AdbPortRule.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AdbPortRule.GetHashCode() -> int +override Xamarin.Android.Tools.AdbPortRule.ToString() -> string! +override Xamarin.Android.Tools.AdbPortSpec.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AdbPortSpec.GetHashCode() -> int +override Xamarin.Android.Tools.AvdDeviceProfile.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AvdDeviceProfile.GetHashCode() -> int +override Xamarin.Android.Tools.AvdDeviceProfile.ToString() -> string! +override Xamarin.Android.Tools.AvdInfo.Equals(object? obj) -> bool +override Xamarin.Android.Tools.AvdInfo.GetHashCode() -> int +override Xamarin.Android.Tools.AvdInfo.ToString() -> string! +override Xamarin.Android.Tools.EmulatorBootOptions.Equals(object? obj) -> bool +override Xamarin.Android.Tools.EmulatorBootOptions.GetHashCode() -> int +override Xamarin.Android.Tools.EmulatorBootOptions.ToString() -> string! +override Xamarin.Android.Tools.EmulatorBootResult.Equals(object? obj) -> bool +override Xamarin.Android.Tools.EmulatorBootResult.GetHashCode() -> int +override Xamarin.Android.Tools.EmulatorBootResult.ToString() -> string! +override Xamarin.Android.Tools.JdkInstallProgress.Equals(object? obj) -> bool +override Xamarin.Android.Tools.JdkInstallProgress.GetHashCode() -> int +override Xamarin.Android.Tools.JdkInstallProgress.ToString() -> string! +override Xamarin.Android.Tools.SdkBootstrapProgress.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkBootstrapProgress.GetHashCode() -> int +override Xamarin.Android.Tools.SdkBootstrapProgress.ToString() -> string! +override Xamarin.Android.Tools.SdkLicense.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkLicense.GetHashCode() -> int +override Xamarin.Android.Tools.SdkLicense.ToString() -> string! +override Xamarin.Android.Tools.SdkPackage.Equals(object? obj) -> bool +override Xamarin.Android.Tools.SdkPackage.GetHashCode() -> int +override Xamarin.Android.Tools.SdkPackage.ToString() -> string! +static Xamarin.Android.Tools.AdbPortRule.operator !=(Xamarin.Android.Tools.AdbPortRule? left, Xamarin.Android.Tools.AdbPortRule? right) -> bool +static Xamarin.Android.Tools.AdbPortRule.operator ==(Xamarin.Android.Tools.AdbPortRule? left, Xamarin.Android.Tools.AdbPortRule? right) -> bool +static Xamarin.Android.Tools.AdbPortSpec.operator !=(Xamarin.Android.Tools.AdbPortSpec? left, Xamarin.Android.Tools.AdbPortSpec? right) -> bool +static Xamarin.Android.Tools.AdbPortSpec.operator ==(Xamarin.Android.Tools.AdbPortSpec? left, Xamarin.Android.Tools.AdbPortSpec? right) -> bool +static Xamarin.Android.Tools.AvdDeviceProfile.operator !=(Xamarin.Android.Tools.AvdDeviceProfile? left, Xamarin.Android.Tools.AvdDeviceProfile? right) -> bool +static Xamarin.Android.Tools.AvdDeviceProfile.operator ==(Xamarin.Android.Tools.AvdDeviceProfile? left, Xamarin.Android.Tools.AvdDeviceProfile? right) -> bool +static Xamarin.Android.Tools.AvdInfo.operator !=(Xamarin.Android.Tools.AvdInfo? left, Xamarin.Android.Tools.AvdInfo? right) -> bool +static Xamarin.Android.Tools.AvdInfo.operator ==(Xamarin.Android.Tools.AvdInfo? left, Xamarin.Android.Tools.AvdInfo? right) -> bool +static Xamarin.Android.Tools.EmulatorBootOptions.operator !=(Xamarin.Android.Tools.EmulatorBootOptions? left, Xamarin.Android.Tools.EmulatorBootOptions? right) -> bool +static Xamarin.Android.Tools.EmulatorBootOptions.operator ==(Xamarin.Android.Tools.EmulatorBootOptions? left, Xamarin.Android.Tools.EmulatorBootOptions? right) -> bool +static Xamarin.Android.Tools.EmulatorBootResult.operator !=(Xamarin.Android.Tools.EmulatorBootResult? left, Xamarin.Android.Tools.EmulatorBootResult? right) -> bool +static Xamarin.Android.Tools.EmulatorBootResult.operator ==(Xamarin.Android.Tools.EmulatorBootResult? left, Xamarin.Android.Tools.EmulatorBootResult? right) -> bool +static Xamarin.Android.Tools.JdkInstallProgress.operator !=(Xamarin.Android.Tools.JdkInstallProgress? left, Xamarin.Android.Tools.JdkInstallProgress? right) -> bool +static Xamarin.Android.Tools.JdkInstallProgress.operator ==(Xamarin.Android.Tools.JdkInstallProgress? left, Xamarin.Android.Tools.JdkInstallProgress? right) -> bool +static Xamarin.Android.Tools.SdkBootstrapProgress.operator !=(Xamarin.Android.Tools.SdkBootstrapProgress? left, Xamarin.Android.Tools.SdkBootstrapProgress? right) -> bool +static Xamarin.Android.Tools.SdkBootstrapProgress.operator ==(Xamarin.Android.Tools.SdkBootstrapProgress? left, Xamarin.Android.Tools.SdkBootstrapProgress? right) -> bool +static Xamarin.Android.Tools.SdkLicense.operator !=(Xamarin.Android.Tools.SdkLicense? left, Xamarin.Android.Tools.SdkLicense? right) -> bool +static Xamarin.Android.Tools.SdkLicense.operator ==(Xamarin.Android.Tools.SdkLicense? left, Xamarin.Android.Tools.SdkLicense? right) -> bool +static Xamarin.Android.Tools.SdkPackage.operator !=(Xamarin.Android.Tools.SdkPackage? left, Xamarin.Android.Tools.SdkPackage? right) -> bool +static Xamarin.Android.Tools.SdkPackage.operator ==(Xamarin.Android.Tools.SdkPackage? left, Xamarin.Android.Tools.SdkPackage? right) -> bool +virtual Xamarin.Android.Tools.AdbPortRule.$() -> Xamarin.Android.Tools.AdbPortRule! +virtual Xamarin.Android.Tools.AdbPortRule.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AdbPortRule.Equals(Xamarin.Android.Tools.AdbPortRule? other) -> bool +virtual Xamarin.Android.Tools.AdbPortRule.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AdbPortSpec.$() -> Xamarin.Android.Tools.AdbPortSpec! +virtual Xamarin.Android.Tools.AdbPortSpec.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AdbPortSpec.Equals(Xamarin.Android.Tools.AdbPortSpec? other) -> bool +virtual Xamarin.Android.Tools.AdbPortSpec.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AvdDeviceProfile.$() -> Xamarin.Android.Tools.AvdDeviceProfile! +virtual Xamarin.Android.Tools.AvdDeviceProfile.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AvdDeviceProfile.Equals(Xamarin.Android.Tools.AvdDeviceProfile? other) -> bool +virtual Xamarin.Android.Tools.AvdDeviceProfile.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.AvdInfo.$() -> Xamarin.Android.Tools.AvdInfo! +virtual Xamarin.Android.Tools.AvdInfo.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.AvdInfo.Equals(Xamarin.Android.Tools.AvdInfo? other) -> bool +virtual Xamarin.Android.Tools.AvdInfo.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.EmulatorBootOptions.$() -> Xamarin.Android.Tools.EmulatorBootOptions! +virtual Xamarin.Android.Tools.EmulatorBootOptions.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.EmulatorBootOptions.Equals(Xamarin.Android.Tools.EmulatorBootOptions? other) -> bool +virtual Xamarin.Android.Tools.EmulatorBootOptions.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.EmulatorBootResult.$() -> Xamarin.Android.Tools.EmulatorBootResult! +virtual Xamarin.Android.Tools.EmulatorBootResult.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.EmulatorBootResult.Equals(Xamarin.Android.Tools.EmulatorBootResult? other) -> bool +virtual Xamarin.Android.Tools.EmulatorBootResult.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.JdkInstallProgress.$() -> Xamarin.Android.Tools.JdkInstallProgress! +virtual Xamarin.Android.Tools.JdkInstallProgress.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.JdkInstallProgress.Equals(Xamarin.Android.Tools.JdkInstallProgress? other) -> bool +virtual Xamarin.Android.Tools.JdkInstallProgress.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkBootstrapProgress.$() -> Xamarin.Android.Tools.SdkBootstrapProgress! +virtual Xamarin.Android.Tools.SdkBootstrapProgress.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkBootstrapProgress.Equals(Xamarin.Android.Tools.SdkBootstrapProgress? other) -> bool +virtual Xamarin.Android.Tools.SdkBootstrapProgress.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkLicense.$() -> Xamarin.Android.Tools.SdkLicense! +virtual Xamarin.Android.Tools.SdkLicense.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkLicense.Equals(Xamarin.Android.Tools.SdkLicense? other) -> bool +virtual Xamarin.Android.Tools.SdkLicense.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual Xamarin.Android.Tools.SdkPackage.$() -> Xamarin.Android.Tools.SdkPackage! +virtual Xamarin.Android.Tools.SdkPackage.EqualityContract.get -> System.Type! +virtual Xamarin.Android.Tools.SdkPackage.Equals(Xamarin.Android.Tools.SdkPackage? other) -> bool +virtual Xamarin.Android.Tools.SdkPackage.PrintMembers(System.Text.StringBuilder! builder) -> bool +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortRule! original) -> void +Xamarin.Android.Tools.AdbPortRule.Deconstruct(out Xamarin.Android.Tools.AdbPortSpec! Remote, out Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbPortSpec! original) -> void +Xamarin.Android.Tools.AdbPortSpec.Deconstruct(out Xamarin.Android.Tools.AdbProtocol Protocol, out int Port) -> void +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(Xamarin.Android.Tools.AvdDeviceProfile! original) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Deconstruct(out string! Id) -> void +Xamarin.Android.Tools.AvdInfo.AvdInfo(Xamarin.Android.Tools.AvdInfo! original) -> void +Xamarin.Android.Tools.AvdInfo.Deconstruct(out string! Name, out string? DeviceProfile, out string? Path) -> void +Xamarin.Android.Tools.EmulatorBootOptions.EmulatorBootOptions() -> void +Xamarin.Android.Tools.EmulatorBootOptions.EmulatorBootOptions(Xamarin.Android.Tools.EmulatorBootOptions! original) -> void +Xamarin.Android.Tools.EmulatorBootResult.EmulatorBootResult() -> void +Xamarin.Android.Tools.EmulatorBootResult.EmulatorBootResult(Xamarin.Android.Tools.EmulatorBootResult! original) -> void +Xamarin.Android.Tools.JdkInstallProgress.Deconstruct(out Xamarin.Android.Tools.JdkInstallPhase Phase, out double PercentComplete, out string? Message) -> void +Xamarin.Android.Tools.JdkInstallProgress.JdkInstallProgress(Xamarin.Android.Tools.JdkInstallProgress! original) -> void +Xamarin.Android.Tools.SdkBootstrapProgress.Deconstruct(out Xamarin.Android.Tools.SdkBootstrapPhase Phase, out int PercentComplete, out string! Message) -> void +Xamarin.Android.Tools.SdkBootstrapProgress.SdkBootstrapProgress(Xamarin.Android.Tools.SdkBootstrapProgress! original) -> void +Xamarin.Android.Tools.SdkLicense.Deconstruct(out string! Id, out string! Text) -> void +Xamarin.Android.Tools.SdkLicense.SdkLicense(Xamarin.Android.Tools.SdkLicense! original) -> void +Xamarin.Android.Tools.SdkPackage.Deconstruct(out string! Path, out string? Version, out string? Description, out bool IsInstalled) -> void +Xamarin.Android.Tools.SdkPackage.SdkPackage(Xamarin.Android.Tools.SdkPackage! original) -> void diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs new file mode 100644 index 00000000000..190e6661196 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents an adb port forwarding rule as reported by 'adb reverse --list' or 'adb forward --list'. +/// +public record AdbPortRule (AdbPortSpec Remote, AdbPortSpec Local); diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs new file mode 100644 index 00000000000..765c6ad29c3 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Xamarin.Android.Tools; + +/// +/// Represents a port and protocol pair for adb forwarding/reverse operations. +/// +public record AdbPortSpec (AdbProtocol Protocol, int Port) +{ + /// + /// Returns the adb socket spec string, e.g. "tcp:5000". + /// + public string ToSocketSpec () => Protocol switch { + AdbProtocol.Tcp => FormattableString.Invariant ($"tcp:{Port}"), + _ => throw new ArgumentOutOfRangeException (nameof (Protocol), Protocol, $"Unsupported ADB protocol: {Protocol}"), + }; + + /// + /// Parses an adb socket spec string like "tcp:5000" into an . + /// Returns null if the format is unrecognized. + /// + public static AdbPortSpec? TryParse (string? socketSpec) + { + if (socketSpec is not { Length: > 0 } value || string.IsNullOrWhiteSpace (value)) + return null; + + var colonIndex = value.IndexOf (':'); + if (colonIndex <= 0 || colonIndex >= value.Length - 1) + return null; + + var protocolStr = value.Substring (0, colonIndex); + var portStr = value.Substring (colonIndex + 1); + + if (!int.TryParse (portStr, out var port) || port <= 0 || port > 65535) + return null; + + var protocol = protocolStr.ToLowerInvariant () switch { + "tcp" => (AdbProtocol?) AdbProtocol.Tcp, + _ => null, + }; + + return protocol.HasValue ? new AdbPortSpec (protocol.Value, port) : null; + } + + public override string ToString () => ToSocketSpec (); +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs new file mode 100644 index 00000000000..8b2c2dc4817 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Protocol types supported by adb port forwarding and reverse port forwarding. +/// +public enum AdbProtocol +{ + /// + /// TCP socket spec, e.g. "tcp:5000". + /// + Tcp, +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs new file mode 100644 index 00000000000..eefb155c24a --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -0,0 +1,671 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +/// +/// Runs Android Debug Bridge (adb) commands. +/// Parsing logic ported from dotnet/android GetAvailableAndroidDevices task. +/// +public class AdbRunner +{ + readonly string adbPath; + readonly IDictionary? environmentVariables; + readonly Action logger; + + // Pattern to match device lines: [key:value ...] + // Uses \s+ to match one or more whitespace characters (spaces or tabs) between fields. + // Explicit state list prevents false positives from non-device lines. + static readonly Regex AdbDevicesRegex = new Regex ( + @"^([^\s]+)\s+(device|offline|unauthorized|authorizing|no permissions|recovery|sideload|bootloader|connecting|host)\s*(.*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex ApiRegex = new Regex (@"\bApi\b", RegexOptions.Compiled); + + /// + /// Creates a new AdbRunner with the full path to the adb executable. + /// + /// Full path to the adb executable (e.g., "/path/to/sdk/platform-tools/adb"). + /// Optional environment variables to pass to adb processes. + /// Optional logger callback receiving a and message string. + public AdbRunner (string adbPath, IDictionary? environmentVariables = null, Action? logger = null) + { + if (string.IsNullOrWhiteSpace (adbPath)) + throw new ArgumentException ("Path to adb must not be empty.", nameof (adbPath)); + this.adbPath = adbPath; + this.environmentVariables = environmentVariables; + this.logger = logger ?? RunnerDefaults.NullLogger; + } + + /// + /// Lists connected devices using 'adb devices -l'. + /// For online emulators, queries the AVD name via getprop / emu avd name. + /// Offline emulators are included but without AVD names (querying them would fail). + /// + public virtual async Task> ListDevicesAsync (CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "devices", "-l"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, "adb devices -l", stderr, stdout); + + var devices = ParseAdbDevicesOutput (stdout.ToString ().Split ('\n')); + + // For each online emulator, try to get the AVD name. + // Skip offline emulators — neither getprop nor 'emu avd name' work on them + // and attempting these commands causes unnecessary delays during boot polling. + foreach (var device in devices) { + if (device.Type == AdbDeviceType.Emulator && device.Status == AdbDeviceStatus.Online) { + device.AvdName = await GetEmulatorAvdNameAsync (device.Serial, cancellationToken).ConfigureAwait (false); + device.Description = BuildDeviceDescription (device); + } else if (device.Type == AdbDeviceType.Emulator) { + logger.Invoke (TraceLevel.Verbose, $"Skipping AVD name query for {device.Status} emulator {device.Serial}"); + } + } + + return devices; + } + + /// + /// Queries the emulator for its AVD name. + /// Tries adb shell getprop ro.boot.qemu.avd_name first (reliable on all modern + /// emulators), then falls back to adb -s <serial> emu avd name (emulator + /// console protocol) for older emulator versions. The emu avd name command returns + /// empty output on emulator 36.4.10+ (observed with adb v36), so getprop is the + /// preferred method. + /// + internal async Task GetEmulatorAvdNameAsync (string serial, CancellationToken cancellationToken = default) + { + // Try 1: Shell property (reliable on modern emulators, always set by the emulator kernel) + try { + var avdName = await GetShellPropertyAsync (serial, "ro.boot.qemu.avd_name", cancellationToken).ConfigureAwait (false); + if (avdName is { Length: > 0 } name && !string.IsNullOrWhiteSpace (name)) + return name.Trim (); + } catch (OperationCanceledException) { + throw; + } catch (Exception ex) { + logger.Invoke (TraceLevel.Warning, $"GetEmulatorAvdNameAsync: getprop failed for {serial}: {ex.Message}"); + } + + // Try 2: Console command (fallback for older emulators where getprop may not be available) + try { + using var stdout = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "emu", "avd", "name"); + await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken, environmentVariables).ConfigureAwait (false); + + foreach (var line in stdout.ToString ().Split ('\n')) { + var trimmed = line.Trim (); + if (!string.IsNullOrEmpty (trimmed) && + !string.Equals (trimmed, "OK", StringComparison.OrdinalIgnoreCase)) { + return trimmed; + } + } + } catch (OperationCanceledException) { + throw; + } catch (Exception ex) { + logger.Invoke (TraceLevel.Warning, $"GetEmulatorAvdNameAsync: both methods failed for {serial}: {ex.Message}"); + } + + return null; + } + + public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var effectiveTimeout = timeout ?? TimeSpan.FromSeconds (60); + + if (effectiveTimeout <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException (nameof (timeout), effectiveTimeout, "Timeout must be a positive value."); + + var args = serial is { Length: > 0 } s + ? new [] { "-s", s, "wait-for-device" } + : new [] { "wait-for-device" }; + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, args); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + cts.CancelAfter (effectiveTimeout); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + + try { + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cts.Token, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, "adb wait-for-device", stderr, stdout); + } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { + throw new TimeoutException ($"Timed out waiting for device after {effectiveTimeout.TotalSeconds}s."); + } + } + + public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "emu", "kill"); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} emu kill", stderr); + } + + /// + /// Gets a system property from a device via 'adb -s <serial> shell getprop <property>'. + /// Returns the property value (first non-empty line of stdout), or null on failure. + /// + public virtual async Task GetShellPropertyAsync (string serial, string propertyName, CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", "getprop", propertyName); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + if (exitCode != 0) { + var stderrText = stderr.ToString ().Trim (); + if (stderrText.Length > 0) + logger.Invoke (TraceLevel.Warning, $"adb shell getprop {propertyName} failed (exit {exitCode}): {stderrText}"); + return null; + } + return FirstNonEmptyLine (stdout.ToString ()); + } + + /// + /// Runs a shell command on a device via 'adb -s <serial> shell <command>'. + /// Returns the full stdout output trimmed, or null on failure. + /// + /// + /// The is passed as a single argument to adb shell, + /// which means the device's shell interprets it (shell expansion, pipes, semicolons are active). + /// Do not pass untrusted or user-supplied input without proper validation. + /// + public virtual async Task RunShellCommandAsync (string serial, string command, CancellationToken cancellationToken) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", command); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + if (exitCode != 0) { + var stderrText = stderr.ToString ().Trim (); + if (stderrText.Length > 0) + logger.Invoke (TraceLevel.Warning, $"adb shell {command} failed (exit {exitCode}): {stderrText}"); + return null; + } + var output = stdout.ToString ().Trim (); + return output.Length > 0 ? output : null; + } + + /// + /// Runs a shell command on a device via adb -s <serial> shell <command> <args>. + /// Returns the full stdout output trimmed, or null on failure. + /// + /// + /// When adb shell receives the command and arguments as separate tokens, it uses + /// exec() directly on the device — bypassing the device's shell interpreter. + /// This avoids shell expansion, pipes, and injection risks, making it safer for dynamic input. + /// + public virtual async Task RunShellCommandAsync (string serial, string command, string[] args, CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + // Build: adb -s shell ... + var allArgs = new string [3 + 1 + args.Length]; + allArgs [0] = "-s"; + allArgs [1] = serial; + allArgs [2] = "shell"; + allArgs [3] = command; + Array.Copy (args, 0, allArgs, 4, args.Length); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, allArgs); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + if (exitCode != 0) { + var stderrText = stderr.ToString ().Trim (); + if (stderrText.Length > 0) + logger.Invoke (TraceLevel.Warning, $"adb shell {command} failed (exit {exitCode}): {stderrText}"); + return null; + } + var output = stdout.ToString ().Trim (); + return output.Length > 0 ? output : null; + } + + internal static string? FirstNonEmptyLine (string output) + { + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (trimmed.Length > 0) + return trimmed; + } + return null; + } + + /// + /// Sets up reverse port forwarding via 'adb -s <serial> reverse <remote> <local>'. + /// + /// Device serial number. + /// Remote (device-side) port spec. + /// Local (host-side) port spec. + /// Cancellation token. + public virtual async Task ReversePortAsync (string serial, AdbPortSpec remote, AdbPortSpec local, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (remote is null) + throw new ArgumentNullException (nameof (remote)); + if (local is null) + throw new ArgumentNullException (nameof (local)); + if (remote.Port <= 0 || remote.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (remote), remote.Port, "Port must be between 1 and 65535."); + if (local.Port <= 0 || local.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (local), local.Port, "Port must be between 1 and 65535."); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remote.ToSocketSpec (), local.ToSocketSpec ()); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remote} {local}", stderr); + } + + /// + /// Removes a specific reverse port forwarding rule via + /// 'adb -s <serial> reverse --remove <remote>'. + /// + /// Device serial number. + /// Remote (device-side) port spec to remove. + /// Cancellation token. + public virtual async Task RemoveReversePortAsync (string serial, AdbPortSpec remote, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (remote is null) + throw new ArgumentNullException (nameof (remote)); + if (remote.Port <= 0 || remote.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (remote), remote.Port, "Port must be between 1 and 65535."); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remote.ToSocketSpec ()); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remote}", stderr); + } + + /// + /// Removes all reverse port forwarding rules via + /// 'adb -s <serial> reverse --remove-all'. + /// + public virtual async Task RemoveAllReversePortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove-all"); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove-all", stderr); + } + + /// + /// Lists all active reverse port forwarding rules via + /// 'adb -s <serial> reverse --list'. + /// + public virtual async Task> ListReversePortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--list"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --list", stderr, stdout); + + return ParseReverseListOutput (stdout.ToString ().Split ('\n')); + } + + /// + /// Parses the output of 'adb reverse --list'. + /// Each line is "(reverse) <remote> <local>", e.g. "(reverse) tcp:5000 tcp:5000". + /// Lines with unparseable socket specs are skipped. + /// + internal static IReadOnlyList ParseReverseListOutput (IEnumerable lines) + { + var rules = new List (); + + foreach (var line in lines) { + var trimmed = line.Trim (); + if (string.IsNullOrEmpty (trimmed)) + continue; + + // Expected format: "(reverse) tcp:5000 tcp:5000" + if (!trimmed.StartsWith ("(reverse)", StringComparison.Ordinal)) + continue; + + var parts = trimmed.Substring ("(reverse)".Length).Trim ().Split ((char[]?) null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) { + var remote = AdbPortSpec.TryParse (parts [0]); + var local = AdbPortSpec.TryParse (parts [1]); + if (remote is { } r && local is { } l) + rules.Add (new AdbPortRule (r, l)); + } + } + + return rules; + } + + /// + /// Sets up forward port forwarding via 'adb -s <serial> forward <local> <remote>'. + /// The host-side <local> socket is forwarded to the device-side <remote> socket, + /// the symmetric pair to . + /// + /// Device serial number. + /// Local (host-side) port spec. + /// Remote (device-side) port spec. + /// Cancellation token. + public virtual async Task ForwardPortAsync (string serial, AdbPortSpec local, AdbPortSpec remote, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (local is null) + throw new ArgumentNullException (nameof (local)); + if (remote is null) + throw new ArgumentNullException (nameof (remote)); + if (local.Port <= 0 || local.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (local), local.Port, "Port must be between 1 and 65535."); + if (remote.Port <= 0 || remote.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (remote), remote.Port, "Port must be between 1 and 65535."); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "forward", local.ToSocketSpec (), remote.ToSocketSpec ()); + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} forward {local} {remote}", stderr, stdout); + } + + /// + /// Removes a specific forward port forwarding rule via + /// 'adb -s <serial> forward --remove <local>'. + /// + /// Device serial number. + /// Local (host-side) port spec to remove. + /// Cancellation token. + public virtual async Task RemoveForwardPortAsync (string serial, AdbPortSpec local, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (local is null) + throw new ArgumentNullException (nameof (local)); + if (local.Port <= 0 || local.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (local), local.Port, "Port must be between 1 and 65535."); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "forward", "--remove", local.ToSocketSpec ()); + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} forward --remove {local}", stderr, stdout); + } + + /// + /// Removes all forward port forwarding rules for the specified device. + /// + /// + /// The underlying adb forward --remove-all command (and its wire-protocol + /// equivalent host-serial:<serial>:killforward-all) operates globally on the + /// adb daemon — the -s <serial> flag does not scope it, and calling it + /// would remove forwards for every connected device. To honour the per-device + /// contract of this method, we list the forwards for + /// via and remove them individually via + /// . + /// + public virtual async Task RemoveAllForwardPortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + var rules = await ListForwardPortsAsync (serial, cancellationToken).ConfigureAwait (false); + foreach (var rule in rules) { + cancellationToken.ThrowIfCancellationRequested (); + await RemoveForwardPortAsync (serial, rule.Local, cancellationToken).ConfigureAwait (false); + } + } + + /// + /// Lists active forward port forwarding rules for the specified device via + /// 'adb forward --list'. + /// The underlying command always lists rules across all devices, so the + /// result is filtered to entries matching . + /// + public virtual async Task> ListForwardPortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "forward", "--list"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb forward --list", stderr, stdout); + + return ParseForwardListOutput (stdout.ToString ().Split ('\n'), serial); + } + + /// + /// Parses the output of 'adb forward --list'. + /// Each line is "<serial> <local> <remote>", e.g. "emulator-5554 tcp:5000 tcp:6000". + /// Only rules matching are returned. Lines with + /// unparseable socket specs are skipped. + /// + /// + /// Note the field-order asymmetry vs : + /// forward --list: <serial> <local> <remote> + /// reverse --list: (reverse) <remote> <local> + /// Both parsers construct an whose constructor takes + /// (Remote, Local), so the order in which we pass the parsed parts differs between + /// the two parsers — keep that in mind when modifying either of them. + /// + internal static IReadOnlyList ParseForwardListOutput (IEnumerable lines, string serial) + { + var rules = new List (); + if (string.IsNullOrEmpty (serial)) + return rules; + + foreach (var line in lines) { + var trimmed = line.Trim (); + if (string.IsNullOrEmpty (trimmed)) + continue; + + // Expected format: " " — see above for + // the field-order asymmetry with reverse --list. + var parts = trimmed.Split ((char[]?) null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 3) + continue; + + if (!string.Equals (parts [0], serial, StringComparison.Ordinal)) + continue; + + var local = AdbPortSpec.TryParse (parts [1]); + var remote = AdbPortSpec.TryParse (parts [2]); + if (local is { } l && remote is { } r) + rules.Add (new AdbPortRule (r, l)); + } + + return rules; + } + + /// + /// Parses the output lines from 'adb devices -l'. + /// Accepts an to avoid allocating a joined string. + /// + public static IReadOnlyList ParseAdbDevicesOutput (IEnumerable lines) + { + var devices = new List (); + + foreach (var line in lines) { + var trimmed = line.Trim (); + if (string.IsNullOrEmpty (trimmed) || + trimmed.IndexOf ("List of devices", StringComparison.OrdinalIgnoreCase) >= 0 || + trimmed.StartsWith ("*", StringComparison.Ordinal)) + continue; + + var match = AdbDevicesRegex.Match (trimmed); + if (!match.Success) + continue; + + var serial = match.Groups [1].Value.Trim (); + var state = match.Groups [2].Value.Trim (); + var properties = match.Groups [3].Value.Trim (); + + // Parse key:value pairs from the properties string + var propDict = new Dictionary (StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty (properties)) { + var pairs = properties.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var pair in pairs) { + var colonIndex = pair.IndexOf (':'); + if (colonIndex > 0 && colonIndex < pair.Length - 1) { + var key = pair.Substring (0, colonIndex); + var value = pair.Substring (colonIndex + 1); + propDict [key] = value; + } + } + } + + var deviceType = serial.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) + ? AdbDeviceType.Emulator + : AdbDeviceType.Device; + + var device = new AdbDeviceInfo { + Serial = serial, + Type = deviceType, + Status = MapAdbStateToStatus (state), + }; + + if (propDict.TryGetValue ("model", out var model)) + device.Model = model; + if (propDict.TryGetValue ("product", out var product)) + device.Product = product; + if (propDict.TryGetValue ("device", out var deviceCodeName)) + device.Device = deviceCodeName; + if (propDict.TryGetValue ("transport_id", out var transportId)) + device.TransportId = transportId; + + // Build description (will be updated later if emulator AVD name is available) + device.Description = BuildDeviceDescription (device); + + devices.Add (device); + } + + return devices; + } + + /// + /// Maps adb device states to status values. + /// Ported from dotnet/android GetAvailableAndroidDevices.MapAdbStateToStatus. + /// + public static AdbDeviceStatus MapAdbStateToStatus (string adbState) => adbState.ToLowerInvariant () switch { + "device" => AdbDeviceStatus.Online, + "offline" => AdbDeviceStatus.Offline, + "unauthorized" => AdbDeviceStatus.Unauthorized, + "no permissions" => AdbDeviceStatus.NoPermissions, + _ => AdbDeviceStatus.Unknown, + }; + + /// + /// Builds a human-friendly description for a device. + /// Priority: AVD name (for emulators) > model > product > device > serial. + /// Ported from dotnet/android GetAvailableAndroidDevices.BuildDeviceDescription. + /// + public static string BuildDeviceDescription (AdbDeviceInfo device, Action? logger = null) + { + logger ??= RunnerDefaults.NullLogger; + if (device.Type == AdbDeviceType.Emulator && device.AvdName is { Length: > 0 } avdName) { + logger.Invoke (TraceLevel.Verbose, $"Emulator {device.Serial}, original AVD name: {avdName}"); + var formatted = FormatDisplayName (avdName); + logger.Invoke (TraceLevel.Verbose, $"Emulator {device.Serial}, formatted AVD display name: {formatted}"); + return formatted; + } + + if (device.Model is { Length: > 0 } model) + return model.Replace ('_', ' '); + + if (device.Product is { Length: > 0 } product) + return product.Replace ('_', ' '); + + if (device.Device is { Length: > 0 } deviceName) + return deviceName.Replace ('_', ' '); + + return device.Serial; + } + + /// + /// Formats an AVD name into a user-friendly display name. + /// Replaces underscores with spaces, applies title case, and capitalizes "API". + /// ToTitleCase naturally preserves fully-uppercase segments (e.g. "XL", "SE"). + /// Ported from dotnet/android GetAvailableAndroidDevices.FormatDisplayName. + /// + public static string FormatDisplayName (string avdName) + { + if (string.IsNullOrEmpty (avdName)) + return avdName ?? string.Empty; + + var textInfo = CultureInfo.InvariantCulture.TextInfo; + avdName = textInfo.ToTitleCase (avdName.Replace ('_', ' ')); + + // Replace "Api" with "API" + avdName = ApiRegex.Replace (avdName, "API"); + return avdName; + } + + /// + /// Merges devices from adb with available emulators from 'emulator -list-avds'. + /// Running emulators are not duplicated. Non-running emulators are added with Status=NotRunning. + /// Ported from dotnet/android GetAvailableAndroidDevices.MergeDevicesAndEmulators. + /// + public static IReadOnlyList MergeDevicesAndEmulators (IReadOnlyList adbDevices, IReadOnlyList availableEmulators, Action? logger = null) + { + logger ??= RunnerDefaults.NullLogger; + var result = new List (adbDevices); + + // Build a set of AVD names that are already running + var runningAvdNames = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (var device in adbDevices) { + if (device.AvdName is { Length: > 0 } avdName) + runningAvdNames.Add (avdName); + } + + logger.Invoke (TraceLevel.Verbose, $"Running emulators AVD names: {string.Join (", ", runningAvdNames)}"); + + // Add non-running emulators + foreach (var avdName in availableEmulators) { + if (runningAvdNames.Contains (avdName)) { + logger.Invoke (TraceLevel.Verbose, $"Emulator '{avdName}' is already running, skipping"); + continue; + } + + var displayName = FormatDisplayName (avdName); + result.Add (new AdbDeviceInfo { + Serial = avdName, + Description = displayName + " (Not Running)", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.NotRunning, + AvdName = avdName, + }); + logger.Invoke (TraceLevel.Verbose, $"Added non-running emulator: {avdName}"); + } + + // Sort: online devices first, then not-running emulators, alphabetically by description + result.Sort ((a, b) => { + var aNotRunning = a.Status == AdbDeviceStatus.NotRunning; + var bNotRunning = b.Status == AdbDeviceStatus.NotRunning; + + if (aNotRunning != bNotRunning) + return aNotRunning ? 1 : -1; + + return string.Compare (a.Description, b.Description, StringComparison.OrdinalIgnoreCase); + }); + + return result; + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs new file mode 100644 index 00000000000..51f6309d2f5 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Xamarin.Android.Tools; + +/// +/// Helper for building environment variables for Android SDK tools. +/// Returns a dictionary that can be passed to . +/// +internal static class AndroidEnvironmentHelper +{ + /// + /// Builds environment variables needed to run Android SDK tools. + /// Pass the result to via the environmentVariables parameter. + /// + internal static Dictionary GetEnvironmentVariables (string? sdkPath, string? jdkPath) + { + var env = new Dictionary (); + + if (sdkPath is { Length: > 0 }) + env [EnvironmentVariableNames.AndroidHome] = sdkPath; + + if (jdkPath is { Length: > 0 }) { + env [EnvironmentVariableNames.JavaHome] = jdkPath; + var jdkBin = Path.Combine (jdkPath, "bin"); + var currentPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.Path) ?? ""; + env [EnvironmentVariableNames.Path] = string.IsNullOrEmpty (currentPath) ? jdkBin : jdkBin + Path.PathSeparator + currentPath; + } + + if (string.IsNullOrEmpty (Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidUserHome))) + env [EnvironmentVariableNames.AndroidUserHome] = Path.Combine ( + Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".android"); + + return env; + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs new file mode 100644 index 00000000000..cb0dcfdaa0d --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +/// +/// Runs Android Virtual Device Manager (avdmanager) commands. +/// +public class AvdManagerRunner +{ + readonly string avdManagerPath; + readonly IDictionary? environmentVariables; + readonly Action logger; + + /// + /// Creates a new AvdManagerRunner with the full path to the avdmanager executable. + /// + /// Full path to avdmanager (e.g., "/path/to/sdk/cmdline-tools/latest/bin/avdmanager"). + /// Optional environment variables to pass to avdmanager processes. + /// Optional logger callback for diagnostic messages. + public AvdManagerRunner (string avdManagerPath, IDictionary? environmentVariables = null, Action? logger = null) + { + if (string.IsNullOrWhiteSpace (avdManagerPath)) + throw new ArgumentException ("Path to avdmanager must not be empty.", nameof (avdManagerPath)); + this.avdManagerPath = avdManagerPath; + this.environmentVariables = environmentVariables; + this.logger = logger ?? RunnerDefaults.NullLogger; + } + + public async Task> ListAvdsAsync (CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "avd"); + logger.Invoke (TraceLevel.Verbose, "Running: avdmanager list avd"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list avd", stderr, stdout); + + return ParseAvdListOutput (stdout.ToString ()); + } + + /// + /// Creates an AVD with the specified name and system image. If is false + /// and an AVD with the same name already exists, returns the existing AVD without re-creating it. + /// + public async Task GetOrCreateAvdAsync (string name, string systemImage, string? deviceProfile = null, + bool force = false, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (name)) + throw new ArgumentException ("Value cannot be null or whitespace.", nameof (name)); + if (string.IsNullOrWhiteSpace (systemImage)) + throw new ArgumentException ("Value cannot be null or whitespace.", nameof (systemImage)); + + // Check if AVD already exists — return it instead of failing + if (!force) { + var existing = (await ListAvdsAsync (cancellationToken).ConfigureAwait (false)) + .FirstOrDefault (a => string.Equals (a.Name, name, StringComparison.OrdinalIgnoreCase)); + if (existing is not null) { + logger.Invoke (TraceLevel.Verbose, $"AVD '{name}' already exists, returning existing"); + return existing; + } + } + + var args = new List { "create", "avd", "-n", name, "-k", systemImage }; + if (deviceProfile is { Length: > 0 }) + args.AddRange (new [] { "-d", deviceProfile }); + if (force) + args.Add ("--force"); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, args.ToArray ()); + psi.RedirectStandardInput = true; + + // avdmanager prompts "Do you wish to create a custom hardware profile?" — answer "no" + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables, + onStarted: p => { + try { + p.StandardInput.WriteLine ("no"); + p.StandardInput.Close (); + } catch (IOException ex) { + logger.Invoke (TraceLevel.Warning, $"Failed to write to avdmanager stdin: {ex.Message}"); + } + }).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager create avd -n {name}", stderr, stdout); + + // Re-list to get the actual path from avdmanager (respects ANDROID_USER_HOME/ANDROID_AVD_HOME) + var avds = await ListAvdsAsync (cancellationToken).ConfigureAwait (false); + var created = avds.FirstOrDefault (a => string.Equals (a.Name, name, StringComparison.OrdinalIgnoreCase)); + if (created is not null) + return created; + + throw new InvalidOperationException ($"avdmanager reported success but AVD '{name}' was not found in the list."); + } + + public async Task DeleteAvdAsync (string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (name)) + throw new ArgumentException ("Value cannot be null or whitespace.", nameof (name)); + + // Idempotent: if the AVD doesn't exist, treat as success + var avds = await ListAvdsAsync (cancellationToken).ConfigureAwait (false); + if (!avds.Any (a => string.Equals (a.Name, name, StringComparison.OrdinalIgnoreCase))) { + logger.Invoke (TraceLevel.Verbose, $"AVD '{name}' does not exist, nothing to delete"); + return; + } + + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "delete", "avd", "--name", name); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr); + } + + /// + /// Lists available device profiles (hardware definitions) using avdmanager list device --compact. + /// + public async Task> ListDeviceProfilesAsync (CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "device", "--compact"); + logger.Invoke (TraceLevel.Verbose, "Running: avdmanager list device --compact"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list device --compact", stderr, stdout); + + return ParseCompactDeviceListOutput (stdout.ToString ()); + } + + internal static IReadOnlyList ParseCompactDeviceListOutput (string output) + { + var profiles = new List (); + + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (trimmed.Length > 0) + profiles.Add (new AvdDeviceProfile (trimmed)); + } + + return profiles; + } + + internal static IReadOnlyList ParseAvdListOutput (string output) + { + var avds = new List (); + string? currentName = null, currentDevice = null, currentPath = null; + + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (trimmed.StartsWith ("Name:", StringComparison.OrdinalIgnoreCase)) { + if (currentName is not null) + avds.Add (new AvdInfo (currentName, currentDevice, currentPath)); + currentName = trimmed.Substring (5).Trim (); + currentDevice = currentPath = null; + } + else if (trimmed.StartsWith ("Device:", StringComparison.OrdinalIgnoreCase)) + currentDevice = trimmed.Substring (7).Trim (); + else if (trimmed.StartsWith ("Path:", StringComparison.OrdinalIgnoreCase)) + currentPath = trimmed.Substring (5).Trim (); + } + + if (currentName is not null) + avds.Add (new AvdInfo (currentName, currentDevice, currentPath)); + + return avds; + } + +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs new file mode 100644 index 00000000000..eff26f578bb --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -0,0 +1,340 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +/// +/// Runs Android Emulator commands. +/// +public class EmulatorRunner +{ + readonly string emulatorPath; + readonly IDictionary? environmentVariables; + readonly Action logger; + + /// + /// Creates a new EmulatorRunner with the full path to the emulator executable. + /// + /// Full path to the emulator executable (e.g., "/path/to/sdk/emulator/emulator"). + /// Optional environment variables to pass to emulator processes. + /// Optional logger callback for diagnostic messages. + public EmulatorRunner (string emulatorPath, IDictionary? environmentVariables = null, Action? logger = null) + { + if (string.IsNullOrWhiteSpace (emulatorPath)) + throw new ArgumentException ("Path to emulator must not be empty.", nameof (emulatorPath)); + this.emulatorPath = emulatorPath; + this.environmentVariables = environmentVariables; + this.logger = logger ?? RunnerDefaults.NullLogger; + } + + /// + /// Launches an emulator process for the specified AVD and returns immediately. + /// The returned represents the running emulator — the caller + /// is responsible for managing its lifetime (e.g., killing it on shutdown). + /// This method does not wait for the emulator to finish booting. + /// To launch and wait until the device is fully booted, use instead. + /// + /// Name of the AVD to launch (as shown by emulator -list-avds). + /// When true, forces a cold boot by passing -no-snapshot-load. + /// Optional extra arguments to pass to the emulator command line. + /// The running the emulator. Stdout/stderr are redirected and forwarded to the logger. + public Process LaunchEmulator (string avdName, bool coldBoot = false, List? additionalArgs = null) + { + if (string.IsNullOrWhiteSpace (avdName)) + throw new ArgumentException ("AVD name must not be empty.", nameof (avdName)); + + var args = new List { "-avd", avdName }; + if (coldBoot) + args.Add ("-no-snapshot-load"); + if (additionalArgs != null) + args.AddRange (additionalArgs); + + ProcessStartInfo psi; + if (OS.IsWindows) { + psi = ProcessUtils.CreateProcessStartInfo (emulatorPath, args.ToArray ()); + } else { + // On Unix, launch through a shell that ignores SIGINT before exec'ing + // the emulator. This prevents Ctrl+C in the parent terminal from killing + // the emulator process. 'trap "" INT' sets SIGINT to SIG_IGN, which POSIX + // guarantees is preserved across exec: + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html + var shellCmd = new StringBuilder ("trap '' INT; exec "); + shellCmd.Append (ShellQuote (emulatorPath)); + foreach (var arg in args) { + shellCmd.Append (' '); + shellCmd.Append (ShellQuote (arg)); + } + psi = ProcessUtils.CreateProcessStartInfo ("/bin/sh", "-c", shellCmd.ToString ()); + } + + if (environmentVariables != null) { + foreach (var kvp in environmentVariables) + psi.EnvironmentVariables[kvp.Key] = kvp.Value; + } + + // Redirect stdout/stderr so the emulator process doesn't inherit the + // caller's pipes. Without this, parent processes (e.g. VS Code spawn) + // never see the 'close' event because the emulator holds the pipes open. + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + + logger.Invoke (TraceLevel.Verbose, $"Launching emulator AVD '{avdName}'"); + + var process = new Process { StartInfo = psi }; + + // Forward emulator output to the logger so crash messages (e.g. "HAX is + // not working", "image not found") are captured instead of silently lost. + process.OutputDataReceived += (_, e) => { + if (e.Data != null) + logger.Invoke (TraceLevel.Verbose, $"[emulator] {e.Data}"); + }; + process.ErrorDataReceived += (_, e) => { + if (e.Data != null) + logger.Invoke (TraceLevel.Warning, $"[emulator] {e.Data}"); + }; + + if (!process.Start ()) { + process.Dispose (); + throw new InvalidOperationException ($"Failed to start emulator process '{emulatorPath}'."); + } + + // Drain redirected streams asynchronously to prevent pipe buffer deadlocks + process.BeginOutputReadLine (); + process.BeginErrorReadLine (); + + return process; + } + + public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (emulatorPath, "-list-avds"); + + logger.Invoke (TraceLevel.Verbose, "Running: emulator -list-avds"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, "emulator -list-avds", stderr, stdout); + + return ParseListAvdsOutput (stdout.ToString ()); + } + + internal static List ParseListAvdsOutput (string output) + { + var avds = new List (); + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (!string.IsNullOrEmpty (trimmed)) + avds.Add (trimmed); + } + return avds; + } + + /// + /// Boots an emulator for the specified AVD and waits until it is fully ready to accept commands. + /// + /// Unlike , which only spawns the emulator process, this method + /// handles the full lifecycle: it checks whether the device is already online, launches + /// the emulator if needed, then polls sys.boot_completed and pm path android + /// until the Android OS is fully booted and the package manager is responsive. + /// + /// Ported from the dotnet/android BootAndroidEmulator MSBuild task. + /// + /// + /// Either an ADB device serial (e.g., emulator-5554) to wait for, + /// or an AVD name (e.g., Pixel_7_API_35) to launch and boot. + /// + /// An used to query device status and boot properties. + /// Optional boot configuration (timeout, poll interval, cold boot, extra args). + /// Cancellation token to abort the operation. + /// + /// An indicating success or failure, including the device serial on success + /// or an error message on timeout/failure. + /// + public async Task BootEmulatorAsync ( + string deviceOrAvdName, + AdbRunner adbRunner, + EmulatorBootOptions? options = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (deviceOrAvdName)) + throw new ArgumentException ("Device or AVD name must not be empty.", nameof (deviceOrAvdName)); + if (adbRunner == null) + throw new ArgumentNullException (nameof (adbRunner)); + + options ??= new EmulatorBootOptions (); + if (options.BootTimeout <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException (nameof (options), "BootTimeout must be positive."); + if (options.PollInterval <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException (nameof (options), "PollInterval must be positive."); + + logger.Invoke (TraceLevel.Info, $"Booting emulator for '{deviceOrAvdName}'..."); + + // Phase 1: Check if deviceOrAvdName is already an online ADB device by serial + var devices = await adbRunner.ListDevicesAsync (cancellationToken).ConfigureAwait (false); + var onlineDevice = devices.FirstOrDefault (d => + d.Status == AdbDeviceStatus.Online && + string.Equals (d.Serial, deviceOrAvdName, StringComparison.OrdinalIgnoreCase)); + + if (onlineDevice != null) { + logger.Invoke (TraceLevel.Info, $"Device '{deviceOrAvdName}' is already online."); + return new EmulatorBootResult { Success = true, Serial = onlineDevice.Serial, ErrorKind = EmulatorBootErrorKind.None }; + } + + // Single timeout CTS for the entire boot operation (covers Phase 2 and Phase 3). + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + timeoutCts.CancelAfter (options.BootTimeout); + + // Phase 2: Check if AVD is already running (possibly still booting) + var runningSerial = FindRunningAvdSerial (devices, deviceOrAvdName); + if (runningSerial != null) { + logger.Invoke (TraceLevel.Info, $"AVD '{deviceOrAvdName}' is already running as '{runningSerial}', waiting for full boot..."); + try { + return await WaitForFullBootAsync (adbRunner, runningSerial, options, timeoutCts.Token).ConfigureAwait (false); + } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { + return new EmulatorBootResult { + Success = false, + ErrorKind = EmulatorBootErrorKind.Timeout, + ErrorMessage = $"Timed out waiting for emulator '{deviceOrAvdName}' to boot within {options.BootTimeout.TotalSeconds}s.", + }; + } + } + + // Phase 3: Launch the emulator + logger.Invoke (TraceLevel.Info, $"Launching AVD '{deviceOrAvdName}'..."); + Process emulatorProcess; + try { + emulatorProcess = LaunchEmulator (deviceOrAvdName, options.ColdBoot, options.AdditionalArgs); + } catch (Exception ex) { + return new EmulatorBootResult { + Success = false, + ErrorKind = EmulatorBootErrorKind.LaunchFailed, + ErrorMessage = $"Failed to launch emulator: {ex.Message}", + }; + } + + // Poll for the new emulator serial to appear. + // If the boot times out or is cancelled, terminate the process we spawned + // to avoid leaving orphan emulator processes. + // + // On macOS, the emulator binary may fork the real QEMU process and exit with + // code 0 immediately. The real emulator continues as a separate process and + // will eventually appear in 'adb devices'. We only treat non-zero exit codes + // as immediate failures; exit code 0 means we continue polling. + // + // Dispose the Process handle when done — the emulator process keeps running. + using (emulatorProcess) { + try { + string? newSerial = null; + bool processExitedWithZero = false; + while (newSerial == null) { + timeoutCts.Token.ThrowIfCancellationRequested (); + + // Detect early process exit for fast failure. + // Guard against InvalidOperationException in case no OS process + // is associated with the object (e.g. broken emulator binary). + try { + if (emulatorProcess.HasExited && !processExitedWithZero) { + if (emulatorProcess.ExitCode != 0) { + return new EmulatorBootResult { + Success = false, + ErrorKind = EmulatorBootErrorKind.LaunchFailed, + ErrorMessage = $"Emulator process for '{deviceOrAvdName}' exited with code {emulatorProcess.ExitCode} before becoming available.", + }; + } + // Exit code 0: emulator likely forked (common on macOS). + // The real emulator runs as a separate process — keep polling. + logger.Invoke (TraceLevel.Verbose, $"Emulator launcher process exited with code 0 (likely forked). Continuing to poll adb devices."); + processExitedWithZero = true; + } + } catch (InvalidOperationException ex) { + return new EmulatorBootResult { + Success = false, + ErrorKind = EmulatorBootErrorKind.LaunchFailed, + ErrorMessage = $"Emulator process for '{deviceOrAvdName}' is no longer available: {ex.Message}", + }; + } + + await Task.Delay (options.PollInterval, timeoutCts.Token).ConfigureAwait (false); + + devices = await adbRunner.ListDevicesAsync (timeoutCts.Token).ConfigureAwait (false); + newSerial = FindRunningAvdSerial (devices, deviceOrAvdName); + } + + logger.Invoke (TraceLevel.Info, $"Emulator appeared as '{newSerial}', waiting for full boot..."); + return await WaitForFullBootAsync (adbRunner, newSerial, options, timeoutCts.Token).ConfigureAwait (false); + } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { + TryKillProcess (emulatorProcess); + return new EmulatorBootResult { + Success = false, + ErrorKind = EmulatorBootErrorKind.Timeout, + ErrorMessage = $"Timed out waiting for emulator '{deviceOrAvdName}' to boot within {options.BootTimeout.TotalSeconds}s.", + }; + } catch { + TryKillProcess (emulatorProcess); + throw; + } + } + } + + static string? FindRunningAvdSerial (IReadOnlyList devices, string avdName) + { + foreach (var d in devices) { + if (d.Type == AdbDeviceType.Emulator && + !string.IsNullOrEmpty (d.AvdName) && + string.Equals (d.AvdName, avdName, StringComparison.OrdinalIgnoreCase)) { + return d.Serial; + } + } + return null; + } + + void TryKillProcess (Process process) + { + try { + process.Kill (); + } catch (Exception ex) { + // Best-effort: process may have already exited + logger.Invoke (TraceLevel.Verbose, $"Failed to stop emulator process: {ex.Message}"); + } + } + + async Task WaitForFullBootAsync ( + AdbRunner adbRunner, + string serial, + EmulatorBootOptions options, + CancellationToken cancellationToken) + { + // The caller is responsible for enforcing the overall boot timeout via + // cancellationToken (a linked CTS with CancelAfter). This method simply + // polls until boot completes or the token is cancelled. + while (!cancellationToken.IsCancellationRequested) { + var bootCompleted = await adbRunner.GetShellPropertyAsync (serial, "sys.boot_completed", cancellationToken).ConfigureAwait (false); + if (string.Equals (bootCompleted, "1", StringComparison.Ordinal)) { + var pmResult = await adbRunner.RunShellCommandAsync (serial, "pm path android", cancellationToken).ConfigureAwait (false); + if (pmResult != null && pmResult.StartsWith ("package:", StringComparison.Ordinal)) { + logger.Invoke (TraceLevel.Info, $"Emulator '{serial}' is fully booted."); + return new EmulatorBootResult { Success = true, Serial = serial, ErrorKind = EmulatorBootErrorKind.None }; + } + } + + await Task.Delay (options.PollInterval, cancellationToken).ConfigureAwait (false); + } + + cancellationToken.ThrowIfCancellationRequested (); + return new EmulatorBootResult { Success = false, ErrorKind = EmulatorBootErrorKind.Cancelled, ErrorMessage = "Boot cancelled." }; + } + + /// Quotes a string for safe use in a POSIX shell command. + /// Wraps in single quotes and escapes embedded single quotes. + static string ShellQuote (string arg) => "'" + arg.Replace ("'", "'\\''") + "'"; +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/RunnerDefaults.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/RunnerDefaults.cs new file mode 100644 index 00000000000..e71b2df1b09 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Runners/RunnerDefaults.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Xamarin.Android.Tools; + +/// +/// Shared defaults for Android SDK tool runners. +/// +static class RunnerDefaults +{ + /// + /// A no-op logger that discards all messages. Used as the default + /// when callers don't provide a logger callback. + /// + internal static readonly Action NullLogger = static (_, _) => { }; +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Bootstrap.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Bootstrap.cs new file mode 100644 index 00000000000..39286a9ac19 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Bootstrap.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +public partial class SdkManager +{ + static readonly IProgress NullProgress = new Progress (); + + /// + /// Downloads command-line tools from the manifest feed and extracts them to + /// <targetPath>/cmdline-tools/<version>/. + /// + public async Task BootstrapAsync (string targetPath, IProgress? progress = null, CancellationToken cancellationToken = default) + { + ThrowIfDisposed (); + if (string.IsNullOrEmpty (targetPath)) + throw new ArgumentNullException (nameof (targetPath)); + + progress ??= NullProgress; + + var cmdlineTools = await FindLatestCmdlineToolsAsync (progress, cancellationToken).ConfigureAwait (false); + var tempArchivePath = Path.GetTempFileName (); + + try { + await DownloadAndVerifyAsync (cmdlineTools, tempArchivePath, progress, cancellationToken).ConfigureAwait (false); + + var versionDir = Path.Combine (targetPath, "cmdline-tools", cmdlineTools.Revision); + Directory.CreateDirectory (Path.Combine (targetPath, "cmdline-tools")); + + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.Extracting, Message: "Extracting cmdline-tools...")); + FileUtil.ExtractAndInstall (tempArchivePath, versionDir, "cmdline-tools", logger, cancellationToken); + + if (!OS.IsWindows) + await FileUtil.SetExecutablePermissionsAsync (versionDir, logger, cancellationToken).ConfigureAwait (false); + + AndroidSdkPath = targetPath; + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.Complete, 100, "Bootstrap complete.")); + logger (TraceLevel.Info, "Android SDK bootstrap complete."); + } + finally { + FileUtil.TryDeleteFile (tempArchivePath, logger); + } + } + + async Task FindLatestCmdlineToolsAsync (IProgress progress, CancellationToken cancellationToken) + { + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.ReadingManifest, Message: "Reading manifest feed...")); + logger (TraceLevel.Info, $"Reading manifest from {ManifestFeedUrl}..."); + + var components = await GetManifestComponentsAsync (cancellationToken).ConfigureAwait (false); + var cmdlineTools = components + .Where (c => string.Equals (c.ElementName, "cmdline-tools", StringComparison.OrdinalIgnoreCase) && !c.IsObsolete) + .OrderByDescending (c => Version.TryParse (c.Revision, out var v) ? v : new Version (0, 0)) + .FirstOrDefault (); + + if (cmdlineTools is null || string.IsNullOrEmpty (cmdlineTools.DownloadUrl)) + throw new InvalidOperationException ("Could not find command-line tools in the Android manifest feed."); + + logger (TraceLevel.Info, $"Found cmdline-tools {cmdlineTools.Revision}: {cmdlineTools.DownloadUrl}"); + return cmdlineTools; + } + + async Task DownloadAndVerifyAsync (SdkManifestComponent component, string archivePath, IProgress progress, CancellationToken cancellationToken) + { + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.Downloading, Message: $"Downloading cmdline-tools {component.Revision}...")); + await DownloadFileAsync (component.DownloadUrl!, archivePath, component.Size, progress, cancellationToken).ConfigureAwait (false); + + if (!string.IsNullOrEmpty (component.Checksum)) { + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.Verifying, Message: "Verifying checksum...")); + DownloadUtils.VerifyChecksum (archivePath, component.Checksum!, component.ChecksumType); + logger (TraceLevel.Info, "Checksum verification passed."); + } + else { + logger (TraceLevel.Warning, "No checksum available; skipping verification."); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Licenses.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Licenses.cs new file mode 100644 index 00000000000..3262065e245 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Licenses.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +public partial class SdkManager +{ + /// + /// Accepts all SDK licenses using sdkmanager --licenses. + /// + public Task AcceptLicensesAsync () => AcceptLicensesAsync (CancellationToken.None); + + /// + /// Accepts all SDK licenses using sdkmanager --licenses. + /// + /// Cancellation token. + public async Task AcceptLicensesAsync (CancellationToken cancellationToken) + { + var sdkManagerPath = RequireSdkManagerPath (); + logger (TraceLevel.Info, "Accepting SDK licenses..."); + var (exitCode, stdout, stderr) = await RunSdkManagerAsync ( + sdkManagerPath, new[] { "--licenses" }, acceptLicenses: true, cancellationToken: cancellationToken).ConfigureAwait (false); + + logger (TraceLevel.Verbose, $"sdkmanager --licenses exited with code {exitCode}."); + + if (exitCode != 0) { + string output = (stdout ?? string.Empty) + Environment.NewLine + (stderr ?? string.Empty); + + bool alreadyAccepted = + output.IndexOf ("all sdk package licenses accepted", StringComparison.OrdinalIgnoreCase) >= 0 || + output.IndexOf ("licenses have already been accepted", StringComparison.OrdinalIgnoreCase) >= 0 || + output.IndexOf ("no sdk licenses to review", StringComparison.OrdinalIgnoreCase) >= 0 || + output.IndexOf ("no licenses to review", StringComparison.OrdinalIgnoreCase) >= 0; + + if (!alreadyAccepted) { + logger (TraceLevel.Error, $"sdkmanager --licenses failed with exit code {exitCode}. stderr: {stderr}"); + throw new InvalidOperationException ($"sdkmanager --licenses failed with exit code {exitCode}. stderr: {stderr}"); + } + + logger (TraceLevel.Info, "SDK licenses are already accepted."); + return; + } + + logger (TraceLevel.Info, "License acceptance complete."); + } + + /// + /// Gets pending licenses that need to be accepted, along with their full text. + /// This allows IDEs and CLI tools to present licenses to the user before accepting. + /// + /// Cancellation token. + /// A list of pending licenses with their ID and full text content. + public async Task> GetPendingLicensesAsync (CancellationToken cancellationToken = default) + { + var sdkManagerPath = RequireSdkManagerPath (); + + logger (TraceLevel.Verbose, "Checking for pending licenses..."); + + var envVars = AndroidEnvironmentHelper.GetEnvironmentVariables (AndroidSdkPath, JavaSdkPath); + + // Run --licenses without auto-accept to get the license text + var psi = ProcessUtils.CreateProcessStartInfo (sdkManagerPath, "--licenses"); + psi.RedirectStandardInput = true; + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + + // Send 'n' to decline all licenses so we just get the text + Action onStarted = process => { + Task.Run (async () => { + try { + while (!process.HasExited && !cancellationToken.IsCancellationRequested) { + process.StandardInput.WriteLine ("n"); + await Task.Delay (StdinPollDelayMs, cancellationToken).ConfigureAwait (false); + } + } + catch (Exception ex) { + // Process may have exited - expected behavior when process completes + logger (TraceLevel.Verbose, $"License check loop ended: {ex.GetType ().Name}"); + } + }, cancellationToken); + }; + + try { + await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, envVars, onStarted).ConfigureAwait (false); + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) { + // sdkmanager may exit with non-zero when declining licenses - that's expected + logger (TraceLevel.Verbose, $"License check exited non-zero (expected): {ex.GetType ().Name}"); + } + + return ParseLicenseOutput (stdout.ToString ()); + } + + /// + /// Accepts specific licenses by ID. + /// + /// The license IDs to accept (e.g., "android-sdk-license"). + public Task AcceptLicensesAsync (IEnumerable licenseIds) => AcceptLicensesAsync (licenseIds, CancellationToken.None); + + /// + /// Accepts specific licenses by ID. + /// + /// The license IDs to accept (e.g., "android-sdk-license"). + /// Cancellation token. + public async Task AcceptLicensesAsync (IEnumerable licenseIds, CancellationToken cancellationToken) + { + ThrowIfDisposed (); + if (licenseIds is null || !licenseIds.Any ()) + return; + + if (string.IsNullOrEmpty (AndroidSdkPath)) + throw new InvalidOperationException ("AndroidSdkPath must be set before accepting individual licenses."); + + // Accept licenses by writing the hash to the licenses directory + var licensesDir = Path.Combine (AndroidSdkPath!, "licenses"); + Directory.CreateDirectory (licensesDir); + + // Get pending licenses to find their hashes + var pendingLicenses = await GetPendingLicensesAsync (cancellationToken).ConfigureAwait (false); + var licenseIdSet = new HashSet (licenseIds, StringComparer.OrdinalIgnoreCase); + + foreach (var license in pendingLicenses) { + if (licenseIdSet.Contains (license.Id)) { + var licensePath = Path.Combine (licensesDir, license.Id); + // Compute hash of license text and write it + var hash = ComputeLicenseHash (license.Text); + File.WriteAllText (licensePath, $"\n{hash}"); + logger (TraceLevel.Info, $"Accepted license: {license.Id}"); + } + } + } + + /// + /// Checks whether SDK licenses have been accepted by checking the licenses directory. + /// + /// true if at least one license file exists; otherwise false. + public bool AreLicensesAccepted () + { + if (string.IsNullOrEmpty (AndroidSdkPath)) + return false; + + var licensesPath = Path.Combine (AndroidSdkPath, "licenses"); + if (!Directory.Exists (licensesPath)) + return false; + + return Directory.EnumerateFiles (licensesPath).Any (); + } + + /// + /// Parses the output of sdkmanager --licenses to extract license information. + /// + internal static IReadOnlyList ParseLicenseOutput (string output) + { + var licenses = new List (); + var lines = output.Split (new[] { '\n' }, StringSplitOptions.None); + + string? currentLicenseId = null; + var currentLicenseText = new StringBuilder (); + bool inLicenseText = false; + + foreach (var rawLine in lines) { + var line = rawLine.TrimEnd ('\r'); + + // License header: "License android-sdk-license:" + if (line.StartsWith ("License ", StringComparison.OrdinalIgnoreCase) && line.TrimEnd ().EndsWith (":")) { + // Save previous license if any + if (currentLicenseId is not null && currentLicenseText.Length > 0) { + licenses.Add (new SdkLicense (currentLicenseId, currentLicenseText.ToString ().Trim ())); + } + + var trimmedLine = line.TrimEnd (); + currentLicenseId = trimmedLine.Substring (8, trimmedLine.Length - 9).Trim (); + currentLicenseText.Clear (); + inLicenseText = true; + continue; + } + + // End of license text when we see the accept prompt + if (line.Contains ("Accept?") || line.Contains ("(y/N)")) { + if (currentLicenseId is not null && currentLicenseText.Length > 0) { + licenses.Add (new SdkLicense (currentLicenseId, currentLicenseText.ToString ().Trim ())); + } + currentLicenseId = null; + currentLicenseText.Clear (); + inLicenseText = false; + continue; + } + + // Accumulate license text + if (inLicenseText && currentLicenseId is not null) { + // Skip separator lines + if (!line.TrimStart ().StartsWith ("-------", StringComparison.Ordinal)) { + currentLicenseText.AppendLine (line); + } + } + } + + // Add last license if not yet added + if (currentLicenseId is not null && currentLicenseText.Length > 0) { + licenses.Add (new SdkLicense (currentLicenseId, currentLicenseText.ToString ().Trim ())); + } + + return licenses.AsReadOnly (); + } + + static string ComputeLicenseHash (string licenseText) + { + var bytes = Encoding.UTF8.GetBytes (licenseText.Replace ("\r\n", "\n").Trim ()); + return DownloadUtils.ComputeHashString (ChecksumType.Sha1, bytes); + } + +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Manifest.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Manifest.cs new file mode 100644 index 00000000000..fd96cf8bc64 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Manifest.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace Xamarin.Android.Tools; + +public partial class SdkManager +{ + // --- Manifest Parsing --- + + /// + /// Downloads and parses the Android manifest feed to discover available components. + /// + /// Cancellation token. + /// A list of manifest components available for the current platform. + internal async Task> GetManifestComponentsAsync (CancellationToken cancellationToken = default) + { + ThrowIfDisposed (); + logger (TraceLevel.Info, $"Downloading manifest from {ManifestFeedUrl}..."); + // netstandard2.0 GetStringAsync has no CancellationToken overload; use GetAsync instead + using var response = await httpClient.GetAsync (ManifestFeedUrl, cancellationToken).ConfigureAwait (false); + response.EnsureSuccessStatusCode (); + var xml = await DownloadUtils.ReadAsStringAsync (response.Content, cancellationToken).ConfigureAwait (false); + return ParseManifest (xml); + } + + /// + /// Parses the Android manifest XML and returns components for the current platform. + /// Uses XmlReader for better performance than XDocument/XElement. + /// + internal IReadOnlyList ParseManifest (string xml) + { + var hostOs = GetManifestHostOs (); + var hostArch = GetManifestHostArch (); + var components = new List (); + + using var stringReader = new StringReader (xml); + using var reader = XmlReader.Create (stringReader, new XmlReaderSettings { IgnoreWhitespace = true }); + + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element) + continue; + + // Skip root element + if (reader.Depth == 0) + continue; + + var elementName = reader.LocalName; + var revision = reader.GetAttribute ("revision"); + if (string.IsNullOrEmpty (revision)) + continue; + + var component = new SdkManifestComponent { + ElementName = elementName, + Revision = revision!, + Path = reader.GetAttribute ("path"), + FilesystemPath = reader.GetAttribute ("filesystem-path"), + Description = reader.GetAttribute ("description"), + IsObsolete = string.Equals (reader.GetAttribute ("obsolete"), "True", StringComparison.OrdinalIgnoreCase), + }; + + // Read child elements to find matching URL + if (!reader.IsEmptyElement) { + var componentDepth = reader.Depth; + while (reader.Read () && reader.Depth > componentDepth) { + if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "urls") { + var urlsDepth = reader.Depth; + while (reader.Read () && reader.Depth > urlsDepth) { + if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "url") { + var urlHostOs = reader.GetAttribute ("host-os"); + var urlHostArch = reader.GetAttribute ("host-arch"); + + if (!MatchesPlatform (urlHostOs, hostOs)) + continue; + + if (!string.IsNullOrEmpty (urlHostArch) && !string.Equals (urlHostArch, hostArch, StringComparison.OrdinalIgnoreCase)) + continue; + + var checksumTypeStr = reader.GetAttribute ("checksum-type"); + if (string.Equals (checksumTypeStr, "sha-256", StringComparison.OrdinalIgnoreCase)) + component.ChecksumType = ChecksumType.Sha256; + else + component.ChecksumType = ChecksumType.Sha1; + component.Checksum = reader.GetAttribute ("checksum"); + + var sizeStr = reader.GetAttribute ("size"); + if (long.TryParse (sizeStr, out var size)) + component.Size = size; + + // Read the URL text content + component.DownloadUrl = reader.ReadElementContentAsString ()?.Trim (); + break; + } + } + } + } + } + + if (!string.IsNullOrEmpty (component.DownloadUrl)) + components.Add (component); + } + + logger (TraceLevel.Verbose, $"Parsed {components.Count} components from manifest."); + return components.AsReadOnly (); + } + + static bool MatchesPlatform (string? urlHostOs, string hostOs) + { + if (string.IsNullOrEmpty (urlHostOs)) + return true; // No filter means any platform + return string.Equals (urlHostOs, hostOs, StringComparison.OrdinalIgnoreCase); + } + + static string GetManifestHostOs () + { + if (OS.IsWindows) return "windows"; + if (OS.IsMac) return "macosx"; + if (OS.IsLinux) return "linux"; + throw new PlatformNotSupportedException ($"Unsupported operating system for Android SDK manifest."); + } + + static string GetManifestHostArch () + { + var arch = RuntimeInformation.OSArchitecture; + switch (arch) { + case Architecture.Arm64: + return "aarch64"; + case Architecture.X64: + return "x64"; + case Architecture.X86: + return "x86"; + default: + throw new PlatformNotSupportedException ($"Unsupported architecture '{arch}' for Android SDK manifest."); + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Packages.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Packages.cs new file mode 100644 index 00000000000..0a548d985f1 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Packages.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; + +public partial class SdkManager +{ + public string? FindSdkManagerPath () + { + if (string.IsNullOrEmpty (AndroidSdkPath)) + return null; + + var ext = OS.IsWindows ? ".bat" : string.Empty; + var cmdlineToolsDir = Path.Combine (AndroidSdkPath, "cmdline-tools"); + + if (Directory.Exists (cmdlineToolsDir)) { + try { + // Versioned dirs sorted descending, then "latest" as fallback + var searchDirs = Directory.GetDirectories (cmdlineToolsDir) + .Select (Path.GetFileName) + .Where (n => n != "latest" && !string.IsNullOrEmpty (n)) + .OrderByDescending (n => Version.TryParse (n, out var v) ? v : new Version (0, 0)) + .Append ("latest"); + + foreach (var dir in searchDirs) { + var toolPath = Path.Combine (cmdlineToolsDir, dir!, "bin", "sdkmanager" + ext); + if (File.Exists (toolPath)) + return toolPath; + } + } catch (Exception ex) { + logger (TraceLevel.Verbose, $"Error enumerating cmdline-tools directories: {ex.Message}"); + } + } + + // Legacy fallback: tools/bin/sdkmanager + var legacyPath = Path.Combine (AndroidSdkPath, "tools", "bin", "sdkmanager" + ext); + return File.Exists (legacyPath) ? legacyPath : null; + } + + public async Task<(IReadOnlyList Installed, IReadOnlyList Available)> ListAsync (CancellationToken cancellationToken = default) + { + var sdkManagerPath = RequireSdkManagerPath (); + logger (TraceLevel.Info, "Running sdkmanager --list..."); + var (exitCode, stdout, stderr) = await RunSdkManagerAsync (sdkManagerPath, new[] { "--list" }, cancellationToken: cancellationToken).ConfigureAwait (false); + ThrowOnSdkManagerFailure (exitCode, "sdkmanager --list", stderr); + return ParseSdkManagerList (stdout); + } + + public async Task InstallAsync (IEnumerable packages, bool acceptLicenses = true, CancellationToken cancellationToken = default) + { + var packageArray = ValidatePackages (packages); + var sdkManagerPath = RequireSdkManagerPath (); + logger (TraceLevel.Info, $"Installing packages: {string.Join (", ", packageArray)}"); + + // Install one at a time to work around sdkmanager shell script quoting issues + foreach (var package in packageArray) { + logger (TraceLevel.Info, $"Installing package: {package}"); + var (exitCode, _, stderr) = await RunSdkManagerAsync ( + sdkManagerPath, new[] { package }, acceptLicenses, cancellationToken).ConfigureAwait (false); + ThrowOnSdkManagerFailure (exitCode, $"Package installation ({package})", stderr); + } + logger (TraceLevel.Info, "Packages installed successfully."); + } + + public async Task UninstallAsync (IEnumerable packages, CancellationToken cancellationToken = default) + { + var packageArray = ValidatePackages (packages); + var sdkManagerPath = RequireSdkManagerPath (); + logger (TraceLevel.Info, $"Uninstalling packages: {string.Join (", ", packageArray)}"); + + var args = new[] { "--uninstall" }.Concat (packageArray).ToArray (); + var (exitCode, _, stderr) = await RunSdkManagerAsync ( + sdkManagerPath, args, cancellationToken: cancellationToken).ConfigureAwait (false); + ThrowOnSdkManagerFailure (exitCode, "Package uninstall", stderr); + logger (TraceLevel.Info, "Packages uninstalled successfully."); + } + + public async Task UpdateAsync (CancellationToken cancellationToken = default) + { + var sdkManagerPath = RequireSdkManagerPath (); + logger (TraceLevel.Info, "Updating all installed packages..."); + var (exitCode, _, stderr) = await RunSdkManagerAsync ( + sdkManagerPath, new[] { "--update" }, acceptLicenses: true, cancellationToken: cancellationToken).ConfigureAwait (false); + ThrowOnSdkManagerFailure (exitCode, "Package update", stderr); + logger (TraceLevel.Info, "All packages updated successfully."); + } + + static string[] ValidatePackages (IEnumerable packages) + { + if (packages is null) + throw new ArgumentException ("At least one package must be specified.", nameof (packages)); + + var array = packages.ToArray (); + if (array.Length == 0) + throw new ArgumentException ("At least one package must be specified.", nameof (packages)); + + return array; + } + + internal static (IReadOnlyList Installed, IReadOnlyList Available) ParseSdkManagerList (string output) + { + var installed = new List (); + var available = new List (); + List? target = null; + + foreach (var line in output.Split (new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) { + var trimmed = line.Trim (); + + if (trimmed.Contains ("Installed packages:")) { target = installed; continue; } + if (trimmed.Contains ("Available Packages:")) { target = available; continue; } + if (trimmed.Contains ("Available Updates:")) { target = null; continue; } + + if (target is null || trimmed.StartsWith ("Path", StringComparison.Ordinal) || trimmed.StartsWith ("---", StringComparison.Ordinal)) + continue; + + var parts = trimmed.Split ('|'); + if (parts.Length < 2) + continue; + + var path = parts[0].Trim (); + if (string.IsNullOrEmpty (path)) + continue; + + target.Add (new SdkPackage ( + path, + Version: parts[1].Trim (), + Description: parts.Length > 2 ? parts[2].Trim () : null, + IsInstalled: target == installed + )); + } + + return (installed.AsReadOnly (), available.AsReadOnly ()); + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Process.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Process.cs new file mode 100644 index 00000000000..c526d27b0c4 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.Process.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools; +public partial class SdkManager +{ + async Task<(int ExitCode, string Stdout, string Stderr)> RunSdkManagerAsync ( + string sdkManagerPath, string[] arguments, bool acceptLicenses = false, CancellationToken cancellationToken = default) + { + var argumentsStr = string.Join (" ", arguments); + + // On macOS/Linux the sdkmanager shell script uses 'save()'/'eval' which + // concatenates individually-quoted arguments. Pass as a single Arguments + // string so the script receives them correctly. + var psi = OS.IsWindows + ? ProcessUtils.CreateProcessStartInfo (sdkManagerPath, arguments) + : new ProcessStartInfo { + FileName = sdkManagerPath, + Arguments = argumentsStr, + UseShellExecute = false, + CreateNoWindow = true, + }; + psi.RedirectStandardInput = acceptLicenses; + + var envVars = AndroidEnvironmentHelper.GetEnvironmentVariables (AndroidSdkPath, JavaSdkPath); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + + Action? onStarted = null; + if (acceptLicenses) { + onStarted = process => { + // Feed "y\n" continuously for license prompts + Task.Run (async () => { + try { + while (!process.HasExited && !cancellationToken.IsCancellationRequested) { + process.StandardInput.WriteLine ("y"); + await Task.Delay (StdinPollDelayMs, cancellationToken).ConfigureAwait (false); + } + } + catch (Exception ex) { + // Process may have exited or cancellation requested - expected behavior + logger (TraceLevel.Verbose, $"Auto-accept loop ended: {ex.GetType ().Name}"); + } + }, cancellationToken); + }; + } + + logger (TraceLevel.Verbose, $"Running: {sdkManagerPath} {argumentsStr}"); + int exitCode; + try { + exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, envVars, onStarted).ConfigureAwait (false); + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) { + logger (TraceLevel.Error, $"Failed to run sdkmanager: {ex.Message}"); + logger (TraceLevel.Verbose, ex.ToString ()); + throw; + } + + var stdoutStr = stdout.ToString (); + var stderrStr = stderr.ToString (); + + if (exitCode != 0) { + logger (TraceLevel.Warning, $"sdkmanager exited with code {exitCode}"); + logger (TraceLevel.Verbose, $"stdout: {stdoutStr}"); + logger (TraceLevel.Verbose, $"stderr: {stderrStr}"); + } + + return (exitCode, stdoutStr, stderrStr); + } + + async Task DownloadFileAsync (string url, string destinationPath, long expectedSize, IProgress progress, CancellationToken cancellationToken) + { + logger (TraceLevel.Info, $"Downloading {url}..."); + + var downloadProgress = new Progress<(double percent, string message)> (p => { + progress.Report (new SdkBootstrapProgress (SdkBootstrapPhase.Downloading, (int) p.percent, p.message)); + }); + + await DownloadUtils.DownloadFileAsync (httpClient, url, destinationPath, expectedSize, downloadProgress, cancellationToken).ConfigureAwait (false); + logger (TraceLevel.Info, $"Download complete: {destinationPath}"); + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.cs new file mode 100644 index 00000000000..cf8e3a345cb --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/SdkManager.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Net.Http; + +namespace Xamarin.Android.Tools; +/// +/// Provides Android SDK bootstrap and management capabilities using the sdkmanager CLI. +/// +/// +/// +/// Downloads the Android command-line tools from the Xamarin Android manifest feed, +/// extracts them to cmdline-tools/<version>/, then uses the included sdkmanager +/// to install, uninstall, list, and update SDK packages. +/// +/// +/// The manifest feed URL defaults to https://aka.ms/AndroidManifestFeed/d18-0 +/// but can be configured via the property. +/// +/// +public partial class SdkManager : IDisposable +{ + /// Default manifest feed URL (Xamarin/Microsoft). + public const string DefaultManifestFeedUrl = "https://aka.ms/AndroidManifestFeed/d18-0"; + + const int StdinPollDelayMs = 500; + + static readonly HttpClient httpClient = new HttpClient (); + readonly Action logger; + bool disposed; + + /// + /// Gets or sets the manifest feed URL used to discover command-line tools. + /// Defaults to . + /// + public string ManifestFeedUrl { get; set; } = DefaultManifestFeedUrl; + + /// + /// Gets or sets the Android SDK root path. Used to locate and invoke sdkmanager. + /// + public string? AndroidSdkPath { get; set; } + + /// + /// Gets or sets the Java SDK (JDK) home path. Set as JAVA_HOME when invoking sdkmanager. + /// + public string? JavaSdkPath { get; set; } + + /// + /// Creates a new instance. + /// + /// Optional logger callback. Defaults to . + public SdkManager (Action? logger = null) + { + this.logger = logger ?? AndroidSdkInfo.DefaultConsoleLogger; + } + + /// + /// Disposes the . + /// + public void Dispose () + { + if (disposed) + return; + disposed = true; + } + + void ThrowIfDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (SdkManager)); + } + + string RequireSdkManagerPath () + { + ThrowIfDisposed (); + return FindSdkManagerPath () + ?? throw new InvalidOperationException ("sdkmanager not found. Run BootstrapAsync first."); + } + + void ThrowOnSdkManagerFailure (int exitCode, string operation, string stderr) + { + if (exitCode == 0) + return; + logger (TraceLevel.Error, $"{operation} failed (exit code {exitCode}): {stderr}"); + throw new InvalidOperationException ($"{operation} failed: {stderr}"); + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs new file mode 100644 index 00000000000..f05668f6e87 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs @@ -0,0 +1,329 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.IO; +using System.Collections.Generic; + +namespace Xamarin.Android.Tools +{ + abstract class AndroidSdkBase + { + // When this changes, update the test: Xamarin.Android.Tools.Tests.AndroidSdkInfoTests.Ndk_MultipleNdkVersionsInSdk + const int MinimumCompatibleNDKMajorVersion = 16; + const int MaximumCompatibleNDKMajorVersion = 28; + + static readonly char[] SourcePropertiesKeyValueSplit = new char[] { '=' }; + + // Per https://developer.android.com/studio/command-line/variables#envar + #pragma warning disable CS0618 // ANDROID_SDK_ROOT is obsolete but still needed for compat + protected static readonly string[] AndroidSdkEnvVars = {EnvironmentVariableNames.AndroidHome, EnvironmentVariableNames.AndroidSdkRoot}; + #pragma warning restore CS0618 + + string[]? allAndroidSdks; + + public string[] AllAndroidSdks { + get { + if (allAndroidSdks == null) { + var dirs = new List (); + dirs.Add (AndroidSdkPath); + dirs.AddRange (GetAllAvailableAndroidSdks ()); + allAndroidSdks = dirs.Where (d => ValidateAndroidSdkLocation ("AllAndroidSdks", d)) + .Select (d => d!) + .Distinct () + .ToArray (); + } + return allAndroidSdks; + } + } + + public readonly Action Logger; + + public AndroidSdkBase (Action logger) + { + Logger = logger; + } + + public string? AndroidSdkPath { get; private set; } + public string? AndroidNdkPath { get; private set; } + public string? JavaSdkPath { get; private set; } + public string? JavaBinPath { get; private set; } + public string? AndroidPlatformToolsPath { get; private set; } + public string? AndroidPlatformToolsPathShort { get; private set; } + + public virtual string Adb { get; protected set; } = "adb"; + public virtual string ZipAlign { get; protected set; } = "zipalign"; + public virtual string JarSigner { get; protected set; } = "jarsigner"; + public virtual string KeyTool { get; protected set; } = "keytool"; + + public virtual string NdkStack { get; protected set; } = "ndk-stack"; + public abstract string NdkHostPlatform32Bit { get; } + public abstract string NdkHostPlatform64Bit { get; } + public virtual string Javac { get; protected set; } = "javac"; + + public abstract string? PreferedAndroidSdkPath { get; } + public abstract string? PreferedAndroidNdkPath { get; } + public abstract string? PreferedJavaSdkPath { get; } + + public virtual void Initialize (string? androidSdkPath = null, string? androidNdkPath = null, string? javaSdkPath = null) + { + AndroidSdkPath = GetValidPath (ValidateAndroidSdkLocation, androidSdkPath, () => PreferedAndroidSdkPath, () => GetAllAvailableAndroidSdks ()); + JavaSdkPath = GetValidPath (ValidateJavaSdkLocation, javaSdkPath, () => PreferedJavaSdkPath, () => GetJavaSdkPaths ()); + + AndroidNdkPath = GetValidNdkPath (androidNdkPath); + + if (!string.IsNullOrEmpty (JavaSdkPath)) { + JavaBinPath = Path.Combine (JavaSdkPath, "bin"); + } else { + JavaBinPath = null; + } + + if (!string.IsNullOrEmpty (AndroidSdkPath)) { + AndroidPlatformToolsPath = Path.Combine (AndroidSdkPath, "platform-tools"); + AndroidPlatformToolsPathShort = GetShortFormPath (AndroidPlatformToolsPath); + } else { + AndroidPlatformToolsPath = null; + AndroidPlatformToolsPathShort = null; + } + + if (!string.IsNullOrEmpty (AndroidNdkPath)) { + // It would be nice if .NET had real globbing support in System.IO... + string toolchainsDir = Path.Combine (AndroidNdkPath, "toolchains"); + string llvmNdkToolchainDir = Path.Combine (toolchainsDir, "llvm", "prebuilt", NdkHostPlatform64Bit); + + if (Directory.Exists (llvmNdkToolchainDir)) { + IsNdk64Bit = true; + } else if (Directory.Exists (toolchainsDir)) { + IsNdk64Bit = Directory.EnumerateDirectories (toolchainsDir, "arm-linux-androideabi-*") + .Any (dir => Directory.Exists (Path.Combine (dir, "prebuilt", NdkHostPlatform64Bit))); + } + } + // we need to look for extensions other than the default .exe|.bat + // google have a habbit of changing them. + Adb = GetExecutablePath (AndroidPlatformToolsPath, Adb); + NdkStack = GetExecutablePath (AndroidNdkPath, NdkStack); + } + + static string? GetValidPath (Func pathValidator, string? ctorParam, Func getPreferredPath, Func> getAllPaths) + { + if (pathValidator ("constructor param", ctorParam)) + return ctorParam; + ctorParam = getPreferredPath (); + if (pathValidator ("preferred path", ctorParam)) + return ctorParam; + foreach (var path in getAllPaths ()) { + if (pathValidator ("all paths", path)) { + return path; + } + } + return null; + } + + string? GetValidNdkPath (string? ctorParam) + { + if (ValidateAndroidNdkLocation ("constructor param", ctorParam)) + return ctorParam; + if (AndroidSdkPath != null) { + string bundle = FindBestNDK (AndroidSdkPath); + if (Directory.Exists (bundle) && ValidateAndroidNdkLocation ("within Android SDK", bundle)) + return bundle; + } + ctorParam = PreferedAndroidNdkPath; + if (ValidateAndroidNdkLocation ("preferred path", ctorParam)) + return ctorParam; + foreach (var path in GetAllAvailableAndroidNdks ()) { + if (ValidateAndroidNdkLocation ("all paths", path)) + return path; + } + return null; + } + + protected abstract IEnumerable GetAllAvailableAndroidSdks (); + protected abstract string GetShortFormPath (string path); + + protected IEnumerable GetSdkFromEnvironmentVariables () + { + foreach (string envVar in AndroidSdkEnvVars) { + var ev = Environment.GetEnvironmentVariable (envVar); + if (String.IsNullOrEmpty (ev)) { + continue; + } + + yield return ev; + } + } + + protected virtual IEnumerable GetAllAvailableAndroidNdks () + { + // Look in PATH + foreach (var ndkStack in ProcessUtils.FindExecutablesInPath (NdkStack)) { + var ndkDir = Path.GetDirectoryName (ndkStack); + if (string.IsNullOrEmpty (ndkDir)) + continue; + yield return ndkDir; + } + + // Check for the "ndk-bundle" directory inside other SDK directories + foreach (var sdk in GetAllAvailableAndroidSdks ()) { + if (sdk == AndroidSdkPath) + continue; + var ndkDir = FindBestNDK (sdk); + if (string.IsNullOrEmpty (ndkDir)) + continue; + yield return ndkDir; + } + } + + string FindBestNDK (string androidSdkPath) + { + if (!Directory.Exists (androidSdkPath)) { + return String.Empty; + } + + var ndkInstances = new SortedDictionary (Comparer.Create ((Version l, Version r) => r.CompareTo (l))); + + foreach (string ndkPath in Directory.EnumerateDirectories (androidSdkPath, "ndk*", SearchOption.TopDirectoryOnly)) { + if (String.Compare ("ndk-bundle", Path.GetFileName (ndkPath), StringComparison.OrdinalIgnoreCase) == 0) { + LoadNDKVersion (ndkPath); + continue; + } + + if (String.Compare ("ndk", Path.GetFileName (ndkPath), StringComparison.OrdinalIgnoreCase) != 0) { + continue; + } + + foreach (string versionedNdkPath in Directory.EnumerateDirectories (ndkPath, "*", SearchOption.TopDirectoryOnly)) { + LoadNDKVersion (versionedNdkPath); + } + } + + if (ndkInstances.Count == 0) { + return String.Empty; + } + + var kvp = ndkInstances.First (); + Logger (TraceLevel.Verbose, $"Best NDK selected: v{kvp.Key} in {kvp.Value}"); + return kvp.Value; + + void LoadNDKVersion (string path) + { + string propsFilePath = Path.Combine (path, "source.properties"); + if (!File.Exists (propsFilePath)) { + Logger (TraceLevel.Verbose, $"Skipping NDK in '{path}': no source.properties, cannot determine version"); + return; + } + + foreach (string line in File.ReadLines (propsFilePath)) { + string[] parts = line.Split (SourcePropertiesKeyValueSplit, 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) { + continue; + } + + if (String.Compare ("Pkg.Revision", parts[0].Trim (), StringComparison.Ordinal) != 0) { + continue; + } + + if (!Version.TryParse (parts[1].Trim (), out Version? ndkVer) || ndkVer == null || ndkInstances.ContainsKey (ndkVer)) { + continue; + } + + if (ndkVer.Major < MinimumCompatibleNDKMajorVersion || ndkVer.Major > MaximumCompatibleNDKMajorVersion) { + Logger (TraceLevel.Verbose, $"Skipping NDK in '{path}': version {ndkVer} is out of the accepted range (major version must be between {MinimumCompatibleNDKMajorVersion} and {MaximumCompatibleNDKMajorVersion}"); + continue; + } + + ndkInstances.Add (ndkVer, path); + return; + } + } + } + + public abstract void SetPreferredAndroidSdkPath (string? path); + public abstract void SetPreferredJavaSdkPath (string? path); + public abstract void SetPreferredAndroidNdkPath (string? path); + + public bool IsNdk64Bit { get; private set; } + + public string NdkHostPlatform { + get { return IsNdk64Bit ? NdkHostPlatform64Bit : NdkHostPlatform32Bit; } + } + + IEnumerable GetJavaSdkPaths () + { + return JdkInfo.GetKnownSystemJdkInfos (Logger) + .Select (jdk => jdk.HomePath); + } + + /// + /// Checks that a value is the location of an Android SDK. + /// + public bool ValidateAndroidSdkLocation (string locator, [NotNullWhen (true)] string? loc) + { + bool result = !string.IsNullOrEmpty (loc); + if (result) { + bool foundAdb = false; + foreach (var p in ProcessUtils.FindExecutablesInDirectory (Path.Combine (loc!, "platform-tools"), Adb)) { + Logger (TraceLevel.Verbose, $"{nameof (ValidateAndroidSdkLocation)}: for locator={locator}, path=`{loc}`, found adb `{p}`"); + foundAdb = true; + } + result = foundAdb; + } + Logger (TraceLevel.Verbose, $"{nameof (ValidateAndroidSdkLocation)}: for locator={locator}, path=`{loc}`, result={result}"); + return result; + } + + /// + /// Checks that a value is the location of a Java SDK. + /// + public virtual bool ValidateJavaSdkLocation (string locator, [NotNullWhen (true)] string? loc) + { + bool result = !string.IsNullOrEmpty (loc); + if (result) { + bool foundSigner = false; + foreach (var p in ProcessUtils.FindExecutablesInDirectory (Path.Combine (loc!, "bin"), JarSigner)) { + Logger (TraceLevel.Verbose, $"{nameof (ValidateJavaSdkLocation)}: for locator={locator}, path=`{loc}`, found jarsigner `{p}`"); + foundSigner = true; + } + result = foundSigner; + } + Logger (TraceLevel.Verbose, $"{nameof (ValidateJavaSdkLocation)}: locator={locator}, path=`{loc}`, result={result}"); + return result; + } + + /// + /// Checks that a value is the location of an Android SDK. + /// + public bool ValidateAndroidNdkLocation (string locator, [NotNullWhen (true)] string? loc) + { + bool result = !string.IsNullOrEmpty (loc) && + ProcessUtils.FindExecutablesInDirectory (loc!, NdkStack).Any (); + Logger (TraceLevel.Verbose, $"{nameof (ValidateAndroidNdkLocation)}: for locator={locator}, path=`{loc}`, result={result}"); + return result; + } + + internal static string? NullIfEmpty (string? s) + { + if (s == null || s.Length != 0) + return s; + + return null; + } + + static string GetExecutablePath (string? dir, string exe) + { + if (string.IsNullOrEmpty (dir)) + return exe; + + foreach (var e in ProcessUtils.ExecutableFiles (exe)) { + try { + if (File.Exists (Path.Combine (dir, e))) + return e; + } catch (ArgumentException) { + continue; + } + } + return exe; + } + } +} + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs new file mode 100644 index 00000000000..8b315f88a0c --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs @@ -0,0 +1,303 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +using Xamarin.Android.Tools.AndroidSdk.Properties; + +namespace Xamarin.Android.Tools +{ + class AndroidSdkUnix : AndroidSdkBase + { + // See comments above UnixConfigPath for explanation on why these are needed + static readonly string? sudo_user; + static readonly string? user; + static readonly bool need_chown; + + static AndroidSdkUnix () + { + sudo_user = Environment.GetEnvironmentVariable ("SUDO_USER"); + if (String.IsNullOrEmpty (sudo_user)) + return; + + user = Environment.GetEnvironmentVariable ("USER"); + if (String.IsNullOrEmpty (user) || String.Compare (user, sudo_user, StringComparison.Ordinal) == 0) + return; + need_chown = true; + } + + public AndroidSdkUnix (Action logger) + : base (logger) + { + } + + public override string NdkHostPlatform32Bit { + get { return OS.IsMac ? "darwin-x86" : "linux-x86"; } + } + public override string NdkHostPlatform64Bit { + get { return OS.IsMac ? "darwin-x86_64" : "linux-x86_64"; } + } + + public override string? PreferedAndroidSdkPath { + get { + var config_file = GetUnixConfigFile (Logger); + var androidEl = config_file.Element ("android-sdk"); + + if (androidEl != null) { + var path = (string?)androidEl.Attribute ("path"); + + if (ValidateAndroidSdkLocation ("preferred path", path)) + return path; + } + return null; + } + } + + public override string? PreferedAndroidNdkPath { + get { + var config_file = GetUnixConfigFile (Logger); + var androidEl = config_file.Element ("android-ndk"); + + if (androidEl != null) { + var path = (string?)androidEl.Attribute ("path"); + + if (ValidateAndroidNdkLocation ("preferred path", path)) + return path; + } + return null; + } + } + + public override string? PreferedJavaSdkPath { + get { + var config_file = GetUnixConfigFile (Logger); + var javaEl = config_file.Element ("java-sdk"); + + if (javaEl != null) { + var path = (string?)javaEl.Attribute ("path"); + + if (ValidateJavaSdkLocation ("preferred path", path)) + return path; + } + return null; + } + } + + protected override IEnumerable GetAllAvailableAndroidSdks () + { + var preferedSdkPath = PreferedAndroidSdkPath; + if (!string.IsNullOrEmpty (preferedSdkPath)) + yield return preferedSdkPath!; + + foreach (string dir in GetSdkFromEnvironmentVariables ()) { + yield return dir; + } + + // Look in PATH + foreach (var adb in ProcessUtils.FindExecutablesInPath (Adb)) { + var path = Path.GetDirectoryName (adb); + // Strip off "platform-tools" + var dir = Path.GetDirectoryName (path); + + if (dir == null) + continue; + + yield return dir; + } + + // Check some hardcoded paths for good measure + // macOS: ~/Library/Android/sdk + var macSdkPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Android", "sdk"); + if (Directory.Exists (macSdkPath)) + yield return macSdkPath; + // Linux: ~/Android/Sdk + var linuxSdkPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Android", "Sdk"); + if (Directory.Exists (linuxSdkPath)) + yield return linuxSdkPath; + } + + protected override string GetShortFormPath (string path) + { + // This is a Windows-ism, don't do anything for Unix + return path; + } + + public override void SetPreferredAndroidSdkPath (string? path) + { + path = NullIfEmpty (path); + + var doc = GetUnixConfigFile (Logger); + + var androidEl = doc.Element ("android-sdk"); + + if (androidEl == null) { + androidEl = new XElement ("android-sdk"); + doc.Add (androidEl); + } + + androidEl.SetAttributeValue ("path", path); + SaveConfig (doc); + } + + public override void SetPreferredJavaSdkPath (string? path) + { + path = NullIfEmpty (path); + + var doc = GetUnixConfigFile (Logger); + + var javaEl = doc.Element ("java-sdk"); + + if (javaEl == null) { + javaEl = new XElement ("java-sdk"); + doc.Add (javaEl); + } + + javaEl.SetAttributeValue ("path", path); + SaveConfig (doc); + } + + public override void SetPreferredAndroidNdkPath (string? path) + { + path = NullIfEmpty (path); + + var doc = GetUnixConfigFile (Logger); + + var androidEl = doc.Element ("android-ndk"); + + if (androidEl == null) { + androidEl = new XElement ("android-ndk"); + doc.Add (androidEl); + } + + androidEl.SetAttributeValue ("path", path); + SaveConfig (doc); + } + + void SaveConfig (XElement doc) + { + string cfg = UnixConfigPath; + List ? created = null; + + if (!File.Exists (cfg)) { + string? dir = Path.GetDirectoryName (cfg); + if (dir != null && !Directory.Exists (dir)) { + Directory.CreateDirectory (dir); + AddToList (dir); + } + AddToList (cfg); + } + doc.Save (cfg); + FixOwnership (created); + + void AddToList (string path) + { + if (created == null) + created = new List (); + created.Add (path); + } + } + + static readonly string GetUnixConfigDirOverrideName = $"UnixConfigPath directory override! {typeof (AndroidSdkInfo).AssemblyQualifiedName}"; + + // There's a small problem with the code below. Namely, if it runs under `sudo` the folder location + // returned by Environment.GetFolderPath will depend on how sudo was invoked: + // + // 1. `sudo command` will not reset the environment and while the user running the command will be + // `root` (or any other user specified in the command), the `$HOME` environment variable will point + // to the original user's home. The effect will be that any files/directories created in this + // session will be owned by `root` (or any other user as above) and not the original user. Thus, on + // return, the original user will not have write (or read/write) access to the directory/file + // created. This causes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/597752 + // + // 2. `sudo -i command` starts an "interactive" session which resets the environment (by reading shell + // startup scripts among other steps) and the above problem doesn't occur. + // + // The behavior of 1. is, arguably, a bug in Mono fixing of which may bring unknown side effects, + // however. Therefore we'll do our best below to work around the issue. `sudo` puts the original user's + // login name in the `SUDO_USER` environment variable and we can use its presence to both detect 1. + // above and work around the issue. We will do it in the simplest possible manner, by invoking chown(1) + // to set the proper ownership. + // Note that it will NOT fix situations when a mechanism other than `sudo`, but with similar effects, is + // used! The generic fix would require a number of more complicated checks as well as a number of + // p/invokes (with quite a bit of data marshaling) and it is likely that it would be mostly wasted + // effort, as the sudo situation appears to be the most common (while happening few and far between in + // general) + // + private static string UnixConfigPath { + get { + var p = AppDomain.CurrentDomain.GetData (GetUnixConfigDirOverrideName)?.ToString (); + if (string.IsNullOrEmpty (p)) { + p = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".config", "xbuild"); + } + return Path.Combine (p, "monodroid-config.xml"); + } + } + + internal static XElement GetUnixConfigFile (Action logger) + { + var file = UnixConfigPath; + XDocument? doc = null; + if (File.Exists (file)) { + try { + doc = XDocument.Load (file); + } catch (Exception ex) { + logger (TraceLevel.Error, string.Format (Resources.InvalidMonodroidConfigFile_path_message, file, ex.Message)); + logger (TraceLevel.Verbose, ex.ToString ()); + + // move out of the way and create a new one + doc = new XDocument (new XElement ("monodroid")); + var newFileName = file + ".old"; + if (File.Exists (newFileName)) { + File.Delete (newFileName); + } + + File.Move (file, newFileName); + } + } + + if (doc == null || doc.Root == null) { + doc = new XDocument (new XElement ("monodroid")); + } + return doc.Root!; + } + + void FixOwnership (List? paths) + { + if (!need_chown || paths == null || paths.Count == 0) + return; + + var stdout = new StringWriter (); + var stderr = new StringWriter (); + var args = new List { + QuoteString (sudo_user!) + }; + + foreach (string p in paths) + args.Add (QuoteString (p)); + + var psi = new ProcessStartInfo (OS.IsMac ? "/usr/sbin/chown" : "/bin/chown") { + CreateNoWindow = true, + Arguments = String.Join (" ", args), + }; + Logger (TraceLevel.Verbose, $"Changing filesystem object ownership: {psi.FileName} {psi.Arguments}"); + Task chown_task = ProcessUtils.StartProcess (psi, stdout, stderr, System.Threading.CancellationToken.None); + + if (chown_task.Result != 0) { + Logger (TraceLevel.Warning, $"Failed to change ownership of filesystem object(s)"); + Logger (TraceLevel.Verbose, $"standard output: {stdout}"); + Logger (TraceLevel.Verbose, $"standard error: {stderr}"); + } + + string QuoteString (string p) + { + return $"\"{p}\""; + } + } + + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs new file mode 100644 index 00000000000..ee6b8ab750f --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +using Xamarin.Android.Tools.AndroidSdk.Properties; + +namespace Xamarin.Android.Tools +{ + using static RegistryEx; + + class AndroidSdkWindows : AndroidSdkBase + { + const string MDREG_KEY = @"SOFTWARE\Novell\Mono for Android"; + const string MDREG_ANDROID_SDK = "AndroidSdkDirectory"; + const string MDREG_ANDROID_NDK = "AndroidNdkDirectory"; + internal const string MDREG_JAVA_SDK = "JavaSdkDirectory"; + const string ANDROID_INSTALLER_PATH = @"SOFTWARE\Android SDK Tools"; + const string ANDROID_INSTALLER_KEY = "Path"; + const string XAMARIN_ANDROID_INSTALLER_PATH = @"SOFTWARE\Xamarin\MonoAndroid"; + const string XAMARIN_ANDROID_INSTALLER_KEY = "PrivateAndroidSdkPath"; + const string MICROSOFT_OPENJDK_PATH = @"SOFTWARE\Microsoft\JDK"; + + public AndroidSdkWindows (Action logger) + : base (logger) + { + } + + static readonly string _JarSigner = "jarsigner.exe"; + + public override string ZipAlign { get; protected set; } = "zipalign.exe"; + public override string JarSigner { get; protected set; } = _JarSigner; + public override string KeyTool { get; protected set; } = "keytool.exe"; + + public override string NdkHostPlatform32Bit { get { return "windows"; } } + public override string NdkHostPlatform64Bit { get { return "windows-x86_64"; } } + public override string Javac { get; protected set; } = "javac.exe"; + + public override string? PreferedAndroidSdkPath { + get { + var wow = RegistryEx.Wow64.Key32; + var regKey = GetMDRegistryKey (); + if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_SDK, wow, "platform-tools", Adb)) + return RegistryEx.GetValueString (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_SDK, wow); + return null; + } + } + public override string? PreferedAndroidNdkPath { + get { + var wow = RegistryEx.Wow64.Key32; + var regKey = GetMDRegistryKey (); + if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_NDK, wow, ".", NdkStack)) + return RegistryEx.GetValueString (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_NDK, wow); + return null; + } + } + public override string? PreferedJavaSdkPath { + get { + var wow = RegistryEx.Wow64.Key32; + var regKey = GetMDRegistryKey (); + if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, regKey, MDREG_JAVA_SDK, wow, "bin", JarSigner)) + return RegistryEx.GetValueString (RegistryEx.CurrentUser, regKey, MDREG_JAVA_SDK, wow); + return null; + } + } + + internal static string GetMDRegistryKey () + { + var regKey = Environment.GetEnvironmentVariable ("XAMARIN_ANDROID_REGKEY"); + return string.IsNullOrWhiteSpace (regKey) ? MDREG_KEY : regKey; + } + + protected override IEnumerable GetAllAvailableAndroidSdks () + { + var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; + var wow = RegistryEx.Wow64.Key32; + var regKey = GetMDRegistryKey (); + + Logger (TraceLevel.Info, "Looking for Android SDK..."); + + // Check for the key the user gave us in the VS/addin options + foreach (var root in roots) + if (CheckRegistryKeyForExecutable (root, regKey, MDREG_ANDROID_SDK, wow, "platform-tools", Adb)) + yield return RegistryEx.GetValueString (root, regKey, MDREG_ANDROID_SDK, wow) ?? ""; + + // Check for the key written by the Xamarin installer + if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, XAMARIN_ANDROID_INSTALLER_PATH, XAMARIN_ANDROID_INSTALLER_KEY, wow, "platform-tools", Adb)) + yield return RegistryEx.GetValueString (RegistryEx.CurrentUser, XAMARIN_ANDROID_INSTALLER_PATH, XAMARIN_ANDROID_INSTALLER_KEY, wow) ?? ""; + + // Check for the key written by the Android SDK installer + foreach (var root in roots) + if (CheckRegistryKeyForExecutable (root, ANDROID_INSTALLER_PATH, ANDROID_INSTALLER_KEY, wow, "platform-tools", Adb)) + yield return RegistryEx.GetValueString (root, ANDROID_INSTALLER_PATH, ANDROID_INSTALLER_KEY, wow) ?? ""; + + foreach (string dir in GetSdkFromEnvironmentVariables ()) + yield return dir; + + // Check some hardcoded paths for good measure + var paths = new string [] { + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Xamarin", "MonoAndroid", "android-sdk-windows"), + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), "Android", "android-sdk"), + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), "Android", "android-sdk-windows"), + !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("ProgramW6432")) + ? Path.Combine (Environment.GetEnvironmentVariable ("ProgramW6432") ?? "", "Android", "android-sdk") + : Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFiles), "Android", "android-sdk"), + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android", "android-sdk"), + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData), "Android", "android-sdk"), + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android", "Sdk"), + }; + foreach (var basePath in paths) + if (Directory.Exists (basePath)) + yield return basePath; + } + + protected override IEnumerable GetAllAvailableAndroidNdks () + { + + var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; + var wow = RegistryEx.Wow64.Key32; + var regKey = GetMDRegistryKey (); + + Logger (TraceLevel.Info, "Looking for Android NDK..."); + + // Check for the key the user gave us in the VS/addin options + foreach (var root in roots) + if (CheckRegistryKeyForExecutable (root, regKey, MDREG_ANDROID_NDK, wow, ".", NdkStack)) + yield return RegistryEx.GetValueString (root, regKey, MDREG_ANDROID_NDK, wow) ?? ""; + + foreach (string dir in GetSdkFromEnvironmentVariables ()) { + yield return dir; + } + + /* + // Check for the key written by the Xamarin installer + if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, XAMARIN_ANDROID_INSTALLER_PATH, XAMARIN_ANDROID_INSTALLER_KEY, wow, "platform-tools", Adb)) + yield return RegistryEx.GetValueString (RegistryEx.CurrentUser, XAMARIN_ANDROID_INSTALLER_PATH, XAMARIN_ANDROID_INSTALLER_KEY, wow); + */ + + // Check some hardcoded paths for good measure + var xamarin_private = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Xamarin", "MonoAndroid"); + var vs_default = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData), "Microsoft", "AndroidNDK"); + var vs_default32bit = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData), "Microsoft", "AndroidNDK32"); + var vs_2017_default = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData), "Microsoft", "AndroidNDK64"); + var android_default = Path.Combine (OS.ProgramFilesX86 ?? "", "Android"); + var cdrive_default = @"C:\"; + + foreach (var basePath in new string [] {xamarin_private, android_default, vs_default, vs_default32bit, vs_2017_default, cdrive_default}) + if (Directory.Exists (basePath)) + foreach (var dir in Directory.GetDirectories (basePath, "android-ndk-r*")) + if (ValidateAndroidNdkLocation ("Windows known NDK path", dir)) + yield return dir; + + foreach (var dir in base.GetAllAvailableAndroidNdks ()) { + yield return dir; + } + } + + protected override string GetShortFormPath (string path) + { + return KernelEx.GetShortPathName (path); + } + + public override void SetPreferredAndroidSdkPath (string? path) + { + var regKey = GetMDRegistryKey (); + RegistryEx.SetValueString (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_SDK, path ?? "", RegistryEx.Wow64.Key32); + } + + public override void SetPreferredJavaSdkPath (string? path) + { + var regKey = GetMDRegistryKey (); + RegistryEx.SetValueString (RegistryEx.CurrentUser, regKey, MDREG_JAVA_SDK, path ?? "", RegistryEx.Wow64.Key32); + } + + public override void SetPreferredAndroidNdkPath (string? path) + { + var regKey = GetMDRegistryKey (); + RegistryEx.SetValueString (RegistryEx.CurrentUser, regKey, MDREG_ANDROID_NDK, path ?? "", RegistryEx.Wow64.Key32); + } + + public override void Initialize (string? androidSdkPath = null, string? androidNdkPath = null, string? javaSdkPath = null) + { + base.Initialize (androidSdkPath, androidNdkPath, javaSdkPath); + + var jdkPath = JavaSdkPath; + if (!string.IsNullOrEmpty (jdkPath)) { + var cur = Environment.GetEnvironmentVariable (EnvironmentVariableNames.JavaHome); + if (!string.IsNullOrEmpty (cur)) + Environment.SetEnvironmentVariable (EnvironmentVariableNames.JavaHome, jdkPath); + + var javaBinPath = this.JavaBinPath; + if (!string.IsNullOrEmpty (javaBinPath)) { + var environmentPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.Path) ?? ""; + if (!environmentPath.Contains (javaBinPath)) { + var processPath = string.Concat (javaBinPath, Path.PathSeparator, environmentPath); + Environment.SetEnvironmentVariable (EnvironmentVariableNames.Path, processPath); + } + } + } + } + } +} diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj new file mode 100644 index 00000000000..47f817434a0 --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj @@ -0,0 +1,75 @@ + + + + netstandard2.0 + $(TargetFrameworks);$(DotNetTargetFramework) + latest + enable + INTERNAL_NULLABLE_ATTRIBUTES + true + ..\..\product.snk + Xamarin.Android.Tools + Xamarin + MIT + https://github.com/dotnet/android-tools + Xamarin tools for interacting with the Android SDK. + Copyright © Xamarin 2011-2016 + Xamarin;Xamarin.Android + $(VendorPrefix)Xamarin.Android.Tools.AndroidSdk$(VendorSuffix) + + + + $(ToolOutputFullPath) + + + + true + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + Microsoft400 + + + Microsoft400 + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.userprefs b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.userprefs new file mode 100644 index 00000000000..37a28df6a1a --- /dev/null +++ b/external/xamarin-android-tools/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.userprefs @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidRidAbiHelperTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidRidAbiHelperTests.cs new file mode 100644 index 00000000000..f2db03049ee --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidRidAbiHelperTests.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AndroidRidAbiHelperTests + { + static object [] StringValueSource = new object [] { + new[] { + /* input */ default (string), + /* expected */ default (string) + }, + new[] { + /* input */ "", + /* expected */ default + }, + new[] { + /* input */ "armeabi-v7a/libfoo.so", + /* expected */ "armeabi-v7a" + }, + new[] { + /* input */ "arm64-v8a/libfoo.so", + /* expected */ "arm64-v8a" + }, + new[] { + /* input */ "x86/libfoo.so", + /* expected */ "x86" + }, + new[] { + /* input */ "x86_64/libfoo.so", + /* expected */ "x86_64" + }, + new[] { + /* input */ "android-arm/libfoo.so", + /* expected */ "armeabi-v7a" + }, + new[] { + /* input */ "android-arm64/libfoo.so", + /* expected */ "arm64-v8a" + }, + new[] { + /* input */ "android-x86/libfoo.so", + /* expected */ "x86" + }, + new[] { + /* input */ "android-x64/libfoo.so", + /* expected */ "x86_64" + }, + new[] { + /* input */ "android-arm/native/libfoo.so", + /* expected */ "armeabi-v7a" + }, + new[] { + /* input */ "android-arm64/native/libfoo.so", + /* expected */ "arm64-v8a" + }, + new[] { + /* input */ "android-x86/native/libfoo.so", + /* expected */ "x86" + }, + new[] { + /* input */ "android-x64/native/libfoo.so", + /* expected */ "x86_64" + }, + new[] { + /* input */ "android.21-x64/native/libfoo.so", + /* expected */ "x86_64" + }, + new[] { + /* input */ "packages/sqlitepclraw.lib.e_sqlite3.android/1.1.11/runtimes/android-arm64/native/libe_sqlite3.so", + /* expected */ "arm64-v8a" + }, + new[] { + /* input */ "arm64-v8a\\libfoo.so", + /* expected */ "arm64-v8a" + }, + new[] { + /* input */ "android-arm64\\libfoo.so", + /* expected */ "arm64-v8a" + }, + }; + + [Test] + [TestCaseSource (nameof (StringValueSource))] + public void StringValue (string input, string expected) + { + Assert.AreEqual (expected, AndroidRidAbiHelper.GetNativeLibraryAbi (input)); + } + + static object [] ITaskItemValueSource = new object [] { + new object [] { + /* input */ + new TaskItem(""), + /* expected */ + default (string) + }, + new object [] { + /* input */ + new TaskItem("armeabi-v7a/libfoo.so"), + /* expected */ + "armeabi-v7a" + }, + new object [] { + /* input */ + new TaskItem("libabi.so", new Dictionary { + { "Abi", "armeabi-v7a" } + }), + /* expected */ + "armeabi-v7a" + }, + new object [] { + /* input */ + new TaskItem("librid.so", new Dictionary { + { "RuntimeIdentifier", "android-arm" } + }), + /* expected */ + "armeabi-v7a" + }, + new object [] { + /* input */ + new TaskItem("liblink.so", new Dictionary { + { "Link", "armeabi-v7a/libfoo.so" } + }), + /* expected */ + "armeabi-v7a" + }, + new object [] { + /* input */ + new TaskItem("liblink.so", new Dictionary { + { "Link", "x86/libfoo.so" } + }), + /* expected */ + "x86" + }, + new object [] { + /* input */ + new TaskItem("liblink.so", new Dictionary { + { "Link", "x86_64/libfoo.so" } + }), + /* expected */ + "x86_64" + }, + new object [] { + /* input */ + new TaskItem("libridlink.so", new Dictionary { + { "Link", "android-arm/libfoo.so" } + }), + /* expected */ + "armeabi-v7a" + }, + new object [] { + /* input */ + new TaskItem("liblinkwin.so", new Dictionary { + { "Link", "x86_64\\libfoo.so" } + }), + /* expected */ + "x86_64" + }, + new object [] { + /* input */ + new TaskItem("liblinkwin.so", new Dictionary { + { "Link", "android-arm64\\libfoo.so" }, + }), + /* expected */ + "arm64-v8a", + }, + }; + + [Test] + [TestCaseSource (nameof (ITaskItemValueSource))] + public void ITaskItemValue (ITaskItem input, string expected) + { + Assert.AreEqual (expected, AndroidRidAbiHelper.GetNativeLibraryAbi (input)); + } + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs new file mode 100644 index 00000000000..834d3986a71 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs @@ -0,0 +1,146 @@ +using System.IO; +using System.Collections.Generic; +using Microsoft.Android.Build.BaseTasks.Tests.Utilities; +using Microsoft.Android.Build.Tasks; +using NUnit.Framework; +using Microsoft.Build.Framework; +using NUnit.Framework.Internal; +using System.Linq; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AndroidToolTaskTests + { + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + public class MyAndroidTask : AndroidTask + { + public override string TaskPrefix {get;} = "MAT"; + public string Key { get; set; } + public string Value { get; set; } + public bool ProjectSpecific { get; set; } = false; + public override bool RunTask () + { + var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty); + BuildEngine4.RegisterTaskObjectAssemblyLocal (key, Value, RegisteredTaskObjectLifetime.Build); + return true; + } + } + + public class MyOtherAndroidTask : AndroidTask + { + public override string TaskPrefix {get;} = "MOAT"; + public string Key { get; set; } + public bool ProjectSpecific { get; set; } = false; + + [Output] + public string Value { get; set; } + public override bool RunTask () + { + var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty); + Value = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (key, RegisteredTaskObjectLifetime.Build); + return true; + } + } + + public class DotnetToolOutputTestTask : AndroidToolTask + { + public override string TaskPrefix {get;} = "DTOT"; + protected override string ToolName => "dotnet"; + protected override string GenerateFullPathToTool () => ToolExe; + public string CommandLineArgs { get; set; } = "--info"; + protected override string GenerateCommandLineCommands () => CommandLineArgs; + } + + [SetUp] + public void TestSetup() + { + errors = new List (); + warnings = new List (); + messages = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings, messages); + } + + [Test] + [TestCase (true, true, true)] + [TestCase (false, false, true)] + [TestCase (true, false, false)] + [TestCase (false, true, false)] + public void TestRegisterTaskObjectCanRetrieveCorrectItem (bool projectSpecificA, bool projectSpecificB, bool expectedResult) + { + var task = new MyAndroidTask () { + BuildEngine = engine, + Key = "Foo", + Value = "Foo", + ProjectSpecific = projectSpecificA, + }; + task.Execute (); + var task2 = new MyOtherAndroidTask () { + BuildEngine = engine, + Key = "Foo", + ProjectSpecific = projectSpecificB, + }; + task2.Execute (); + Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0); + } + + [Test] + [TestCase (true, true, false)] + [TestCase (false, false, true)] + [TestCase (true, false, false)] + [TestCase (false, true, false)] + public void TestRegisterTaskObjectFailsWhenDirectoryChanges (bool projectSpecificA, bool projectSpecificB, bool expectedResult) + { + MyAndroidTask task; + var currentDir = Directory.GetCurrentDirectory (); + Directory.SetCurrentDirectory (Path.Combine (currentDir, "..")); + try { + task = new MyAndroidTask () { + BuildEngine = engine, + Key = "Foo", + Value = "Foo", + ProjectSpecific = projectSpecificA, + }; + } finally { + Directory.SetCurrentDirectory (currentDir); + } + task.Execute (); + var task2 = new MyOtherAndroidTask () { + BuildEngine = engine, + Key = "Foo", + ProjectSpecific = projectSpecificB, + }; + task2.Execute (); + Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0); + } + + [Test] + [TestCase ("invalidcommand", false, "You intended to execute a .NET program, but dotnet-invalidcommand does not exist.")] + [TestCase ("--info", true, "")] + public void FailedAndroidToolTaskShouldLogOutputAsError (string args, bool expectedResult, string expectedErrorText) + { + var task = new DotnetToolOutputTestTask () { + BuildEngine = engine, + CommandLineArgs = args, + }; + var taskSucceeded = task.Execute (); + Assert.AreEqual (expectedResult, taskSucceeded, "Task execution did not return expected value."); + + if (taskSucceeded) { + Assert.IsEmpty (errors, "Successful task should not have any errors."); + } else { + Assert.IsNotEmpty (errors, "Task expected to fail should have errors."); + Assert.AreEqual ("MSB6006", errors [0].Code, + $"Expected error code MSB6006 but got {errors [0].Code}"); + Assert.AreEqual ("XADTOT0000", errors [1].Code, + $"Expected error code XADTOT0000 but got {errors [1].Code}"); + Assert.IsTrue (errors.Any (e => e.Message.Contains (expectedErrorText)), + "Task expected to fail should contain expected error text."); + } + } + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs new file mode 100644 index 00000000000..13a9762d170 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Android.Build.Tasks; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AsyncTaskExtensionsTests + { + const int Iterations = 32; + + class TestAsyncTask : AsyncTask + { + public override string TaskPrefix => "TEST"; + } + + [Test] + public async Task RunTask () + { + bool set = false; + await new TestAsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type + Assert.IsTrue (set); + } + + [Test] + public async Task RunTaskOfT () + { + bool set = false; + Assert.IsTrue (await new TestAsyncTask ().RunTask (() => set = true), "RunTask should return true"); + Assert.IsTrue (set); + } + + [Test] + public async Task WhenAll () + { + bool set = false; + await new TestAsyncTask ().WhenAll (new [] { 0 }, _ => set = true); + Assert.IsTrue (set); + } + + [Test] + public async Task WhenAllWithLock () + { + var input = new int [Iterations]; + var output = new List (); + await new TestAsyncTask ().WhenAllWithLock (input, (i, l) => { + lock (l) output.Add (i); + }); + Assert.AreEqual (Iterations, output.Count); + } + + [Test] + public void ParallelForEach () + { + bool set = false; + new TestAsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true); + Assert.IsTrue (set); + } + + [Test] + public void ParallelForEachWithLock () + { + var input = new int [Iterations]; + var output = new List (); + new TestAsyncTask ().ParallelForEachWithLock (input, (i, l) => { + lock (l) output.Add (i); + }); + Assert.AreEqual (Iterations, output.Count); + } + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs new file mode 100644 index 00000000000..912b4c1a207 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.Android.Build.BaseTasks.Tests.Utilities; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AsyncTaskTests + { + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + [SetUp] + public void TestSetup () + { + errors = new List (); + warnings = new List (); + messages = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings, messages); + } + + class AsyncTaskTest : AsyncTask + { + public override string TaskPrefix => "ATT"; + } + + public class AsyncMessage : AsyncTask + { + public override string TaskPrefix => "AM"; + + public string Text { get; set; } + + public override bool Execute () + { + LogTelemetry ("Test", new Dictionary () { { "Property", "Value" } }); + return base.Execute (); + } + + public override async Task RunTaskAsync () + { + await Task.Delay (5000); + LogMessage (Text); + Complete (); + } + } + + class AsyncTaskExceptionTest : AsyncTask + { + public override string TaskPrefix => "ATET"; + + public string ExceptionMessage { get; set; } + + public override Task RunTaskAsync () + { + throw new System.InvalidOperationException (ExceptionMessage); + } + } + + + [Test] + public void RunAsyncTask () + { + var task = new AsyncTaskTest () { + BuildEngine = engine, + }; + + Assert.IsTrue (task.Execute (), "Empty AsyncTask should have ran successfully."); + } + + [Test] + public void RunAsyncTaskOverride () + { + var message = "Hello Async World!"; + var task = new AsyncMessage () { + BuildEngine = engine, + Text = message, + }; + var taskSucceeded = task.Execute (); + Assert.IsTrue (messages.Any (e => e.Message.Contains (message)), + $"Task did not contain expected message text: '{message}'."); + } + + [Test] + public void RunAsyncTaskExpectedException () + { + var expectedException = "test exception!"; + var task = new AsyncTaskExceptionTest () { + BuildEngine = engine, + ExceptionMessage = expectedException, + }; + + Assert.IsFalse (task.Execute (), "Exception AsyncTask should have failed."); + Assert.IsTrue (errors.Count == 1, "Exception AsyncTask should have produced one error."); + Assert.IsTrue (errors[0].Message.Contains (expectedException), + $"Task did not contain expected error text: '{expectedException}'."); + } + + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/FilesTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/FilesTests.cs new file mode 100644 index 00000000000..df7171dd2c4 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/FilesTests.cs @@ -0,0 +1,804 @@ +// https://github.com/xamarin/xamarin-android/blob/34acbbae6795854cc4e9f8eb7167ab011e0266b4/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/MonoAndroidHelperTests.cs +// https://github.com/xamarin/xamarin-android/blob/799506a9dfb746b8bdc8a4ab77e19eee875f00e3/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/FilesTests.cs + +using NUnit.Framework; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.Tools.Zip; +using Microsoft.Android.Build.Tasks; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class FilesTests + { + bool IsWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows); + const int MaxFileName = 255; + + static readonly Encoding encoding = Encoding.UTF8; + string tempDir; + MemoryStream stream; + + [SetUp] + public void SetUp () + { + tempDir = Path.Combine (Path.GetTempPath (), TestContext.CurrentContext.Test.Name); + stream = new MemoryStream (); + } + + [TearDown] + public void TearDown () + { + stream.Dispose (); + + var dir = Files.ToLongPath (tempDir); + if (Directory.Exists (dir)) + Directory.Delete (dir, recursive: true); + } + + [Test] + public void CopyIfStringChanged () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + var foo = "bar"; + Assert.IsTrue (Files.CopyIfStringChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + Assert.IsFalse (Files.CopyIfStringChanged (foo, tempFile), "Should *not* write unless changed."); + foo += "\n"; + Assert.IsTrue (Files.CopyIfStringChanged (foo, tempFile), "Should write when changed."); + } + + [Test] + public void CopyIfBytesChanged () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.bin"); + + var foo = new byte [32]; + Assert.IsTrue (Files.CopyIfBytesChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + Assert.IsFalse (Files.CopyIfBytesChanged (foo, tempFile), "Should *not* write unless changed."); + foo [0] = 0xFF; + Assert.IsTrue (Files.CopyIfBytesChanged (foo, tempFile), "Should write when changed."); + } + + [Test] + public void CopyIfStreamChanged () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + using (var foo = new MemoryStream ()) + using (var writer = new StreamWriter (foo)) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (Files.CopyIfStreamChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + Assert.IsFalse (Files.CopyIfStreamChanged (foo, tempFile), "Should *not* write unless changed."); + writer.WriteLine (); + writer.Flush (); + Assert.IsTrue (Files.CopyIfStreamChanged (foo, tempFile), "Should write when changed."); + } + } + + [Test] + public void CopyIfStringChanged_NewDirectory () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + var foo = "bar"; + Assert.IsTrue (Files.CopyIfStringChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + + [Test] + public void CopyIfBytesChanged_NewDirectory () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.bin"); + + var foo = new byte [32]; + Assert.IsTrue (Files.CopyIfBytesChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + + [Test] + public void CopyIfStreamChanged_NewDirectory () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + using (var foo = new MemoryStream ()) + using (var writer = new StreamWriter (foo)) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (Files.CopyIfStreamChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + } + + [Test] + public void CopyIfBytesChanged_Readonly () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.bin"); + + if (File.Exists (tempFile)) { + File.SetAttributes (tempFile, FileAttributes.Normal); + } + File.WriteAllText (tempFile, ""); + File.SetAttributes (tempFile, FileAttributes.ReadOnly); + + var foo = new byte [32]; + Assert.IsTrue (Files.CopyIfBytesChanged (foo, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + + [Test] + public void CleanBOM_Readonly () + { + var encoding = Encoding.UTF8; + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + if (File.Exists (tempFile)) { + File.SetAttributes (tempFile, FileAttributes.Normal); + } + using (var stream = File.Create (tempFile)) + using (var writer = new StreamWriter (stream, encoding)) { + writer.Write ("This will have a BOM"); + } + File.SetAttributes (tempFile, FileAttributes.ReadOnly); + var before = File.ReadAllBytes (tempFile); + Files.CleanBOM (tempFile); + var after = File.ReadAllBytes (tempFile); + var preamble = encoding.GetPreamble (); + Assert.AreEqual (before.Length, after.Length + preamble.Length, "BOM should be removed!"); + } + + [Test] + public void CopyIfStreamChanged_MemoryStreamPool_StreamWriter () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + var pool = new MemoryStreamPool (); + var expected = pool.Rent (); + pool.Return (expected); + + using (var writer = pool.CreateStreamWriter ()) { + writer.WriteLine ("bar"); + writer.Flush (); + + Assert.IsTrue (Files.CopyIfStreamChanged (writer.BaseStream, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + + var actual = pool.Rent (); + Assert.AreSame (expected, actual); + Assert.AreEqual (0, actual.Length); + } + + [Test] + public void CopyIfStreamChanged_MemoryStreamPool_BinaryWriter () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.bin"); + + var pool = new MemoryStreamPool (); + var expected = pool.Rent (); + pool.Return (expected); + + using (var writer = pool.CreateBinaryWriter ()) { + writer.Write (42); + writer.Flush (); + + Assert.IsTrue (Files.CopyIfStreamChanged (writer.BaseStream, tempFile), "Should write on new file."); + FileAssert.Exists (tempFile); + } + + var actual = pool.Rent (); + Assert.AreSame (expected, actual); + Assert.AreEqual (0, actual.Length); + } + + [Test] + public void SetWriteable () + { + Directory.CreateDirectory (tempDir); + var tempFile = Path.Combine (tempDir, "foo.txt"); + + File.WriteAllText (tempFile, contents: "foo"); + File.SetAttributes (tempFile, FileAttributes.ReadOnly); + + Files.SetWriteable (tempFile); + + var attributes = File.GetAttributes (tempFile); + Assert.AreEqual (FileAttributes.Normal, attributes); + File.WriteAllText (tempFile, contents: "bar"); + } + + [Test] + public void SetDirectoryWriteable () + { + Directory.CreateDirectory (tempDir); + try { + var directoryInfo = new DirectoryInfo (tempDir); + directoryInfo.Attributes |= FileAttributes.ReadOnly; + Files.SetDirectoryWriteable (tempDir); + + directoryInfo = new DirectoryInfo (tempDir); + Assert.AreEqual (FileAttributes.Directory, directoryInfo.Attributes); + } finally { + Directory.Delete (tempDir); + } + } + + void AssertFile (string path, string contents) + { + var fullPath = Path.Combine (tempDir, path); + FileAssert.Exists (fullPath); + Assert.AreEqual (contents, File.ReadAllText (fullPath), $"Contents did not match at path: {path}"); + } + + void AssertFileDoesNotExist (string path) + { + FileAssert.DoesNotExist (Path.Combine (tempDir, path)); + } + + bool ExtractAll (MemoryStream stream) + { + using (var zip = ZipArchive.Open (stream)) { + return Files.ExtractAll (zip, tempDir); + } + } + + string NewFile (string contents = null, string fileName = "") + { + if (string.IsNullOrEmpty (fileName)) { + fileName = Path.GetRandomFileName (); + } + var path = Path.Combine (tempDir, fileName); + if (!string.IsNullOrEmpty (contents)) { + Directory.CreateDirectory (Path.GetDirectoryName (path)); + if (IsWindows && path.Length >= Files.MaxPath) { + File.WriteAllText (Files.ToLongPath (path), contents); + } else { + File.WriteAllText (path, contents); + } + } + return path; + } + + Stream NewStream (string contents) => new MemoryStream (Encoding.Default.GetBytes (contents)); + + [Test] + public void CopyIfChanged_NoChanges () + { + var src = NewFile ("foo"); + var dest = NewFile ("foo"); + Assert.IsFalse (Files.CopyIfChanged (src, dest), "No change should have occurred"); + FileAssert.AreEqual (src, dest); + } + + [Test] + public void CopyIfChanged_NoExist () + { + var src = NewFile ("foo"); + var dest = NewFile (); + Assert.IsTrue (Files.CopyIfChanged (src, dest), "Changes should have occurred"); + FileAssert.AreEqual (src, dest); + } + + [Test] + public void CopyIfChanged_LongPath () + { + var src = NewFile (contents: "foo"); + var dest = NewFile (contents: "bar", fileName: "bar".PadRight (MaxFileName, 'N')); + dest = Files.ToLongPath (dest); + Assert.IsTrue (Files.CopyIfChanged (src, dest), "Changes should have occurred"); + FileAssert.AreEqual (src, dest); + } + + [Test] + public void CopyIfChanged_Changes () + { + var src = NewFile ("foo"); + var dest = NewFile ("bar"); + Assert.IsTrue (Files.CopyIfChanged (src, dest), "Changes should have occurred"); + FileAssert.AreEqual (src, dest); + } + + [Test] + public void CopyIfChanged_Readonly () + { + var src = NewFile ("foo"); + var dest = NewFile ("bar"); + File.SetAttributes (dest, FileAttributes.ReadOnly); + Assert.IsTrue (Files.CopyIfChanged (src, dest), "Changes should have occurred"); + FileAssert.AreEqual (src, dest); + } + + [Test] + public void CopyIfChanged_CasingChange () + { + var src = NewFile (contents: "foo"); + var dest = NewFile (contents: "Foo", fileName: "foo"); + dest = dest.Replace ("foo", "Foo"); + Assert.IsTrue (Files.CopyIfChanged (src, dest), "Changes should have occurred"); + FileAssert.AreEqual (src, dest); + + var files = Directory.GetFiles (Path.GetDirectoryName (dest), "Foo"); + Assert.AreEqual ("Foo", Path.GetFileName (files [0])); + } + + [Test] + public void CopyIfStringChanged_NoChanges () + { + var dest = NewFile ("foo"); + Assert.IsFalse (Files.CopyIfStringChanged ("foo", dest), "No change should have occurred"); + FileAssert.Exists (dest); + } + + [Test] + public void CopyIfStringChanged_NoExist () + { + var dest = NewFile (); + Assert.IsTrue (Files.CopyIfStringChanged ("foo", dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + + [Test] + public void CopyIfStringChanged_LongPath () + { + var dest = NewFile (fileName: "bar".PadRight (MaxFileName, 'N')); + dest = Files.ToLongPath (dest); + Assert.IsTrue (Files.CopyIfStringChanged ("foo", dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + + [Test] + public void CopyIfStringChanged_Changes () + { + var dest = NewFile ("bar"); + Assert.IsTrue (Files.CopyIfStringChanged ("foo", dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + + [Test] + public void CopyIfStringChanged_Readonly () + { + var dest = NewFile ("bar"); + File.SetAttributes (dest, FileAttributes.ReadOnly); + Assert.IsTrue (Files.CopyIfStringChanged ("foo", dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + + [Test] + public void CopyIfStringChanged_CasingChange () + { + var dest = NewFile (contents: "foo", fileName: "foo"); + dest = dest.Replace ("foo", "Foo"); + Assert.IsTrue (Files.CopyIfStringChanged ("Foo", dest), "Changes should have occurred"); + FileAssert.Exists (dest); + Assert.AreEqual ("Foo", File.ReadAllText (dest), "File contents should match"); + + var files = Directory.GetFiles (Path.GetDirectoryName (dest), "Foo"); + Assert.AreEqual ("Foo", Path.GetFileName (files [0]), "File name should match"); + } + + [Test] + public void CopyIfStreamChanged_NoChanges () + { + using (var src = NewStream ("foo")) { + var dest = NewFile ("foo"); + Assert.IsFalse (Files.CopyIfStreamChanged (src, dest), "No change should have occurred"); + FileAssert.Exists (dest); + } + } + + [Test] + public void CopyIfStreamChanged_LongPath () + { + using (var src = NewStream ("foo")) { + var dest = NewFile (fileName: "bar".PadRight (MaxFileName, 'N')); + dest = Files.ToLongPath (dest); + Assert.IsTrue (Files.CopyIfStreamChanged (src, dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + } + + [Test] + public void CopyIfStreamChanged_NoExist () + { + using (var src = NewStream ("foo")) { + var dest = NewFile (); + Assert.IsTrue (Files.CopyIfStreamChanged (src, dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + } + + [Test] + public void CopyIfStreamChanged_Changes () + { + using (var src = NewStream ("foo")) { + var dest = NewFile ("bar"); + Assert.IsTrue (Files.CopyIfStreamChanged (src, dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + } + + [Test] + public void CopyIfStreamChanged_Readonly () + { + using (var src = NewStream ("foo")) { + var dest = NewFile ("bar"); + File.SetAttributes (dest, FileAttributes.ReadOnly); + Assert.IsTrue (Files.CopyIfStreamChanged (src, dest), "Changes should have occurred"); + FileAssert.Exists (dest); + } + } + + [Test] + public void CopyIfStreamChanged_CasingChange () + { + using (var src = NewStream ("Foo")) { + var dest = NewFile (contents: "foo", fileName: "foo"); + dest = dest.Replace ("foo", "Foo"); + Assert.IsTrue (Files.CopyIfStreamChanged (src, dest), "Changes should have occurred"); + FileAssert.Exists (dest); + Assert.AreEqual ("Foo", File.ReadAllText (dest), "File contents should match"); + + var files = Directory.GetFiles (Path.GetDirectoryName (dest), "Foo"); + Assert.AreEqual ("Foo", Path.GetFileName (files [0]), "File name should match"); + } + } + + [Test] + public async Task CopyIfChanged_LockedFile () + { + var dest = NewFile (contents: "foo", fileName: "foo_locked"); + var src = NewFile (contents: "foo0", fileName: "foo"); + using (var file = File.OpenWrite (dest)) { + Assert.Throws (() => Files.CopyIfChanged (src, dest)); + } + src = NewFile (contents: "foo1", fileName: "foo"); + Assert.IsTrue (Files.CopyIfChanged (src, dest)); + src = NewFile (contents: "foo2", fileName: "foo"); + dest = NewFile (contents: "foo", fileName: "foo_locked2"); + var ev = new ManualResetEvent (false); + var task = Task.Run (async () => { + var file = File.Open (dest, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + try { + ev.Set (); + await Task.Delay (2500); + } finally { + file.Close(); + file.Dispose (); + } + }); + ev.WaitOne (); + Assert.IsTrue (Files.CopyIfChanged (src, dest)); + await task; + } + + [Test] + public void ExtractAll () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + bool changes = ExtractAll (stream); + + Assert.IsTrue (changes, "ExtractAll should report changes."); + AssertFile ("a.txt", "a"); + AssertFile (Path.Combine ("b", "b.txt"), "b"); + } + + [Test] + public void ExtractAll_NoChanges () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + bool changes = ExtractAll (stream); + Assert.IsTrue (changes, "ExtractAll should report changes."); + + stream.SetLength (0); + using (var zip = ZipArchive.Open (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + changes = ExtractAll (stream); + + Assert.IsFalse (changes, "ExtractAll should *not* report changes."); + AssertFile ("a.txt", "a"); + AssertFile (Path.Combine ("b", "b.txt"), "b"); + } + + [Test] + public void ExtractAll_NewFile () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + bool changes = ExtractAll (stream); + Assert.IsTrue (changes, "ExtractAll should report changes."); + + stream.SetLength (0); + using (var zip = ZipArchive.Open (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + zip.AddEntry ("c/c.txt", "c", encoding); + } + + changes = ExtractAll (stream); + + Assert.IsTrue (changes, "ExtractAll should report changes."); + AssertFile ("a.txt", "a"); + AssertFile (Path.Combine ("b", "b.txt"), "b"); + AssertFile (Path.Combine ("c", "c.txt"), "c"); + } + + [Test] + public void ExtractAll_FileChanged () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("foo.txt", "foo", encoding); + } + + bool changes = ExtractAll (stream); + Assert.IsTrue (changes, "ExtractAll should report changes."); + + stream.SetLength (0); + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("foo.txt", "bar", encoding); + } + + changes = ExtractAll (stream); + + Assert.IsTrue (changes, "ExtractAll should report changes."); + AssertFile ("foo.txt", "bar"); + } + + [Test] + public void ExtractAll_FileDeleted () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + bool changes = ExtractAll (stream); + Assert.IsTrue (changes, "ExtractAll should report changes."); + + stream.SetLength (0); + using (var zip = ZipArchive.Open (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + + changes = ExtractAll (stream); + + Assert.IsTrue (changes, "ExtractAll should report changes."); + AssertFile ("a.txt", "a"); + FileAssert.DoesNotExist (Path.Combine (tempDir, "b", "b.txt")); + } + + [Test] + public void ExtractAll_ModifyCallback () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("foo/a.txt", "a", encoding); + zip.AddEntry ("foo/b/b.txt", "b", encoding); + } + + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + bool changes = Files.ExtractAll (zip, tempDir, modifyCallback: e => e.Replace ("foo/", "")); + Assert.IsTrue (changes, "ExtractAll should report changes."); + } + + AssertFile ("a.txt", "a"); + AssertFile (Path.Combine ("b", "b.txt"), "b"); + } + + [Test] + public void ExtractAll_SkipCallback () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + zip.AddEntry ("b/b.txt", "b", encoding); + } + + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + bool changes = Files.ExtractAll (zip, tempDir, skipCallback: e => e == "a.txt"); + Assert.IsTrue (changes, "ExtractAll should report changes."); + } + + AssertFileDoesNotExist ("a.txt"); + AssertFile (Path.Combine ("b", "b.txt"), "b"); + } + + [Test] + public void ExtractAll_MacOSFiles () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a/.DS_Store", "a", encoding); + zip.AddEntry ("b/__MACOSX/b.txt", "b", encoding); + zip.AddEntry ("c/__MACOSX", "c", encoding); + } + + bool changes = ExtractAll (stream); + Assert.IsFalse (changes, "ExtractAll should *not* report changes."); + DirectoryAssert.DoesNotExist (tempDir); + } + + [Test] + public void ExtractAll_SkipsPathTraversal () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + + var destinationDir = Path.Combine (tempDir, "dest"); + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + // modifyCallback introduces a path traversal + bool changes = Files.ExtractAll (zip, destinationDir, modifyCallback: e => "../" + e); + Assert.IsFalse (changes, "ExtractAll should not report changes for skipped entries."); + } + FileAssert.DoesNotExist (Path.Combine (tempDir, "a.txt")); + } + + [Test] + public void ExtractAll_SkipsPathTraversal_ExtractsValidEntries () + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("good.txt", "good", encoding); + zip.AddEntry ("relative.txt", "relative", encoding); + } + + var destinationDir = Path.Combine (tempDir, "dest"); + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + // Only relative.txt gets a traversal prefix + bool changes = Files.ExtractAll (zip, destinationDir, modifyCallback: e => + e == "relative.txt" ? "../" + e : e); + Assert.IsTrue (changes, "ExtractAll should report changes for the valid entry."); + } + AssertFile (Path.Combine ("dest", "good.txt"), "good"); + FileAssert.DoesNotExist (Path.Combine (tempDir, "relative.txt")); + } + + [TestCase ("../../")] + [TestCase ("foo/../../../")] + public void ExtractAll_SkipsPathTraversal_ForwardSlash (string prefix) + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + + var destinationDir = Path.Combine (tempDir, "dest"); + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + bool changes = Files.ExtractAll (zip, destinationDir, modifyCallback: e => prefix + e); + Assert.IsFalse (changes, $"Entry with prefix '{prefix}' should be skipped."); + } + } + + [TestCase ("..\\")] + [TestCase ("..\\..\\")] + [Platform ("Win")] + public void ExtractAll_SkipsPathTraversal_BackSlash (string prefix) + { + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + + var destinationDir = Path.Combine (tempDir, "dest"); + stream.Position = 0; + using (var zip = ZipArchive.Open (stream)) { + bool changes = Files.ExtractAll (zip, destinationDir, modifyCallback: e => prefix + e); + Assert.IsFalse (changes, $"Entry with prefix '{prefix}' should be skipped."); + } + } + + [Test] + public void ToHashString () + { + var bytes = new byte [] { 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF }; + var expected = BitConverter.ToString (bytes).Replace ("-", string.Empty); + Assert.AreEqual (expected, Files.ToHexString (bytes)); + } + + [Test] + public void CopyIfZipChanged_Stream () + { + Directory.CreateDirectory (tempDir); + var destination = Path.Combine (tempDir, "dest.zip"); + + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + stream.Position = 0; + + Assert.IsTrue (Files.CopyIfZipChanged (stream, destination), "Should copy on new file."); + FileAssert.Exists (destination); + Assert.IsFalse (Files.CopyIfZipChanged (stream, destination), "Should *not* copy when unchanged."); + } + + [Test] + public void CopyIfZipChanged_String () + { + Directory.CreateDirectory (tempDir); + var source = Path.Combine (tempDir, "source.zip"); + var destination = Path.Combine (tempDir, "dest.zip"); + + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + stream.Position = 0; + using (var f = File.Create (source)) { + stream.CopyTo (f); + } + + Assert.IsTrue (Files.CopyIfZipChanged (source, destination), "Should copy on new file."); + FileAssert.Exists (destination); + Assert.IsFalse (Files.CopyIfZipChanged (source, destination), "Should *not* copy when unchanged."); + } + + [Test] + public void CopyIfZipChanged_BareFileName () + { + // Regression test: Path.GetDirectoryName("file.zip") returns "", + // which previously caused Directory.CreateDirectory("") to throw. + var cwd = Directory.GetCurrentDirectory (); + try { + Directory.CreateDirectory (tempDir); + Directory.SetCurrentDirectory (tempDir); + + using (var zip = ZipArchive.Create (stream)) { + zip.AddEntry ("a.txt", "a", encoding); + } + stream.Position = 0; + + Assert.IsTrue (Files.CopyIfZipChanged (stream, "bare.zip"), "Should copy bare filename."); + FileAssert.Exists (Path.Combine (tempDir, "bare.zip")); + } finally { + Directory.SetCurrentDirectory (cwd); + } + } + + [Test] + public void DeleteFile_NullLog_DoesNotThrow () + { + var path = Path.Combine (tempDir, "directory-instead-of-file"); + Directory.CreateDirectory (path); + Assert.DoesNotThrow (() => Files.DeleteFile (path, null)); + } + + [Test] + public void DeleteFile_NonTaskLoggingHelperLog_DoesNotThrow () + { + var path = Path.Combine (tempDir, "directory-instead-of-file-nontasklog"); + Directory.CreateDirectory (path); + Assert.DoesNotThrow (() => Files.DeleteFile (path, "not a TaskLoggingHelper")); + } + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/LockCheckTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/LockCheckTests.cs new file mode 100644 index 00000000000..eaf2806ea69 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/LockCheckTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.BaseTasks.Tests.Utilities; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests; + +[TestFixture] +public class LockCheckTests +{ + string tempFile; + Stream tempStream; + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + [SetUp] + public void Setup () + { + tempFile = Path.GetTempFileName (); + tempStream = File.Create (tempFile); + errors = new List (); + warnings = new List (); + messages = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings, messages); + } + + [TearDown] + public void TearDown () + { + tempStream.Dispose (); + if (File.Exists (tempFile)) + File.Delete (tempFile); + } + + class MyTask : AndroidTask + { + public override string TaskPrefix => "MYT"; + + public Action Action { get; set; } + + public override bool RunTask () + { + Action (); + return false; + } + } + + void AssertFileLocked (string actual) + { + Assert.IsNotEmpty (actual); + StringAssert.StartsWith ("The file is locked by:", actual); + StringAssert.IsMatch (@"\d+", actual, "Should contain a PID!"); + } + + [Test] + public void LockCheck_FileLocked () + { + string actual = LockCheck.GetLockedFileMessage (tempFile); + if (OperatingSystem.IsWindows ()) { + AssertFileLocked (actual); + } else { + Assert.IsEmpty (actual); + } + } + + [Test] + public void LockCheck_AndroidTask_FileCreate () => + LockCheck_AndroidTask (() => File.Create (tempFile)); + + [Test] + public void LockCheck_AndroidTask_FileDelete () => + LockCheck_AndroidTask (() => File.Delete (tempFile)); + + [Test] + public void LockCheck_AndroidTask_UnauthorizedAccessException () => + LockCheck_AndroidTask (() => throw new UnauthorizedAccessException ($"Access to the path '{tempFile}' is denied.")); + + void LockCheck_AndroidTask (Action action) + { + if (!OperatingSystem.IsWindows ()) + Assert.Ignore ("Test only valid on Windows"); + + var task = new MyTask { + BuildEngine = engine, + Action = action, + }; + task.Execute (); + + // error XAMYT7024: The file is locked by: "testhost (22040)" + // System.IO.IOException: The process cannot access the file 'D:\temp\tmphkqpda.tmp' because it is being used by another process. + // at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile (String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options) + // ... rest of stacktrace + Assert.AreEqual (1, errors.Count); + AssertFileLocked (errors [0].Message); + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/MemoryStreamPoolTests.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/MemoryStreamPoolTests.cs new file mode 100644 index 00000000000..17d7b8c6e6c --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/MemoryStreamPoolTests.cs @@ -0,0 +1,70 @@ +// https://github.com/xamarin/xamarin-android/blob/799506a9dfb746b8bdc8a4ab77e19eee875f00e3/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/MemoryStreamPoolTests.cs + +using System; +using System.IO; +using NUnit.Framework; +using Microsoft.Android.Build.Tasks; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class MemoryStreamPoolTests + { + MemoryStreamPool pool; + + [SetUp] + public void SetUp () + { + pool = new MemoryStreamPool (); + } + + [Test] + public void Reuse () + { + var expected = pool.Rent (); + expected.Write (new byte [] { 1, 2, 3 }, 0, 3); + pool.Return (expected); + var actual = pool.Rent (); + Assert.AreSame (expected, actual); + Assert.AreEqual (0, actual.Length); + } + + [Test] + public void PutDisposed () + { + var stream = new MemoryStream (); + stream.Dispose (); + Assert.Throws (() => pool.Return (stream)); + } + + [Test] + public void CreateStreamWriter () + { + var pool = new MemoryStreamPool (); + var expected = pool.Rent (); + using (var writer = MemoryStreamPool.Shared.CreateStreamWriter ()) { + writer.WriteLine ("foobar"); + } + pool.Return (expected); + + var actual = pool.Rent (); + Assert.AreSame (expected, actual); + Assert.AreEqual (0, actual.Length); + } + + [Test] + public void CreateBinaryWriter () + { + var pool = new MemoryStreamPool (); + var expected = pool.Rent (); + using (var writer = MemoryStreamPool.Shared.CreateBinaryWriter ()) { + writer.Write (42); + } + pool.Return (expected); + + var actual = pool.Rent (); + Assert.AreSame (expected, actual); + Assert.AreEqual (0, actual.Length); + } + } +} diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj new file mode 100644 index 00000000000..fa8b2e1d1dd --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj @@ -0,0 +1,24 @@ + + + + $(DotNetTargetFramework) + Microsoft.Android.Build.BaseTasks.Tests + false + $(TestOutputFullPath) + false + Major + + + + + + + + + + + + + + + diff --git a/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs new file mode 100644 index 00000000000..38155fcf2c9 --- /dev/null +++ b/external/xamarin-android-tools/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests.Utilities { + public class MockBuildEngine : IBuildEngine, IBuildEngine2, IBuildEngine3, IBuildEngine4 { + public MockBuildEngine (TextWriter output, IList errors = null, IList warnings = null, IList messages = null, IList customEvents = null) + { + this.Output = output; + this.Errors = errors; + this.Warnings = warnings; + this.Messages = messages; + this.CustomEvents = customEvents; + } + + private TextWriter Output { get; } + + private IList Errors { get; } + + private IList Warnings { get; } + + private IList Messages { get; } + + private IList CustomEvents { get; } + + int IBuildEngine.ColumnNumberOfTaskNode => -1; + + bool IBuildEngine.ContinueOnError => false; + + int IBuildEngine.LineNumberOfTaskNode => -1; + + string IBuildEngine.ProjectFileOfTaskNode => "this.xml"; + + bool IBuildEngine2.IsRunningMultipleNodes => false; + + bool IBuildEngine.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + + void IBuildEngine.LogCustomEvent (CustomBuildEventArgs e) + { + this.Output.WriteLine ($"Custom: {e.Message}"); + if (CustomEvents != null) + CustomEvents.Add (e); + } + + void IBuildEngine.LogErrorEvent (BuildErrorEventArgs e) + { + this.Output.WriteLine ($"Error: {e.Message}"); + if (Errors != null) + Errors.Add (e); + } + + void IBuildEngine.LogMessageEvent (BuildMessageEventArgs e) + { + this.Output.WriteLine ($"Message: {e.Message}"); + if (Messages != null) + Messages.Add (e); + } + + void IBuildEngine.LogWarningEvent (BuildWarningEventArgs e) + { + this.Output.WriteLine ($"Warning: {e.Message}"); + if (Warnings != null) + Warnings.Add (e); + } + + private Dictionary Tasks = new Dictionary (); + + void IBuildEngine4.RegisterTaskObject (object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection) + { + Tasks.Add (key, obj); + } + + object IBuildEngine4.GetRegisteredTaskObject (object key, RegisteredTaskObjectLifetime lifetime) + { + object obj = null; + Tasks.TryGetValue (key, out obj); + return obj; + } + + object IBuildEngine4.UnregisterTaskObject (object key, RegisteredTaskObjectLifetime lifetime) + { + var obj = Tasks [key]; + Tasks.Remove (key); + return obj; + } + + BuildEngineResult IBuildEngine3.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IList [] removeGlobalProperties, string [] toolsVersion, bool returnTargetOutputs) + { + throw new NotImplementedException (); + } + + void IBuildEngine3.Yield () { } + + void IBuildEngine3.Reacquire () { } + + bool IBuildEngine2.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) => true; + + bool IBuildEngine2.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IDictionary [] targetOutputsPerProject, string [] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) => true; + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs new file mode 100644 index 00000000000..acd29f1d865 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -0,0 +1,1470 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests; + +/// +/// Tests for AdbRunner parsing, formatting, and merging logic. +/// Ported from dotnet/android GetAvailableAndroidDevicesTests. +/// +/// API consumer reference: +/// - ParseAdbDevicesOutput: used by dotnet/android GetAvailableAndroidDevices task +/// - BuildDeviceDescription: used by dotnet/android GetAvailableAndroidDevices task +/// - FormatDisplayName: used by dotnet/android GetAvailableAndroidDevices tests +/// - MergeDevicesAndEmulators: used by dotnet/android GetAvailableAndroidDevices task +/// - MapAdbStateToStatus: used internally by ParseAdbDevicesOutput, public for extensibility +/// - ListDevicesAsync: used by MAUI DevTools Adb provider (Providers/Android/Adb.cs) +/// - WaitForDeviceAsync: used by MAUI DevTools Adb provider +/// - StopEmulatorAsync: used by MAUI DevTools Adb provider +/// - GetEmulatorAvdNameAsync: internal, used by ListDevicesAsync only +/// +[TestFixture] +public class AdbRunnerTests +{ + // Consumer: dotnet/android GetAvailableAndroidDevices.cs, MAUI DevTools (via ListDevicesAsync) + + [Test] + public void ParseAdbDevicesOutput_RealWorldData () + { + var output = + "List of devices attached\n" + + "0A041FDD400327 device product:redfin model:Pixel_5 device:redfin transport_id:2\n" + + "emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (2, devices.Count); + + // Physical device + Assert.AreEqual ("0A041FDD400327", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Device, devices [0].Type); + Assert.AreEqual (AdbDeviceStatus.Online, devices [0].Status); + Assert.AreEqual ("Pixel 5", devices [0].Description); + Assert.AreEqual ("Pixel_5", devices [0].Model); + Assert.AreEqual ("redfin", devices [0].Product); + Assert.AreEqual ("redfin", devices [0].Device); + Assert.AreEqual ("2", devices [0].TransportId); + Assert.IsFalse (devices [0].IsEmulator); + + // Emulator + Assert.AreEqual ("emulator-5554", devices [1].Serial); + Assert.AreEqual (AdbDeviceType.Emulator, devices [1].Type); + Assert.AreEqual (AdbDeviceStatus.Online, devices [1].Status); + Assert.AreEqual ("sdk gphone64 x86 64", devices [1].Description); // model with underscores replaced + Assert.AreEqual ("sdk_gphone64_x86_64", devices [1].Model); + Assert.AreEqual ("1", devices [1].TransportId); + Assert.IsTrue (devices [1].IsEmulator); + } + + [Test] + public void ParseAdbDevicesOutput_EmptyOutput () + { + var output = "List of devices attached\n\n"; + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + Assert.AreEqual (0, devices.Count); + } + + [Test] + public void ParseAdbDevicesOutput_SingleEmulator () + { + var output = + "List of devices attached\n" + + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("emulator-5554", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Emulator, devices [0].Type); + Assert.AreEqual (AdbDeviceStatus.Online, devices [0].Status); + Assert.AreEqual ("sdk_gphone64_arm64", devices [0].Model); + Assert.AreEqual ("1", devices [0].TransportId); + } + + [Test] + public void ParseAdbDevicesOutput_SinglePhysicalDevice () + { + var output = + "List of devices attached\n" + + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("0A041FDD400327", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Device, devices [0].Type); + Assert.AreEqual (AdbDeviceStatus.Online, devices [0].Status); + Assert.AreEqual ("Pixel 6 Pro", devices [0].Description); + Assert.AreEqual ("Pixel_6_Pro", devices [0].Model); + Assert.AreEqual ("raven", devices [0].Product); + Assert.AreEqual ("2", devices [0].TransportId); + } + + [Test] + public void ParseAdbDevicesOutput_OfflineDevice () + { + var output = + "List of devices attached\n" + + "emulator-5554 offline product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual (AdbDeviceStatus.Offline, devices [0].Status); + } + + [Test] + public void ParseAdbDevicesOutput_UnauthorizedDevice () + { + var output = + "List of devices attached\n" + + "0A041FDD400327 unauthorized usb:1-1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("0A041FDD400327", devices [0].Serial); + Assert.AreEqual (AdbDeviceStatus.Unauthorized, devices [0].Status); + Assert.AreEqual (AdbDeviceType.Device, devices [0].Type); + } + + [Test] + public void ParseAdbDevicesOutput_NoPermissionsDevice () + { + var output = + "List of devices attached\n" + + "???????????????? no permissions usb:1-1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("????????????????", devices [0].Serial); + Assert.AreEqual (AdbDeviceStatus.NoPermissions, devices [0].Status); + } + + [Test] + public void ParseAdbDevicesOutput_DeviceWithMinimalMetadata () + { + var output = + "List of devices attached\n" + + "ABC123 device\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("ABC123", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Device, devices [0].Type); + Assert.AreEqual (AdbDeviceStatus.Online, devices [0].Status); + Assert.AreEqual ("ABC123", devices [0].Description, "Should fall back to serial"); + } + + [Test] + public void ParseAdbDevicesOutput_InvalidLines () + { + var output = + "List of devices attached\n" + + "\n" + + " \n" + + "Some random text\n" + + "* daemon not running; starting now at tcp:5037\n" + + "* daemon started successfully\n" + + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count, "Should only return valid device lines"); + Assert.AreEqual ("emulator-5554", devices [0].Serial); + } + + [Test] + public void ParseAdbDevicesOutput_MixedDeviceStates () + { + var output = + "List of devices attached\n" + + "emulator-5554 device product:sdk_gphone64_arm64 model:Pixel_7 device:emu64a\n" + + "emulator-5556 offline\n" + + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro\n" + + "0B123456789ABC unauthorized usb:1-2\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (4, devices.Count); + Assert.AreEqual (AdbDeviceStatus.Online, devices [0].Status); + Assert.AreEqual (AdbDeviceStatus.Offline, devices [1].Status); + Assert.AreEqual (AdbDeviceStatus.Online, devices [2].Status); + Assert.AreEqual (AdbDeviceStatus.Unauthorized, devices [3].Status); + } + + [Test] + public void ParseAdbDevicesOutput_WindowsNewlines () + { + var output = + "List of devices attached\r\n" + + "emulator-5554 device transport_id:1\r\n" + + "\r\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("emulator-5554", devices [0].Serial); + Assert.IsTrue (devices [0].IsEmulator); + } + + [Test] + public void ParseAdbDevicesOutput_TabSeparator () + { + var output = + "List of devices attached\n" + + "emulator-5554\tdevice\n" + + "R5CR10YZQPJ\tdevice\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (2, devices.Count); + Assert.AreEqual ("emulator-5554", devices [0].Serial); + Assert.AreEqual ("R5CR10YZQPJ", devices [1].Serial); + } + + [Test] + public void ParseAdbDevicesOutput_IpPortDevice () + { + var output = + "List of devices attached\n" + + "192.168.1.100:5555 device product:sdk_gphone64_arm64 model:Remote_Device\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("192.168.1.100:5555", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Device, devices [0].Type, "IP devices should be Device"); + Assert.AreEqual ("Remote Device", devices [0].Description); + } + + [Test] + public void ParseAdbDevicesOutput_AdbDaemonStarting () + { + var output = + "* daemon not running; starting now at tcp:5037\n" + + "* daemon started successfully\n" + + "List of devices attached\n" + + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1\n" + + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (2, devices.Count, "Should parse devices even with daemon startup messages"); + } + + // Consumer: dotnet/android GetAvailableAndroidDevices.cs + + [Test] + public void DescriptionPriorityOrder () + { + // Model has highest priority + var output1 = "List of devices attached\ndevice1 device product:product_name model:model_name device:device_name\n"; + var devices1 = AdbRunner.ParseAdbDevicesOutput (output1.Split ('\n')); + Assert.AreEqual ("model name", devices1 [0].Description, "Model should have highest priority"); + + // Product has second priority + var output2 = "List of devices attached\ndevice2 device product:product_name device:device_name\n"; + var devices2 = AdbRunner.ParseAdbDevicesOutput (output2.Split ('\n')); + Assert.AreEqual ("product name", devices2 [0].Description, "Product should have second priority"); + + // Device code name has third priority + var output3 = "List of devices attached\ndevice3 device device:device_name\n"; + var devices3 = AdbRunner.ParseAdbDevicesOutput (output3.Split ('\n')); + Assert.AreEqual ("device name", devices3 [0].Description, "Device should have third priority"); + } + + [Test] + public void BuildDeviceDescription_EmulatorWithUppercaseAvdName () + { + // Mirrors dotnet/android ParseMixedDeviceStates: offline emulator gets AVD name "Pixel_9_Pro_XL" + // BuildDeviceDescription should format it preserving "XL" uppercase + var device = new AdbDeviceInfo { + Serial = "emulator-5556", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Offline, + AvdName = "Pixel_9_Pro_XL", + }; + + var description = AdbRunner.BuildDeviceDescription (device); + Assert.AreEqual ("Pixel 9 Pro XL", description, "Offline emulator should still get AVD name with uppercase preserved"); + } + + // Consumer: dotnet/android GetAvailableAndroidDevicesTests, BuildDeviceDescription (via AVD name formatting) + + [Test] + public void FormatDisplayName_ReplacesUnderscoresWithSpaces () + { + Assert.AreEqual ("Pixel 7 Pro", AdbRunner.FormatDisplayName ("pixel_7_pro")); + } + + [Test] + public void FormatDisplayName_AppliesTitleCase () + { + Assert.AreEqual ("Pixel 7 Pro", AdbRunner.FormatDisplayName ("pixel 7 pro")); + } + + [Test] + public void FormatDisplayName_ReplacesApiWithAPIUppercase () + { + Assert.AreEqual ("Pixel 5 API 34", AdbRunner.FormatDisplayName ("pixel_5_api_34")); + } + + [Test] + public void FormatDisplayName_HandlesMultipleApiOccurrences () + { + Assert.AreEqual ("Test API Device API 35", AdbRunner.FormatDisplayName ("test_api_device_api_35")); + } + + [Test] + public void FormatDisplayName_HandlesMixedCaseInput () + { + Assert.AreEqual ("Pixel 7 API 35", AdbRunner.FormatDisplayName ("PiXeL_7_API_35")); + } + + [Test] + public void FormatDisplayName_HandlesComplexNames () + { + // Lowercase input: "xl" gets title-cased to "Xl" + Assert.AreEqual ("Pixel 9 Pro Xl API 36", AdbRunner.FormatDisplayName ("pixel_9_pro_xl_api_36")); + } + + [Test] + public void FormatDisplayName_PreservesUppercaseSegments () + { + // Fully-uppercase segments like "XL", "SE", "FE" are preserved + Assert.AreEqual ("Pixel 9 Pro XL", AdbRunner.FormatDisplayName ("Pixel_9_Pro_XL")); + Assert.AreEqual ("Galaxy S24 FE", AdbRunner.FormatDisplayName ("Galaxy_S24_FE")); + } + + [Test] + public void FormatDisplayName_PreservesNumbersAndSpecialChars () + { + Assert.AreEqual ("Pixel 7-Pro API 35", AdbRunner.FormatDisplayName ("pixel_7-pro_api_35")); + } + + [Test] + public void FormatDisplayName_HandlesEmptyString () + { + Assert.AreEqual ("", AdbRunner.FormatDisplayName ("")); + } + + [Test] + public void FormatDisplayName_HandlesSingleWord () + { + Assert.AreEqual ("Pixel", AdbRunner.FormatDisplayName ("pixel")); + } + + [Test] + public void FormatDisplayName_DoesNotReplaceApiInsideWords () + { + Assert.AreEqual ("Erapidevice", AdbRunner.FormatDisplayName ("erapidevice")); + } + + // Consumer: ParseAdbDevicesOutput (internal mapping), public for custom consumers + + [Test] + public void MapAdbStateToStatus_AllStates () + { + Assert.AreEqual (AdbDeviceStatus.Online, AdbRunner.MapAdbStateToStatus ("device")); + Assert.AreEqual (AdbDeviceStatus.Offline, AdbRunner.MapAdbStateToStatus ("offline")); + Assert.AreEqual (AdbDeviceStatus.Unauthorized, AdbRunner.MapAdbStateToStatus ("unauthorized")); + Assert.AreEqual (AdbDeviceStatus.NoPermissions, AdbRunner.MapAdbStateToStatus ("no permissions")); + Assert.AreEqual (AdbDeviceStatus.Unknown, AdbRunner.MapAdbStateToStatus ("something-else")); + } + + // Consumer: dotnet/android GetAvailableAndroidDevices.cs + + [Test] + public void MergeDevicesAndEmulators_NoEmulators_ReturnsAdbDevicesOnly () + { + var adbDevices = new List { + new AdbDeviceInfo { Serial = "0A041FDD400327", Description = "Pixel 5", Type = AdbDeviceType.Device, Status = AdbDeviceStatus.Online }, + }; + + var result = AdbRunner.MergeDevicesAndEmulators (adbDevices, new List ()); + + Assert.AreEqual (1, result.Count); + Assert.AreEqual ("0A041FDD400327", result [0].Serial); + } + + [Test] + public void MergeDevicesAndEmulators_NoRunningEmulators_AddsAllAvailable () + { + var adbDevices = new List { + new AdbDeviceInfo { Serial = "0A041FDD400327", Description = "Pixel 5", Type = AdbDeviceType.Device, Status = AdbDeviceStatus.Online }, + }; + var available = new List { "pixel_7_api_35", "pixel_9_api_36" }; + + var result = AdbRunner.MergeDevicesAndEmulators (adbDevices, available); + + Assert.AreEqual (3, result.Count); + + // Online first + Assert.AreEqual ("0A041FDD400327", result [0].Serial); + + // Non-running sorted alphabetically + Assert.AreEqual ("pixel_7_api_35", result [1].Serial); + Assert.AreEqual (AdbDeviceStatus.NotRunning, result [1].Status); + Assert.AreEqual ("pixel_7_api_35", result [1].AvdName); + Assert.AreEqual ("Pixel 7 API 35 (Not Running)", result [1].Description); + + Assert.AreEqual ("pixel_9_api_36", result [2].Serial); + Assert.AreEqual (AdbDeviceStatus.NotRunning, result [2].Status); + Assert.AreEqual ("Pixel 9 API 36 (Not Running)", result [2].Description); + } + + [Test] + public void MergeDevicesAndEmulators_RunningEmulator_NoDuplicate () + { + var adbDevices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", Description = "Pixel 7 API 35", + Type = AdbDeviceType.Emulator, Status = AdbDeviceStatus.Online, + AvdName = "pixel_7_api_35" + }, + }; + var available = new List { "pixel_7_api_35" }; + + var result = AdbRunner.MergeDevicesAndEmulators (adbDevices, available); + + Assert.AreEqual (1, result.Count, "Should not duplicate running emulator"); + Assert.AreEqual ("emulator-5554", result [0].Serial); + Assert.AreEqual (AdbDeviceStatus.Online, result [0].Status); + } + + [Test] + public void MergeDevicesAndEmulators_MixedRunningAndNotRunning () + { + var adbDevices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", Description = "Pixel 7 API 35", + Type = AdbDeviceType.Emulator, Status = AdbDeviceStatus.Online, + AvdName = "pixel_7_api_35" + }, + new AdbDeviceInfo { + Serial = "0A041FDD400327", Description = "Pixel 5", + Type = AdbDeviceType.Device, Status = AdbDeviceStatus.Online, + }, + }; + var available = new List { "pixel_7_api_35", "pixel_9_api_36", "nexus_5_api_30" }; + + var result = AdbRunner.MergeDevicesAndEmulators (adbDevices, available); + + Assert.AreEqual (4, result.Count); + + // Online devices first, sorted alphabetically + Assert.AreEqual ("0A041FDD400327", result [0].Serial); + Assert.AreEqual (AdbDeviceStatus.Online, result [0].Status); + + Assert.AreEqual ("emulator-5554", result [1].Serial); + Assert.AreEqual (AdbDeviceStatus.Online, result [1].Status); + + // Non-running emulators second, sorted alphabetically + Assert.AreEqual ("nexus_5_api_30", result [2].Serial); + Assert.AreEqual (AdbDeviceStatus.NotRunning, result [2].Status); + Assert.AreEqual ("Nexus 5 API 30 (Not Running)", result [2].Description); + + Assert.AreEqual ("pixel_9_api_36", result [3].Serial); + Assert.AreEqual (AdbDeviceStatus.NotRunning, result [3].Status); + } + + [Test] + public void MergeDevicesAndEmulators_CaseInsensitiveAvdNameMatching () + { + var adbDevices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", Description = "Pixel 7 API 35", + Type = AdbDeviceType.Emulator, Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35" + }, + }; + var available = new List { "pixel_7_api_35" }; // lowercase + + var result = AdbRunner.MergeDevicesAndEmulators (adbDevices, available); + + Assert.AreEqual (1, result.Count, "Should match AVD names case-insensitively"); + } + + [Test] + public void MergeDevicesAndEmulators_EmptyAdbDevices_ReturnsAllAvailable () + { + var result = AdbRunner.MergeDevicesAndEmulators (new List (), new List { "pixel_7_api_35", "pixel_9_api_36" }); + + Assert.AreEqual (2, result.Count); + Assert.AreEqual ("Pixel 7 API 35 (Not Running)", result [0].Description); + Assert.AreEqual ("Pixel 9 API 36 (Not Running)", result [1].Description); + } + + // Consumer: MAUI DevTools Adb provider (AdbPath, IsAvailable properties) + + [Test] + public void Constructor_NullPath_ThrowsArgumentException () + { + Assert.Throws (() => new AdbRunner (null!)); + } + + [Test] + public void Constructor_EmptyPath_ThrowsArgumentException () + { + Assert.Throws (() => new AdbRunner ("")); + } + + [Test] + public void Constructor_WhitespacePath_ThrowsArgumentException () + { + Assert.Throws (() => new AdbRunner (" ")); + } + + [Test] + public void ParseAdbDevicesOutput_DeviceWithProductOnly () + { + var output = + "List of devices attached\n" + + "emulator-5554 device product:aosp_x86_64\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("aosp_x86_64", devices [0].Product); + Assert.AreEqual (AdbDeviceType.Emulator, devices [0].Type); + } + + [Test] + public void ParseAdbDevicesOutput_MultipleDevices () + { + var output = + "List of devices attached\n" + + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1\n" + + "emulator-5556 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64x transport_id:3\n" + + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (3, devices.Count); + Assert.AreEqual ("emulator-5554", devices [0].Serial); + Assert.AreEqual (AdbDeviceType.Emulator, devices [0].Type); + Assert.AreEqual ("emulator-5556", devices [1].Serial); + Assert.AreEqual (AdbDeviceType.Emulator, devices [1].Type); + Assert.AreEqual ("0A041FDD400327", devices [2].Serial); + Assert.AreEqual (AdbDeviceType.Device, devices [2].Type); + Assert.AreEqual ("Pixel_6_Pro", devices [2].Model); + } + + [Test] + public void MergeDevicesAndEmulators_AllEmulatorsRunning_NoDuplicate () + { + var emulator1 = new AdbDeviceInfo { + Serial = "emulator-5554", Description = "Pixel 7 API 35", + Type = AdbDeviceType.Emulator, Status = AdbDeviceStatus.Online, + AvdName = "pixel_7_api_35", + }; + var emulator2 = new AdbDeviceInfo { + Serial = "emulator-5556", Description = "Pixel 9 API 36", + Type = AdbDeviceType.Emulator, Status = AdbDeviceStatus.Online, + AvdName = "pixel_9_api_36", + }; + + var result = AdbRunner.MergeDevicesAndEmulators ( + new List { emulator1, emulator2 }, + new List { "pixel_7_api_35", "pixel_9_api_36" }); + + Assert.AreEqual (2, result.Count, "Should not add duplicates when all emulators are running"); + Assert.IsTrue (result.All (d => d.Status == AdbDeviceStatus.Online)); + } + + [Test] + public void MergeDevicesAndEmulators_NonRunningEmulatorHasFormattedDescription () + { + var result = AdbRunner.MergeDevicesAndEmulators ( + new List (), + new List { "pixel_7_pro_api_35" }); + + Assert.AreEqual (1, result.Count); + Assert.AreEqual ("Pixel 7 Pro API 35 (Not Running)", result [0].Description); + Assert.AreEqual (AdbDeviceStatus.NotRunning, result [0].Status); + } + + [Test] + public void ParseAdbDevicesOutput_RecoveryState () + { + var output = "List of devices attached\n" + + "0A041FDD400327 recovery product:redfin model:Pixel_5 device:redfin transport_id:2\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("0A041FDD400327", devices [0].Serial); + Assert.AreEqual (AdbDeviceStatus.Unknown, devices [0].Status); + } + + [Test] + public void ParseAdbDevicesOutput_SideloadState () + { + var output = "List of devices attached\n" + + "0A041FDD400327 sideload\n"; + + var devices = AdbRunner.ParseAdbDevicesOutput (output.Split ('\n')); + + Assert.AreEqual (1, devices.Count); + Assert.AreEqual ("0A041FDD400327", devices [0].Serial); + Assert.AreEqual (AdbDeviceStatus.Unknown, devices [0].Status); + } + + [Test] + public void MapAdbStateToStatus_Recovery_ReturnsUnknown () + { + Assert.AreEqual (AdbDeviceStatus.Unknown, AdbRunner.MapAdbStateToStatus ("recovery")); + } + + [Test] + public void MapAdbStateToStatus_Sideload_ReturnsUnknown () + { + Assert.AreEqual (AdbDeviceStatus.Unknown, AdbRunner.MapAdbStateToStatus ("sideload")); + } + + // Consumer: MAUI DevTools Adb provider (WaitForDeviceAsync) + + [Test] + public void WaitForDeviceAsync_NegativeTimeout_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.WaitForDeviceAsync (timeout: System.TimeSpan.FromSeconds (-1))); + } + + [Test] + public void WaitForDeviceAsync_ZeroTimeout_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.WaitForDeviceAsync (timeout: System.TimeSpan.Zero)); + } + + [Test] + public void FirstNonEmptyLine_ReturnsFirstLine () + { + Assert.AreEqual ("hello", AdbRunner.FirstNonEmptyLine ("hello\nworld\n")); + } + + [Test] + public void FirstNonEmptyLine_SkipsBlankLines () + { + Assert.AreEqual ("hello", AdbRunner.FirstNonEmptyLine ("\n\nhello\nworld\n")); + } + + [Test] + public void FirstNonEmptyLine_TrimsWhitespace () + { + Assert.AreEqual ("hello", AdbRunner.FirstNonEmptyLine (" hello \n")); + } + + [Test] + public void FirstNonEmptyLine_HandlesWindowsNewlines () + { + Assert.AreEqual ("hello", AdbRunner.FirstNonEmptyLine ("\r\nhello\r\nworld\r\n")); + } + + [Test] + public void FirstNonEmptyLine_EmptyString_ReturnsNull () + { + Assert.IsNull (AdbRunner.FirstNonEmptyLine ("")); + } + + [Test] + public void FirstNonEmptyLine_OnlyNewlines_ReturnsNull () + { + Assert.IsNull (AdbRunner.FirstNonEmptyLine ("\n\n\n")); + } + + [Test] + public void FirstNonEmptyLine_SingleValue_ReturnsIt () + { + Assert.AreEqual ("1", AdbRunner.FirstNonEmptyLine ("1\n")); + } + + [Test] + public void FirstNonEmptyLine_TypicalGetpropOutput () + { + // adb shell getprop sys.boot_completed returns "1\n" or "1\r\n" + Assert.AreEqual ("1", AdbRunner.FirstNonEmptyLine ("1\r\n")); + } + + [Test] + public void FirstNonEmptyLine_PmPathOutput () + { + // adb shell pm path android returns "package:/system/framework/framework-res.apk\n" + var output = "package:/system/framework/framework-res.apk\n"; + Assert.AreEqual ("package:/system/framework/framework-res.apk", AdbRunner.FirstNonEmptyLine (output)); + } + + // Consumer: MAUI DevTools (via ListReversePortsAsync), vscode-maui ServiceHub replacement + + [Test] + public void ParseReverseListOutput_SingleRule () + { + var output = new [] { + "(reverse) tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Local.Protocol); + Assert.AreEqual (5000, rules [0].Local.Port); + } + + [Test] + public void ParseReverseListOutput_MultipleRules () + { + var output = new [] { + "(reverse) tcp:5000 tcp:5000", + "(reverse) tcp:8081 tcp:8081", + "(reverse) tcp:19000 tcp:19001", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (3, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + Assert.AreEqual (8081, rules [1].Local.Port); + Assert.AreEqual (19000, rules [2].Remote.Port); + Assert.AreEqual (19001, rules [2].Local.Port); + } + + [Test] + public void ParseReverseListOutput_EmptyOutput () + { + var output = new [] { "", " " }; + var rules = AdbRunner.ParseReverseListOutput (output); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_NoLines () + { + var rules = AdbRunner.ParseReverseListOutput (Array.Empty ()); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_IgnoresNonReverseLines () + { + var output = new [] { + "some random header", + "(reverse) tcp:5000 tcp:5000", + "* daemon started successfully", + "(reverse) tcp:8081 tcp:8081", + "", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + } + + [Test] + public void ParseReverseListOutput_MalformedLine_InsufficientParts () + { + var output = new [] { + "(reverse) tcp:5000", // missing local spec + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_DifferentRemoteAndLocalPorts () + { + var output = new [] { + "(reverse) tcp:8080 tcp:3000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (8080, rules [0].Remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Local.Protocol); + Assert.AreEqual (3000, rules [0].Local.Port); + } + + [Test] + public void ParseReverseListOutput_NonTcpSpecs_SkipsUnparseable () + { + var output = new [] { + "(reverse) localabstract:chrome_devtools_remote tcp:9222", + "(reverse) tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + // localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); + } + + [Test] + public void ParseReverseListOutput_WindowsLineEndings () + { + // Simulate \r\n line endings (split on \n leaves trailing \r) + var output = new [] { + "(reverse) tcp:5000 tcp:5000\r", + "(reverse) tcp:8081 tcp:8081\r", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + } + + [Test] + public void ParseReverseListOutput_TabSeparated () + { + var output = new [] { + "(reverse)\ttcp:5000\ttcp:5000", + "(reverse)\ttcp:8081\ttcp:8081", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + } + + // Consumer: MAUI DevTools (via ListForwardPortsAsync — host→device tunnel for JDWP debugger + // attach, perf endpoints, host-side DevFlow agent connect), vscode-maui ServiceHub replacement. + // Output format differs from reverse: "(reverse) " → " ". + + [Test] + public void ParseForwardListOutput_SingleRule () + { + var output = new [] { + "emulator-5554 tcp:5000 tcp:6000", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Local.Protocol); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (6000, rules [0].Remote.Port); + } + + [Test] + public void ParseForwardListOutput_MultipleRulesSameDevice () + { + var output = new [] { + "emulator-5554 tcp:5000 tcp:6000", + "emulator-5554 tcp:8081 tcp:8081", + "emulator-5554 tcp:9222 tcp:9223", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + Assert.AreEqual (3, rules.Count); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (6000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Local.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + Assert.AreEqual (9222, rules [2].Local.Port); + Assert.AreEqual (9223, rules [2].Remote.Port); + } + + [Test] + public void ParseForwardListOutput_FiltersByDeviceSerial () + { + // adb forward --list returns rules across ALL devices; we must filter. + var output = new [] { + "emulator-5554 tcp:5000 tcp:5000", + "emulator-5556 tcp:5001 tcp:5001", + "emulator-5554 tcp:8081 tcp:8081", + "abcd1234device tcp:9000 tcp:9000", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (8081, rules [1].Local.Port); + } + + [Test] + public void ParseForwardListOutput_EmptyOutput () + { + var output = new [] { "", " " }; + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseForwardListOutput_NoLines () + { + var rules = AdbRunner.ParseForwardListOutput (Array.Empty (), "emulator-5554"); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseForwardListOutput_EmptySerial_ReturnsEmpty () + { + var output = new [] { "emulator-5554 tcp:5000 tcp:5000" }; + var rules = AdbRunner.ParseForwardListOutput (output, ""); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseForwardListOutput_NoMatchingDevice () + { + var output = new [] { + "emulator-5554 tcp:5000 tcp:5000", + "emulator-5556 tcp:5001 tcp:5001", + }; + var rules = AdbRunner.ParseForwardListOutput (output, "missing-device"); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseForwardListOutput_MalformedLine_InsufficientParts () + { + var output = new [] { + "emulator-5554 tcp:5000", // missing remote spec + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseForwardListOutput_NonTcpSpecs_SkipsUnparseable () + { + var output = new [] { + "emulator-5554 tcp:9222 localabstract:chrome_devtools_remote", + "emulator-5554 tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + // localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (5000, rules [0].Remote.Port); + } + + [Test] + public void ParseForwardListOutput_WindowsLineEndings () + { + var output = new [] { + "emulator-5554 tcp:5000 tcp:6000\r", + "emulator-5554 tcp:8081 tcp:8081\r", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (6000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Local.Port); + } + + [Test] + public void ParseForwardListOutput_TabSeparated () + { + var output = new [] { + "emulator-5554\ttcp:5000\ttcp:6000", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "emulator-5554"); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (6000, rules [0].Remote.Port); + } + + [Test] + public void ParseForwardListOutput_SerialMatch_IsCaseSensitive () + { + // adb device serials are case-sensitive. + var output = new [] { + "emulator-5554 tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseForwardListOutput (output, "EMULATOR-5554"); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void AdbPortSpec_TryParse_ValidTcp () + { + var spec = AdbPortSpec.TryParse ("tcp:5000"); + if (spec is null) { + Assert.Fail ("Expected non-null AdbPortSpec"); + return; + } + Assert.AreEqual (AdbProtocol.Tcp, spec.Protocol); + Assert.AreEqual (5000, spec.Port); + } + + [Test] + public void AdbPortSpec_TryParse_NonTcpProtocol_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("localabstract:9222")); + } + + [Test] + public void AdbPortSpec_TryParse_Null_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse (default)); + } + + [Test] + public void AdbPortSpec_TryParse_Empty_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("")); + } + + [Test] + public void AdbPortSpec_TryParse_NoColon_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp5000")); + } + + [Test] + public void AdbPortSpec_TryParse_NonNumericPort_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("localabstract:chrome_devtools_remote")); + } + + [Test] + public void AdbPortSpec_TryParse_ZeroPort_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp:0")); + } + + [Test] + public void AdbPortSpec_TryParse_PortAbove65535_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp:70000")); + } + + [Test] + public void AdbPortSpec_TryParse_UnknownProtocol_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("udp:5000")); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_Tcp () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 5000); + Assert.AreEqual ("tcp:5000", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_HighPort () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 65535); + Assert.AreEqual ("tcp:65535", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_LowPort () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 1); + Assert.AreEqual ("tcp:1", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_InvalidProtocol_Throws () + { + var spec = new AdbPortSpec ((AdbProtocol) 99, 5000); + Assert.Throws (() => spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToString_MatchesSocketSpec () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 8080); + Assert.AreEqual ("tcp:8080", spec.ToString ()); + } + + [Test] + public void AdbPortSpec_TryParse_Roundtrip () + { + var original = new AdbPortSpec (AdbProtocol.Tcp, 3000); + var parsed = AdbPortSpec.TryParse (original.ToSocketSpec ()); + Assert.AreEqual (original, parsed); + } + + [Test] + public void AdbPortRule_ValueEquality () + { + var rule1 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000)); + var rule2 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000)); + var rule3 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); + + Assert.AreEqual (rule1, rule2); + Assert.AreNotEqual (rule1, rule3); + Assert.IsTrue (rule1 == rule2); + Assert.IsFalse (rule1 == rule3); + } + + [Test] + public void AdbPortRule_Deconstruct () + { + var rule = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); + var (remote, local) = rule; + + Assert.AreEqual (AdbProtocol.Tcp, remote.Protocol); + Assert.AreEqual (5000, remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, local.Protocol); + Assert.AreEqual (3000, local.Port); + } + + [Test] + public void AdbPortRule_ToString () + { + var rule = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); + var str = rule.ToString (); + + Assert.That (str, Does.Contain ("tcp:5000")); + Assert.That (str, Does.Contain ("tcp:3000")); + } + + [Test] + public void ReversePortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void ReversePortAsync_NullRemote_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", (AdbPortSpec) null, new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void ReversePortAsync_NullLocal_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", new AdbPortSpec (AdbProtocol.Tcp, 5000), (AdbPortSpec) null)); + } + + [Test] + public void RemoveReversePortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void RemoveReversePortAsync_NullRemote_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("emulator-5554", (AdbPortSpec) null)); + } + + [Test] + public void RemoveAllReversePortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveAllReversePortsAsync ("")); + } + + [Test] + public void ListReversePortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ListReversePortsAsync ("")); + } + + [Test] + public void ForwardPortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ForwardPortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void ForwardPortAsync_NullLocal_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ForwardPortAsync ("emulator-5554", (AdbPortSpec) null, new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void ForwardPortAsync_NullRemote_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ForwardPortAsync ("emulator-5554", new AdbPortSpec (AdbProtocol.Tcp, 5000), (AdbPortSpec) null)); + } + + [Test] + public void RemoveForwardPortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveForwardPortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void RemoveForwardPortAsync_NullLocal_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveForwardPortAsync ("emulator-5554", (AdbPortSpec) null)); + } + + [Test] + public void RemoveAllForwardPortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveAllForwardPortsAsync ("")); + } + + [Test] + public async Task RemoveAllForwardPortsAsync_RemovesOnlyPortsForGivenSerial () + { + var listedSerials = new List (); + var removed = new List<(string Serial, AdbPortSpec Local)> (); + var runner = new RecordingAdbRunner ( + listForwards: (serial, _) => { + listedSerials.Add (serial); + return Task.FromResult> (new [] { + new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 6000), new AdbPortSpec (AdbProtocol.Tcp, 5000)), + new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 6001), new AdbPortSpec (AdbProtocol.Tcp, 5001)), + }); + }, + removeForward: (serial, local, _) => { + removed.Add ((serial, local)); + return Task.CompletedTask; + }); + + await runner.RemoveAllForwardPortsAsync ("emulator-5554"); + + Assert.AreEqual (new [] { "emulator-5554" }, listedSerials, "ListForwardPortsAsync should be called exactly once with the target serial."); + Assert.AreEqual (2, removed.Count, "Both listed rules should be removed."); + Assert.That (removed.Select (r => r.Serial), Is.All.EqualTo ("emulator-5554"), "Removes must target only the requested serial."); + Assert.AreEqual (5000, removed [0].Local.Port); + Assert.AreEqual (5001, removed [1].Local.Port); + } + + [Test] + public async Task RemoveAllForwardPortsAsync_EmptyList_IsNoOp () + { + var removed = new List<(string Serial, AdbPortSpec Local)> (); + var runner = new RecordingAdbRunner ( + listForwards: (_, __) => Task.FromResult> (Array.Empty ()), + removeForward: (serial, local, _) => { + removed.Add ((serial, local)); + return Task.CompletedTask; + }); + + await runner.RemoveAllForwardPortsAsync ("emulator-5554"); + + Assert.IsEmpty (removed, "No removes should be issued when the listing is empty."); + } + + sealed class RecordingAdbRunner : AdbRunner + { + readonly Func>> listForwards; + readonly Func removeForward; + + public RecordingAdbRunner ( + Func>> listForwards, + Func removeForward) + : base ("/fake/sdk/platform-tools/adb") + { + this.listForwards = listForwards; + this.removeForward = removeForward; + } + + public override Task> ListForwardPortsAsync (string serial, System.Threading.CancellationToken cancellationToken = default) + => listForwards (serial, cancellationToken); + + public override Task RemoveForwardPortAsync (string serial, AdbPortSpec local, System.Threading.CancellationToken cancellationToken = default) + => removeForward (serial, local, cancellationToken); + } + + [Test] + public void ListForwardPortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ListForwardPortsAsync ("")); + } + + // These tests use a fake 'adb' script to control process output, + // verifying AVD detection order and offline emulator handling. + + static string CreateFakeAdb (string scriptBody) + { + if (OS.IsWindows) + Assert.Ignore ("Fake adb tests use bash scripts and are not supported on Windows."); + + var dir = Path.Combine (Path.GetTempPath (), $"fake-adb-{Guid.NewGuid ():N}"); + Directory.CreateDirectory (dir); + var path = Path.Combine (dir, "adb"); + File.WriteAllText (path, "#!/bin/bash\n" + scriptBody); + FileUtil.Chmod (path, 0x1ED); // 0755 + + return path; + } + + static void CleanupFakeAdb (string adbPath) + { + var dir = Path.GetDirectoryName (adbPath); + if (dir is { Length: > 0 }) { + File.Delete (adbPath); + Directory.Delete (dir); + } + } + + [Test] + public async Task GetEmulatorAvdNameAsync_PrefersGetprop () + { + // getprop returns a value — should be used, emu avd name should NOT be needed + var adbPath = CreateFakeAdb (""" + if [[ "$3" == "shell" && "$4" == "getprop" ]]; then + echo "My_AVD_Name" + exit 0 + fi + if [[ "$3" == "emu" ]]; then + echo "WRONG_NAME" + echo "OK" + exit 0 + fi + exit 1 + """); + + try { + var runner = new AdbRunner (adbPath); + var name = await runner.GetEmulatorAvdNameAsync ("emulator-5554"); + Assert.AreEqual ("My_AVD_Name", name, "Should return getprop result"); + } finally { + CleanupFakeAdb (adbPath); + } + } + + [Test] + public async Task GetEmulatorAvdNameAsync_FallsBackToEmuAvdName () + { + // getprop returns empty — should fall back to emu avd name + var adbPath = CreateFakeAdb (""" + if [[ "$3" == "shell" && "$4" == "getprop" ]]; then + echo "" + exit 0 + fi + if [[ "$3" == "emu" ]]; then + echo "Fallback_AVD" + echo "OK" + exit 0 + fi + exit 1 + """); + + try { + var runner = new AdbRunner (adbPath); + var name = await runner.GetEmulatorAvdNameAsync ("emulator-5554"); + Assert.AreEqual ("Fallback_AVD", name, "Should fall back to emu avd name"); + } finally { + CleanupFakeAdb (adbPath); + } + } + + [Test] + public async Task GetEmulatorAvdNameAsync_BothFail_ReturnsNull () + { + // Both getprop and emu avd name return empty + var adbPath = CreateFakeAdb (""" + echo "" + exit 0 + """); + + try { + var runner = new AdbRunner (adbPath); + var name = await runner.GetEmulatorAvdNameAsync ("emulator-5554"); + Assert.IsNull (name, "Should return null when both methods fail"); + } finally { + CleanupFakeAdb (adbPath); + } + } + + [Test] + public async Task ListDevicesAsync_SkipsAvdQueryForOfflineEmulators () + { + // adb devices returns one online emulator and one offline emulator. + // Only the online one should get an AVD name query. + var adbPath = CreateFakeAdb (""" + if [[ "$1" == "devices" ]]; then + echo "List of devices attached" + echo "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1" + echo "emulator-5556 offline" + exit 0 + fi + if [[ "$1" == "-s" && "$2" == "emulator-5554" && "$3" == "shell" && "$4" == "getprop" ]]; then + echo "Online_AVD" + exit 0 + fi + if [[ "$1" == "-s" && "$2" == "emulator-5556" ]]; then + # This should NOT be called for offline emulators. + # Return a name anyway so we can detect if it was incorrectly queried. + echo "OFFLINE_SHOULD_NOT_APPEAR" + exit 0 + fi + exit 1 + """); + + try { + var runner = new AdbRunner (adbPath); + var devices = await runner.ListDevicesAsync (); + + Assert.AreEqual (2, devices.Count, "Should return both emulators"); + + var online = devices.First (d => d.Serial == "emulator-5554"); + var offline = devices.First (d => d.Serial == "emulator-5556"); + + Assert.AreEqual (AdbDeviceStatus.Online, online.Status); + Assert.AreEqual ("Online_AVD", online.AvdName, "Online emulator should have AVD name"); + + Assert.AreEqual (AdbDeviceStatus.Offline, offline.Status); + Assert.IsNull (offline.AvdName, "Offline emulator should NOT have AVD name queried"); + } finally { + CleanupFakeAdb (adbPath); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidAppManifestTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidAppManifestTests.cs new file mode 100644 index 00000000000..92e81dbe0b6 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidAppManifestTests.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidAppManifestTests + { + [Test] + public void Load () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + Assert.Throws (() => AndroidAppManifest.Load ((string) null, versions)); + Assert.Throws (() => AndroidAppManifest.Load ("filename", null)); + Assert.Throws (() => AndroidAppManifest.Load ((XDocument) null, versions)); + Assert.Throws (() => AndroidAppManifest.Load (GetTestAppManifest (), null)); + + Assert.Throws (() => AndroidAppManifest.Load (XDocument.Parse (""), versions)); + } + + [Test] + public void ParsePermissions () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + var manifest = AndroidAppManifest.Load (GetTestAppManifest (), versions); + var permissions = manifest.AndroidPermissions.ToArray (); + Assert.AreEqual (3, permissions.Length, "#1"); + Assert.IsTrue (permissions.Contains ("INTERNET"), "#2"); + Assert.IsTrue (permissions.Contains ("READ_CONTACTS"), "#3"); + Assert.IsTrue (permissions.Contains ("WRITE_CONTACTS"), "#4"); + } + + static XDocument GetTestAppManifest () + { + using (var xml = typeof (AndroidAppManifestTests).Assembly.GetManifestResourceStream ("manifest-simplewidget.xml")) { + return XDocument.Load (xml); + } + } + + [Test] + public void GetLaunchableActivityNames () + { + var versions = new AndroidVersions (Array.Empty()); + var manifest = AndroidAppManifest.Load (GetTestAppManifest (), versions); + var launchers = manifest.GetLaunchableActivityNames ().ToList (); + Assert.AreEqual (2, launchers.Count); + Assert.AreEqual (".HasMultipleIntentFilters", launchers [0]); + Assert.AreEqual (".ActivityAlias", launchers [1]); + } + + [Test] + public void SetNewPermissions () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + var manifest = AndroidAppManifest.Load (GetTestAppManifest (), versions); + manifest.SetAndroidPermissions (new [] { "FOO" }); + + var sb = new StringBuilder (); + using (var writer = XmlWriter.Create (sb)) { + manifest.Write (writer); + } + + manifest = AndroidAppManifest.Load (XDocument.Parse (sb.ToString ()), versions); + Assert.AreEqual (1, manifest.AndroidPermissions.Count (), "#1"); + Assert.AreEqual ("FOO", manifest.AndroidPermissions.ElementAt (0)); + } + + [Test] + public void CanonicalizePackageName () + { + Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName (null)); + Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName ("")); + Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName (" ")); + + Assert.AreEqual ("A.A", + AndroidAppManifest.CanonicalizePackageName ("A")); + Assert.AreEqual ("Foo.Bar", + AndroidAppManifest.CanonicalizePackageName ("Foo.Bar")); + Assert.AreEqual ("foo_bar.foo_bar", + AndroidAppManifest.CanonicalizePackageName ("foo-bar")); + Assert.AreEqual ("x1.x1", + AndroidAppManifest.CanonicalizePackageName ("1")); + Assert.AreEqual ("x_1.x_2", + AndroidAppManifest.CanonicalizePackageName ("_1._2")); + Assert.AreEqual ("mfa1.x0.x2_2", + AndroidAppManifest.CanonicalizePackageName ("mfa1.0.2_2")); + Assert.AreEqual ("My.Cool_Assembly", + AndroidAppManifest.CanonicalizePackageName ("My.Cool Assembly")); + Assert.AreEqual ("x7Cats.x7Cats", + AndroidAppManifest.CanonicalizePackageName ("7Cats")); + } + + [Test] + public void CanParseNonNumericSdkVersion () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + var doc = XDocument.Parse (@" + + + + + "); + var manifest = AndroidAppManifest.Load (doc, versions); + + var mininum = manifest.MinSdkVersion; + var target = manifest.TargetSdkVersion; + + Assert.IsTrue (mininum.HasValue); + Assert.IsTrue (target.HasValue); + Assert.AreEqual (21, mininum.Value); + Assert.AreEqual (21, target.Value); + } + + [Test] + public void EnsureMinAndTargetSdkVersionsAreReadIndependently () + { + // Regression test for https://bugzilla.xamarin.com/show_bug.cgi?id=21296 + var versions = new AndroidVersions (new AndroidVersion [0]); + var doc = XDocument.Parse (@" + + + + + "); + var manifest = AndroidAppManifest.Load (doc, versions); + + var mininum = manifest.MinSdkVersion; + var target = manifest.TargetSdkVersion; + + Assert.IsTrue (mininum.HasValue); + Assert.IsTrue (target.HasValue); + Assert.AreEqual (8, mininum.Value); + Assert.AreEqual (12, target.Value); + } + + [Test] + public void EnsureUsesPermissionElementOrder () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + var manifest = AndroidAppManifest.Create ("com.xamarin.test", "Xamarin Test", versions); + manifest.SetAndroidPermissions (new string[] { "FOO" }); + var sb = new StringBuilder (); + using (var writer = XmlWriter.Create (sb)) { + manifest.Write (writer); + } + + var doc = XDocument.Parse (sb.ToString ()); + var app = doc.Element ("manifest").Element ("application"); + Assert.IsNotNull (app, "Application element should exist"); + Assert.IsFalse (app.ElementsAfterSelf ().Any (x => x.Name == "uses-permission")); + Assert.IsTrue (app.ElementsBeforeSelf ().Any (x => x.Name == "uses-permission")); + } + + [Test] + public void CanGetAppTheme () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + var doc = XDocument.Parse (@" + + + + + "); + var manifest = AndroidAppManifest.Load (doc, versions); + + Assert.AreEqual ("@android:style/Theme.Material.Light", manifest.ApplicationTheme); + } + + [Test] + public void CanAddAndRemoveUsesSdk () + { + XNamespace aNS = "http://schemas.android.com/apk/res/android"; + var versions = new AndroidVersions (new AndroidVersion [0]); + var doc = XDocument.Parse (@" + + + + + "); + var manifest = AndroidAppManifest.Load (doc, versions); + + manifest.MinSdkVersion = null; + manifest.TargetSdkVersion = null; + + var sb = new StringBuilder (); + using (var writer = XmlWriter.Create (sb)) { + manifest.Write (writer); + } + + var newDoc = XDocument.Parse (sb.ToString ()); + var usesSdk = newDoc.Element ("manifest").Element ("uses-sdk"); + Assert.IsNull (usesSdk, "uses-sdk should not exist"); + + manifest.MinSdkVersion = 8; + manifest.TargetSdkVersion = 12; + + sb = new StringBuilder (); + using (var writer = XmlWriter.Create (sb)) { + manifest.Write (writer); + } + + newDoc = XDocument.Parse (sb.ToString ()); + usesSdk = newDoc.Element ("manifest").Element ("uses-sdk"); + Assert.IsNotNull (usesSdk, "uses-sdk should exist"); + Assert.AreEqual ("8", usesSdk.Attribute (aNS + "minSdkVersion").Value); + Assert.AreEqual ("12", usesSdk.Attribute (aNS + "targetSdkVersion").Value); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkInfoTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkInfoTests.cs new file mode 100644 index 00000000000..9461f1cde33 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkInfoTests.cs @@ -0,0 +1,708 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidSdkInfoTests + { + const string NdkVersion = "21.0.6113669"; + + string UnixConfigDirOverridePath; + string PreferredJdksOverridePath; + + static readonly string GetMacOSMicrosoftJdkPathsOverrideName = $"GetMacOSMicrosoftJdkPaths jdks override! {typeof (JdkInfo).AssemblyQualifiedName}"; + static readonly string GetUnixConfigDirOverrideName = $"UnixConfigPath directory override! {typeof (AndroidSdkInfo).AssemblyQualifiedName}"; + + [OneTimeSetUp] + public void FixtureSetUp () + { + UnixConfigDirOverridePath = Path.GetTempFileName (); + File.Delete (UnixConfigDirOverridePath); + Directory.CreateDirectory (UnixConfigDirOverridePath); + AppDomain.CurrentDomain.SetData (GetUnixConfigDirOverrideName, UnixConfigDirOverridePath); + + PreferredJdksOverridePath = Path.GetTempFileName (); + File.Delete (PreferredJdksOverridePath); + Directory.CreateDirectory (PreferredJdksOverridePath); + AppDomain.CurrentDomain.SetData (GetMacOSMicrosoftJdkPathsOverrideName, PreferredJdksOverridePath); + } + + [OneTimeTearDown] + public void FixtureTearDown () + { + AppDomain.CurrentDomain.SetData (GetMacOSMicrosoftJdkPathsOverrideName, null); + AppDomain.CurrentDomain.SetData (GetUnixConfigDirOverrideName, null); + Directory.Delete (UnixConfigDirOverridePath, recursive: true); + Directory.Delete (PreferredJdksOverridePath, recursive: true); + } + + [Test] + public void Constructor_Paths () + { + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + var logs = new StringWriter (); + Action logger = (level, message) => { + logs.WriteLine ($"[{level}] {message}"); + }; + + try { + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: ndk, javaSdkPath: jdk); + + Assert.AreEqual (ndk, info.AndroidNdkPath, "AndroidNdkPath not preserved!"); + Assert.AreEqual (sdk, info.AndroidSdkPath, "AndroidSdkPath not preserved!"); + Assert.AreEqual (jdk, info.JavaSdkPath, "JavaSdkPath not preserved!"); + } + finally { + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Ndk_MultipleNdkVersionsInSdk () + { + // Must match like-named constants in AndroidSdkBase + const int MinimumCompatibleNDKMajorVersion = 16; + const int MaximumCompatibleNDKMajorVersion = 28; + + CreateSdks(out string root, out string jdk, out string ndk, out string sdk); + + Action logger = (level, message) => { + Console.WriteLine($"[{level}] {message}"); + }; + + var ndkVersions = new List { + "16.1.4479499", + "17.2.4988734", + "18.1.5063045", + "19.2.5345600", + "20.0.5594570", + "20.1.5948944", + "21.0.6113669", + "21.1.6352462", + "21.2.6472646", + "21.3.6528147", + "22.0.7026061", + "22.1.7171670", + "23.1.7779620", + "24.0.8215888", + "25.0.8775105", + "26.0.10792818", + "27.0.11718014", + "28.0.12433566", + "29.0.3735928559", // 0xdeadbeef + }; + string expectedVersion = ndkVersions [^2]; // Second to last is the highest compatible version + string expectedNdkPath = Path.Combine (sdk, "ndk", expectedVersion); + + try { + MakeNdkDir (Path.Combine (sdk, "ndk-bundle"), NdkVersion); + + foreach (string ndkVer in ndkVersions) { + MakeNdkDir (Path.Combine (sdk, "ndk", ndkVer), ndkVer); + } + + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk); + + Assert.AreEqual (expectedNdkPath, info.AndroidNdkPath, "AndroidNdkPath not found inside sdk!"); + + string ndkVersion = Path.GetFileName (info.AndroidNdkPath); + if (!Version.TryParse (ndkVersion, out Version ver)) { + Assert.Fail ($"Unable to parse '{ndkVersion}' as a valid version."); + } + + Assert.True (ver.Major >= MinimumCompatibleNDKMajorVersion, $"NDK version must be at least {MinimumCompatibleNDKMajorVersion}"); + Assert.True (ver.Major <= MaximumCompatibleNDKMajorVersion, $"NDK version must be at most {MinimumCompatibleNDKMajorVersion}"); + } finally { + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Ndk_PathInSdk() + { + CreateSdks(out string root, out string jdk, out string ndk, out string sdk); + + Action logger = (level, message) => { + Console.WriteLine($"[{level}] {message}"); + }; + + try + { + var extension = OS.IsWindows ? ".cmd" : ""; + var ndkPath = Path.Combine(sdk, "ndk-bundle"); + Directory.CreateDirectory(ndkPath); + File.WriteAllText(Path.Combine (ndkPath, "source.properties"), $"Pkg.Revision = {NdkVersion}"); + Directory.CreateDirectory(Path.Combine(ndkPath, "toolchains")); + File.WriteAllText(Path.Combine(ndkPath, $"ndk-stack{extension}"), ""); + + var info = new AndroidSdkInfo(logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk); + + Assert.AreEqual(ndkPath, info.AndroidNdkPath, "AndroidNdkPath not found inside sdk!"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public void Ndk_Path_InvalidChars () + { + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + if (level == TraceLevel.Error) + Assert.Fail (message); + }; + + var oldPath = Environment.GetEnvironmentVariable ("PATH"); + try { + Environment.SetEnvironmentVariable ("PATH", "\"C:\\IHAVEQUOTES\\\""); + // Check that this doesn't throw + new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk); + } finally { + Environment.SetEnvironmentVariable ("PATH", oldPath); + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Ndk_PathExt_InvalidChars () + { + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + if (level == TraceLevel.Error) + Assert.Fail (message); + }; + + var oldPathExt = Environment.GetEnvironmentVariable ("PATHEXT"); + try { + Environment.SetEnvironmentVariable ("PATHEXT", string.Join (Path.PathSeparator.ToString (), "\"", ".EXE", ".BAT")); + // Check that this doesn't throw + new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk); + } finally { + Environment.SetEnvironmentVariable ("PATHEXT", oldPathExt); + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Ndk_AndroidSdkDoesNotExist () + { + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + if (level == TraceLevel.Error) + Assert.Fail (message); + }; + + var oldAndroidHome = Environment.GetEnvironmentVariable ("ANDROID_HOME"); + try { + Environment.SetEnvironmentVariable ("ANDROID_HOME", "/i/dont/exist"); + // Check that this doesn't throw + new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk); + } finally { + Environment.SetEnvironmentVariable ("ANDROID_HOME", oldAndroidHome); + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Constructor_SetValuesFromPath () + { + if (OS.IsWindows) + Assert.Ignore ("Windows does not look for values in %PATH%"); + + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + JdkInfoTests.CreateFauxJdk (jdk, releaseVersion: "1.8.0", releaseBuildNumber: "42", javaVersion: "100.100.100_100"); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + }; + var oldPath = Environment.GetEnvironmentVariable ("PATH"); + var oldJavaHome = Environment.GetEnvironmentVariable ("JAVA_HOME"); + var oldAndroidHome = Environment.GetEnvironmentVariable ("ANDROID_HOME"); + var oldAndroidSdkRoot = Environment.GetEnvironmentVariable ("ANDROID_SDK_ROOT"); + try { + var paths = new List () { + Path.Combine (jdk, "bin"), + ndk, + Path.Combine (sdk, "platform-tools"), + }; + paths.AddRange (oldPath.Split (new[]{Path.PathSeparator}, StringSplitOptions.RemoveEmptyEntries)); + Environment.SetEnvironmentVariable ("PATH", string.Join (Path.PathSeparator.ToString (), paths)); + if (!string.IsNullOrEmpty (oldJavaHome)) { + Environment.SetEnvironmentVariable ("JAVA_HOME", string.Empty); + } + if (!string.IsNullOrEmpty (oldAndroidHome)) { + Environment.SetEnvironmentVariable ("ANDROID_HOME", string.Empty); + } + if (!string.IsNullOrEmpty (oldAndroidSdkRoot)) { + Environment.SetEnvironmentVariable ("ANDROID_SDK_ROOT", string.Empty); + } + + var info = new AndroidSdkInfo (logger); + + Assert.AreEqual (ndk, info.AndroidNdkPath, "AndroidNdkPath not set from $PATH!"); + Assert.AreEqual (sdk, info.AndroidSdkPath, "AndroidSdkPath not set from $PATH!"); + Assert.AreEqual (jdk, info.JavaSdkPath, "JavaSdkPath not set from $PATH!"); + } + finally { + Environment.SetEnvironmentVariable ("PATH", oldPath); + if (!string.IsNullOrEmpty (oldJavaHome)) { + Environment.SetEnvironmentVariable ("JAVA_HOME", oldJavaHome); + } + if (!string.IsNullOrEmpty (oldAndroidHome)) { + Environment.SetEnvironmentVariable ("ANDROID_HOME", oldAndroidHome); + } + if (!string.IsNullOrEmpty (oldAndroidSdkRoot)) { + Environment.SetEnvironmentVariable ("ANDROID_SDK_ROOT", oldAndroidSdkRoot); + } + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void JdkDirectory_JavaHome ([Values ("JI_JAVA_HOME", "JAVA_HOME")] string envVar) + { + if (envVar.Equals ("JAVA_HOME", StringComparison.OrdinalIgnoreCase)) { + Assert.Ignore ("This test will only work locally if you rename/remove your Open JDK directory."); + return; + } + + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + JdkInfoTests.CreateFauxJdk (jdk, releaseVersion: "1.8.999", releaseBuildNumber: "9", javaVersion: "1.8.999-9"); + + var logs = new StringWriter (); + Action logger = (level, message) => { + logs.WriteLine ($"[{level}] {message}"); + }; + + string java_home = null; + try { + // We only set via JAVA_HOME + java_home = Environment.GetEnvironmentVariable (envVar, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable (envVar, jdk, EnvironmentVariableTarget.Process); + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: ndk, javaSdkPath: ""); + + Assert.AreEqual (ndk, info.AndroidNdkPath, "AndroidNdkPath not preserved!"); + Assert.AreEqual (sdk, info.AndroidSdkPath, "AndroidSdkPath not preserved!"); + Assert.AreEqual (jdk, info.JavaSdkPath, "JavaSdkPath not preserved!"); + } finally { + Environment.SetEnvironmentVariable (envVar, java_home, EnvironmentVariableTarget.Process); + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void Sdk_GetCommandLineToolsPaths () + { + CreateSdks(out string root, out string jdk, out string ndk, out string sdk); + + var cmdlineTools = Path.Combine (sdk, "cmdline-tools"); + var latestToolsVersion = "latest"; + var toolsVersion = "2.1"; + var higherToolsVersion = "11.2"; + + void recreateCmdlineToolsDirectory () { + Directory.Delete (cmdlineTools, recursive: true); + Directory.CreateDirectory (cmdlineTools); + } + + try { + var info = new AndroidSdkInfo (androidSdkPath: sdk); + + // Test cmdline-tools path + recreateCmdlineToolsDirectory(); + CreateFauxAndroidSdkToolsDirectory (sdk, createToolsDir: true, toolsVersion: toolsVersion, createOldToolsDir: false); + var toolsPaths = info.GetCommandLineToolsPaths (); + + Assert.AreEqual (toolsPaths.Count (), 1, "Incorrect number of elements"); + Assert.AreEqual (toolsPaths.First (), Path.Combine (sdk, "cmdline-tools", toolsVersion), "Incorrect command line tools path"); + + // Test that cmdline-tools is preferred over tools + recreateCmdlineToolsDirectory(); + CreateFauxAndroidSdkToolsDirectory (sdk, createToolsDir: true, toolsVersion: latestToolsVersion, createOldToolsDir: true); + toolsPaths = info.GetCommandLineToolsPaths (); + + Assert.AreEqual (toolsPaths.Count (), 2, "Incorrect number of elements"); + Assert.AreEqual (toolsPaths.First (), Path.Combine (sdk, "cmdline-tools", latestToolsVersion), "Incorrect command line tools path"); + Assert.AreEqual (toolsPaths.Last (), Path.Combine (sdk, "tools"), "Incorrect tools path"); + + // Test sorting + recreateCmdlineToolsDirectory (); + CreateFauxAndroidSdkToolsDirectory (sdk, createToolsDir: true, toolsVersion: latestToolsVersion, createOldToolsDir: false); + CreateFauxAndroidSdkToolsDirectory (sdk, createToolsDir: true, toolsVersion: toolsVersion, createOldToolsDir: false); + CreateFauxAndroidSdkToolsDirectory (sdk, createToolsDir: true, toolsVersion: higherToolsVersion, createOldToolsDir: true); + toolsPaths = info.GetCommandLineToolsPaths (); + + var toolsPathsList = toolsPaths.ToList (); + Assert.AreEqual (toolsPaths.Count (), 4, "Incorrect number of elements"); + bool isOrderCorrect = toolsPathsList [0].Equals (Path.Combine (sdk, "cmdline-tools", latestToolsVersion), StringComparison.Ordinal) + && toolsPathsList [1].Equals (Path.Combine (sdk, "cmdline-tools", higherToolsVersion), StringComparison.Ordinal) + && toolsPathsList [2].Equals (Path.Combine (sdk, "cmdline-tools", toolsVersion), StringComparison.Ordinal) + && toolsPathsList [3].Equals (Path.Combine (sdk, "tools"), StringComparison.Ordinal); + + Assert.IsTrue (isOrderCorrect, "Tools order is not descending"); + } finally { + Directory.Delete (root, recursive: true); + } + } + + static bool IsWindows => OS.IsWindows; + + static string CreateRoot () + { + var root = Path.GetTempFileName (); + File.Delete (root); + Directory.CreateDirectory (root); + return root; + } + + static void CreateSdks (out string root, out string jdk, out string ndk, out string sdk) + { + root = CreateRoot (); + + ndk = Path.Combine (root, "ndk"); + sdk = Path.Combine (root, "sdk"); + jdk = Path.Combine (root, "jdk"); + + Directory.CreateDirectory (sdk); + Directory.CreateDirectory (ndk); + Directory.CreateDirectory (jdk); + + CreateFauxAndroidSdkDirectory (sdk, "26.0.0"); + CreateFauxAndroidNdkDirectory (ndk, NdkVersion); + CreateFauxJavaSdkDirectory (jdk, "1.8.0", out var _, out var _); + } + + static void CreateFauxAndroidSdkToolsDirectory (string androidSdkDirectory, bool createToolsDir, string toolsVersion, bool createOldToolsDir) + { + if (createToolsDir) { + string androidSdkToolsPath = Path.Combine (androidSdkDirectory, "cmdline-tools", toolsVersion ?? "1.0"); + string androidSdkToolsBinPath = Path.Combine (androidSdkToolsPath, "bin"); + + Directory.CreateDirectory (androidSdkToolsPath); + Directory.CreateDirectory (androidSdkToolsBinPath); + + File.WriteAllText (Path.Combine (androidSdkToolsBinPath, IsWindows ? "lint.bat" : "lint"), ""); + } + + if (createOldToolsDir) { + string androidSdkToolsPath = Path.Combine (androidSdkDirectory, "tools"); + string androidSdkToolsBinPath = Path.Combine (androidSdkToolsPath, "bin"); + + Directory.CreateDirectory (androidSdkToolsPath); + Directory.CreateDirectory (androidSdkToolsBinPath); + + File.WriteAllText (Path.Combine (androidSdkToolsBinPath, IsWindows ? "lint.bat" : "lint"), ""); + } + + } + + static void CreateFauxAndroidSdkDirectory ( + string androidSdkDirectory, + string buildToolsVersion, + bool createToolsDir = true, + string toolsVersion = null, + bool createOldToolsDir = false, + ApiInfo[] apiLevels = null) + { + CreateFauxAndroidSdkToolsDirectory (androidSdkDirectory, createToolsDir, toolsVersion, createOldToolsDir); + + var androidSdkPlatformToolsPath = Path.Combine (androidSdkDirectory, "platform-tools"); + var androidSdkPlatformsPath = Path.Combine (androidSdkDirectory, "platforms"); + var androidSdkBuildToolsPath = Path.Combine (androidSdkDirectory, "build-tools", buildToolsVersion); + + Directory.CreateDirectory (androidSdkDirectory); + Directory.CreateDirectory (androidSdkPlatformToolsPath); + Directory.CreateDirectory (androidSdkPlatformsPath); + Directory.CreateDirectory (androidSdkBuildToolsPath); + + File.WriteAllText (Path.Combine (androidSdkPlatformToolsPath, IsWindows ? "adb.exe" : "adb"), ""); + File.WriteAllText (Path.Combine (androidSdkBuildToolsPath, IsWindows ? "zipalign.exe" : "zipalign"), ""); + File.WriteAllText (Path.Combine (androidSdkBuildToolsPath, IsWindows ? "aapt.exe" : "aapt"), ""); + + List defaults = new List (); + for (int i = 10; i < 26; i++) { + defaults.Add (new ApiInfo () { + Id = i.ToString (), + }); + } + foreach (var level in ((IEnumerable) apiLevels) ?? defaults) { + var dir = Path.Combine (androidSdkPlatformsPath, $"android-{level.Id}"); + Directory.CreateDirectory(dir); + File.WriteAllText (Path.Combine (dir, "android.jar"), ""); + } + } + + struct ApiInfo { + public string Id; + } + + static void CreateFauxAndroidNdkDirectory (string androidNdkDirectory, string ndkVersion) + { + File.WriteAllText (Path.Combine (androidNdkDirectory, "source.properties"), $"Pkg.Revision = {ndkVersion}"); + File.WriteAllText (Path.Combine (androidNdkDirectory, "ndk-stack"), ""); + File.WriteAllText (Path.Combine (androidNdkDirectory, "ndk-stack.cmd"), ""); + + string prebuiltHostName = ""; + if (OS.IsWindows) + prebuiltHostName = "windows-x86_64"; + else if (OS.IsMac) + prebuiltHostName = "darwin-x86_64"; + else + prebuiltHostName = "linux-x86_64"; + + + var toolchainsDir = Path.Combine (androidNdkDirectory, "toolchains"); + var armToolchainDir = Path.Combine (toolchainsDir, "arm-linux-androideabi-4.9"); + var armPrebuiltDir = Path.Combine (armToolchainDir, "prebuilt", prebuiltHostName); + + Directory.CreateDirectory (armPrebuiltDir); + } + + static string CreateFauxJavaSdkDirectory (string javaPath, string javaVersion, out string javaExe, out string javacExe) + { + javaExe = IsWindows ? "Java.cmd" : "java.bash"; + javacExe = IsWindows ? "Javac.cmd" : "javac.bash"; + var jarSigner = IsWindows ? "jarsigner.exe" : "jarsigner"; + var javaBinPath = Path.Combine (javaPath, "bin"); + + Directory.CreateDirectory (javaBinPath); + + CreateFauxJavaExe (Path.Combine (javaBinPath, javaExe), javaVersion); + CreateFauxJavacExe (Path.Combine (javaBinPath, javacExe), javaVersion); + + File.WriteAllText (Path.Combine (javaBinPath, jarSigner), ""); + return javaPath; + } + + static void CreateFauxJavaExe (string javaExeFullPath, string version) + { + var sb = new StringBuilder (); + if (IsWindows) { + sb.AppendLine ("@echo off"); + sb.AppendLine ($"echo java version \"{version}\""); + sb.AppendLine ($"echo Java(TM) SE Runtime Environment (build {version}-b13)"); + sb.AppendLine ($"echo Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)"); + } else { + sb.AppendLine ("#!/bin/bash"); + sb.AppendLine ($"echo \"java version \\\"{version}\\\"\""); + sb.AppendLine ($"echo \"Java(TM) SE Runtime Environment (build {version}-b13)\""); + sb.AppendLine ($"echo \"Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)\""); + } + File.WriteAllText (javaExeFullPath, sb.ToString ()); + if (!IsWindows) { + Exec ("chmod", $"u+x \"{javaExeFullPath}\""); + } + } + + static void CreateFauxJavacExe (string javacExeFullPath, string version) + { + var sb = new StringBuilder (); + if (IsWindows) { + sb.AppendLine ("@echo off"); + sb.AppendLine ($"echo javac {version}"); + } else { + sb.AppendLine ("#!/bin/bash"); + sb.AppendLine ($"echo \"javac {version}\""); + } + File.WriteAllText (javacExeFullPath, sb.ToString ()); + if (!IsWindows) { + Exec ("chmod", $"u+x \"{javacExeFullPath}\""); + } + } + + static void Exec (string exe, string args) + { + var psi = new ProcessStartInfo { + FileName = exe, + Arguments = args, + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + + }; + var proc = Process.Start (psi); + if (!proc.WaitForExit ((int) TimeSpan.FromSeconds(30).TotalMilliseconds)) { + proc.Kill (); + proc.WaitForExit (); + } + } + + [Test] + public void DetectAndSetPreferredJavaSdkPathToLatest () + { + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + }; + + var backupConfig = UnixConfigPath + "." + Path.GetRandomFileName (); + try { + if (OS.IsWindows) { + Assert.Throws(() => AndroidSdkInfo.DetectAndSetPreferredJavaSdkPathToLatest (logger)); + return; + } + Assert.Throws(() => AndroidSdkInfo.DetectAndSetPreferredJavaSdkPathToLatest (logger)); + var newJdkPath = Path.Combine (PreferredJdksOverridePath, "microsoft_dist_openjdk_1.8.999.9"); + JdkInfoTests.CreateFauxJdk (newJdkPath, releaseVersion: "1.8.999", releaseBuildNumber: "9", javaVersion: "1.8.999-9"); + + if (File.Exists (UnixConfigPath)) + File.Move (UnixConfigPath, backupConfig); + + AndroidSdkInfo.DetectAndSetPreferredJavaSdkPathToLatest (logger); + AssertJdkPath (newJdkPath); + } + finally { + if (File.Exists (backupConfig)) { + File.Delete (UnixConfigPath); + File.Move (backupConfig, UnixConfigPath); + } + } + } + + void AssertJdkPath (string expectedJdkPath) + { + var config_file = XDocument.Load (UnixConfigPath); + var javaEl = config_file.Root.Element ("java-sdk"); + var actualJdkPath = (string) javaEl.Attribute ("path"); + + Assert.AreEqual (expectedJdkPath, actualJdkPath); + } + + string UnixConfigPath { + get { + return Path.Combine (UnixConfigDirOverridePath, "monodroid-config.xml"); + } + } + + [Test] + public void TryGetPlatformDirectoryFromApiLevel_MinorVersionDoesNotFallback () + { + // Verifies that when requesting a minor API level (like 36.1), we don't fall back + // to the major version (36) if the minor version platform is not installed. + // See: https://github.com/dotnet/android/issues/10720 + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + // Only create android-36, not android-36.1 + var platformsPath = Path.Combine (sdk, "platforms"); + var platform36Path = Path.Combine (platformsPath, "android-36"); + Directory.CreateDirectory (platform36Path); + File.WriteAllText (Path.Combine (platform36Path, "android.jar"), ""); + + var logs = new StringWriter (); + Action logger = (level, message) => { + logs.WriteLine ($"[{level}] {message}"); + }; + + try { + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: ndk, javaSdkPath: jdk); + var versions = new AndroidVersions (new [] { + new AndroidVersion (36, "16.0"), + new AndroidVersion (new Version (36, 1), "16.0"), + }); + + // Requesting "36" should find android-36 + var dir36 = info.TryGetPlatformDirectoryFromApiLevel ("36", versions); + Assert.IsNotNull (dir36, "Should find android-36"); + Assert.AreEqual (platform36Path, dir36); + + // Requesting "36.1" should NOT fall back to android-36 + var dir361 = info.TryGetPlatformDirectoryFromApiLevel ("36.1", versions); + Assert.IsNull (dir361, "Should NOT fall back to android-36 when android-36.1 is requested but not installed"); + } + finally { + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void TryGetPlatformDirectoryFromApiLevel_MajorFallsBackToMajorDotZero () + { + // Starting with API 37, Google installs platforms to "android-37.0" + // instead of "android-37". Verify the fallback works. + // See: https://github.com/dotnet/android-tools/issues/319 + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + + // Only create android-37.0, not android-37 + var platformsPath = Path.Combine (sdk, "platforms"); + var platform370Path = Path.Combine (platformsPath, "android-37.0"); + Directory.CreateDirectory (platform370Path); + File.WriteAllText (Path.Combine (platform370Path, "android.jar"), ""); + + var logs = new StringWriter (); + Action logger = (level, message) => { + logs.WriteLine ($"[{level}] {message}"); + }; + + try { + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: ndk, javaSdkPath: jdk); + var versions = new AndroidVersions (new [] { + new AndroidVersion (37, "17.0"), + }); + + // Requesting "37" should fall back to android-37.0 + var dir37 = info.TryGetPlatformDirectoryFromApiLevel ("37", versions); + Assert.IsNotNull (dir37, "Should fall back from android-37 to android-37.0"); + Assert.AreEqual (platform370Path, dir37); + + // IsPlatformInstalled should also find android-37.0 + Assert.IsTrue (info.IsPlatformInstalled (37), "IsPlatformInstalled should find android-37.0"); + } + finally { + Directory.Delete (root, recursive: true); + } + } + + [Test] + public void GetBuildToolsPaths_StableVersionsFirst () + { + CreateSdks (out string root, out string jdk, out string ndk, out string sdk); + CreateFauxAndroidSdkDirectory (sdk, "27.0.0-rc4"); + + var logs = new StringWriter (); + Action logger = (level, message) => { + logs.WriteLine ($"[{level}] {message}"); + }; + + try { + var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: ndk, javaSdkPath: jdk); + + var buildToolsPaths = info.GetBuildToolsPaths ().ToList (); + Assert.AreEqual (3, buildToolsPaths.Count); + Assert.AreEqual (Path.Combine (sdk, "build-tools", "26.0.0"), buildToolsPaths [0]); + Assert.AreEqual (Path.Combine (sdk, "build-tools", "27.0.0-rc4"), buildToolsPaths [1]); + Assert.AreEqual (Path.Combine (sdk, "platform-tools"), buildToolsPaths [2]); + } + finally { + Directory.Delete (root, recursive: true); + } + } + + void MakeNdkDir (string rootPath, string version) + { + var extension = OS.IsWindows ? ".cmd" : String.Empty; + Directory.CreateDirectory(rootPath); + File.WriteAllText(Path.Combine (rootPath, "source.properties"), $"Pkg.Revision = {version}"); + Directory.CreateDirectory(Path.Combine(rootPath, "toolchains")); + File.WriteAllText(Path.Combine(rootPath, $"ndk-stack{extension}"), String.Empty); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs new file mode 100644 index 00000000000..73232a47795 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidSdkWindowsTests + { + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs new file mode 100644 index 00000000000..d215469f6a8 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs @@ -0,0 +1,115 @@ +using System; +using System.IO; +using System.Text; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidVersionTests + { + [Test] + public void Constructor_Exceptions () + { + Assert.Throws (() => new AndroidVersion (0, null)); + Assert.Throws (() => new AndroidVersion (0, "not a number")); + Assert.Throws (() => new AndroidVersion ((Version) null, osVersion: "1.0")); + } + + [Test] + public void Constructor () + { + var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", id: "E", stable: false); + Assert.AreEqual (1, v.ApiLevel); + Assert.AreEqual (new Version (1, 0), v.VersionCodeFull); + Assert.AreEqual ("E", v.Id); + Assert.AreEqual ("Four", v.CodeName); + Assert.AreEqual ("2.3", v.OSVersion); + Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion); + Assert.AreEqual ("v2.3", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0", "E" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + + [Test] + public void Constructor_NoId () + { + var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", stable: false); + Assert.AreEqual (1, v.ApiLevel); + Assert.AreEqual (new Version (1, 0), v.VersionCodeFull); + Assert.AreEqual ("1", v.Id); + Assert.AreEqual ("Four", v.CodeName); + Assert.AreEqual ("2.3", v.OSVersion); + Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion); + Assert.AreEqual ("v2.3", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0" })); + + v = new AndroidVersion (new Version (2, 3), osVersion: "2.3", codeName: "Four", stable: false); + Assert.AreEqual ("2.3", v.Id); + Assert.IsTrue (v.Ids.SetEquals (new [] { "2", "2.3" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + + [Test] + public void Load_NoFile () + { + Assert.Throws (() => AndroidVersion.Load ((string) null)); + + var p = Path.GetTempFileName (); + File.Delete (p); + Assert.Throws (() => AndroidVersion.Load (p)); + } + + [Test] + public void Load_NoStream () + { + Assert.Throws (() => AndroidVersion.Load ((Stream) null)); + } + + [Test] + public void Load () + { + var xml = @" + O + 26 + Android O + v7.99.0 + False +"; + var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml))); + Assert.AreEqual (26, v.ApiLevel); + Assert.AreEqual (new Version (26, 0), v.VersionCodeFull); + Assert.AreEqual ("O", v.Id); + Assert.AreEqual ("Android O", v.CodeName); + Assert.AreEqual ("7.99.0", v.OSVersion); + Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion); + Assert.AreEqual ("v7.99.0", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "26", "26.0", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + + [Test] + public void Load_VersionCodeFull_Replaces_Level () + { + var xml = @" + O + 26 + 27.1 + Android O + v7.99.0 + False +"; + var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml))); + Assert.AreEqual (27, v.ApiLevel); + Assert.AreEqual (new Version (27, 1), v.VersionCodeFull); + Assert.AreEqual ("O", v.Id); + Assert.AreEqual ("Android O", v.CodeName); + Assert.AreEqual ("7.99.0", v.OSVersion); + Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion); + Assert.AreEqual ("v7.99.0", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "27", "27.1", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs new file mode 100644 index 00000000000..5b61e0576e8 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidVersionsTests + { + [Test] + public void Constructor_Exceptions () + { + Assert.Throws (() => new AndroidVersions ((IEnumerable) null)); + Assert.Throws (() => new AndroidVersions ((IEnumerable) null)); + + var tempDir = Path.GetTempFileName (); + File.Delete (tempDir); + // Directory not found + Assert.Throws(() => new AndroidVersions (new[]{tempDir})); + } + + [Test] + public void Constructor_NoDirectories () + { + var versions = new AndroidVersions (new string [0]); + Assert.AreEqual (null, versions.MaxStableVersion); + Assert.IsNotNull (versions.FrameworkDirectories); + Assert.AreEqual (0, versions.FrameworkDirectories.Count); + } + + [Test] + public void Constructor_NoVersions () + { + var versions = new AndroidVersions (new AndroidVersion [0]); + Assert.AreEqual (null, versions.MaxStableVersion); + Assert.IsNotNull (versions.FrameworkDirectories); + Assert.AreEqual (0, versions.FrameworkDirectories.Count); + } + + [Test] + public void Contructor_UnstableVersions () + { + var versions = new AndroidVersions ( + new [] { new AndroidVersion (apiLevel: 100, osVersion: "100.0", codeName: "Test", id: "Z", stable: false) } + ); + Assert.IsNull (versions.MaxStableVersion); + Assert.IsNull (versions.MinStableVersion); + } + + [Test] + public void Constructor_FrameworkDirectories () + { + var frameworkDir = Path.GetTempFileName (); + File.Delete (frameworkDir); + Directory.CreateDirectory (frameworkDir); + try { + Directory.CreateDirectory (Path.Combine (frameworkDir, "MonoAndroid")); + Directory.CreateDirectory (Path.Combine (frameworkDir, "MonoAndroid", "v5.1")); + File.WriteAllLines (Path.Combine (frameworkDir, "MonoAndroid", "v5.1", "AndroidApiInfo.xml"), new []{ + "", + " 22", + " 22", + " Marshmallow", + " v5.1", + " True", + "", + }); + Directory.CreateDirectory (Path.Combine (frameworkDir, "MonoAndroid", "v6.0")); + File.WriteAllLines (Path.Combine (frameworkDir, "MonoAndroid", "v6.0", "AndroidApiInfo.xml"), new []{ + "", + " 23", + " 23", + " Marshmallow", + " v6.0", + " True", + "", + }); + Directory.CreateDirectory (Path.Combine (frameworkDir, "MonoAndroid", "v8.0")); + File.WriteAllLines (Path.Combine (frameworkDir, "MonoAndroid", "v8.0", "AndroidApiInfo.xml"), new []{ + "", + " O", + " 26", + " Oreo", + " v8.0", + " False", + "", + }); + Directory.CreateDirectory (Path.Combine (frameworkDir, "MonoAndroid", "v108.1.99")); + File.WriteAllLines (Path.Combine (frameworkDir, "MonoAndroid", "v108.1.99", "AndroidApiInfo.xml"), new []{ + "", + " Z", + " 127", + " 127.1", + " Z", + " v108.1.99", + " False", + "", + }); + var versions = new AndroidVersions (new [] { + Path.Combine (frameworkDir, "MonoAndroid", "v5.1"), + Path.Combine (frameworkDir, "MonoAndroid", "v6.0"), + Path.Combine (frameworkDir, "MonoAndroid", "v108.1.99"), + }); + Assert.IsNotNull (versions.FrameworkDirectories); + Assert.AreEqual (1, versions.FrameworkDirectories.Count); + Assert.AreEqual (Path.Combine (frameworkDir, "MonoAndroid"), versions.FrameworkDirectories [0]); + Assert.IsNotNull (versions.MaxStableVersion); + Assert.AreEqual (23, versions.MaxStableVersion.ApiLevel); + Assert.IsNotNull (versions.MinStableVersion); + Assert.AreEqual (22, versions.MinStableVersion.ApiLevel); + } + finally { + Directory.Delete (frameworkDir, recursive: true); + } + } + + [Test] + public void Constructor_Versions () + { + var versions = new AndroidVersions (new []{ + new AndroidVersion (apiLevel: 1, osVersion: "1.0", codeName: "One", id: "A", stable: true), + new AndroidVersion (apiLevel: 2, osVersion: "1.1", codeName: "One.One", id: "B", stable: true), + }); + Assert.IsNotNull (versions.FrameworkDirectories); + Assert.AreEqual (0, versions.FrameworkDirectories.Count); + Assert.IsNotNull (versions.MaxStableVersion); + Assert.AreEqual (2, versions.MaxStableVersion.ApiLevel); + Assert.IsNotNull (versions.MinStableVersion); + Assert.AreEqual (1, versions.MinStableVersion.ApiLevel); + } + + static AndroidVersions CreateTestVersions () + { + return new AndroidVersions (new []{ + new AndroidVersion (apiLevel: 1, osVersion: "1.0", id: "A", stable: true), + new AndroidVersion (apiLevel: 2, osVersion: "1.1", id: "B", stable: false), + new AndroidVersion (apiLevel: 3, osVersion: "1.2", id: "C", stable: true), + // Hides/shadows a Known Version + new AndroidVersion (apiLevel: 14, osVersion: "4.0", id: "II", stable: false), + // Demonstrates new "minor" release support + new AndroidVersion (versionCodeFull: new Version (36, 1), osVersion: "16.1", id: "CANARY", stable: true), + new AndroidVersion (versionCodeFull: new Version (36, 0), osVersion: "16.0", id: "Baklava", stable: true), + new AndroidVersion (versionCodeFull: new Version (37, 1), osVersion: "17.1", id: "E", stable: false), + new AndroidVersion (versionCodeFull: new Version (37, 0), osVersion: "17.0", id: "D", stable: true), + }); + } + + [Test] + public void GetApiLevelFromFrameworkVersion () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetApiLevelFromFrameworkVersion (null)); + Assert.AreEqual (1, versions.GetApiLevelFromFrameworkVersion ("v1.0")); + Assert.AreEqual (1, versions.GetApiLevelFromFrameworkVersion ("1.0")); + Assert.AreEqual (2, versions.GetApiLevelFromFrameworkVersion ("v1.1")); + Assert.AreEqual (2, versions.GetApiLevelFromFrameworkVersion ("1.1")); + Assert.AreEqual (3, versions.GetApiLevelFromFrameworkVersion ("v1.2")); + Assert.AreEqual (3, versions.GetApiLevelFromFrameworkVersion ("1.2")); + Assert.AreEqual (null, versions.GetApiLevelFromFrameworkVersion ("v1.3")); + Assert.AreEqual (null, versions.GetApiLevelFromFrameworkVersion ("1.3")); + Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("v4.0")); + Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("4.0")); + Assert.AreEqual (36, versions.GetApiLevelFromFrameworkVersion ("16.0")); + Assert.AreEqual (36, versions.GetApiLevelFromFrameworkVersion ("16.1")); + Assert.AreEqual (37, versions.GetApiLevelFromFrameworkVersion ("17.0")); + Assert.AreEqual (37, versions.GetApiLevelFromFrameworkVersion ("17.1")); + + // via KnownVersions + Assert.AreEqual (4, versions.GetApiLevelFromFrameworkVersion ("v1.6")); + Assert.AreEqual (4, versions.GetApiLevelFromFrameworkVersion ("1.6")); + } + + [Test] + public void GetApiLevelFromId () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetApiLevelFromId (null)); + Assert.AreEqual (1, versions.GetApiLevelFromId ("A")); + Assert.AreEqual (1, versions.GetApiLevelFromId ("1")); + Assert.AreEqual (2, versions.GetApiLevelFromId ("B")); + Assert.AreEqual (2, versions.GetApiLevelFromId ("2")); + Assert.AreEqual (3, versions.GetApiLevelFromId ("C")); + Assert.AreEqual (3, versions.GetApiLevelFromId ("3")); + Assert.AreEqual (14, versions.GetApiLevelFromId ("14")); + Assert.AreEqual (14, versions.GetApiLevelFromId ("II")); + Assert.AreEqual (36, versions.GetApiLevelFromId ("36")); + Assert.AreEqual (36, versions.GetApiLevelFromId ("36.1")); + Assert.AreEqual (36, versions.GetApiLevelFromId ("CANARY")); + Assert.AreEqual (37, versions.GetApiLevelFromId ("37")); + Assert.AreEqual (37, versions.GetApiLevelFromId ("37.1")); + Assert.AreEqual (37, versions.GetApiLevelFromId ("D")); + + Assert.AreEqual (null, versions.GetApiLevelFromId ("Z")); + + // via KnownVersions + Assert.AreEqual (11, versions.GetApiLevelFromId ("H")); + } + + [Test] + public void GetIdFromApiLevel () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetIdFromApiLevel (null)); + Assert.AreEqual ("A", versions.GetIdFromApiLevel (1)); + Assert.AreEqual ("A", versions.GetIdFromApiLevel ("1")); + Assert.AreEqual ("A", versions.GetIdFromApiLevel ("A")); + Assert.AreEqual ("B", versions.GetIdFromApiLevel (2)); + Assert.AreEqual ("B", versions.GetIdFromApiLevel ("2")); + Assert.AreEqual ("B", versions.GetIdFromApiLevel ("B")); + Assert.AreEqual ("C", versions.GetIdFromApiLevel (3)); + Assert.AreEqual ("C", versions.GetIdFromApiLevel ("3")); + Assert.AreEqual ("C", versions.GetIdFromApiLevel ("C")); + Assert.AreEqual ("II", versions.GetIdFromApiLevel ("14")); + Assert.AreEqual ("II", versions.GetIdFromApiLevel ("II")); + + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel (36)); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36")); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36.0")); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("Baklava")); + Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("36.1")); + Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("CANARY")); + + Assert.AreEqual ("D", versions.GetIdFromApiLevel (37)); + Assert.AreEqual ("D", versions.GetIdFromApiLevel ("37")); + Assert.AreEqual ("D", versions.GetIdFromApiLevel ("37.0")); + Assert.AreEqual ("D", versions.GetIdFromApiLevel ("D")); + Assert.AreEqual ("E", versions.GetIdFromApiLevel ("37.1")); + Assert.AreEqual ("E", versions.GetIdFromApiLevel ("E")); + + // "GIGO" + Assert.AreEqual ("-1", versions.GetIdFromApiLevel (-1)); + Assert.AreEqual ("-1", versions.GetIdFromApiLevel ("-1")); + Assert.AreEqual ("Z", versions.GetIdFromApiLevel ("Z")); + + // via KnownVersions + Assert.AreEqual ("11", versions.GetIdFromApiLevel (11)); + Assert.AreEqual ("11", versions.GetIdFromApiLevel ("11")); + Assert.AreEqual ("11", versions.GetIdFromApiLevel ("H")); + } + + [Test] + public void GetIdFromFrameworkVersion () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetIdFromFrameworkVersion (null)); + Assert.AreEqual ("A", versions.GetIdFromFrameworkVersion ("v1.0")); + Assert.AreEqual ("A", versions.GetIdFromFrameworkVersion ("1.0")); + Assert.AreEqual ("B", versions.GetIdFromFrameworkVersion ("v1.1")); + Assert.AreEqual ("B", versions.GetIdFromFrameworkVersion ("1.1")); + Assert.AreEqual ("C", versions.GetIdFromFrameworkVersion ("v1.2")); + Assert.AreEqual ("C", versions.GetIdFromFrameworkVersion ("1.2")); + Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("v4.0")); + Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("4.0")); + Assert.AreEqual ("Baklava", versions.GetIdFromFrameworkVersion ("16.0")); + Assert.AreEqual ("CANARY", versions.GetIdFromFrameworkVersion ("16.1")); + Assert.AreEqual ("D", versions.GetIdFromFrameworkVersion ("17.0")); + Assert.AreEqual ("E", versions.GetIdFromFrameworkVersion ("17.1")); + + // Unknown values + Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("v0.99")); + Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("0.99")); + + // via KnownVersions + Assert.AreEqual ("10", versions.GetIdFromFrameworkVersion ("v2.3")); + Assert.AreEqual ("10", versions.GetIdFromFrameworkVersion ("2.3")); + } + + [Test] + public void GetFrameworkVersionFromApiLevel () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetFrameworkVersionFromApiLevel (0)); + Assert.AreEqual ("v1.0", versions.GetFrameworkVersionFromApiLevel (1)); + Assert.AreEqual ("v1.1", versions.GetFrameworkVersionFromApiLevel (2)); + Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromApiLevel (3)); + Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromApiLevel (14)); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromApiLevel (36)); + Assert.AreEqual ("v17.0", versions.GetFrameworkVersionFromApiLevel (37)); + + // via KnownVersions + Assert.AreEqual ("v2.3", versions.GetFrameworkVersionFromApiLevel (10)); + } + + [Test] + public void GetFrameworkVersionFromId () + { + var versions = CreateTestVersions (); + + Assert.AreEqual (null, versions.GetFrameworkVersionFromId (null)); + Assert.AreEqual ("v1.0", versions.GetFrameworkVersionFromId ("1")); + Assert.AreEqual ("v1.0", versions.GetFrameworkVersionFromId ("A")); + Assert.AreEqual ("v1.1", versions.GetFrameworkVersionFromId ("2")); + Assert.AreEqual ("v1.1", versions.GetFrameworkVersionFromId ("B")); + Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromId ("3")); + Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromId ("C")); + Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("14")); + Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("II")); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("36")); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("Baklava")); + Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("36.1")); + Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("CANARY")); + Assert.AreEqual ("v17.0", versions.GetFrameworkVersionFromId ("37")); + Assert.AreEqual ("v17.0", versions.GetFrameworkVersionFromId ("D")); + Assert.AreEqual ("v17.1", versions.GetFrameworkVersionFromId ("37.1")); + Assert.AreEqual ("v17.1", versions.GetFrameworkVersionFromId ("E")); + + // via KnownVersions + Assert.AreEqual ("v3.0", versions.GetFrameworkVersionFromId ("11")); + Assert.AreEqual ("v3.0", versions.GetFrameworkVersionFromId ("H")); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs new file mode 100644 index 00000000000..187beedb430 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests; + +[TestFixture] +public class AvdManagerRunnerTests +{ + [Test] + public void ParseAvdListOutput_MultipleAvds () + { + var output = + "Available Android Virtual Devices:\n" + + " Name: Pixel_7_API_35\n" + + " Device: pixel_7 (Google)\n" + + " Path: /Users/test/.android/avd/Pixel_7_API_35.avd\n" + + " Target: Google APIs (Google Inc.)\n" + + " Based on: Android 15 Tag/ABI: google_apis/arm64-v8a\n" + + "---------\n" + + " Name: MAUI_Emulator\n" + + " Device: pixel_6 (Google)\n" + + " Path: /Users/test/.android/avd/MAUI_Emulator.avd\n" + + " Target: Google APIs (Google Inc.)\n" + + " Based on: Android 14 Tag/ABI: google_apis/x86_64\n"; + + var avds = AvdManagerRunner.ParseAvdListOutput (output); + + Assert.AreEqual (2, avds.Count); + + Assert.AreEqual ("Pixel_7_API_35", avds [0].Name); + Assert.AreEqual ("pixel_7 (Google)", avds [0].DeviceProfile); + Assert.AreEqual ("/Users/test/.android/avd/Pixel_7_API_35.avd", avds [0].Path); + + Assert.AreEqual ("MAUI_Emulator", avds [1].Name); + Assert.AreEqual ("pixel_6 (Google)", avds [1].DeviceProfile); + Assert.AreEqual ("/Users/test/.android/avd/MAUI_Emulator.avd", avds [1].Path); + } + + [Test] + public void ParseAvdListOutput_WindowsNewlines () + { + var output = + "Available Android Virtual Devices:\r\n" + + " Name: Test_AVD\r\n" + + " Device: Nexus 5X (Google)\r\n" + + " Path: C:\\Users\\test\\.android\\avd\\Test_AVD.avd\r\n" + + " Target: Google APIs (Google Inc.)\r\n"; + + var avds = AvdManagerRunner.ParseAvdListOutput (output); + + Assert.AreEqual (1, avds.Count); + Assert.AreEqual ("Test_AVD", avds [0].Name); + Assert.AreEqual ("Nexus 5X (Google)", avds [0].DeviceProfile); + Assert.AreEqual ("C:\\Users\\test\\.android\\avd\\Test_AVD.avd", avds [0].Path); + } + + [Test] + public void ParseAvdListOutput_EmptyOutput () + { + var avds = AvdManagerRunner.ParseAvdListOutput (""); + Assert.AreEqual (0, avds.Count); + } + + [Test] + public void ParseAvdListOutput_NoAvds () + { + var output = "Available Android Virtual Devices:\n"; + var avds = AvdManagerRunner.ParseAvdListOutput (output); + Assert.AreEqual (0, avds.Count); + } + + [Test] + public void ParseAvdListOutput_SingleAvdNoDevice () + { + var output = + " Name: Minimal_AVD\n" + + " Path: /home/user/.android/avd/Minimal_AVD.avd\n"; + + var avds = AvdManagerRunner.ParseAvdListOutput (output); + + Assert.AreEqual (1, avds.Count); + Assert.AreEqual ("Minimal_AVD", avds [0].Name); + Assert.IsNull (avds [0].DeviceProfile); + Assert.AreEqual ("/home/user/.android/avd/Minimal_AVD.avd", avds [0].Path); + } + + [Test] + public void ParseAvdListOutput_ReturnsIReadOnlyList () + { + var avds = AvdManagerRunner.ParseAvdListOutput (""); + Assert.IsInstanceOf> (avds); + } + + [Test] + public void FindCmdlineTool_FindsVersionedDir () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}"); + var binDir = Path.Combine (tempDir, "cmdline-tools", "12.0", "bin"); + Directory.CreateDirectory (binDir); + + try { + var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager"; + File.WriteAllText (Path.Combine (binDir, avdMgrName), ""); + + var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.That (path, Does.Contain ("12.0")); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + public void FindCmdlineTool_PrefersHigherVersion () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}"); + var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager"; + + var binDir10 = Path.Combine (tempDir, "cmdline-tools", "10.0", "bin"); + var binDir12 = Path.Combine (tempDir, "cmdline-tools", "12.0", "bin"); + Directory.CreateDirectory (binDir10); + Directory.CreateDirectory (binDir12); + File.WriteAllText (Path.Combine (binDir10, avdMgrName), ""); + File.WriteAllText (Path.Combine (binDir12, avdMgrName), ""); + + try { + var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.That (path, Does.Contain ("12.0")); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + public void FindCmdlineTool_HandlesPreReleaseVersionDir () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}"); + var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager"; + + // Pre-release "13.0-rc1" should be preferred over "12.0" + var binDir12 = Path.Combine (tempDir, "cmdline-tools", "12.0", "bin"); + var binDirRc = Path.Combine (tempDir, "cmdline-tools", "13.0-rc1", "bin"); + Directory.CreateDirectory (binDir12); + Directory.CreateDirectory (binDirRc); + File.WriteAllText (Path.Combine (binDir12, avdMgrName), ""); + File.WriteAllText (Path.Combine (binDirRc, avdMgrName), ""); + + try { + var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.That (path, Does.Contain ("13.0-rc1")); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + public void FindCmdlineTool_PrefersLatest () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}"); + var binDir = Path.Combine (tempDir, "cmdline-tools", "latest", "bin"); + Directory.CreateDirectory (binDir); + + try { + var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager"; + File.WriteAllText (Path.Combine (binDir, avdMgrName), ""); + + var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.That (path, Does.Contain ("latest")); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + public void FindCmdlineTool_MissingSdk_ReturnsNull () + { + var path = ProcessUtils.FindCmdlineTool ("/nonexistent/path", "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.IsNull (path); + } + + [Test] + public void Constructor_NullPath_ThrowsArgumentException () + { + Assert.Throws (() => new AvdManagerRunner (null!)); + } + + [Test] + public void Constructor_EmptyPath_ThrowsArgumentException () + { + Assert.Throws (() => new AvdManagerRunner ("")); + } + + [Test] + public void Constructor_WhitespacePath_ThrowsArgumentException () + { + Assert.Throws (() => new AvdManagerRunner (" ")); + } + + [Test] + public void Constructor_AcceptsEnvironmentVariables () + { + var env = new Dictionary { { "ANDROID_HOME", "/test/sdk" } }; + var runner = new AvdManagerRunner ("/fake/avdmanager", env); + Assert.IsNotNull (runner); + } + + [Test] + public void GetOrCreateAvdAsync_NullName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync (null!, "system-image")); + } + + [Test] + public void GetOrCreateAvdAsync_EmptyName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync ("", "system-image")); + } + + [Test] + public void GetOrCreateAvdAsync_WhitespaceName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync (" ", "system-image")); + } + + [Test] + public void GetOrCreateAvdAsync_NullSystemImage_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync ("test-avd", null!)); + } + + [Test] + public void GetOrCreateAvdAsync_EmptySystemImage_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync ("test-avd", "")); + } + + [Test] + public void GetOrCreateAvdAsync_WhitespaceSystemImage_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.GetOrCreateAvdAsync ("test-avd", " \t ")); + } + + [Test] + public void DeleteAvdAsync_NullName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.DeleteAvdAsync (null!)); + } + + [Test] + public void DeleteAvdAsync_EmptyName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.DeleteAvdAsync ("")); + } + + [Test] + public void DeleteAvdAsync_WhitespaceName_ThrowsArgumentException () + { + var runner = new AvdManagerRunner ("/fake/avdmanager"); + Assert.ThrowsAsync (() => runner.DeleteAvdAsync (" \t ")); + } + + [Test] + public void FindCmdlineTool_PrefersStableOverPreRelease () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}"); + var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager"; + + // Both "13.0" (stable) and "13.0-rc1" (prerelease) exist — stable should win + var binDirStable = Path.Combine (tempDir, "cmdline-tools", "13.0", "bin"); + var binDirRc = Path.Combine (tempDir, "cmdline-tools", "13.0-rc1", "bin"); + Directory.CreateDirectory (binDirStable); + Directory.CreateDirectory (binDirRc); + File.WriteAllText (Path.Combine (binDirStable, avdMgrName), ""); + File.WriteAllText (Path.Combine (binDirRc, avdMgrName), ""); + + try { + var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : ""); + Assert.That (path, Does.Contain (Path.Combine ("13.0", "bin"))); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + public void ParseCompactDeviceListOutput_MultipleProfiles () + { + var output = + "automotive_1024p_landscape\n" + + "pixel_7\n" + + "Nexus 5X\n" + + "Galaxy Nexus\n"; + + var profiles = AvdManagerRunner.ParseCompactDeviceListOutput (output); + + Assert.AreEqual (4, profiles.Count); + Assert.AreEqual ("automotive_1024p_landscape", profiles [0].Id); + Assert.AreEqual ("pixel_7", profiles [1].Id); + Assert.AreEqual ("Nexus 5X", profiles [2].Id); + Assert.AreEqual ("Galaxy Nexus", profiles [3].Id); + } + + [Test] + public void ParseCompactDeviceListOutput_EmptyOutput () + { + var profiles = AvdManagerRunner.ParseCompactDeviceListOutput (""); + Assert.AreEqual (0, profiles.Count); + } + + [Test] + public void ParseCompactDeviceListOutput_WindowsNewlines () + { + var output = + "pixel_fold\r\n" + + "pixel_9_pro\r\n"; + + var profiles = AvdManagerRunner.ParseCompactDeviceListOutput (output); + + Assert.AreEqual (2, profiles.Count); + Assert.AreEqual ("pixel_fold", profiles [0].Id); + Assert.AreEqual ("pixel_9_pro", profiles [1].Id); + } + + [Test] + public void ParseCompactDeviceListOutput_SkipsBlankLines () + { + var output = "\n\npixel_7\n\n"; + + var profiles = AvdManagerRunner.ParseCompactDeviceListOutput (output); + + Assert.AreEqual (1, profiles.Count); + Assert.AreEqual ("pixel_7", profiles [0].Id); + } + + [Test] + public void ParseCompactDeviceListOutput_ReturnsIReadOnlyList () + { + var profiles = AvdManagerRunner.ParseCompactDeviceListOutput (""); + Assert.IsInstanceOf> (profiles); + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/DownloadUtilsTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/DownloadUtilsTests.cs new file mode 100644 index 00000000000..67b05125436 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/DownloadUtilsTests.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Threading; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class DownloadUtilsTests + { + string tempDir = null!; + + [SetUp] + public void SetUp () + { + tempDir = Path.Combine (Path.GetTempPath (), $"DownloadUtilsTests-{Guid.NewGuid ():N}"); + Directory.CreateDirectory (tempDir); + } + + [TearDown] + public void TearDown () + { + if (Directory.Exists (tempDir)) + Directory.Delete (tempDir, recursive: true); + } + [Test] + public void ParseChecksumFile_Null_ReturnsNull () + { + Assert.IsNull (DownloadUtils.ParseChecksumFile (null!)); + } + + [Test] + public void ParseChecksumFile_Empty_ReturnsNull () + { + Assert.IsNull (DownloadUtils.ParseChecksumFile ("")); + } + + [Test] + public void ParseChecksumFile_WhitespaceOnly_ReturnsNull () + { + Assert.IsNull (DownloadUtils.ParseChecksumFile (" \n\t ")); + } + + [Test] + public void ParseChecksumFile_HashOnly () + { + Assert.AreEqual ("abc123def456", DownloadUtils.ParseChecksumFile ("abc123def456")); + } + + [Test] + public void ParseChecksumFile_HashOnly_WithTrailingNewline () + { + Assert.AreEqual ("abc123def456", DownloadUtils.ParseChecksumFile ("abc123def456\n")); + } + + [Test] + public void ParseChecksumFile_HashAndFilename () + { + // Standard sha256sum format: " " + Assert.AreEqual ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + DownloadUtils.ParseChecksumFile ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 microsoft-jdk-21-linux-x64.tar.gz")); + } + + [Test] + public void ParseChecksumFile_HashAndFilename_WithTab () + { + Assert.AreEqual ("abc123", DownloadUtils.ParseChecksumFile ("abc123\tfilename.zip")); + } + + [Test] + public void ParseChecksumFile_MultipleLines_ReturnsFirstHash () + { + var content = "abc123 file1.zip\ndef456 file2.zip\n"; + Assert.AreEqual ("abc123", DownloadUtils.ParseChecksumFile (content)); + } + + [Test] + public void ParseChecksumFile_LeadingAndTrailingWhitespace () + { + Assert.AreEqual ("abc123", DownloadUtils.ParseChecksumFile (" abc123 filename.zip \n")); + } + + [TestCase ("abc123\r\n")] + [TestCase ("abc123\r")] + [TestCase ("abc123\n")] + public void ParseChecksumFile_VariousLineEndings (string content) + { + Assert.AreEqual ("abc123", DownloadUtils.ParseChecksumFile (content)); + } + + // --- VerifyChecksum tests --- + + [Test] + public void VerifyChecksum_MatchingHash_DoesNotThrow () + { + var filePath = Path.Combine (tempDir, "test.bin"); + var content = new byte [] { 0x48, 0x65, 0x6c, 0x6c, 0x6f }; // "Hello" + File.WriteAllBytes (filePath, content); + + using var sha = SHA256.Create (); + var expected = BitConverter.ToString (sha.ComputeHash (content)).Replace ("-", "").ToLowerInvariant (); + + Assert.DoesNotThrow (() => DownloadUtils.VerifyChecksum (filePath, expected)); + } + + [Test] + public void VerifyChecksum_MismatchedHash_Throws () + { + var filePath = Path.Combine (tempDir, "test.bin"); + File.WriteAllBytes (filePath, new byte [] { 1, 2, 3 }); + + var ex = Assert.Throws (() => + DownloadUtils.VerifyChecksum (filePath, "0000000000000000000000000000000000000000000000000000000000000000")); + Assert.That (ex!.Message, Does.Contain ("Checksum verification failed")); + } + + [Test] + public void VerifyChecksum_CaseInsensitive () + { + var filePath = Path.Combine (tempDir, "test.bin"); + var content = new byte [] { 0xFF }; + File.WriteAllBytes (filePath, content); + + using var sha = SHA256.Create (); + var upperHash = BitConverter.ToString (sha.ComputeHash (content)).Replace ("-", "").ToUpperInvariant (); + + Assert.DoesNotThrow (() => DownloadUtils.VerifyChecksum (filePath, upperHash)); + } + + [Test] + public void VerifyChecksum_NonExistentFile_Throws () + { + var filePath = Path.Combine (tempDir, "nonexistent.bin"); + Assert.Throws (() => + DownloadUtils.VerifyChecksum (filePath, "abc123")); + } + + // --- ExtractZipSafe tests --- + + [Test] + public void ExtractZipSafe_ValidZip_ExtractsFiles () + { + var zipPath = Path.Combine (tempDir, "test.zip"); + var extractPath = Path.Combine (tempDir, "extracted"); + Directory.CreateDirectory (extractPath); + + using (var archive = ZipFile.Open (zipPath, ZipArchiveMode.Create)) { + var entry = archive.CreateEntry ("subdir/hello.txt"); + using var writer = new StreamWriter (entry.Open ()); + writer.Write ("hello world"); + } + + DownloadUtils.ExtractZipSafe (zipPath, extractPath, CancellationToken.None); + + var extractedFile = Path.Combine (extractPath, "subdir", "hello.txt"); + Assert.IsTrue (File.Exists (extractedFile), "Extracted file should exist"); + Assert.AreEqual ("hello world", File.ReadAllText (extractedFile)); + } + + [Test] + public void ExtractZipSafe_ZipSlip_Throws () + { + var zipPath = Path.Combine (tempDir, "evil.zip"); + var extractPath = Path.Combine (tempDir, "extracted"); + Directory.CreateDirectory (extractPath); + + using (var archive = ZipFile.Open (zipPath, ZipArchiveMode.Create)) { + // Create an entry with a path traversal + var entry = archive.CreateEntry ("../relative.txt"); + using var writer = new StreamWriter (entry.Open ()); + writer.Write ("malicious"); + } + + var ex = Assert.Throws (() => + DownloadUtils.ExtractZipSafe (zipPath, extractPath, CancellationToken.None)); + Assert.That (ex!.Message, Does.Contain ("outside target directory")); + } + + [Test] + public void ExtractZipSafe_EmptyZip_NoFilesExtracted () + { + var zipPath = Path.Combine (tempDir, "empty.zip"); + var extractPath = Path.Combine (tempDir, "extracted"); + Directory.CreateDirectory (extractPath); + + using (ZipFile.Open (zipPath, ZipArchiveMode.Create)) { } + + DownloadUtils.ExtractZipSafe (zipPath, extractPath, CancellationToken.None); + + Assert.AreEqual (0, Directory.GetFiles (extractPath, "*", SearchOption.AllDirectories).Length); + } + + [Test] + public void ExtractZipSafe_CancellationToken_Throws () + { + var zipPath = Path.Combine (tempDir, "test.zip"); + var extractPath = Path.Combine (tempDir, "extracted"); + Directory.CreateDirectory (extractPath); + + using (var archive = ZipFile.Open (zipPath, ZipArchiveMode.Create)) { + for (int i = 0; i < 10; i++) { + var entry = archive.CreateEntry ($"file{i}.txt"); + using var writer = new StreamWriter (entry.Open ()); + writer.Write ($"content {i}"); + } + } + + using var cts = new CancellationTokenSource (); + cts.Cancel (); + + Assert.Throws (() => + DownloadUtils.ExtractZipSafe (zipPath, extractPath, cts.Token)); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs new file mode 100644 index 00000000000..c94da2ece99 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs @@ -0,0 +1,642 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests; + +[TestFixture] +public class EmulatorRunnerTests +{ + [Test] + public void ParseListAvdsOutput_MultipleAvds () + { + var output = "Pixel_7_API_35\nMAUI_Emulator\nNexus_5X\n"; + + var avds = EmulatorRunner.ParseListAvdsOutput (output); + + Assert.AreEqual (3, avds.Count); + Assert.AreEqual ("Pixel_7_API_35", avds [0]); + Assert.AreEqual ("MAUI_Emulator", avds [1]); + Assert.AreEqual ("Nexus_5X", avds [2]); + } + + [Test] + public void ParseListAvdsOutput_EmptyOutput () + { + var avds = EmulatorRunner.ParseListAvdsOutput (""); + Assert.AreEqual (0, avds.Count); + } + + [Test] + public void ParseListAvdsOutput_WindowsNewlines () + { + var output = "Pixel_7_API_35\r\nMAUI_Emulator\r\n"; + + var avds = EmulatorRunner.ParseListAvdsOutput (output); + + Assert.AreEqual (2, avds.Count); + Assert.AreEqual ("Pixel_7_API_35", avds [0]); + Assert.AreEqual ("MAUI_Emulator", avds [1]); + } + + [Test] + public void ParseListAvdsOutput_BlankLines () + { + var output = "\nPixel_7_API_35\n\n\nMAUI_Emulator\n\n"; + + var avds = EmulatorRunner.ParseListAvdsOutput (output); + + Assert.AreEqual (2, avds.Count); + } + + [Test] + public void Constructor_ThrowsOnNullPath () + { + Assert.Throws (() => new EmulatorRunner (null!)); + } + + [Test] + public void Constructor_ThrowsOnEmptyPath () + { + Assert.Throws (() => new EmulatorRunner ("")); + } + + [Test] + public void Constructor_ThrowsOnWhitespacePath () + { + Assert.Throws (() => new EmulatorRunner (" ")); + } + + [Test] + public void LaunchEmulator_ThrowsOnNullAvdName () + { + var runner = new EmulatorRunner ("/fake/emulator"); + Assert.Throws (() => runner.LaunchEmulator (null!)); + } + + [Test] + public void LaunchEmulator_ThrowsOnEmptyAvdName () + { + var runner = new EmulatorRunner ("/fake/emulator"); + Assert.Throws (() => runner.LaunchEmulator ("")); + } + + [Test] + public void LaunchEmulator_ThrowsOnWhitespaceAvdName () + { + var runner = new EmulatorRunner ("/fake/emulator"); + Assert.Throws (() => runner.LaunchEmulator (" ")); + } + + [Test] + public async Task AlreadyOnlineDevice_PassesThrough () + { + var devices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35", + }, + }; + + var mockAdb = new MockAdbRunner (devices); + var runner = new EmulatorRunner ("/fake/emulator"); + + var result = await runner.BootEmulatorAsync ("emulator-5554", mockAdb); + + Assert.IsTrue (result.Success); + Assert.AreEqual ("emulator-5554", result.Serial); + Assert.IsNull (result.ErrorMessage); + } + + [Test] + public async Task AvdAlreadyRunning_WaitsForFullBoot () + { + var devices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35", + }, + }; + + var mockAdb = new MockAdbRunner (devices); + mockAdb.ShellProperties ["sys.boot_completed"] = "1"; + mockAdb.ShellCommands ["pm path android"] = "package:/system/framework/framework-res.apk"; + + var runner = new EmulatorRunner ("/fake/emulator"); + var options = new EmulatorBootOptions { BootTimeout = TimeSpan.FromSeconds (5), PollInterval = TimeSpan.FromMilliseconds (50) }; + + var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); + + Assert.IsTrue (result.Success); + Assert.AreEqual ("emulator-5554", result.Serial); + } + + [Test] + public async Task BootEmulator_AppearsAfterPolling () + { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + mockAdb.ShellProperties ["sys.boot_completed"] = "1"; + mockAdb.ShellCommands ["pm path android"] = "package:/system/framework/framework-res.apk"; + + int pollCount = 0; + mockAdb.OnListDevices = () => { + pollCount++; + if (pollCount >= 2) { + devices.Add (new AdbDeviceInfo { + Serial = "emulator-5554", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35", + }); + } + }; + + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + Process? emulatorProcess = null; + try { + var runner = new EmulatorRunner (emuPath); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromSeconds (10), + PollInterval = TimeSpan.FromMilliseconds (50), + }; + + var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); + + Assert.IsTrue (result.Success); + Assert.AreEqual ("emulator-5554", result.Serial); + Assert.IsTrue (pollCount >= 2); + } finally { + // Kill any emulator process spawned by the test + try { + emulatorProcess = FindEmulatorProcess (emuPath); + emulatorProcess?.Kill (); + emulatorProcess?.WaitForExit (1000); + } catch { } + Directory.Delete (tempDir, true); + } + } + + [Test] + public async Task LaunchFailure_ReturnsError () + { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + + // Nonexistent path → LaunchAvd throws → error result + var runner = new EmulatorRunner ("/nonexistent/emulator"); + var options = new EmulatorBootOptions { BootTimeout = TimeSpan.FromSeconds (2) }; + + var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); + + Assert.IsFalse (result.Success); + Assert.AreEqual (EmulatorBootErrorKind.LaunchFailed, result.ErrorKind); + } + + [Test] + public async Task BootTimeout_BootCompletedNeverReaches1 () + { + var devices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35", + }, + }; + + var mockAdb = new MockAdbRunner (devices); + // boot_completed never returns "1" + mockAdb.ShellProperties ["sys.boot_completed"] = "0"; + + var runner = new EmulatorRunner ("/fake/emulator"); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromMilliseconds (200), + PollInterval = TimeSpan.FromMilliseconds (50), + }; + + var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); + + Assert.IsFalse (result.Success); + Assert.That (result.ErrorMessage, Does.Contain ("Timed out")); + } + + [Test] + public void BootEmulatorAsync_InvalidBootTimeout_Throws () + { + var runner = new EmulatorRunner ("/fake/emulator"); + var mockAdb = new MockAdbRunner (new List ()); + var options = new EmulatorBootOptions { BootTimeout = TimeSpan.Zero }; + + Assert.ThrowsAsync (() => + runner.BootEmulatorAsync ("test", mockAdb, options)); + } + + [Test] + public void BootEmulatorAsync_InvalidPollInterval_Throws () + { + var runner = new EmulatorRunner ("/fake/emulator"); + var mockAdb = new MockAdbRunner (new List ()); + var options = new EmulatorBootOptions { PollInterval = TimeSpan.FromMilliseconds (-1) }; + + Assert.ThrowsAsync (() => + runner.BootEmulatorAsync ("test", mockAdb, options)); + } + + [Test] + public async Task MultipleEmulators_FindsCorrectAvd () + { + var devices = new List { + new AdbDeviceInfo { + Serial = "emulator-5554", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_5_API_30", + }, + new AdbDeviceInfo { + Serial = "emulator-5556", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Pixel_7_API_35", + }, + new AdbDeviceInfo { + Serial = "emulator-5558", + Type = AdbDeviceType.Emulator, + Status = AdbDeviceStatus.Online, + AvdName = "Nexus_5X_API_28", + }, + }; + + var mockAdb = new MockAdbRunner (devices); + mockAdb.ShellProperties ["sys.boot_completed"] = "1"; + mockAdb.ShellCommands ["pm path android"] = "package:/system/framework/framework-res.apk"; + + var runner = new EmulatorRunner ("/fake/emulator"); + var options = new EmulatorBootOptions { BootTimeout = TimeSpan.FromSeconds (5), PollInterval = TimeSpan.FromMilliseconds (50) }; + + var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); + + Assert.IsTrue (result.Success); + Assert.AreEqual ("emulator-5556", result.Serial, "Should find the correct AVD among multiple emulators"); + } + + [Test] + public async Task AlreadyOnlinePhysicalDevice_PassesThrough () + { + // Physical devices have non-emulator serials (e.g., USB serial numbers). + // BootEmulatorAsync should recognise them as already-online devices and + // return immediately without attempting to launch an emulator. + var devices = new List { + new AdbDeviceInfo { + Serial = "0A041FDD400327", + Type = AdbDeviceType.Device, + Status = AdbDeviceStatus.Online, + }, + }; + + var mockAdb = new MockAdbRunner (devices); + var runner = new EmulatorRunner ("/fake/emulator"); + + var result = await runner.BootEmulatorAsync ("0A041FDD400327", mockAdb); + + Assert.IsTrue (result.Success); + Assert.AreEqual ("0A041FDD400327", result.Serial); + Assert.IsNull (result.ErrorMessage); + } + + [Test] + public async Task AdditionalArgs_PassedToLaunchEmulator () + { + // Verify that AdditionalArgs from EmulatorBootOptions are forwarded + // to the emulator process. We use a fake emulator script that logs + // its arguments so we can inspect them after the boot times out. + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + var argsLogPath = Path.Combine (tempDir, "args.log"); + + // Rewrite the fake emulator to log its arguments + if (OS.IsWindows) { + File.WriteAllText (emuPath, $"@echo off\r\necho %* > \"{argsLogPath}\"\r\nping -n 60 127.0.0.1 >nul\r\n"); + } else { + File.WriteAllText (emuPath, $"#!/bin/sh\necho \"$@\" > \"{argsLogPath}\"\nsleep 60\n"); + } + + try { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + + var runner = new EmulatorRunner (emuPath); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromMilliseconds (500), + PollInterval = TimeSpan.FromMilliseconds (50), + AdditionalArgs = new List { "-gpu", "auto", "-no-audio" }, + }; + + // Boot will time out (no device appears), but the emulator process + // should have been launched with the additional args. + var result = await runner.BootEmulatorAsync ("Test_AVD", mockAdb, options); + + Assert.IsFalse (result.Success, "Boot should time out"); + + // Give the script a moment to flush args.log + await Task.Delay (200); + + if (File.Exists (argsLogPath)) { + var logged = File.ReadAllText (argsLogPath); + Assert.That (logged, Does.Contain ("-gpu"), "Should contain -gpu arg"); + Assert.That (logged, Does.Contain ("auto"), "Should contain auto value"); + Assert.That (logged, Does.Contain ("-no-audio"), "Should contain -no-audio arg"); + Assert.That (logged, Does.Contain ("-avd"), "Should contain -avd flag"); + Assert.That (logged, Does.Contain ("Test_AVD"), "Should contain AVD name"); + } + } finally { + // Clean up any spawned processes + try { + foreach (var p in Process.GetProcessesByName ("sleep")) { + try { p.Kill (); p.WaitForExit (1000); } catch { } + } + } catch { } + Directory.Delete (tempDir, true); + } + } + + [Test] + public async Task CancellationToken_AbortsBoot () + { + // Verify that cancelling the token during the polling phase causes + // BootEmulatorAsync to return promptly rather than blocking for the + // full BootTimeout duration. We need a real fake emulator script so + // LaunchEmulator succeeds (starts the process), then cancel while polling. + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + + try { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + + var runner = new EmulatorRunner (emuPath); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromSeconds (30), + PollInterval = TimeSpan.FromMilliseconds (50), + }; + + using var cts = new CancellationTokenSource (); + cts.CancelAfter (TimeSpan.FromMilliseconds (300)); + + var sw = Stopwatch.StartNew (); + try { + await runner.BootEmulatorAsync ("Nonexistent_AVD", mockAdb, options, cts.Token); + Assert.Fail ("Should have thrown OperationCanceledException"); + } catch (OperationCanceledException) { + sw.Stop (); + // Should abort well before the 30s BootTimeout + Assert.That (sw.Elapsed.TotalSeconds, Is.LessThan (5), + "Cancellation should abort within a few seconds, not wait for full timeout"); + } + } finally { + try { + foreach (var p in Process.GetProcessesByName ("sleep")) { + try { p.Kill (); p.WaitForExit (1000); } catch { } + } + } catch { } + Directory.Delete (tempDir, true); + } + } + + [Test] + public async Task ColdBoot_PassesNoSnapshotLoad () + { + // Verify that ColdBoot = true causes -no-snapshot-load to be passed. + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + var argsLogPath = Path.Combine (tempDir, "args.log"); + + if (OS.IsWindows) { + File.WriteAllText (emuPath, $"@echo off\r\necho %* > \"{argsLogPath}\"\r\nping -n 60 127.0.0.1 >nul\r\n"); + } else { + File.WriteAllText (emuPath, $"#!/bin/sh\necho \"$@\" > \"{argsLogPath}\"\nsleep 60\n"); + } + + try { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + + var runner = new EmulatorRunner (emuPath); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromMilliseconds (500), + PollInterval = TimeSpan.FromMilliseconds (50), + ColdBoot = true, + }; + + var result = await runner.BootEmulatorAsync ("Test_AVD", mockAdb, options); + + Assert.IsFalse (result.Success, "Boot should time out"); + await Task.Delay (200); + + if (File.Exists (argsLogPath)) { + var logged = File.ReadAllText (argsLogPath); + Assert.That (logged, Does.Contain ("-no-snapshot-load"), "ColdBoot should pass -no-snapshot-load"); + } + } finally { + try { + foreach (var p in Process.GetProcessesByName ("sleep")) { + try { p.Kill (); p.WaitForExit (1000); } catch { } + } + } catch { } + Directory.Delete (tempDir, true); + } + } + + [Test] + public void BootEmulatorAsync_NullAdbRunner_Throws () + { + var runner = new EmulatorRunner ("/fake/emulator"); + + Assert.ThrowsAsync (() => + runner.BootEmulatorAsync ("test", null!)); + } + + [Test] + public void BootEmulatorAsync_EmptyDeviceName_Throws () + { + var runner = new EmulatorRunner ("/fake/emulator"); + var mockAdb = new MockAdbRunner (new List ()); + + Assert.ThrowsAsync (() => + runner.BootEmulatorAsync ("", mockAdb)); + } + + [Test] + [Platform ("Linux,MacOsX")] + public void LaunchEmulator_SurvivesSigint () + { + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + Process? process = null; + try { + var runner = new EmulatorRunner (emuPath); + process = runner.LaunchEmulator ("TestAVD"); + + Assert.IsFalse (process.HasExited, "Process should be running after launch"); + + // Send SIGINT to the emulator process + var killPsi = ProcessUtils.CreateProcessStartInfo ("kill", "-INT", process.Id.ToString ()); + using var kill = new Process { StartInfo = killPsi }; + kill.Start (); + Assert.IsTrue (kill.WaitForExit (5000), "kill command should exit promptly"); + Assert.AreEqual (0, kill.ExitCode, "kill -INT should succeed"); + + // Give the signal a moment to be delivered + Thread.Sleep (500); + + Assert.IsFalse (process.HasExited, "Emulator process should survive SIGINT"); + } finally { + try { process?.Kill (); process?.WaitForExit (5000); } catch { } + process?.Dispose (); + Directory.Delete (tempDir, true); + } + } + + [Test] + public async Task InvalidEmulatorBinary_ReturnsLaunchFailed () + { + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + + // Overwrite with a script that exits immediately with error code 1 + if (OS.IsWindows) { + File.WriteAllText (emuPath, "@echo off\r\nexit /b 1\r\n"); + } else { + File.WriteAllText (emuPath, "#!/bin/sh\nexit 1\n"); + } + + try { + var devices = new List (); + var mockAdb = new MockAdbRunner (devices); + + var runner = new EmulatorRunner (emuPath); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromSeconds (5), + PollInterval = TimeSpan.FromMilliseconds (50), + }; + + var result = await runner.BootEmulatorAsync ("Test_AVD", mockAdb, options); + + Assert.IsFalse (result.Success); + Assert.AreEqual (EmulatorBootErrorKind.LaunchFailed, result.ErrorKind); + Assert.That (result.ErrorMessage, Does.Contain ("exited with code")); + } finally { + Directory.Delete (tempDir, true); + } + } + + [Test] + [Platform ("Linux,MacOsX")] + public void ShellQuote_EscapesSingleQuotes () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"emu-quote-test-{Path.GetRandomFileName ()}"); + var emulatorDir = Path.Combine (tempDir, "emu'dir"); + Directory.CreateDirectory (emulatorDir); + + var emuPath = Path.Combine (emulatorDir, "emulator"); + File.WriteAllText (emuPath, "#!/bin/sh\nsleep 60\n"); + var psi = ProcessUtils.CreateProcessStartInfo ("chmod", "+x", emuPath); + using (var chmod = new Process { StartInfo = psi }) { + chmod.Start (); + Assert.IsTrue (chmod.WaitForExit (5000), "chmod should exit promptly"); + } + + Process? process = null; + try { + var runner = new EmulatorRunner (emuPath); + process = runner.LaunchEmulator ("TestAVD"); + + Assert.IsFalse (process.HasExited, "Process should start even with single-quote in path"); + } finally { + try { process?.Kill (); process?.WaitForExit (5000); } catch { } + process?.Dispose (); + Directory.Delete (tempDir, true); + } + } + + // --- Helpers --- + + static (string tempDir, string emulatorPath) CreateFakeEmulatorSdk () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"emu-boot-test-{Path.GetRandomFileName ()}"); + var emulatorDir = Path.Combine (tempDir, "emulator"); + Directory.CreateDirectory (emulatorDir); + + var emuName = OS.IsWindows ? "emulator.bat" : "emulator"; + var emuPath = Path.Combine (emulatorDir, emuName); + if (OS.IsWindows) { + File.WriteAllText (emuPath, "@echo off\r\nping -n 60 127.0.0.1 >nul\r\n"); + } else { + File.WriteAllText (emuPath, "#!/bin/sh\nsleep 60\n"); + var psi = ProcessUtils.CreateProcessStartInfo ("chmod", "+x", emuPath); + using var chmod = new Process { StartInfo = psi }; + chmod.Start (); + chmod.WaitForExit (); + } + + return (tempDir, emuPath); + } + + static Process? FindEmulatorProcess (string emuPath) + { + // Best-effort: find the process by matching the command line + try { + foreach (var p in Process.GetProcessesByName ("emulator")) { + return p; + } + foreach (var p in Process.GetProcessesByName ("sleep")) { + return p; + } + } catch { } + return null; + } + + /// + /// Mock AdbRunner for testing BootEmulatorAsync without real adb commands. + /// + class MockAdbRunner : AdbRunner + { + readonly List devices; + + public Dictionary ShellProperties { get; } = new Dictionary (StringComparer.OrdinalIgnoreCase); + public Dictionary ShellCommands { get; } = new Dictionary (StringComparer.OrdinalIgnoreCase); + public Action? OnListDevices { get; set; } + + public MockAdbRunner (List devices) + : base ("/fake/adb") + { + this.devices = devices; + } + + public override Task> ListDevicesAsync (CancellationToken cancellationToken = default) + { + OnListDevices?.Invoke (); + return Task.FromResult> (devices); + } + + public override Task GetShellPropertyAsync (string serial, string propertyName, CancellationToken cancellationToken = default) + { + ShellProperties.TryGetValue (propertyName, out var value); + return Task.FromResult (value); + } + + public override Task RunShellCommandAsync (string serial, string command, CancellationToken cancellationToken) + { + ShellCommands.TryGetValue (command, out var value); + return Task.FromResult (value); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/FileUtilTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/FileUtilTests.cs new file mode 100644 index 00000000000..c94d2cc29a7 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/FileUtilTests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class FileUtilTests + { + string tempDir = null!; + Action logger = null!; + + [SetUp] + public void SetUp () + { + tempDir = Path.Combine (Path.GetTempPath (), $"FileUtilTests-{Guid.NewGuid ():N}"); + Directory.CreateDirectory (tempDir); + logger = (level, msg) => TestContext.WriteLine ($"[{level}] {msg}"); + } + + [TearDown] + public void TearDown () + { + if (Directory.Exists (tempDir)) + Directory.Delete (tempDir, recursive: true); + } + + [Test] + public void MoveWithRollback_NewTarget_Succeeds () + { + var source = Path.Combine (tempDir, "source"); + var target = Path.Combine (tempDir, "target"); + Directory.CreateDirectory (source); + File.WriteAllText (Path.Combine (source, "file.txt"), "hello"); + + FileUtil.MoveWithRollback (source, target, logger); + + Assert.IsFalse (Directory.Exists (source), "Source should no longer exist"); + Assert.IsTrue (Directory.Exists (target), "Target should exist"); + Assert.AreEqual ("hello", File.ReadAllText (Path.Combine (target, "file.txt"))); + } + + [Test] + public void MoveWithRollback_ExistingTarget_BacksUpAndReplaces () + { + var source = Path.Combine (tempDir, "source"); + var target = Path.Combine (tempDir, "target"); + Directory.CreateDirectory (source); + File.WriteAllText (Path.Combine (source, "new.txt"), "new"); + + Directory.CreateDirectory (target); + File.WriteAllText (Path.Combine (target, "old.txt"), "old"); + + FileUtil.MoveWithRollback (source, target, logger); + + Assert.IsFalse (Directory.Exists (source), "Source should no longer exist"); + Assert.IsTrue (File.Exists (Path.Combine (target, "new.txt")), "New file should exist"); + Assert.IsFalse (File.Exists (Path.Combine (target, "old.txt")), "Old file should be gone"); + } + + [Test] + public void MoveWithRollback_SourceDoesNotExist_RestoresBackup () + { + var source = Path.Combine (tempDir, "nonexistent"); + var target = Path.Combine (tempDir, "target"); + + // Create an existing target that should be backed up and restored + Directory.CreateDirectory (target); + File.WriteAllText (Path.Combine (target, "original.txt"), "preserve me"); + + Assert.Throws (() => FileUtil.MoveWithRollback (source, target, logger)); + + // The original target should be restored from backup + Assert.IsTrue (Directory.Exists (target), "Target should be restored"); + Assert.AreEqual ("preserve me", File.ReadAllText (Path.Combine (target, "original.txt"))); + } + + [Test] + public void MoveWithRollback_SourceDoesNotExist_NoExistingTarget_Throws () + { + var source = Path.Combine (tempDir, "nonexistent"); + var target = Path.Combine (tempDir, "also-nonexistent"); + + Assert.Throws (() => FileUtil.MoveWithRollback (source, target, logger)); + Assert.IsFalse (Directory.Exists (target)); + } + + [Test] + public void IsUnderDirectory_ChildPath_ReturnsTrue () + { + var parent = Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs"); + var child = Path.Combine (parent, "java", "jdk-21"); + Assert.IsTrue (FileUtil.IsUnderDirectory (child, parent)); + } + + [Test] + public void IsUnderDirectory_ExactMatch_ReturnsTrue () + { + var dir = Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs"); + Assert.IsTrue (FileUtil.IsUnderDirectory (dir, dir)); + } + + [Test] + public void IsUnderDirectory_SiblingPath_ReturnsFalse () + { + Assert.IsFalse (FileUtil.IsUnderDirectory ( + Path.Combine ($"{Path.DirectorySeparatorChar}opt", "data", "java"), + Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs"))); + } + + [Test] + public void IsUnderDirectory_DifferentRoot_ReturnsFalse () + { + Assert.IsFalse (FileUtil.IsUnderDirectory ( + Path.Combine ($"{Path.DirectorySeparatorChar}other", "java"), + Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs"))); + } + + [TestCase (null, "/dir")] + [TestCase ("/dir", null)] + [TestCase ("", "/dir")] + [TestCase ("/dir", "")] + [TestCase (null, null)] + public void IsUnderDirectory_NullOrEmpty_ReturnsFalse (string path, string directory) + { + Assert.IsFalse (FileUtil.IsUnderDirectory (path!, directory!)); + } + + [Test] + public void IsUnderDirectory_CaseInsensitive () + { + var parent = Path.Combine ($"{Path.DirectorySeparatorChar}opt", "Programs"); + var child = Path.Combine ($"{Path.DirectorySeparatorChar}opt", "PROGRAMS", "java"); + Assert.IsTrue (FileUtil.IsUnderDirectory (child, parent)); + } + + [Test] + public void IsUnderDirectory_PartialDirNameMatch_ReturnsFalse () + { + var parent = Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs"); + Assert.IsFalse (FileUtil.IsUnderDirectory ( + Path.Combine ($"{Path.DirectorySeparatorChar}opt", "programs-extra", "java"), + parent)); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInfoTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInfoTests.cs new file mode 100644 index 00000000000..3ec1a50e9aa --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInfoTests.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.IO; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class JdkInfoTests + { + [Test] + public void GetKnownSystemJdkInfos_PrefersJiJavaHome () + { + var previous = Environment.GetEnvironmentVariable ("JI_JAVA_HOME", EnvironmentVariableTarget.Process); + try { + Environment.SetEnvironmentVariable ("JI_JAVA_HOME", FauxJdkDir, EnvironmentVariableTarget.Process); + + var defaultJdkDir = JdkInfo.GetKnownSystemJdkInfos () + .FirstOrDefault (); + Assert.IsNotNull (defaultJdkDir); + Assert.AreEqual (FauxJdkDir, defaultJdkDir.HomePath); + } + finally { + Environment.SetEnvironmentVariable ("JI_JAVA_HOME", previous, EnvironmentVariableTarget.Process); + } + } + + [Test] + public void Constructor_NullPath () + { + Assert.Throws(() => new JdkInfo (null)); + } + + [Test] + public void Constructor_InvalidPath () + { + var dir = Path.GetTempFileName (); + File.Delete (dir); + Directory.CreateDirectory (dir); + Assert.Throws(() => new JdkInfo (dir)); + Directory.Delete (dir); + } + + string FauxJdkDir; + + [OneTimeSetUp] + public void CreateFauxJdk () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + CreateFauxJdk (dir, releaseVersion: "1.2.3", releaseBuildNumber: "42", javaVersion: "100.100.100-100"); + + FauxJdkDir = dir; + } + + internal static void CreateFauxJdk (string dir, string releaseVersion, string releaseBuildNumber, string javaVersion) + { + Directory.CreateDirectory (dir); + + using (var release = new StreamWriter (Path.Combine (dir, "release"))) { + release.WriteLine ($"JAVA_VERSION=\"{releaseVersion}\""); + release.WriteLine ($"BUILD_NUMBER={releaseBuildNumber}"); + release.WriteLine ($"JUST_A_KEY"); + } + + var bin = Path.Combine (dir, "bin"); + var inc = Path.Combine (dir, "include"); + var jre = Path.Combine (dir, "jre"); + var jli = Path.Combine (jre, "lib", "jli"); + + Directory.CreateDirectory (bin); + Directory.CreateDirectory (inc); + Directory.CreateDirectory (jli); + Directory.CreateDirectory (jre); + + string quote = OS.IsWindows ? "" : "\""; + string java = + $"echo {quote}Property settings:{quote}{Environment.NewLine}" + + $"echo {quote} java.home = {dir}{quote}{Environment.NewLine}" + + $"echo {quote} java.vendor = Xamarin.Android Unit Tests{quote}{Environment.NewLine}" + + $"echo {quote} java.version = {javaVersion}{quote}{Environment.NewLine}" + + $"echo {quote} xamarin.multi-line = line the first{quote}{Environment.NewLine}" + + $"echo {quote} line the second{quote}{Environment.NewLine}" + + $"echo {quote} .{quote}{Environment.NewLine}"; + + if (OS.IsWindows) { + java = $"@echo off{Environment.NewLine}{java}"; + } + + CreateShellScript (Path.Combine (bin, "jar"), ""); + CreateShellScript (Path.Combine (bin, "java"), java); + CreateShellScript (Path.Combine (bin, "javac"), ""); + CreateShellScript (Path.Combine (jli, "libjli.dylib"), ""); + CreateShellScript (Path.Combine (jre, "libjvm.so"), ""); + CreateShellScript (Path.Combine (jre, "jvm.dll"), ""); + } + + [OneTimeTearDown] + public void DeleteFauxJdk () + { + Directory.Delete (FauxJdkDir, recursive: true); + } + + static void CreateShellScript (string path, string contents) + { + if (OS.IsWindows && string.Compare (Path.GetExtension (path), ".dll", true) != 0) + path += ".cmd"; + using (var script = new StreamWriter (path)) { + if (!OS.IsWindows) { + script.WriteLine ("#!/bin/sh"); + } + script.WriteLine (contents); + } + if (OS.IsWindows) + return; + var chmod = new ProcessStartInfo { + FileName = "chmod", + Arguments = $"+x \"{path}\"", + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + var p = Process.Start (chmod); + p.WaitForExit (); + } + + [Test] + public void PathPropertyValidation () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (jdk.HomePath, FauxJdkDir); + Assert.IsTrue (File.Exists (jdk.JarPath)); + Assert.IsTrue (File.Exists (jdk.JavaPath)); + Assert.IsTrue (File.Exists (jdk.JavacPath)); + Assert.IsTrue (File.Exists (jdk.JdkJvmPath)); + Assert.IsTrue (Directory.Exists (jdk.IncludePath [0])); + } + + [Test] + public void VersionPrefersRelease () + { + var jdk = new JdkInfo (FauxJdkDir); + // Note: `release` has JAVA_VERSION=1.2.3 + BUILD_NUMBER=42, while `java` prints java.version=100.100.100. + // We prefer the value constructed from `release`. + Assert.AreEqual (jdk.Version, new Version ("1.2.3.42")); + } + + [Test] + public void ReleaseProperties () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (3, jdk.ReleaseProperties.Count); + Assert.AreEqual ("1.2.3", jdk.ReleaseProperties ["JAVA_VERSION"]); + Assert.AreEqual ("42", jdk.ReleaseProperties ["BUILD_NUMBER"]); + Assert.AreEqual ("", jdk.ReleaseProperties ["JUST_A_KEY"]); + } + + [Test] + public void JavaSettingsProperties () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (4, jdk.JavaSettingsPropertyKeys.Count ()); + + Assert.IsFalse(jdk.GetJavaSettingsPropertyValue ("does-not-exist", out var _)); + Assert.IsFalse(jdk.GetJavaSettingsPropertyValues ("does-not-exist", out var _)); + + Assert.IsTrue (jdk.GetJavaSettingsPropertyValue ("java.home", out var home)); + Assert.AreEqual (FauxJdkDir, home); + + Assert.IsTrue (jdk.GetJavaSettingsPropertyValue ("java.version", out var version)); + Assert.AreEqual ("100.100.100-100", version); + + Assert.IsTrue (jdk.GetJavaSettingsPropertyValue ("java.vendor", out var vendor)); + Assert.AreEqual ("Xamarin.Android Unit Tests", vendor); + Assert.AreEqual (vendor, jdk.Vendor); + + Assert.Throws(() => jdk.GetJavaSettingsPropertyValue ("xamarin.multi-line", out var _)); + Assert.IsTrue (jdk.GetJavaSettingsPropertyValues ("xamarin.multi-line", out var lines)); + Assert.AreEqual (3, lines.Count ()); + Assert.AreEqual ("line the first", lines.ElementAt (0)); + Assert.AreEqual ("line the second", lines.ElementAt (1)); + Assert.AreEqual (".", lines.ElementAt (2)); + } + + [Test] + public void ParseOracleReleaseVersion () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + try { + CreateFauxJdk (dir, releaseVersion: "1.2.3_4", releaseBuildNumber: "", javaVersion: "100.100.100_100"); + var jdk = new JdkInfo (dir); + Assert.AreEqual (new Version (1, 2, 3, 4), jdk.Version); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void ParseOracleJavaVersion () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + try { + CreateFauxJdk (dir, releaseVersion: "", releaseBuildNumber: "", javaVersion: "101.102.103_104"); + var jdk = new JdkInfo (dir); + Assert.AreEqual (new Version (101, 102, 103, 104), jdk.Version); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void ParseMicrosoftReleaseVersion () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + try { + CreateFauxJdk (dir, releaseVersion: "1.2.3", releaseBuildNumber: "4", javaVersion: "100.100.100_100"); + var jdk = new JdkInfo (dir); + Assert.AreEqual (new Version (1, 2, 3, 4), jdk.Version); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void ParseMicrosoftJavaVersion() + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + try { + CreateFauxJdk (dir, releaseVersion: "", releaseBuildNumber: "", javaVersion: "1.2.3-4"); + var jdk = new JdkInfo (dir); + Assert.AreEqual (new Version (1, 2, 3, 4), jdk.Version); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void Version_ThrowsNotSupportedException () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + + try { + CreateFauxJdk (dir, releaseVersion: "", releaseBuildNumber: "", javaVersion: ""); + var jdk = new JdkInfo (dir); + Assert.Throws (() => { var _ = jdk.Version; }); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + [TestCase ("jdk-21.0.99")] + [TestCase ("jdk-21.0.8.1-hotspot")] + [TestCase ("microsoft-21.jdk")] + public void GetKnownSystemJdkInfos_DiscoversWindowsUserJdk (string folderName) + { + if (!OS.IsWindows) { + Assert.Ignore ("This test is only valid on Windows."); + return; + } + + var userJdkRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android"); + var testJdkDir = Path.Combine (userJdkRoot, folderName); + + try { + CreateFauxJdk (testJdkDir, releaseVersion: "21.0.99", releaseBuildNumber: "1", javaVersion: "21.0.99-1"); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + }; + + var jdks = JdkInfo.GetKnownSystemJdkInfos (logger).ToList (); + var foundJdk = jdks.FirstOrDefault (j => j.HomePath == testJdkDir); + + Assert.IsNotNull (foundJdk, $"Expected to find JDK at {testJdkDir} with folder name '{folderName}'"); + Assert.AreEqual (@"%LocalAppData%\Android\*jdk*", foundJdk.Locator, $"Locator should indicate user JDK path for folder '{folderName}'"); + Assert.AreEqual (new Version (21, 0, 99, 1), foundJdk.Version); + } + finally { + if (Directory.Exists (testJdkDir)) + Directory.Delete (testJdkDir, recursive: true); + // Clean up the parent directories if they're empty + if (Directory.Exists (userJdkRoot) && !Directory.EnumerateFileSystemEntries (userJdkRoot).Any ()) + Directory.Delete (userJdkRoot); + } + } + + [Test] + [TestCase ("microsoft-21.jdk", false)] + [TestCase ("jdk-21", true)] + [TestCase ("jdk-21.0.8.1-hotspot", true)] + [TestCase ("temurin-21.jdk", false)] + public void GetKnownSystemJdkInfos_DiscoversMacOSUserJdk (string folderName, bool isFlat) + { + if (!OS.IsMac) { + Assert.Ignore ("This test is only valid on macOS."); + return; + } + + var userJdkRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Android"); + string testJdkDir; + string testJdkBundle; + + if (isFlat) { + // Flat structure: release file directly in folder + testJdkDir = Path.Combine (userJdkRoot, folderName); + testJdkBundle = testJdkDir; + } else { + // Bundle structure: Contents/Home inside folder + testJdkBundle = Path.Combine (userJdkRoot, folderName); + testJdkDir = Path.Combine (testJdkBundle, "Contents", "Home"); + } + + try { + CreateFauxJdk (testJdkDir, releaseVersion: "21.0.99", releaseBuildNumber: "1", javaVersion: "21.0.99-1"); + + Action logger = (level, message) => { + Console.WriteLine ($"[{level}] {message}"); + }; + + var jdks = JdkInfo.GetKnownSystemJdkInfos (logger).ToList (); + var foundJdk = jdks.FirstOrDefault (j => j.HomePath == testJdkDir); + + Assert.IsNotNull (foundJdk, $"Expected to find JDK at {testJdkDir} with folder name '{folderName}' (isFlat={isFlat})"); + Assert.AreEqual ("~/Library/Android/*jdk*/", foundJdk.Locator, $"Locator should indicate user JDK path for folder '{folderName}'"); + Assert.AreEqual (new Version (21, 0, 99, 1), foundJdk.Version); + } + finally { + if (Directory.Exists (testJdkBundle)) + Directory.Delete (testJdkBundle, recursive: true); + // Clean up the parent directories if they're empty + if (Directory.Exists (userJdkRoot) && !Directory.EnumerateFileSystemEntries (userJdkRoot).Any ()) + Directory.Delete (userJdkRoot); + } + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInstallerTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInstallerTests.cs new file mode 100644 index 00000000000..2e1530564ab --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInstallerTests.cs @@ -0,0 +1,266 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class JdkInstallerTests + { + JdkInstaller installer; + + [SetUp] + public void SetUp () + { + installer = new JdkInstaller (logger: (level, message) => { + TestContext.WriteLine ($"[{level}] {message}"); + }); + } + + [TearDown] + public void TearDown () + { + installer?.Dispose (); + installer = null!; + } + + [Test] + public void IsValid_NullPath_ReturnsFalse () + { + Assert.IsFalse (installer.IsValid (null!)); + } + + [Test] + public void IsValid_EmptyPath_ReturnsFalse () + { + Assert.IsFalse (installer.IsValid ("")); + } + + [Test] + public void IsValid_NonExistentPath_ReturnsFalse () + { + Assert.IsFalse (installer.IsValid (Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()))); + } + + [Test] + public void IsValid_EmptyDirectory_ReturnsFalse () + { + var dir = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()); + Directory.CreateDirectory (dir); + try { + Assert.IsFalse (installer.IsValid (dir)); + } + finally { + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void IsValid_FauxJdk_ReturnsTrue () + { + var dir = Path.Combine (Path.GetTempPath (), $"jdk-test-{Guid.NewGuid ()}"); + try { + JdkInfoTests.CreateFauxJdk (dir, releaseVersion: "17.0.1", releaseBuildNumber: "1", javaVersion: "17.0.1-1"); + Assert.IsTrue (installer.IsValid (dir)); + } + finally { + if (Directory.Exists (dir)) + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void IsValid_SystemJdk_ReturnsTrue () + { + // Find first known JDK on the system + var jdk = JdkInfo.GetKnownSystemJdkInfos ().FirstOrDefault (); + if (jdk is null) { + Assert.Ignore ("No system JDK found to validate."); + return; + } + Assert.IsTrue (installer.IsValid (jdk.HomePath)); + } + + [Test] + public async Task DiscoverAsync_ReturnsVersions () + { + IReadOnlyList versions; + try { + using (var cts = new CancellationTokenSource (TimeSpan.FromSeconds (15))) { + versions = await installer.DiscoverAsync (cts.Token); + } + } + catch (Exception ex) when (ex is System.Net.Http.HttpRequestException || ex is TaskCanceledException || ex is OperationCanceledException) { + Assert.Ignore ($"Network unavailable: {ex.Message}"); + return; + } + + // We should get at least one version (if network is available) + Assert.IsNotNull (versions); + if (versions.Count == 0) { + Assert.Ignore ("No versions returned (network may be restricted)."); + return; + } + + // Verify structure of returned info + foreach (var v in versions) { + Assert.Greater (v.MajorVersion, 0, "MajorVersion should be positive"); + Assert.IsNotEmpty (v.DisplayName, "DisplayName should not be empty"); + Assert.IsNotEmpty (v.DownloadUrl, "DownloadUrl should not be empty"); + Assert.That (v.DownloadUrl, Does.Contain ("aka.ms/download-jdk"), "DownloadUrl should use Microsoft OpenJDK"); + } + } + + [Test] + public async Task DiscoverAsync_ContainsExpectedMajorVersions () + { + IReadOnlyList versions; + try { + versions = await installer.DiscoverAsync (); + } + catch (Exception ex) when (ex is System.Net.Http.HttpRequestException || ex is TaskCanceledException) { + Assert.Ignore ($"Network unavailable: {ex.Message}"); + return; + } + + if (versions.Count == 0) { + Assert.Ignore ("No versions returned."); + return; + } + + var majorVersions = versions.Select (v => v.MajorVersion).Distinct ().ToList (); + Assert.That (majorVersions, Does.Contain (21), "Should contain JDK 21"); + } + + [Test] + public async Task DiscoverAsync_CancellationToken_Cancels () + { + using var cts = new CancellationTokenSource (); + cts.Cancel (); + + Assert.ThrowsAsync ( + async () => await installer.DiscoverAsync (cts.Token)); + } + + [Test] + public void InstallAsync_InvalidVersion_Throws () + { + Assert.ThrowsAsync ( + async () => await installer.InstallAsync (8, Path.GetTempPath ())); + } + + [Test] + public void InstallAsync_NullPath_Throws () + { + Assert.ThrowsAsync ( + async () => await installer.InstallAsync (21, null!)); + } + + [Test] + public async Task InstallAsync_ReportsProgress () + { + // This test actually downloads and installs a JDK, so it may be slow. + // Skip if running in CI or if network is unavailable. + if (Environment.GetEnvironmentVariable ("CI") is not null || + Environment.GetEnvironmentVariable ("TF_BUILD") is not null) { + Assert.Ignore ("Skipping download test in CI environment."); + return; + } + + var targetPath = Path.Combine (Path.GetTempPath (), $"jdk-install-test-{Guid.NewGuid ()}"); + var reportedPhases = new List (); + var progress = new Progress (p => { + reportedPhases.Add (p.Phase); + }); + + try { + using var cts = new CancellationTokenSource (TimeSpan.FromMinutes (10)); + await installer.InstallAsync (21, targetPath, progress, cts.Token); + + // Verify installation + Assert.IsTrue (installer.IsValid (targetPath), "Installed JDK should be valid"); + Assert.IsTrue (reportedPhases.Contains (JdkInstallPhase.Downloading), "Should report Downloading phase"); + Assert.IsTrue (reportedPhases.Contains (JdkInstallPhase.Extracting), "Should report Extracting phase"); + Assert.IsTrue (reportedPhases.Contains (JdkInstallPhase.Complete), "Should report Complete phase"); + + // Verify we can create a JdkInfo from it + var jdkInfo = new JdkInfo (targetPath); + Assert.IsNotNull (jdkInfo.Version); + Assert.AreEqual (21, jdkInfo.Version!.Major); + } + catch (Exception ex) when (ex is System.Net.Http.HttpRequestException || ex is TaskCanceledException) { + Assert.Ignore ($"Network unavailable: {ex.Message}"); + } + finally { + if (Directory.Exists (targetPath)) + Directory.Delete (targetPath, recursive: true); + } + } + + [Test] + public void Constructor_DefaultLogger_DoesNotThrow () + { + using var defaultInstaller = new JdkInstaller (); + Assert.IsNotNull (defaultInstaller); + } + + [Test] + public void RecommendedMajorVersion_Is21 () + { + Assert.AreEqual (21, JdkInstaller.RecommendedMajorVersion); + } + + [Test] + public void SupportedVersions_ContainsExpected () + { + Assert.That (JdkInstaller.SupportedVersions, Does.Contain (21)); + } + + [Test] + public void IsTargetPathWritable_TempDir_ReturnsTrue () + { + var dir = Path.Combine (Path.GetTempPath (), $"jdk-write-test-{Guid.NewGuid ()}"); + try { + Assert.IsTrue (FileUtil.IsTargetPathWritable (dir, (level, msg) => TestContext.WriteLine ($"[{level}] {msg}"))); + } + finally { + if (Directory.Exists (dir)) + Directory.Delete (dir, recursive: true); + } + } + + [Test] + public void IsTargetPathWritable_NullOrEmpty_ReturnsFalse () + { + var logger = new Action ((level, msg) => TestContext.WriteLine ($"[{level}] {msg}")); + Assert.IsFalse (FileUtil.IsTargetPathWritable (null!, logger)); + Assert.IsFalse (FileUtil.IsTargetPathWritable ("", logger)); + } + + [Test] + public void Remove_NonExistentPath_ReturnsFalse () + { + Assert.IsFalse (installer.Remove (Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ()))); + } + + [Test] + public void Remove_ExistingDirectory_RemovesIt () + { + var dir = Path.Combine (Path.GetTempPath (), $"jdk-remove-test-{Guid.NewGuid ()}"); + Directory.CreateDirectory (dir); + File.WriteAllText (Path.Combine (dir, "test.txt"), "test"); + + Assert.IsTrue (installer.Remove (dir)); + Assert.IsFalse (Directory.Exists (dir)); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkVersionInfoTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkVersionInfoTests.cs new file mode 100644 index 00000000000..b37f486ef40 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkVersionInfoTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class JdkVersionInfoTests + { + [Test] + public void Constructor_SetsAllProperties () + { + var info = new JdkVersionInfo ( + majorVersion: 21, + displayName: "Microsoft OpenJDK 21", + downloadUrl: "https://example.com/jdk-21.zip", + checksumUrl: "https://example.com/jdk-21.zip.sha256sum.txt", + size: 123456789, + checksum: "abc123"); + + Assert.AreEqual (21, info.MajorVersion); + Assert.AreEqual ("Microsoft OpenJDK 21", info.DisplayName); + Assert.AreEqual ("https://example.com/jdk-21.zip", info.DownloadUrl); + Assert.AreEqual ("https://example.com/jdk-21.zip.sha256sum.txt", info.ChecksumUrl); + Assert.AreEqual (123456789, info.Size); + Assert.AreEqual ("abc123", info.Checksum); + } + + [Test] + public void Constructor_DefaultSizeAndChecksum () + { + var info = new JdkVersionInfo ( + majorVersion: 17, + displayName: "Microsoft OpenJDK 17", + downloadUrl: "https://example.com/jdk-17.zip", + checksumUrl: "https://example.com/jdk-17.zip.sha256sum.txt"); + + Assert.AreEqual (0, info.Size); + Assert.IsNull (info.Checksum); + } + + [Test] + public void ToString_ReturnsDisplayName () + { + var info = new JdkVersionInfo (21, "Microsoft OpenJDK 21", "https://example.com/dl", "https://example.com/cs"); + Assert.AreEqual ("Microsoft OpenJDK 21", info.ToString ()); + } + + [Test] + public void MutableProperties_CanBeSet () + { + var info = new JdkVersionInfo (21, "Test", "https://example.com/dl", "https://example.com/cs"); + + info.Size = 999; + info.Checksum = "deadbeef"; + info.ResolvedUrl = "https://resolved.example.com/jdk-21.0.5.zip"; + + Assert.AreEqual (999, info.Size); + Assert.AreEqual ("deadbeef", info.Checksum); + Assert.AreEqual ("https://resolved.example.com/jdk-21.0.5.zip", info.ResolvedUrl); + } + + [Test] + public void ResolvedUrl_DefaultsToNull () + { + var info = new JdkVersionInfo (21, "Test", "https://example.com/dl", "https://example.com/cs"); + Assert.IsNull (info.ResolvedUrl); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/ProcessUtilsTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/ProcessUtilsTests.cs new file mode 100644 index 00000000000..a9e7be31d8a --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/ProcessUtilsTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class ProcessUtilsTests + { + [Test] + public void CreateProcessStartInfo_SetsFileName () + { + var psi = ProcessUtils.CreateProcessStartInfo ("myapp"); + Assert.AreEqual ("myapp", psi.FileName); + } + + [Test] + public void CreateProcessStartInfo_SetsShellAndWindow () + { + var psi = ProcessUtils.CreateProcessStartInfo ("myapp"); + Assert.IsFalse (psi.UseShellExecute, "UseShellExecute should be false"); + Assert.IsTrue (psi.CreateNoWindow, "CreateNoWindow should be true"); + } + + [Test] + public void CreateProcessStartInfo_NoArgs () + { + var psi = ProcessUtils.CreateProcessStartInfo ("myapp"); + Assert.AreEqual (0, psi.ArgumentList.Count); + } + + [Test] + public void CreateProcessStartInfo_SingleArg () + { + var psi = ProcessUtils.CreateProcessStartInfo ("myapp", "--version"); + Assert.AreEqual (1, psi.ArgumentList.Count); + Assert.AreEqual ("--version", psi.ArgumentList [0]); + } + + [Test] + public void CreateProcessStartInfo_MultipleArgs () + { + var psi = ProcessUtils.CreateProcessStartInfo ("tar", "-xzf", "archive.tar.gz", "-C", "/tmp/output"); + Assert.AreEqual (4, psi.ArgumentList.Count); + Assert.AreEqual ("-xzf", psi.ArgumentList [0]); + Assert.AreEqual ("archive.tar.gz", psi.ArgumentList [1]); + Assert.AreEqual ("-C", psi.ArgumentList [2]); + Assert.AreEqual ("/tmp/output", psi.ArgumentList [3]); + } + + [Test] + public void CreateProcessStartInfo_ArgWithSpaces () + { + var psi = ProcessUtils.CreateProcessStartInfo ("cmd", "/c", "path with spaces"); + Assert.AreEqual (2, psi.ArgumentList.Count); + Assert.AreEqual ("path with spaces", psi.ArgumentList [1]); + } + + [Test] + public void IsElevated_DoesNotThrow () + { + // Smoke test: just verify it returns without crashing + bool result = ProcessUtils.IsElevated (); + Assert.That (result, Is.TypeOf ()); + } + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Resources/manifest-simplewidget.xml b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Resources/manifest-simplewidget.xml new file mode 100644 index 00000000000..b3e146a099e --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Resources/manifest-simplewidget.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/RunnerIntegrationTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/RunnerIntegrationTests.cs new file mode 100644 index 00000000000..5f771026cc4 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/RunnerIntegrationTests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests; + +/// +/// Integration tests that verify AdbRunner works against real Android SDK tools. +/// +/// These tests only run on CI (TF_BUILD=True or CI=true) where hosted +/// images have JDK (JAVA_HOME) and Android SDK (ANDROID_HOME) pre-installed. +/// Tests are skipped on local developer machines. +/// +[TestFixture] +[Category ("Integration")] +public class RunnerIntegrationTests +{ + static string sdkPath; + static string jdkPath; + static string adbPath; + static SdkManager sdkManager; + + static void Log (TraceLevel level, string message) + { + TestContext.Progress.WriteLine ($"[{level}] {message}"); + } + + static void RequireCi () + { + var tfBuild = Environment.GetEnvironmentVariable ("TF_BUILD"); + var ci = Environment.GetEnvironmentVariable ("CI"); + + if (!string.Equals (tfBuild, "true", StringComparison.OrdinalIgnoreCase) && + !string.Equals (ci, "true", StringComparison.OrdinalIgnoreCase)) { + Assert.Ignore ("Integration tests only run on CI (TF_BUILD=True or CI=true)."); + } + } + + /// + /// One-time setup: use pre-installed JDK/SDK on CI agents. + /// Azure Pipelines hosted images have JAVA_HOME and ANDROID_HOME already configured. + /// + [OneTimeSetUp] + public void OneTimeSetUp () + { + RequireCi (); + + // Use pre-installed JDK from JAVA_HOME (always available on CI agents) + jdkPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.JavaHome); + if (string.IsNullOrEmpty (jdkPath) || !Directory.Exists (jdkPath)) { + Assert.Ignore ("JAVA_HOME not set or invalid — cannot run integration tests."); + return; + } + TestContext.Progress.WriteLine ($"Using JDK from JAVA_HOME: {jdkPath}"); + + // Use pre-installed Android SDK from ANDROID_HOME (must be available on CI agents) + sdkPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidHome); + if (string.IsNullOrEmpty (sdkPath) || !Directory.Exists (sdkPath)) { + Assert.Ignore ("ANDROID_HOME not set or invalid — cannot run integration tests. Provision Android SDK and set ANDROID_HOME to enable these tests."); + return; + } + + TestContext.Progress.WriteLine ($"Using SDK from ANDROID_HOME: {sdkPath}"); + sdkManager = new SdkManager (Log); + sdkManager.JavaSdkPath = jdkPath; + sdkManager.AndroidSdkPath = sdkPath; + + // Resolve the full path to adb for AdbRunner + var adbExe = OS.IsWindows ? "adb.exe" : "adb"; + adbPath = Path.Combine (sdkPath, "platform-tools", adbExe); + if (!File.Exists (adbPath)) + Assert.Ignore ($"adb not found at {adbPath}"); + } + + [OneTimeTearDown] + public void OneTimeTearDown () + { + sdkManager?.Dispose (); + } + + [Test] + public void AdbRunner_Constructor_AcceptsValidPath () + { + var runner = new AdbRunner (adbPath); + Assert.IsNotNull (runner); + } + + [Test] + public async Task AdbRunner_ListDevicesAsync_ReturnsWithoutError () + { + var runner = new AdbRunner (adbPath); + + // On CI there are no physical devices or emulators, but the command + // should succeed and return an empty (or non-null) list. + var devices = await runner.ListDevicesAsync (); + + Assert.IsNotNull (devices); + TestContext.Progress.WriteLine ($"ListDevicesAsync returned {devices.Count} device(s)"); + } + + [Test] + public void AdbRunner_WaitForDeviceAsync_TimesOut_WhenNoDevice () + { + var runner = new AdbRunner (adbPath); + var ex = Assert.ThrowsAsync (async () => + await runner.WaitForDeviceAsync (timeout: TimeSpan.FromSeconds (5))); + + Assert.That (ex, Is.Not.Null); + TestContext.Progress.WriteLine ($"WaitForDeviceAsync timed out as expected: {ex?.Message}"); + } + + [Test] + public void AllRunners_ToolDiscovery_ConsistentWithSdk () + { + var runner = new AdbRunner (adbPath); + + // adb path should be under the SDK + Assert.IsTrue (File.Exists (adbPath), $"adb should exist at {adbPath}"); + StringAssert.StartsWith (sdkPath, adbPath); + } + + [Test] + public void AvdManagerRunner_ToolDiscovery_FindsAvdManager () + { + var ext = OS.IsWindows ? ".bat" : ""; + var avdManagerPath = ProcessUtils.FindCmdlineTool (sdkPath, "avdmanager", ext); + + // avdmanager may not be present if cmdline-tools are not installed + if (avdManagerPath is null) { + Assert.Ignore ("avdmanager not found in SDK — cmdline-tools may not be installed."); + return; + } + + Assert.IsTrue (File.Exists (avdManagerPath), $"avdmanager should exist at {avdManagerPath}"); + TestContext.Progress.WriteLine ($"Found avdmanager at: {avdManagerPath}"); + } + + [Test] + public async Task AvdManagerRunner_ListAvdsAsync_ReturnsWithoutError () + { + var ext = OS.IsWindows ? ".bat" : ""; + var avdManagerPath = ProcessUtils.FindCmdlineTool (sdkPath, "avdmanager", ext); + + if (avdManagerPath is null) { + Assert.Ignore ("avdmanager not found in SDK — cmdline-tools may not be installed."); + return; + } + + var env = new System.Collections.Generic.Dictionary { + { EnvironmentVariableNames.JavaHome, jdkPath }, + { EnvironmentVariableNames.AndroidHome, sdkPath }, + }; + + var runner = new AvdManagerRunner (avdManagerPath, env); + var avds = await runner.ListAvdsAsync (); + + Assert.IsNotNull (avds); + TestContext.Progress.WriteLine ($"ListAvdsAsync returned {avds.Count} AVD(s)"); + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/SdkManagerTests.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/SdkManagerTests.cs new file mode 100644 index 00000000000..ab931c07a4f --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/SdkManagerTests.cs @@ -0,0 +1,515 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests; +[TestFixture] +public class SdkManagerTests +{ + SdkManager manager; + + [SetUp] + public void SetUp () + { + manager = new SdkManager (logger: (level, message) => { + TestContext.WriteLine ($"[{level}] {message}"); + }); + } + + [TearDown] + public void TearDown () + { + manager?.Dispose (); + manager = null; + } + + [Test] + public void ParseManifest_CmdlineTools_ReturnsComponents () + { + var xml = @" + + + + https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip + https://dl.google.com/android/repository/commandlinetools-mac-13114758_latest.zip + https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip + + + + + https://dl.google.com/android/repository/platform-tools_r36.0.0-linux.zip + https://dl.google.com/android/repository/platform-tools_r36.0.0-darwin.zip + https://dl.google.com/android/repository/platform-tools_r36.0.0-win.zip + + +"; + + var components = manager.ParseManifest (xml); + + Assert.IsNotNull (components); + Assert.IsTrue (components.Count >= 2, $"Expected at least 2 components, got {components.Count}"); + + var cmdline = components.FirstOrDefault (c => c.ElementName == "cmdline-tools"); + Assert.IsNotNull (cmdline, "Should find cmdline-tools component"); + Assert.AreEqual ("19.0", cmdline!.Revision); + Assert.IsNotEmpty (cmdline.DownloadUrl!); + Assert.IsNotEmpty (cmdline.Checksum!); + Assert.AreEqual (ChecksumType.Sha1, cmdline.ChecksumType); + Assert.Greater (cmdline.Size, 0); + + var platformTools = components.FirstOrDefault (c => c.ElementName == "platform-tools"); + Assert.IsNotNull (platformTools, "Should find platform-tools component"); + Assert.AreEqual ("36.0.0", platformTools!.Revision); + } + + [Test] + public void ParseManifest_ObsoleteComponents_AreIncluded () + { + var xml = @" + + + + https://example.com/old.zip + https://example.com/old-mac.zip + https://example.com/old-win.zip + + +"; + + var components = manager.ParseManifest (xml); + var obsolete = components.FirstOrDefault (c => c.Revision == "9.0"); + Assert.IsNotNull (obsolete); + Assert.IsTrue (obsolete!.IsObsolete); + } + + [Test] + public void ParseManifest_EmptyXml_ReturnsEmpty () + { + var xml = @""; + var components = manager.ParseManifest (xml); + Assert.IsNotNull (components); + Assert.AreEqual (0, components.Count); + } + + [Test] + public void ParseManifest_MultipleVersions_AllReturned () + { + var xml = @" + + + + https://example.com/19.zip + https://example.com/19-mac.zip + https://example.com/19-win.zip + + + + + https://example.com/17.zip + https://example.com/17-mac.zip + https://example.com/17-win.zip + + +"; + + var components = manager.ParseManifest (xml); + var cmdlineTools = components.Where (c => c.ElementName == "cmdline-tools").ToList (); + Assert.AreEqual (2, cmdlineTools.Count, "Should find both cmdline-tools versions"); + } + + [Test] + public void ParseManifest_JdkElements_Parsed () + { + var xml = @" + + + + https://aka.ms/download-jdk/microsoft-jdk-21.0.9-windows-x64.zip + https://aka.ms/download-jdk/microsoft-jdk-21.0.9-macOS-x64.tar.gz + https://aka.ms/download-jdk/microsoft-jdk-21.0.9-macOS-aarch64.tar.gz + https://aka.ms/download-jdk/microsoft-jdk-21.0.9-linux-x64.tar.gz + + +"; + + var components = manager.ParseManifest (xml); + var jdk = components.FirstOrDefault (c => c.ElementName == "jdk"); + Assert.IsNotNull (jdk, "Should find jdk component"); + Assert.AreEqual ("21.0.9", jdk!.Revision); + Assert.IsNotEmpty (jdk.DownloadUrl!); + } + + [Test] + public void ParseSdkManagerList_ParsesInstalledAndAvailable () + { + var output = @"Installed packages: + Path | Version | Description | Location + ------- | ------- | ------- | ------- + build-tools;35.0.0 | 35.0.0 | Android SDK Build-Tools 35 | build-tools/35.0.0 + emulator | 35.3.10 | Android Emulator | emulator + platform-tools | 36.0.0 | Android SDK Platform-Tools | platform-tools + +Available Packages: + Path | Version | Description + ------- | ------- | ------- + build-tools;36.0.0 | 36.0.0 | Android SDK Build-Tools 36 + platforms;android-35 | 5 | Android SDK Platform 35 + system-images;android-35;google_apis;arm64-v8a | 14 | Google APIs ARM 64 v8a System Image + +Available Updates: + Path | Installed | Available + platform-tools | 35.0.2 | 36.0.0 +"; + + var (installed, available) = SdkManager.ParseSdkManagerList (output); + + Assert.AreEqual (3, installed.Count, "Should have 3 installed packages"); + Assert.AreEqual (3, available.Count, "Should have 3 available packages"); + + var platformTools = installed.FirstOrDefault (p => p.Path == "platform-tools"); + Assert.IsNotNull (platformTools); + Assert.AreEqual ("36.0.0", platformTools!.Version); + Assert.IsTrue (platformTools.IsInstalled); + + var buildTools36 = available.FirstOrDefault (p => p.Path == "build-tools;36.0.0"); + Assert.IsNotNull (buildTools36); + Assert.AreEqual ("36.0.0", buildTools36!.Version); + Assert.IsFalse (buildTools36.IsInstalled); + } + + [Test] + public void ParseSdkManagerList_EmptyOutput_ReturnsEmpty () + { + var (installed, available) = SdkManager.ParseSdkManagerList (""); + Assert.AreEqual (0, installed.Count); + Assert.AreEqual (0, available.Count); + } + + [Test] + public void ParseSdkManagerList_OnlyInstalledSection () + { + var output = @"Installed packages: + Path | Version | Description + ------- | ------- | ------- + platform-tools | 36.0.0 | Android SDK Platform-Tools +"; + + var (installed, available) = SdkManager.ParseSdkManagerList (output); + Assert.AreEqual (1, installed.Count); + Assert.AreEqual (0, available.Count); + Assert.AreEqual ("platform-tools", installed[0].Path); + } + + [Test] + public void FindSdkManagerPath_NullSdkPath_ReturnsNull () + { + manager.AndroidSdkPath = null; + Assert.IsNull (manager.FindSdkManagerPath ()); + } + + [Test] + public void FindSdkManagerPath_CmdlineToolsLatest_Found () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + var binDir = Path.Combine (sdkDir, "cmdline-tools", "latest", "bin"); + Directory.CreateDirectory (binDir); + + var sdkManagerName = OS.IsWindows ? "sdkmanager.bat" : "sdkmanager"; + File.WriteAllText (Path.Combine (binDir, sdkManagerName), "#!/bin/sh\necho test"); + + manager.AndroidSdkPath = sdkDir; + var result = manager.FindSdkManagerPath (); + + Assert.IsNotNull (result, "Should find sdkmanager in cmdline-tools/latest/bin"); + Assert.That (result, Does.Contain ("sdkmanager")); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public void FindSdkManagerPath_VersionedDir_Found () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + var binDir = Path.Combine (sdkDir, "cmdline-tools", "12.0", "bin"); + Directory.CreateDirectory (binDir); + + var sdkManagerName = OS.IsWindows ? "sdkmanager.bat" : "sdkmanager"; + File.WriteAllText (Path.Combine (binDir, sdkManagerName), "#!/bin/sh\necho test"); + + manager.AndroidSdkPath = sdkDir; + var result = manager.FindSdkManagerPath (); + + Assert.IsNotNull (result, "Should find sdkmanager in versioned dir"); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public void FindSdkManagerPath_LegacyToolsDir_Found () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + var binDir = Path.Combine (sdkDir, "tools", "bin"); + Directory.CreateDirectory (binDir); + + var sdkManagerName = OS.IsWindows ? "sdkmanager.bat" : "sdkmanager"; + File.WriteAllText (Path.Combine (binDir, sdkManagerName), "#!/bin/sh\necho test"); + + manager.AndroidSdkPath = sdkDir; + var result = manager.FindSdkManagerPath (); + + Assert.IsNotNull (result, "Should find sdkmanager in legacy tools/bin"); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public void FindSdkManagerPath_NoSdkManager_ReturnsNull () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + Directory.CreateDirectory (sdkDir); + manager.AndroidSdkPath = sdkDir; + Assert.IsNull (manager.FindSdkManagerPath ()); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public void DefaultManifestFeedUrl_IsSet () + { + Assert.AreEqual ("https://aka.ms/AndroidManifestFeed/d18-0", SdkManager.DefaultManifestFeedUrl); + Assert.AreEqual (SdkManager.DefaultManifestFeedUrl, manager.ManifestFeedUrl); + } + + [Test] + public void ManifestFeedUrl_IsConfigurable () + { + manager.ManifestFeedUrl = "https://example.com/manifest.xml"; + Assert.AreEqual ("https://example.com/manifest.xml", manager.ManifestFeedUrl); + } + + [Test] + public void Constructor_DefaultLogger_DoesNotThrow () + { + var defaultManager = new SdkManager (); + Assert.IsNotNull (defaultManager); + } + + // --- AreLicensesAccepted --- + + [Test] + public void AreLicensesAccepted_NullSdkPath_ReturnsFalse () + { + manager.AndroidSdkPath = null; + Assert.IsFalse (manager.AreLicensesAccepted ()); + } + + [Test] + public void AreLicensesAccepted_NoLicensesDir_ReturnsFalse () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + Directory.CreateDirectory (sdkDir); + manager.AndroidSdkPath = sdkDir; + Assert.IsFalse (manager.AreLicensesAccepted ()); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public void AreLicensesAccepted_WithLicenseFiles_ReturnsTrue () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-test-{Guid.NewGuid ()}"); + try { + var licensesDir = Path.Combine (sdkDir, "licenses"); + Directory.CreateDirectory (licensesDir); + File.WriteAllText (Path.Combine (licensesDir, "android-sdk-license"), "abc123"); + + manager.AndroidSdkPath = sdkDir; + Assert.IsTrue (manager.AreLicensesAccepted ()); + } + finally { + if (Directory.Exists (sdkDir)) + Directory.Delete (sdkDir, recursive: true); + } + } + + [Test] + public async Task GetManifestComponentsAsync_ReturnsComponents () + { + IReadOnlyList components; + try { + components = await manager.GetManifestComponentsAsync (); + } + catch (Exception ex) when (ex is System.Net.Http.HttpRequestException || ex is TaskCanceledException) { + Assert.Ignore ($"Network unavailable: {ex.Message}"); + return; + } + + Assert.IsNotNull (components); + if (components.Count == 0) { + Assert.Ignore ("No components returned."); + return; + } + + // Should find cmdline-tools + var cmdline = components.FirstOrDefault (c => c.ElementName == "cmdline-tools"); + Assert.IsNotNull (cmdline, "Manifest should contain cmdline-tools"); + Assert.IsNotEmpty (cmdline!.DownloadUrl!); + Assert.IsNotEmpty (cmdline.Checksum!); + + // Should find platform-tools + var platformTools = components.FirstOrDefault (c => c.ElementName == "platform-tools"); + Assert.IsNotNull (platformTools, "Manifest should contain platform-tools"); + } + + [Test] + public async Task BootstrapAsync_NullPath_Throws () + { + Assert.ThrowsAsync ( + async () => await manager.BootstrapAsync (null!)); + } + + [Test] + public void InstallAsync_NoSdkManager_Throws () + { + manager.AndroidSdkPath = Path.Combine (Path.GetTempPath (), "nonexistent"); + Assert.ThrowsAsync ( + async () => await manager.InstallAsync (new[] { "platform-tools" })); + } + + [Test] + public void InstallAsync_EmptyPackages_Throws () + { + Assert.ThrowsAsync ( + async () => await manager.InstallAsync (new string[0])); + } + + [Test] + public void InstallAsync_NullPackages_Throws () + { + Assert.ThrowsAsync ( + async () => await manager.InstallAsync (null!)); + } + + [Test] + public void UninstallAsync_NoSdkManager_Throws () + { + manager.AndroidSdkPath = Path.Combine (Path.GetTempPath (), "nonexistent"); + Assert.ThrowsAsync ( + async () => await manager.UninstallAsync (new[] { "platform-tools" })); + } + + [Test] + public void UninstallAsync_EmptyPackages_Throws () + { + Assert.ThrowsAsync ( + async () => await manager.UninstallAsync (new string[0])); + } + + [Test] + public void ListAsync_NoSdkManager_Throws () + { + manager.AndroidSdkPath = Path.Combine (Path.GetTempPath (), "nonexistent"); + Assert.ThrowsAsync ( + async () => await manager.ListAsync ()); + } + + [Test] + public void UpdateAsync_NoSdkManager_Throws () + { + manager.AndroidSdkPath = Path.Combine (Path.GetTempPath (), "nonexistent"); + Assert.ThrowsAsync ( + async () => await manager.UpdateAsync ()); + } + + [Test] + public void AcceptLicensesAsync_NoSdkManager_Throws () + { + manager.AndroidSdkPath = Path.Combine (Path.GetTempPath (), "nonexistent"); + Assert.ThrowsAsync ( + async () => await manager.AcceptLicensesAsync ()); + } + + // --- License Parsing --- + + [Test] + public void ParseLicenseOutput_SingleLicense_Parsed () + { + var output = @" +License android-sdk-license: +--------------------------------------- +Terms and Conditions + +This is the license text. + +--------------------------------------- +Accept? (y/N): "; + + var licenses = SdkManager.ParseLicenseOutput (output); + + Assert.AreEqual (1, licenses.Count, "Should parse one license"); + Assert.AreEqual ("android-sdk-license", licenses[0].Id); + Assert.That (licenses[0].Text, Does.Contain ("Terms and Conditions")); + Assert.That (licenses[0].Text, Does.Contain ("This is the license text")); + } + + [Test] + public void ParseLicenseOutput_MultipleLicenses_Parsed () + { + var output = @" +License android-sdk-license: +--------------------------------------- +SDK License Text +--------------------------------------- +Accept? (y/N): n +License android-sdk-preview-license: +--------------------------------------- +Preview License Text +--------------------------------------- +Accept? (y/N): "; + + var licenses = SdkManager.ParseLicenseOutput (output); + + Assert.AreEqual (2, licenses.Count, "Should parse two licenses"); + Assert.AreEqual ("android-sdk-license", licenses[0].Id); + Assert.AreEqual ("android-sdk-preview-license", licenses[1].Id); + } + + [Test] + public void ParseLicenseOutput_NoLicenses_ReturnsEmpty () + { + var output = "All SDK package licenses accepted."; + + var licenses = SdkManager.ParseLicenseOutput (output); + + Assert.AreEqual (0, licenses.Count, "Should return empty list"); + } +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj new file mode 100644 index 00000000000..fb5973c374a --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj @@ -0,0 +1,29 @@ + + + + $(DotNetTargetFramework) + true + ..\..\product.snk + false + $(TestOutputFullPath) + false + Major + + + + + + + + + + + manifest-simplewidget.xml + + + + + + + + diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/FilesHashBenchmarks.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/FilesHashBenchmarks.cs new file mode 100644 index 00000000000..5c119144ca4 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/FilesHashBenchmarks.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using BenchmarkDotNet.Attributes; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tools.Benchmarks; + +[MemoryDiagnoser] +public class FilesHashBenchmarks +{ + const int OneMB = 1024 * 1024; + + byte [] _data = Array.Empty (); + MemoryStream _stream = new MemoryStream (); + string _tempFile1 = string.Empty; + string _tempFile2 = string.Empty; + + [GlobalSetup] + public void Setup () + { + // 1MB byte array with reproducible random data + _data = new byte [OneMB]; + new Random (42).NextBytes (_data); + _stream.Dispose (); + _stream = new MemoryStream (_data); + + // Two identical 1MB temp files + _tempFile1 = Path.GetTempFileName (); + _tempFile2 = Path.GetTempFileName (); + File.WriteAllBytes (_tempFile1, _data); + File.WriteAllBytes (_tempFile2, _data); + } + + [GlobalCleanup] + public void Cleanup () + { + _stream?.Dispose (); + if (File.Exists (_tempFile1)) + File.Delete (_tempFile1); + if (File.Exists (_tempFile2)) + File.Delete (_tempFile2); + } + + [Benchmark] + public string HashBytes () => Files.HashBytes (_data); + + [Benchmark] + public string HashStream () => Files.HashStream (_stream); + + [Benchmark] + public string HashFile () => Files.HashFile (_tempFile1); + + [Benchmark] + public bool HasFileChanged () => Files.HasFileChanged (_tempFile1, _tempFile2); +} diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Program.cs b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Program.cs new file mode 100644 index 00000000000..7da2c3c4580 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly (typeof (Program).Assembly).Run (args); diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/README.md b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/README.md new file mode 100644 index 00000000000..30da3f69ae6 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/README.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.8246) +Intel Core i9-14900KF, 1 CPU, 32 logical and 24 physical cores +.NET SDK 10.0.202 + [Host] : .NET 10.0.6 (10.0.626.17701), X64 RyuJIT AVX2 + DefaultJob : .NET 10.0.6 (10.0.626.17701), X64 RyuJIT AVX2 + + +``` +| Method | Mean | Error | StdDev | Allocated | +|--------------- |----------:|---------:|---------:|----------:| +| HashBytes | 23.34 μs | 0.123 μs | 0.115 μs | 56 B | +| HashStream | 23.07 μs | 0.075 μs | 0.070 μs | 120 B | +| HashFile | 52.98 μs | 0.766 μs | 0.716 μs | 360 B | +| HasFileChanged | 118.44 μs | 2.285 μs | 2.138 μs | 720 B | diff --git a/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Xamarin.Android.Tools.Benchmarks.csproj b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Xamarin.Android.Tools.Benchmarks.csproj new file mode 100644 index 00000000000..0fd2ed52a06 --- /dev/null +++ b/external/xamarin-android-tools/tests/Xamarin.Android.Tools.Benchmarks/Xamarin.Android.Tools.Benchmarks.csproj @@ -0,0 +1,20 @@ + + + + Exe + $(DotNetTargetFramework) + false + $(TestOutputFullPath) + false + Major + + + + + + + + + + + diff --git a/external/xamarin-android-tools/tools/ls-jdks/App.cs b/external/xamarin-android-tools/tools/ls-jdks/App.cs new file mode 100644 index 00000000000..ebcc5d5bed2 --- /dev/null +++ b/external/xamarin-android-tools/tools/ls-jdks/App.cs @@ -0,0 +1,40 @@ +using System; + +namespace Xamarin.Android.Tools +{ + class App + { + static void Main(string[] args) + { + foreach (var path in args) { + PrintProperties (path); + } + if (args.Length != 0) + return; + foreach (var jdk in JdkInfo.GetKnownSystemJdkInfos ()) { + Console.WriteLine ($"Found JDK: {jdk.HomePath}"); + Console.WriteLine ($" Locator: {jdk.Locator}"); + // Force parsing of java properties. + var keys = jdk.JavaSettingsPropertyKeys; + } + } + + static void PrintProperties (string jdkPath) + { + try { + var jdk = new JdkInfo (jdkPath, "ls-jdks"); + Console.WriteLine ($"Property settings for JDK Path: {jdk.HomePath}"); + foreach (var key in jdk.JavaSettingsPropertyKeys) { + if (!jdk.GetJavaSettingsPropertyValues (key, out var v)) { + Console.Error.WriteLine ($"ls-jdks: Could not retrieve value for key {key}."); + continue; + } + Console.WriteLine ($" {key} = {string.Join (Environment.NewLine + " ", v)}"); + } + } + catch (Exception e) { + Console.Error.WriteLine (e); + } + } + } +} diff --git a/external/xamarin-android-tools/tools/ls-jdks/ls-jdks.csproj b/external/xamarin-android-tools/tools/ls-jdks/ls-jdks.csproj new file mode 100644 index 00000000000..523b2fd0c78 --- /dev/null +++ b/external/xamarin-android-tools/tools/ls-jdks/ls-jdks.csproj @@ -0,0 +1,15 @@ + + + + Exe + $(DotNetTargetFramework) + Xamarin.Android.Tools + false + $(ToolOutputFullPath) + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets index 6c562f5378c..db484eb0660 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets @@ -137,7 +137,7 @@ + Replacements="@JAVA_INTEROP_COMMIT@=in-tree;@SQLITE_COMMIT@=$(_BuildInfo_SqliteCommit);@XAMARIN_ANDROID_TOOLS_COMMIT@=in-tree;"> <_XACommonPropsReplacement Include="@COMMAND_LINE_TOOLS_VERSION@=$(CommandLineToolsFolder)" />