Skip to content
Draft
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
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
164 changes: 164 additions & 0 deletions src/dnvm/DnvmConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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>
/// Provides access to the dnvm configuration file.
/// </summary>
public sealed class DnvmConfigFile
{
private const string ConfigFileName = "dnvmConfig.json";

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

private readonly string _configDirectory;

public DnvmConfigFile() : this(GetConfigDirectory())
{
}

internal DnvmConfigFile(string configDirectory)
{
_configDirectory = configDirectory;
}

/// <summary>
/// Get the platform-specific config directory path.
/// - Linux: ~/.config/dnvm/ (XDG_CONFIG_HOME/dnvm)
/// - macOS: ~/.config/dnvm/
/// - Windows: %APPDATA%/dnvm/
/// </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 ApplicationData for roaming config files
// This is ~/.config on macOS and %APPDATA% on Windows
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"dnvm");
}
}

private IFileSystem GetConfigFileSystem()
{
Directory.CreateDirectory(_configDirectory);
return new SubFileSystem(
DnvmEnv.PhysicalFs,
DnvmEnv.PhysicalFs.ConvertPathFromInternal(_configDirectory));
}

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 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 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
8 changes: 6 additions & 2 deletions src/dnvm/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ 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 configFile = new DnvmConfigFile();
var config = configFile.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 +245,10 @@ public async Task<Result> UpdateSelf(Manifest manifest)
return Result.NotASingleFile;
}

var configFile = new DnvmConfigFile();
var config = configFile.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
17 changes: 6 additions & 11 deletions test/IntegrationTests/SelfInstallTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,23 +305,18 @@ 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 };
var configFile = new DnvmConfigFile();
configFile.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
6 changes: 4 additions & 2 deletions test/IntegrationTests/UpdateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ 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 };
var configFile = new DnvmConfigFile();
configFile.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();
}
}
Loading