-
Notifications
You must be signed in to change notification settings - Fork 31
Add JDK installation support (Microsoft OpenJDK) #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
985f6fb
Add JDK installer with discovery, download, verification, and extraction
rmarinho 78d04e3
Fix race condition, checksum fetch, and geteuid guard
rmarinho 815ffa9
Fix architecture validation, path boundary check, and catch pattern
rmarinho 2e4088e
Fix cancellation propagation, mandatory checksum in elevated install,…
rmarinho 9e9fc22
Add comment explaining netstandard2.0 tar path validation
rmarinho 8e589f2
Handle null checksum from FetchChecksumAsync at all call sites
rmarinho 0774d0c
Extract shared utilities from JdkInstaller into FileUtil and Download…
rmarinho e3f4188
Move file/path utilities from JdkInstaller to FileUtil
rmarinho 4efe463
Fix test compilation and remove dead code
rmarinho 43c9606
Change IsElevated and IsTargetPathWritable to internal
rmarinho 1c1e8a1
Address PR review: rollback safety, cancellation handling, test timeouts
rmarinho d0f3bea
Create `ProcessUtils.CreateProcessStartInfo()`
jonathanpeppers 9ed5ddc
More ProcessUtils.CreateProcessStartInfo()
jonathanpeppers 8b7446a
Move elevation checks to ProcessUtils
jonathanpeppers 3f2177b
Missing `using`
jonathanpeppers 48ede26
Simplify nullable `progress?`
jonathanpeppers 7c85bd7
Simplify `DownloadUtils.ParseChecksumFile()` write unit tests
jonathanpeppers e8e9db7
Remove `IsElevated()`
jonathanpeppers 6e4d117
MOAR Tests!
jonathanpeppers b2a8a7b
We can't use these new lang features
jonathanpeppers 6e167e0
Use ArrayPool.Rent/Return
jonathanpeppers 94b3ac4
Cache WhitespaceChars
jonathanpeppers 212ac43
Pass `CancellationToken`
jonathanpeppers 70966f7
Better `FileUtil.IsTargetPathWritable()`
jonathanpeppers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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 | ||
| { | ||
| /// <summary> | ||
| /// Shared helpers for downloading files, verifying checksums, and extracting archives. | ||
| /// </summary> | ||
| static class DownloadUtils | ||
| { | ||
| const int BufferSize = 81920; | ||
| const long BytesPerMB = 1024 * 1024; | ||
| static readonly char[] WhitespaceChars = [' ', '\t', '\n', '\r']; | ||
|
|
||
| static Task<Stream> ReadAsStreamAsync (HttpContent content, CancellationToken cancellationToken) | ||
| { | ||
| #if NET5_0_OR_GREATER | ||
| return content.ReadAsStreamAsync (cancellationToken); | ||
| #else | ||
| return content.ReadAsStreamAsync (); | ||
| #endif | ||
| } | ||
|
|
||
| static Task<string> ReadAsStringAsync (HttpContent content, CancellationToken cancellationToken) | ||
| { | ||
| #if NET5_0_OR_GREATER | ||
| return content.ReadAsStringAsync (cancellationToken); | ||
| #else | ||
| return content.ReadAsStringAsync (); | ||
| #endif | ||
| } | ||
|
|
||
| /// <summary>Downloads a file from the given URL with optional progress reporting.</summary> | ||
| 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<byte>.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<byte>.Shared.Return (buffer); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Verifies a file's SHA-256 hash against an expected value.</summary> | ||
| public static void VerifyChecksum (string filePath, string expectedChecksum) | ||
| { | ||
| using var sha256 = SHA256.Create (); | ||
| using var stream = File.OpenRead (filePath); | ||
|
|
||
| var hash = sha256.ComputeHash (stream); | ||
| var actual = BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); | ||
|
|
||
| if (!string.Equals (actual, expectedChecksum, StringComparison.OrdinalIgnoreCase)) | ||
| throw new InvalidOperationException ($"Checksum verification failed. Expected: {expectedChecksum}, Actual: {actual}"); | ||
| } | ||
|
|
||
| /// <summary>Extracts a ZIP archive with Zip Slip protection.</summary> | ||
| 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); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Extracts a tar.gz archive using the system tar command.</summary> | ||
| public static async Task ExtractTarGzAsync (string archivePath, string destinationPath, Action<TraceLevel, string> 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}"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Fetches a SHA-256 checksum from a remote URL, returning null on failure.</summary> | ||
| public static async Task<string?> FetchChecksumAsync (HttpClient httpClient, string checksumUrl, string label, Action<TraceLevel, string> 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; | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Parses "hash filename" or just "hash" from .sha256sum.txt content.</summary> | ||
| 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; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.