diff --git a/ref/Microsoft.Build/net/Microsoft.Build.cs b/ref/Microsoft.Build/net/Microsoft.Build.cs index fb5b4b8da5e..9f42d2be544 100644 --- a/ref/Microsoft.Build/net/Microsoft.Build.cs +++ b/ref/Microsoft.Build/net/Microsoft.Build.cs @@ -498,6 +498,7 @@ namespace Microsoft.Build.Definition public partial class ProjectOptions { public ProjectOptions() { } + public Microsoft.Build.FileSystem.IDirectoryCacheFactory DirectoryCacheFactory { get { throw null; } set { } } public Microsoft.Build.Evaluation.Context.EvaluationContext EvaluationContext { get { throw null; } set { } } public System.Collections.Generic.IDictionary GlobalProperties { get { throw null; } set { } } public Microsoft.Build.Evaluation.ProjectLoadSettings LoadSettings { get { throw null; } set { } } @@ -1504,6 +1505,19 @@ public ProxyTargets(System.Collections.Generic.IReadOnlyDictionary fileName); + public delegate TResult FindTransform(ref System.ReadOnlySpan fileName); + public partial interface IDirectoryCache + { + bool DirectoryExists(string path); + System.Collections.Generic.IEnumerable EnumerateDirectories(string path, string pattern, Microsoft.Build.FileSystem.FindPredicate predicate, Microsoft.Build.FileSystem.FindTransform transform); + System.Collections.Generic.IEnumerable EnumerateFiles(string path, string pattern, Microsoft.Build.FileSystem.FindPredicate predicate, Microsoft.Build.FileSystem.FindTransform transform); + bool FileExists(string path); + } + public partial interface IDirectoryCacheFactory + { + Microsoft.Build.FileSystem.IDirectoryCache GetDirectoryCacheForEvaluation(int evaluationId); + } public abstract partial class MSBuildFileSystemBase { protected MSBuildFileSystemBase() { } diff --git a/ref/Microsoft.Build/netstandard/Microsoft.Build.cs b/ref/Microsoft.Build/netstandard/Microsoft.Build.cs index b387429467c..c12fdafce68 100644 --- a/ref/Microsoft.Build/netstandard/Microsoft.Build.cs +++ b/ref/Microsoft.Build/netstandard/Microsoft.Build.cs @@ -498,6 +498,7 @@ namespace Microsoft.Build.Definition public partial class ProjectOptions { public ProjectOptions() { } + public Microsoft.Build.FileSystem.IDirectoryCacheFactory DirectoryCacheFactory { get { throw null; } set { } } public Microsoft.Build.Evaluation.Context.EvaluationContext EvaluationContext { get { throw null; } set { } } public System.Collections.Generic.IDictionary GlobalProperties { get { throw null; } set { } } public Microsoft.Build.Evaluation.ProjectLoadSettings LoadSettings { get { throw null; } set { } } @@ -1498,6 +1499,19 @@ public ProxyTargets(System.Collections.Generic.IReadOnlyDictionary fileName); + public delegate TResult FindTransform(ref System.ReadOnlySpan fileName); + public partial interface IDirectoryCache + { + bool DirectoryExists(string path); + System.Collections.Generic.IEnumerable EnumerateDirectories(string path, string pattern, Microsoft.Build.FileSystem.FindPredicate predicate, Microsoft.Build.FileSystem.FindTransform transform); + System.Collections.Generic.IEnumerable EnumerateFiles(string path, string pattern, Microsoft.Build.FileSystem.FindPredicate predicate, Microsoft.Build.FileSystem.FindTransform transform); + bool FileExists(string path); + } + public partial interface IDirectoryCacheFactory + { + Microsoft.Build.FileSystem.IDirectoryCache GetDirectoryCacheForEvaluation(int evaluationId); + } public abstract partial class MSBuildFileSystemBase { protected MSBuildFileSystemBase() { } diff --git a/src/Build.UnitTests/Definition/ProjectEvaluationContext_Tests.cs b/src/Build.UnitTests/Definition/ProjectEvaluationContext_Tests.cs index 232d22c62dd..49497a0e870 100644 --- a/src/Build.UnitTests/Definition/ProjectEvaluationContext_Tests.cs +++ b/src/Build.UnitTests/Definition/ProjectEvaluationContext_Tests.cs @@ -129,6 +129,41 @@ public void IsolatedContextShouldNotSupportBeingPassedAFileSystem() Should.Throw(() => EvaluationContext.Create(EvaluationContext.SharingPolicy.Isolated, fileSystem)); } + [Fact] + public void EvaluationShouldUseDirectoryCache() + { + var projectFile = _env.CreateFile("1.proj", @" ".Cleanup()).Path; + + var projectCollection = _env.CreateProjectCollection().Collection; + var directoryCacheFactory = new Helpers.LoggingDirectoryCacheFactory(); + + var project = Project.FromFile( + projectFile, + new ProjectOptions + { + ProjectCollection = projectCollection, + DirectoryCacheFactory = directoryCacheFactory, + } + ); + + directoryCacheFactory.DirectoryCaches.Count.ShouldBe(1); + var directoryCache = directoryCacheFactory.DirectoryCaches[0]; + + directoryCache.EvaluationId.ShouldBe(project.LastEvaluationId); + + directoryCache.ExistenceChecks.OrderBy(kvp => kvp.Key).ShouldBe( + new Dictionary + { + { _env.DefaultTestDirectory.Path, 1}, + { Path.Combine(_env.DefaultTestDirectory.Path, "1.file"), 2 } + }.OrderBy(kvp => kvp.Key)); + directoryCache.Enumerations.ShouldBe( + new Dictionary + { + { _env.DefaultTestDirectory.Path, 1 } + }); + } + [Theory] [InlineData(EvaluationContext.SharingPolicy.Shared)] [InlineData(EvaluationContext.SharingPolicy.Isolated)] diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs index 109cb49b9bf..f96e90a2822 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -28,8 +28,6 @@ internal class ItemGroupIntrinsicTask : IntrinsicTask /// private ProjectItemGroupTaskInstance _taskInstance; - private EngineFileUtilities _engineFileUtilities; - /// /// Instantiates an ItemGroup task /// @@ -41,7 +39,6 @@ public ItemGroupIntrinsicTask(ProjectItemGroupTaskInstance taskInstance, TargetL : base(loggingContext, projectInstance, logTaskInputs) { _taskInstance = taskInstance; - _engineFileUtilities = EngineFileUtilities.Default; } /// @@ -431,7 +428,7 @@ ISet removeMetadata // The expression is not of the form "@(X)". Treat as string // Pass the non wildcard expanded excludes here to fix https://github.com/Microsoft/msbuild/issues/2621 - string[] includeSplitFiles = _engineFileUtilities.GetFileListEscaped( + string[] includeSplitFiles = EngineFileUtilities.GetFileListEscaped( Project.Directory, includeSplit, excludes); @@ -455,7 +452,7 @@ ISet removeMetadata foreach (string excludeSplit in excludes) { - string[] excludeSplitFiles = _engineFileUtilities.GetFileListUnescaped(Project.Directory, excludeSplit); + string[] excludeSplitFiles = EngineFileUtilities.GetFileListUnescaped(Project.Directory, excludeSplit); foreach (string excludeSplitFile in excludeSplitFiles) { @@ -540,7 +537,7 @@ Expander expander // Don't unescape wildcards just yet - if there were any escaped, the caller wants to treat them // as literals. Everything else is safe to unescape at this point, since we're only matching // against the file system. - string[] fileList = _engineFileUtilities.GetFileListEscaped(Project.Directory, piece); + string[] fileList = EngineFileUtilities.GetFileListEscaped(Project.Directory, piece); foreach (string file in fileList) { diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index ff8abfdce9b..ff29b8ec3a5 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -32,6 +32,7 @@ using EvaluationItemSpec = Microsoft.Build.Evaluation.ItemSpec; using EvaluationItemExpressionFragment = Microsoft.Build.Evaluation.ItemSpec.ItemExpressionFragment; using SdkResult = Microsoft.Build.BackEnd.SdkResolution.SdkResult; +using Microsoft.Build.FileSystem; namespace Microsoft.Build.Evaluation { @@ -68,6 +69,11 @@ public class Project : ILinkableObject internal ProjectLink Link => implementation; object ILinkableObject.Link => IsLinked ? Link : null; + /// + /// Host-provided factory for interfaces to be used during evaluation. + /// + private readonly IDirectoryCacheFactory _directoryCacheFactory; + /// /// Default project template options (include all features). /// @@ -250,20 +256,22 @@ public Project(ProjectRootElement xml, IDictionary globalPropert /// The the project is added to. /// The to use for evaluation. public Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) - : this(xml, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null) + : this(xml, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null, null) { } - private Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) + private Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, + EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory) { ErrorUtilities.VerifyThrowArgumentNull(xml, nameof(xml)); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); ErrorUtilities.VerifyThrowArgumentNull(projectCollection, nameof(projectCollection)); ProjectCollection = projectCollection; - var defaultImplementation = new ProjectImpl(this, xml, globalProperties, toolsVersion, subToolsetVersion, loadSettings, evaluationContext); + var defaultImplementation = new ProjectImpl(this, xml, globalProperties, toolsVersion, subToolsetVersion, loadSettings); implementationInternal = (IProjectLinkInternal)defaultImplementation; implementation = defaultImplementation; + _directoryCacheFactory = directoryCacheFactory; defaultImplementation.Initialize(globalProperties, toolsVersion, subToolsetVersion, loadSettings, evaluationContext); } @@ -342,11 +350,12 @@ public Project(XmlReader xmlReader, IDictionary globalProperties /// The collection with which this project should be associated. May not be null. /// The load settings for this project. public Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) - : this(xmlReader, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null) + : this(xmlReader, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null, null) { } - private Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) + private Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, + EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory) { ErrorUtilities.VerifyThrowArgumentNull(xmlReader, nameof(xmlReader)); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); @@ -356,6 +365,7 @@ private Project(XmlReader xmlReader, IDictionary globalPropertie implementationInternal = (IProjectLinkInternal)defaultImplementation; implementation = defaultImplementation; + _directoryCacheFactory = directoryCacheFactory; defaultImplementation.Initialize(globalProperties, toolsVersion, subToolsetVersion, loadSettings, evaluationContext); } @@ -436,11 +446,12 @@ public Project(string projectFile, IDictionary globalProperties, /// The collection with which this project should be associated. May not be null. /// The load settings for this project. public Project(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) - : this(projectFile, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null) + : this(projectFile, globalProperties, toolsVersion, subToolsetVersion, projectCollection, loadSettings, null, null) { } - private Project(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) + private Project(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, + EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory) { ErrorUtilities.VerifyThrowArgumentNull(projectFile, nameof(projectFile)); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); @@ -451,6 +462,8 @@ private Project(string projectFile, IDictionary globalProperties implementationInternal = (IProjectLinkInternal)defaultImplementation; implementation = defaultImplementation; + _directoryCacheFactory = directoryCacheFactory; + // Note: not sure why only this ctor flavor do TryUnloadProject // seems the XmlReader based one should also clean the same way. try @@ -488,7 +501,8 @@ public static Project FromFile(string file, ProjectOptions options) options.SubToolsetVersion, options.ProjectCollection ?? ProjectCollection.GlobalProjectCollection, options.LoadSettings, - options.EvaluationContext); + options.EvaluationContext, + options.DirectoryCacheFactory); } /// @@ -505,7 +519,8 @@ public static Project FromProjectRootElement(ProjectRootElement rootElement, Pro options.SubToolsetVersion, options.ProjectCollection ?? ProjectCollection.GlobalProjectCollection, options.LoadSettings, - options.EvaluationContext); + options.EvaluationContext, + options.DirectoryCacheFactory); } /// @@ -522,7 +537,8 @@ public static Project FromXmlReader(XmlReader reader, ProjectOptions options) options.SubToolsetVersion, options.ProjectCollection ?? ProjectCollection.GlobalProjectCollection, options.LoadSettings, - options.EvaluationContext); + options.EvaluationContext, + options.DirectoryCacheFactory); } /// @@ -1767,6 +1783,18 @@ internal void VerifyThrowInvalidOperationNotImported(ProjectRootElement otherXml ErrorUtilities.VerifyThrowInvalidOperation(ReferenceEquals(Xml, otherXml), "OM_CannotModifyEvaluatedObjectInImportedFile", otherXml.Location.File); } + /// + /// Returns as provided by the passed when creating the + /// project, specific for a given evaluation ID. + /// + /// The evaluation ID for which the cache is requested. + /// An implementation, or null if this project has no + /// associated with it or it returned null. + internal IDirectoryCache GetDirectoryCacheForEvaluation(int evaluationId) + { + return _directoryCacheFactory?.GetDirectoryCacheForEvaluation(evaluationId); + } + /// /// Internal project evaluation implementation. /// @@ -1829,8 +1857,7 @@ private class ProjectImpl : ProjectLink, IProjectLinkInternal /// Tools version to evaluate with. May be null. /// Sub-toolset version to explicitly evaluate the toolset with. May be null. /// The to use for evaluation. - /// The evaluation context to use in case reevaluation is required. - public ProjectImpl(Project owner, ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) + public ProjectImpl(Project owner, ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings) { ErrorUtilities.VerifyThrowArgumentNull(xml, nameof(xml)); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); @@ -3623,6 +3650,7 @@ private void Reevaluate( Evaluator.Evaluate( _data, + Owner, Xml, loadSettings, ProjectCollection.MaxNodeCount, @@ -4164,7 +4192,7 @@ internal Data(Project project, PropertyDictionary globa /// /// Prepares the data object for evaluation. /// - public void InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSystem fileSystem) + public void InitializeForEvaluation(IToolsetProvider toolsetProvider, EvaluationContext evaluationContext) { DefaultTargets = null; Properties = new PropertyDictionary(); @@ -4172,7 +4200,7 @@ public void InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSyste Items = new ItemDictionary(); ItemsIgnoringCondition = new ItemDictionary(); ItemsByEvaluatedIncludeCache = new MultiDictionary(StringComparer.OrdinalIgnoreCase); - Expander = new Expander(Properties, Items, fileSystem); + Expander = new Expander(Properties, Items, evaluationContext); ItemDefinitions = new RetrievableEntryHashSet(MSBuildNameIgnoreCaseComparer.Default); Targets = new RetrievableEntryHashSet(StringComparer.OrdinalIgnoreCase); ImportClosure = new List(); diff --git a/src/Build/Definition/ProjectOptions.cs b/src/Build/Definition/ProjectOptions.cs index 44d2ecccfd6..fadfe73a6e3 100644 --- a/src/Build/Definition/ProjectOptions.cs +++ b/src/Build/Definition/ProjectOptions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Microsoft.Build.Evaluation; using Microsoft.Build.Evaluation.Context; +using Microsoft.Build.FileSystem; namespace Microsoft.Build.Definition { @@ -38,5 +39,10 @@ public class ProjectOptions /// The to use for evaluation. /// public EvaluationContext EvaluationContext { get; set; } + + /// + /// Provides to be used for evaluation. + /// + public IDirectoryCacheFactory DirectoryCacheFactory { get; set; } } } diff --git a/src/Build/Evaluation/Context/EvaluationContext.cs b/src/Build/Evaluation/Context/EvaluationContext.cs index 827d9465d75..19510f6d663 100644 --- a/src/Build/Evaluation/Context/EvaluationContext.cs +++ b/src/Build/Evaluation/Context/EvaluationContext.cs @@ -7,7 +7,6 @@ using System.Threading; using Microsoft.Build.BackEnd.SdkResolution; using Microsoft.Build.FileSystem; -using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -43,27 +42,22 @@ public enum SharingPolicy internal ISdkResolverService SdkResolverService { get; } internal IFileSystem FileSystem { get; } - internal EngineFileUtilities EngineFileUtilities { get; } + internal FileMatcher FileMatcher { get; } /// /// Key to file entry list. Example usages: cache glob expansion and intermediary directory expansions during glob expansion. /// private ConcurrentDictionary> FileEntryExpansionCache { get; } - private EvaluationContext(SharingPolicy policy, IFileSystem fileSystem) + private EvaluationContext(SharingPolicy policy, IFileSystem fileSystem, ISdkResolverService sdkResolverService = null, + ConcurrentDictionary> fileEntryExpansionCache = null) { - // Unsupported case: isolated context with non null file system. - // Isolated means caches aren't reused, but the given file system might cache. - ErrorUtilities.VerifyThrowArgument( - policy == SharingPolicy.Shared || fileSystem == null, - "IsolatedContextDoesNotSupportFileSystem"); - Policy = policy; - SdkResolverService = new CachingSdkResolverService(); - FileEntryExpansionCache = new ConcurrentDictionary>(); + SdkResolverService = sdkResolverService ?? new CachingSdkResolverService(); + FileEntryExpansionCache = fileEntryExpansionCache ?? new ConcurrentDictionary>(); FileSystem = fileSystem ?? new CachingFileSystemWrapper(FileSystems.Default); - EngineFileUtilities = new EngineFileUtilities(new FileMatcher(FileSystem, FileEntryExpansionCache)); + FileMatcher = new FileMatcher(FileSystem, FileEntryExpansionCache); } /// @@ -89,6 +83,12 @@ public static EvaluationContext Create(SharingPolicy policy) /// public static EvaluationContext Create(SharingPolicy policy, MSBuildFileSystemBase fileSystem) { + // Unsupported case: isolated context with non null file system. + // Isolated means caches aren't reused, but the given file system might cache. + ErrorUtilities.VerifyThrowArgument( + policy == SharingPolicy.Shared || fileSystem == null, + "IsolatedContextDoesNotSupportFileSystem"); + var context = new EvaluationContext( policy, fileSystem); @@ -124,5 +124,19 @@ internal EvaluationContext ContextForNewProject() return null; } } + + /// + /// Creates a copy of this with a given swapped in. + /// + /// The file system to use by the new evaluation context. + /// The new evaluation context. + internal EvaluationContext ContextWithFileSystem(IFileSystem fileSystem) + { + var newContext = new EvaluationContext(this.Policy, fileSystem, this.SdkResolverService, this.FileEntryExpansionCache) + { + _used = 1 + }; + return newContext; + } } } diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 712307191f4..e72f28469c6 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using ObjectModel = System.Collections.ObjectModel; using System.Diagnostics; @@ -20,6 +19,7 @@ using Microsoft.Build.Eventing; using Microsoft.Build.Execution; using Microsoft.Build.Experimental.ProjectCache; +using Microsoft.Build.FileSystem; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; using Microsoft.Build.Internal; @@ -144,6 +144,9 @@ internal class Evaluator /// private readonly int _submissionId; + /// + /// The evaluation context to use. + /// private readonly EvaluationContext _evaluationContext; /// @@ -189,6 +192,7 @@ internal class Evaluator /// private Evaluator( IEvaluatorData data, + Project project, ProjectRootElement projectRootElement, ProjectLoadSettings loadSettings, int maxNodeCount, @@ -206,6 +210,7 @@ private Evaluator( { ErrorUtilities.VerifyThrowInternalNull(data, nameof(data)); ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, nameof(projectRootElementCache)); + ErrorUtilities.VerifyThrowInternalNull(evaluationContext, nameof(evaluationContext)); ErrorUtilities.VerifyThrowInternalNull(loggingService, nameof(loggingService)); ErrorUtilities.VerifyThrowInternalNull(buildEventContext, nameof(buildEventContext)); @@ -220,12 +225,20 @@ private Evaluator( // Wrap the IEvaluatorData<> object passed in. data = new PropertyTrackingEvaluatorDataWrapper(data, _evaluationLoggingContext, Traits.Instance.LogPropertyTracking); } - _evaluationContext = evaluationContext ?? EvaluationContext.Create(EvaluationContext.SharingPolicy.Isolated); + + // If the host wishes to provide a directory cache for this evaluation, create a new EvaluationContext with the right file system. + _evaluationContext = evaluationContext; + IDirectoryCache directoryCache = project?.GetDirectoryCacheForEvaluation(_evaluationLoggingContext.BuildEventContext.EvaluationId); + if (directoryCache is not null) + { + IFileSystem fileSystem = new DirectoryCacheFileSystemWrapper(evaluationContext.FileSystem, directoryCache); + _evaluationContext = evaluationContext.ContextWithFileSystem(fileSystem); + } // Create containers for the evaluation results - data.InitializeForEvaluation(toolsetProvider, _evaluationContext.FileSystem); + data.InitializeForEvaluation(toolsetProvider, _evaluationContext); - _expander = new Expander(data, data, _evaluationContext.FileSystem); + _expander = new Expander(data, data, _evaluationContext); // This setting may change after the build has started, therefore if the user has not set the property to true on the build parameters we need to check to see if it is set to true on the environment variable. _expander.WarnForUninitializedProperties = BuildParameters.WarnOnUninitializedProperty || Traits.Instance.EscapeHatches.WarnOnUninitializedProperty; @@ -284,6 +297,7 @@ private Evaluator( /// internal static void Evaluate( IEvaluatorData data, + Project project, ProjectRootElement root, ProjectLoadSettings loadSettings, int maxNodeCount, @@ -295,13 +309,14 @@ internal static void Evaluate( BuildEventContext buildEventContext, ISdkResolverService sdkResolverService, int submissionId, - EvaluationContext evaluationContext = null, + EvaluationContext evaluationContext, bool interactive = false) { MSBuildEventSource.Log.EvaluateStart(root.ProjectFileLocation.File); var profileEvaluation = (loadSettings & ProjectLoadSettings.ProfileEvaluation) != 0 || loggingService.IncludeEvaluationProfile; var evaluator = new Evaluator( data, + project, root, loadSettings, maxNodeCount, @@ -357,7 +372,7 @@ internal static List CreateItemsFromInclude(string rootDirectory, ProjectItem else { // The expression is not of the form "@(X)". Treat as string - string[] includeSplitFilesEscaped = EngineFileUtilities.Default.GetFileListEscaped(rootDirectory, includeSplitEscaped); + string[] includeSplitFilesEscaped = EngineFileUtilities.GetFileListEscaped(rootDirectory, includeSplitEscaped, excludeSpecsEscaped: null, forceEvaluate: false, fileMatcher: expander.EvaluationContext?.FileMatcher); if (includeSplitFilesEscaped.Length > 0) { @@ -2009,7 +2024,7 @@ private LoadImportsResult ExpandAndLoadImportsFromUnescapedImportExpression(stri } // Expand the wildcards and provide an alphabetical order list of import statements. - importFilesEscaped = _evaluationContext.EngineFileUtilities.GetFileListEscaped(directoryOfImportingFile, importExpressionEscapedItem, forceEvaluate: true); + importFilesEscaped = EngineFileUtilities.GetFileListEscaped(directoryOfImportingFile, importExpressionEscapedItem, forceEvaluate: true, fileMatcher: _evaluationContext.FileMatcher); } catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex)) { diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index fe397e4469a..1fe132a8d21 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -13,6 +13,7 @@ using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -301,6 +302,11 @@ private void FlushFirstValueIfNeeded() private readonly IFileSystem _fileSystem; + /// + /// Non-null if the expander was constructed for evaluation. + /// + internal EvaluationContext EvaluationContext { get; } + /// /// Creates an expander passing it some properties to use. /// Properties may be null. @@ -312,6 +318,18 @@ internal Expander(IPropertyProvider

properties, IFileSystem fileSystem) _fileSystem = fileSystem; } + ///

+ /// Creates an expander passing it some properties to use and the evaluation context. + /// Properties may be null. + /// + internal Expander(IPropertyProvider

properties, EvaluationContext evaluationContext) + { + _properties = properties; + _usedUninitializedProperties = new UsedUninitializedProperties(); + _fileSystem = evaluationContext.FileSystem; + EvaluationContext = evaluationContext; + } + ///

