Skip to content

Commit 430e782

Browse files
authored
Refactor DetermineTargetFramework to use msbuild & support multiple tfms (#4715)
1 parent 171a9bd commit 430e782

File tree

4 files changed

+73
-54
lines changed

4 files changed

+73
-54
lines changed

src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public override async Task RunAsync()
175175
string projectFilePath = ProjectHelpers.FindProjectFile(functionAppRoot);
176176
if (projectFilePath != null)
177177
{
178-
var targetFramework = await DotnetHelpers.DetermineTargetFramework(Path.GetDirectoryName(projectFilePath));
178+
var targetFramework = await DotnetHelpers.DetermineTargetFrameworkAsync(Path.GetDirectoryName(projectFilePath));
179179

180180
var majorDotnetVersion = StacksApiHelper.GetMajorDotnetVersionFromDotnetVersionInProject(targetFramework);
181181

src/Cli/func/Actions/LocalActions/InitAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ private static async Task WriteDockerfile(WorkerRuntime workerRuntime, string la
419419
var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory);
420420
if (functionAppRoot != null)
421421
{
422-
targetFramework = await DotnetHelpers.DetermineTargetFramework(functionAppRoot);
422+
targetFramework = await DotnetHelpers.DetermineTargetFrameworkAsync(functionAppRoot);
423423
}
424424
}
425425

src/Cli/func/Helpers/DotnetHelpers.cs

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
using System.Runtime.InteropServices;
54
using System.Text;
65
using System.Text.RegularExpressions;
76
using Azure.Functions.Cli.Common;
@@ -39,52 +38,62 @@ public static void EnsureDotnet()
3938
/// Function that determines TargetFramework of a project even when it's defined outside of the .csproj file,
4039
/// e.g. in Directory.Build.props.
4140
/// </summary>
42-
/// <param name="projectDirectory">Directory containing the .csproj file.</param>
43-
/// <param name="projectFilename">Name of the .csproj file.</param>
41+
/// <param name="workingDirectory">Directory containing the .csproj file.</param>
4442
/// <returns>Target framework, e.g. net8.0.</returns>
4543
/// <exception cref="CliException">Unable to determine target framework.</exception>
46-
public static async Task<string> DetermineTargetFramework(string projectDirectory, string projectFilename = null)
44+
public static async Task<string> DetermineTargetFrameworkAsync(string workingDirectory)
4745
{
4846
EnsureDotnet();
49-
if (projectFilename == null)
50-
{
51-
var projectFilePath = ProjectHelpers.FindProjectFile(projectDirectory);
52-
if (projectFilePath != null)
53-
{
54-
projectFilename = Path.GetFileName(projectFilePath);
55-
}
56-
}
47+
48+
string projectFilePath = ProjectHelpers.FindProjectFile(workingDirectory);
49+
50+
string args =
51+
$"msbuild \"{projectFilePath}\" " +
52+
"-nologo -v:q -restore:false " +
53+
"-getProperty:TargetFrameworks " +
54+
"-getProperty:TargetFramework";
5755

5856
var exe = new Executable(
5957
"dotnet",
60-
$"build {projectFilename} -getproperty:TargetFramework",
61-
workingDirectory: projectDirectory,
62-
environmentVariables: new Dictionary<string, string>
63-
{
64-
// https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables
65-
["DOTNET_NOLOGO"] = "1", // do not write disclaimer to stdout
66-
["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1", // just in case
67-
});
58+
args,
59+
workingDirectory: workingDirectory,
60+
environmentVariables: new Dictionary<string, string> { ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" });
6861

69-
StringBuilder output = new();
70-
var exitCode = await exe.RunAsync(o => output.Append(o), e => ColoredConsole.Error.WriteLine(ErrorColor(e)));
71-
if (exitCode != 0)
62+
var stdOut = new StringBuilder();
63+
var stdErr = new StringBuilder();
64+
65+
int exit = await exe.RunAsync(s => stdOut.Append(s), s => stdErr.Append(s));
66+
if (exit != 0)
7267
{
73-
throw new CliException($"Can not determine target framework for dotnet project at ${projectDirectory}");
68+
throw new CliException(
69+
$"Unable to evaluate target framework for '{projectFilePath}'.\nError output:\n{stdErr}");
7470
}
7571

76-
// Extract the target framework moniker (TFM) from the output using regex pattern matching
77-
var outputString = output.ToString();
72+
string output = stdOut.ToString();
73+
74+
var uniqueTfms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
75+
foreach (Match m in TargetFrameworkHelper.TfmRegex.Matches(output))
76+
{
77+
if (m.Success && !string.IsNullOrEmpty(m.Value))
78+
{
79+
uniqueTfms.Add(m.Value);
80+
}
81+
}
7882

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

82-
if (!tfm.Success)
89+
if (uniqueTfms.Count == 1)
8390
{
84-
throw new CliException($"Could not parse target framework from output: {outputString}");
91+
return uniqueTfms.First();
8592
}
8693

87-
return tfm.Value;
94+
ColoredConsole.WriteLine("Multiple target frameworks detected.");
95+
SelectionMenuHelper.DisplaySelectionWizardPrompt("target framework");
96+
return SelectionMenuHelper.DisplaySelectionWizard(uniqueTfms.ToArray());
8897
}
8998

9099
public static async Task DeployDotnetProject(string name, bool force, WorkerRuntime workerRuntime, string targetFramework = "")

test/Cli/Func.E2ETests/Commands/FuncInit/DotnetIsolatedInitTests.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using Azure.Functions.Cli.Common;
45
using Azure.Functions.Cli.E2ETests.Traits;
56
using Azure.Functions.Cli.TestFramework.Assertions;
67
using Azure.Functions.Cli.TestFramework.Commands;
@@ -127,28 +128,37 @@ public void Init_WithTargetFrameworkAndDockerFlag_GeneratesDockerFile(string tar
127128
[InlineData("net10.0")]
128129
public async void Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile(string targetFramework)
129130
{
130-
var targetFrameworkstr = targetFramework.Replace("net", string.Empty);
131-
var workingDir = WorkingDirectory;
132-
var testName = nameof(Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile);
133-
var funcInitCommand = new FuncInitCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log)));
134-
var dockerFilePath = Path.Combine(workingDir, "Dockerfile");
135-
var expectedDockerfileContent = new[] { $"FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated{targetFrameworkstr}" };
136-
var filesToValidate = new List<(string FilePath, string[] ExpectedContent)>
137-
{
138-
(dockerFilePath, expectedDockerfileContent)
139-
};
140-
141-
// Initialize dotnet-isolated function app using retry helper
142-
await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "dotnet-isolated", "--target-framework", targetFramework]);
131+
var workingDir = Path.Combine(WorkingDirectory, targetFramework);
143132

