From 452ee7b4a85c11ab746e37968f7d8479a99d849b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:09:50 +0000 Subject: [PATCH 1/5] Initial plan From 32e7fc270ba8978cdae966581d5f00ae54bc72a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:24:59 +0000 Subject: [PATCH 2/5] Fix muxer version handling to compare file versions instead of SDK versions Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com> --- .../Internal/DotnetArchiveExtractor.cs | 229 ++++++++++-------- .../MuxerVersionHandlingTests.cs | 201 +++++++++++++++ 2 files changed, 334 insertions(+), 96 deletions(-) create mode 100644 test/dotnetup.Tests/MuxerVersionHandlingTests.cs diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs index c63b44469954..6f04fc6d279f 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs @@ -2,15 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. 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 +53,115 @@ 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); + var muxerConfig = new MuxerHandlingConfig(muxerName, muxerTargetPath); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return ExtractTarArchive(archivePath, targetDir, muxerConfig, installTask); + ExtractTarArchive(archivePath, targetDir, muxerConfig, installTask); } else { - return ExtractZipArchive(archivePath, targetDir, muxerConfig, installTask); + ExtractZipArchive(archivePath, targetDir, muxerConfig, installTask); } } - /** - * Configure muxer handling by determining if it needs to be updated. - */ - private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable existingSdkVersions) + /// + /// Determines if the new muxer should replace the existing one by comparing file versions. + /// + /// Path to the new muxer file (extracted to temp location) + /// Path to the existing muxer file + /// True if the new muxer should replace the existing one + internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxerPath) { - // 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; + // If there's no existing muxer, we should install the new one + if (!File.Exists(existingMuxerPath)) + { + return true; + } - string muxerName = DotnetupUtilities.GetDotnetExeName(); - string muxerTargetPath = Path.Combine(_request.InstallRoot.Path!, muxerName); + // Compare file versions + Version? existingVersion = GetMuxerFileVersion(existingMuxerPath); + Version? newVersion = GetMuxerFileVersion(newMuxerPath); + + // If we can't determine the new version, don't update (safety measure) + if (newVersion is null) + { + return false; + } + + // If we can't determine the existing version, update to the new one + if (existingVersion is null) + { + return true; + } - return new MuxerHandlingConfig( - muxerName, - muxerTargetPath, - shouldUpdateMuxer); + // Only update if the new version is greater than the existing version + return newVersion > existingVersion; } - /** - * Extracts a tar or tar.gz archive to the target directory. - */ - private string? ExtractTarArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Gets the file version of a muxer executable. + /// + internal static Version? GetMuxerFileVersion(string muxerPath) + { + if (!File.Exists(muxerPath)) + { + return null; + } + + try + { + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(muxerPath); + if (versionInfo.FileVersion is not null && Version.TryParse(versionInfo.FileVersion, out Version? version)) + { + return version; + } + + // Fallback to constructing version from individual parts + if (versionInfo.FileMajorPart > 0 || versionInfo.FileMinorPart > 0) + { + return new Version( + versionInfo.FileMajorPart, + versionInfo.FileMinorPart, + versionInfo.FileBuildPart, + versionInfo.FilePrivatePart); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Extracts a tar or tar.gz archive to the target directory. + /// + private void ExtractTarArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { string decompressedPath = DecompressTarGzIfNeeded(archivePath, out bool needsDecompression); @@ -134,8 +178,6 @@ private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable e // Extract files directly to target ExtractTarContents(decompressedPath, targetDir, muxerConfig, installTask); - - return null; } finally { @@ -147,9 +189,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 +210,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,9 +225,9 @@ private long CountTarEntries(string tarPath) return totalFiles; } - /** - * Extracts the contents of a tar file to the target directory. - */ + /// + /// Extracts the contents of a tar file to the target directory. + /// private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { using var tarStream = File.OpenRead(tarPath); @@ -213,9 +255,9 @@ private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingC } } - /** - * Extracts a single file entry from a tar archive. - */ + /// + /// Extracts a single file entry from a tar archive. + /// private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { var fileName = Path.GetFileName(entry.Name); @@ -223,10 +265,7 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) { - if (muxerConfig.ShouldUpdateMuxer) - { - HandleMuxerUpdateFromTar(entry, muxerConfig.MuxerTargetPath); - } + HandleMuxerFromTar(entry, muxerConfig.MuxerTargetPath); } else { @@ -238,12 +277,12 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling 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) + /// + /// Handles the muxer from a tar entry, comparing file versions to determine if update is needed. + /// + private void HandleMuxerFromTar(TarEntry entry, string muxerTargetPath) { - // Create a temporary file for the muxer first to avoid locking issues + // Create a temporary file for the muxer first var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name); using (var outStream = File.Create(tempMuxerPath)) { @@ -252,8 +291,12 @@ private void HandleMuxerUpdateFromTar(TarEntry entry, string muxerTargetPath) try { - // Replace the muxer using the utility that handles locking - DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + // Check if we should update the muxer based on file version comparison + if (ShouldUpdateMuxer(tempMuxerPath, muxerTargetPath)) + { + // Replace the muxer using the utility that handles locking + DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + } } finally { @@ -264,36 +307,37 @@ private void HandleMuxerUpdateFromTar(TarEntry entry, string muxerTargetPath) } } - /** - * Extracts a zip archive to the target directory. - */ - private string? ExtractZipArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) + /// + /// Extracts a zip archive to the target directory. + /// + private void ExtractZipArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { long totalFiles = CountZipEntries(archivePath); - installTask?.MaxValue = totalFiles > 0 ? totalFiles : 1; + if (installTask is not null) + { + installTask.MaxValue = totalFiles > 0 ? totalFiles : 1; + } using var zip = ZipFile.OpenRead(archivePath); foreach (var entry in zip.Entries) { ExtractZipEntry(entry, targetDir, muxerConfig, 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. - */ + /// + /// Extracts a single entry from a zip archive. + /// private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { var fileName = Path.GetFileName(entry.FullName); @@ -310,10 +354,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl // Special handling for dotnet executable (muxer) if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) { - if (muxerConfig.ShouldUpdateMuxer) - { - HandleMuxerUpdateFromZip(entry, muxerConfig.MuxerTargetPath); - } + HandleMuxerFromZip(entry, muxerConfig.MuxerTargetPath); } else { @@ -324,18 +365,22 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl 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) + /// + /// Handles the muxer from a zip entry, comparing file versions to determine if update is needed. + /// + private void HandleMuxerFromZip(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); + // Check if we should update the muxer based on file version comparison + if (ShouldUpdateMuxer(tempMuxerPath, muxerTargetPath)) + { + // Replace the muxer using the utility that handles locking + DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + } } finally { @@ -346,20 +391,18 @@ private void HandleMuxerUpdateFromZip(ZipArchiveEntry entry, string muxerTargetP } } - /** - * Configuration class for muxer handling. - */ + /// + /// 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) + public MuxerHandlingConfig(string muxerName, string muxerTargetPath) { MuxerName = muxerName; MuxerTargetPath = muxerTargetPath; - ShouldUpdateMuxer = shouldUpdateMuxer; } } @@ -377,10 +420,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/MuxerVersionHandlingTests.cs b/test/dotnetup.Tests/MuxerVersionHandlingTests.cs new file mode 100644 index 000000000000..ccc4e223d9da --- /dev/null +++ b/test/dotnetup.Tests/MuxerVersionHandlingTests.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Dotnet.Installation.Internal; +using FluentAssertions; +using Xunit; + +namespace Microsoft.DotNet.Tools.Dotnetup.Tests; + +/// +/// Tests for muxer version handling during SDK installation. +/// +public class MuxerVersionHandlingTests +{ + private readonly ITestOutputHelper _log; + + public MuxerVersionHandlingTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public void ShouldUpdateMuxer_WhenExistingMuxerDoesNotExist_ReturnsTrue() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + var newMuxerPath = Path.Combine(tempDir, "new_dotnet.exe"); + var existingMuxerPath = Path.Combine(tempDir, "existing_dotnet.exe"); + + // Create a dummy new muxer file (doesn't need actual version info for this test) + File.WriteAllText(newMuxerPath, "dummy content"); + + // Act + var result = DotnetArchiveExtractor.ShouldUpdateMuxer(newMuxerPath, existingMuxerPath); + + // Assert + result.Should().BeTrue("when there is no existing muxer, we should install the new one"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] + public void ShouldUpdateMuxer_WhenNewMuxerDoesNotExist_ReturnsFalse() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + var newMuxerPath = Path.Combine(tempDir, "new_dotnet.exe"); + var existingMuxerPath = Path.Combine(tempDir, "existing_dotnet.exe"); + + // Create a dummy existing muxer file + File.WriteAllText(existingMuxerPath, "dummy content"); + // New muxer does not exist + + // Act + var result = DotnetArchiveExtractor.ShouldUpdateMuxer(newMuxerPath, existingMuxerPath); + + // Assert + result.Should().BeFalse("when the new muxer file doesn't exist, we should not attempt to update"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] + public void GetMuxerFileVersion_WhenFileDoesNotExist_ReturnsNull() + { + // Arrange + var nonExistentPath = Path.Combine(Path.GetTempPath(), $"non-existent-{Guid.NewGuid():N}.exe"); + + // Act + var version = DotnetArchiveExtractor.GetMuxerFileVersion(nonExistentPath); + + // Assert + version.Should().BeNull("non-existent files should return null version"); + } + + [Fact] + public void GetMuxerFileVersion_WithCurrentDotnetExe_ReturnsValidVersion() + { + // Skip this test on non-Windows as FileVersionInfo may not work the same way + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _log.WriteLine("Skipping test on non-Windows platform"); + return; + } + + // Arrange - get the path to the current dotnet executable + var dotnetExePath = GetCurrentDotnetExePath(); + + if (dotnetExePath is null || !File.Exists(dotnetExePath)) + { + _log.WriteLine($"Could not find dotnet executable at: {dotnetExePath}"); + return; + } + + _log.WriteLine($"Testing with dotnet at: {dotnetExePath}"); + + // Act + var version = DotnetArchiveExtractor.GetMuxerFileVersion(dotnetExePath); + + // Assert + version.Should().NotBeNull("the dotnet executable should have version information"); + _log.WriteLine($"Detected version: {version}"); + version!.Major.Should().BeGreaterThan(0, "the major version should be a positive number"); + } + + [Fact] + public void ShouldUpdateMuxer_WithRealMuxerVersionComparison_WorksCorrectly() + { + // Skip this test on non-Windows as FileVersionInfo may not work the same way + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _log.WriteLine("Skipping test on non-Windows platform"); + return; + } + + // Arrange - get the path to the current dotnet executable + var dotnetExePath = GetCurrentDotnetExePath(); + + if (dotnetExePath is null || !File.Exists(dotnetExePath)) + { + _log.WriteLine($"Could not find dotnet executable at: {dotnetExePath}"); + return; + } + + // Act - compare the muxer with itself (should not update since versions are equal) + var result = DotnetArchiveExtractor.ShouldUpdateMuxer(dotnetExePath, dotnetExePath); + + // Assert - same version should not trigger an update + result.Should().BeFalse("comparing the same file should not trigger an update"); + } + + [Fact] + public void GetMuxerFileVersion_WithDummyFile_ReturnsNull() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + var dummyFilePath = Path.Combine(tempDir, "dummy.exe"); + File.WriteAllText(dummyFilePath, "This is not a real executable"); + + // Act + var version = DotnetArchiveExtractor.GetMuxerFileVersion(dummyFilePath); + + // Assert + version.Should().BeNull("a dummy file without version info should return null"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + private static string? GetCurrentDotnetExePath() + { + // Try to find the dotnet executable in the current directory or PATH + var exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + + // First, try the .dotnet directory in the repo root + var currentDir = AppContext.BaseDirectory; + while (currentDir != null) + { + var repoDotnet = Path.Combine(currentDir, ".dotnet", exeName); + if (File.Exists(repoDotnet)) + { + return repoDotnet; + } + + var parentDir = Directory.GetParent(currentDir); + currentDir = parentDir?.FullName; + } + + // Fallback to system dotnet + var systemDotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(systemDotnet) && File.Exists(systemDotnet)) + { + return systemDotnet; + } + + return null; + } +} From 7b7cd502ad5d2c4d6f232a9dd6e6f7b82b67d0f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:27:54 +0000 Subject: [PATCH 3/5] Add missing using statement for Xunit.Abstractions Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com> --- test/dotnetup.Tests/MuxerVersionHandlingTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/dotnetup.Tests/MuxerVersionHandlingTests.cs b/test/dotnetup.Tests/MuxerVersionHandlingTests.cs index ccc4e223d9da..0e523e6f162f 100644 --- a/test/dotnetup.Tests/MuxerVersionHandlingTests.cs +++ b/test/dotnetup.Tests/MuxerVersionHandlingTests.cs @@ -7,6 +7,7 @@ using Microsoft.Dotnet.Installation.Internal; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Tools.Dotnetup.Tests; From 665d370e2f4ff538b632d420021ba0a7b0c26c51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:38:45 +0000 Subject: [PATCH 4/5] Replace unit tests with integration tests in LibraryTests - Remove MuxerVersionHandlingTests.cs as requested - Add two integration tests to LibraryTests: - MuxerIsUpdated_WhenInstallingNewerSdk: verifies muxer is upgraded when installing SDK 10 after SDK 9 - MuxerIsNotDowngraded_WhenInstallingOlderSdk: verifies muxer is not downgraded when installing SDK 9 after SDK 10 - Tests skip on non-Windows platforms since FileVersionInfo doesn't work with ELF binaries Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com> --- test/dotnetup.Tests/DnupE2Etest.cs | 10 +- test/dotnetup.Tests/LibraryTests.cs | 94 ++++++++ .../MuxerVersionHandlingTests.cs | 202 ------------------ .../Utilities/UpdateChannelExtensions.cs | 2 +- 4 files changed, 100 insertions(+), 208 deletions(-) delete mode 100644 test/dotnetup.Tests/MuxerVersionHandlingTests.cs 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..be0e0facbd93 100644 --- a/test/dotnetup.Tests/LibraryTests.cs +++ b/test/dotnetup.Tests/LibraryTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; using System.Text; using Microsoft.Dotnet.Installation; using Microsoft.Dotnet.Installation.Internal; @@ -60,4 +62,96 @@ public void TestGetSupportedChannels() channels.Should().NotContain("7.0.1xx"); } + + [Fact] + public void MuxerIsUpdated_WhenInstallingNewerSdk() + { + // Skip test on non-Windows as FileVersionInfo doesn't work with ELF binaries + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Log.WriteLine("Skipping test on non-Windows platform (FileVersionInfo doesn't work with ELF binaries)"); + return; + } + + 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()); + var versionAfterSdk9 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + Log.WriteLine($"Muxer version after SDK 9.0 install: {versionAfterSdk9}"); + versionAfterSdk9.Should().NotBeNull("muxer 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 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + Log.WriteLine($"Muxer version after SDK 10.0 install: {versionAfterSdk10}"); + versionAfterSdk10.Should().NotBeNull("muxer 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() + { + // Skip test on non-Windows as FileVersionInfo doesn't work with ELF binaries + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Log.WriteLine("Skipping test on non-Windows platform (FileVersionInfo doesn't work with ELF binaries)"); + return; + } + + 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()); + var versionAfterSdk10 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + Log.WriteLine($"Muxer version after SDK 10.0 install: {versionAfterSdk10}"); + versionAfterSdk10.Should().NotBeNull("muxer 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 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + Log.WriteLine($"Muxer version after SDK 9.0 install: {versionAfterSdk9}"); + versionAfterSdk9.Should().NotBeNull("muxer 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"); + } } diff --git a/test/dotnetup.Tests/MuxerVersionHandlingTests.cs b/test/dotnetup.Tests/MuxerVersionHandlingTests.cs deleted file mode 100644 index 0e523e6f162f..000000000000 --- a/test/dotnetup.Tests/MuxerVersionHandlingTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using System.Runtime.InteropServices; -using Microsoft.Dotnet.Installation.Internal; -using FluentAssertions; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.DotNet.Tools.Dotnetup.Tests; - -/// -/// Tests for muxer version handling during SDK installation. -/// -public class MuxerVersionHandlingTests -{ - private readonly ITestOutputHelper _log; - - public MuxerVersionHandlingTests(ITestOutputHelper log) - { - _log = log; - } - - [Fact] - public void ShouldUpdateMuxer_WhenExistingMuxerDoesNotExist_ReturnsTrue() - { - // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - - try - { - var newMuxerPath = Path.Combine(tempDir, "new_dotnet.exe"); - var existingMuxerPath = Path.Combine(tempDir, "existing_dotnet.exe"); - - // Create a dummy new muxer file (doesn't need actual version info for this test) - File.WriteAllText(newMuxerPath, "dummy content"); - - // Act - var result = DotnetArchiveExtractor.ShouldUpdateMuxer(newMuxerPath, existingMuxerPath); - - // Assert - result.Should().BeTrue("when there is no existing muxer, we should install the new one"); - } - finally - { - Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public void ShouldUpdateMuxer_WhenNewMuxerDoesNotExist_ReturnsFalse() - { - // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - - try - { - var newMuxerPath = Path.Combine(tempDir, "new_dotnet.exe"); - var existingMuxerPath = Path.Combine(tempDir, "existing_dotnet.exe"); - - // Create a dummy existing muxer file - File.WriteAllText(existingMuxerPath, "dummy content"); - // New muxer does not exist - - // Act - var result = DotnetArchiveExtractor.ShouldUpdateMuxer(newMuxerPath, existingMuxerPath); - - // Assert - result.Should().BeFalse("when the new muxer file doesn't exist, we should not attempt to update"); - } - finally - { - Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public void GetMuxerFileVersion_WhenFileDoesNotExist_ReturnsNull() - { - // Arrange - var nonExistentPath = Path.Combine(Path.GetTempPath(), $"non-existent-{Guid.NewGuid():N}.exe"); - - // Act - var version = DotnetArchiveExtractor.GetMuxerFileVersion(nonExistentPath); - - // Assert - version.Should().BeNull("non-existent files should return null version"); - } - - [Fact] - public void GetMuxerFileVersion_WithCurrentDotnetExe_ReturnsValidVersion() - { - // Skip this test on non-Windows as FileVersionInfo may not work the same way - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _log.WriteLine("Skipping test on non-Windows platform"); - return; - } - - // Arrange - get the path to the current dotnet executable - var dotnetExePath = GetCurrentDotnetExePath(); - - if (dotnetExePath is null || !File.Exists(dotnetExePath)) - { - _log.WriteLine($"Could not find dotnet executable at: {dotnetExePath}"); - return; - } - - _log.WriteLine($"Testing with dotnet at: {dotnetExePath}"); - - // Act - var version = DotnetArchiveExtractor.GetMuxerFileVersion(dotnetExePath); - - // Assert - version.Should().NotBeNull("the dotnet executable should have version information"); - _log.WriteLine($"Detected version: {version}"); - version!.Major.Should().BeGreaterThan(0, "the major version should be a positive number"); - } - - [Fact] - public void ShouldUpdateMuxer_WithRealMuxerVersionComparison_WorksCorrectly() - { - // Skip this test on non-Windows as FileVersionInfo may not work the same way - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _log.WriteLine("Skipping test on non-Windows platform"); - return; - } - - // Arrange - get the path to the current dotnet executable - var dotnetExePath = GetCurrentDotnetExePath(); - - if (dotnetExePath is null || !File.Exists(dotnetExePath)) - { - _log.WriteLine($"Could not find dotnet executable at: {dotnetExePath}"); - return; - } - - // Act - compare the muxer with itself (should not update since versions are equal) - var result = DotnetArchiveExtractor.ShouldUpdateMuxer(dotnetExePath, dotnetExePath); - - // Assert - same version should not trigger an update - result.Should().BeFalse("comparing the same file should not trigger an update"); - } - - [Fact] - public void GetMuxerFileVersion_WithDummyFile_ReturnsNull() - { - // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"muxer-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - - try - { - var dummyFilePath = Path.Combine(tempDir, "dummy.exe"); - File.WriteAllText(dummyFilePath, "This is not a real executable"); - - // Act - var version = DotnetArchiveExtractor.GetMuxerFileVersion(dummyFilePath); - - // Assert - version.Should().BeNull("a dummy file without version info should return null"); - } - finally - { - Directory.Delete(tempDir, recursive: true); - } - } - - private static string? GetCurrentDotnetExePath() - { - // Try to find the dotnet executable in the current directory or PATH - var exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - - // First, try the .dotnet directory in the repo root - var currentDir = AppContext.BaseDirectory; - while (currentDir != null) - { - var repoDotnet = Path.Combine(currentDir, ".dotnet", exeName); - if (File.Exists(repoDotnet)) - { - return repoDotnet; - } - - var parentDir = Directory.GetParent(currentDir); - currentDir = parentDir?.FullName; - } - - // Fallback to system dotnet - var systemDotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); - if (!string.IsNullOrEmpty(systemDotnet) && File.Exists(systemDotnet)) - { - return systemDotnet; - } - - 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; From 0c5e20136ab12210b91fafb716b788a6f0f210db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:14:35 +0000 Subject: [PATCH 5/5] Support muxer version detection on Linux/Mac using runtime versions - Update GetMuxerFileVersion to fallback to runtime version detection on non-Windows platforms - Add GetRuntimeVersionFromArchive to extract runtime version from tar/zip archives - Add GetLatestRuntimeVersionFromInstallRoot to get highest runtime version from shared/Microsoft.NETCore.App - Update ShouldUpdateMuxer to accept archive path and install root for runtime version fallback - Pass archive and install root context through MuxerHandlingConfig - Remove Windows-only test restrictions - tests now run on all platforms Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com> --- .../Internal/DotnetArchiveExtractor.cs | 235 ++++++++++++++++-- test/dotnetup.Tests/LibraryTests.cs | 23 +- 2 files changed, 225 insertions(+), 33 deletions(-) diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs index 6f04fc6d279f..5157156d3f03 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. 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; @@ -76,7 +78,7 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir string muxerName = DotnetupUtilities.GetDotnetExeName(); string muxerTargetPath = Path.Combine(targetDir, muxerName); - var muxerConfig = new MuxerHandlingConfig(muxerName, muxerTargetPath); + var muxerConfig = new MuxerHandlingConfig(muxerName, muxerTargetPath, archivePath, targetDir); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -93,8 +95,10 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir /// /// Path to the new muxer file (extracted to temp location) /// Path to the existing muxer file + /// Path to the archive being installed (used to determine runtime version on non-Windows) + /// Install root directory (used to find existing runtime versions on non-Windows) /// True if the new muxer should replace the existing one - internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxerPath) + internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxerPath, string? archivePath = null, string? installRoot = null) { // If there's no existing muxer, we should install the new one if (!File.Exists(existingMuxerPath)) @@ -103,8 +107,8 @@ internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxer } // Compare file versions - Version? existingVersion = GetMuxerFileVersion(existingMuxerPath); - Version? newVersion = GetMuxerFileVersion(newMuxerPath); + Version? existingVersion = GetMuxerFileVersion(existingMuxerPath, installRoot); + Version? newVersion = GetMuxerFileVersion(newMuxerPath, archivePath); // If we can't determine the new version, don't update (safety measure) if (newVersion is null) @@ -124,8 +128,11 @@ internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxer /// /// Gets the file version of a muxer executable. + /// On Windows, uses FileVersionInfo. On other platforms, uses runtime version from archive/install root. /// - internal static Version? GetMuxerFileVersion(string muxerPath) + /// Path to the muxer executable + /// Archive path or install root path for fallback version detection + internal static Version? GetMuxerFileVersion(string muxerPath, string? contextPath = null) { if (!File.Exists(muxerPath)) { @@ -150,8 +157,204 @@ internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxer versionInfo.FilePrivatePart); } + // On non-Windows, FileVersionInfo doesn't work with ELF binaries + // Fall back to using runtime version from context path + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && contextPath != null) + { + return GetRuntimeVersionFromContext(contextPath); + } + + return null; + } + catch + { + // On error, try fallback for non-Windows + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && contextPath != null) + { + return GetRuntimeVersionFromContext(contextPath); + } + return null; + } + } + + /// + /// Gets the runtime version from a context path (either an archive or install root directory). + /// + private static Version? GetRuntimeVersionFromContext(string contextPath) + { + if (string.IsNullOrEmpty(contextPath)) + { return null; } + + // Check if it's an archive file + if (File.Exists(contextPath)) + { + return GetRuntimeVersionFromArchive(contextPath); + } + + // Otherwise treat it as an install root directory + if (Directory.Exists(contextPath)) + { + return GetLatestRuntimeVersionFromInstallRoot(contextPath); + } + + return null; + } + + /// + /// Gets the runtime version from an archive by examining the runtime directories. + /// + private static Version? GetRuntimeVersionFromArchive(string archivePath) + { + try + { + if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + using var zip = ZipFile.OpenRead(archivePath); + return GetRuntimeVersionFromZipEntries(zip.Entries); + } + else if (archivePath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || archivePath.EndsWith(".tar", StringComparison.OrdinalIgnoreCase)) + { + string tarPath = archivePath; + bool needsCleanup = false; + + if (archivePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + { + tarPath = DecompressTarGzToTemp(archivePath); + needsCleanup = true; + } + + try + { + using var tarStream = File.OpenRead(tarPath); + using var tarReader = new TarReader(tarStream); + return GetRuntimeVersionFromTarEntries(tarReader); + } + finally + { + if (needsCleanup && File.Exists(tarPath)) + { + File.Delete(tarPath); + } + } + } + } + catch + { + // If we can't read the archive, return null + } + + return null; + } + + /// + /// Decompresses a .tar.gz file to a temporary location. + /// + private static string DecompressTarGzToTemp(string gzPath) + { + string tempPath = Path.Combine(Path.GetTempPath(), $"dotnet-{Guid.NewGuid()}.tar"); + using FileStream originalFileStream = File.OpenRead(gzPath); + using FileStream decompressedFileStream = File.Create(tempPath); + using GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress); + decompressionStream.CopyTo(decompressedFileStream); + return tempPath; + } + + /// + /// Gets the runtime version from zip archive entries. + /// + private static Version? GetRuntimeVersionFromZipEntries(IEnumerable entries) + { + Version? highestVersion = null; + + foreach (var entry in entries) + { + // Look for shared/Microsoft.NETCore.App/{version}/ pattern + if (entry.FullName.Contains("shared/Microsoft.NETCore.App/", StringComparison.OrdinalIgnoreCase) || + entry.FullName.Contains("shared\\Microsoft.NETCore.App\\", StringComparison.OrdinalIgnoreCase)) + { + var parts = entry.FullName.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var appIndex = Array.FindIndex(parts, p => p.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase)); + + if (appIndex >= 0 && appIndex + 1 < parts.Length) + { + var versionString = parts[appIndex + 1]; + if (Version.TryParse(versionString, out Version? version)) + { + if (highestVersion == null || version > highestVersion) + { + highestVersion = version; + } + } + } + } + } + + return highestVersion; + } + + /// + /// Gets the runtime version from tar archive entries. + /// + private static Version? GetRuntimeVersionFromTarEntries(TarReader tarReader) + { + Version? highestVersion = null; + TarEntry? entry; + + while ((entry = tarReader.GetNextEntry()) is not null) + { + // Look for shared/Microsoft.NETCore.App/{version}/ pattern + if (entry.Name.Contains("shared/Microsoft.NETCore.App/", StringComparison.OrdinalIgnoreCase)) + { + var parts = entry.Name.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var appIndex = Array.FindIndex(parts, p => p.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase)); + + if (appIndex >= 0 && appIndex + 1 < parts.Length) + { + var versionString = parts[appIndex + 1]; + if (Version.TryParse(versionString, out Version? version)) + { + if (highestVersion == null || version > highestVersion) + { + highestVersion = version; + } + } + } + } + } + + return highestVersion; + } + + /// + /// Gets the latest runtime version from the install root by checking the shared/Microsoft.NETCore.App directory. + /// + private static Version? GetLatestRuntimeVersionFromInstallRoot(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; @@ -265,7 +468,7 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) { - HandleMuxerFromTar(entry, muxerConfig.MuxerTargetPath); + HandleMuxerFromTar(entry, muxerConfig); } else { @@ -280,7 +483,7 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling /// /// Handles the muxer from a tar entry, comparing file versions to determine if update is needed. /// - private void HandleMuxerFromTar(TarEntry entry, string muxerTargetPath) + private void HandleMuxerFromTar(TarEntry entry, MuxerHandlingConfig muxerConfig) { // Create a temporary file for the muxer first var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name); @@ -292,10 +495,10 @@ private void HandleMuxerFromTar(TarEntry entry, string muxerTargetPath) try { // Check if we should update the muxer based on file version comparison - if (ShouldUpdateMuxer(tempMuxerPath, muxerTargetPath)) + if (ShouldUpdateMuxer(tempMuxerPath, muxerConfig.MuxerTargetPath, muxerConfig.ArchivePath, muxerConfig.InstallRoot)) { // Replace the muxer using the utility that handles locking - DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerConfig.MuxerTargetPath); } } finally @@ -354,7 +557,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl // Special handling for dotnet executable (muxer) if (string.Equals(fileName, muxerConfig.MuxerName, StringComparison.OrdinalIgnoreCase)) { - HandleMuxerFromZip(entry, muxerConfig.MuxerTargetPath); + HandleMuxerFromZip(entry, muxerConfig); } else { @@ -368,7 +571,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl /// /// Handles the muxer from a zip entry, comparing file versions to determine if update is needed. /// - private void HandleMuxerFromZip(ZipArchiveEntry entry, string muxerTargetPath) + private void HandleMuxerFromZip(ZipArchiveEntry entry, MuxerHandlingConfig muxerConfig) { var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name); entry.ExtractToFile(tempMuxerPath, overwrite: true); @@ -376,10 +579,10 @@ private void HandleMuxerFromZip(ZipArchiveEntry entry, string muxerTargetPath) try { // Check if we should update the muxer based on file version comparison - if (ShouldUpdateMuxer(tempMuxerPath, muxerTargetPath)) + if (ShouldUpdateMuxer(tempMuxerPath, muxerConfig.MuxerTargetPath, muxerConfig.ArchivePath, muxerConfig.InstallRoot)) { // Replace the muxer using the utility that handles locking - DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerTargetPath); + DotnetupUtilities.ForceReplaceFile(tempMuxerPath, muxerConfig.MuxerTargetPath); } } finally @@ -398,11 +601,15 @@ private readonly struct MuxerHandlingConfig { public string MuxerName { get; } public string MuxerTargetPath { get; } + public string ArchivePath { get; } + public string InstallRoot { get; } - public MuxerHandlingConfig(string muxerName, string muxerTargetPath) + public MuxerHandlingConfig(string muxerName, string muxerTargetPath, string archivePath, string installRoot) { MuxerName = muxerName; MuxerTargetPath = muxerTargetPath; + ArchivePath = archivePath; + InstallRoot = installRoot; } } diff --git a/test/dotnetup.Tests/LibraryTests.cs b/test/dotnetup.Tests/LibraryTests.cs index be0e0facbd93..3f52066aa1bb 100644 --- a/test/dotnetup.Tests/LibraryTests.cs +++ b/test/dotnetup.Tests/LibraryTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using System.Text; using Microsoft.Dotnet.Installation; using Microsoft.Dotnet.Installation.Internal; @@ -66,13 +65,6 @@ public void TestGetSupportedChannels() [Fact] public void MuxerIsUpdated_WhenInstallingNewerSdk() { - // Skip test on non-Windows as FileVersionInfo doesn't work with ELF binaries - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Log.WriteLine("Skipping test on non-Windows platform (FileVersionInfo doesn't work with ELF binaries)"); - return; - } - var releaseInfoProvider = InstallerFactory.CreateReleaseInfoProvider(); var installer = InstallerFactory.CreateInstaller(new NullProgressTarget()); @@ -89,7 +81,7 @@ public void MuxerIsUpdated_WhenInstallingNewerSdk() sdk9Version!); var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName()); - var versionAfterSdk9 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + var versionAfterSdk9 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath, testEnv.InstallPath); Log.WriteLine($"Muxer version after SDK 9.0 install: {versionAfterSdk9}"); versionAfterSdk9.Should().NotBeNull("muxer should exist after SDK 9.0 installation"); @@ -101,7 +93,7 @@ public void MuxerIsUpdated_WhenInstallingNewerSdk() InstallComponent.SDK, sdk10Version!); - var versionAfterSdk10 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + var versionAfterSdk10 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath, testEnv.InstallPath); Log.WriteLine($"Muxer version after SDK 10.0 install: {versionAfterSdk10}"); versionAfterSdk10.Should().NotBeNull("muxer should exist after SDK 10.0 installation"); @@ -112,13 +104,6 @@ public void MuxerIsUpdated_WhenInstallingNewerSdk() [Fact] public void MuxerIsNotDowngraded_WhenInstallingOlderSdk() { - // Skip test on non-Windows as FileVersionInfo doesn't work with ELF binaries - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Log.WriteLine("Skipping test on non-Windows platform (FileVersionInfo doesn't work with ELF binaries)"); - return; - } - var releaseInfoProvider = InstallerFactory.CreateReleaseInfoProvider(); var installer = InstallerFactory.CreateInstaller(new NullProgressTarget()); @@ -135,7 +120,7 @@ public void MuxerIsNotDowngraded_WhenInstallingOlderSdk() sdk10Version!); var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName()); - var versionAfterSdk10 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + var versionAfterSdk10 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath, testEnv.InstallPath); Log.WriteLine($"Muxer version after SDK 10.0 install: {versionAfterSdk10}"); versionAfterSdk10.Should().NotBeNull("muxer should exist after SDK 10.0 installation"); @@ -147,7 +132,7 @@ public void MuxerIsNotDowngraded_WhenInstallingOlderSdk() InstallComponent.SDK, sdk9Version!); - var versionAfterSdk9 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath); + var versionAfterSdk9 = DotnetArchiveExtractor.GetMuxerFileVersion(muxerPath, testEnv.InstallPath); Log.WriteLine($"Muxer version after SDK 9.0 install: {versionAfterSdk9}"); versionAfterSdk9.Should().NotBeNull("muxer should exist after SDK 9.0 installation");