Skip to content

Commit

Permalink
Find assemblies by path from RoslynCodeTaskFactory Fixes #5106 (#7467)
Browse files Browse the repository at this point in the history
Fixes #5106

Context
When using an inline task with CodeTaskFactory (Framework-specific), it automatically adds resolving information to AppDomain.CurrentDomain if necessary. That didn't happen with RoslynCodeTaskFactory. This adds it.

Changes Made
Added referenced assemblies for inline tasks using RoslynCodeTaskFactory to the current AppDomain/AssemblyLoadContext.

Testing
The repro no longer repros, and I added two unit tests.
  • Loading branch information
Forgind authored Apr 21, 2022
1 parent 88bdcd6 commit 3ade642
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ProjectReference Include="..\Utilities\Microsoft.Build.Utilities.csproj" />
<ProjectReference Include="..\Xunit.NetCore.Extensions\Xunit.NetCore.Extensions.csproj" />
<ProjectReference Include="..\Samples\PortableTask\PortableTask.csproj" ReferenceOutputAssembly="false" Private="false" />
<ProjectReference Include="..\Samples\Dependency\Dependency.csproj" ReferenceOutputAssembly="false" Private="false" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
Expand Down
110 changes: 110 additions & 0 deletions src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
using Microsoft.Build.Utilities;
#if NETFRAMEWORK
using Microsoft.IO;
#else
using System.IO;
#endif
using Shouldly;
using Xunit;