/// Creates an expander passing it some properties and items to use. /// Either or both may be null. @@ -322,6 +340,16 @@ internal Expander(IPropertyProvider

properties, IItemProvider items, IFile _items = items; } + ///

+ /// Creates an expander passing it some properties and items to use, and the evaluation context. + /// Either or both may be null. + /// + internal Expander(IPropertyProvider

properties, IItemProvider items, EvaluationContext evaluationContext) + : this(properties, evaluationContext) + { + _items = items; + } + ///

/// Creates an expander passing it some properties, items, and/or metadata to use. /// Any or all may be null. @@ -2216,7 +2244,7 @@ internal static IEnumerable> GetItemPairEnumerable(IEnumerable EvaluatedItemElements /// /// Prepares the data block for a new evaluation pass /// - void InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSystem fileSystem); + void InitializeForEvaluation(IToolsetProvider toolsetProvider, EvaluationContext evaluationContext); /// /// Indicates to the data block that evaluation has completed, diff --git a/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs b/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs index 4c88a6d976f..c493969feb5 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs @@ -7,8 +7,8 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.SdkResolution; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; -using Microsoft.Build.Shared.FileSystem; namespace Microsoft.Build.Evaluation { @@ -285,9 +285,9 @@ public ProjectTargetInstance GetTarget(string targetName) return _wrappedData.GetTarget(targetName); } - public void InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSystem fileSystem) + public void InitializeForEvaluation(IToolsetProvider toolsetProvider, EvaluationContext evaluationContext) { - _wrappedData.InitializeForEvaluation(toolsetProvider, fileSystem); + _wrappedData.InitializeForEvaluation(toolsetProvider, evaluationContext); } public void RecordImport(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated, SdkResult sdkResult) diff --git a/src/Build/Evaluation/LazyItemEvaluator.IncludeOperation.cs b/src/Build/Evaluation/LazyItemEvaluator.IncludeOperation.cs index da5f61449b6..b50ed189861 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.IncludeOperation.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.IncludeOperation.cs @@ -113,7 +113,8 @@ protected override ImmutableList SelectItems(OrderedItemDataCollection.Builde includeSplitFilesEscaped = EngineFileUtilities.GetFileListEscaped( _rootDirectory, glob, - excludePatternsForGlobs + excludePatternsForGlobs, + fileMatcher: FileMatcher ); } if (MSBuildEventSource.Log.IsEnabled()) diff --git a/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs b/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs index 288d11ce9b9..20706932be6 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs @@ -44,12 +44,12 @@ protected LazyItemOperation(OperationBuilder builder, LazyItemEvaluator GetReferencedItems(itemType, ImmutableHashSet.Empty)); _itemFactory = new ItemFactoryWrapper(_itemElement, _lazyEvaluator._itemFactory); - _expander = new Expander(_evaluatorData, _evaluatorData, _lazyEvaluator.FileSystem); + _expander = new Expander(_evaluatorData, _evaluatorData, _lazyEvaluator.EvaluationContext); _itemSpec.Expander = _expander; } - protected EngineFileUtilities EngineFileUtilities => _lazyEvaluator.EngineFileUtilities; + protected FileMatcher FileMatcher => _lazyEvaluator.FileMatcher; public void Apply(OrderedItemDataCollection.Builder listBuilder, ImmutableHashSet globsToIgnore) { diff --git a/src/Build/Evaluation/LazyItemEvaluator.cs b/src/Build/Evaluation/LazyItemEvaluator.cs index 9fd3eec87e7..ada3da6a27f 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.cs @@ -6,7 +6,6 @@ using Microsoft.Build.Construction; using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Eventing; -using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -40,22 +39,22 @@ internal partial class LazyItemEvaluator new Dictionary() : new Dictionary(StringComparer.OrdinalIgnoreCase); - protected IFileSystem FileSystem { get; } + protected EvaluationContext EvaluationContext { get; } - protected EngineFileUtilities EngineFileUtilities { get; } + protected IFileSystem FileSystem => EvaluationContext.FileSystem; + protected FileMatcher FileMatcher => EvaluationContext.FileMatcher; public LazyItemEvaluator(IEvaluatorData data, IItemFactory itemFactory, LoggingContext loggingContext, EvaluationProfiler evaluationProfiler, EvaluationContext evaluationContext) { _outerEvaluatorData = data; - _outerExpander = new Expander(_outerEvaluatorData, _outerEvaluatorData, evaluationContext.FileSystem); + _outerExpander = new Expander(_outerEvaluatorData, _outerEvaluatorData, evaluationContext); _evaluatorData = new EvaluatorData(_outerEvaluatorData, itemType => GetItems(itemType)); - _expander = new Expander(_evaluatorData, _evaluatorData, evaluationContext.FileSystem); + _expander = new Expander(_evaluatorData, _evaluatorData, evaluationContext); _itemFactory = itemFactory; _loggingContext = loggingContext; _evaluationProfiler = evaluationProfiler; - FileSystem = evaluationContext.FileSystem; - EngineFileUtilities = evaluationContext.EngineFileUtilities; + EvaluationContext = evaluationContext; } private ImmutableList GetItems(string itemType) diff --git a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs index 6e3dc3a3ec7..54effc239c5 100644 --- a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs +++ b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs @@ -5,10 +5,10 @@ using Microsoft.Build.Collections; using Microsoft.Build.Construction; using Microsoft.Build.Execution; -using Microsoft.Build.Shared.FileSystem; using System; using System.Collections.Generic; using Microsoft.Build.BackEnd.Components.Logging; +using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using SdkResult = Microsoft.Build.BackEnd.SdkResolution.SdkResult; @@ -135,7 +135,7 @@ public P SetProperty(ProjectPropertyElement propertyElement, string evaluatedVal public ItemDictionary Items => _wrapped.Items; public List EvaluatedItemElements => _wrapped.EvaluatedItemElements; public PropertyDictionary EnvironmentVariablePropertiesDictionary => _wrapped.EnvironmentVariablePropertiesDictionary; - public void InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSystem fileSystem) => _wrapped.InitializeForEvaluation(toolsetProvider, fileSystem); + public void InitializeForEvaluation(IToolsetProvider toolsetProvider, EvaluationContext evaluationContext) => _wrapped.InitializeForEvaluation(toolsetProvider, evaluationContext); public void FinishEvaluation() => _wrapped.FinishEvaluation(); public void AddItem(I item) => _wrapped.AddItem(item); public void AddItemIgnoringCondition(I item) => _wrapped.AddItemIgnoringCondition(item); diff --git a/src/Build/FileSystem/DirectoryCacheFileSystemWrapper.cs b/src/Build/FileSystem/DirectoryCacheFileSystemWrapper.cs new file mode 100644 index 00000000000..84c24fb02cc --- /dev/null +++ b/src/Build/FileSystem/DirectoryCacheFileSystemWrapper.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +#if FEATURE_MSIOREDIST +using Path = Microsoft.IO.Path; +#endif + +namespace Microsoft.Build.FileSystem +{ + internal class DirectoryCacheFileSystemWrapper : IFileSystem + { + /// + /// The base to fall back to for functionality not provided by . + /// + private readonly IFileSystem _fileSystem; + + /// + /// A host-provided cache used for file existence and directory enumeration. + /// + private readonly IDirectoryCache _directoryCache; + + public DirectoryCacheFileSystemWrapper(IFileSystem fileSystem, IDirectoryCache directoryCache) + { + _fileSystem = fileSystem; + _directoryCache = directoryCache; + } + + #region IFileSystem implementation based on IDirectoryCache + + public bool FileOrDirectoryExists(string path) + { + return _directoryCache.FileExists(path) || _directoryCache.DirectoryExists(path); + } + + public bool DirectoryExists(string path) + { + return _directoryCache.DirectoryExists(path); + } + + public bool FileExists(string path) + { + return _directoryCache.FileExists(path); + } + + public IEnumerable EnumerateDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (searchOption != SearchOption.TopDirectoryOnly) + { + // Recursive enumeration is not used during evaluation, pass it through. + return _fileSystem.EnumerateDirectories(path, searchPattern, searchOption); + } + return EnumerateFullFileSystemPaths(path, searchPattern, includeFiles: false, includeDirectories: true); + } + + public IEnumerable EnumerateFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (searchOption != SearchOption.TopDirectoryOnly) + { + // Recursive enumeration is not used during evaluation, pass it through. + return _fileSystem.EnumerateFiles(path, searchPattern, searchOption); + } + return EnumerateFullFileSystemPaths(path, searchPattern, includeFiles: true, includeDirectories: false); + } + + public IEnumerable EnumerateFileSystemEntries(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (searchOption != SearchOption.TopDirectoryOnly) + { + // Recursive enumeration is not used during evaluation, pass it through. + return _fileSystem.EnumerateFileSystemEntries(path, searchPattern, searchOption); + } + return EnumerateFullFileSystemPaths(path, searchPattern, includeFiles: true, includeDirectories: true); + } + + private IEnumerable EnumerateFullFileSystemPaths(string path, string searchPattern, bool includeFiles, bool includeDirectories) + { + FindPredicate predicate = (ref ReadOnlySpan fileName) => + { + return FileMatcher.IsAllFilesWildcard(searchPattern) || FileMatcher.IsMatch(fileName, searchPattern); + }; + FindTransform transform = (ref ReadOnlySpan fileName) => Path.Join(path.AsSpan(), fileName); + + IEnumerable directories = includeDirectories + ? _directoryCache.EnumerateDirectories(path, searchPattern, predicate, transform) + : Enumerable.Empty(); + IEnumerable files = includeFiles + ? _directoryCache.EnumerateFiles(path, searchPattern, predicate, transform) + : Enumerable.Empty(); + + return Enumerable.Concat(directories, files); + } + + #endregion + + #region IFileSystem pass-through implementation + + public FileAttributes GetAttributes(string path) => _fileSystem.GetAttributes(path); + + public DateTime GetLastWriteTimeUtc(string path) => _fileSystem.GetLastWriteTimeUtc(path); + + public TextReader ReadFile(string path) => _fileSystem.ReadFile(path); + + public Stream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share) => _fileSystem.GetFileStream(path, mode, access, share); + + public string ReadFileAllText(string path) => _fileSystem.ReadFileAllText(path); + + public byte[] ReadFileAllBytes(string path) => _fileSystem.ReadFileAllBytes(path); + + #endregion + } +} diff --git a/src/Build/FileSystem/IDirectoryCache.cs b/src/Build/FileSystem/IDirectoryCache.cs new file mode 100644 index 00000000000..fb6e62c1b6a --- /dev/null +++ b/src/Build/FileSystem/IDirectoryCache.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.FileSystem +{ + /// + /// A provider of instances. To be implemented by MSBuild hosts that wish to intercept + /// file existence checks and file enumerations performed during project evaluation. + /// + /// + /// Unlike , file enumeration returns file/directory names, not full paths. + /// The host uses to specify the directory cache + /// factory per project. + /// + public interface IDirectoryCacheFactory + { + /// + /// Returns an to be used when evaluating the project associated with this . + /// + /// The ID of the evaluation for which the interface is requested. + IDirectoryCache GetDirectoryCacheForEvaluation(int evaluationId); + } + + /// + /// A predicate taking file name. + /// + /// The file name to check. + public delegate bool FindPredicate(ref ReadOnlySpan fileName); + + /// + /// A function taking file name and returning an arbitrary result. + /// + /// The type of the result to return + /// The file name to transform. + public delegate TResult FindTransform(ref ReadOnlySpan fileName); + + /// + /// Allows the implementor to intercept file existence checks and file enumerations performed during project evaluation. + /// + public interface IDirectoryCache + { + /// + /// Returns true if the given path points to an existing file on disk. + /// + /// A full and normalized path. + bool FileExists(string path); + + /// + /// Returns true if the given path points to an existing directory on disk. + /// + /// A full and normalized path. + bool DirectoryExists(string path); + + /// + /// Enumerates files in the given directory only (non-recursively). + /// + /// The desired return type. + /// The directory to enumerate, specified as a full normalized path. + /// A search pattern supported by the platform which is guaranteed to return a superset of relevant files. + /// A predicate to test whether a file should be included. + /// A transform from ReadOnlySpan<char> to . + /// + /// The parameter may match more files than what the caller is interested in. In other words, + /// can return false even if the implementation enumerates only files whose names + /// match the pattern. The implementation is free to ignore the pattern and call the predicate for all files on the given + /// . + /// + IEnumerable EnumerateFiles(string path, string pattern, FindPredicate predicate, FindTransform transform); + + /// + /// Enumerates subdirectories in the given directory only (non-recursively). + /// + /// The desired return type. + /// The directory to enumerate, specified as a full normalized path. + /// A search pattern supported by the platform which is guaranteed to return a superset of relevant directories. + /// A predicate to test whether a directory should be included. + /// A transform from ReadOnlySpan<char> to . + /// + /// The parameter may match more direcories than what the caller is interested in. In other words, + /// can return false even if the implementation enumerates only directories whose names + /// match the pattern. The implementation is free to ignore the pattern and call the predicate for all directories on the given + /// . + /// + IEnumerable EnumerateDirectories(string path, string pattern, FindPredicate predicate, FindTransform transform); + } +} diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index b67aba3de21..35795e9d705 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -1347,7 +1347,7 @@ ICollection IItemProvider.GetItems(str /// Only called during evaluation, so does not check for immutability. /// void IEvaluatorData. - InitializeForEvaluation(IToolsetProvider toolsetProvider, IFileSystem fileSystem) + InitializeForEvaluation(IToolsetProvider toolsetProvider, EvaluationContext evaluationContext) { // All been done in the constructor. We don't allow re-evaluation of project instances. } @@ -2761,6 +2761,7 @@ out var usingDifferentToolsVersionFromProjectFile Evaluator.Evaluate( this, + null, xml, projectLoadSettings ?? buildParameters.ProjectLoadSettings, /* Use override ProjectLoadSettings if specified */ buildParameters.MaxNodeCount, diff --git a/src/Build/Utilities/EngineFileUtilities.cs b/src/Build/Utilities/EngineFileUtilities.cs index da8165d3369..242085521c9 100644 --- a/src/Build/Utilities/EngineFileUtilities.cs +++ b/src/Build/Utilities/EngineFileUtilities.cs @@ -12,10 +12,8 @@ namespace Microsoft.Build.Internal { - internal class EngineFileUtilities + internal static class EngineFileUtilities { - private readonly FileMatcher _fileMatcher; - // Regexes for wildcard filespecs that should not get expanded // By default all wildcards are expanded. private static List s_lazyWildCardExpansionRegexes; @@ -34,13 +32,6 @@ internal static void CaptureLazyWildcardRegexes() s_lazyWildCardExpansionRegexes = PopulateRegexFromEnvironment(); } - public static EngineFileUtilities Default = new EngineFileUtilities(FileMatcher.Default); - - public EngineFileUtilities(FileMatcher fileMatcher) - { - _fileMatcher = fileMatcher; - } - /// /// Used for the purposes of evaluating an item specification. Given a filespec that may include wildcard characters * and /// ?, we translate it into an actual list of files. If the input filespec doesn't contain any wildcard characters, and it @@ -54,14 +45,14 @@ public EngineFileUtilities(FileMatcher fileMatcher) /// The directory to evaluate, escaped. /// The filespec to evaluate, escaped. /// Array of file paths, unescaped. - internal string[] GetFileListUnescaped + internal static string[] GetFileListUnescaped ( string directoryEscaped, string filespecEscaped ) { - return GetFileList(directoryEscaped, filespecEscaped, returnEscaped: false, forceEvaluateWildCards: false); + return GetFileList(directoryEscaped, filespecEscaped, returnEscaped: false, forceEvaluateWildCards: false, excludeSpecsEscaped: null, fileMatcher: FileMatcher.Default); } /// @@ -78,16 +69,18 @@ string filespecEscaped /// The filespec to evaluate, escaped. /// Filespecs to exclude, escaped. /// Whether to force file glob expansion when eager expansion is turned off + /// /// Array of file paths, escaped. - internal string[] GetFileListEscaped + internal static string[] GetFileListEscaped ( string directoryEscaped, string filespecEscaped, IEnumerable excludeSpecsEscaped = null, - bool forceEvaluate = false + bool forceEvaluate = false, + FileMatcher fileMatcher = null ) { - return GetFileList(directoryEscaped, filespecEscaped, returnEscaped: true, forceEvaluate, excludeSpecsEscaped); + return GetFileList(directoryEscaped, filespecEscaped, returnEscaped: true, forceEvaluate, excludeSpecsEscaped, fileMatcher ?? FileMatcher.Default); } internal static bool FilespecHasWildcards(string filespecEscaped) @@ -119,14 +112,16 @@ internal static bool FilespecHasWildcards(string filespecEscaped) /// true to return escaped specs. /// Whether to force file glob expansion when eager expansion is turned off /// The exclude specification, escaped. + /// /// Array of file paths. - private string[] GetFileList + private static string[] GetFileList ( string directoryEscaped, string filespecEscaped, bool returnEscaped, bool forceEvaluateWildCards, - IEnumerable excludeSpecsEscaped = null + IEnumerable excludeSpecsEscaped, + FileMatcher fileMatcher ) { ErrorUtilities.VerifyThrowInternalLength(filespecEscaped, nameof(filespecEscaped)); @@ -156,7 +151,7 @@ private string[] GetFileList // as a relative path, we will get back a bunch of relative paths. // If the filespec started out as an absolute path, we will get // back a bunch of absolute paths. - fileList = _fileMatcher.GetFiles(directoryUnescaped, filespecUnescaped, excludeSpecsUnescaped); + fileList = fileMatcher.GetFiles(directoryUnescaped, filespecUnescaped, excludeSpecsUnescaped); ErrorUtilities.VerifyThrow(fileList != null, "We must have a list of files here, even if it's empty."); diff --git a/src/Shared/FileMatcher.cs b/src/Shared/FileMatcher.cs index 9af0619bab7..22a9572fc82 100644 --- a/src/Shared/FileMatcher.cs +++ b/src/Shared/FileMatcher.cs @@ -132,7 +132,7 @@ internal FileMatcher(IFileSystem fileSystem, GetFileSystemEntries getFileSystemE directory, false)); IEnumerable filteredEntriesForPath = (pattern != null && !IsAllFilesWildcard(pattern)) - ? allEntriesForPath.Where(o => IsMatch(Path.GetFileName(o), pattern)) + ? allEntriesForPath.Where(o => IsFileNameMatch(o, pattern)) : allEntriesForPath; return stripProjectDirectory ? RemoveProjectDirectory(filteredEntriesForPath, directory).ToArray() @@ -244,7 +244,7 @@ private static IReadOnlyList GetAccessibleFileSystemEntries(IFileSystem { case FileSystemEntity.Files: return GetAccessibleFiles(fileSystem, path, pattern, projectDirectory, stripProjectDirectory); case FileSystemEntity.Directories: return GetAccessibleDirectories(fileSystem, path, pattern); - case FileSystemEntity.FilesAndDirectories: return GetAccessibleFilesAndDirectories(fileSystem,path, pattern); + case FileSystemEntity.FilesAndDirectories: return GetAccessibleFilesAndDirectories(fileSystem, path, pattern); default: ErrorUtilities.VerifyThrow(false, "Unexpected filesystem entity type."); break; @@ -268,7 +268,7 @@ private static IReadOnlyList GetAccessibleFilesAndDirectories(IFileSyste { return (ShouldEnforceMatching(pattern) ? fileSystem.EnumerateFileSystemEntries(path, pattern) - .Where(o => IsMatch(Path.GetFileName(o), pattern)) + .Where(o => IsFileNameMatch(o, pattern)) : fileSystem.EnumerateFileSystemEntries(path, pattern) ).ToArray(); } @@ -351,7 +351,7 @@ bool stripProjectDirectory files = fileSystem.EnumerateFiles(dir, filespec); if (ShouldEnforceMatching(filespec)) { - files = files.Where(o => IsMatch(Path.GetFileName(o), filespec)); + files = files.Where(o => IsFileNameMatch(o, filespec)); } } // If the Item is based on a relative path we need to strip @@ -414,7 +414,7 @@ string pattern directories = fileSystem.EnumerateDirectories((path.Length == 0) ? s_thisDirectory : path, pattern); if (ShouldEnforceMatching(pattern)) { - directories = directories.Where(o => IsMatch(Path.GetFileName(o), pattern)); + directories = directories.Where(o => IsFileNameMatch(o, pattern)); } } @@ -956,7 +956,7 @@ private void GetFilesRecursive( for (int i = 0; i < excludeNextSteps.Length; i++) { if (excludeNextSteps[i].NeedsDirectoryRecursion && - (excludeNextSteps[i].DirectoryPattern == null || IsMatch(Path.GetFileName(subdir), excludeNextSteps[i].DirectoryPattern))) + (excludeNextSteps[i].DirectoryPattern == null || IsFileNameMatch(subdir, excludeNextSteps[i].DirectoryPattern))) { RecursionState thisExcludeStep = searchesToExclude[i]; thisExcludeStep.BaseDirectory = subdir; @@ -1097,7 +1097,7 @@ private static bool MatchFileRecursionStep(RecursionState recursionState, string } else if (recursionState.SearchData.Filespec != null) { - return IsMatch(Path.GetFileName(file), recursionState.SearchData.Filespec); + return IsFileNameMatch(file, recursionState.SearchData.Filespec); } // if no file-spec provided, match the file to the regular expression @@ -1664,12 +1664,39 @@ internal Result() internal string wildcardDirectoryPart = string.Empty; } + /// + /// A wildcard (* and ?) matching algorithm that tests whether the input path file name matches against the pattern. + /// + /// The path whose file name is matched against the pattern. + /// The pattern. + internal static bool IsFileNameMatch(string path, string pattern) + { + // Use a span-based Path.GetFileName if it is available. +#if FEATURE_MSIOREDIST + return IsMatch(Microsoft.IO.Path.GetFileName(path.AsSpan()), pattern); +#elif NETSTANDARD2_0 + return IsMatch(Path.GetFileName(path), pattern); +#else + return IsMatch(Path.GetFileName(path.AsSpan()), pattern); +#endif + } + /// /// A wildcard (* and ?) matching algorithm that tests whether the input string matches against the pattern. /// /// String which is matched against the pattern. /// Pattern against which string is matched. internal static bool IsMatch(string input, string pattern) + { + return IsMatch(input.AsSpan(), pattern); + } + + /// + /// A wildcard (* and ?) matching algorithm that tests whether the input string matches against the pattern. + /// + /// String which is matched against the pattern. + /// Pattern against which string is matched. + internal static bool IsMatch(ReadOnlySpan input, string pattern) { if (input == null) { @@ -1705,9 +1732,12 @@ internal static bool IsMatch(string input, string pattern) // to using the string indexer. The iIndex and pIndex parameters are only used // when we have to compare two non ASCII characters. Using just string.Compare for // character comparison, would reduce the speed by approx. 5 times. - bool CompareIgnoreCase(char inputChar, char patternChar, int iIndex, int pIndex) + bool CompareIgnoreCase(ref ReadOnlySpan input, int iIndex, int pIndex) #endif { + char inputChar = input[iIndex]; + char patternChar = pattern[pIndex]; + // We will mostly be comparing ASCII characters, check English letters first. char inputCharLower = (char)(inputChar | 0x20); if (inputCharLower >= 'a' && inputCharLower <= 'z') @@ -1721,7 +1751,7 @@ bool CompareIgnoreCase(char inputChar, char patternChar, int iIndex, int pIndex) // and a non ASCII character cannot have its lowercase/uppercase inside the ASCII table return inputChar == patternChar; } - return string.Compare(input, iIndex, pattern, pIndex, 1, StringComparison.OrdinalIgnoreCase) == 0; + return MemoryExtensions.Equals(input.Slice(iIndex, 1), pattern.AsSpan(pIndex, 1), StringComparison.OrdinalIgnoreCase); } #if MONO ; // The end of the CompareIgnoreCase anonymous function @@ -1761,7 +1791,7 @@ bool CompareIgnoreCase(char inputChar, char patternChar, int iIndex, int pIndex) break; } // If the tail doesn't match, we can safely return e.g. ("aaa", "*b") - if (!CompareIgnoreCase(input[inputTailIndex], pattern[patternTailIndex], patternTailIndex, inputTailIndex) && + if (!CompareIgnoreCase(ref input, inputTailIndex, patternTailIndex) && pattern[patternTailIndex] != '?') { return false; @@ -1781,7 +1811,7 @@ bool CompareIgnoreCase(char inputChar, char patternChar, int iIndex, int pIndex) // The ? wildcard cannot be skipped as we will have a wrong result for e.g. ("aab" "*?b") if (pattern[patternIndex] != '?') { - while (!CompareIgnoreCase(input[inputIndex], pattern[patternIndex], inputIndex, patternIndex)) + while (!CompareIgnoreCase(ref input, inputIndex, patternIndex)) { // Return if there is no character that match e.g. ("aa", "*b") if (++inputIndex >= inputLength) @@ -1796,7 +1826,7 @@ bool CompareIgnoreCase(char inputChar, char patternChar, int iIndex, int pIndex) } // If we have a match, step to the next character - if (CompareIgnoreCase(input[inputIndex], pattern[patternIndex], inputIndex, patternIndex) || + if (CompareIgnoreCase(ref input, inputIndex, patternIndex) || pattern[patternIndex] == '?') { patternIndex++; @@ -2557,14 +2587,14 @@ private static bool IsSubdirectoryOf(string possibleChild, string possibleParent private static bool DirectoryEndsWithPattern(string directoryPath, string pattern) { int index = directoryPath.LastIndexOfAny(FileUtilities.Slashes); - return (index != -1 && IsMatch(directoryPath.Substring(index + 1), pattern)); + return (index != -1 && IsMatch(directoryPath.AsSpan(index + 1), pattern)); } /// /// Returns true if is * or *.*. /// /// The filename pattern to check. - private static bool IsAllFilesWildcard(string pattern) => pattern?.Length switch + internal static bool IsAllFilesWildcard(string pattern) => pattern?.Length switch { 1 => pattern[0] == '*', 3 => pattern[0] == '*' && pattern[1] == '.' && pattern[2] == '*', diff --git a/src/Shared/UnitTests/ObjectModelHelpers.cs b/src/Shared/UnitTests/ObjectModelHelpers.cs index 9261b45785c..e647fd709c0 100644 --- a/src/Shared/UnitTests/ObjectModelHelpers.cs +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -2019,6 +2019,65 @@ public void Dispose() } } + internal sealed class LoggingDirectoryCacheFactory : IDirectoryCacheFactory + { + public List DirectoryCaches { get; } = new(); + + public IDirectoryCache GetDirectoryCacheForEvaluation(int evaluationId) + { + var directoryCache = new LoggingDirectoryCache(evaluationId); + DirectoryCaches.Add(directoryCache); + return directoryCache; + } + } + + internal sealed class LoggingDirectoryCache : IDirectoryCache + { + internal int EvaluationId { get; } + + public ConcurrentDictionary ExistenceChecks { get; } = new(); + public ConcurrentDictionary Enumerations { get; } = new(); + + public LoggingDirectoryCache(int evaluationId) + { + EvaluationId = evaluationId; + } + + public bool DirectoryExists(string path) + { + IncrementExistenceChecks(path); + return Directory.Exists(path); + } + + public bool FileExists(string path) + { + IncrementExistenceChecks(path); + return File.Exists(path); + } + + public IEnumerable EnumerateDirectories(string path, string pattern, FindPredicate predicate, FindTransform transform) + { + IncrementEnumerations(path); + return Enumerable.Empty(); + } + + public IEnumerable EnumerateFiles(string path, string pattern, FindPredicate predicate, FindTransform transform) + { + IncrementEnumerations(path); + return Enumerable.Empty(); + } + + private void IncrementExistenceChecks(string path) + { + ExistenceChecks.AddOrUpdate(path, p => 1, (p, c) => c + 1); + } + + private void IncrementEnumerations(string path) + { + Enumerations.AddOrUpdate(path, p => 1, (p, c) => c + 1); + } + } + internal class LoggingFileSystem : MSBuildFileSystemBase { private int _fileSystemCalls;