6
6
using System . Linq ;
7
7
using System . Reflection ;
8
8
using System . Text ;
9
+ using System . Xml ;
9
10
using BenchmarkDotNet . Characteristics ;
10
11
using BenchmarkDotNet . Extensions ;
11
12
using BenchmarkDotNet . Helpers ;
@@ -22,8 +23,20 @@ public class CsProjGenerator : DotNetCliGenerator, IEquatable<CsProjGenerator>
22
23
{
23
24
private const string DefaultSdkName = "Microsoft.NET.Sdk" ;
24
25
25
- private static readonly ImmutableArray < string > SettingsWeWantToCopy =
26
- new [ ] { "NetCoreAppImplicitPackageVersion" , "RuntimeFrameworkVersion" , "PackageTargetFallback" , "LangVersion" , "UseWpf" , "UseWindowsForms" , "CopyLocalLockFileAssemblies" , "PreserveCompilationContext" , "UserSecretsId" , "EnablePreviewFeatures" } . ToImmutableArray ( ) ;
26
+ private static readonly ImmutableArray < string > SettingsWeWantToCopy = new [ ]
27
+ {
28
+ "NetCoreAppImplicitPackageVersion" ,
29
+ "RuntimeFrameworkVersion" ,
30
+ "PackageTargetFallback" ,
31
+ "LangVersion" ,
32
+ "UseWpf" ,
33
+ "UseWindowsForms" ,
34
+ "CopyLocalLockFileAssemblies" ,
35
+ "PreserveCompilationContext" ,
36
+ "UserSecretsId" ,
37
+ "EnablePreviewFeatures" ,
38
+ "PackageReference"
39
+ } . ToImmutableArray ( ) ;
27
40
28
41
public string RuntimeFrameworkVersion { get ; }
29
42
@@ -57,24 +70,23 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
57
70
var benchmark = buildPartition . RepresentativeBenchmarkCase ;
58
71
var projectFile = GetProjectFilePath ( benchmark . Descriptor . Type , logger ) ;
59
72
60
- using ( var file = new StreamReader ( File . OpenRead ( projectFile . FullName ) ) )
61
- {
62
- var ( customProperties , sdkName ) = GetSettingsThatNeedsToBeCopied ( file , projectFile ) ;
63
-
64
- var content = new StringBuilder ( ResourceHelper . LoadTemplate ( "CsProj.txt" ) )
65
- . Replace ( "$PLATFORM$" , buildPartition . Platform . ToConfig ( ) )
66
- . Replace ( "$CODEFILENAME$" , Path . GetFileName ( artifactsPaths . ProgramCodePath ) )
67
- . Replace ( "$CSPROJPATH$" , projectFile . FullName )
68
- . Replace ( "$TFM$" , TargetFrameworkMoniker )
69
- . Replace ( "$PROGRAMNAME$" , artifactsPaths . ProgramName )
70
- . Replace ( "$RUNTIMESETTINGS$" , GetRuntimeSettings ( benchmark . Job . Environment . Gc , buildPartition . Resolver ) )
71
- . Replace ( "$COPIEDSETTINGS$" , customProperties )
72
- . Replace ( "$CONFIGURATIONNAME$" , buildPartition . BuildConfiguration )
73
- . Replace ( "$SDKNAME$" , sdkName )
74
- . ToString ( ) ;
75
-
76
- File . WriteAllText ( artifactsPaths . ProjectFilePath , content ) ;
77
- }
73
+ var xmlDoc = new XmlDocument ( ) ;
74
+ xmlDoc . Load ( projectFile . FullName ) ;
75
+ var ( customProperties , sdkName ) = GetSettingsThatNeedToBeCopied ( xmlDoc , projectFile ) ;
76
+
77
+ var content = new StringBuilder ( ResourceHelper . LoadTemplate ( "CsProj.txt" ) )
78
+ . Replace ( "$PLATFORM$" , buildPartition . Platform . ToConfig ( ) )
79
+ . Replace ( "$CODEFILENAME$" , Path . GetFileName ( artifactsPaths . ProgramCodePath ) )
80
+ . Replace ( "$CSPROJPATH$" , projectFile . FullName )
81
+ . Replace ( "$TFM$" , TargetFrameworkMoniker )
82
+ . Replace ( "$PROGRAMNAME$" , artifactsPaths . ProgramName )
83
+ . Replace ( "$RUNTIMESETTINGS$" , GetRuntimeSettings ( benchmark . Job . Environment . Gc , buildPartition . Resolver ) )
84
+ . Replace ( "$COPIEDSETTINGS$" , customProperties )
85
+ . Replace ( "$CONFIGURATIONNAME$" , buildPartition . BuildConfiguration )
86
+ . Replace ( "$SDKNAME$" , sdkName )
87
+ . ToString ( ) ;
88
+
89
+ File . WriteAllText ( artifactsPaths . ProjectFilePath , content ) ;
78
90
}
79
91
80
92
/// <summary>
@@ -97,45 +109,134 @@ protected virtual string GetRuntimeSettings(GcMode gcMode, IResolver resolver)
97
109
// the host project or one of the .props file that it imports might contain some custom settings that needs to be copied, sth like
98
110
// <NetCoreAppImplicitPackageVersion>2.0.0-beta-001607-00</NetCoreAppImplicitPackageVersion>
99
111
// <RuntimeFrameworkVersion>2.0.0-beta-001607-00</RuntimeFrameworkVersion>
100
- internal ( string customProperties , string sdkName ) GetSettingsThatNeedsToBeCopied ( TextReader streamReader , FileInfo projectFile )
112
+ internal ( string customProperties , string sdkName ) GetSettingsThatNeedToBeCopied ( XmlDocument xmlDoc , FileInfo projectFile )
101
113
{
102
114
if ( ! string . IsNullOrEmpty ( RuntimeFrameworkVersion ) ) // some power users knows what to configure, just do it and copy nothing more
103
- return ( $ "<RuntimeFrameworkVersion>{ RuntimeFrameworkVersion } </RuntimeFrameworkVersion>", DefaultSdkName ) ;
115
+ {
116
+ return ( @$ "<PropertyGroup>
117
+ <RuntimeFrameworkVersion>{ RuntimeFrameworkVersion } </RuntimeFrameworkVersion>
118
+ </PropertyGroup>" , DefaultSdkName ) ;
119
+ }
120
+
121
+ XmlElement projectElement = xmlDoc . DocumentElement ;
122
+ // custom SDKs are not added for non-netcoreapp apps (like net471), so when the TFM != netcoreapp we dont parse "<Import Sdk="
123
+ // we don't allow for that mostly to prevent from edge cases like the following
124
+ // <Import Sdk="Microsoft.NET.Sdk.WindowsDesktop" Project="Sdk.props" Condition="'$(TargetFramework)'=='netcoreapp3.0'"/>
125
+ string sdkName = null ;
126
+ if ( TargetFrameworkMoniker . StartsWith ( "netcoreapp" , StringComparison . InvariantCultureIgnoreCase ) )
127
+ {
128
+ foreach ( XmlElement importElement in projectElement . GetElementsByTagName ( "Import" ) )
129
+ {
130
+ sdkName = importElement . GetAttribute ( "Sdk" ) ;
131
+ if ( ! string . IsNullOrEmpty ( sdkName ) )
132
+ {
133
+ break ;
134
+ }
135
+ }
136
+ }
137
+ if ( string . IsNullOrEmpty ( sdkName ) )
138
+ {
139
+ sdkName = projectElement . GetAttribute ( "Sdk" ) ;
140
+ }
141
+ // If Sdk isn't an attribute on the Project element, it could be a child element.
142
+ if ( string . IsNullOrEmpty ( sdkName ) )
143
+ {
144
+ foreach ( XmlElement sdkElement in projectElement . GetElementsByTagName ( "Sdk" ) )
145
+ {
146
+ sdkName = sdkElement . GetAttribute ( "Name" ) ;
147
+ if ( string . IsNullOrEmpty ( sdkName ) )
148
+ {
149
+ continue ;
150
+ }
151
+ string version = sdkElement . GetAttribute ( "Version" ) ;
152
+ // Version is optional
153
+ if ( ! string . IsNullOrEmpty ( version ) )
154
+ {
155
+ sdkName += $ "/{ version } ";
156
+ }
157
+ break ;
158
+ }
159
+ }
160
+ if ( string . IsNullOrEmpty ( sdkName ) )
161
+ {
162
+ sdkName = DefaultSdkName ;
163
+ }
164
+
165
+ XmlDocument itemGroupsettings = null ;
166
+ XmlDocument propertyGroupSettings = null ;
104
167
105
- var customProperties = new StringBuilder ( ) ;
106
- var sdkName = DefaultSdkName ;
168
+ GetSettingsThatNeedToBeCopied ( projectElement , ref itemGroupsettings , ref propertyGroupSettings , projectFile ) ;
107
169
108
- string line ;
109
- while ( ( line = streamReader . ReadLine ( ) ) != null )
170
+ List < string > customSettings = new List < string > ( 2 ) ;
171
+ if ( itemGroupsettings != null )
110
172
{
111
- var trimmedLine = line . Trim ( ) ;
173
+ customSettings . Add ( GetIndentedXmlString ( itemGroupsettings ) ) ;
174
+ }
175
+ if ( propertyGroupSettings != null )
176
+ {
177
+ customSettings . Add ( GetIndentedXmlString ( propertyGroupSettings ) ) ;
178
+ }
112
179
113
- foreach ( string setting in SettingsWeWantToCopy )
114
- if ( trimmedLine . Contains ( setting ) )
115
- customProperties . AppendLine ( trimmedLine ) ;
180
+ return ( string . Join ( Environment . NewLine + Environment . NewLine , customSettings ) , sdkName ) ;
181
+ }
116
182
117
- if ( trimmedLine . StartsWith ( "<Import Project" ) )
183
+ private static void GetSettingsThatNeedToBeCopied ( XmlElement projectElement , ref XmlDocument itemGroupsettings , ref XmlDocument propertyGroupSettings , FileInfo projectFile )
184
+ {
185
+ CopyProperties ( projectElement , ref itemGroupsettings , "ItemGroup" ) ;
186
+ CopyProperties ( projectElement , ref propertyGroupSettings , "PropertyGroup" ) ;
187
+
188
+ foreach ( XmlElement importElement in projectElement . GetElementsByTagName ( "Import" ) )
189
+ {
190
+ string propsFilePath = importElement . GetAttribute ( "Project" ) ;
191
+ var directoryName = projectFile . DirectoryName ?? throw new DirectoryNotFoundException ( projectFile . DirectoryName ) ;
192
+ string absolutePath = File . Exists ( propsFilePath )
193
+ ? propsFilePath // absolute path or relative to current dir
194
+ : Path . Combine ( directoryName , propsFilePath ) ; // relative to csproj
195
+ if ( File . Exists ( absolutePath ) )
118
196
{
119
- string propsFilePath = trimmedLine . Split ( '"' ) [ 1 ] ; // its sth like <Import Project="..\..\build\common.props" />
120
- var directoryName = projectFile . DirectoryName ?? throw new DirectoryNotFoundException ( projectFile . DirectoryName ) ;
121
- string absolutePath = File . Exists ( propsFilePath )
122
- ? propsFilePath // absolute path or relative to current dir
123
- : Path . Combine ( directoryName , propsFilePath ) ; // relative to csproj
124
-
125
- if ( File . Exists ( absolutePath ) )
126
- using ( var importedFile = new StreamReader ( File . OpenRead ( absolutePath ) ) )
127
- customProperties . Append ( GetSettingsThatNeedsToBeCopied ( importedFile , new FileInfo ( absolutePath ) ) . customProperties ) ;
197
+ var importXmlDoc = new XmlDocument ( ) ;
198
+ importXmlDoc . Load ( absolutePath ) ;
199
+ GetSettingsThatNeedToBeCopied ( importXmlDoc . DocumentElement , ref itemGroupsettings , ref propertyGroupSettings , projectFile ) ;
128
200
}
201
+ }
202
+ }
129
203
130
- // custom SDKs are not added for non-netcoreapp apps (like net471), so when the TFM != netcoreapp we dont parse "<Import Sdk="
131
- // we don't allow for that mostly to prevent from edge cases like the following
132
- // <Import Sdk="Microsoft.NET.Sdk.WindowsDesktop" Project="Sdk.props" Condition="'$(TargetFramework)'=='netcoreapp3.0'"/>
133
- if ( trimmedLine . StartsWith ( "<Project Sdk=\" " )
134
- || ( TargetFrameworkMoniker . StartsWith ( "netcoreapp" , StringComparison . InvariantCultureIgnoreCase ) && trimmedLine . StartsWith ( "<Import Sdk=\" " ) ) )
135
- sdkName = trimmedLine . Split ( '"' ) [ 1 ] ; // its sth like Sdk="name"
204
+ private static void CopyProperties ( XmlElement projectElement , ref XmlDocument copyToDocument , string groupName )
205
+ {
206
+ XmlElement itemGroupElement = copyToDocument ? . DocumentElement ;
207
+ foreach ( XmlElement groupElement in projectElement . GetElementsByTagName ( groupName ) )
208
+ {
209
+ foreach ( var node in groupElement . ChildNodes )
210
+ {
211
+ if ( node is XmlElement setting && SettingsWeWantToCopy . Contains ( setting . Name ) )
212
+ {
213
+ if ( copyToDocument is null )
214
+ {
215
+ copyToDocument = new XmlDocument ( ) ;
216
+ itemGroupElement = copyToDocument . CreateElement ( groupName ) ;
217
+ copyToDocument . AppendChild ( itemGroupElement ) ;
218
+ }
219
+ XmlNode copiedNode = copyToDocument . ImportNode ( setting , true ) ;
220
+ itemGroupElement . AppendChild ( copiedNode ) ;
221
+ }
222
+ }
136
223
}
224
+ }
137
225
138
- return ( customProperties . ToString ( ) , sdkName ) ;
226
+ private static string GetIndentedXmlString ( XmlDocument doc )
227
+ {
228
+ StringBuilder sb = new StringBuilder ( ) ;
229
+ XmlWriterSettings settings = new XmlWriterSettings
230
+ {
231
+ OmitXmlDeclaration = true ,
232
+ Indent = true ,
233
+ IndentChars = " "
234
+ } ;
235
+ using ( XmlWriter writer = XmlWriter . Create ( sb , settings ) )
236
+ {
237
+ doc . Save ( writer ) ;
238
+ }
239
+ return sb . ToString ( ) ;
139
240
}
140
241
141
242
/// <summary>
0 commit comments