Skip to content

Commit 72828c0

Browse files
authored
Relocate storage and cover source-mode environment variable
2 parents 0803d18 + 3593e80 commit 72828c0

14 files changed

Lines changed: 348 additions & 23 deletions

File tree

‎README.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,34 @@ Pinget is designed to keep **source-backed functionality** working cross-platfor
248248
- `list` and upgrade inventory return empty results with an unsupported warning
249249
- `install`, `uninstall`, executed `upgrade`, and non-dry-run `import` return explicit no-op results with unsupported warnings
250250

251+
## Storage location and source mode
252+
253+
Two environment variables control where Pinget keeps its state and which sources it resolves
254+
against. Both are read by the Rust CLI and the C# core.
255+
256+
| Variable | Effect |
257+
| --- | --- |
258+
| `PINGET_APPROOT` | Overrides the per-user storage root (`%LOCALAPPDATA%\Devolutions\Pinget` by default). Useful for embedding Pinget in a portable or sandboxed host. |
259+
| `PINGET_SOURCE_MODE` | Selects the source mode explicitly: `auto`, `private`, or `system-winget-mirror`. |
260+
261+
Source modes:
262+
263+
- `auto` mirrors the machine's configured WinGet sources, falling back to the private store when
264+
they cannot be read.
265+
- `private` uses only the source list stored under the app root, seeded with the stock `winget`
266+
and `msstore` entries.
267+
- `system-winget-mirror` mirrors the machine's WinGet sources with no fallback.
268+
269+
Without `PINGET_SOURCE_MODE`, setting `PINGET_APPROOT` also selects `private`, on the assumption
270+
that a caller supplying its own root wants an isolated instance. A host that relocates storage but
271+
still wants the machine's real source list must set `PINGET_SOURCE_MODE=auto` as well; otherwise
272+
sources the user added to WinGet are silently absent.
273+
274+
Precedence is the same in both implementations: an explicitly chosen mode wins, then
275+
`PINGET_SOURCE_MODE`, then the app-root inference. In the C# API `RepositoryOptions.SourceMode` is
276+
nullable for exactly this reason — leave it unset to let the environment decide, and note that
277+
setting it to `SourceMode.Auto` is an explicit choice that the environment will not override.
278+
251279
## Custom REST sources
252280

253281
Both implementations support custom REST sources, including third-party services such as `winget.pro`.

‎dotnet/src/Devolutions.Pinget.Cli/Program.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Devolutions.Pinget.Cli;
77
using Devolutions.Pinget.Core;
88

9-
const string Version = "0.10.0";
9+
const string Version = "0.11.0";
1010
const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made.";
1111

1212
if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase)))

‎dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212

1313
namespace Devolutions.Pinget.Core.Tests;
1414

15+
[CollectionDefinition(Name, DisableParallelization = true)]
16+
public class RepositoryStateCollection
17+
{
18+
public const string Name = "RepositoryState";
19+
}
20+
1521
public class VersionCompareTests
1622
{
1723
[Theory]
@@ -112,6 +118,7 @@ public void SelectRestVersion_WithRequestedChannelMismatch_Throws()
112118
}
113119
}
114120

121+
[Collection(RepositoryStateCollection.Name)]
115122
public class SourceStoreTests
116123
{
117124
[Fact]
@@ -365,6 +372,127 @@ public void PackagedSecureSettingsStub_DelegatesToSystemWingetExport()
365372
}
366373
}
367374

