Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 2, 2025

DotnetArchiveExtractor.ConfigureMuxerHandling was incorrectly comparing SDK versions to determine whether to update the muxer (dotnet.exe). The muxer version is tied to runtime version, not SDK version, so this comparison was fundamentally broken.

Changes

  • Removed ConfigureMuxerHandling - deleted the flawed SDK version comparison logic
  • Added ShouldUpdateMuxer(newMuxerPath, existingMuxerPath, archivePath, installRoot) - compares actual file versions using FileVersionInfo.GetVersionInfo on Windows, with fallback to runtime version detection on Linux/Mac
  • Added GetMuxerFileVersion(muxerPath, contextPath) - extracts version from PE file metadata on Windows, falls back to runtime version on Linux/Mac
  • Added runtime version detection for non-Windows platforms:
    • GetRuntimeVersionFromArchive - extracts runtime version from tar/zip archives by scanning shared/Microsoft.NETCore.App/{version} directories
    • GetLatestRuntimeVersionFromInstallRoot - gets the highest runtime version from the install root's shared/Microsoft.NETCore.App directory
  • Updated extraction logic - both tar and zip handlers now:
    1. Extract muxer to temp location
    2. Compare file versions (or runtime versions on Linux/Mac)
    3. Only update if new version > existing version
internal static bool ShouldUpdateMuxer(string newMuxerPath, string existingMuxerPath, string? archivePath = null, string? installRoot = null)
{
    if (!File.Exists(existingMuxerPath)) return true;
    
    Version? existingVersion = GetMuxerFileVersion(existingMuxerPath, installRoot);
    Version? newVersion = GetMuxerFileVersion(newMuxerPath, archivePath);
    
    if (newVersion is null) return false;  // Safety: don't update if we can't read new version
    if (existingVersion is null) return true;
    
    return newVersion > existingVersion;
}

This ensures installing .NET 10 then .NET 9 won't downgrade the muxer, while installing .NET 9 then .NET 10 will correctly upgrade it.

Platform Support

Windows: Uses FileVersionInfo.GetVersionInfo to read the PE file version from the muxer executable.

Linux/Mac: When FileVersionInfo returns null (ELF binaries), falls back to runtime version detection:

  • For new muxer: extracts runtime version from the archive being installed
  • For existing muxer: gets the latest runtime version from the install root

Tests

Added two integration tests to LibraryTests.cs:

  • MuxerIsUpdated_WhenInstallingNewerSdk: Installs SDK 9.0 then 10.0, verifies muxer version increases
  • MuxerIsNotDowngraded_WhenInstallingOlderSdk: Installs SDK 10.0 then 9.0, verifies muxer version stays the same

Tests run on all platforms (Windows, Linux, Mac).


Fixes #51691

Original prompt

This section details on the original issue you should resolve

<issue_title>Fix muxer version handling for dotnetup (and installation library)</issue_title>
<issue_description>When installing the .NET SDK or runtime, there may be existing versions of the muxer installed in the dotnet root. We should install a new one if it is newer than the existing one. Currently DotnetArchiveExtractor tries to do this but does it incorrectly by comparing SDK versions to runtime versions.

If possible, the right way to do this is to probably look at the file metadata for the existing version and the version coming from the new install, to determine which one is newer.</issue_description>

<agent_instructions>The DotnetArchiveExtractor class should be updated so that it compares the file version of the existing muxer (dotnet.exe on Windows) to the version that would come from the tar or zip archive. It should use the one with the newer file version, so if the existing version is newer it should skip copying that file from the archive.

The ConfigureMuxerHandling method can probably be deleted entirely in favor of a new method that is called by the zip or tar extraction logic.

Also, you should add tests for this. If the .NET 9 SDK is installed first and then the .NET 10 SDK is installed, then the muxer should be updated. However, if the .NET 10 SDK is installed first, then the muxer should not be updated when the .NET 9 SDK is installed.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits December 2, 2025 15:24
…rsions

Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix muxer version handling for dotnetup installation library Fix muxer version handling to compare file versions instead of SDK versions Dec 2, 2025
Copilot AI requested a review from dsplaisted December 2, 2025 15:31
Copilot finished work on behalf of dsplaisted December 2, 2025 15:31
/// <summary>
/// Gets the file version of a muxer executable.
/// </summary>
internal static Version? GetMuxerFileVersion(string muxerPath)
Copy link
Member

