1
+ // Copyright (c) .NET Foundation. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3
+
4
+ using System ;
5
+ using System . Collections . Generic ;
6
+ using System . IO ;
7
+ using System . Text ;
8
+ using System . Xml ;
9
+ using System . Xml . Linq ;
10
+ using Microsoft . Build . Framework ;
11
+ using Microsoft . Build . Utilities ;
12
+
13
+ namespace Microsoft . AspNetCore . Razor . Tasks
14
+ {
15
+ public class GenerateStaticWebAssetsManifest : Task
16
+ {
17
+ private const string ContentRoot = "ContentRoot" ;
18
+ private const string BasePath = "BasePath" ;
19
+
20
+ [ Required ]
21
+ public string TargetManifestPath { get ; set ; }
22
+
23
+ [ Required ]
24
+ public ITaskItem [ ] ContentRootDefinitions { get ; set ; }
25
+
26
+ public override bool Execute ( )
27
+ {
28
+ if ( ! ValidateArguments ( ) )
29
+ {
30
+ return false ;
31
+ }
32
+
33
+ return ExecuteCore ( ) ;
34
+ }
35
+
36
+ private bool ExecuteCore ( )
37
+ {
38
+ var document = new XDocument ( new XDeclaration ( "1.0" , "utf-8" , "yes" ) ) ;
39
+ var root = new XElement (
40
+ "StaticWebAssets" ,
41
+ new XAttribute ( "Version" , "1.0" ) ,
42
+ CreateNodes ( ) ) ;
43
+
44
+ document . Add ( root ) ;
45
+
46
+ var settings = new XmlWriterSettings
47
+ {
48
+ Encoding = Encoding . UTF8 ,
49
+ CloseOutput = true ,
50
+ OmitXmlDeclaration = true ,
51
+ Indent = true ,
52
+ NewLineOnAttributes = false ,
53
+ Async = true
54
+ } ;
55
+
56
+ using ( var xmlWriter = GetXmlWriter ( settings ) )
57
+ {
58
+ document . WriteTo ( xmlWriter ) ;
59
+ }
60
+
61
+ return ! Log . HasLoggedErrors ;
62
+ }
63
+
64
+ private IEnumerable < XElement > CreateNodes ( )
65
+ {
66
+ var nodes = new List < XElement > ( ) ;
67
+ for ( var i = 0 ; i < ContentRootDefinitions . Length ; i ++ )
68
+ {
69
+ var contentRootDefinition = ContentRootDefinitions [ i ] ;
70
+ var basePath = contentRootDefinition . GetMetadata ( BasePath ) ;
71
+ var contentRoot = contentRootDefinition . GetMetadata ( ContentRoot ) ;
72
+
73
+ // basePath is meant to be a prefix for the files under contentRoot. MSbuild
74
+ // normalizes '\' according to the OS, but this is going to be part of the url
75
+ // so it needs to always be '/'.
76
+ var normalizedBasePath = basePath . Replace ( "\\ " , "/" ) ;
77
+
78
+ // At this point we already know that there are no elements with different base paths and same content roots
79
+ // or viceversa. Here we simply skip additional items that have the same base path and same content root.
80
+ if ( ! nodes . Exists ( e => e . Attribute ( BasePath ) . Value . Equals ( normalizedBasePath , StringComparison . OrdinalIgnoreCase ) ) )
81
+ {
82
+ nodes . Add ( new XElement ( "ContentRoot" ,
83
+ new XAttribute ( "BasePath" , normalizedBasePath ) ,
84
+ new XAttribute ( "Path" , contentRoot ) ) ) ;
85
+ }
86
+ }
87
+
88
+ return nodes ;
89
+ }
90
+
91
+ private XmlWriter GetXmlWriter ( XmlWriterSettings settings )
92
+ {
93
+ var fileStream = new FileStream ( TargetManifestPath , FileMode . Create ) ;
94
+ return XmlWriter . Create ( fileStream , settings ) ;
95
+ }
96
+
97
+ private bool ValidateArguments ( )
98
+ {
99
+ for ( var i = 0 ; i < ContentRootDefinitions . Length ; i ++ )
100
+ {
101
+ var contentRootDefinition = ContentRootDefinitions [ i ] ;
102
+ if ( ! EnsureRequiredMetadata ( contentRootDefinition , BasePath ) ||
103
+ ! EnsureRequiredMetadata ( contentRootDefinition , ContentRoot ) )
104
+ {
105
+ return false ;
106
+ }
107
+ }
108
+
109
+ // We want to validate that there are no different item groups that share either the same base path
110
+ // but different content roots or that share the same content root but different base paths.
111
+ // We pass in all the static web assets that we discovered to this task without making any distinction for
112
+ // duplicates, so here we skip elements for which we are already tracking an element with the same
113
+ // content root path and same base path.
114
+
115
+ // Case-sensitivity depends on the underlying OS so we are not going to do anything to enforce it here.
116
+ // Any two items that match base path and content root in a case-insensitive way won't produce an error.
117
+ // Any other two items will produce an error even if there is only a casing difference between either the
118
+ // base paths or the content roots.
119
+ var basePaths = new Dictionary < string , ITaskItem > ( StringComparer . OrdinalIgnoreCase ) ;
120
+ var contentRootPaths = new Dictionary < string , ITaskItem > ( StringComparer . OrdinalIgnoreCase ) ;
121
+
122
+ for ( var i = 0 ; i < ContentRootDefinitions . Length ; i ++ )
123
+ {
124
+ var contentRootDefinition = ContentRootDefinitions [ i ] ;
125
+ var basePath = contentRootDefinition . GetMetadata ( BasePath ) ;
126
+ var contentRoot = contentRootDefinition . GetMetadata ( ContentRoot ) ;
127
+
128
+ if ( basePaths . TryGetValue ( basePath , out var existingBasePath ) )
129
+ {
130
+ var existingBasePathContentRoot = existingBasePath . GetMetadata ( ContentRoot ) ;
131
+ if ( ! string . Equals ( contentRoot , existingBasePathContentRoot , StringComparison . OrdinalIgnoreCase ) )
132
+ {
133
+ // Case:
134
+ // Item1: /_content/Library, /package/aspnetContent1
135
+ // Item2: /_content/Library, /package/aspnetContent2
136
+ Log . LogError ( $ "Duplicate base paths '{ basePath } ' for content root paths '{ contentRoot } ' and '{ existingBasePathContentRoot } '. " +
137
+ $ "('{ contentRootDefinition . ItemSpec } ', '{ existingBasePath . ItemSpec } ')") ;
138
+ return false ;
139
+ }
140
+ // It was a duplicate, so we skip it.
141
+ // Case:
142
+ // Item1: /_content/Library, /package/aspnetContent
143
+ // Item2: /_content/Library, /package/aspnetContent
144
+ }
145
+ else
146
+ {
147
+ if ( contentRootPaths . TryGetValue ( contentRoot , out var existingContentRoot ) )
148
+ {
149
+ // Case:
150
+ // Item1: /_content/Library1, /package/aspnetContent
151
+ // Item2: /_content/Library2, /package/aspnetContent
152
+ Log . LogError ( $ "Duplicate content root paths '{ contentRoot } ' for base paths '{ basePath } ' and '{ existingContentRoot . GetMetadata ( BasePath ) } ' " +
153
+ $ "('{ contentRootDefinition . ItemSpec } ', '{ existingContentRoot . ItemSpec } ')") ;
154
+ return false ;
155
+ }
156
+ }
157
+
158
+ if ( ! basePaths . ContainsKey ( basePath ) )
159
+ {
160
+ basePaths . Add ( basePath , contentRootDefinition ) ;
161
+ }
162
+
163
+ if ( ! contentRootPaths . ContainsKey ( contentRoot ) )
164
+ {
165
+ contentRootPaths . Add ( contentRoot , contentRootDefinition ) ;
166
+ }
167
+ }
168
+
169
+ return true ;
170
+ }
171
+
172
+ private bool EnsureRequiredMetadata ( ITaskItem item , string metadataName )
173
+ {
174
+ var value = item . GetMetadata ( metadataName ) ;
175
+ if ( string . IsNullOrEmpty ( value ) )
176
+ {
177
+ Log . LogError ( $ "Missing required metadata '{ metadataName } ' for '{ item . ItemSpec } '.") ;
178
+ return false ;
179
+ }
180
+
181
+ return true ;
182
+ }
183
+ }
184
+ }
0 commit comments