Skip to content

Commit f64a80d

Browse files
jaredpar333fred
andauthored
Make compiler detection more resilient (#78402)
* Make compiler detection more resilient This changes the way our build task finds the csc to launch to longer depend on the `CompilerType` task argument. At the core for every deployment kind of the task there is only one possible compiler that can be loaded. Hence change our logic to better assert which deployment we are in and locate the compiler based on that. * more * more * PR feedback and tests * rename APIs * PR feedback * Apply suggestions from code review Co-authored-by: Fred Silberberg <fred@silberberg.xyz> * PR feedback * PR feedback * PR feedback --------- Co-authored-by: Fred Silberberg <fred@silberberg.xyz>
1 parent c53bdd8 commit f64a80d

File tree

9 files changed

+234
-110
lines changed

9 files changed

+234
-110
lines changed

src/Compilers/Core/MSBuildTask/ManagedCompiler.cs

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -493,21 +493,6 @@ public string? CompilerType
493493
get { return (string?)_store[nameof(CompilerType)]; }
494494
}
495495

496-
protected override RoslynCompilerType GetCompilerType()
497-
{
498-
if (string.IsNullOrWhiteSpace(CompilerType))
499-
{
500-
return DefaultCompilerType;
501-
}
502-
503-
if (Enum.TryParse<RoslynCompilerType>(CompilerType, ignoreCase: true, out var compilerType))
504-
{
505-
return compilerType;
506-
}
507-
508-
throw new ArgumentException($"Invalid {nameof(CompilerType)} '{CompilerType}' specified. Valid values are {string.Join(", ", Enum.GetNames(typeof(RoslynCompilerType)))}.");
509-
}
510-
511496
#endregion
512497

513498
/// <summary>
@@ -544,7 +529,7 @@ internal int ExecuteTool(string pathToTool, string responseFileCommands, string
544529
string? tempDirectory = Path.GetTempPath();
545530

546531
if (!UseSharedCompilation ||
547-
!IsManagedTool ||
532+
!UsingBuiltinTool ||
548533
!BuildServerConnection.IsCompilerServerSupported)
549534
{
550535
LogCompilationMessage(logger, requestId, CompilationKind.Tool, $"using command line tool by design '{pathToTool}'");
@@ -555,10 +540,10 @@ internal int ExecuteTool(string pathToTool, string responseFileCommands, string
555540
logger.Log($"CommandLine = '{commandLineCommands}'");
556541
logger.Log($"BuildResponseFile = '{responseFileCommands}'");
557542

558-
var clientDirectory = Path.GetDirectoryName(PathToManagedTool);
543+
var clientDirectory = Path.GetDirectoryName(PathToBuiltInTool);
559544
if (clientDirectory is null || tempDirectory is null)
560545
{
561-
LogCompilationMessage(logger, requestId, CompilationKind.Tool, $"using command line tool because we could not find client or temp directory '{PathToManagedTool}'");
546+
LogCompilationMessage(logger, requestId, CompilationKind.Tool, $"using command line tool because we could not find client or temp directory '{PathToBuiltInTool}'");
562547
return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands);
563548
}
564549

@@ -1247,11 +1232,4 @@ internal string? GetWin32ManifestSwitch
12471232
return win32Manifest;
12481233
}
12491234
}
1250-
1251-
public enum RoslynCompilerType
1252-
{
1253-
Core,
1254-
Framework,
1255-
FrameworkPackage,
1256-
}
12571235
}

src/Compilers/Core/MSBuildTask/ManagedToolTask.cs

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.IO;
89
using System.Resources;
910
using System.Text;
@@ -15,8 +16,10 @@ namespace Microsoft.CodeAnalysis.BuildTasks
1516
{
1617
public abstract class ManagedToolTask : ToolTask
1718
{
19+
private static bool DefaultIsSdkFrameworkToCoreBridgeTask { get; } = CalculateIsSdkFrameworkToCoreBridgeTask();
20+
1821
/// <summary>
19-
/// Is the standard tool being used here? When false the developer has specified a custom tool
22+
/// Is the builtin tool being used here? When false the developer has specified a custom tool
2023
/// to be run by this task
2124
/// </summary>
2225
/// <remarks>
@@ -26,39 +29,33 @@ public abstract class ManagedToolTask : ToolTask
2629
/// ToolExe == ToolName, we know nothing is overridden, and
2730
/// we can use our own csc.
2831
/// </remarks>
29-
protected bool IsManagedTool => string.IsNullOrEmpty(ToolPath) && ToolExe == ToolName;
32+
protected bool UsingBuiltinTool => string.IsNullOrEmpty(ToolPath) && ToolExe == ToolName;
3033

3134
/// <summary>
32-
/// Used to determine the directory where the tools (like csc) are located.
33-
/// See <see cref="Utilities.GenerateFullPathToTool"/>.
35+
/// A copy of this task, compiled for .NET Framework, is deployed into the .NET SDK. It is a bridge task
36+
/// that is loaded into .NET Framework MSBuild but launches the .NET Core compiler. This task necessarily
37+
/// has different behaviors than the standard build task compiled for .NET Framework and loaded into the
38+
/// .NET Framework MSBuild.
3439
/// </summary>
35-
protected virtual RoslynCompilerType GetCompilerType() => DefaultCompilerType;
36-
37-
protected const RoslynCompilerType DefaultCompilerType
38-
#if NET
39-
= RoslynCompilerType.Core;
40-
#else
41-
= RoslynCompilerType.Framework;
42-
#endif
40+
/// <remarks>
41+
/// This is mutable to facilitate testing
42+
/// </remarks>
43+
internal bool IsSdkFrameworkToCoreBridgeTask { get; init; } = DefaultIsSdkFrameworkToCoreBridgeTask;
4344

44-
internal string PathToManagedTool => Utilities.GenerateFullPathToTool(ToolName, GetCompilerType());
45+
/// <summary>
46+
/// Is the builtin tool executed by this task running on .NET Core?
47+
/// </summary>
48+
internal bool IsBuiltinToolRunningOnCoreClr => RuntimeHostInfo.IsCoreClrRuntime || IsSdkFrameworkToCoreBridgeTask;
4549

46-
private string PathToManagedToolWithoutExtension
47-
{
48-
get
49-
{
50-
var extension = Path.GetExtension(PathToManagedTool);
51-
return PathToManagedTool.Substring(0, PathToManagedTool.Length - extension.Length);
52-
}
53-
}
50+
internal string PathToBuiltInTool => Path.Combine(GetToolDirectory(), ToolName);
5451

5552
protected ManagedToolTask(ResourceManager resourceManager)
5653
: base(resourceManager)
5754
{
5855
}
5956

6057
/// <summary>
61-
/// Generate the arguments to pass directly to the managed tool. These do not include
58+
/// Generate the arguments to pass directly to the buitin tool. These do not include
6259
/// arguments in the response file.
6360
/// </summary>
6461
/// <remarks>
@@ -77,9 +74,9 @@ internal string GenerateToolArguments()
7774
protected sealed override string GenerateCommandLineCommands()
7875
{
7976
var commandLineArguments = GenerateToolArguments();
80-
if (IsManagedTool)
77+
if (UsingBuiltinTool && IsBuiltinToolRunningOnCoreClr)
8178
{
82-
(_, commandLineArguments, _) = RuntimeHostInfo.GetProcessInfo(PathToManagedToolWithoutExtension, commandLineArguments);
79+
commandLineArguments = RuntimeHostInfo.GetDotNetExecCommandLine(PathToBuiltInTool, commandLineArguments);
8380
}
8481

8582
return commandLineArguments;
@@ -96,7 +93,7 @@ protected sealed override string GenerateResponseFileCommands()
9693
}
9794

9895
/// <summary>
99-
/// Generate the arguments to pass directly to the managed tool. These do not include
96+
/// Generate the arguments to pass directly to the buitin tool. These do not include
10097
/// arguments in the response file.
10198
/// </summary>
10299
/// <remarks>
@@ -117,10 +114,12 @@ protected sealed override string GenerateResponseFileCommands()
117114
/// This could be the managed assembly itself (on desktop .NET on Windows),
118115
/// or a runtime such as dotnet.
119116
/// </summary>
120-
protected sealed override string GenerateFullPathToTool() =>
121-
IsManagedTool
122-
? RuntimeHostInfo.GetProcessInfo(PathToManagedToolWithoutExtension, string.Empty).processFilePath
123-
: Path.Combine(ToolPath ?? "", ToolExe);
117+
protected sealed override string GenerateFullPathToTool() => (UsingBuiltinTool, IsBuiltinToolRunningOnCoreClr) switch
118+
{
119+
(true, true) => RuntimeHostInfo.GetDotNetPathOrDefault(),
120+
(true, false) => PathToBuiltInTool,
121+
(false, _) => Path.Combine(ToolPath ?? "", ToolExe)
122+
};
124123

125124
protected abstract string ToolNameWithoutExtension { get; }
126125

@@ -129,18 +128,17 @@ protected sealed override string GenerateFullPathToTool() =>
129128
protected abstract void AddResponseFileCommands(CommandLineBuilderExtension commandLine);
130129

131130
/// <summary>
132-
/// ToolName is only used in cases where <see cref="IsManagedTool"/> returns true.
133-
/// It returns the name of the managed assembly, which might not be the path returned by
134-
/// GenerateFullPathToTool, which can return the path to e.g. the dotnet executable.
131+
/// This is the file name of the builtin tool that will be executed.
135132
/// </summary>
136133
/// <remarks>
137-
/// We *cannot* actually call IsManagedTool in the implementation of this method,
138-
/// as the implementation of IsManagedTool calls this property. See the comment in
139-
/// <see cref="ManagedToolTask.IsManagedTool"/>.
134+
/// ToolName is only used in cases where <see cref="UsingBuiltinTool"/> returns true.
135+
/// It returns the name of the managed assembly, which might not be the path returned by
136+
/// GenerateFullPathToTool, which can return the path to e.g. the dotnet executable.
140137
/// </remarks>
141-
protected sealed override string ToolName => RuntimeHostInfo.IsCoreClrRuntime
142-
? $"{ToolNameWithoutExtension}.dll"
143-
: $"{ToolNameWithoutExtension}.exe";
138+
protected sealed override string ToolName =>
139+
IsBuiltinToolRunningOnCoreClr
140+
? $"{ToolNameWithoutExtension}.dll"
141+
: $"{ToolNameWithoutExtension}.exe";
144142

145143
/// <summary>
146144
/// This generates the command line arguments passed to the tool.
@@ -177,5 +175,60 @@ protected static ITaskItem[] GenerateCommandLineArgsTaskItems(List<string> comma
177175

178176
return items;
179177
}
178+
179+
private string GetToolDirectory()
180+
{
181+
var buildTask = typeof(ManagedToolTask).Assembly;
182+
var buildTaskDirectory = GetBuildTaskDirectory();
183+
#if NET
184+
return Path.Combine(buildTaskDirectory, "bincore");
185+
#else
186+
return IsSdkFrameworkToCoreBridgeTask
187+
? Path.Combine(buildTaskDirectory, "..", "bincore")
188+
: buildTaskDirectory;
189+
#endif
190+
}
191+
192+
/// <summary>
193+
/// <see cref="IsSdkFrameworkToCoreBridgeTask"/>
194+
/// </summary>
195+
/// <remarks>
196+
/// Using the file system as a way to differentiate between the two tasks is not ideal, but it is effective
197+
/// and allows us to avoid significantly complicating the build process. The alternative is another parameter
198+
/// to the Csc / Vbc / etc ... tasks that all invocations would need to pass along.
199+
/// </remarks>
200+
internal static bool CalculateIsSdkFrameworkToCoreBridgeTask()
201+
{
202+
#if NET
203+
return false;
204+
#else
205+
// This logic needs to be updated when this issue is fixed. That moves csc.exe out to a subdirectory
206+
// and hence the check below will need to change
207+
//
208+
// https://github.com/dotnet/roslyn/issues/78001
209+
210+
var buildTaskDirectory = GetBuildTaskDirectory();
211+
var buildTaskDirectoryName = Path.GetFileName(buildTaskDirectory);
212+
return
213+
string.Equals(buildTaskDirectoryName, "binfx", StringComparison.OrdinalIgnoreCase) &&
214+
!File.Exists(Path.Combine(buildTaskDirectory, "csc.exe")) &&
215+
Directory.Exists(Path.Combine(buildTaskDirectory, "..", "bincore"));
216+
#endif
217+
}
218+
219+
internal static string GetBuildTaskDirectory()
220+
{
221+
var buildTask = typeof(ManagedToolTask).Assembly;
222+
var buildTaskDirectory = Path.GetDirectoryName(buildTask.Location);
223+
if (buildTaskDirectory is null)
224+
{
225+
// This should not happen in supported product scenarios but could happen if
226+
// a non-supported scenario tried to load our task (like AOT) and call
227+
// through these members.
228+
throw new InvalidOperationException("Unable to determine the location of the build task assembly.");
229+
}
230+
231+
return buildTaskDirectory;
232+
}
180233
}
181234
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
7+
namespace Microsoft.CodeAnalysis.BuildTasks
8+
{
9+
#if NETFRAMEWORK && DEBUG
10+
11+
/// <summary>
12+
/// This class is used in <see cref="AppDomain"/> to allow us to test custom
13+
/// disk layouts and its behavior in the context of the managed tool task.
14+
/// </summary>
15+
internal sealed class TaskTestHost : MarshalByRefObject
16+
{
17+
internal bool IsSdkFrameworkToCoreBridgeTask => ManagedToolTask.CalculateIsSdkFrameworkToCoreBridgeTask();
18+
}
19+
20+
#endif
21+
}

src/Compilers/Core/MSBuildTask/Utilities.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,5 @@ internal static Exception GetLocalizedArgumentException(string errorString,
167167
return assembly.Location;
168168
#endif
169169
}
170-
171-
/// <summary>
172-
/// Generate the full path to the tool that is deployed with our build tasks.
173-
/// </summary>
174-
internal static string GenerateFullPathToTool(string toolFileName, RoslynCompilerType compilerType)
175-
{
176-
var buildTask = typeof(Utilities).GetTypeInfo().Assembly;
177-
var assemblyPath = buildTask.Location;
178-
var assemblyDirectory = Path.GetDirectoryName(assemblyPath)!;
179-
180-
return RuntimeHostInfo.IsDesktopRuntime
181-
? Path.Combine(assemblyDirectory, compilerType is RoslynCompilerType.Core ? "../bincore" : "", toolFileName)
182-
: Path.Combine(assemblyDirectory, "bincore", toolFileName);
183-
}
184170
}
185171
}

0 commit comments

Comments
 (0)