Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds SDK bootstrap and sdkmanager wrapper functionality to Xamarin.Android.Tools.AndroidSdk, enabling programmatic Android SDK setup from scratch. The implementation uses a two-phase approach: first, download and extract command-line tools from a manifest feed with SHA-1 verification, then use the extracted sdkmanager CLI for package management operations.
Changes:
- Added
SdkManagerclass with manifest parsing, bootstrap, and package management capabilities - Added supporting types:
ManifestComponent,SdkPackage,SdkBootstrapProgress, andSdkBootstrapPhase - Added comprehensive unit tests covering manifest parsing, sdkmanager output parsing, path resolution, and configuration
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| src/Xamarin.Android.Tools.AndroidSdk/SdkManager.cs | Core implementation providing manifest-driven bootstrap, HTTP downloads with SHA-1 verification, and sdkmanager CLI wrapper for package operations |
| tests/Xamarin.Android.Tools.AndroidSdk-Tests/SdkManagerTests.cs | Unit tests covering manifest/output parsing, path resolution, configuration, and error handling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Implements manifest-driven download of Android command-line tools and wraps the sdkmanager CLI for package management operations. New public API: - SdkManager.BootstrapAsync() - downloads cmdline-tools from manifest feed, verifies SHA-1 checksum, extracts to cmdline-tools/latest/ - SdkManager.InstallAsync() - install packages via sdkmanager - SdkManager.UninstallAsync() - uninstall packages via sdkmanager - SdkManager.ListAsync() - list installed and available packages - SdkManager.UpdateAsync() - update all installed packages - SdkManager.AcceptLicensesAsync() - accept SDK licenses - ManifestComponent - parsed component from manifest feed - SdkPackage - package info from sdkmanager --list output Design decisions: - Manifest feed URL configurable (default: aka.ms/AndroidManifestFeed/d18-0) - HttpClient injection for VS proxy/auth support - Action<TraceLevel, string> logging pattern - netstandard2.0 compatible for VS hosting - SHA-1 checksum verification from manifest - Safe directory swap to avoid Windows race conditions Closes #271 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
10b4527 to
1a94a8d
Compare
tests/Xamarin.Android.Tools.AndroidSdk-Tests/SdkManagerTests.cs
Outdated
Show resolved
Hide resolved
Suggestions from related workI've been working on related tooling (#281-#284) and noticed a few patterns that might be useful: 1. Manifest Caching for Offline/CIOur implementation includes manifest caching for offline scenarios: public AndroidManifestFeedParser(HttpClient? httpClient = null, string? cacheDirectory = null)
{
_cacheDirectory = cacheDirectory;
// ...
}
// On successful load, cache for offline use
if (!string.IsNullOrEmpty(_cacheDirectory))
SaveToCache(manifestContent);
// On network failure, try cache
manifestContent = LoadFromCache();This helps CI pipelines that may have intermittent network access. 2. Separate Tool RunnersFor testability and reuse, we split the tool-running logic into a base class that can be shared:
This allows other runners (AvdManager, Adb, Emulator) to reuse the same patterns. Would be happy to collaborate on merging these approaches! The PRs #281-#284 are designed to complement this work. |
Review Response: PR #275 (SdkManager)Thank you for the review! Here's my analysis: ✅ ACCEPTED1. IDisposable for HttpClient (Accept)
Agree. The class conditionally owns an HttpClient and should implement 2. CancellationToken in GetStringAsync (Accept)
Agree. Should use: using var response = await httpClient.GetAsync(ManifestFeedUrl, cancellationToken);
response.EnsureSuccessStatusCode();
var xml = await response.Content.ReadAsStringAsync();3. ReadAsStreamAsync cancellation (Accept with modification)
Agree. The pattern of registering 4. Package parameter validation (Accept)
Agree. Should throw 5. Cross-device Directory.Move (Accept)
Agree. This is a real scenario on Linux where
|
Review Status: Already Implemented ✅After reviewing the code, I found that all accepted feedback items are already implemented:
The code already addresses all the review concerns properly. Nice work! |
- Add libc chmod p/invoke for Unix permission setting - Fallback to process spawn if p/invoke fails - Avoids overhead of creating a new process per file Addresses review feedback from Jonathan Peppers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implemented: p/invoke for chmodAddressed Jonathan's feedback in commit 14db704: `csharp static bool Chmod (string path, int mode) The implementation:
This avoids the overhead of creating a new process for each file in the bin directory. |
Changes: - Rename ManifestComponent → SdkManifestComponent (avoid AndroidManifest.xml confusion) - Add SdkManifestSource enum with Google manifest option (repository2-3.xml) - Add GoogleManifestFeedUrl constant - Use ArrayPool<byte> for download buffer (NET5_0_OR_GREATER only) - Validate checksumType parameter, throw NotSupportedException for unknown types - Throw PlatformNotSupportedException for unsupported OS/architecture - Remove all #region directives per coding guidelines - Log exceptions in catch blocks instead of empty catches - Pass logger to static SetExecutablePermissions method Already implemented from earlier commit: - p/invoke for chmod instead of spawning process Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Jonathan's Review Feedback - Addressed ✅Commit c52a6b5 addresses all review feedback:
API ChangesNew enum: public enum SdkManifestSource { Xamarin, Google }New property: public SdkManifestSource ManifestSource { get; set; }Renamed type:
Questions from ReviewQ: Why pass in HttpClient? Q: Should we use XmlReader instead of XElement? Q: Use version number instead of 'latest' for directory? Q: API to present licenses to user?
|
| try { | ||
| return chmod (path, mode) == 0; | ||
| } | ||
| catch { | ||
| // p/invoke failed (e.g., not on Unix) - caller will use fallback | ||
| return false; | ||
| } |
There was a problem hiding this comment.
If this fails, should we just let the exception happen?
The problem is you won't be able to use sdkmanager if it fails, so seems like it should error?
Simplify API by having SdkManager manage its own HttpClient internally. Removes complexity of ownership tracking (ownsHttpClient). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use version number for cmdline-tools directory instead of 'latest' (e.g., cmdline-tools/19.0/ instead of cmdline-tools/latest/) - Clean up System.IO.Path -> Path (using is already at top) - Remove extra empty lines - Update FindSdkManagerPath to search versioned dirs first, then 'latest' for backward compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Additional Feedback Addressed ✅Commit fd485a2 addresses remaining items:
Behavior ChangeBefore: Bootstrap extracts to
Still Pending (lower priority)
|
XmlReader changes: - Replace XDocument/XElement with XmlReader for better performance - Forward-only parsing without building DOM tree - Same test coverage maintained License API additions: - GetPendingLicensesAsync() - Get pending licenses with their full text for UI presentation - AcceptLicensesAsync(IEnumerable<string> licenseIds) - Accept specific licenses by ID - SdkLicense class with Id and Text properties - ParseLicenseOutput() internal parser for sdkmanager --licenses output Also removes #region directives from test file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implemented: XmlReader + License Presentation API ✅Commit 5d54af9 addresses the remaining items: 1. XmlReader instead of XElementReplaced \XDocument.Parse()\ / \XElement\ with forward-only \XmlReader: \\csharp Benefits:
2. License Presentation APINew methods for IDEs/CLI tools to present licenses before accepting: \\csharp New types:
|
- New EnvironmentVariableNames static class with constants: - AndroidHome, AndroidSdkRoot, JavaHome, JiJavaHome, Path, PathExt - Updated SdkManager.cs to use constants instead of magic strings - Updated ProcessUtils.cs to use constants - Updated JdkInfo.cs to use constants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ConfigureEnvironment(ProcessStartInfo) helper to reduce duplication - Fix empty catch block in GetPendingLicensesAsync to log exception - Both RunSdkManagerAsync and GetPendingLicensesAsync now use helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SetExecutablePermissions now throws InvalidOperationException on failure instead of silently continuing (sdkmanager won't work without +x) - Remove ANDROID_SDK_ROOT from ConfigureEnvironment (deprecated per https://developer.android.com/tools/variables#envar) - Only set ANDROID_HOME for SDK path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed in commit 8e8f59cchmod failure (r2842575105): ✅
ANDROID_SDK_ROOT deprecated (r2842566684): ✅
|
Moved to Models/Sdk/: - SdkBootstrapPhase.cs (enum) - SdkBootstrapProgress.cs (class) - SdkLicense.cs (class) - SdkManifestComponent.cs (class) - SdkManifestSource.cs (enum) - SdkPackage.cs (class) SdkManager.cs remains in root as it's the service class, not a POCO. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
|
|
||
| /// <summary> |
There was a problem hiding this comment.
The XML documentation comment has incorrect indentation. It should start at the same indentation level as the method it documents, without the leading whitespace and tab.
| /// <summary> | |
| /// <summary> |
| TestContext.WriteLine ($"[{level}] {message}"); | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
The SdkManager class implements IDisposable and owns an HttpClient that needs disposal. However, the test creates SdkManager instances in SetUp but never disposes them. Consider adding a TearDown method that calls manager?.Dispose() to ensure proper resource cleanup after each test.
| [TearDown] | |
| public void TearDown () | |
| { | |
| manager?.Dispose (); | |
| manager = null; | |
| } |
| - **This project uses xUnit** - use xUnit for all new tests | ||
|
|
||
| ### xUnit | ||
|
|
||
| - Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio` | ||
| - No class attribute; use `[Fact]` | ||
| - Parameterized tests: `[Theory]` with `[InlineData]` | ||
| - Setup/teardown: constructor and `IDisposable` |
There was a problem hiding this comment.
The documentation states "This project uses xUnit" and provides xUnit-specific guidance (lines 185-193), but the actual test project uses NUnit as evidenced by the test attributes [TestFixture], [Test], and [SetUp] in SdkManagerTests.cs, as well as the NUnit package reference in the csproj file. This inconsistency should be corrected to accurately reflect that NUnit is the testing framework used in this project.
| - **This project uses xUnit** - use xUnit for all new tests | |
| ### xUnit | |
| - Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio` | |
| - No class attribute; use `[Fact]` | |
| - Parameterized tests: `[Theory]` with `[InlineData]` | |
| - Setup/teardown: constructor and `IDisposable` | |
| - **This project uses NUnit** - use NUnit for all new tests | |
| ### NUnit | |
| - Packages: `Microsoft.NET.Test.Sdk`, `NUnit`, `NUnit3TestAdapter` | |
| - Class attribute: `[TestFixture]`; test methods: `[Test]` | |
| - Parameterized tests: `[TestCase]` (or `[TestCaseSource]` for more complex cases) | |
| - Setup/teardown: `[SetUp]` / `[TearDown]` |
| ## Formatting | ||
|
|
||
| - Apply code-formatting style defined in `.editorconfig` | ||
| - Prefer file-scoped namespace declarations and single-line using directives |
There was a problem hiding this comment.
There is a contradiction in the formatting guidelines. Line 39 states that "File-scoped namespaces (must use traditional namespace X { })" under "NOT available (C# 10+)", but line 53 states "Prefer file-scoped namespace declarations". Since this project uses C# 9.0, file-scoped namespaces are not available, so line 53 should be corrected to indicate traditional namespace blocks should be used.
| - Prefer file-scoped namespace declarations and single-line using directives | |
| - Use traditional `namespace X { }` blocks for namespaces and prefer single-line using directives |
| } | ||
| } | ||
|
|
||
| /// <summary> |
There was a problem hiding this comment.
The XML documentation comment has incorrect indentation. It should start at the same indentation level as the method it documents, without the leading whitespace and tab. The comment should be /// <summary> not /// <summary> with leading whitespace.
Summary
Adds SDK bootstrap and
sdkmanagerwrapper toXamarin.Android.Tools.AndroidSdk, enabling programmatic Android SDK setup from scratch.Closes #271
Architecture
Two-phase approach as requested in the issue:
cmdline-tools/<version>/sdkmanagerCLI for all package operationsAPI Surface
Key Design Decisions
SdkManifestSource.Googleoption usesdl.google.com/android/repository/repository2-3.xmlcmdline-tools/<version>/instead oflatestsymlinkGetPendingLicensesAsync()returns license text for IDE/CLI UI presentationsdkmanagerHttpClientlifecyclenetstandard2.0andnet9.0for VS hostingchmoddirectly instead of spawning processes on UnixArrayPool<byte>.Rent()for download buffers (NET5_0_OR_GREATER)ANDROID_HOME,JAVA_HOME, etc.Directory.MovefailsPlatformNotSupportedExceptionfor unsupported OS/architectureReview Feedback Addressed
SdkManifestComponentSdkManifestSourceenumcmdline-tools/<version>/Tests
Comprehensive unit tests covering:
SdkManifestComponent)sdkmanager --listoutput parsingCo-authored-by: Copilot 223556219+Copilot@users.noreply.github.com