Skip to content
Draft
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
26 changes: 19 additions & 7 deletions src/Microsoft.DotNet.GenAPI/GenAPITask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@

namespace Microsoft.DotNet.GenAPI;

public class GenAPITask : Task
// Deliberately not marked multithreadable: HostEnvironment resolves the raw LibPath and
// Assembly values below through Environment.ExpandEnvironmentVariables plus Directory.Exists/
// File.Exists (Microsoft.Cci.Extensions/HostEnvironment.cs:719-740), so relative inputs and
// per-project variables would bind to process-wide state in a shared node. Migrating requires
// expanding and resolving those paths through TaskEnvironment before they enter HostEnvironment.
#pragma warning disable MSBuildTask0013 // Interface without the attribute is deliberate; see the comment above.
public class GenAPITask : Task, IMultiThreadableTask
{
#pragma warning restore MSBuildTask0013
private const string InternalsVisibleTypeName = "System.Runtime.CompilerServices.InternalsVisibleToAttribute";
private const string DefaultFileHeader =
"//------------------------------------------------------------------------------\r\n" +
Expand All @@ -37,6 +44,9 @@ public class GenAPITask : Task
private SyntaxWriterType _syntaxWriterType;
private DocIdKinds _docIdKinds = Cci.Writers.DocIdKinds.All;

/// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

/// <summary>
/// Path for an specific assembly or a directory to get all assemblies.
/// </summary>
Expand Down Expand Up @@ -197,7 +207,7 @@ public override bool Execute()
}

string headerText = GetHeaderText(HeaderFile, _writerType, _syntaxWriterType);
bool loopPerAssembly = Directory.Exists(OutputPath);
bool loopPerAssembly = !string.IsNullOrEmpty(OutputPath) && Directory.Exists(TaskEnvironment.GetAbsolutePath(OutputPath));

if (loopPerAssembly)
{
Expand Down Expand Up @@ -260,11 +270,11 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}