Choose a reason for hiding this comment

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

@JeremyKuhne Copilot wrote this. Does the logic make sense for getting a file version to find out which version is newer? What's the difference between FileVersionInfo.FileVersion and the FileMajorPart etc. properties?

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

Please remove the tests you added in MuxerVersionHandlingTests.cs. I don't think unit-level tests of this are going to be very valuable.

Instead, add two tests to LibraryTests that follow the pattern of those tests. One test should install .NET SDK 9.0 and then 10.0, and verify that the file version of the muxer has been updated. The other test should install 10.0 and then 9.0 and verify that the muxer version isn't updated.

- 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>
Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

Please remove the tests you added in MuxerVersionHandlingTests.cs. I don't think unit-level tests of this are going to be very valuable.

Instead, add two tests to LibraryTests that follow the patt...

Done. Removed MuxerVersionHandlingTests.cs and added two integration tests to LibraryTests:

  • MuxerIsUpdated_WhenInstallingNewerSdk: Installs SDK 9.0 then 10.0, verifies muxer version increases
  • MuxerIsNotDowngraded_WhenInstallingOlderSdk: Installs SDK 10.0 then 9.0, verifies muxer version stays the same

Both tests skip on non-Windows since FileVersionInfo doesn't extract version info from ELF binaries.

Commit: 665d370

Copilot AI requested a review from dsplaisted December 2, 2025 19:41
Copilot finished work on behalf of dsplaisted December 2, 2025 19:41
Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

This logic also needs to work on Linux and Mac, where the file version info is not available.

On those platforms, assume that the version of the muxer corresponds to the version of the runtime included in the archive or the latest runtime installed in the dotnet install root folder. The latest runtime version will be the highest version folder under shared\Microsoft.NETCore.App in the install root.

When this is done, update the tests you added to run on all platforms.

/// </summary>
private void HandleMuxerFromZip(ZipArchiveEntry entry, string muxerTargetPath)
{
var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name);
Copy link
Member

Choose a reason for hiding this comment

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

It seems like we should move the temp directory creation and extraction to happen if and only if there already exists a muxer, so we avoid this unnecessary work to compare muxer versions.

/// <summary>
/// Handles the muxer from a tar entry, comparing file versions to determine if update is needed.
/// </summary>
private void HandleMuxerFromTar(TarEntry entry, string muxerTargetPath)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a generic Entry class that can be used for TarEntry and ZipEntry? I don't see why this logic needs to be different based on the archive type.

- 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>
Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

This logic also needs to work on Linux and Mac, where the file version info is not available.

On those platforms, assume that the version of the muxer corresponds to the version of the runtime incl...

Done. The muxer version handling now works on all platforms:

On Windows: Uses FileVersionInfo.GetVersionInfo to read the PE file version

On Linux/Mac: Falls back to runtime version detection when FileVersionInfo returns null:

  • For new muxer: extracts runtime version from the archive being installed by scanning shared/Microsoft.NETCore.App/{version} directories
  • For existing muxer: gets the latest runtime version from the install root's shared/Microsoft.NETCore.App directory

Tests updated to run on all platforms without skipping.

Commit: 0c5e201

Copilot AI requested a review from dsplaisted December 2, 2025 21:16
Copilot finished work on behalf of dsplaisted December 2, 2025 21:16
Copy link
Member

@nagilson nagilson left a comment

Choose a reason for hiding this comment

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

It seems like we should move the temp directory creation and extraction to happen if and only if there already exists a muxer, so we avoid this unnecessary work to compare muxer versions.

Is there a generic Entry class that can be used for TarEntry and ZipEntry? I don't see why this logic needs to be different based on the archive type.

@nagilson nagilson requested a review from Copilot December 2, 2025 21:34
Copilot finished reviewing on behalf of nagilson December 2, 2025 21:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a critical bug in muxer version handling where SDK versions were incorrectly compared to determine muxer updates. The muxer version is tied to runtime version, not SDK version, so the fix replaces SDK version comparison with actual file version comparison using PE file metadata on Windows and runtime version detection as a fallback on Linux/Mac.

