Skip to content

Add SDK bootstrap and sdkmanager wrapper#275

Merged
jonathanpeppers merged 46 commits intomainfrom
feature/sdk-manager
Mar 2, 2026
Merged

Add SDK bootstrap and sdkmanager wrapper#275
jonathanpeppers merged 46 commits intomainfrom
feature/sdk-manager

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Feb 20, 2026

Summary

Adds SdkManager class and sdkmanager CLI wrapper to Xamarin.Android.Tools.AndroidSdk, enabling programmatic Android SDK bootstrap and package management.

Closes #271

Architecture

Two-phase approach:

  1. Bootstrap: Download command-line tools from the Google manifest feed, SHA-1 verify, extract to cmdline-tools/<version>/
  2. Manage: Use the extracted sdkmanager CLI for all package operations (list, install, uninstall, update, licenses)

SdkManager is split into focused partial classes for maintainability:

File Responsibility
SdkManager.cs Core class, properties, constructor, IDisposable
SdkManager.Manifest.cs Manifest download and XmlReader-based parsing
SdkManager.Bootstrap.cs SDK bootstrap/download/extract flow
SdkManager.Packages.cs Package list/install/uninstall/update + output parsing
SdkManager.Licenses.cs License accept/parse/check, GetPendingLicensesAsync
SdkManager.Process.cs Process execution via ProcessUtils, environment config

New Files

Models (Models/Sdk/):

  • ChecksumType.cs — Enum for hash algorithms (SHA-256, SHA-1)
  • SdkBootstrapProgress.cs — Progress reporting during bootstrap
  • SdkLicense.cs — License data with Id and Text
  • SdkManifestComponent.cs — Parsed manifest entry (URL, checksum, platform)
  • SdkPackage.cs — Installed/available SDK package info

Utilities (extended existing files):

  • DownloadUtils.cs — Added SHA-1 support, ComputeHashString shared helper, ChecksumType enum
  • FileUtil.cs — Added ExtractAndInstall (safe extraction with rollback) and IsUnderDirectory

Key Features

  • Manifest-driven bootstrap: XmlReader-based parsing with platform/architecture filtering
  • IDisposable with ThrowIfDisposed guard on all public methods
  • ANDROID_HOME only: Sets env var per Android docs; deprecated ANDROID_SDK_ROOT not used
  • ANDROID_USER_HOME: Sets ~/.android so sdkmanager writes to user-writable location
  • Version-based cmdline-tools directory: Installs to cmdline-tools/<version>/ (not latest/)
  • Elevated execution (Windows): Detects admin-protected paths, re-launches via UAC; read-only ops skip elevation
  • Zip Slip protection: Entry-by-entry extraction with path traversal validation
  • Rollback on failure: Backup existing installation, restore on extract/move failure
  • Cross-device fallback: Directory.Move falls back to recursive copy on IOException
  • SHA-1 checksum verification of downloaded archives
  • p/invoke chmod with process fallback for Unix executable permissions (throws on failure)
  • ArrayPool buffers (NET5_0_OR_GREATER) for download operations
  • ProcessUtils for all process execution — no direct System.Diagnostics.Process usage
  • ProcessStartInfo.ArgumentList via ProcessUtils.CreateProcessStartInfo (falls back to Arguments on netstandard2.0)
  • License presentation API: GetPendingLicensesAsync() returns SdkLicense objects for IDE/CLI display; AcceptLicensesAsync(IEnumerable<string>) accepts specific licenses by ID
  • netstandard2.0 + net10.0 compatible, full CancellationToken support
  • EnvironmentVariableNames constants used throughout (no raw strings)
  • No empty catch blocks — all exceptions are logged

Coding Conventions Updated

.github/copilot-instructions.md updated with conventions established during review:

  • One type per file
  • Minimal public API (internal by default, InternalsVisibleTo for tests)
  • Use ProcessUtils for process execution
  • Use FileUtil for file operations
  • File-scoped namespaces for new files
  • netstandard2.0 awareness

Tests

SdkManagerTests.cs covers:

  • Manifest XML parsing (components, platform filtering)
  • Package list output parsing (installed, available, updates)
  • License output parsing (single, multiple, none)
  • Argument validation (null/empty packages → ArgumentException)
  • Missing sdkmanager → InvalidOperationException (Install, Uninstall, List, Update, AcceptLicenses)
  • Bootstrap null path → ArgumentNullException
  • Network-dependent tests (manifest fetch, HasAcceptedLicenses) with Assert.Ignore on failure

Review Feedback Addressed