private static string GetHeaderText(string headerFile, WriterType writerType, SyntaxWriterType syntaxWriterType)
private string GetHeaderText(string headerFile, WriterType writerType, SyntaxWriterType syntaxWriterType)
{
if (!string.IsNullOrEmpty(headerFile))
{
return File.ReadAllText(headerFile);
return File.ReadAllText(TaskEnvironment.GetAbsolutePath(headerFile));
}

string defaultHeader = string.Empty;
Expand All @@ -286,12 +296,14 @@ private TextWriter GetOutput(string outFilePath, string filename = "")
if (string.IsNullOrWhiteSpace(outFilePath))
return new LogTextWriter(Log);

if (Directory.Exists(outFilePath) && !string.IsNullOrEmpty(filename))
AbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(outFilePath);

if (Directory.Exists(outputPath) && !string.IsNullOrEmpty(filename))
{
return File.CreateText(Path.Combine(outFilePath, filename));
return File.CreateText(Path.Combine(outputPath, filename));
}

return File.CreateText(outFilePath);
return File.CreateText(outputPath);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ namespace Microsoft.DotNet.GenFacades;
/// <summary>
/// Rewrites an Assembly's references to be version 0.0.0.0.
/// </summary>
public class ClearAssemblyReferenceVersions : Task
[MSBuildMultiThreadableTask]
public class ClearAssemblyReferenceVersions : Task, IMultiThreadableTask
{
/// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

/// <summary>
/// Assembly to rewrite.
/// </summary>
Expand All @@ -26,7 +30,7 @@ public override bool Execute()
{
try
{
using (FileStream stream = File.Open(Assembly, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
using (FileStream stream = File.Open(TaskEnvironment.GetAbsolutePath(Assembly), FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
using (PEReader peReader = new PEReader(stream))
{
using (BinaryWriter writer = new BinaryWriter(stream))
Expand Down
41 changes: 36 additions & 5 deletions src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@

namespace Microsoft.DotNet.GenFacades;

public class GenPartialFacadeSource : RoslynBuildTask
// TODO: Not opted into multithreading. RoslynBuildTask.Execute subscribes every instance to the
// process-wide AssemblyLoadContext.Resolving event, so with differing RoslynAssembliesPath values
// one instance can satisfy another instance's resolution. The TaskEnvironment below is still used
// for path resolution. Tracked by https://github.com/dotnet/arcade/issues/17378.
//
// Implementing IMultiThreadableTask without the attribute is deliberate. Routing is decided by
// the attribute alone (TaskRouter.NeedsTaskHostInMultiThreadedMode); it cannot key off the
// interface, because ToolTask implements it and that would opt in every ToolTask-derived task in
// the ecosystem. The interface only causes TaskEnvironment to be injected. Do not remove it to
// "make this safe" - that would revert the path resolution below to the process current
// directory while leaving the task exactly as unsafe as it is now.
#pragma warning disable MSBuildTask0013 // Interface without the attribute is deliberate; see the comment above.
public class GenPartialFacadeSource : RoslynBuildTask, IMultiThreadableTask
{
#pragma warning restore MSBuildTask0013
/// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

[Required]
public ITaskItem[] ReferencePaths { get; set; }

Expand Down Expand Up @@ -39,13 +55,18 @@ public override bool ExecuteCore()
bool result = true;
try
{
AbsolutePath[] referencePaths = GetAbsolutePaths(ReferencePaths);
AbsolutePath referenceAssembly = TaskEnvironment.GetAbsolutePath(ReferenceAssembly);
AbsolutePath[] compileFiles = GetAbsolutePaths(CompileFiles);
AbsolutePath outputSourcePath = TaskEnvironment.GetAbsolutePath(OutputSourcePath);

result = GenPartialFacadeSourceGenerator.Execute(
ReferencePaths?.Select(item => item.ItemSpec).ToArray(),
ReferenceAssembly,
CompileFiles?.Select(item => item.ItemSpec).ToArray(),
referencePaths,
referenceAssembly,
compileFiles,
DefineConstants,
LangVersion,
OutputSourcePath,
outputSourcePath,
Log,
IgnoreMissingTypes,
IgnoreMissingTypesList,
Expand All @@ -59,4 +80,14 @@ public override bool ExecuteCore()

return result && !Log.HasLoggedErrors;
}

private AbsolutePath[] GetAbsolutePaths(ITaskItem[] items)
{
// Empty item specs were previously skipped by TypeParser.GetSourceTrees rather than
// treated as an error, and GetAbsolutePath throws on an empty path, so filter them here.
return items?
.Where(item => !string.IsNullOrEmpty(item.ItemSpec))
.Select(item => TaskEnvironment.GetAbsolutePath(item.ItemSpec))
.ToArray();
}
}
27 changes: 15 additions & 12 deletions src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@

namespace Microsoft.DotNet.GenFacades;

public class GenPartialFacadeSourceGenerator
internal class GenPartialFacadeSourceGenerator
{
public static bool Execute(
string[] seeds,
string contractAssembly,
string[] compileFiles,
AbsolutePath[] seeds,
AbsolutePath contractAssembly,
AbsolutePath[] compileFiles,
string defineConstants,
string langVersion,
Comment thread
Copilot marked this conversation as resolved.
string outputSourcePath,
AbsolutePath outputSourcePath,
TaskLoggingHelper logger,
bool ignoreMissingTypes = false,
string[] ignoreMissingTypesList = null,
Expand All @@ -32,9 +32,12 @@ public static bool Execute(

IEnumerable<string> referenceTypes = GetPublicVisibleTypes(contractAssembly, includeTypeForwards: true);

// Normalizing and Removing Relative Segments from the seed paths.
string[] distinctSeeds = seeds.Select(seed => Path.GetFullPath(seed)).Distinct().ToArray();
string[] seedNames = distinctSeeds.Select(seed => Path.GetFileName(seed)).ToArray();
// Normalizing and Removing Relative Segments from the seed paths. GetAbsolutePath only
// anchors, so the canonical form is what makes Distinct below collapse two spellings of
// the same seed; otherwise the duplicate-name check underneath reports them as two
// different versions of one assembly.
AbsolutePath[] distinctSeeds = seeds.Select(seed => seed.GetCanonicalForm()).Distinct().ToArray();
string[] seedNames = distinctSeeds.Select(seed => Path.GetFileName(seed.Value)).ToArray();

if (distinctSeeds.Count() != seedNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count())
{
Expand Down Expand Up @@ -92,7 +95,7 @@ private static Dictionary<string, string> ParseSeedTypePreferences(ITaskItem[] p
return dictionary;
}

private static IEnumerable<string> GetPublicVisibleTypes(string assembly, bool includeTypeForwards = false)
private static IEnumerable<string> GetPublicVisibleTypes(AbsolutePath assembly, bool includeTypeForwards = false)
{
using (var peReader = new PEReader(new FileStream(assembly, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read)))
{
Expand Down Expand Up @@ -139,15 +142,15 @@ private static bool IsPublic(TypeDefinition typeDefination)
return (typeDefination.Attributes & TypeAttributes.Public) != 0;
}

private static IReadOnlyDictionary<string, IList<string>> GenerateTypeTable(IEnumerable<string> seedAssemblies)
private static IReadOnlyDictionary<string, IList<string>> GenerateTypeTable(IEnumerable<AbsolutePath> seedAssemblies)
{
var typeTable = new Dictionary<string, IList<string>>();
foreach(string assembly in seedAssemblies)
foreach(AbsolutePath assembly in seedAssemblies)
{
IEnumerable<string> types = GetPublicVisibleTypes(assembly);
foreach (string type in types)
{
AddTypeToTable(typeTable, type, Path.GetFileName(assembly));
AddTypeToTable(typeTable, type, Path.GetFileName(assembly.Value));
}
}
return typeTable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<IsBuildTaskProject>true</IsBuildTaskProject>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Common\Internal\AbsolutePathExtensions.cs" Link="Internal\AbsolutePathExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Tasks.Core" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
Expand Down
39 changes: 31 additions & 8 deletions src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,26 @@ namespace Microsoft.DotNet.GenFacades;
/// <summary>
/// The class generates an NotSupportedAssembly from the reference sources.
/// </summary>
public class NotSupportedAssemblyGenerator : RoslynBuildTask
/// <remarks>
/// TODO: Not opted into multithreading. RoslynBuildTask.Execute subscribes every instance to the
/// process-wide AssemblyLoadContext.Resolving event, so with differing RoslynAssembliesPath values
/// one instance can satisfy another instance's resolution. The TaskEnvironment below is still used
/// for path resolution. Tracked by https://github.com/dotnet/arcade/issues/17378.
///
/// Implementing IMultiThreadableTask without the attribute is deliberate. Routing is decided by
/// the attribute alone (TaskRouter.NeedsTaskHostInMultiThreadedMode); it cannot key off the
/// interface, because ToolTask implements it and that would opt in every ToolTask-derived task in
/// the ecosystem. The interface only causes TaskEnvironment to be injected. Do not remove it to
/// "make this safe" - that would revert the path resolution below to the process current
/// directory while leaving the task exactly as unsafe as it is now.
/// </remarks>
#pragma warning disable MSBuildTask0013 // Interface without the attribute is deliberate; see the comment above.
public class NotSupportedAssemblyGenerator : RoslynBuildTask, IMultiThreadableTask
{
#pragma warning restore MSBuildTask0013
/// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

[Required]
public ITaskItem[] SourceFiles { get; set; }

Expand All @@ -44,27 +62,32 @@ public override bool ExecuteCore()
private void GenerateNotSupportedAssemblyFiles(IEnumerable<ITaskItem> sourceFiles)
{
string[] apiExclusions = null;
if (!string.IsNullOrEmpty(ApiExclusionListPath) && File.Exists(ApiExclusionListPath))
if (!string.IsNullOrEmpty(ApiExclusionListPath))
{
apiExclusions = File.ReadAllLines(ApiExclusionListPath);
AbsolutePath apiExclusionListPath = TaskEnvironment.GetAbsolutePath(ApiExclusionListPath);
if (File.Exists(apiExclusionListPath))
{
apiExclusions = File.ReadAllLines(apiExclusionListPath);
}
}

foreach (ITaskItem item in sourceFiles)
{
string sourceFile = item.ItemSpec;
string outputPath = item.GetMetadata("OutputPath");
AbsolutePath sourceFilePath = TaskEnvironment.GetAbsolutePath(sourceFile);

if (!File.Exists(sourceFile))
if (!File.Exists(sourceFilePath))
{
Log.LogError($"File {sourceFile} was not found.");
continue;
}

GenerateNotSupportedAssemblyForSourceFile(sourceFile, outputPath, apiExclusions);
GenerateNotSupportedAssemblyForSourceFile(sourceFilePath, outputPath, apiExclusions);
}
}

private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string outputPath, string[] apiExclusions)
private void GenerateNotSupportedAssemblyForSourceFile(AbsolutePath sourceFilePath, string outputPath, string[] apiExclusions)
{
SyntaxTree syntaxTree;

Expand All @@ -76,7 +99,7 @@ private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string
Log.LogError($"Invalid LangVersion value '{LangVersion}'");
return;
}
syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFile), new CSharpParseOptions(languageVersion));
syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFilePath), new CSharpParseOptions(languageVersion));
}
catch(Exception ex)
{
Expand All @@ -87,7 +110,7 @@ private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string
var rewriter = new NotSupportedAssemblyRewriter(Message, apiExclusions);
SyntaxNode root = rewriter.Visit(syntaxTree.GetRoot());
string text = root.GetText().ToString();
File.WriteAllText(outputPath, text);
File.WriteAllText(TaskEnvironment.GetAbsolutePath(outputPath), text);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/Microsoft.DotNet.GenFacades/SourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Collections.Generic;
using System.IO;
Expand All @@ -14,15 +15,15 @@ internal class SourceGenerator
private readonly IReadOnlyDictionary<string, string> _seedTypePreferences;
private readonly IEnumerable<string> _referenceTypes;
private readonly IReadOnlyDictionary<string, IList<string>> _seedTypes;
private readonly string _outputSourcePath;
private readonly AbsolutePath _outputSourcePath;
private readonly HashSet<string> _ignoreMissingTypesList = new HashSet<string>();
private readonly TaskLoggingHelper _logger;

public SourceGenerator(
IEnumerable<string> referenceTypes,
IReadOnlyDictionary<string, IList<string>> seedTypes,
IReadOnlyDictionary<string, string> seedTypePreferences,
string outputSourcePath,
AbsolutePath outputSourcePath,
string[] ignoreMissingTypesList,
TaskLoggingHelper logger
)
Expand All @@ -38,7 +39,7 @@ TaskLoggingHelper logger
}

public bool GenerateSource(
IEnumerable<string> compileFiles,
IEnumerable<AbsolutePath> compileFiles,
IEnumerable<string> constants,
string langVersion,
bool ignoreMissingTypes)
Expand Down
9 changes: 5 additions & 4 deletions src/Microsoft.DotNet.GenFacades/TypeParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -13,7 +14,7 @@ namespace Microsoft.DotNet.GenFacades;

internal class TypeParser
{
public static HashSet<string> GetAllPublicTypes(IEnumerable<string> files, IEnumerable<string> constants, string langVersion)
public static HashSet<string> GetAllPublicTypes(IEnumerable<AbsolutePath> files, IEnumerable<string> constants, string langVersion)
{
HashSet<string> types = new HashSet<string>();

Expand Down Expand Up @@ -130,13 +131,13 @@ private static string GetNamespaceName(NamespaceDeclarationSyntax namespaceSynta
return namespaceSyntax.Name.ToFullString().Trim();
}

private static IEnumerable<SyntaxTree> GetSourceTrees(IEnumerable<string> sourceFiles, IEnumerable<string> constants, LanguageVersion languageVersion)
private static IEnumerable<SyntaxTree> GetSourceTrees(IEnumerable<AbsolutePath> sourceFiles, IEnumerable<string> constants, LanguageVersion languageVersion)
{
CSharpParseOptions options = new CSharpParseOptions(languageVersion: languageVersion, preprocessorSymbols: constants);
List<SyntaxTree> result = new List<SyntaxTree>();
foreach (string sourceFile in sourceFiles)
foreach (AbsolutePath sourceFile in sourceFiles)
{
if (string.IsNullOrEmpty(sourceFile))
if (string.IsNullOrEmpty(sourceFile.Value))
{
continue;
}
Expand Down
Loading