Skip to content
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

Sdk Updater - Uno Version Validation (backport #799) #825

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion tools/Uno.Sdk.Updater/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Uno.Sdk.Models;
using Uno.Sdk.Services;
using Uno.Sdk.Updater;
using Uno.Sdk.Updater.Utils;

const string UnoSdkPackageId = "Uno.Sdk.Private";

Expand Down Expand Up @@ -288,9 +289,11 @@ static async Task<ManifestGroup> UpdateGroup(ManifestGroup group, NuGetVersion u
preview = false;
}

var packageId = group.Packages.First();
var packageId = group.Packages.FirstOrDefault(x => x.Contains("WinUI", StringComparison.InvariantCultureIgnoreCase) && x.Contains("Uno", StringComparison.InvariantCultureIgnoreCase)) ??
group.Packages.First();

var version = await client.GetVersionAsync(packageId, preview);
version = !string.IsNullOrEmpty(group.Version) && NuGetVersion.Parse(version) < NuGetVersion.Parse(group.Version) ? group.Version : version;
var newGroup = group with { Version = version };

if (group.Version != newGroup.Version)
Expand All @@ -314,6 +317,8 @@ static async Task<ManifestGroup> UpdateGroup(ManifestGroup group, NuGetVersion u
{
Console.WriteLine($"Updated Version Override for '{group.Group}' - '{key}' to '{version}'.");
}

version = NuGetVersion.Parse(version) < versionOverride ? versionOverrideString : version;
updatedOverrides.Add(key, version);
}

Expand Down
339 changes: 238 additions & 101 deletions tools/Uno.Sdk.Updater/Services/NuGetClient.cs
Original file line number Diff line number Diff line change
@@ -1,109 +1,246 @@
#nullable enable
using System.Net.Http.Json;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Uno.Sdk.Models;
using Uno.Sdk.Updater.Utils;

namespace Uno.Sdk.Services;

internal class NuGetApiClient : IDisposable
{
private HttpClient PublicNuGetClient { get; } = new HttpClient
{
BaseAddress = new Uri("https://api.nuget.org")
};

private HttpClient PrivateNuGetClient { get; } = new HttpClient
{
BaseAddress = new Uri("https://pkgs.dev.azure.com")
};

public NuGetVersion? UnoVersion { get; set; }

public async Task<Stream> DownloadPackageAsync(string packageId, string version)
{
var downloadUrl = $"/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_apis/packaging/feeds/e7ce08df-613a-41a3-8449-d42784dd45ce/nuget/packages/{packageId}/versions/{version}/content";
using var response = await PrivateNuGetClient.GetAsync(downloadUrl);

if (!response.IsSuccessStatusCode)
return Stream.Null;

using var tempStream = await response.Content.ReadAsStreamAsync();
var memoryStream = new MemoryStream();
await tempStream.CopyToAsync(memoryStream);

return memoryStream;
}

internal record VersionsResponse(string[] Versions);

public async Task<IEnumerable<NuGetVersion>> GetPackageVersions(string packageId)
{
var allVersions = new List<string>();
var publicVersions = await GetPublicPackageVersions(packageId);
allVersions.AddRange(publicVersions);

if (!UnoVersion.HasValue || !UnoVersion.Value.IsPreview)
{
var privateVersions = await GetPrivatePackageVersions(packageId);
allVersions.AddRange(privateVersions);
}

var output = new List<NuGetVersion>();
foreach (var version in allVersions.Distinct())
{
if (NuGetVersion.TryParse(version, out var nugetVersion))
{
output.Add(nugetVersion);
}
}

return output.OrderByDescending(x => x);
}

private async Task<IEnumerable<string>> GetPrivatePackageVersions(string packageId)
{
try
{
var response = await PrivateNuGetClient.GetFromJsonAsync<VersionsResponse>($"/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_packaging/e7ce08df-613a-41a3-8449-d42784dd45ce/nuget/v3/flat2/{packageId.ToLowerInvariant()}/index.json");
return response?.Versions ?? [];
}
catch
{
return [];
}
}

private async Task<IEnumerable<string>> GetPublicPackageVersions(string packageId)
{
try
{
var response = await PublicNuGetClient.GetFromJsonAsync<VersionsResponse>($"/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json");
return response?.Versions ?? [];
}
catch
{
return [];
}
}

public async Task<string> GetVersionAsync(string packageId, bool preview, string? minimumVersionString = null)
{
var versions = await GetPackageVersions(packageId);
versions = versions.Where(x => x.IsPreview == preview);

if (NuGetVersion.TryParse(minimumVersionString, out var minimumVersion))
{
versions = versions.Where(x => minimumVersion.Version <= x.Version);
}

if (!versions.Any())
{
return string.Empty;
}

return versions.OrderByDescending(x => x).First().OriginalVersion;
}

public void Dispose()
{
PublicNuGetClient.Dispose();
}
private const string UnoWinUIPackageId = "Uno.WinUI";
private static PackageValidationRecord _validation = new ();
private static Dictionary<string, IEnumerable<NuGetVersion>> _cachedVersions = [];

private HttpClient PublicNuGetClient { get; } = new HttpClient
{
BaseAddress = new Uri("https://api.nuget.org")
};

private HttpClient PrivateNuGetClient { get; } = new HttpClient
{
BaseAddress = new Uri("https://pkgs.dev.azure.com")
};

public NuGetVersion? UnoVersion { get; set; }

public async Task<Stream> DownloadPackageAsync(string packageId, string version)
{
var downloadUrl = $"/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_apis/packaging/feeds/e7ce08df-613a-41a3-8449-d42784dd45ce/nuget/packages/{packageId}/versions/{version}/content";
using var response = await PrivateNuGetClient.GetAsync(downloadUrl);

if (!response.IsSuccessStatusCode)
return Stream.Null;

using var tempStream = await response.Content.ReadAsStreamAsync();
var memoryStream = new MemoryStream();
await tempStream.CopyToAsync(memoryStream);

return memoryStream;
}

internal record VersionsResponse(string[] Versions);

public async Task<IEnumerable<NuGetVersion>> GetPackageVersions(string packageId)
{
if (_cachedVersions.TryGetValue(packageId, out var cachedVersions))
{
return cachedVersions;
}

var allVersions = new List<string>();
var publicVersions = await GetPublicPackageVersions(packageId);
allVersions.AddRange(publicVersions);

if (!UnoVersion.HasValue || !UnoVersion.Value.IsPreview)
{
var privateVersions = await GetPrivatePackageVersions(packageId);
allVersions.AddRange(privateVersions);
}

var output = new List<NuGetVersion>();
foreach (var version in allVersions.Distinct())
{
if (NuGetVersion.TryParse(version, out var nugetVersion))
{
output.Add(nugetVersion);
}
}

var latestVersions = output
.GroupBy(x => GetGroupVersion(packageId, x))
.Select(g => g.OrderByDescending(x => x).First())
.OrderByDescending(x => x)
.ToArray();

if (!RequiresValidation(packageId))
{
_cachedVersions[packageId] = latestVersions;
return latestVersions;
}

var validatedOutput = new List<NuGetVersion>();
Console.WriteLine($"Validating available versions for {packageId}...");
foreach(var version in latestVersions)
{
if (await ValidatePackage(packageId, version))
{
validatedOutput.Add(version);
continue;
}

break;
}

_cachedVersions[packageId] = validatedOutput;
return validatedOutput;
}

private static NuGetVersion GetGroupVersion(string packageId, NuGetVersion packageVersion)
{
if (packageId.StartsWith("Uno", StringComparison.InvariantCultureIgnoreCase))
{
return NuGetVersion.Parse($"{packageVersion.Version.Major}.{packageVersion.Version.Minor}.0");
}

return NuGetVersion.Parse($"{packageVersion.Version.Major}.{packageVersion.Version.Minor}.{packageVersion.Version.Build}");
}

private bool RequiresValidation(string packageId) =>
UnoVersion is not null &&
packageId.Contains("Uno", StringComparison.InvariantCultureIgnoreCase) &&
!packageId.StartsWith("Uno.Sdk", StringComparison.InvariantCultureIgnoreCase);

private async Task<bool> ValidatePackage(string packageId, NuGetVersion version)
{
if (_validation.HasBeenChecked(packageId, version))
{
return _validation.IsValid(packageId, version);
}

var nuspecUrl = $"/v3-flatcontainer/{packageId.ToLowerInvariant()}/{version.OriginalVersion}/{packageId.ToLowerInvariant()}.nuspec";
using var response = PublicNuGetClient.GetAsync(nuspecUrl).Result;
if (!response.IsSuccessStatusCode)
{
// This could happen if the package we are checking is not publicly available.
return true;
}

var packageResponse = await response.Content.ReadAsStringAsync();
var xDocument = XDocument.Parse(packageResponse);

// Define the namespace manager
var namespaceManager = new XmlNamespaceManager(new NameTable());
namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd");

// Select dependency groups
var dependencyGroups = xDocument.XPathSelectElements("//ns:dependencies/ns:group", namespaceManager);

foreach (var group in dependencyGroups)
{
if (_validation.HasBeenChecked(packageId, version))
continue;

_validation.AddResult(packageId, version, await IsCompatibleWithUnoWinUI(group));
}

_validation.AddResult(packageId, version, true);
return _validation.IsValid(packageId, version);
}

private async Task<bool> IsCompatibleWithUnoWinUI(XElement group)
{
var dependencies = group.Elements().Where(e => e.Name.LocalName == "dependency").ToList();
var unoWinUIDependency = dependencies.FirstOrDefault(d => d.Attribute("id")?.Value == UnoWinUIPackageId);

// We don't have a dependency on Uno.WinUI
if (unoWinUIDependency is null)
{
var unoDependencies = dependencies.Where(x => x.Attribute("id")?.Value.Contains("Uno", StringComparison.InvariantCultureIgnoreCase) ?? false);

// Check for Transitive Dependency
if (unoDependencies.Any())
{
var transitiveDependencies = unoDependencies.ToDictionary(x => x.Attribute("id")!.Value, x => NuGetVersion.Parse(x.Attribute("version")!.Value));
foreach((var packageId, var version) in transitiveDependencies)
{
if (_validation.HasBeenChecked(packageId, version))
{
continue;
}
else if (!await ValidatePackage(packageId, version))
{
return false;
}
}
}

return true;
}

if (!NuGetVersion.TryParse(unoWinUIDependency.Attribute("version")?.Value, out var unoWinUIVersion))
{
// This shouldn't happen
return false;
}

if (!_validation.HasBeenChecked(UnoWinUIPackageId, unoWinUIVersion))
{
_validation.AddResult(UnoWinUIPackageId, unoWinUIVersion, UnoVersion >= unoWinUIVersion);
}

return _validation.IsValid(UnoWinUIPackageId, unoWinUIVersion);
}

private async Task<IEnumerable<string>> GetPrivatePackageVersions(string packageId)
{
try
{
var response = await PrivateNuGetClient.GetFromJsonAsync<VersionsResponse>($"/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_packaging/e7ce08df-613a-41a3-8449-d42784dd45ce/nuget/v3/flat2/{packageId.ToLowerInvariant()}/index.json");
return response?.Versions ?? [];
}
catch
{
return [];
}
}

private async Task<IEnumerable<string>> GetPublicPackageVersions(string packageId)
{
try
{
var response = await PublicNuGetClient.GetFromJsonAsync<VersionsResponse>($"/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json");
return response?.Versions ?? [];
}
catch
{
return [];
}
}

public async Task<string> GetVersionAsync(string packageId, bool preview, string? minimumVersionString = null)
{
var versions = await GetPackageVersions(packageId);
versions = versions.Where(x => x.IsPreview == preview);

// https://api.nuget.org/v3-flatcontainer/uno.extensions.hosting.winui/4.2.0-dev.137/uno.extensions.hosting.winui.nuspec
if (!string.IsNullOrEmpty(minimumVersionString) && NuGetVersion.TryParse(minimumVersionString, out var minimumVersion))
{
versions = versions.Where(x => minimumVersion.Version <= x.Version);
}

if (!versions.Any())
{
return string.Empty;
}

return versions.OrderByDescending(x => x).First().OriginalVersion;
}

public void Dispose()
{
PublicNuGetClient.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Uno.Sdk.Models;

namespace Uno.Sdk.Updater;
namespace Uno.Sdk.Updater.Utils;

internal static class UpdaterBuildContext
{
Expand Down
Loading
Loading