144-
var funcInitResult = funcInitCommand
145-
.WithWorkingDirectory(workingDir)
146-
.Execute(["--docker-only"]);
147-
148-
// Validate expected output content
149-
funcInitResult.Should().ExitWith(0);
150-
funcInitResult.Should().WriteDockerfile();
151-
funcInitResult.Should().FilesExistsWithExpectContent(filesToValidate);
133+
try
134+
{
135+
var targetFrameworkstr = targetFramework.Replace("net", string.Empty);
136+
FileSystemHelpers.EnsureDirectory(workingDir);
137+
var testName = nameof(Init_DockerOnlyOnExistingProjectWithTargetFramework_GeneratesDockerfile);
138+
var funcInitCommand = new FuncInitCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log)));
139+
var dockerFilePath = Path.Combine(workingDir, "Dockerfile");
140+
var expectedDockerfileContent = new[] { $"FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated{targetFrameworkstr}" };
141+
var filesToValidate = new List<(string FilePath, string[] ExpectedContent)>
142+
{
143+
(dockerFilePath, expectedDockerfileContent)
144+
};
145+
146+
// Initialize dotnet-isolated function app using retry helper
147+
await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "dotnet-isolated", "--target-framework", targetFramework]);
148+
149+
var funcInitResult = funcInitCommand
150+
.WithWorkingDirectory(workingDir)
151+
.Execute(["--docker-only"]);
152+
153+
// Validate expected output content
154+
funcInitResult.Should().ExitWith(0);
155+
funcInitResult.Should().WriteDockerfile();
156+
funcInitResult.Should().FilesExistsWithExpectContent(filesToValidate);
157+
}
158+
finally
159+
{
160+
Directory.Delete(workingDir, recursive: true);
161+
}
152162
}
153163
}
154164
}

0 commit comments

Comments
 (0)