diff --git a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs index 20b7b95157e..8d947fda184 100644 --- a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs +++ b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs @@ -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" + @@ -37,6 +44,9 @@ public class GenAPITask : Task private SyntaxWriterType _syntaxWriterType; private DocIdKinds _docIdKinds = Cci.Writers.DocIdKinds.All; + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + /// /// Path for an specific assembly or a directory to get all assemblies. /// @@ -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) { @@ -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; @@ -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); } /// diff --git a/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs b/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs index 64f6af191f7..e20693cd028 100644 --- a/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs +++ b/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs @@ -14,8 +14,12 @@ namespace Microsoft.DotNet.GenFacades; /// /// Rewrites an Assembly's references to be version 0.0.0.0. /// -public class ClearAssemblyReferenceVersions : Task +[MSBuildMultiThreadableTask] +public class ClearAssemblyReferenceVersions : Task, IMultiThreadableTask { + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + /// /// Assembly to rewrite. /// @@ -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)) diff --git a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs index c9a7847a057..9c673268fd7 100644 --- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs +++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs @@ -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 + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + [Required] public ITaskItem[] ReferencePaths { get; set; } @@ -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, @@ -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(); + } } diff --git a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs index dfe1da24f83..67c699291b4 100644 --- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs +++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs @@ -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, - string outputSourcePath, + AbsolutePath outputSourcePath, TaskLoggingHelper logger, bool ignoreMissingTypes = false, string[] ignoreMissingTypesList = null, @@ -32,9 +32,12 @@ public static bool Execute( IEnumerable 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()) { @@ -92,7 +95,7 @@ private static Dictionary ParseSeedTypePreferences(ITaskItem[] p return dictionary; } - private static IEnumerable GetPublicVisibleTypes(string assembly, bool includeTypeForwards = false) + private static IEnumerable GetPublicVisibleTypes(AbsolutePath assembly, bool includeTypeForwards = false) { using (var peReader = new PEReader(new FileStream(assembly, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read))) { @@ -139,15 +142,15 @@ private static bool IsPublic(TypeDefinition typeDefination) return (typeDefination.Attributes & TypeAttributes.Public) != 0; } - private static IReadOnlyDictionary> GenerateTypeTable(IEnumerable seedAssemblies) + private static IReadOnlyDictionary> GenerateTypeTable(IEnumerable seedAssemblies) { var typeTable = new Dictionary>(); - foreach(string assembly in seedAssemblies) + foreach(AbsolutePath assembly in seedAssemblies) { IEnumerable types = GetPublicVisibleTypes(assembly); foreach (string type in types) { - AddTypeToTable(typeTable, type, Path.GetFileName(assembly)); + AddTypeToTable(typeTable, type, Path.GetFileName(assembly.Value)); } } return typeTable; diff --git a/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj b/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj index 334ddeefb28..f8f12397d3b 100644 --- a/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj +++ b/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs index 57bea594708..550631929a0 100644 --- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs +++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs @@ -16,8 +16,26 @@ namespace Microsoft.DotNet.GenFacades; /// /// The class generates an NotSupportedAssembly from the reference sources. /// -public class NotSupportedAssemblyGenerator : 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 NotSupportedAssemblyGenerator : RoslynBuildTask, IMultiThreadableTask { +#pragma warning restore MSBuildTask0013 + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + [Required] public ITaskItem[] SourceFiles { get; set; } @@ -44,27 +62,32 @@ public override bool ExecuteCore() private void GenerateNotSupportedAssemblyFiles(IEnumerable 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; @@ -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) { @@ -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); } } diff --git a/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs b/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs index c4b7e9703cc..7a658b6c901 100644 --- a/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs +++ b/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs @@ -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; @@ -14,7 +15,7 @@ internal class SourceGenerator private readonly IReadOnlyDictionary _seedTypePreferences; private readonly IEnumerable _referenceTypes; private readonly IReadOnlyDictionary> _seedTypes; - private readonly string _outputSourcePath; + private readonly AbsolutePath _outputSourcePath; private readonly HashSet _ignoreMissingTypesList = new HashSet(); private readonly TaskLoggingHelper _logger; @@ -22,7 +23,7 @@ public SourceGenerator( IEnumerable referenceTypes, IReadOnlyDictionary> seedTypes, IReadOnlyDictionary seedTypePreferences, - string outputSourcePath, + AbsolutePath outputSourcePath, string[] ignoreMissingTypesList, TaskLoggingHelper logger ) @@ -38,7 +39,7 @@ TaskLoggingHelper logger } public bool GenerateSource( - IEnumerable compileFiles, + IEnumerable compileFiles, IEnumerable constants, string langVersion, bool ignoreMissingTypes) diff --git a/src/Microsoft.DotNet.GenFacades/TypeParser.cs b/src/Microsoft.DotNet.GenFacades/TypeParser.cs index 86186fa6a25..f575b1d1cb3 100644 --- a/src/Microsoft.DotNet.GenFacades/TypeParser.cs +++ b/src/Microsoft.DotNet.GenFacades/TypeParser.cs @@ -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; @@ -13,7 +14,7 @@ namespace Microsoft.DotNet.GenFacades; internal class TypeParser { - public static HashSet GetAllPublicTypes(IEnumerable files, IEnumerable constants, string langVersion) + public static HashSet GetAllPublicTypes(IEnumerable files, IEnumerable constants, string langVersion) { HashSet types = new HashSet(); @@ -130,13 +131,13 @@ private static string GetNamespaceName(NamespaceDeclarationSyntax namespaceSynta return namespaceSyntax.Name.ToFullString().Trim(); } - private static IEnumerable GetSourceTrees(IEnumerable sourceFiles, IEnumerable constants, LanguageVersion languageVersion) + private static IEnumerable GetSourceTrees(IEnumerable sourceFiles, IEnumerable constants, LanguageVersion languageVersion) { CSharpParseOptions options = new CSharpParseOptions(languageVersion: languageVersion, preprocessorSymbols: constants); List result = new List(); - foreach (string sourceFile in sourceFiles) + foreach (AbsolutePath sourceFile in sourceFiles) { - if (string.IsNullOrEmpty(sourceFile)) + if (string.IsNullOrEmpty(sourceFile.Value)) { continue; }