Expand All @@ -19,6 +27,108 @@ public class RoslynCodeTaskFactory_Tests
{
private const string TaskName = "MyInlineTask";

[Fact]
public void InlineTaskWithAssemblyPlatformAgnostic()
{
using (TestEnvironment env = TestEnvironment.Create())
{
TransientTestFolder folder = env.CreateFolder(createFolder: true);
string location = Assembly.GetExecutingAssembly().Location;
TransientTestFile inlineTask = env.CreateFile(folder, "5106.proj", @$"
<Project>
<UsingTask TaskName=""MyInlineTask"" TaskFactory=""RoslynCodeTaskFactory"" AssemblyFile=""$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"">
<Task>
<Reference Include=""" + Path.Combine(Path.GetDirectoryName(location), "..", "..", "..", "Samples", "Dependency",
#if DEBUG
"Debug"
#else
"Release"
#endif
, "net472", "Dependency.dll") + @""" />
<Using Namespace=""Dependency"" />
<Code Type=""Fragment"" Language=""cs"" >
<![CDATA[
Log.LogError(Alpha.GetString());
]]>
</Code>
</Task>
</UsingTask>
<Target Name=""ToRun"">
<MyInlineTask/>
</Target>
</Project>
");
string output = RunnerUtilities.ExecMSBuild(inlineTask.Path, out bool success);
success.ShouldBeTrue(output);
output.ShouldContain("Alpha.GetString");
}
}

[Fact]
[SkipOnPlatform(TestPlatforms.AnyUnix, ".NETFramework 4.0 isn't on unix machines.")]
public void InlineTaskWithAssembly()
{
using (TestEnvironment env = TestEnvironment.Create())
{
TransientTestFolder folder = env.CreateFolder(createFolder: true);
TransientTestFile assemblyProj = env.CreateFile(folder, "5106.csproj", @$"
<Project DefaultTargets=""Build"">
<PropertyGroup>
<TargetFrameworkVersion>{MSBuildConstants.StandardTestTargetFrameworkVersion}</TargetFrameworkVersion>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include=""System""/>
<Compile Include=""Class1.cs""/>
</ItemGroup>
<Import Project=""$(MSBuildBinPath)\Microsoft.CSharp.targets"" />
</Project>
");
TransientTestFile csFile = env.CreateFile(folder, "Class1.cs", @"
using System;
namespace _5106 {
public class Class1 {
public static string ToPrint() {
return ""Hello!"";
}
}
}
");
string output = RunnerUtilities.ExecMSBuild(assemblyProj.Path + $" /p:OutDir={Path.Combine(folder.Path, "subFolder")} /restore", out bool success);
success.ShouldBeTrue(output);

TransientTestFile inlineTask = env.CreateFile(folder, "5106.proj", @$"
<Project>
<UsingTask TaskName=""MyInlineTask"" TaskFactory=""RoslynCodeTaskFactory"" AssemblyFile=""$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"">
<Task>
<Reference Include=""{Path.Combine(folder.Path, "subFolder", "5106.dll")}"" />
<Reference Include=""netstandard"" />
<Using Namespace=""_5106"" />
<Code Type=""Fragment"" Language=""cs"" >
<![CDATA[
Log.LogError(Class1.ToPrint());
]]>
</Code>
</Task>
</UsingTask>
<Target Name=""ToRun"">
<MyInlineTask/>
</Target>
</Project>
");
output = RunnerUtilities.ExecMSBuild(inlineTask.Path, out success);
success.ShouldBeTrue();
output.ShouldContain("Hello!");
}
}

[Fact]
public void RoslynCodeTaskFactory_ReuseCompilation()
{
Expand Down
44 changes: 42 additions & 2 deletions src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory
/// </summary>
private TaskLoggingHelper _log;

/// <summary>
/// Stores functions that were added to the current app domain. Should be removed once we're finished.
/// </summary>
private ResolveEventHandler handlerAddedToAppDomain = null;

/// <summary>
/// Stores the parameters parsed in the &lt;UsingTask /&gt;.
/// </summary>
Expand All @@ -123,6 +128,10 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory
/// <inheritdoc cref="ITaskFactory.CleanupTask(ITask)"/>
public void CleanupTask(ITask task)
{
if (handlerAddedToAppDomain is not null)
{
AppDomain.CurrentDomain.AssemblyResolve -= handlerAddedToAppDomain;
}
}

/// <inheritdoc cref="ITaskFactory.CreateTask(IBuildEngine)"/>
Expand Down Expand Up @@ -515,7 +524,7 @@ internal static bool TryLoadTaskBody(TaskLoggingHelper log, string taskName, str
/// Perhaps in the future this could be more powerful by using NuGet to resolve assemblies but we think
/// that is too complicated for a simple in-line task. If users have more complex requirements, they
/// can compile their own task library.</remarks>
internal static bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTaskFactoryTaskInfo taskInfo, out ITaskItem[] items)
internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTaskFactoryTaskInfo taskInfo, out ITaskItem[] items)
{
// Store the list of resolved assemblies because a user can specify a short name or a full path
ISet<string> resolvedAssemblyReferences = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand All @@ -532,14 +541,18 @@ internal static bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynC
references = references.Union(DefaultReferences[taskInfo.CodeLanguage]);
}

List<string> directoriesToAddToAppDomain = new();

// Loop through the user specified references as well as the default references
foreach (string reference in references)
{
// The user specified a full path to an assembly, so there is no need to resolve
if (FileSystems.Default.FileExists(reference))
{
// The path could be relative like ..\Assembly.dll so we need to get the full path
resolvedAssemblyReferences.Add(Path.GetFullPath(reference));
string fullPath = Path.GetFullPath(reference);
directoriesToAddToAppDomain.Add(Path.GetDirectoryName(fullPath));
resolvedAssemblyReferences.Add(fullPath);
continue;
}

Expand Down Expand Up @@ -572,7 +585,34 @@ internal static bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynC
// Transform the list of resolved assemblies to TaskItems if they were all resolved
items = hasInvalidReference ? null : resolvedAssemblyReferences.Select(i => (ITaskItem)new TaskItem(i)).ToArray();

handlerAddedToAppDomain = (_, eventArgs) => TryLoadAssembly(directoriesToAddToAppDomain, new AssemblyName(eventArgs.Name));
AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain;

return !hasInvalidReference;

static Assembly TryLoadAssembly(List<string> directories, AssemblyName name)
{
foreach (string directory in directories)
{
string path;
if (!string.IsNullOrEmpty(name.CultureName))
{
path = Path.Combine(directory, name.CultureName, name.Name + ".dll");
if (File.Exists(path))
{
return Assembly.LoadFrom(path);
}
}

path = Path.Combine(directory, name.Name + ".dll");
if (File.Exists(path))
{
return Assembly.LoadFrom(path);
}
}

return null;
}
}

private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null)
Expand Down

0 comments on commit 3ade642

Please sign in to comment.