66using System . Linq ;
77using System . Reflection ;
88using System . Text ;
9+ using System . Xml ;
910using BenchmarkDotNet . Characteristics ;
1011using BenchmarkDotNet . Extensions ;
1112using BenchmarkDotNet . Helpers ;
@@ -22,8 +23,20 @@ public class CsProjGenerator : DotNetCliGenerator, IEquatable<CsProjGenerator>
2223 {
2324 private const string DefaultSdkName = "Microsoft.NET.Sdk" ;
2425
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 ( ) ;
2740
2841 public string RuntimeFrameworkVersion { get ; }
2942
@@ -57,24 +70,23 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
5770 var benchmark = buildPartition . RepresentativeBenchmarkCase ;
5871 var projectFile = GetProjectFilePath ( benchmark . Descriptor . Type , logger ) ;
5972
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 ) ;
7890 }
7991
8092 /// <summary>
@@ -97,45 +109,134 @@ protected virtual string GetRuntimeSettings(GcMode gcMode, IResolver resolver)
97109 // the host project or one of the .props file that it imports might contain some custom settings that needs to be copied, sth like
98110 // <NetCoreAppImplicitPackageVersion>2.0.0-beta-001607-00</NetCoreAppImplicitPackageVersion>
99111 // <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 )
101113 {
102114 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 ;
104167
105- var customProperties = new StringBuilder ( ) ;
106- var sdkName = DefaultSdkName ;
168+ GetSettingsThatNeedToBeCopied ( projectElement , ref itemGroupsettings , ref propertyGroupSettings , projectFile ) ;
107169
108- string line ;
109- while ( ( line = streamReader . ReadLine ( ) ) != null )
170+ List < string > customSettings = new List < string > ( 2 ) ;
171+ if ( itemGroupsettings != null )
110172 {
111- var trimmedLine = line . Trim ( ) ;
173+ customSettings . Add ( GetIndentedXmlString ( itemGroupsettings ) ) ;
174+ }
175+ if ( propertyGroupSettings != null )
176+ {
177+ customSettings . Add ( GetIndentedXmlString ( propertyGroupSettings ) ) ;
178+ }
112179
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+ }
116182
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 ) )
118196 {
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 ) ;
128200 }
201+ }
202+ }
129203
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+ }
136223 }
224+ }
137225
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 ( ) ;
139240 }
140241
141242 /// <summary>
0 commit comments