Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -13,3 +13,4 @@
- Refactor to use msbuild for determining .NET target framework & add support multiple TFMs (#4715)
- When using `func init --docker-only` on a .NET project with multiple target frameworks, the CLI will now
prompt the user to select which target framework to use for the Dockerfile.
- 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