Key Changes

  • Replaced flawed SDK comparison logic with file version comparison using FileVersionInfo.GetVersionInfo on Windows and runtime version detection from archives/install root on Linux/Mac
  • Added robust version detection methods that scan shared/Microsoft.NETCore.App/{version} directories in archives and install roots to determine runtime versions
  • Added comprehensive integration tests to verify muxer is upgraded when installing newer SDKs and not downgraded when installing older SDKs

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs Core fix: removed ConfigureMuxerHandling method and added ShouldUpdateMuxer, GetMuxerFileVersion, and runtime version detection methods; updated tar/zip extraction to use version comparison
test/dotnetup.Tests/LibraryTests.cs Added two integration tests to verify muxer upgrade/no-downgrade behavior when installing SDKs in different orders
test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs Minor: alphabetized using statements
test/dotnetup.Tests/DnupE2Etest.cs Minor: alphabetized using statements and formatting adjustments
Comments suppressed due to low confidence (1)

test/dotnetup.Tests/LibraryTests.cs:10

  • Missing required using directives for test framework. Add using Xunit; and using FluentAssertions; to support the [Fact] attributes and .Should() assertion methods used in the test methods below.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Dotnet.Installation;
using Microsoft.Dotnet.Installation.Internal;
using Microsoft.DotNet.Tools.Dotnetup.Tests.Utilities;

Comment on lines +358 to +359
catch
{
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Suggested change
catch
{
catch (Exception ex)
{
// Swallowing exceptions here because failure to detect a runtime version is not always exceptional,
// but log the exception for diagnostic purposes.
Debug.WriteLine($"Failed to get latest runtime version from install root '{installRoot}': {ex}");

Copilot uses AI. Check for mistakes.
Comment on lines +254 to +261
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;
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Code duplication: DecompressTarGzToTemp duplicates the decompression logic from DecompressTarGzIfNeeded. Consider extracting the core decompression logic into a shared private method to improve maintainability and reduce duplication.

Suggested change
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;
/// <summary>
/// Decompresses a .tar.gz file to a temporary location.
/// </summary>
private static string DecompressTarGzToTemp(string gzPath)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"dotnet-{Guid.NewGuid()}.tar");
DecompressTarGz(gzPath, tempPath);
return tempPath;
}
/// <summary>
/// Decompresses a .tar.gz file to the specified destination path.
/// </summary>
private static void DecompressTarGz(string sourcePath, string destinationPath)
{
using FileStream originalFileStream = File.OpenRead(sourcePath);
using FileStream decompressedFileStream = File.Create(destinationPath);
using GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress);
decompressionStream.CopyTo(decompressedFileStream);

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +158
if (versionInfo.FileMajorPart > 0 || versionInfo.FileMinorPart > 0)
{
return new Version(
versionInfo.FileMajorPart,
versionInfo.FileMinorPart,
versionInfo.FileBuildPart,
versionInfo.FilePrivatePart);
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

Potentially incorrect fallback condition: The check versionInfo.FileMajorPart > 0 || versionInfo.FileMinorPart > 0 will skip valid version 0.0.x.y. Consider changing the condition to check if any part is non-zero, or if the FileVersion property exists but failed to parse, to handle edge cases more robustly. For example: if (versionInfo.FileMajorPart != 0 || versionInfo.FileMinorPart != 0 || versionInfo.FileBuildPart != 0 || versionInfo.FilePrivatePart != 0)

Copilot uses AI. Check for mistakes.
}
catch
{
// If we can't read the archive, return null
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Suggested change
// If we can't read the archive, return null
// Intentionally suppressing all exceptions here because failure to read the archive
// should result in a null version (no version found). If debugging issues with version
// detection, consider adding logging here to capture exception details.
// Example: Console.WriteLine($"Failed to read archive '{archivePath}': {ex}");
// (Logging is omitted here to avoid introducing dependencies.)

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +177
catch
{
// On error, try fallback for non-Windows
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && contextPath != null)
{
return GetRuntimeVersionFromContext(contextPath);
}
return null;
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants