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
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public override async Task RunAsync()
string projectFilePath = ProjectHelpers.FindProjectFile(functionAppRoot);
if (projectFilePath != null)
{
var targetFramework = await DotnetHelpers.DetermineTargetFramework(Path.GetDirectoryName(projectFilePath));
var targetFramework = await DotnetHelpers.DetermineTargetFrameworkAsync(Path.GetDirectoryName(projectFilePath));

var majorDotnetVersion = StacksApiHelper.GetMajorDotnetVersionFromDotnetVersionInProject(targetFramework);

Expand Down
2 changes: 1 addition & 1 deletion src/Cli/func/Actions/LocalActions/InitAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ private static async Task WriteDockerfile(WorkerRuntime workerRuntime, string la
var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory);
if (functionAppRoot != null)
{
targetFramework = await DotnetHelpers.DetermineTargetFramework(functionAppRoot);
targetFramework = await DotnetHelpers.DetermineTargetFrameworkAsync(functionAppRoot);
}
}

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

using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Azure.Functions.Cli.Common;
Expand Down Expand Up @@ -39,52 +38,62 @@ public static void EnsureDotnet()
/// Function that determines TargetFramework of a project even when it's defined outside of the .csproj file,
/// e.g. in Directory.Build.props.
/// </summary>
/// <param name="projectDirectory">Directory containing the .csproj file.</param>
/// <param name="projectFilename">Name of the .csproj file.</param>
/// <param name="workingDirectory">Directory containing the .csproj file.</param>
/// <returns>Target framework, e.g. net8.0.</returns>
/// <exception cref="CliException">Unable to determine target framework.</exception>
public static async Task<string> DetermineTargetFramework(string projectDirectory, string projectFilename = null)
public static async Task<string> DetermineTargetFrameworkAsync(string workingDirectory)
{
EnsureDotnet();
if (projectFilename == null)
{
var projectFilePath = ProjectHelpers.FindProjectFile(projectDirectory);
if (projectFilePath != null)
{
projectFilename = Path.GetFileName(projectFilePath);
}
}

string projectFilePath = ProjectHelpers.FindProjectFile(workingDirectory);

string args =
$"msbuild \"{projectFilePath}\" " +
"-nologo -v:q -restore:false " +
"-getProperty:TargetFrameworks " +
"-getProperty:TargetFramework";

var exe = new Executable(
"dotnet",
$"build {projectFilename} -getproperty:TargetFramework",
workingDirectory: projectDirectory,
environmentVariables: new Dictionary<string, string>
{
// https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables
["DOTNET_NOLOGO"] = "1", // do not write disclaimer to stdout
["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1", // just in case
});
args,
workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string> { ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" });

StringBuilder output = new();
var exitCode = await exe.RunAsync(o => output.Append(o), e => ColoredConsole.Error.WriteLine(ErrorColor(e)));
if (exitCode != 0)
var stdOut = new StringBuilder();
var stdErr = new StringBuilder();

int exit = await exe.RunAsync(s => stdOut.Append(s), s => stdErr.Append(s));
if (exit != 0)
{
throw new CliException($"Can not determine target framework for dotnet project at ${projectDirectory}");
throw new CliException(
$"Unable to evaluate target framework for '{projectFilePath}'.\nError output:\n{stdErr}");
}

// Extract the target framework moniker (TFM) from the output using regex pattern matching
var outputString = output.ToString();
string output = stdOut.ToString();

var uniqueTfms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in TargetFrameworkHelper.TfmRegex.Matches(output))
{
if (m.Success && !string.IsNullOrEmpty(m.Value))
{
uniqueTfms.Add(m.Value);
}
}

// Look for a line that looks like a target framework moniker
var tfm = TargetFrameworkHelper.TfmRegex.Match(outputString);
if (uniqueTfms.Count == 0)
{
throw new CliException(
$"Could not parse target framework from msbuild output for '{projectFilePath}'.\nStdout:\n{output}\nStderr:\n{stdErr}");
}

if (!tfm.Success)
if (uniqueTfms.Count == 1)
{
throw new CliException($"Could not parse target framework from output: {outputString}");
return uniqueTfms.First();
}

return tfm.Value;
ColoredConsole.WriteLine("Multiple target frameworks detected.");
SelectionMenuHelper.DisplaySelectionWizardPrompt("target framework");
return SelectionMenuHelper.DisplaySelectionWizard(uniqueTfms.ToArray());
}

public static async Task DeployDotnetProject(string name, bool force, WorkerRuntime workerRuntime, string targetFramework = "")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Common;
using Azure.Functions.Cli.E2ETests.Traits;
using Azure.Functions.Cli.TestFramework.Assertions;
using Azure.Functions.Cli.TestFramework.Commands;
Expand Down Expand Up @@ -127,28 +128,37 @@ public void Init_WithTargetFrameworkAndDockerFlag_GeneratesDockerFile(string tar
[InlineData("net10.0")]
public async void Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile(string targetFramework)
{
var targetFrameworkstr = targetFramework.Replace("net", string.Empty);
var workingDir = WorkingDirectory;
var testName = nameof(Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile);
var funcInitCommand = new FuncInitCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log)));
var dockerFilePath = Path.Combine(workingDir, "Dockerfile");
var expectedDockerfileContent = new[] { $"FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated{targetFrameworkstr}" };
var filesToValidate = new List<(string FilePath, string[] ExpectedContent)>
{
(dockerFilePath, expectedDockerfileContent)
};

// Initialize dotnet-isolated function app using retry helper
await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "dotnet-isolated", "--target-framework", targetFramework]);
var workingDir = Path.Combine(WorkingDirectory, targetFramework);

var funcInitResult = funcInitCommand
.WithWorkingDirectory(workingDir)
.Execute(["--docker-only"]);

// Validate expected output content
funcInitResult.Should().ExitWith(0);
funcInitResult.Should().WriteDockerfile();
funcInitResult.Should().FilesExistsWithExpectContent(filesToValidate);
try
{
var targetFrameworkstr = targetFramework.Replace("net", string.Empty);
FileSystemHelpers.EnsureDirectory(workingDir);
var testName = nameof(Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile);
var funcInitCommand = new FuncInitCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log)));
var dockerFilePath = Path.Combine(workingDir, "Dockerfile");
var expectedDockerfileContent = new[] { $"FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated{targetFrameworkstr}" };
var filesToValidate = new List<(string FilePath, string[] ExpectedContent)>
{
(dockerFilePath, expectedDockerfileContent)
};

// Initialize dotnet-isolated function app using retry helper
await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "dotnet-isolated", "--target-framework", targetFramework]);

var funcInitResult = funcInitCommand
.WithWorkingDirectory(workingDir)
.Execute(["--docker-only"]);

// Validate expected output content
funcInitResult.Should().ExitWith(0);
funcInitResult.Should().WriteDockerfile();
funcInitResult.Should().FilesExistsWithExpectContent(filesToValidate);
}
finally
{
Directory.Delete(workingDir, recursive: true);
}
}
}
}
Loading