11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Collections . Immutable ;
45using Microsoft . Build . Graph ;
56
67namespace Microsoft . DotNet . Watch ;
78
8- internal sealed class EvaluationResult ( IReadOnlyDictionary < string , FileItem > files , ProjectGraph ? projectGraph )
9+ internal sealed class EvaluationResult ( IReadOnlyDictionary < string , FileItem > files , ProjectGraph projectGraph )
910{
1011 public readonly IReadOnlyDictionary < string , FileItem > Files = files ;
11- public readonly ProjectGraph ? ProjectGraph = projectGraph ;
12+ public readonly ProjectGraph ProjectGraph = projectGraph ;
1213
1314 public readonly FilePathExclusions ItemExclusions
1415 = projectGraph != null ? FilePathExclusions . Create ( projectGraph ) : FilePathExclusions . Empty ;
1516
1617 private readonly Lazy < IReadOnlySet < string > > _lazyBuildFiles
1718 = new ( ( ) => projectGraph != null ? CreateBuildFileSet ( projectGraph ) : new HashSet < string > ( ) ) ;
1819
19- public static IReadOnlySet < string > CreateBuildFileSet ( ProjectGraph projectGraph )
20+ private static IReadOnlySet < string > CreateBuildFileSet ( ProjectGraph projectGraph )
2021 => projectGraph . ProjectNodes . SelectMany ( p => p . ProjectInstance . ImportPaths )
2122 . Concat ( projectGraph . ProjectNodes . Select ( p => p . ProjectInstance . FullPath ) )
2223 . ToHashSet ( PathUtilities . OSSpecificPathComparer ) ;
@@ -29,4 +30,138 @@ public void WatchFiles(FileWatcher fileWatcher)
2930 fileWatcher . WatchContainingDirectories ( Files . Keys , includeSubdirectories : true ) ;
3031 fileWatcher . WatchFiles ( BuildFiles ) ;
3132 }
33+
34+ /// <summary>
35+ /// Loads project graph and performs design-time build.
36+ /// </summary>
37+ public static EvaluationResult ? TryCreate (
38+ string rootProjectPath ,
39+ IEnumerable < string > buildArguments ,
40+ IReporter reporter ,
41+ EnvironmentOptions environmentOptions ,
42+ bool restore ,
43+ CancellationToken cancellationToken )
44+ {
45+ var buildReporter = new BuildReporter ( reporter , environmentOptions ) ;
46+
47+ // See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md
48+
49+ var globalOptions = CommandLineOptions . ParseBuildProperties ( buildArguments )
50+ . ToImmutableDictionary ( keySelector : arg => arg . key , elementSelector : arg => arg . value )
51+ . SetItem ( PropertyNames . DotNetWatchBuild , "true" )
52+ . SetItem ( PropertyNames . DesignTimeBuild , "true" )
53+ . SetItem ( PropertyNames . SkipCompilerExecution , "true" )
54+ . SetItem ( PropertyNames . ProvideCommandLineArgs , "true" )
55+ // F# targets depend on host path variable:
56+ . SetItem ( "DOTNET_HOST_PATH" , environmentOptions . MuxerPath ) ;
57+
58+ var projectGraph = ProjectGraphUtilities . TryLoadProjectGraph (
59+ rootProjectPath ,
60+ globalOptions ,
61+ reporter ,
62+ projectGraphRequired : true ,
63+ cancellationToken ) ;
64+
65+ if ( projectGraph == null )
66+ {
67+ return null ;
68+ }
69+
70+ var rootNode = projectGraph . GraphRoots . Single ( ) ;
71+
72+ if ( restore )
73+ {
74+ using ( var loggers = buildReporter . GetLoggers ( rootNode . ProjectInstance . FullPath , "Restore" ) )
75+ {
76+ if ( ! rootNode . ProjectInstance . Build ( [ TargetNames . Restore ] , loggers ) )
77+ {
78+ reporter . Error ( $ "Failed to restore project '{ rootProjectPath } '.") ;
79+ loggers . ReportOutput ( ) ;
80+ return null ;
81+ }
82+ }
83+ }
84+
85+ var fileItems = new Dictionary < string , FileItem > ( ) ;
86+
87+ foreach ( var project in projectGraph . ProjectNodesTopologicallySorted )
88+ {
89+ // Deep copy so that we can reuse the graph for building additional targets later on.
90+ // If we didn't copy the instance the targets might duplicate items that were already
91+ // populated by design-time build.
92+ var projectInstance = project . ProjectInstance . DeepCopy ( ) ;
93+
94+ // skip outer build project nodes:
95+ if ( projectInstance . GetPropertyValue ( PropertyNames . TargetFramework ) == "" )
96+ {
97+ continue ;
98+ }
99+
100+ var customCollectWatchItems = projectInstance . GetStringListPropertyValue ( PropertyNames . CustomCollectWatchItems ) ;
101+
102+ using ( var loggers = buildReporter . GetLoggers ( projectInstance . FullPath , "DesignTimeBuild" ) )
103+ {
104+ if ( ! projectInstance . Build ( [ TargetNames . Compile , .. customCollectWatchItems ] , loggers ) )
105+ {
106+ reporter . Error ( $ "Failed to build project '{ projectInstance . FullPath } '.") ;
107+ loggers . ReportOutput ( ) ;
108+ return null ;
109+ }
110+ }
111+
112+ var projectPath = projectInstance . FullPath ;
113+ var projectDirectory = Path . GetDirectoryName ( projectPath ) ! ;
114+
115+ // TODO: Compile and AdditionalItems should be provided by Roslyn
116+ var items = projectInstance . GetItems ( ItemNames . Compile )
117+ . Concat ( projectInstance . GetItems ( ItemNames . AdditionalFiles ) )
118+ . Concat ( projectInstance . GetItems ( ItemNames . Watch ) ) ;
119+
120+ foreach ( var item in items )
121+ {
122+ AddFile ( item . EvaluatedInclude , staticWebAssetPath : null ) ;
123+ }
124+
125+ if ( ! environmentOptions . SuppressHandlingStaticContentFiles &&
126+ projectInstance . GetBooleanPropertyValue ( PropertyNames . UsingMicrosoftNETSdkRazor ) &&
127+ projectInstance . GetBooleanPropertyValue ( PropertyNames . DotNetWatchContentFiles , defaultValue : true ) )
128+ {
129+ foreach ( var item in projectInstance . GetItems ( ItemNames . Content ) )
130+ {
131+ if ( item . GetBooleanMetadataValue ( MetadataNames . Watch , defaultValue : true ) )
132+ {
133+ var relativeUrl = item . EvaluatedInclude . Replace ( '\\ ' , '/' ) ;
134+ if ( relativeUrl . StartsWith ( "wwwroot/" ) )
135+ {
136+ AddFile ( item . EvaluatedInclude , staticWebAssetPath : relativeUrl ) ;
137+ }
138+ }
139+ }
140+ }
141+
142+ void AddFile ( string include , string ? staticWebAssetPath )
143+ {
144+ var filePath = Path . GetFullPath ( Path . Combine ( projectDirectory , include ) ) ;
145+
146+ if ( ! fileItems . TryGetValue ( filePath , out var existingFile ) )
147+ {
148+ fileItems . Add ( filePath , new FileItem
149+ {
150+ FilePath = filePath ,
151+ ContainingProjectPaths = [ projectPath ] ,
152+ StaticWebAssetPath = staticWebAssetPath ,
153+ } ) ;
154+ }
155+ else if ( ! existingFile . ContainingProjectPaths . Contains ( projectPath ) )
156+ {
157+ // linked files might be included to multiple projects:
158+ existingFile . ContainingProjectPaths . Add ( projectPath ) ;
159+ }
160+ }
161+ }
162+
163+ buildReporter . ReportWatchedFiles ( fileItems ) ;
164+
165+ return new EvaluationResult ( fileItems , projectGraph ) ;
166+ }
32167}
0 commit comments