diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs index c63b44469954..1e41d719f87a 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs @@ -3,14 +3,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Formats.Tar; using System.IO; using System.IO.Compression; using System.Linq; using System.Runtime.InteropServices; using Microsoft.Deployment.DotNet.Releases; -using Microsoft.DotNet.NativeWrapper; namespace Microsoft.Dotnet.Installation.Internal; @@ -56,68 +54,143 @@ public void Prepare() } } public void Commit() - { - Commit(GetExistingSdkVersions(_request.InstallRoot)); - } - - public void Commit(IEnumerable existingSdkVersions) { using var activity = InstallationActivitySource.ActivitySource.StartActivity("DotnetInstaller.Commit"); - using (var progressReporter = _progressTarget.CreateProgressReporter()) { var installTask = progressReporter.AddTask($"Installing .NET SDK {_resolvedVersion}", maxValue: 100); // Extract archive directly to target directory with special handling for muxer - ExtractArchiveDirectlyToTarget(_archivePath!, _request.InstallRoot.Path!, existingSdkVersions, installTask); + ExtractArchiveDirectlyToTarget(_archivePath!, _request.InstallRoot.Path!, installTask); installTask.Value = installTask.MaxValue; } } - /** - * Extracts the archive directly to the target directory with special handling for muxer. - * Combines extraction and installation into a single operation. - */ - private string? ExtractArchiveDirectlyToTarget(string archivePath, string targetDir, IEnumerable existingSdkVersions, IProgressTask? installTask) + /// + /// Extracts the archive directly to the target directory with special handling for muxer. + /// Combines extraction and installation into a single operation. + /// + private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir, IProgressTask? installTask) { Directory.CreateDirectory(targetDir); - var muxerConfig = ConfigureMuxerHandling(existingSdkVersions); + string muxerName = DotnetupUtilities.GetDotnetExeName(); + string muxerTargetPath = Path.Combine(targetDir, muxerName); + string muxerTempPath = muxerTargetPath + ".tmp"; + + // Step 1: Read the version of the existing muxer (if any) by looking at the latest runtime + Version? existingMuxerVersion = null; + bool hadExistingMuxer = File.Exists(muxerTargetPath); + if (hadExistingMuxer) + { + existingMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir); + } + // Step 2: If there is an existing muxer, rename it to .tmp + if (hadExistingMuxer) + { + try + { + if (File.Exists(muxerTempPath)) + { + File.Delete(muxerTempPath); + } + File.Move(muxerTargetPath, muxerTempPath); + } + catch + { + // If we can't rename, just continue - extraction will overwrite + } + } + + // Step 3: Extract the archive (all files directly since muxer has been renamed) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return ExtractTarArchive(archivePath, targetDir, muxerConfig, installTask); + ExtractTarArchive(archivePath, targetDir, installTask); } else { - return ExtractZipArchive(archivePath, targetDir, muxerConfig, installTask); + ExtractZipArchive(archivePath, targetDir, installTask); + } + + // Step 4: If there was a previous muxer, compare versions and restore if needed + if (hadExistingMuxer && File.Exists(muxerTempPath)) + { + Version? newMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir); + + // If the new version is less than the old version, restore the old muxer + if (newMuxerVersion != null && existingMuxerVersion != null && newMuxerVersion < existingMuxerVersion) + { + try + { + if (File.Exists(muxerTargetPath)) + { + File.Delete(muxerTargetPath); + } + File.Move(muxerTempPath, muxerTargetPath); + } + catch + { + // If we can't restore, the new muxer will remain + } + } + else + { + // New version is >= old version, or we couldn't determine versions - keep new muxer + try + { + if (File.Exists(muxerTempPath)) + { + File.Delete(muxerTempPath); + } + } + catch + { + // Ignore cleanup errors + } + } } } - /** - * Configure muxer handling by determining if it needs to be updated. - */ - private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable existingSdkVersions) + /// + /// Gets the latest runtime version from the install root by checking the shared/Microsoft.NETCore.App directory. + /// + private static Version? GetLatestRuntimeVersionFromInstallRoot(string installRoot) { - // TODO: This is very wrong - its comparing a runtime version and sdk version, plus it needs to respect the muxer version - ReleaseVersion? existingMuxerVersion = existingSdkVersions.Any() ? existingSdkVersions.Max() : (ReleaseVersion?)null; - ReleaseVersion newRuntimeVersion = _resolvedVersion; - bool shouldUpdateMuxer = existingMuxerVersion is null || newRuntimeVersion.CompareTo(existingMuxerVersion) > 0; + try + { + var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App"); + if (!Directory.Exists(runtimePath)) + { + return null; + } - string muxerName = DotnetupUtilities.GetDotnetExeName(); - string muxerTargetPath = Path.Combine(_request.InstallRoot.Path!, muxerName); + Version? highestVersion = null; + foreach (var dir in Directory.GetDirectories(runtimePath)) + { + var versionString = Path.GetFileName(dir); + if (Version.TryParse(versionString, out Version? version)) + { + if (highestVersion == null || version > highestVersion) + { + highestVersion = version; + } + } + } - return new MuxerHandlingConfig( - muxerName, - muxerTargetPath, - shouldUpdateMuxer); + return highestVersion; + } + catch + { + return null; + } } - /** - * Extracts a tar or tar.gz archive to the target directory. - */ - private string? ExtractTarArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Extracts a tar or tar.gz archive to the target directory. + /// + private void ExtractTarArchive(string archivePath, string targetDir, IProgressTask? installTask) { string decompressedPath = DecompressTarGzIfNeeded(archivePath, out bool needsDecompression); @@ -133,9 +206,7 @@ private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable e } // Extract files directly to target - ExtractTarContents(decompressedPath, targetDir, muxerConfig, installTask); - - return null; + ExtractTarContents(decompressedPath, targetDir, installTask); } finally { @@ -147,9 +218,9 @@ private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable e } } - /** - * Decompresses a .tar.gz file if needed, returning the path to the tar file. - */ + /// + /// Decompresses a .tar.gz file if needed, returning the path to the tar file. + /// private string DecompressTarGzIfNeeded(string archivePath, out bool needsDecompression) { needsDecompression = archivePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase); @@ -168,9 +239,9 @@ private string DecompressTarGzIfNeeded(string archivePath, out bool needsDecompr return decompressedPath; } - /** - * Counts the number of entries in a tar file for progress reporting. - */ + /// + /// Counts the number of entries in a tar file for progress reporting. + /// private long CountTarEntries(string tarPath) { long totalFiles = 0; @@ -183,10 +254,10 @@ private long CountTarEntries(string tarPath) return totalFiles; } - /** - * Extracts the contents of a tar file to the target directory. - */ - private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Extracts the contents of a tar file to the target directory. + /// + private void ExtractTarContents(string tarPath, string targetDir, IProgressTask? installTask) { using var tarStream = File.OpenRead(tarPath); var tarReader = new TarReader(tarStream); @@ -196,7 +267,7 @@ private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingC { if (entry.EntryType == TarEntryType.RegularFile) { - ExtractTarFileEntry(entry, targetDir, muxerConfig, installTask); + ExtractTarFileEntry(entry, targetDir, installTask); } else if (entry.EntryType == TarEntryType.Directory) { @@ -213,88 +284,50 @@ private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingC } } - /** - * Extracts a single file entry from a tar archive. - */ - private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Extracts a single file entry from a tar archive. + /// + private void ExtractTarFileEntry(TarEntry entry, string targetDir, IProgressTask? installTask) { - var fileName = Path.GetFileName(entry.Name); var destPath = Path.Combine(targetDir, entry.Name); - - if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) - { - if (muxerConfig.ShouldUpdateMuxer) - { - HandleMuxerUpdateFromTar(entry, muxerConfig.MuxerTargetPath); - } - } - else - { - Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); - using var outStream = File.Create(destPath); - entry.DataStream?.CopyTo(outStream); - } - + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + using var outStream = File.Create(destPath); + entry.DataStream?.CopyTo(outStream); installTask?.Value += 1; } - /** - * Handles updating the muxer from a tar entry, using a temporary file to avoid locking issues. - */ - private void HandleMuxerUpdateFromTar(TarEntry entry, string muxerTargetPath) + /// + /// Extracts a zip archive to the target directory. + /// + private void ExtractZipArchive(string archivePath, string targetDir, IProgressTask? installTask) { - // Create a temporary file for the muxer first to avoid locking issues - var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name); - using (var outStream = File.Create(tempMuxerPath)) - { - entry.DataStream?.CopyTo(outStream); - } + long totalFiles = CountZipEntries(archivePath); - try + if (installTask is not null) { - // Replace the muxer using the utility that handles locking - DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + installTask.MaxValue = totalFiles > 0 ? totalFiles : 1; } - finally - { - if (File.Exists(tempMuxerPath)) - { - File.Delete(tempMuxerPath); - } - } - } - - /** - * Extracts a zip archive to the target directory. - */ - private string? ExtractZipArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) - { - long totalFiles = CountZipEntries(archivePath); - - installTask?.MaxValue = totalFiles > 0 ? totalFiles : 1; using var zip = ZipFile.OpenRead(archivePath); foreach (var entry in zip.Entries) { - ExtractZipEntry(entry, targetDir, muxerConfig, installTask); + ExtractZipEntry(entry, targetDir, installTask); } - - return null; } - /** - * Counts the number of entries in a zip file for progress reporting. - */ + /// + /// Counts the number of entries in a zip file for progress reporting. + /// private long CountZipEntries(string zipPath) { using var zip = ZipFile.OpenRead(zipPath); return zip.Entries.Count; } - /** - * Extracts a single entry from a zip archive. - */ - private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Extracts a single entry from a zip archive. + /// + private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, IProgressTask? installTask) { var fileName = Path.GetFileName(entry.FullName); var destPath = Path.Combine(targetDir, entry.FullName); @@ -307,62 +340,11 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl return; } - // Special handling for dotnet executable (muxer) - if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) - { - if (muxerConfig.ShouldUpdateMuxer) - { - HandleMuxerUpdateFromZip(entry, muxerConfig.MuxerTargetPath); - } - } - else - { - Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); - entry.ExtractToFile(destPath, overwrite: true); - } - + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + entry.ExtractToFile(destPath, overwrite: true); installTask?.Value += 1; } - /** - * Handles updating the muxer from a zip entry, using a temporary file to avoid locking issues. - */ - private void HandleMuxerUpdateFromZip(ZipArchiveEntry entry, string muxerTargetPath) - { - var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name); - entry.ExtractToFile(tempMuxerPath, overwrite: true); - - try - { - // Replace the muxer using the utility that handles locking - DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); - } - finally - { - if (File.Exists(tempMuxerPath)) - { - File.Delete(tempMuxerPath); - } - } - } - - /** - * Configuration class for muxer handling. - */ - private readonly struct MuxerHandlingConfig - { - public string MuxerName { get; } - public string MuxerTargetPath { get; } - public bool ShouldUpdateMuxer { get; } - - public MuxerHandlingConfig(string muxerName, string muxerTargetPath, bool shouldUpdateMuxer) - { - MuxerName = muxerName; - MuxerTargetPath = muxerTargetPath; - ShouldUpdateMuxer = shouldUpdateMuxer; - } - } - public void Dispose() { try @@ -377,10 +359,4 @@ public void Dispose() { } } - - private static IEnumerable GetExistingSdkVersions(DotnetInstallRoot installRoot) - { - var environmentInfo = HostFxrWrapper.getInfo(installRoot.Path!); - return environmentInfo.SdkInfo.Select(sdk => sdk.Version); - } } diff --git a/test/dotnetup.Tests/DnupE2Etest.cs b/test/dotnetup.Tests/DnupE2Etest.cs index 63d59d00fdb9..a8681955d1b8 100644 --- a/test/dotnetup.Tests/DnupE2Etest.cs +++ b/test/dotnetup.Tests/DnupE2Etest.cs @@ -8,11 +8,11 @@ using System.Threading.Tasks; using FluentAssertions; using Microsoft.Deployment.DotNet.Releases; +using Microsoft.Dotnet.Installation; +using Microsoft.Dotnet.Installation.Internal; using Microsoft.DotNet.Tools.Bootstrapper; using Microsoft.DotNet.Tools.Dotnetup.Tests.Utilities; -using Microsoft.Dotnet.Installation; using Xunit; -using Microsoft.Dotnet.Installation.Internal; namespace Microsoft.DotNet.Tools.Dotnetup.Tests; @@ -60,9 +60,9 @@ public void Test(string channel) Console.WriteLine($"Channel '{channel}' resolved to version: {expectedVersion}"); // Execute the command with explicit manifest path as a separate process - var args = DotnetupTestUtilities.BuildArguments(channel, testEnv.InstallPath, testEnv.ManifestPath); - (int exitCode, string output) = DotnetupTestUtilities.RunDotnetupProcess(args, captureOutput: true, workingDirectory: testEnv.TempRoot); - exitCode.Should().Be(0, $"dotnetup exited with code {exitCode}. Output:\n{output}"); + var args = DotnetupTestUtilities.BuildArguments(channel, testEnv.InstallPath, testEnv.ManifestPath); + (int exitCode, string output) = DotnetupTestUtilities.RunDotnetupProcess(args, captureOutput: true, workingDirectory: testEnv.TempRoot); + exitCode.Should().Be(0, $"dotnetup exited with code {exitCode}. Output:\n{output}"); Directory.Exists(testEnv.InstallPath).Should().BeTrue(); Directory.Exists(Path.GetDirectoryName(testEnv.ManifestPath)).Should().BeTrue(); diff --git a/test/dotnetup.Tests/LibraryTests.cs b/test/dotnetup.Tests/LibraryTests.cs index 55f621c172b6..b5382ad354bc 100644 --- a/test/dotnetup.Tests/LibraryTests.cs +++ b/test/dotnetup.Tests/LibraryTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using Microsoft.Dotnet.Installation; using Microsoft.Dotnet.Installation.Internal; @@ -60,4 +61,117 @@ public void TestGetSupportedChannels() channels.Should().NotContain("7.0.1xx"); } + + [Fact] + public void MuxerIsUpdated_WhenInstallingNewerSdk() + { + var releaseInfoProvider = InstallerFactory.CreateReleaseInfoProvider(); + var installer = InstallerFactory.CreateInstaller(new NullProgressTarget()); + + using var testEnv = DotnetupTestUtilities.CreateTestEnvironment(); + + Log.WriteLine($"Installing to path: {testEnv.InstallPath}"); + + // Install .NET SDK 9.0 first + var sdk9Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "9.0"); + Log.WriteLine($"Installing .NET SDK 9.0: {sdk9Version}"); + installer.Install( + new DotnetInstallRoot(testEnv.InstallPath, InstallerUtilities.GetDefaultInstallArchitecture()), + InstallComponent.SDK, + sdk9Version!); + + var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName()); + File.Exists(muxerPath).Should().BeTrue("muxer should exist after SDK 9.0 installation"); + + var versionAfterSdk9 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath); + Log.WriteLine($"Runtime version after SDK 9.0 install: {versionAfterSdk9}"); + versionAfterSdk9.Should().NotBeNull("runtime should exist after SDK 9.0 installation"); + + // Install .NET SDK 10.0 second + var sdk10Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "10.0"); + Log.WriteLine($"Installing .NET SDK 10.0: {sdk10Version}"); + installer.Install( + new DotnetInstallRoot(testEnv.InstallPath, InstallerUtilities.GetDefaultInstallArchitecture()), + InstallComponent.SDK, + sdk10Version!); + + var versionAfterSdk10 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath); + Log.WriteLine($"Runtime version after SDK 10.0 install: {versionAfterSdk10}"); + versionAfterSdk10.Should().NotBeNull("runtime should exist after SDK 10.0 installation"); + + // Verify muxer was updated to newer version + versionAfterSdk10.Should().BeGreaterThan(versionAfterSdk9!, "muxer should be updated when installing newer SDK"); + } + + [Fact] + public void MuxerIsNotDowngraded_WhenInstallingOlderSdk() + { + var releaseInfoProvider = InstallerFactory.CreateReleaseInfoProvider(); + var installer = InstallerFactory.CreateInstaller(new NullProgressTarget()); + + using var testEnv = DotnetupTestUtilities.CreateTestEnvironment(); + + Log.WriteLine($"Installing to path: {testEnv.InstallPath}"); + + // Install .NET SDK 10.0 first + var sdk10Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "10.0"); + Log.WriteLine($"Installing .NET SDK 10.0: {sdk10Version}"); + installer.Install( + new DotnetInstallRoot(testEnv.InstallPath, InstallerUtilities.GetDefaultInstallArchitecture()), + InstallComponent.SDK, + sdk10Version!); + + var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName()); + File.Exists(muxerPath).Should().BeTrue("muxer should exist after SDK 10.0 installation"); + + var versionAfterSdk10 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath); + Log.WriteLine($"Runtime version after SDK 10.0 install: {versionAfterSdk10}"); + versionAfterSdk10.Should().NotBeNull("runtime should exist after SDK 10.0 installation"); + + // Install .NET SDK 9.0 second + var sdk9Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "9.0"); + Log.WriteLine($"Installing .NET SDK 9.0: {sdk9Version}"); + installer.Install( + new DotnetInstallRoot(testEnv.InstallPath, InstallerUtilities.GetDefaultInstallArchitecture()), + InstallComponent.SDK, + sdk9Version!); + + var versionAfterSdk9 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath); + Log.WriteLine($"Runtime version after SDK 9.0 install: {versionAfterSdk9}"); + versionAfterSdk9.Should().NotBeNull("runtime should exist after SDK 9.0 installation"); + + // Verify muxer was NOT downgraded + versionAfterSdk9.Should().Be(versionAfterSdk10, "muxer should not be downgraded when installing older SDK"); + } + + private static Version? GetRuntimeVersionFromInstallRoot(string installRoot) + { + try + { + var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App"); + if (!Directory.Exists(runtimePath)) + { + return null; + } + + Version? highestVersion = null; + foreach (var dir in Directory.GetDirectories(runtimePath)) + { + var versionString = Path.GetFileName(dir); + if (Version.TryParse(versionString, out Version? version)) + { + if (highestVersion == null || version > highestVersion) + { + highestVersion = version; + } + } + } + + return highestVersion; + } + catch + { + return null; + } + } } diff --git a/test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs b/test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs index 85826c52a572..3c35ac357b97 100644 --- a/test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs +++ b/test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs @@ -3,8 +3,8 @@ using System; using Microsoft.Deployment.DotNet.Releases; -using Microsoft.DotNet.Tools.Bootstrapper; using Microsoft.Dotnet.Installation.Internal; +using Microsoft.DotNet.Tools.Bootstrapper; namespace Microsoft.DotNet.Tools.Dotnetup.Tests.Utilities;