All review feedback from @jonathanpeppers and Copilot reviewer addressed:

  • Removed HttpClient injection (internal only)
  • Added IDisposable with dispose guard
  • XmlReader instead of XElement for manifest parsing
  • Version-based cmdline-tools directory (not latest/)
  • ArrayPool<byte> for download buffers
  • ChecksumType validated (throws NotSupportedException for unknown)
  • p/invoke for chmod (throws InvalidOperationException on failure)
  • EnvironmentVariableNames constants created and used
  • ANDROID_SDK_ROOT removed (deprecated)
  • License presentation API (GetPendingLicensesAsync + AcceptLicensesAsync with IDs)
  • One type per file, no #region directives
  • No empty catch blocks (all log exceptions)
  • Shared ComputeHashString helper (removes duplication)
  • EnumerateFiles().Any() for short-circuit
  • Path.GetTempFileName() instead of manual Guid paths
  • Consistent StdinPollDelayMs const for stdin polling
  • Write-then-delay ordering fix in license acceptance

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings February 20, 2026 17:01

This comment was marked as resolved.

@rmarinho rmarinho force-pushed the feature/sdk-manager branch from 10b4527 to 1a94a8d Compare February 20, 2026 17:16
@rmarinho rmarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 20, 2026
@rmarinho

This comment was marked as resolved.

@rmarinho

This comment was marked as resolved.

@rmarinho

This comment was marked as outdated.

@rmarinho

This comment was marked as outdated.

@rmarinho

This comment was marked as resolved.

@rmarinho

This comment was marked as outdated.

@rmarinho

This comment was marked as resolved.

This comment was marked as resolved.

@rmarinho

This comment was marked as outdated.

@rmarinho

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Contributor

Copilot AI commented Feb 25, 2026

@rmarinho I've opened a new pull request, #288, to work on those changes. Once the pull request is ready, I'll request review from you.

rmarinho and others added 7 commits March 2, 2026 11:26
…cords

Use one-liner positional record syntax and update all call sites
to constructor syntax. Remove EnvironmentVariableNames usage from
this PR (handled by #290).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove StartShellExecuteProcessAsync, RequiresElevation,
RunSdkManagerElevatedAsync, and SanitizeCmdArgument.
Elevation support will be added in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The AbiToArchMap and MapAbiToArchitecture were not used by any code.
Keep only GetEnvironment which is used by SdkManager.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoids typos and string comparisons. The XML attribute
value is parsed into the enum during manifest parsing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove unused AndroidEnvironmentHelper class
- Remove unused SdkManagerTimeout property
- Remove unused sdkManagerPath in AcceptLicensesAsync
- Make SdkManifestComponent and GetManifestComponentsAsync internal
- Remove unused TryDeleteFiles and CopyDirectoryRecursive from FileUtil

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reuse the existing DownloadUtils.ReadAsStringAsync helper which
handles the netstandard2.0 vs NET5+ API difference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use a list reference instead of string section tracking,
eliminating duplicate add logic and string comparisons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/sdk-manager branch from a6990ed to b804814 Compare March 2, 2026 11:26
rmarinho and others added 2 commits March 2, 2026 17:28
…t named arg

- Convert all new .cs files to file-scoped namespaces per project convention
- Use NullProgress no-op pattern in Bootstrap instead of null-conditional calls
- Make private methods take non-nullable IProgress<T>
- Simplify DownloadFileAsync progress adapter (no null check needed)
- Revert onStarted: named arg, pass null explicitly for environmentVariables

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add StdinPollDelayMs const (500ms) for stdin polling loops
- Fix Licenses.cs: write-then-delay ordering (was delay-then-write)
- Use const in both Process.cs and Licenses.cs Task.Delay calls
- Remove consecutive blank lines in SdkManagerTests.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really close now, but I'm also going to ask copilot to review again.

Replace raw "ANDROID_HOME" and "JAVA_HOME" strings with
EnvironmentVariableNames.AndroidHome and .JavaHome constants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.

- Extract ComputeHashString helper in DownloadUtils, shared by
  VerifyChecksum and ComputeLicenseHash (removes duplication)
- Use Directory.EnumerateFiles().Any() in HasAcceptedLicenses
  for short-circuit instead of GetFiles().Length
- Use Path.GetTempFileName() in BootstrapAsync instead of manual
  Guid-based temp path
- Remove unused System.Security.Cryptography using from Licenses

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho requested a review from jonathanpeppers March 2, 2026 18:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Per Microsoft guidelines, HttpClient instances should be static and
shared across the application lifetime. Changed SdkManager.httpClient
from instance to static readonly field and removed disposal from
Dispose(). Added convention to copilot-instructions.md.

See: https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient-guidelines

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers enabled auto-merge (squash) March 2, 2026 21:30
@jonathanpeppers jonathanpeppers merged commit 5f98fec into main Mar 2, 2026
2 checks passed
@jonathanpeppers jonathanpeppers deleted the feature/sdk-manager branch March 2, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SDK bootstrap and sdkmanager wrapper (move from android-platform-support)

5 participants