Skip to content

Commit

Permalink
Merge pull request #825 from unoplatform/mergify/bp/release/stable/5.…
Browse files Browse the repository at this point in the history
…3/pr-799

Sdk Updater - Uno Version Validation (backport #799)
  • Loading branch information
dansiegel authored Jul 10, 2024
2 parents e06f555 + 2b3a111 commit b7f6b0e
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 103 deletions.
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

0 comments on commit b7f6b0e

Please sign in to comment.