private ProjectGraphNode CreateNewNode( ConfigurationMetadata configurationMetadata, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) { // TODO: ProjectInstance just converts the dictionary back to a PropertyDictionary, so find a way to directly provide it. var globalProperties = configurationMetadata.GlobalProperties.ToDictionary(); var projectInstance = projectInstanceFactory( configurationMetadata.ProjectFullPath, globalProperties, projectCollection); if (projectInstance == null) { throw new InvalidOperationException(ResourceUtilities.GetResourceString("NullReferenceFromProjectInstanceFactory")); } var graphNode = new ProjectGraphNode( projectInstance, globalProperties); _allParsedProjects[configurationMetadata] = graphNode; return(graphNode); }
/// <summary> /// Constructs a graph starting from the given graph entry points, evaluating with the provided project collection. /// </summary> /// <param name="entryPoints">The entry points to use in constructing the graph</param> /// <param name="projectCollection"> /// The collection with which all projects in the graph should be associated. May not be /// null. /// </param> /// <param name="projectInstanceFactory"> /// A delegate used for constructing a <see cref="ProjectInstance" />, called for each /// project created during graph creation. This value can be null, which uses /// a default implementation that calls the ProjectInstance constructor. See the remarks /// on <see cref="ProjectInstanceFactoryFunc" /> for other scenarios. /// </param> /// <param name="degreeOfParallelism"> /// Number of threads to participate in building the project graph. /// </param> /// <param name="cancellationToken"> /// The <see cref="T:System.Threading.CancellationToken" /> token to observe. /// </param> /// <exception cref="InvalidProjectFileException"> /// If the evaluation of any project in the graph fails /// </exception> /// <exception cref="InvalidOperationException"> /// If a null reference is returned from <paramref name="projectInstanceFactory" /> /// </exception> /// <exception cref="CircularDependencyException"> /// If the evaluation is successful but the project graph contains a circular /// dependency /// </exception> public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory, int degreeOfParallelism, CancellationToken cancellationToken) { ErrorUtilities.VerifyThrowArgumentNull(projectCollection, nameof(projectCollection)); projectInstanceFactory = projectInstanceFactory ?? DefaultProjectInstanceFactory; var graphBuilder = new GraphBuilder( entryPoints, projectCollection, projectInstanceFactory, ProjectInterpretation.Instance, degreeOfParallelism, cancellationToken); graphBuilder.BuildGraph(); EntryPointNodes = graphBuilder.EntryPointNodes; GraphRoots = graphBuilder.RootNodes; ProjectNodes = graphBuilder.ProjectNodes; Edges = graphBuilder.Edges; _projectNodesTopologicallySorted = new Lazy <IReadOnlyCollection <ProjectGraphNode> >(() => TopologicalSort(GraphRoots, ProjectNodes)); }
/// <summary> /// Constructs a graph starting from the given graph entry points, evaluating with the provided project collection. /// </summary> /// <param name="entryPoints">The entry points to use in constructing the graph</param> /// <param name="projectCollection">The collection with which all projects in the graph should be associated. May not be null.</param> /// <param name="projectInstanceFactory"> /// A delegate used for constructing a <see cref="ProjectInstance"/>, called for each /// project created during graph creation. This value can be null, which uses /// a default implementation that calls the ProjectInstance constructor. See the remarks /// on <see cref="ProjectInstanceFactoryFunc"/> for other scenarios. /// </param> /// <exception cref="AggregateException">If the evaluation of any project in the graph fails, the InnerException contains <see cref="InvalidProjectFileException"/> /// If a null reference is returned from <paramref name="projectInstanceFactory"/>, the InnerException contains <see cref="InvalidOperationException"/></exception> /// <exception cref="CircularDependencyException">If the evaluation is successful but the project graph contains a circular dependency</exception> public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) { ErrorUtilities.VerifyThrowArgumentNull(projectCollection, nameof(projectCollection)); projectInstanceFactory = projectInstanceFactory ?? DefaultProjectInstanceFactory; var nodeStates = new Dictionary <ProjectGraphNode, NodeState>(); var entryPointNodes = new List <ProjectGraphNode>(); var tasksInProgress = new ConcurrentDictionary <ConfigurationMetadata, object>(); var projectsToEvaluate = new ConcurrentQueue <ConfigurationMetadata>(); var entryPointConfigurationMetadata = new List <ConfigurationMetadata>(); foreach (var entryPoint in entryPoints) { PropertyDictionary <ProjectPropertyInstance> globalPropertyDictionary = CreatePropertyDictionary(entryPoint.GlobalProperties); var configurationMetadata = new ConfigurationMetadata(FileUtilities.NormalizePath(entryPoint.ProjectFile), globalPropertyDictionary); projectsToEvaluate.Enqueue(configurationMetadata); entryPointConfigurationMetadata.Add(configurationMetadata); } if (LoadGraph(projectsToEvaluate, projectCollection, tasksInProgress, projectInstanceFactory, out List <Exception> exceptions)) { foreach (var configurationMetadata in entryPointConfigurationMetadata) { entryPointNodes.Add(_allParsedProjects[configurationMetadata]); if (!nodeStates.TryGetValue(_allParsedProjects[configurationMetadata], out var _)) { DetectCycles(_allParsedProjects[configurationMetadata], nodeStates, projectCollection, configurationMetadata.GlobalProperties); } } var graphRoots = new List <ProjectGraphNode>(entryPointNodes.Count); foreach (var entryPointNode in entryPointNodes) { if (entryPointNode.ReferencingProjects.Count == 0) { graphRoots.Add(entryPointNode); } } EntryPointNodes = entryPointNodes.AsReadOnly(); ProjectNodes = _allParsedProjects.Values.ToList(); GraphRoots = graphRoots.AsReadOnly(); _projectNodesTopologicallySorted = new Lazy <IReadOnlyCollection <ProjectGraphNode> >(() => TopologicalSort(GraphRoots, ProjectNodes)); } else { throw new AggregateException(exceptions); } }
public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) : this( entryPoints, projectCollection, projectInstanceFactory, NativeMethodsShared.GetLogicalCoreCount(), CancellationToken.None) { }
(IReadOnlyCollection <ProjectGraphNode> EntryPointNodes, IReadOnlyCollection <ProjectGraphNode> RootNodes, IReadOnlyCollection <ProjectGraphNode> AllNodes) LoadGraph( List <ConfigurationMetadata> entryPointConfigurationMetadata, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory, ProjectInterpretation sdkInfo) { var allParsedProjects = new ConcurrentDictionary <ConfigurationMetadata, ProjectGraphNode>(); var tasksInProgress = new ConcurrentDictionary <ConfigurationMetadata, object>(); var projectsToEvaluate = new ConcurrentQueue <ConfigurationMetadata>(); foreach (var entryPoint in entryPointConfigurationMetadata) { projectsToEvaluate.Enqueue(entryPoint); } if (FindGraphNodes( projectsToEvaluate, projectCollection, tasksInProgress, projectInstanceFactory, sdkInfo, allParsedProjects, out var exceptions)) { var entryPointNodes = entryPointConfigurationMetadata.Select(e => allParsedProjects[e]).ToList(); CreateEdgesAndDetectCycles(entryPointConfigurationMetadata, sdkInfo, allParsedProjects); var graphRoots = GetGraphRoots(entryPointNodes); ProjectInterpretation.PostProcess(allParsedProjects); return(entryPointNodes.AsReadOnly(), graphRoots.AsReadOnly(), allParsedProjects.Values.ToList()); } else { throw new AggregateException(exceptions); } List <ProjectGraphNode> GetGraphRoots(List <ProjectGraphNode> entryPointNodes) { var graphRoots = new List <ProjectGraphNode>(entryPointNodes.Count); foreach (var entryPointNode in entryPointNodes) { if (entryPointNode.ReferencingProjects.Count == 0) { graphRoots.Add(entryPointNode); } } return(graphRoots); } }
/// <summary> /// Constructs a graph starting from the given graph entry points, evaluating with the provided project collection. /// </summary> /// <param name="entryPoints">The entry points to use in constructing the graph</param> /// <param name="projectCollection"> /// The collection with which all projects in the graph should be associated. May not be /// null. /// </param> /// <param name="projectInstanceFactory"> /// A delegate used for constructing a <see cref="ProjectInstance" />, called for each /// project created during graph creation. This value can be null, which uses /// a default implementation that calls the ProjectInstance constructor. See the remarks /// on <see cref="ProjectInstanceFactoryFunc" /> for other scenarios. /// </param> /// <exception cref="InvalidProjectFileException"> /// If the evaluation of any project in the graph fails /// </exception> /// <exception cref="InvalidOperationException"> /// If a null reference is returned from <paramref name="projectInstanceFactory" /> /// </exception> /// <exception cref="CircularDependencyException"> /// If the evaluation is successful but the project graph contains a circular /// dependency /// </exception> public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) : this( entryPoints, projectCollection, projectInstanceFactory, Environment.ProcessorCount, CancellationToken.None) { }
/// <summary> /// Constructs a graph starting from the given graph entry points, evaluating with the provided project collection. /// </summary> /// <param name="entryPoints">The entry points to use in constructing the graph</param> /// <param name="projectCollection">The collection with which all projects in the graph should be associated. May not be null.</param> /// <param name="projectInstanceFactory"> /// A delegate used for constructing a <see cref="ProjectInstance"/>, called for each /// project created during graph creation. This value can be null, which uses /// a default implementation that calls the ProjectInstance constructor. See the remarks /// on <see cref="ProjectInstanceFactoryFunc"/> for other scenarios. /// </param> /// <exception cref="AggregateException">If the evaluation of any project in the graph fails, the InnerException contains <see cref="InvalidProjectFileException"/> /// If a null reference is returned from <paramref name="projectInstanceFactory"/>, the InnerException contains <see cref="InvalidOperationException"/></exception> /// <exception cref="CircularDependencyException">If the evaluation is successful but the project graph contains a circular dependency</exception> public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) { ErrorUtilities.VerifyThrowArgumentNull(projectCollection, nameof(projectCollection)); projectInstanceFactory = projectInstanceFactory ?? DefaultProjectInstanceFactory; _projectInterpretation = ProjectInterpretation.Instance; var entryPointConfigurationMetadata = new List <ConfigurationMetadata>(); foreach (var entryPoint in entryPoints) { var globalPropertyDictionary = CreatePropertyDictionary(entryPoint.GlobalProperties); AddGraphBuildGlobalVariable(globalPropertyDictionary); var configurationMetadata = new ConfigurationMetadata(FileUtilities.NormalizePath(entryPoint.ProjectFile), globalPropertyDictionary); entryPointConfigurationMetadata.Add(configurationMetadata); } var(entryPointNodes, rootNodes, allNodes) = LoadGraph(entryPointConfigurationMetadata, projectCollection, projectInstanceFactory, _projectInterpretation); EntryPointNodes = entryPointNodes; GraphRoots = rootNodes; ProjectNodes = allNodes; _projectNodesTopologicallySorted = new Lazy <IReadOnlyCollection <ProjectGraphNode> >(() => TopologicalSort(GraphRoots, ProjectNodes)); void AddGraphBuildGlobalVariable(PropertyDictionary <ProjectPropertyInstance> globalPropertyDictionary) { if (globalPropertyDictionary.GetProperty(PropertyNames.IsGraphBuild) == null) { globalPropertyDictionary[PropertyNames.IsGraphBuild] = ProjectPropertyInstance.Create(PropertyNames.IsGraphBuild, "true"); } } }
/// <summary> /// Load a graph with root node at entryProjectFile /// Maintain a queue of projects to be processed and evaluate projects in parallel /// Returns false if loading the graph is not successful /// </summary> private bool LoadGraph( ConcurrentQueue <ConfigurationMetadata> projectsToEvaluate, ProjectCollection projectCollection, ConcurrentDictionary <ConfigurationMetadata, object> tasksInProgress, ProjectInstanceFactoryFunc projectInstanceFactory, out List <Exception> exceptions) { var exceptionsInTasks = new ConcurrentBag <Exception>(); var evaluationWaitHandle = new AutoResetEvent(false); while (projectsToEvaluate.Count != 0 || tasksInProgress.Count != 0) { ConfigurationMetadata projectToEvaluate; if (projectsToEvaluate.Count != 0) { projectToEvaluate = projectsToEvaluate.Dequeue(); var task = new Task(() => { ProjectGraphNode parsedProject = CreateNewNode(projectToEvaluate, projectCollection, projectInstanceFactory); IEnumerable <ProjectItemInstance> projectReferenceItems = parsedProject.ProjectInstance.GetItems(MSBuildConstants.ProjectReferenceItemName); foreach (var projectReferenceToParse in projectReferenceItems) { if (!string.IsNullOrEmpty(projectReferenceToParse.GetMetadataValue(ToolsVersionMetadataName))) { throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, ResourceUtilities.GetResourceString( "ProjectGraphDoesNotSupportProjectReferenceWithToolset"), projectReferenceToParse.EvaluatedInclude, parsedProject.ProjectInstance.FullPath)); } string projectReferenceFullPath = projectReferenceToParse.GetMetadataValue(FullPathMetadataName); PropertyDictionary <ProjectPropertyInstance> projectReferenceGlobalProperties = GetProjectReferenceGlobalProperties(projectReferenceToParse, projectToEvaluate.GlobalProperties); var projectReferenceConfigurationMetadata = new ConfigurationMetadata(projectReferenceFullPath, projectReferenceGlobalProperties); if (!tasksInProgress.ContainsKey(projectReferenceConfigurationMetadata)) { if (!_allParsedProjects.ContainsKey(projectReferenceConfigurationMetadata)) { projectsToEvaluate.Enqueue(projectReferenceConfigurationMetadata); evaluationWaitHandle.Set(); } } } }); if (tasksInProgress.TryAdd(projectToEvaluate, null)) { // once the task completes, remove it from tasksInProgress using a chained task // signal the wait handle to process new projects that have been discovered by this task or exit if all projects have been evaluated task.ContinueWith(_ => { if (task.IsFaulted) { exceptionsInTasks.Add(task.Exception.InnerException); } tasksInProgress.TryRemove(projectToEvaluate, out var _); evaluationWaitHandle.Set(); }); task.Start(); } } else { // if projectsToEvaluate is empty but there are tasks in progress, there is nothing to do till a task completes and discovers new projects // wait till a task completes and sends a signal evaluationWaitHandle.WaitOne(); } } if (exceptionsInTasks.Count != 0) { exceptions = exceptionsInTasks.ToList(); return(false); } exceptions = null; return(true); }
/// <summary> /// Constructs a graph starting from the given project file, evaluating with the global project collection and no global properties. /// </summary> /// <param name="entryProjectFile">The project file to use as the entry point in constructing the graph</param> /// <param name="projectCollection">The collection with which all projects in the graph should be associated. May not be null.</param> /// <param name="projectInstanceFactory"> /// A delegate used for constructing a <see cref="ProjectInstance"/>, called for each /// project created during graph creation. This value can be null, which uses /// a default implementation that calls the ProjectInstance constructor. See the remarks /// on the <see cref="ProjectInstanceFactoryFunc"/> for other scenarios. /// </param> /// <exception cref="AggregateException">If the evaluation of any project in the graph fails, the InnerException contains <see cref="InvalidProjectFileException"/> /// If a null reference is returned from <paramref name="projectInstanceFactory"/>, the InnerException contains <see cref="InvalidOperationException"/></exception> /// </exception> public ProjectGraph(string entryProjectFile, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory) : this(new ProjectGraphEntryPoint(entryProjectFile).AsEnumerable(), projectCollection, projectInstanceFactory) { }
public ProjectGraph( IEnumerable <ProjectGraphEntryPoint> entryPoints, ProjectCollection projectCollection, ProjectInstanceFactoryFunc projectInstanceFactory, int degreeOfParallelism, CancellationToken cancellationToken) { ErrorUtilities.VerifyThrowArgumentNull(projectCollection, nameof(projectCollection)); var measurementInfo = BeginMeasurement(); projectInstanceFactory ??= DefaultProjectInstanceFactory; var graphBuilder = new GraphBuilder( entryPoints, projectCollection, projectInstanceFactory, ProjectInterpretation.Instance, degreeOfParallelism, cancellationToken); graphBuilder.BuildGraph(); EntryPointNodes = graphBuilder.EntryPointNodes; GraphRoots = graphBuilder.RootNodes; ProjectNodes = graphBuilder.ProjectNodes; Edges = graphBuilder.Edges; _projectNodesTopologicallySorted = new Lazy <IReadOnlyCollection <ProjectGraphNode> >(() => TopologicalSort(GraphRoots, ProjectNodes)); ConstructionMetrics = EndMeasurement(); (Stopwatch Timer, string ETWArgs) BeginMeasurement() { string etwArgs = null; if (MSBuildEventSource.Log.IsEnabled()) { etwArgs = string.Join(";", entryPoints.Select( e => { var globalPropertyString = e.GlobalProperties == null ? string.Empty : string.Join(", ", e.GlobalProperties.Select(kvp => $"{kvp.Key} = {kvp.Value}")); return($"{e.ProjectFile}({globalPropertyString})"); })); MSBuildEventSource.Log.ProjectGraphConstructionStart(etwArgs); } return(Stopwatch.StartNew(), etwArgs); } GraphConstructionMetrics EndMeasurement() { if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.ProjectGraphConstructionStop(measurementInfo.ETWArgs); } measurementInfo.Timer.Stop(); return(new GraphConstructionMetrics( measurementInfo.Timer.Elapsed, ProjectNodes.Count, Edges.Count)); } }
/// <summary> /// Load a graph with root node at entryProjectFile /// Maintain a queue of projects to be processed and evaluate projects in parallel /// Returns false if loading the graph is not successful /// </summary> private bool FindGraphNodes( ConcurrentQueue <ConfigurationMetadata> projectsToEvaluate, ProjectCollection projectCollection, ConcurrentDictionary <ConfigurationMetadata, object> tasksInProgress, ProjectInstanceFactoryFunc projectInstanceFactory, ProjectInterpretation projectInterpretation, ConcurrentDictionary <ConfigurationMetadata, ProjectGraphNode> allParsedProjects, out List <Exception> exceptions) { var exceptionsInTasks = new ConcurrentBag <Exception>(); var evaluationWaitHandle = new AutoResetEvent(false); while (projectsToEvaluate.Count != 0 || tasksInProgress.Count != 0) { ConfigurationMetadata projectToEvaluate; if (projectsToEvaluate.Count != 0) { projectToEvaluate = projectsToEvaluate.Dequeue(); var task = new Task(() => { var parsedProject = CreateNewNode(projectToEvaluate, projectCollection, projectInstanceFactory, allParsedProjects); foreach (var referenceConfig in projectInterpretation.GetReferences(parsedProject.ProjectInstance)) { /*todo: fix the following double check-then-act concurrency bug: one thread can pass the two checks, loose context, * meanwhile another thread passes the same checks with the same data and inserts its reference. The initial thread regains context * and duplicates the information, leading to wasted work */ if (!tasksInProgress.ContainsKey(referenceConfig)) { if (!allParsedProjects.ContainsKey(referenceConfig)) { projectsToEvaluate.Enqueue(referenceConfig); evaluationWaitHandle.Set(); } } } }); if (tasksInProgress.TryAdd(projectToEvaluate, null)) { // once the task completes, remove it from tasksInProgress using a chained task // signal the wait handle to process new projects that have been discovered by this task or exit if all projects have been evaluated task.ContinueWith(_ => { if (task.IsFaulted) { exceptionsInTasks.Add(task.Exception.InnerException); } tasksInProgress.TryRemove(projectToEvaluate, out var _); evaluationWaitHandle.Set(); }); task.Start(); } } else { // if projectsToEvaluate is empty but there are tasks in progress, there is nothing to do till a task completes and discovers new projects // wait till a task completes and sends a signal evaluationWaitHandle.WaitOne(); } } if (exceptionsInTasks.Count != 0) { exceptions = exceptionsInTasks.ToList(); return(false); } exceptions = null; return(true); }