375+
[Theory]
376+
[InlineData("auto", SourceMode.Auto)]
377+
[InlineData(" Auto ", SourceMode.Auto)]
378+
[InlineData("PRIVATE", SourceMode.Private)]
379+
[InlineData("system-winget-mirror", SourceMode.SystemWingetMirror)]
380+
[InlineData("SystemWingetMirror", SourceMode.SystemWingetMirror)]
381+
public void TryParseSourceMode_AcceptsKnownSpellings(string value, SourceMode expected)
382+
{
383+
Assert.True(Repository.TryParseSourceMode(value, out var parsed));
384+
Assert.Equal(expected, parsed);
385+
}
386+
387+
[Theory]
388+
[InlineData(null)]
389+
[InlineData("")]
390+
[InlineData("mirror")]
391+
[InlineData("system_winget_mirror")]
392+
public void TryParseSourceMode_RejectsUnknownSpellings(string? value)
393+
{
394+
Assert.False(Repository.TryParseSourceMode(value, out _));
395+
}
396+
397+
[Fact]
398+
public void ResolveRequestedSourceMode_KeepsPrivateDefaultForACustomAppRoot()
399+
{
400+
Assert.Equal(
401+
SourceMode.Private,
402+
Repository.ResolveRequestedSourceMode(null, @"C:\custom", null));
403+
Assert.Equal(
404+
SourceMode.Auto,
405+
Repository.ResolveRequestedSourceMode(null, null, null));
406+
}
407+
408+
[Fact]
409+
public void ResolveRequestedSourceMode_LetsTheEnvironmentOverrideACustomAppRoot()
410+
{
411+
Assert.Equal(
412+
SourceMode.Auto,
413+
Repository.ResolveRequestedSourceMode(null, @"C:\custom", "auto"));
414+
Assert.Equal(
415+
SourceMode.SystemWingetMirror,
416+
Repository.ResolveRequestedSourceMode(null, @"C:\custom", "system-winget-mirror"));
417+
Assert.Equal(
418+
SourceMode.Private,
419+
Repository.ResolveRequestedSourceMode(null, null, "private"));
420+
}
421+
422+
[Fact]
423+
public void ResolveRequestedSourceMode_IgnoresTheEnvironmentWhenTheCallerIsExplicit()
424+
{
425+
Assert.Equal(
426+
SourceMode.SystemWingetMirror,
427+
Repository.ResolveRequestedSourceMode(SourceMode.SystemWingetMirror, @"C:\custom", "private"));
428+
Assert.Equal(
429+
SourceMode.Private,
430+
Repository.ResolveRequestedSourceMode(SourceMode.Private, null, "auto"));
431+
}
432+
433+
[Fact]
434+
public void ResolveRequestedSourceMode_TreatsAnExplicitAutoAsAChoiceRatherThanAnUnsetValue()
435+
{
436+
Assert.Equal(
437+
SourceMode.Auto,
438+
Repository.ResolveRequestedSourceMode(SourceMode.Auto, @"C:\custom", "private"));
439+
Assert.Equal(
440+
SourceMode.Auto,
441+
Repository.ResolveRequestedSourceMode(SourceMode.Auto, @"C:\custom", null));
442+
}
443+
444+
[Fact]
445+
public void ResolveRequestedSourceMode_IgnoresAnUnparseableEnvironmentValue()
446+
{
447+
Assert.Equal(
448+
SourceMode.Private,
449+
Repository.ResolveRequestedSourceMode(null, @"C:\custom", "not-a-mode"));
450+
Assert.Equal(
451+
SourceMode.Auto,
452+
Repository.ResolveRequestedSourceMode(null, null, "not-a-mode"));
453+
}
454+
455+
[Theory]
456+
[InlineData(null, false)]
457+
[InlineData("auto", true)]
458+
[InlineData("system-winget-mirror", true)]
459+
[InlineData("private", false)]
460+
[InlineData("not-a-mode", false)]
461+
public void RepositoryOpen_ReadsTheSourceModeEnvironmentVariable(string? sourceMode, bool expectsMirror)
462+
{
463+
var appRoot = TestPaths.CreateTempAppRoot();
464+
var originalRunner = SystemWingetSourceStore.CommandRunner;
465+
var originalSourceMode = Environment.GetEnvironmentVariable("PINGET_SOURCE_MODE");
466+
try
467+
{
468+
SystemWingetSourceStore.CommandRunner = _ => new WingetCommandResult(
469+
0,
470+
"""
471+
{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
472+
""",
473+
"");
474+
Environment.SetEnvironmentVariable("PINGET_SOURCE_MODE", sourceMode);
475+
476+
using var repo = Repository.Open(new RepositoryOptions
477+
{
478+
AppRoot = appRoot,
479+
UserAgent = "pinget-dotnet-tests/1.0",
480+
});
481+
482+
var mirrorPath = Path.Combine(appRoot, "system-sources.json");
483+
Assert.Equal(expectsMirror, File.Exists(mirrorPath));
484+
485+
if (expectsMirror)
486+
Assert.Contains("contoso", File.ReadAllText(mirrorPath));
487+
}
488+
finally
489+
{
490+
SystemWingetSourceStore.CommandRunner = originalRunner;
491+
Environment.SetEnvironmentVariable("PINGET_SOURCE_MODE", originalSourceMode);
492+
TestPaths.DeleteAppRoot(appRoot);
493+
}
494+
}
495+
368496
[Fact]
369497
public void RepositoryOpen_UsesCustomAppRoot()
370498
{
@@ -1177,6 +1305,7 @@ PRIMARY KEY (package_id, source_id)
11771305
}
11781306
}
11791307

