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
10 changes: 10 additions & 0 deletions src/Compilers/Core/MSBuildTask/Csc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ protected override void AddResponseFileCommands(CommandLineBuilderExtension comm
{
commandLine.AppendSwitchIfNotNull("/sdkpath:", RuntimeEnvironment.GetRuntimeDirectory());

#if NETFRAMEWORK
// This branch only runs in the .NET Framework to .NET Core bridge task, which always
Comment thread
JeremyKuhne marked this conversation as resolved.
// executes on .NET Framework MSBuild where this assembly is deployed as loose files on
// disk. Assembly.Location is therefore valid here and unreachable from any single-file or
// native AOT host, so it does not need to satisfy the IL3000 single-file analyzer.
if (!NoConfig)
{
var rspFile = Path.Combine(Path.GetDirectoryName(typeof(ManagedCompiler).Assembly.Location)!, "csc.rsp");
Expand All @@ -214,6 +219,11 @@ protected override void AddResponseFileCommands(CommandLineBuilderExtension comm
commandLine.AppendSwitchIfNotNull("@", rspFile);
}
}
#else
// IsSdkFrameworkToCoreBridgeTask is only ever true on .NET Framework, so the bridge-only
// response file handling above is never reached on .NET Core.
Debug.Fail("The SDK framework-to-core bridge task only runs on .NET Framework MSBuild.");
#endif
}

commandLine.AppendSwitchIfNotNull("/lib:", AdditionalLibPaths, ",");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<RootNamespace>Microsoft.CodeAnalysis.BuildTasks</RootNamespace>
<TargetFrameworks>$(NetRoslynSourceBuild);net472</TargetFrameworks>
<DefineConstants>$(DefineConstants);MICROSOFT_CODEANALYSIS_CONTRACTS_NO_VALUE_TASK</DefineConstants>
<!--
A .NET copy of this task assembly is included in the .NET SDK's Native AOT CLI, so it must be
clean for trimming, AOT, and single-file publishing. IsAotCompatible (enabled only for the .NET
build, not net472) turns on the trim, AOT, and single-file analyzers.
-->
<IsAotCompatible Condition="'$(TargetFramework)' == '$(NetRoslynSourceBuild)'">true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
Expand Down
86 changes: 75 additions & 11 deletions src/Compilers/Core/MSBuildTask/ManagedToolTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

namespace Microsoft.CodeAnalysis.BuildTasks
{
/// <summary>
/// Base class for the MSBuild tasks that run the built-in managed compilers (<c>Csc</c>, <c>Vbc</c>,
/// <c>Csi</c>).
/// </summary>
public abstract class ManagedToolTask : ToolTask
{
private bool? _useAppHost;
Expand Down Expand Up @@ -62,6 +66,11 @@ public abstract class ManagedToolTask : ToolTask
/// </remarks>
internal static bool IsBuiltinToolRunningOnCoreClr => RuntimeHostInfo.IsCoreClrRuntime || IsSdkFrameworkToCoreBridgeTask;

/// <summary>
/// The full path to the built-in compiler managed assembly (<c>csc.dll</c>) or, when an apphost is
/// present, the apphost executable (<c>csc.exe</c>) - the file named by <see cref="ToolName"/> inside
/// <see cref="GetToolDirectory"/>.
/// </summary>
internal string PathToBuiltInTool => Path.Combine(GetToolDirectory(), ToolName);

/// <summary>
Expand Down Expand Up @@ -89,11 +98,11 @@ protected ManagedToolTask(ResourceManager resourceManager)
}

/// <summary>
/// Generate the arguments to pass directly to the buitin tool. These do not include
/// Generate the arguments to pass directly to the built-in tool. These do not include
/// arguments in the response file.
/// </summary>
/// <remarks>
/// This will be the same value whether the build occurs on .NET Core or .NET Framework.
/// This will be the same value whether the build occurs on .NET Core or .NET Framework.
/// </remarks>
internal string GenerateToolArguments()
{
Expand Down Expand Up @@ -128,7 +137,7 @@ protected sealed override string GenerateResponseFileCommands()
}

/// <summary>
/// Generate the arguments to pass directly to the buitin tool. These do not include
/// Generate the arguments to pass directly to the built-in tool. These do not include
/// arguments in the response file.
/// </summary>
/// <remarks>
Expand Down Expand Up @@ -164,6 +173,9 @@ protected sealed override string GenerateFullPathToTool()
return Path.Combine(ToolPath ?? "", ToolExe);
}

/// <summary>
/// Gets the base name used to derive the built-in tool's managed assembly and apphost names.
/// </summary>
protected abstract string ToolNameWithoutExtension { get; }

protected abstract void AddCommandLineCommands(CommandLineBuilderExtension commandLine);
Expand Down Expand Up @@ -224,6 +236,25 @@ protected static ITaskItem[] GenerateCommandLineArgsTaskItems(List<string> comma
return items;
}

// On-disk layout in the partially-AOT .NET SDK CLI: the runtime muxer (dotnet.exe) lives at the
// install root and loads <sdk_dir>\dotnet-aot.dll as a native library, so it is not a managed entry
// point running from the SDK directory - AppContext.BaseDirectory is the install root, not the
// SDK directory (see GetBuildTaskDirectory for how the SDK directory is recovered):
//
// C:\Program Files\dotnet\ <- install root; the muxer process runs from here
// ├─ dotnet.exe <- the muxer (the process / host)
// ├─ host\fxr\<ver>\hostfxr.dll
// └─ sdk\10.0.300\ <- sdk_dir (passed to dotnet_execute)
// ├─ dotnet.dll <- managed CLI fallback: Path.Join(sdk_dir, "dotnet.dll")
// ├─ dotnet-aot.dll <- loaded by the muxer as a native lib; build tasks linked in
// ├─ MSBuild.dll
// └─ Roslyn\bincore\csc.dll <- the compiler the task launches (at <sdk_dir>\Roslyn\bincore)

/// <summary>
/// Returns the folder that holds the built-in compiler: on .NET Core the <c>bincore</c> subfolder of
/// the build task directory; on .NET Framework the build task directory itself, except the SDK
/// framework-to-core bridge task which reaches the sibling <c>..\bincore</c>.
/// </summary>
internal static string GetToolDirectory()
{
var buildTaskDirectory = GetBuildTaskDirectory();
Expand All @@ -236,19 +267,52 @@ internal static string GetToolDirectory()
#endif
}

/// <summary>
/// Answers a single question - "where, on disk, is this build task assembly?" - and is the anchor
/// from which <see cref="GetToolDirectory"/> locates the sibling compiler.
/// <para>
/// In the common loose-file MSBuild deployment this is the directory of
/// <see cref="System.Reflection.Assembly.Location"/>. In the partially-AOT .NET SDK CLI that path is
/// empty, so the versioned SDK directory the AOT host publishes as the <c>Microsoft.DotNet.Sdk.Root</c>
/// <see cref="AppContext"/> value is read first and this task's <c>Roslyn</c> subfolder is appended.
/// See <see href="https://github.com/dotnet/sdk/pull/55110"/>.
/// </para>
/// </summary>
#if NET
// Assembly.Location is a loose-file-only API flagged by the single-file analyzer (IL3000): it
// returns an empty string under single-file / AOT. It cannot be annotated with
// [RequiresAssemblyFiles] because that attribute would have to flow onto the sealed ToolTask
// overrides (GenerateFullPathToTool, GenerateCommandLineCommands, ToolName, ExecuteTool), whose base
// declarations are not annotated - and that mismatch is itself an error (IL3003). Under single-file /
// AOT the Microsoft.DotNet.Sdk.Root AppContext value (checked first) supplies the directory, so the
// empty Assembly.Location is never used there and the IL3000 is suppressed.
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Assembly.Location is the loose-file path; single-file/AOT hosts have no on-disk location and instead read the Microsoft.DotNet.Sdk.Root AppContext value (checked first). [RequiresAssemblyFiles] cannot be used because it would flow onto the unannotated ToolTask overrides (IL3003).")]
#endif
internal static string GetBuildTaskDirectory()
Comment thread
JeremyKuhne marked this conversation as resolved.
{
var buildTask = typeof(ManagedToolTask).Assembly;
var buildTaskDirectory = Path.GetDirectoryName(buildTask.Location);
if (buildTaskDirectory is null)
#if NET
// Partially-AOT .NET SDK CLI host (the SDK's dotnet-aot.dll, loaded directly by the runtime
// muxer): Assembly.Location is empty and AppContext.BaseDirectory is the muxer's install root
// rather than the versioned SDK directory. The AOT bridge publishes the resolved SDK directory as
// the Microsoft.DotNet.Sdk.Root AppContext value for the assemblies compiled into it; out-of-repo
// code such as this build task reads it first. The build task ships in the "Roslyn" subfolder
// beneath the SDK directory. This name must match the constant published by the SDK's
// Microsoft.DotNet.Cli.Utils.SdkPaths.
if (AppContext.GetData("Microsoft.DotNet.Sdk.Root") is string { Length: > 0 } sdkDirectory)
{
return Path.Combine(sdkDirectory, "Roslyn");
}
Comment thread
JeremyKuhne marked this conversation as resolved.
Comment thread
JeremyKuhne marked this conversation as resolved.
#endif

// Loose-file MSBuild deployment (the common scenario on both .NET Framework and .NET Core, and
// the compiler toolset NuGet package): this assembly is on disk, so its own directory is the
// build task directory (for example <sdk>\Roslyn).
if (Path.GetDirectoryName(typeof(ManagedToolTask).Assembly.Location) is { Length: > 0 } buildTaskDirectory)
{
// This should not happen in supported product scenarios but could happen if
// a non-supported scenario tried to load our task (like AOT) and call
// through these members.
throw new InvalidOperationException("Unable to determine the location of the build task assembly.");
return buildTaskDirectory;
}

return buildTaskDirectory;
throw new InvalidOperationException("Unable to determine the location of the build task assembly.");
}

protected override bool ValidateParameters()
Expand Down
21 changes: 0 additions & 21 deletions src/Compilers/Core/MSBuildTask/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Security;

namespace Microsoft.CodeAnalysis.BuildTasks
Expand Down Expand Up @@ -162,25 +161,5 @@ internal static Exception GetLocalizedArgumentException(string errorString,
{
return new ArgumentException(string.Format(CultureInfo.CurrentCulture, errorString, args));
}

internal static string? TryGetAssemblyPath(Assembly assembly)
{
#if NETFRAMEWORK
if (assembly.GlobalAssemblyCache)
{
return null;
}

if (assembly.CodeBase is { } codebase)
{
var uri = new Uri(codebase);
return uri.IsFile ? uri.LocalPath : assembly.Location;
}

return null;
#else
return assembly.Location;
#endif
}
}
}
10 changes: 10 additions & 0 deletions src/Compilers/Core/MSBuildTask/Vbc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ protected override void AddResponseFileCommands(CommandLineBuilderExtension comm
{
commandLine.AppendSwitchIfNotNull("/sdkpath:", RuntimeEnvironment.GetRuntimeDirectory());

#if NETFRAMEWORK
// This branch only runs in the .NET Framework to .NET Core bridge task, which always
// executes on .NET Framework MSBuild where this assembly is deployed as loose files on
// disk. Assembly.Location is therefore valid here and unreachable from any single-file or
// native AOT host, so it does not need to satisfy the IL3000 single-file analyzer.
if (!NoConfig)
{
var rspFile = Path.Combine(Path.GetDirectoryName(typeof(ManagedCompiler).Assembly.Location)!, "vbc.rsp");
Expand All @@ -398,6 +403,11 @@ protected override void AddResponseFileCommands(CommandLineBuilderExtension comm
commandLine.AppendSwitchIfNotNull("@", rspFile);
}
}
#else
// IsSdkFrameworkToCoreBridgeTask is only ever true on .NET Framework, so the bridge-only
// response file handling above is never reached on .NET Core.
Debug.Fail("The SDK framework-to-core bridge task only runs on .NET Framework MSBuild.");
#endif
}

commandLine.AppendSwitchIfNotNull("/baseaddress:", this.GetBaseAddressInHex());
Expand Down
64 changes: 64 additions & 0 deletions src/Compilers/Core/MSBuildTaskTests/MSBuildManagedToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#if NET
using System;
#endif
using System.IO;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests;

// GetBuildTaskDirectory_UsesSdkRootAppContextData mutates the process-global "Microsoft.DotNet.Sdk.Root"
// AppContext value, which GetBuildTaskDirectory (and therefore GetToolDirectory) reads. A non-parallel
// collection gives this class exclusive execution so the mutation cannot leak into any other test.
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class ManagedToolTaskTestCollection
Comment thread
JeremyKuhne marked this conversation as resolved.
{
public const string Name = nameof(ManagedToolTaskTestCollection);
}

[Collection(ManagedToolTaskTestCollection.Name)]
public sealed class MSBuildManagedToolTests
{
[Fact]
Expand All @@ -32,4 +45,55 @@ public void IsBuiltinToolRunningOnCoreClr()
{
Assert.Equal(RuntimeHostInfo.IsCoreClrRuntime, ManagedToolTask.IsBuiltinToolRunningOnCoreClr);
}

#if NET
[Fact]
public void GetBuildTaskDirectory_UsesSdkRootAppContextData()
{
// In the partially-AOT SDK CLI the host publishes the versioned SDK directory as this AppContext
// value; GetBuildTaskDirectory reads it and appends the "Roslyn" subfolder (this branch is .NET only).
// The value is not set in a normal test host - verify that first, then restore it in the finally.
const string sdkRootDataName = "Microsoft.DotNet.Sdk.Root";
Assert.Null(AppContext.GetData(sdkRootDataName));

var sdkDirectory = Path.Combine(Path.GetTempPath(), nameof(GetBuildTaskDirectory_UsesSdkRootAppContextData));
AppContext.SetData(sdkRootDataName, sdkDirectory);
try
{
var expectedBuildTaskDirectory = Path.Combine(sdkDirectory, "Roslyn");
var expectedToolDirectory = Path.Combine(expectedBuildTaskDirectory, "bincore");

// Derive a task so the resolved folders can be observed end to end.
var task = new TestManagedToolTask();

Assert.Equal(expectedBuildTaskDirectory, ManagedToolTask.GetBuildTaskDirectory());
Assert.Equal(expectedToolDirectory, ManagedToolTask.GetToolDirectory());
Assert.Equal(Path.Combine(expectedToolDirectory, "testtool.dll"), task.PathToBuiltInTool);
}
finally
{
AppContext.SetData(sdkRootDataName, null);
}

Assert.Null(AppContext.GetData(sdkRootDataName));
}

private sealed class TestManagedToolTask : ManagedToolTask
{
public TestManagedToolTask()
: base(ErrorString.ResourceManager)
{
}

protected override string ToolNameWithoutExtension => "testtool";

protected override void AddCommandLineCommands(CommandLineBuilderExtension commandLine)
{
}

protected override void AddResponseFileCommands(CommandLineBuilderExtension commandLine)
{
}
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal static class ReflectionUtilities
{
private static readonly Type Missing = typeof(void);

#if NET
[RequiresUnreferencedCode("Type.GetType with a non-constant type name cannot be statically analyzed for trimming.")]
#endif
public static Type? TryGetType(string assemblyQualifiedName)
{
try
Expand All @@ -28,6 +31,9 @@ internal static class ReflectionUtilities
}
}

#if NET
[RequiresUnreferencedCode("Resolves a type by its assembly-qualified name, which cannot be statically analyzed for trimming.")]
#endif
public static Type? TryGetType([NotNull] ref Type? lazyType, string assemblyQualifiedName)
{
if (lazyType == null)
Expand All @@ -42,6 +48,9 @@ internal static class ReflectionUtilities
/// Find a <see cref="Type"/> instance by first probing the contract name and then the name as it
/// would exist in mscorlib. This helps satisfy both the CoreCLR and Desktop scenarios.
/// </summary>
#if NET
[RequiresUnreferencedCode("Resolves a type by name, which cannot be statically analyzed for trimming.")]
#endif
public static Type? GetTypeFromEither(string contractName, string desktopName)
{
var type = TryGetType(contractName);
Expand All @@ -54,6 +63,9 @@ internal static class ReflectionUtilities
return type;
}

#if NET
[RequiresUnreferencedCode("Resolves a type by name, which cannot be statically analyzed for trimming.")]
#endif
public static Type? GetTypeFromEither([NotNull] ref Type? lazyType, string contractName, string desktopName)
{
if (lazyType == null)
Expand Down Expand Up @@ -94,12 +106,20 @@ internal static class ReflectionUtilities
return null;
}

internal static MethodInfo? GetDeclaredMethod(this TypeInfo typeInfo, string name, params Type[] paramTypes)
internal static MethodInfo? GetDeclaredMethod(
#if NET
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)]
#endif
this TypeInfo typeInfo, string name, params Type[] paramTypes)
{
return FindItem(typeInfo.GetDeclaredMethods(name), paramTypes);
}

internal static ConstructorInfo? GetDeclaredConstructor(this TypeInfo typeInfo, params Type[] paramTypes)
internal static ConstructorInfo? GetDeclaredConstructor(
#if NET
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
#endif
this TypeInfo typeInfo, params Type[] paramTypes)
{
return FindItem(typeInfo.DeclaredConstructors, paramTypes);
}
Expand Down
5 changes: 4 additions & 1 deletion src/Dependencies/Contracts/ErrorReporting/FatalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ public static void OverwriteHandler(ErrorReporterHandler? value)
/// Copies the handler in this instance to the linked copy of this type in this other assembly.
/// </summary>
/// <remarks>
/// This file is in linked into multiple layers, but we want to ensure that all layers have the same copy.
/// This file is linked into multiple layers, but we want to ensure that all layers have the same copy.
/// This lets us copy the handler in this instance into the same in another instance.
/// </remarks>
#if NET
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Reflects over the FatalError type in the target assembly to copy error handlers, which cannot be statically analyzed for trimming.")]
#endif
public static void CopyHandlersTo(Assembly assembly)
{
copyHandlerTo(assembly, s_handler, nameof(s_handler));
Expand Down