Skip to content

Add JDK installation support (Microsoft OpenJDK)#274

Merged
jonathanpeppers merged 24 commits intomainfrom
feature/jdk-installer
Feb 26, 2026
Merged

Add JDK installation support (Microsoft OpenJDK)#274
jonathanpeppers merged 24 commits intomainfrom
feature/jdk-installer

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Feb 20, 2026

Summary

Adds JDK installation support to Xamarin.Android.Tools.AndroidSdk using the Microsoft Build of OpenJDK (https://www.microsoft.com/openjdk).

Closes #270

New Files

File Purpose
JdkInstaller.cs Discovers, downloads, installs, validates, and removes Microsoft OpenJDK
DownloadUtils.cs Shared helpers: download with progress, SHA-256 checksum verification, Zip Slip-safe ZIP extraction, tar.gz extraction, checksum file fetching/parsing
Models/JdkVersionInfo.cs Version metadata (major, minor, patch, build, platform, architecture, URL, checksum URL)
Models/JdkInstallPhase.cs enum for progress reporting phases (Downloading, Verifying, Extracting, etc.)
Models/JdkInstallProgress.cs record combining phase, percentage, and message for IProgress<T> callbacks
IsExternalInit.cs Enables init accessors on netstandard2.0
.github/copilot-instructions.md Repository-specific Copilot context

Modified Files

File Changes
FileUtil.cs Added TryDeleteFile, TryDeleteDirectory, MoveWithRollback, CommitMove, IsTargetPathWritable, IsUnderDirectory, GetInstallerExtension, GetArchiveExtension
ProcessUtils.cs Added CreateProcessStartInfo (uses ArgumentList on net5+, JoinArguments fallback), IsElevated (uses Environment.IsPrivilegedProcess on net7+, P/Invoke fallback)
.csproj Added System.Buffers 4.5.1 (netstandard2.0), IsExternalInit.cs exclusion on modern TFMs

API Surface

public class JdkInstaller : IDisposable
{
    public JdkInstaller(Action<string>? logger = null);
    public static int RecommendedMajorVersion { get; }          // 21
    public static IReadOnlyList<int> SupportedVersions { get; } // [21]
    public Task<IReadOnlyList<JdkVersionInfo>> DiscoverAsync(CancellationToken ct = default);
    public Task InstallAsync(int majorVersion, string targetPath,
        IProgress<JdkInstallProgress>? progress = null, CancellationToken ct = default);
    public bool IsValid(string jdkPath);
    public bool Remove(string jdkPath);
    public void Dispose();
}

// Helpers used by JdkInstaller (not on JdkInstaller itself):
// FileUtil.IsTargetPathWritable(string, Action<TraceLevel, string>)
// ProcessUtils.IsElevated()

Key Design Decisions

  • Microsoft OpenJDK only — downloads from https://aka.ms/download-jdk with SHA-256 checksum verification
  • JDK 21 onlyRecommendedMajorVersion = 21, SupportedVersions = [21]. Array allows future additions
  • Owns its HttpClient — no injection; Dispose() cleans it up
  • Elevated install mode — when ProcessUtils.IsElevated() is true, uses platform-native silent installers (.msi via msiexec /qn, .pkg via /usr/sbin/installer). When not elevated, extracts archive to targetPath
  • Safe directory replacementMoveWithRollback: rename existing → temp, move new in place, delete temp via CommitMove. Avoids Delete/Move race on Windows
  • Zip Slip protection — ZIP extraction validates that all entry paths resolve under the destination directory
  • ArrayPool<byte> download bufferArrayPool<byte>.Shared.Rent for the 80 KB download buffer, returned in finally
  • Cross-platform P/Invoke fallbacksIsUserAnAdmin() (shell32) on Windows, geteuid() (libc) on Unix, behind #if !NET5_0_OR_GREATER
  • NullProgress pattern — avoids null checks throughout install flow
  • netstandard2.0 + net10.0 — conditional compilation for ArgumentList, Environment.IsPrivilegedProcess, ReadAsStringAsync/ReadAsStreamAsync overloads. Private helpers encapsulate #if blocks

Tests

66 unit tests across 5 test files (all target net10.0):

Test File Count Coverage
JdkInstallerTests.cs 19 IsValid, DiscoverAsync, InstallAsync, IsTargetPathWritable, Remove, properties
DownloadUtilsTests.cs 20 ParseChecksumFile (12 cases), VerifyChecksum (4), ExtractZipSafe + Zip Slip (4)
FileUtilTests.cs 15 MoveWithRollback (4), IsUnderDirectory (11 including cross-platform path cases)
ProcessUtilsTests.cs 7 CreateProcessStartInfo (6), IsElevated smoke (1)
JdkVersionInfoTests.cs 5 Constructor, defaults, ToString, mutable properties

Review Feedback Addressed

JDK 21 only (removed 17), removed HttpClient injection, IDisposable, CancellationToken pattern, path normalization, validation cleanup, ArgumentList for tar, absolute /usr/bin/tar, null comparison pattern, one type per file, record type, checksum errors throw, test TearDown dispose, CI skip, elevated install mode, Zip Slip protection, ArrayPool buffer, extracted constants (BufferSize, BytesPerMB, WhitespaceChars), private ReadAsStreamAsync/ReadAsStringAsync helpers, cross-platform test paths, comprehensive unit test coverage

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

Copilot AI review requested due to automatic review settings February 20, 2026 16:18

This comment was marked as resolved.

@rmarinho rmarinho added copilot `copilot-cli` or other AIs were used to author this enhancement labels Feb 20, 2026
@rmarinho rmarinho force-pushed the feature/jdk-installer branch 2 times, most recently from b807a5e to 7740819 Compare February 20, 2026 16:34
@rmarinho rmarinho changed the title Add JDK installation support (Adoptium API) Add JDK installation support (Microsoft OpenJDK) Feb 20, 2026
@rmarinho rmarinho force-pushed the feature/jdk-installer branch from 7740819 to 938fdae Compare February 20, 2026 16:43

This comment was marked as resolved.

@rmarinho

This comment was marked as resolved.

@rmarinho

This comment was marked as resolved.

rmarinho added a commit that referenced this pull request Feb 24, 2026
- Fix GetStringAsync to use GetAsync + ReadAsStringAsync for cancellation support
- Improve tar argument escaping using single quotes for paths with special chars
- Implement rollback logic: restore previous JDK if move fails
- Document .NET Standard 2.0 ZipFile cancellation limitation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit that referenced this pull request Feb 24, 2026
@rmarinho rmarinho requested a review from Copilot February 24, 2026 15:34
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 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs:445

  • When totalBytes is 0 (because both ContentLength and expectedSize are 0), the progress message will show "0 MB / 0 MB" which is acceptable, but the percentage calculation is skipped due to the check on line 443. However, if totalBytes is very small (less than 1 MB), the division by (1024 * 1024) will result in 0 for both values in the message, which could be confusing. Consider using KB or bytes for small files, or formatting the size more intelligently (e.g., using a helper method that chooses appropriate units).
				if (progress != null && totalBytes > 0) {
					var pct = (double) totalRead / totalBytes * 100;
					progress.Report (new JdkInstallProgress (JdkInstallPhase.Downloading, pct, $"Downloaded {totalRead / (1024 * 1024)} MB / {totalBytes / (1024 * 1024)} MB"));

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@rmarinho

This comment was marked as resolved.

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 6 out of 6 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (2)

src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs:207

  • InstallAsync treats an empty string targetPath the same as null and throws ArgumentNullException. For API clarity, consider throwing ArgumentException for empty/whitespace paths and reserving ArgumentNullException for null only (matching typical .NET conventions).
				logger (TraceLevel.Error, $"JDK installation failed: {ex.Message}");
				logger (TraceLevel.Verbose, ex.ToString ());
				throw;
			}

src/Xamarin.Android.Tools.AndroidSdk/JdkInstaller.cs:571

  • Zip Slip validation uses StringComparison.OrdinalIgnoreCase unconditionally. On case-sensitive filesystems (Linux/macOS), a ZIP entry with an absolute path that differs only by case (e.g. "/TMP/..." vs root "/tmp/...") can incorrectly pass the StartsWith check, allowing extraction outside the destination directory. Use Ordinal on non-Windows (and ideally explicitly reject rooted entry.FullName) and keep OrdinalIgnoreCase only for Windows.

rmarinho added a commit that referenced this pull request Feb 24, 2026
- Fix GetStringAsync to use GetAsync + ReadAsStringAsync for cancellation support
- Improve tar argument escaping using single quotes for paths with special chars
- Implement rollback logic: restore previous JDK if move fails
- Document .NET Standard 2.0 ZipFile cancellation limitation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/jdk-installer branch from 4148670 to 03c994f Compare February 24, 2026 19:10
rmarinho and others added 5 commits February 25, 2026 22:59
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DiscoverAsync: log warning when checksum unavailable
- InstallElevatedAsync: throw if checksum is null (matches InstallAsync behavior)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Utils

Move reusable methods to shared utility classes:
- FileUtil: TryDeleteFile, TryDeleteDirectory, MoveWithRollback
- DownloadUtils: FetchChecksumAsync

JdkInstaller reduced from 546 to 464 lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extracted IsTargetPathWritable, IsUnderDirectory, GetInstallerExtension,
and GetArchiveExtension to FileUtil for reuse across the codebase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix IsTargetPathWritable tests to use FileUtil static method
- Remove redundant checksum null check (already enforced above)
- Remove double blank line

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 10 out of 10 changed files in this pull request and generated 5 comments.

rmarinho and others added 2 commits February 26, 2026 13:19
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- MoveWithRollback: defer backup deletion until after validation succeeds
- Add CommitMove() to explicitly clean up backups post-validation
- InstallAsync: clarify docs that elevated path ignores targetPath
- Add dedicated OperationCanceledException catch (not logged as error)
- DiscoverAsync test: add 15s CancellationToken timeout for CI reliability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/jdk-installer branch from 6a66d2e to 1c1e8a1 Compare February 26, 2026 14:21
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 15 out of 15 changed files in this pull request and generated 3 comments.

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.

I'm squashing/merging, getting a nice commit message.

@jonathanpeppers jonathanpeppers merged commit c6fc83d into main Feb 26, 2026
2 checks passed
@jonathanpeppers jonathanpeppers deleted the feature/jdk-installer branch February 26, 2026 16:13
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 JDK installation support (move from android-platform-support)

4 participants