From 0354e932a14014b132574455db1a1599df545916 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 17 Jan 2026 09:59:05 -0800 Subject: [PATCH] Add fix for #311 The problem is actually the consequences from #274. The basic problem is that, before that PR, uninstall didn't properly remove all pieces of the SDK from the manifest. This causes issues with prune because it thinks an SDK is still installed, even though all the pieces on disk have already been removed. The fix is to change the error for this situation into a warning and remove the stale entries from the manifest. Fixes #311 --- src/dnvm/PruneCommand.cs | 31 ++++++++++++++++++ test/UnitTests/PruneTests.cs | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/dnvm/PruneCommand.cs b/src/dnvm/PruneCommand.cs index 08da401..8d0d83d 100644 --- a/src/dnvm/PruneCommand.cs +++ b/src/dnvm/PruneCommand.cs @@ -48,12 +48,25 @@ public static async Task Run(DnvmEnv env, Logger logger, Options options) } else { + // Check if the SDK is actually in InstalledSdks. It may not be if the manifest + // was corrupted by a bug fixed in https://github.com/dn-vm/dnvm/pull/274. + // In that case, we just need to clean up the stale entry from RegisteredChannels. + if (!manifest.IsSdkInstalled(sdk.Version, sdk.Dir)) + { + env.Console.Warn($"SDK {sdk.Version} was not found in installed SDKs, cleaning up stale manifest entry."); + manifest = RemoveSdkFromChannels(manifest, sdk.Version); + await @lock.WriteManifest(env, manifest); + continue; + } + Console.WriteLine($"Removing {sdk}"); int result = await UninstallCommand.Run(@lock, env, logger, sdk.Version, sdk.Dir); if (result != 0) { return result; } + // Re-read manifest after uninstall since it was modified + manifest = await @lock.ReadManifest(env); } } return 0; @@ -99,4 +112,22 @@ public static async Task Run(DnvmEnv env, Logger logger, Options options) return sdksToRemove; } + + /// + /// Removes an SDK version from all RegisteredChannels.InstalledSdkVersions. + /// This is used to clean up stale entries left by a bug fixed in + /// https://github.com/dn-vm/dnvm/pull/274. + /// + private static Manifest RemoveSdkFromChannels(Manifest manifest, SemVersion sdkVersion) + { + var updatedChannels = manifest.RegisteredChannels.Select(channel => + { + var updatedInstalledVersions = channel.InstalledSdkVersions + .Where(version => version != sdkVersion) + .ToEq(); + return channel with { InstalledSdkVersions = updatedInstalledVersions }; + }).ToEq(); + + return manifest with { RegisteredChannels = updatedChannels }; + } } \ No newline at end of file diff --git a/test/UnitTests/PruneTests.cs b/test/UnitTests/PruneTests.cs index 79df449..c5f131f 100644 --- a/test/UnitTests/PruneTests.cs +++ b/test/UnitTests/PruneTests.cs @@ -177,4 +177,65 @@ public Task MissingDirectoriesHandled() => RunWithServer(async (server, env) => Assert.Single(finalManifest.InstalledSdks); Assert.Equal(upgradeVersion, finalManifest.InstalledSdks[0].SdkVersion); }); + + /// + /// Tests the scenario from https://github.com/dn-vm/dnvm/issues/311: + /// Before PR #274, uninstalling an SDK removed it from InstalledSdks but not from + /// RegisteredChannels.InstalledSdkVersions. Prune should handle this gracefully by + /// logging a warning and cleaning up the stale manifest entry. + /// + [Fact] + public Task PruneHandlesCorruptedManifestFromPre274Uninstall() => RunWithServer(async (server, env) => + { + var oldVersion = SemVersion.Parse("8.0.100", SemVersionStyles.Strict); + var newVersion = SemVersion.Parse("8.0.101", SemVersionStyles.Strict); + Channel channel = new Channel.Latest(); + + // Create a corrupted manifest that simulates the state left by a pre-PR#274 uninstall: + // - The old SDK version is in RegisteredChannels.InstalledSdkVersions (was not removed) + // - But the old SDK is NOT in InstalledSdks (was removed) + // - The new SDK is properly tracked in both places + var corruptedManifest = Manifest.Empty with + { + InstalledSdks = [ + // Only the new version is in InstalledSdks (old version was uninstalled) + new InstalledSdk + { + SdkVersion = newVersion, + RuntimeVersion = newVersion, + AspNetVersion = newVersion, + ReleaseVersion = newVersion, + SdkDirName = DnvmEnv.DefaultSdkDirName + } + ], + RegisteredChannels = [ + new RegisteredChannel + { + ChannelName = channel, + SdkDirName = DnvmEnv.DefaultSdkDirName, + // Bug: old version is still in InstalledSdkVersions even though it was uninstalled + InstalledSdkVersions = [oldVersion, newVersion] + } + ] + }; + + await Manifest.WriteManifestUnsafe(env, corruptedManifest); + + var console = (TestConsole)env.Console; + var initialOutput = console.Output; + + // Prune should succeed and clean up the stale manifest entry + var pruneResult = await PruneCommand.Run(env, _logger, new PruneCommand.Options()); + + var output = console.Output[initialOutput.Length..]; + + Assert.Equal(0, pruneResult); + Assert.Contains("was not found in installed SDKs", output); + + // Verify the manifest was cleaned up - old version should be removed from channel + var finalManifest = await Manifest.ReadManifestUnsafe(env); + var registeredChannel = Assert.Single(finalManifest.RegisteredChannels); + Assert.Single(registeredChannel.InstalledSdkVersions); + Assert.Equal(newVersion, registeredChannel.InstalledSdkVersions[0]); + }); } \ No newline at end of file