Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

#### Changes

- Enhanced dotnet installation discovery by adopting the same `Muxer` logic used by the .NET SDK itself (#4732)
10 changes: 8 additions & 2 deletions src/Cli/func/Helpers/DotnetHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ private static Task<int> RunDotnetNewAsync(string args)

public static void EnsureDotnet()
{
if (!CommandChecker.CommandExists("dotnet"))
try
{
_ = DotnetMuxer.GetMuxerPath();
}
catch (InvalidOperationException ex)
{
throw new CliException("dotnet sdk is required for dotnet based functions. Please install https://microsoft.com/net");
throw new CliException(
"Unable to locate the .NET SDK. Install the .NET SDK from https://aka.ms/dotnet-download or configure PATH | DOTNET_ROOT | DOTNET_HOST_PATH so that 'dotnet' is discoverable.",
ex);
}
}

Expand Down
61 changes: 61 additions & 0 deletions src/Cli/func/Helpers/DotnetMuxer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

// Adapted from: https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.DotNet.Cli.Utils/Muxer.cs
using System.Runtime.InteropServices;

namespace Azure.Functions.Cli.Helpers;

internal static class DotnetMuxer
{
private static readonly string _muxerName = "dotnet";
private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;

/// <summary>
/// Locates the dotnet muxer (dotnet executable).
/// </summary>
/// <returns>The full path to the dotnet executable.</returns>
/// <exception cref="InvalidOperationException">Thrown when the dotnet executable cannot be located.</exception>
public static string GetMuxerPath()
{
string muxerPath;

// Most scenarios are running dotnet.dll as the app
// Root directory with muxer should be two above app base: <root>/sdk/<version>
string rootPath = Path.GetDirectoryName(Path.GetDirectoryName(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)));
if (rootPath is not null)
{
muxerPath = Path.Combine(rootPath, $"{_muxerName}{_exeSuffix}");
if (File.Exists(muxerPath))
{
return muxerPath;
}
}

// Best-effort search for muxer.
muxerPath = Environment.ProcessPath;

// The current process should be dotnet in most normal scenarios except when dotnet.dll is loaded in a custom host like the testhost
if (muxerPath is not null && !Path.GetFileNameWithoutExtension(muxerPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
{
// SDK sets DOTNET_HOST_PATH as absolute path to current dotnet executable
muxerPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (muxerPath is null)
{
// fallback to DOTNET_ROOT which typically holds some dotnet executable
string root = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (root is not null)
{
muxerPath = Path.Combine(root, $"dotnet{_exeSuffix}");
}
}
}

if (muxerPath is null)
{
throw new InvalidOperationException("Unable to locate dotnet multiplexer");
}

return muxerPath;
}
}
8 changes: 8 additions & 0 deletions test/Cli/Func.UnitTests/HelperTests/DotnetHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ namespace Azure.Functions.Cli.UnitTests.HelperTests
{
public class DotnetHelpersTests
{
[Fact]
public void EnsureDotnet_DoesNotThrow_WhenDotnetExists()
{
// dotnet is always installed in the test environment
var exception = Record.Exception(() => DotnetHelpers.EnsureDotnet());
Assert.Null(exception);
}

[Theory]
[InlineData("BlobTrigger", "blob")]
[InlineData("HttpTrigger", "http")]
Expand Down
76 changes: 76 additions & 0 deletions test/Cli/Func.UnitTests/HelperTests/DotnetMuxerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Azure.Functions.Cli.Helpers;
using Xunit;

namespace Azure.Functions.Cli.UnitTests.HelperTests
{
public class DotnetMuxerTests
{
[Fact]
public void GetMuxerPath_ReturnsMuxerPath_WhenDotnetExists()
{
// Act
var path = DotnetMuxer.GetMuxerPath();

// Assert
Assert.NotNull(path);
Assert.False(string.IsNullOrWhiteSpace(path));
}

[Fact]
public void GetMuxerPath_ContainsDotnetExecutable()
{
// Act
var path = DotnetMuxer.GetMuxerPath();

// Assert
Assert.Contains("dotnet", path, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void GetMuxerPath_PointsToExecutableFile()
{
// Act
var path = DotnetMuxer.GetMuxerPath();

// Assert
// The path should exist as a file
Assert.True(
File.Exists(path) || File.Exists(path + ".exe"),
$"Expected muxer path '{path}' to exist");
}

[Fact]
public void GetMuxerPath_PointsToFunctionalDotnetExecutable()
{
// Act
var path = DotnetMuxer.GetMuxerPath();

// Assert - invoke dotnet --version to verify it's functional
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = path,
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = System.Diagnostics.Process.Start(startInfo);
Assert.NotNull(process);

var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();

// Assert
Assert.Equal(0, process.ExitCode);
Assert.False(string.IsNullOrWhiteSpace(output), "Expected dotnet --version to produce output");

// Verify output looks like a version number (e.g., "8.0.100")
Assert.Matches(@"^\d+\.\d+\.\d+", output.Trim());
}
}
}
Loading