1308+
[Collection(RepositoryStateCollection.Name)]
11801309
public class RepositoryParityTests
11811310
{
11821311
[Fact]
@@ -2992,6 +3121,7 @@ public void CreateRepairInstallRequest_ForcesReinstallOfResolvedInstalledPackage
29923121
}
29933122
}
29943123

3124+
[Collection(RepositoryStateCollection.Name)]
29953125
public class RepositoryEmbeddingTests
29963126
{
29973127
private const string TesslPackageId = "tessl.tessl";

‎dotnet/src/Devolutions.Pinget.Core/Models.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ public record RepositoryOptions
4747
/// Storage root for Pinget state and caches.
4848
/// </summary>
4949
public string? AppRoot { get; init; }
50-
public SourceMode SourceMode { get; init; } = SourceMode.Auto;
50+
/// <summary>
51+
/// Source resolution mode. Leave unset to let the app root and the
52+
/// <c>PINGET_SOURCE_MODE</c> environment variable decide; any explicit value,
53+
/// including <see cref="Core.SourceMode.Auto"/>, wins over both.
54+
/// </summary>
55+
public SourceMode? SourceMode { get; init; }
5156
public string UserAgent { get; init; } = "pinget-dotnet/0.1";
5257
public Action<RepositoryWarning>? Diagnostics { get; init; }
5358

‎dotnet/src/Devolutions.Pinget.Core/Repository.cs‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Devolutions.Pinget.Core;
1212
public class Repository : IDisposable
1313
{
1414
private const string AppRootEnvironmentVariable = "PINGET_APPROOT";
15+
private const string SourceModeEnvironmentVariable = "PINGET_SOURCE_MODE";
1516

1617
internal const string InstalledStateUnsupportedWarning = "Installed package discovery is not supported on this platform; returning no installed packages.";
1718
internal const string InstallUnsupportedWarning = "Installing packages is not supported on this platform; no changes were made.";
@@ -65,9 +66,7 @@ public static Repository Open(RepositoryOptions? options = null)
6566
var requestedAppRoot = options.AppRoot ?? Environment.GetEnvironmentVariable(AppRootEnvironmentVariable);
6667
var appRoot = SourceStoreManager.NormalizeAppRoot(requestedAppRoot);
6768
SourceStoreManager.EnsureAppDirs(appRoot);
68-
var requestedSourceMode = options.SourceMode == SourceMode.Auto && requestedAppRoot is not null
69-
? SourceMode.Private
70-
: options.SourceMode;
69+
var requestedSourceMode = ResolveRequestedSourceMode(options.SourceMode, requestedAppRoot);
7170
var (sourceMode, store) = SourceStoreManager.LoadEffective(appRoot, requestedSourceMode);
7271
var client = new HttpClient(new SocketsHttpHandler
7372
{
@@ -79,6 +78,46 @@ public static Repository Open(RepositoryOptions? options = null)
7978
return new Repository(appRoot, client, store, sourceMode, options.Diagnostics, options.PreIndexedSourceAutoUpdateInterval);
8079
}
8180

81+
internal static SourceMode ResolveRequestedSourceMode(SourceMode? configuredSourceMode, string? requestedAppRoot) =>
82+
ResolveRequestedSourceMode(
83+
configuredSourceMode,
84+
requestedAppRoot,
85+
Environment.GetEnvironmentVariable(SourceModeEnvironmentVariable));
86+
87+
internal static SourceMode ResolveRequestedSourceMode(
88+
SourceMode? configuredSourceMode,
89+
string? requestedAppRoot,
90+
string? sourceModeFromEnvironment)
91+
{
92+
if (configuredSourceMode is { } explicitSourceMode)
93+
return explicitSourceMode;
94+
95+
if (TryParseSourceMode(sourceModeFromEnvironment, out var parsedSourceMode))
96+
return parsedSourceMode;
97+
98+
return requestedAppRoot is null ? SourceMode.Auto : SourceMode.Private;
99+
}
100+
101+
internal static bool TryParseSourceMode(string? value, out SourceMode sourceMode)
102+
{
103+
switch (value?.Trim().ToLowerInvariant())
104+
{
105+
case "auto":
106+
sourceMode = SourceMode.Auto;
107+
return true;
108+
case "private":
109+
sourceMode = SourceMode.Private;
110+
return true;
111+
case "system-winget-mirror":
112+
case "systemwingetmirror":
113+
sourceMode = SourceMode.SystemWingetMirror;
114+
return true;
115+
default:
116+
sourceMode = SourceMode.Auto;
117+
return false;
118+
}
119+
}
120+
82121
internal static IEnumerable<string> GetSqliteNativeLibraryCandidates(string assemblyDirectory)
83122
{
84123
yield return Path.Combine(assemblyDirectory, "e_sqlite3.dll");

‎dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@{
22
RootModule = 'Devolutions.Pinget.Client.psm1'
3-
ModuleVersion = '0.10.0'
3+
ModuleVersion = '0.11.0'
44
CompatiblePSEditions = @('Desktop', 'Core')
55
GUID = 'c6d1b5f2-5ccd-4771-9480-25caad7c58bd'
66
Author = 'Devolutions'

‎dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ namespace Devolutions.Pinget.PowerShell.Engine;
22

33
public static class PowerShellEngineVersion
44
{
5-
public const string Current = "0.10.0";
5+
public const string Current = "0.11.0";
66
}

‎nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="Pack">
22

33
<PropertyGroup>
4-
<Version>0.10.0</Version>
4+
<Version>0.11.0</Version>
55
<Company>Devolutions Inc.</Company>
66
<Authors>Devolutions</Authors>
77
<PackageId>Devolutions.Pinget.Cli.DotNet</PackageId>

‎nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="Pack">
22

33
<PropertyGroup>
4-
<Version>0.10.0</Version>
4+
<Version>0.11.0</Version>
55
<Company>Devolutions Inc.</Company>
66
<Authors>Devolutions</Authors>
77
<PackageId>Devolutions.Pinget.Cli.Rust</PackageId>

‎rust/crates/pinget-cli/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pinget-cli"
3-
version = "0.10.0"
3+
version = "0.11.0"
44
edition = "2024"
55

66
[lints]
@@ -13,7 +13,7 @@ path = "src/main.rs"
1313
[dependencies]
1414
anyhow = "1.0.102"
1515
clap = { version = "4.6.1", features = ["derive"] }
16-
pinget-core = { version = "0.10.0", path = "../pinget-core" }
16+
pinget-core = { version = "0.11.0", path = "../pinget-core" }
1717
chrono = "0.4.44"
1818
dirs = "6.0"
1919
jsonschema = { version = "0.30", default-features = false, features = ["resolve-file"] }

0 commit comments

Comments
 (0)