Skip to content

Commit a519b6c

Browse files
authored
Copy PackageReferences to generated csproj (#2347)
* Copy PackageReferences to generated csproj. Explicitly specify program entry point. * Fix closing tag check. * Parse `CsProj` with `XmlDocument` instead of `TextReader`. Added `PackageReference` to `SettingsWeWantToCopy`.
1 parent 0286c45 commit a519b6c

File tree

7 files changed

+292
-113
lines changed

7 files changed

+292
-113
lines changed

src/BenchmarkDotNet/Templates/CsProj.txt

+6-3
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
<Deterministic>true</Deterministic>
2222
<!-- needed for custom build configurations (only "Release" builds are optimized by default) -->
2323
<Optimize Condition=" '$(Configuration)' != 'Debug' ">true</Optimize>
24-
<!-- Begin copied settings from benchmarks project -->
25-
$COPIEDSETTINGS$
26-
<!-- End copied settings -->
2724
<!-- we set LangVersion after any copied settings which might contain LangVersion copied from the benchmarks project -->
2825
<LangVersion Condition="'$(LangVersion)' == '' Or ($([System.Char]::IsDigit('$(LangVersion)', 0)) And '$(LangVersion)' &lt; '7.3')">latest</LangVersion>
2926
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
3027
<!-- fix for NETSDK1150: https://docs.microsoft.com/en-us/dotnet/core/compatibility/sdk/5.0/referencing-executable-generates-error -->
3128
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
29+
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>
3230
</PropertyGroup>
3331

3432
<ItemGroup>
@@ -38,6 +36,11 @@
3836
<ItemGroup>
3937
<ProjectReference Include="$CSPROJPATH$" />
4038
</ItemGroup>
39+
40+
<!-- Begin copied settings from benchmarks project -->
41+
$COPIEDSETTINGS$
42+
<!-- End copied settings -->
43+
4144
$RUNTIMESETTINGS$
4245

4346
</Project>

src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<AssemblyName>$PROGRAMNAME$</AssemblyName>
1111
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
1212
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
13+
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>
1314
</PropertyGroup>
1415

1516
<ItemGroup>
@@ -24,6 +25,10 @@
2425
<ProjectReference Include="$CSPROJPATH$" />
2526
</ItemGroup>
2627

28+
<!-- Begin copied settings from benchmarks project -->
29+
$COPIEDSETTINGS$
30+
<!-- End copied settings -->
31+
2732
<!-- Redirect 'dotnet publish' to in-tree runtime pack -->
2833
<Target Name="TrickRuntimePackLocation" AfterTargets="ProcessFrameworkReferences">
2934
<ItemGroup>

src/BenchmarkDotNet/Templates/WasmCsProj.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<WasmGenerateRunV8Script>true</WasmGenerateRunV8Script>
2626
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
2727
<EnableDefaultWasmAssembliesToBundle>false</EnableDefaultWasmAssembliesToBundle>
28-
$COPIEDSETTINGS$
28+
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>
2929
</PropertyGroup>
3030

3131
<ItemGroup>
@@ -38,6 +38,10 @@
3838
<ProjectReference Include="$(OriginalCSProjPath)" />
3939
</ItemGroup>
4040

41+
<!-- Begin copied settings from benchmarks project -->
42+
$COPIEDSETTINGS$
43+
<!-- End copied settings -->
44+
4145
<PropertyGroup>
4246
<WasmBuildAppAfterThisTarget>PrepareForWasmBuild</WasmBuildAppAfterThisTarget>
4347
</PropertyGroup>

src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs

+148-47
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using System.Reflection;
88
using System.Text;
9+
using System.Xml;
910
using BenchmarkDotNet.Characteristics;
1011
using BenchmarkDotNet.Extensions;
1112
using 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>

src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs

+19-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO;
22
using System.Text;
3+
using System.Xml;
34
using BenchmarkDotNet.Extensions;
45
using BenchmarkDotNet.Helpers;
56
using BenchmarkDotNet.Loggers;
@@ -30,27 +31,26 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
3031

3132
string useLLVM = AotCompilerMode == MonoAotCompilerMode.llvm ? "true" : "false";
3233

33-
using (var file = new StreamReader(File.OpenRead(projectFile.FullName)))
34-
{
35-
var (customProperties, sdkName) = GetSettingsThatNeedsToBeCopied(file, projectFile);
34+
var xmlDoc = new XmlDocument();
35+
xmlDoc.Load(projectFile.FullName);
36+
var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile);
3637

37-
string content = new StringBuilder(ResourceHelper.LoadTemplate("MonoAOTLLVMCsProj.txt"))
38-
.Replace("$PLATFORM$", buildPartition.Platform.ToConfig())
39-
.Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath))
40-
.Replace("$CSPROJPATH$", projectFile.FullName)
41-
.Replace("$TFM$", TargetFrameworkMoniker)
42-
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName)
43-
.Replace("$COPIEDSETTINGS$", customProperties)
44-
.Replace("$CONFIGURATIONNAME$", buildPartition.BuildConfiguration)
45-
.Replace("$SDKNAME$", sdkName)
46-
.Replace("$RUNTIMEPACK$", CustomRuntimePack ?? "")
47-
.Replace("$COMPILERBINARYPATH$", AotCompilerPath)
48-
.Replace("$RUNTIMEIDENTIFIER$", CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier())
49-
.Replace("$USELLVM$", useLLVM)
50-
.ToString();
38+
string content = new StringBuilder(ResourceHelper.LoadTemplate("MonoAOTLLVMCsProj.txt"))
39+
.Replace("$PLATFORM$", buildPartition.Platform.ToConfig())
40+
.Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath))
41+
.Replace("$CSPROJPATH$", projectFile.FullName)
42+
.Replace("$TFM$", TargetFrameworkMoniker)
43+
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName)
44+
.Replace("$COPIEDSETTINGS$", customProperties)
45+
.Replace("$CONFIGURATIONNAME$", buildPartition.BuildConfiguration)
46+
.Replace("$SDKNAME$", sdkName)
47+
.Replace("$RUNTIMEPACK$", CustomRuntimePack ?? "")
48+
.Replace("$COMPILERBINARYPATH$", AotCompilerPath)
49+
.Replace("$RUNTIMEIDENTIFIER$", CustomDotNetCliToolchainBuilder.GetPortableRuntimeIdentifier())
50+
.Replace("$USELLVM$", useLLVM)
51+
.ToString();
5152

52-
File.WriteAllText(artifactsPaths.ProjectFilePath, content);
53-
}
53+
File.WriteAllText(artifactsPaths.ProjectFilePath, content);
5454
}
5555

5656
protected override string GetExecutablePath(string binariesDirectoryPath, string programName)

0 commit comments

Comments
 (0)