Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
5 changes: 1 addition & 4 deletions src/dnvm/CommandLineArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ namespace Dnvm;
[Command("dnvm", Summary = "Install and manage .NET SDKs.")]
public partial record DnvmArgs
{
[CommandOption("--enable-dnvm-previews", Description = "Enable dnvm previews.")]
public bool? EnableDnvmPreviews { get; init; }

[CommandGroup("command")]
public DnvmSubCommand? SubCommand { get; init; }
}
Expand Down Expand Up @@ -246,7 +243,7 @@ public static class CommandLineArguments
{
case CmdLine.ParsedArgsOrHelpInfos<DnvmArgs>.Parsed(var value):
dnvmCmd = value;
if (dnvmCmd.EnableDnvmPreviews is null && dnvmCmd.SubCommand is null)
if (dnvmCmd.SubCommand is null)
{
// Empty command is a help request.
console.WriteLine(CmdLine.GetHelpText<DnvmArgs>(includeHelp: true));
Expand Down
154 changes: 154 additions & 0 deletions src/dnvm/DnvmConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Serde;
using Serde.Json;
using Zio;
using Zio.FileSystems;

namespace Dnvm;

/// <summary>
/// Represents the dnvm configuration file.
/// </summary>
[GenerateSerde]
public sealed partial record DnvmConfig
{
public static readonly DnvmConfig Default = new();

/// <summary>
/// Whether to enable dnvm preview releases in the update command.
/// </summary>
public bool PreviewsEnabled { get; init; } = false;
}

/// <summary>
/// Static methods for config file operations.
/// </summary>
public static class DnvmConfigFile
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Let's make this a class with instance methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use instance methods in commit 8c42896. DnvmConfigFile is now a class that can be instantiated and used with instance methods.

{
private const string ConfigFileName = "dnvmConfig.json";

// Allow tests to override the config directory
public static string? TestConfigDirectory { get; set; }

/// <summary>
/// Get the platform-specific config directory path.
/// - Linux: ~/.config/dnvm/ (XDG_CONFIG_HOME/dnvm)
/// - macOS: ~/Library/Application Support/dnvm/
/// - Windows: %LOCALAPPDATA%/dnvm/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Configuration data should not be local by default -- I think this should be AppData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use ApplicationData in commit 8f70456. Config files now use roaming app data: %APPDATA% on Windows and ~/.config on macOS/Linux.

/// </summary>
private static string GetConfigDirectory()
{
if (TestConfigDirectory is not null)
{
return TestConfigDirectory;
}

// Allow tests to override config directory via environment variable
var testOverride = Environment.GetEnvironmentVariable("DNVM_TEST_CONFIG_DIR");
if (!string.IsNullOrWhiteSpace(testOverride))
{
return testOverride;
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Use XDG_CONFIG_HOME on Linux, defaulting to ~/.config
var xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
var configBase = string.IsNullOrWhiteSpace(xdgConfigHome)
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config")
: xdgConfigHome;
return Path.Combine(configBase, "dnvm");
}
else
{
// On macOS and Windows, use LocalApplicationData
// This is ~/Library/Application Support on macOS and %LOCALAPPDATA% on Windows
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"dnvm");
}
}

private static IFileSystem GetConfigFileSystem()
{
var configDir = GetConfigDirectory();
Directory.CreateDirectory(configDir);
return new SubFileSystem(
DnvmEnv.PhysicalFs,
DnvmEnv.PhysicalFs.ConvertPathFromInternal(configDir));
}

private static UPath ConfigPath => UPath.Root / ConfigFileName;

/// <summary>
/// Reads the config file from the platform-specific config directory.
/// Returns the default config if the file does not exist.
/// </summary>
public static DnvmConfig Read()
{
try
{
var fs = GetConfigFileSystem();
if (!fs.FileExists(ConfigPath))
{
return DnvmConfig.Default;
}

var text = fs.ReadAllText(ConfigPath);
return JsonSerializer.Deserialize<DnvmConfig>(text);
}
catch (Exception)
{
// If there's any error reading or parsing the config, return default
return DnvmConfig.Default;
}
}

/// <summary>
/// Writes the config file to the platform-specific config directory.
/// </summary>
public static void Write(DnvmConfig config)
{
var fs = GetConfigFileSystem();
var tempFileName = $"{ConfigFileName}.{Path.GetRandomFileName()}.tmp";
var tempPath = UPath.Root / tempFileName;

var text = JsonSerializer.Serialize(config);

// Write to temporary file first
fs.WriteAllText(tempPath, text, Encoding.UTF8);

// Create backup of existing config if it exists
if (fs.FileExists(ConfigPath))
{
var backupPath = UPath.Root / $"{ConfigFileName}.backup";
try
{
// Should not throw if the file doesn't exist
fs.DeleteFile(backupPath);
fs.MoveFile(ConfigPath, backupPath);
}
catch (IOException)
{
// Best effort cleanup - ignore if we can't delete the backup file
}
}

// Atomic rename operation
fs.MoveFile(tempPath, ConfigPath);

// Clean up temporary file
try
{
fs.DeleteFile(tempPath);
}
catch (IOException)
{
// Best effort cleanup - ignore if we can't delete the temp file
}
}
}
1 change: 0 additions & 1 deletion src/dnvm/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public sealed partial record Manifest
{
public static readonly Manifest Empty = new();

public bool PreviewsEnabled { get; init; } = false;
public SdkDirName CurrentSdkDir { get; init; } = DnvmEnv.DefaultSdkDirName;
public EqArray<InstalledSdk> InstalledSdks { get; init; } = [];
public EqArray<RegisteredChannel> RegisteredChannels { get; init; } = [];
Expand Down
3 changes: 1 addition & 2 deletions src/dnvm/ManifestSchema/ManifestSerialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ public static Manifest Convert(this ManifestV9 manifestV9)
{
return new Manifest
{
PreviewsEnabled = manifestV9.PreviewsEnabled,
CurrentSdkDir = manifestV9.CurrentSdkDir.Convert(),
InstalledSdks = manifestV9.InstalledSdks.SelectAsArray(sdk => new InstalledSdk
{
Expand All @@ -94,7 +93,7 @@ internal static ManifestV9 ConvertToLatest(this Manifest @this)
{
return new ManifestV9
{
PreviewsEnabled = @this.PreviewsEnabled,
PreviewsEnabled = false, // Always write false since this is now in config file
CurrentSdkDir = @this.CurrentSdkDir.ConvertToLatest(),
InstalledSdks = @this.InstalledSdks.SelectAsArray(sdk => new InstalledSdkV9
{
Expand Down
20 changes: 2 additions & 18 deletions src/dnvm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,13 @@ public static async Task<int> Main(string[] args)
using var env = DnvmEnv.CreateDefault();
if (parsedArgs.SubCommand is null)
{
if (parsedArgs.EnableDnvmPreviews == true)
{
return await EnableDnvmPreviews(env);
}
else
{
// Help was requested, exit with success.
return 0;
}
// Help was requested, exit with success.
return 0;
}

return await Dnvm(env, logger, parsedArgs);
}

public static async Task<int> EnableDnvmPreviews(DnvmEnv env)
{
using var @lock = await ManifestLock.Acquire(env);
var manifest = await @lock.ReadOrCreateManifest(env);
manifest = manifest with { PreviewsEnabled = true };
await @lock.WriteManifest(env, manifest);
return 0;
}

internal static async Task<int> Dnvm(DnvmEnv env, Logger logger, DnvmArgs args)
{
return args.SubCommand switch
Expand Down
6 changes: 4 additions & 2 deletions src/dnvm/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ public static async Task<Result> UpdateSdks(
{
env.Console.WriteLine("Looking for available updates");
// Check for dnvm updates
if (await CheckForSelfUpdates(env.HttpClient, env.Console, logger, releasesUrl, manifest.PreviewsEnabled) is (true, _))
var config = DnvmConfigFile.Read();
if (await CheckForSelfUpdates(env.HttpClient, env.Console, logger, releasesUrl, config.PreviewsEnabled) is (true, _))
{
env.Console.WriteLine("dnvm is out of date. Run 'dnvm update --self' to update dnvm.");
}
Expand Down Expand Up @@ -243,8 +244,9 @@ public async Task<Result> UpdateSelf(Manifest manifest)
return Result.NotASingleFile;
}

var config = DnvmConfigFile.Read();
DnvmReleases.Release release;
switch (await CheckForSelfUpdates(_env.HttpClient, _env.Console, _logger, _releasesUrl, manifest.PreviewsEnabled))
switch (await CheckForSelfUpdates(_env.HttpClient, _env.Console, _logger, _releasesUrl, config.PreviewsEnabled))
{
case (false, null):
return Result.SelfUpdateFailed;
Expand Down
16 changes: 11 additions & 5 deletions test/IntegrationTests/Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ internal static class DnvmRunner
}
try
{
var envVars = new Dictionary<string, string>
{
["HOME"] = env.UserHome,
["DNVM_HOME"] = env.RealPath(UPath.Root)
};
// Use the test config directory if it's set
if (DnvmConfigFile.TestConfigDirectory is not null)
{
envVars["DNVM_TEST_CONFIG_DIR"] = DnvmConfigFile.TestConfigDirectory;
}
var procResult = await ProcUtil.RunWithOutput(
dnvmPath,
dnvmArgs,
new()
{
["HOME"] = env.UserHome,
["DNVM_HOME"] = env.RealPath(UPath.Root)
}
envVars
);
// Allow the test to check the environment variables before they are restored
envChecker?.Invoke();
Expand Down
16 changes: 5 additions & 11 deletions test/IntegrationTests/SelfInstallTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,23 +305,17 @@ public Task UpdateSelfPreview() => RunWithServer(async (mockServer, env) =>
Assert.True(timeAfterUpdate == timeBeforeUpdate);
Assert.Contains("Dnvm is up-to-date", result.Out);

result = await ProcUtil.RunWithOutput(
copiedExe,
$"--enable-dnvm-previews",
new() {
["HOME"] = env.UserHome,
["DNVM_HOME"] = env.RealPath(UPath.Root)
}
);
Assert.Equal(0, result.ExitCode);
// Manually create config file with previews enabled
var config = new DnvmConfig { PreviewsEnabled = true };
DnvmConfigFile.Write(config);

result = await DnvmRunner.RunAndRestoreEnv(
env,
copiedExe,
$"update --self --dnvm-url {mockServer.DnvmReleasesUrl} -v"
);
timeAfterUpdate = File.GetLastWriteTimeUtc(copiedExe);
Assert.True(timeAfterUpdate > timeBeforeUpdate);
var timeAfterPreviewUpdate = File.GetLastWriteTimeUtc(copiedExe);
Assert.True(timeAfterPreviewUpdate > timeBeforeUpdate);
Assert.Contains("Process successfully upgraded", result.Out);
});

Expand Down
5 changes: 3 additions & 2 deletions test/IntegrationTests/UpdateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ public async Task EnablePreviewsAndDownload() => await TestWithServer(async (moc
Assert.Contains("dnvm is up-to-date", output, StringComparison.OrdinalIgnoreCase);
Assert.Equal(0, proc.ExitCode);

proc = await DnvmRunner.RunAndRestoreEnv(env, SelfInstallTests.DnvmExe, "--enable-dnvm-previews");
Assert.Equal(0, proc.ExitCode);
// Manually create config file with previews enabled
var config = new DnvmConfig { PreviewsEnabled = true };
DnvmConfigFile.Write(config);

proc = await DnvmRunner.RunAndRestoreEnv(env, SelfInstallTests.DnvmExe,
$"update --self -v --dnvm-url {mockServer.DnvmReleasesUrl}");
Expand Down
9 changes: 9 additions & 0 deletions test/Shared/TestOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

using System.IO;
using Spectre.Console.Testing;
using Zio;
using Zio.FileSystems;
Expand All @@ -10,9 +11,11 @@ public sealed class TestEnv : IDisposable
private readonly TempDirectory _userHome = TestUtils.CreateTempDirectory();
private readonly TempDirectory _dnvmHome = TestUtils.CreateTempDirectory();
private readonly TempDirectory _workingDir = TestUtils.CreateTempDirectory();
private readonly TempDirectory _configDir = TestUtils.CreateTempDirectory();
private readonly Dictionary<string, string> _envVars = new();

public DnvmEnv DnvmEnv { get; init; }
public string ConfigDirPath => _configDir.Path;

public TestEnv(
string dotnetFeedUrl,
Expand All @@ -26,6 +29,9 @@ public TestEnv(
var cwdFs = new SubFileSystem(physicalFs, physicalFs.ConvertPathFromInternal(_workingDir.Path));
cwdFs.CreateDirectory(cwd);

// Set the test config directory
DnvmConfigFile.TestConfigDirectory = _configDir.Path;

DnvmEnv = new DnvmEnv(
userHome: _userHome.Path,
dnvmFs,
Expand All @@ -43,6 +49,9 @@ public void Dispose()
{
_userHome.Dispose();
_dnvmHome.Dispose();
_workingDir.Dispose();
_configDir.Dispose();
DnvmConfigFile.TestConfigDirectory = null;
DnvmEnv.Dispose();
}
}
12 changes: 1 addition & 11 deletions test/UnitTests/CommandLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ namespace Dnvm.Test;
public sealed class CommandLineTests
{
private static readonly string ExpectedHelpText = """
usage: dnvm [--enable-dnvm-previews] [-h | --help] <command>
usage: dnvm [-h | --help] <command>

Install and manage .NET SDKs.

Options:
--enable-dnvm-previews Enable dnvm previews.
-h, --help Show help information.

Commands:
Expand Down Expand Up @@ -177,15 +176,6 @@ public void TrackMixedCase()
});
}

[Fact]
public void EnableDnvmPreviews()
{
var options = CommandLineArguments.ParseRaw(new TestConsole(), [
"--enable-dnvm-previews",
]);
Assert.True(options!.EnableDnvmPreviews);
}

[Theory]
[InlineData("-h")]
[InlineData("--help")]
Expand Down
3 changes: 1 addition & 2 deletions test/UnitTests/ManifestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ public void WriteManifestV9()
InstalledSdkVersions = [ SemVersion.Parse("8.0.100-preview.3.23178.7", SemVersionStyles.Strict) ]
}
],
CurrentSdkDir = new SdkDirName("dn"),
PreviewsEnabled = false
CurrentSdkDir = new SdkDirName("dn")
};
var expected = """
{
Expand Down