From 6337bdba02e4eb74f284bae6e1714dd91b2834bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 29 Apr 2026 16:33:35 -0400 Subject: [PATCH 1/2] Prepare Pinget Core 0.3.0 for UniGetUI --- dotnet/Directory.Build.props | 2 +- .../CoreTests.cs | 57 +++++++- .../Devolutions.Pinget.Core.csproj | 1 + dotnet/src/Devolutions.Pinget.Core/Models.cs | 3 +- .../src/Devolutions.Pinget.Core/Repository.cs | 133 +++++++----------- .../src/Devolutions.Pinget.Core/RestSource.cs | 10 +- .../Devolutions.Pinget.Core/SourceStore.cs | 4 +- 7 files changed, 118 insertions(+), 92 deletions(-) diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 6d59fcf..7433b59 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -1,5 +1,5 @@ - + Devolutions Devolutions Inc. MIT diff --git a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs index 96c3c22..73ce79c 100644 --- a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs +++ b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs @@ -736,14 +736,51 @@ public void ParseYamlManifest_PreservesTopLevelNestedInstallerType() } [Fact] - public void GetSqliteNativeLibraryCandidates_ProbesRootAndRidFolder() + public void TryGetWinGetPackageIdentityFromLocalId_UsesSourceIdentifierSuffix() { - var candidates = Repository.GetSqliteNativeLibraryCandidates(@"C:\module").ToList(); + SourceRecord source = new() + { + Name = "winget", + Kind = SourceKind.PreIndexed, + Arg = "https://cdn.winget.microsoft.com/cache", + Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe", + }; + + bool result = Repository.TryGetWinGetPackageIdentityFromLocalId( + @"ARP\User\X64\Atlassian.AtlassianCLI_Microsoft.Winget.Source_8wekyb3d8bbwe", + [source], + out string? packageId, + out string? sourceName + ); + + Assert.True(result); + Assert.Equal("Atlassian.AtlassianCLI", packageId); + Assert.Equal("winget", sourceName); + } + + [Fact] + public void TryGetWinGetPackageIdentityFromLocalId_IgnoresUnknownSourceIdentifier() + { + SourceRecord source = new() + { + Name = "winget", + Kind = SourceKind.PreIndexed, + Arg = "https://cdn.winget.microsoft.com/cache", + Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe", + }; - Assert.Equal(@"C:\module\e_sqlite3.dll", candidates[0]); - Assert.Contains(Path.Combine(@"C:\module", "runtimes"), candidates[1]); - Assert.EndsWith(Path.Combine("native", "e_sqlite3.dll"), candidates[1], StringComparison.OrdinalIgnoreCase); + bool result = Repository.TryGetWinGetPackageIdentityFromLocalId( + @"ARP\Machine\X64\Contoso.Tool_Unknown.Source_1234", + [source], + out string? packageId, + out string? sourceName + ); + + Assert.False(result); + Assert.Null(packageId); + Assert.Null(sourceName); } + } public class PinStoreTests @@ -1552,6 +1589,16 @@ public void ShowManifest_RestExactId_ReturnsSerializableManifestAndSelectedInsta Assert.Empty(result.SourceWarnings); Assert.Empty(diagnostics); + var typedResult = repo.Show(new PackageQuery + { + Id = TesslPackageId, + Exact = true, + Source = "test", + InstallerArchitecture = "x64", + }); + + Assert.Equal("Tessl short description", typedResult.Manifest.ShortDescription); + var json = JsonSerializer.Serialize(result, PingetJsonContext.Default.SerializableShowManifest); using var document = JsonDocument.Parse(json); Assert.Equal(TesslPackageId, document.RootElement.GetProperty(nameof(SerializableShowManifest.PackageIdentifier)).GetString()); diff --git a/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj b/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj index 62a8689..4b1460a 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj +++ b/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj @@ -19,6 +19,7 @@ + diff --git a/dotnet/src/Devolutions.Pinget.Core/Models.cs b/dotnet/src/Devolutions.Pinget.Core/Models.cs index 74662e7..fc8dfb5 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Models.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Models.cs @@ -247,6 +247,7 @@ public record Manifest public string Channel { get; init; } = ""; public string? Publisher { get; init; } public string? Description { get; init; } + public string? ShortDescription { get; init; } public string? Moniker { get; init; } public string? PackageUrl { get; init; } public string? PublisherUrl { get; init; } @@ -343,7 +344,7 @@ public SerializableShowManifest ToSerializableManifest() Author = Manifest.Author, Moniker = Manifest.Moniker, Description = Manifest.Description, - ShortDescription = GetString("ShortDescription"), + ShortDescription = Manifest.ShortDescription ?? GetString("ShortDescription"), PackageUrl = Manifest.PackageUrl, PublisherUrl = Manifest.PublisherUrl, PublisherSupportUrl = Manifest.PublisherSupportUrl, diff --git a/dotnet/src/Devolutions.Pinget.Core/Repository.cs b/dotnet/src/Devolutions.Pinget.Core/Repository.cs index f235522..ab4899d 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Repository.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Repository.cs @@ -1,8 +1,6 @@ -using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json.Nodes; -using System.Threading; using Microsoft.Data.Sqlite; using YamlDotNet.Core; using YamlDotNet.Serialization; @@ -20,8 +18,6 @@ public class Repository : IDisposable internal const string RepairUnsupportedWarning = "Repairing packages is not supported on this platform; no changes were made."; internal const string RepairReinstallWarning = "Pinget repair currently re-runs the package install flow for the selected package."; - private static int s_sqliteNativeLibraryInitialized; - private readonly string _appRoot; private readonly HttpClient _client; private readonly bool _useSystemWingetSources; @@ -48,7 +44,7 @@ private Repository( public static Repository Open(RepositoryOptions? options = null) { options ??= new RepositoryOptions(); - EnsureSqliteNativeLibraryLoaded(); + SQLitePCL.Batteries_V2.Init(); var appRoot = SourceStoreManager.NormalizeAppRoot(options.AppRoot ?? Environment.GetEnvironmentVariable(AppRootEnvironmentVariable)); SourceStoreManager.EnsureAppDirs(appRoot); var useSystemWingetSources = SourceStoreManager.UsesSystemWingetSourceCommands(appRoot); @@ -58,79 +54,6 @@ public static Repository Open(RepositoryOptions? options = null) return new Repository(appRoot, client, store, useSystemWingetSources, options.Diagnostics); } - internal static IEnumerable GetSqliteNativeLibraryCandidates(string assemblyDirectory) - { - yield return Path.Combine(assemblyDirectory, "e_sqlite3.dll"); - - var rid = RuntimeInformation.ProcessArchitecture switch - { - Architecture.X86 => "win-x86", - Architecture.Arm => "win-arm", - Architecture.Arm64 => "win-arm64", - _ => "win-x64", - }; - - yield return Path.Combine(assemblyDirectory, "runtimes", rid, "native", "e_sqlite3.dll"); - } - - private static void EnsureSqliteNativeLibraryLoaded() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return; - - if (Interlocked.Exchange(ref s_sqliteNativeLibraryInitialized, 1) != 0) - return; - - var assemblyDirectory = Path.GetDirectoryName(typeof(Repository).Assembly.Location); - if (string.IsNullOrWhiteSpace(assemblyDirectory)) - return; - - var nativeLibraryPath = GetSqliteNativeLibraryCandidates(assemblyDirectory) - .FirstOrDefault(File.Exists); - - if (string.IsNullOrWhiteSpace(nativeLibraryPath)) - return; - - RegisterSqliteDllImportResolvers(nativeLibraryPath); - - try - { - NativeLibrary.Load(nativeLibraryPath); - } - catch - { - // The resolver path is the important fix for library-hosted environments. - } - } - - private static void RegisterSqliteDllImportResolvers(string nativeLibraryPath) - { - RegisterSqliteDllImportResolver("SQLitePCLRaw.provider.e_sqlite3", nativeLibraryPath); - RegisterSqliteDllImportResolver("SQLitePCLRaw.core", nativeLibraryPath); - } - - private static void RegisterSqliteDllImportResolver(string assemblyName, string nativeLibraryPath) - { - try - { - var assembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(candidate => string.Equals(candidate.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) - ?? Assembly.Load(new AssemblyName(assemblyName)); - - NativeLibrary.SetDllImportResolver(assembly, (libraryName, _, _) => - { - if (!string.Equals(libraryName, "e_sqlite3", StringComparison.OrdinalIgnoreCase)) - return IntPtr.Zero; - - return NativeLibrary.Load(nativeLibraryPath); - }); - } - catch - { - // Ignore duplicate resolver registration or unavailable optional assemblies. - } - } - public void Dispose() => _client.Dispose(); public string AppRoot => _appRoot; public static IReadOnlyList SupportedAdminSettings => SettingsStoreManager.SupportedAdminSettings; @@ -1562,6 +1485,7 @@ List InstArr(string key) Version = GetStr("PackageVersion"), Publisher = GetOptStr("Publisher"), Description = GetOptStr("Description") ?? GetOptStr("ShortDescription"), + ShortDescription = GetOptStr("ShortDescription"), Moniker = GetOptStr("Moniker"), PackageUrl = GetOptStr("PackageUrl"), PublisherUrl = GetOptStr("PublisherUrl"), @@ -1952,7 +1876,7 @@ private static int ListSortWeight(InstalledPackage pkg) return 2; } - private static ListMatch ListMatchFromInstalled(InstalledPackage pkg) + private ListMatch ListMatchFromInstalled(InstalledPackage pkg) { string? availableVersion = null; if (pkg.Correlated?.Version is string av) @@ -1961,14 +1885,27 @@ private static ListMatch ListMatchFromInstalled(InstalledPackage pkg) RestSource.CompareVersionStrings(av, pkg.InstalledVersion) > 0) availableVersion = av; } + + string packageId = pkg.Correlated?.Id ?? pkg.LocalId; + string? sourceName = pkg.Correlated?.SourceName; + if (sourceName is null && TryGetWinGetPackageIdentityFromLocalId( + pkg.LocalId, + _store.Sources, + out string? localPackageId, + out string? localSourceName)) + { + packageId = localPackageId; + sourceName = localSourceName; + } + return new() { Name = pkg.Name, - Id = pkg.Correlated?.Id ?? pkg.LocalId, + Id = packageId, LocalId = pkg.LocalId, InstalledVersion = pkg.InstalledVersion, AvailableVersion = availableVersion, - SourceName = pkg.Correlated?.SourceName, + SourceName = sourceName, Publisher = pkg.Publisher, Scope = pkg.Scope, InstallerCategory = pkg.InstallerCategory, @@ -1979,6 +1916,40 @@ private static ListMatch ListMatchFromInstalled(InstalledPackage pkg) }; } + internal static bool TryGetWinGetPackageIdentityFromLocalId( + string localId, + IReadOnlyList sources, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? packageId, + out string? sourceName) + { + packageId = null; + sourceName = null; + + int packageIdStartIndex = localId.LastIndexOf('\\') + 1; + SourceRecord? source = null; + int sourceIdentifierStartIndex = -1; + foreach (SourceRecord candidate in sources) + { + string sourceSuffix = "_" + candidate.Identifier; + if (!localId.EndsWith(sourceSuffix, StringComparison.OrdinalIgnoreCase)) + continue; + + source = candidate; + sourceIdentifierStartIndex = localId.Length - sourceSuffix.Length; + break; + } + + if (source is null) + return false; + + if (sourceIdentifierStartIndex <= packageIdStartIndex) + return false; + + packageId = localId[packageIdStartIndex..sourceIdentifierStartIndex]; + sourceName = source.Name; + return !string.IsNullOrWhiteSpace(packageId); + } + private static bool ListQueryNeedsAvailableLookup(ListQuery query) => query.Query is not null || query.Id is not null || query.Name is not null || query.Moniker is not null || query.Tag is not null || query.Command is not null; diff --git a/dotnet/src/Devolutions.Pinget.Core/RestSource.cs b/dotnet/src/Devolutions.Pinget.Core/RestSource.cs index 3c587b9..aa9c71b 100644 --- a/dotnet/src/Devolutions.Pinget.Core/RestSource.cs +++ b/dotnet/src/Devolutions.Pinget.Core/RestSource.cs @@ -46,7 +46,8 @@ public static RestInformation LoadInformation(HttpClient client, SourceRecord so using var response = client.GetAsync(url).GetAwaiter().GetResult(); response.EnsureSuccessStatusCode(); var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var json = JsonSerializer.Deserialize(body); + using var document = JsonDocument.Parse(body); + var json = document.RootElement; // The REST response wraps data in a "Data" property var data = json.TryGetProperty("Data", out var d) ? d : json; @@ -96,7 +97,8 @@ public static (List Results, bool Truncated) Search( response.EnsureSuccessStatusCode(); var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var json = JsonSerializer.Deserialize(responseBody); + using var document = JsonDocument.Parse(responseBody); + var json = document.RootElement; var data = json.TryGetProperty("Data", out var d) && d.ValueKind == JsonValueKind.Array ? d.EnumerateArray().ToList() @@ -140,7 +142,8 @@ public static (Manifest Manifest, object StructuredDocuments) FetchManifestWithD response.EnsureSuccessStatusCode(); var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var json = JsonSerializer.Deserialize(body); + using var document = JsonDocument.Parse(body); + var json = document.RootElement; return ParseRestManifest(json, packageId, version, channel); } @@ -388,6 +391,7 @@ InstallerSwitches GetSwitches(JsonElement el) Channel = channel, Publisher = GetOptStr(defaultLocale, "Publisher"), Description = GetOptStr(defaultLocale, "Description") ?? GetOptStr(defaultLocale, "ShortDescription"), + ShortDescription = GetOptStr(defaultLocale, "ShortDescription"), Moniker = GetOptStr(data, "Moniker"), PackageUrl = GetOptStr(defaultLocale, "PackageUrl"), PublisherUrl = GetOptStr(defaultLocale, "PublisherUrl"), diff --git a/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs b/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs index 1e91f7d..c69ac4e 100644 --- a/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs +++ b/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs @@ -301,7 +301,9 @@ private static bool TryGetPackagedLocalStateRoot(string root, out string localSt private static string GetPackagedSecureSettingsRoot() { var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); - var sid = WindowsIdentity.GetCurrent().User?.Value ?? "UnknownSid"; + var sid = OperatingSystem.IsWindows() + ? WindowsIdentity.GetCurrent().User?.Value ?? "UnknownSid" + : "UnknownSid"; return Path.Combine(programData, "Microsoft", "WinGet", sid, "settings", "pkg", PackagedName); } From d94d2b8ff5615352c2efef038720b846132eb06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 29 Apr 2026 16:46:19 -0400 Subject: [PATCH 2/2] Restore manual SQLite resolver --- .../CoreTests.cs | 10 +++ .../Devolutions.Pinget.Core.csproj | 1 - .../src/Devolutions.Pinget.Core/Repository.cs | 79 ++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs index 73ce79c..421908a 100644 --- a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs +++ b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs @@ -735,6 +735,16 @@ public void ParseYamlManifest_PreservesTopLevelNestedInstallerType() Assert.Equal("portable", installer.NestedInstallerType); } + [Fact] + public void GetSqliteNativeLibraryCandidates_ProbesRootAndRidFolder() + { + var candidates = Repository.GetSqliteNativeLibraryCandidates(@"C:\module").ToList(); + + Assert.Equal(@"C:\module\e_sqlite3.dll", candidates[0]); + Assert.Contains(Path.Combine(@"C:\module", "runtimes"), candidates[1]); + Assert.EndsWith(Path.Combine("native", "e_sqlite3.dll"), candidates[1], StringComparison.OrdinalIgnoreCase); + } + [Fact] public void TryGetWinGetPackageIdentityFromLocalId_UsesSourceIdentifierSuffix() { diff --git a/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj b/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj index 4b1460a..62a8689 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj +++ b/dotnet/src/Devolutions.Pinget.Core/Devolutions.Pinget.Core.csproj @@ -19,7 +19,6 @@ - diff --git a/dotnet/src/Devolutions.Pinget.Core/Repository.cs b/dotnet/src/Devolutions.Pinget.Core/Repository.cs index ab4899d..66097ed 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Repository.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Repository.cs @@ -1,6 +1,8 @@ +using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json.Nodes; +using System.Threading; using Microsoft.Data.Sqlite; using YamlDotNet.Core; using YamlDotNet.Serialization; @@ -18,6 +20,8 @@ public class Repository : IDisposable internal const string RepairUnsupportedWarning = "Repairing packages is not supported on this platform; no changes were made."; internal const string RepairReinstallWarning = "Pinget repair currently re-runs the package install flow for the selected package."; + private static int s_sqliteNativeLibraryInitialized; + private readonly string _appRoot; private readonly HttpClient _client; private readonly bool _useSystemWingetSources; @@ -44,7 +48,7 @@ private Repository( public static Repository Open(RepositoryOptions? options = null) { options ??= new RepositoryOptions(); - SQLitePCL.Batteries_V2.Init(); + EnsureSqliteNativeLibraryLoaded(); var appRoot = SourceStoreManager.NormalizeAppRoot(options.AppRoot ?? Environment.GetEnvironmentVariable(AppRootEnvironmentVariable)); SourceStoreManager.EnsureAppDirs(appRoot); var useSystemWingetSources = SourceStoreManager.UsesSystemWingetSourceCommands(appRoot); @@ -54,6 +58,79 @@ public static Repository Open(RepositoryOptions? options = null) return new Repository(appRoot, client, store, useSystemWingetSources, options.Diagnostics); } + internal static IEnumerable GetSqliteNativeLibraryCandidates(string assemblyDirectory) + { + yield return Path.Combine(assemblyDirectory, "e_sqlite3.dll"); + + var rid = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X86 => "win-x86", + Architecture.Arm => "win-arm", + Architecture.Arm64 => "win-arm64", + _ => "win-x64", + }; + + yield return Path.Combine(assemblyDirectory, "runtimes", rid, "native", "e_sqlite3.dll"); + } + + private static void EnsureSqliteNativeLibraryLoaded() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + if (Interlocked.Exchange(ref s_sqliteNativeLibraryInitialized, 1) != 0) + return; + + var assemblyDirectory = Path.GetDirectoryName(typeof(Repository).Assembly.Location); + if (string.IsNullOrWhiteSpace(assemblyDirectory)) + return; + + var nativeLibraryPath = GetSqliteNativeLibraryCandidates(assemblyDirectory) + .FirstOrDefault(File.Exists); + + if (string.IsNullOrWhiteSpace(nativeLibraryPath)) + return; + + RegisterSqliteDllImportResolvers(nativeLibraryPath); + + try + { + NativeLibrary.Load(nativeLibraryPath); + } + catch + { + // The resolver path is the important fix for library-hosted environments. + } + } + + private static void RegisterSqliteDllImportResolvers(string nativeLibraryPath) + { + RegisterSqliteDllImportResolver("SQLitePCLRaw.provider.e_sqlite3", nativeLibraryPath); + RegisterSqliteDllImportResolver("SQLitePCLRaw.core", nativeLibraryPath); + } + + private static void RegisterSqliteDllImportResolver(string assemblyName, string nativeLibraryPath) + { + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(candidate => string.Equals(candidate.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) + ?? Assembly.Load(new AssemblyName(assemblyName)); + + NativeLibrary.SetDllImportResolver(assembly, (libraryName, _, _) => + { + if (!string.Equals(libraryName, "e_sqlite3", StringComparison.OrdinalIgnoreCase)) + return IntPtr.Zero; + + return NativeLibrary.Load(nativeLibraryPath); + }); + } + catch + { + // Ignore duplicate resolver registration or unavailable optional assemblies. + } + } + public void Dispose() => _client.Dispose(); public string AppRoot => _appRoot; public static IReadOnlyList SupportedAdminSettings => SettingsStoreManager.SupportedAdminSettings;