@@ -500,37 +500,51 @@ type ProjectLoader2 =
500500 /// <returns>A project graph representing the evaluated projects, each corresponding to a specific TargetFramework
501501 /// or TargetFrameworks defined in the project files.</returns>
502502 /// <remarks>
503- /// This method evaluates each project file and checks for the presence of a "TargetFramework"
504- /// property. If it exists, the project is returned as is. If it does not exist, it checks for the "TargetFrameworks"
505- /// property and splits it into individual TargetFrameworks. For each TargetFramework, it creates
506- /// a new project with the "TargetFramework" global property set to that TargetFramework.
503+ ///
504+ /// MSBuild's ProjectGraph natively handles multi-targeting by creating "outer build" nodes (with TargetFrameworks)
505+ /// that reference "inner build" nodes (with individual TargetFramework values). However, when building a graph,
506+ /// only entry point nodes are built directly. This method performs two evaluations:
507+ /// 1. First pass: Discover all nodes in the graph (including outer/inner builds and all references)
508+ /// 2. Second pass: Create a new graph with only nodes that have a TargetFramework property set
509+ ///
510+ /// This ensures that all inner builds are treated as entry points and get built directly, which is required
511+ /// for design-time analysis scenarios where we need build results for each TFM.
507512 /// </remarks>
508513 static member EvaluateAsGraphAllTfms ( entryProjectFile : ProjectGraphEntryPoint seq , ? projectCollection : ProjectCollection , ? projectInstanceFactory ) =
509- // For some reason, the graph evaluation doesn't handle multiple TFMs well
510- // So first we evaluate the graph to find all projects
514+ // MSBuild's ProjectGraph handles multi-TFM projects by creating:
515+ // - OuterBuild nodes: TargetFramework is empty, TargetFrameworks is set (dispatchers)
516+ // - InnerBuild nodes: TargetFramework is set (actual builds per TFM)
517+ // - NonMultitargeting nodes: Neither property meaningfully set
518+ //
519+ // First pass: Evaluate to discover all project nodes including inner builds created from outer builds
511520 let graph =
512521 ProjectLoader2.EvaluateAsGraph( entryProjectFile, ?projectCollection = projectCollection, ?projectInstanceFactory = projectInstanceFactory)
513522
514- let inline tryGetTfmFromGlobalProps ( node : ProjectGraphNode ) =
523+ // Helper to get TargetFramework from global properties or project properties
524+ let tryGetTargetFramework ( node : ProjectGraphNode ) =
515525 match node.ProjectInstance.GlobalProperties.TryGetValue " TargetFramework" with
516- | true , tfm -> Some tfm
517- | _ -> None
518-
519- let inline tryGetFromProps ( node : ProjectGraphNode ) =
520- node.ProjectInstance.Properties
521- |> ProjectPropertyInstance.tryFind " TargetFramework"
526+ | true , tfm when not ( String.IsNullOrWhiteSpace tfm) -> Some tfm
527+ | _ ->
528+ node.ProjectInstance.Properties
529+ |> ProjectPropertyInstance.tryFind " TargetFramework"
530+ |> Option.filter (
531+ not
532+ << String.IsNullOrWhiteSpace
533+ )
522534
523- // Then we only care about those with a TargetFramework
524- let projects =
535+ // Extract only nodes with a TargetFramework as new entry points
536+ // This filters out outer builds (which have TargetFrameworks but not TargetFramework)
537+ // and includes inner builds (which have TargetFramework set)
538+ let innerBuildEntryPoints =
525539 graph.ProjectNodes
526540 |> Seq.choose ( fun node ->
527- tryGetTfmFromGlobalProps node
528- |> Option.orElseWith ( fun () -> tryGetFromProps node)
541+ tryGetTargetFramework node
529542 |> Option.map ( fun _ -> ProjectGraphEntryPoint( node.ProjectInstance.FullPath, globalProperties = node.ProjectInstance.GlobalProperties))
530543 )
531544
532- // Then, re-evaluate the graph with those projects
533- ProjectLoader2.EvaluateAsGraph( projects, ?projectCollection = projectCollection, ?projectInstanceFactory = projectInstanceFactory)
545+ // Second pass: Re-evaluate with inner builds as entry points
546+ // This ensures all inner builds are built directly and appear in ResultsByNode
547+ ProjectLoader2.EvaluateAsGraph( innerBuildEntryPoints, ?projectCollection = projectCollection, ?projectInstanceFactory = projectInstanceFactory)
534548
535549 /// <summary>
536550 /// Executes a build request against the BuildManagerSession.
@@ -664,47 +678,6 @@ type ProjectLoader2 =
664678 /// <param name="graphBuildResult">The GraphBuildResult to get the project instances from.</param>
665679 /// <returns>The project instances from the GraphBuildResult.</returns>
666680 static member GetProjectInstances ( graphBuildResult : GraphBuildResult ) =
667-
668- // let start =
669- // graphBuildResult.ResultsByNode
670- // |> Seq.map (fun (KeyValue(node, _)) -> node)
671-
672- // let projectsToVisit = Queue<ProjectGraphNode>(start)
673-
674- // let visited = HashSet<ProjectGraphNode>()
675- // let results = ResizeArray<ProjectInstance>()
676-
677- // while projectsToVisit.Count > 0 do
678- // let p = projectsToVisit.Dequeue()
679-
680- // match visited.TryGetValue p with
681- // | true, _ -> ()
682- // | _ ->
683- // visited.Add(p)
684- // |> ignore
685-
686- // p.ProjectReferences
687- // |> Seq.iter (fun r -> projectsToVisit.Enqueue r)
688-
689-
690- // p.ProjectInstance.Properties
691- // |> ProjectPropertyInstance.tryFind "TargetFramework"
692- // |> Option.iter (fun _ -> results.Add p.ProjectInstance)
693-
694-
695- // results :> seq<_>
696- // graphBuildResult.ResultsByNode
697- // |> Seq.collect(fun (KeyValue(node,_)) ->
698-
699- // let pi = node.ProjectInstance
700- // match pi.Properties |> ProjectPropertyInstance.tryFind "TargetFrameworks" with
701- // | Some x ->
702- // Seq.empty
703- // | _ ->
704- // Seq.singleton pi
705-
706- // )
707-
708681 graphBuildResult.ResultsByNode
709682 |> Seq.map ( fun ( KeyValue ( node , result )) -> ProjectLoader2.GetProjectInstance result)
710683
0 commit comments