Skip to content

Commit dd3a6a1

Browse files
authored
Use source generator for PSVersionInfo to improve startup time (PowerShell#15603)
1 parent dd40b59 commit dd3a6a1

File tree

5 files changed

+190
-42
lines changed

5 files changed

+190
-42
lines changed

PowerShell.Common.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104

105105
</PropertyGroup>
106106

107+
<!-- Remove existing generated files by source generators because new source code will soon be generated when the build starts -->
108+
<RemoveDir Directories="gen\SourceGenerated" Condition="Exists('gen\SourceGenerated')" />
109+
107110
<!-- Output For Debugging
108111
<WriteLinesToFile File="targetfile1.txt"
109112
Lines="ReleaseTag=$(ReleaseTag);
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Globalization;
6+
using Microsoft.CodeAnalysis;
7+
8+
namespace SMA
9+
{
10+
/// <summary>
11+
/// Source Code Generator to create partial PSVersionInfo class.
12+
/// </summary>
13+
[Generator]
14+
public class PSVersionInfoGenerator : ISourceGenerator
15+
{
16+
/// <summary>
17+
/// Generate output PSVersionInfo.g.cs file.
18+
/// This allows to directly get ProductVersion and others without reflection.
19+
/// </summary>
20+
/// <param name="context">Generator execution context.</param>
21+
public void Execute(GeneratorExecutionContext context)
22+
{
23+
var result = CreatePSVersionInfoPartialClass(context);
24+
25+
// We must use specific file name suffix (*.g.cs,*.g, *.i.cs, *.generated.cs, *.designer.cs)
26+
// so that Roslyn analyzers skip the file.
27+
context.AddSource("PSVersionInfo.g.cs", result);
28+
}
29+
30+
/// <summary>
31+
/// Not used.
32+
/// </summary>
33+
/// <param name="context">Generator initialization context.</param>
34+
public void Initialize(GeneratorInitializationContext context)
35+
{
36+
// No initialization required for this one.
37+
}
38+
39+
/// <summary>
40+
/// Generate source code for the partial PSVersionInfo class.
41+
/// </summary>
42+
/// <param name="context">Generator execution context.</param>
43+
/// <returns>A string with partial PSVersionInfo class.</returns>
44+
private static string CreatePSVersionInfoPartialClass(GeneratorExecutionContext context)
45+
{
46+
// We must put "<auto-generated" on first line so that Roslyng analyzers skip the file.
47+
const string SourceTemplate = @"// <auto-generated>
48+
// This file is auto-generated by PSVersionInfoGenerator.
49+
// </auto-generated>
50+
51+
namespace System.Management.Automation
52+
{{
53+
public static partial class PSVersionInfo
54+
{{
55+
// Defined in 'PowerShell.Common.props' as 'ProductVersion'
56+
// Example:
57+
// - when built from a commit: ProductVersion = '7.3.0-preview.8 Commits: 29 SHA: 52c6b...'
58+
// - when built from a preview release tag: ProductVersion = '7.3.0-preview.8 SHA: f1ec9...'
59+
// - when built from a stable release tag: ProductVersion = '7.3.0 SHA: f1ec9...'
60+
internal const string ProductVersion = ""{0}"";
61+
62+
// The git commit id that the build is based off.
63+
// Defined in 'PowerShell.Common.props' as 'PowerShellVersion' or 'ReleaseTag',
64+
// depending on whether the '-ReleaseTag' is specified when building.
65+
// Example:
66+
// - when built from a commit: GitCommitId = '7.3.0-preview.8-29-g52c6b...'
67+
// - when built from a preview release tag: GitCommitId = '7.3.0-preview.8'
68+
// - when built from a stable release tag: GitCommitId = '7.3.0'
69+
internal const string GitCommitId = ""{1}"";
70+
71+
// The PowerShell version components.
72+
// The version string is defined in 'PowerShell.Common.props' as 'PSCoreBuildVersion',
73+
// but we break it into components to save the overhead of parsing at runtime.
74+
// Example:
75+
// - '7.3.0-preview.8' for preview release or private build
76+
// - '7.3.0' for stable release
77+
private const int Version_Major = {2};
78+
private const int Version_Minor = {3};
79+
private const int Version_Patch = {4};
80+
private const string Version_Label = ""{5}"";
81+
}}
82+
}}";
83+
84+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.ProductVersion", out var productVersion);
85+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.PSCoreBuildVersion", out var mainVersion);
86+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.PowerShellVersion", out var gitDescribe);
87+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.ReleaseTag", out var releaseTag);
88+
89+
string gitCommitId = string.IsNullOrEmpty(releaseTag) ? gitDescribe : releaseTag;
90+
if (gitCommitId.StartsWith("v"))
91+
{
92+
gitCommitId = gitCommitId.Substring(1);
93+
}
94+
95+
var result = ParsePSVersion(mainVersion);
96+
97+
return string.Format(
98+
CultureInfo.InvariantCulture,
99+
SourceTemplate,
100+
productVersion,
101+
gitCommitId,
102+
result.major,
103+
result.minor,
104+
result.patch,
105+
result.preReleaseLabel);
106+
}
107+
108+
private static (int major, int minor, int patch, string preReleaseLabel) ParsePSVersion(string mainVersion)
109+
{
110+
// We only handle the pre-defined PSVersion format here, e.g. 7.x.x or 7.x.x-preview.x
111+
int dashIndex = mainVersion.IndexOf('-');
112+
bool hasLabel = dashIndex != -1;
113+
string preReleaseLabel = hasLabel ? mainVersion.Substring(dashIndex + 1) : string.Empty;
114+
115+
if (hasLabel)
116+
{
117+
mainVersion = mainVersion.Substring(0, dashIndex);
118+
}
119+
120+
int majorEnd = mainVersion.IndexOf('.');
121+
int minorEnd = mainVersion.LastIndexOf('.');
122+
123+
int major = int.Parse(mainVersion.Substring(0, majorEnd), NumberStyles.Integer, CultureInfo.InvariantCulture);
124+
int minor = int.Parse(mainVersion.Substring(majorEnd + 1, minorEnd - majorEnd - 1), NumberStyles.Integer, CultureInfo.InvariantCulture);
125+
int patch = int.Parse(mainVersion.Substring(minorEnd + 1), NumberStyles.Integer, CultureInfo.InvariantCulture);
126+
127+
return (major, minor, patch, preReleaseLabel);
128+
}
129+
}
130+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
2+
<PropertyGroup>
3+
<Description>Generate code for SMA using source generator</Description>
4+
<AssemblyName>SMA.Generator</AssemblyName>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<!-- source generator project needs to target 'netstandard2.0' -->
9+
<TargetFramework>netstandard2.0</TargetFramework>
10+
<LangVersion>10.0</LangVersion>
11+
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
16+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
17+
</ItemGroup>
18+
</Project>

src/System.Management.Automation/System.Management.Automation.csproj

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
1-
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
22
<Import Project="..\..\PowerShell.Common.props" />
33
<PropertyGroup>
44
<Description>PowerShell's System.Management.Automation project</Description>
55
<NoWarn>$(NoWarn);CS1570;CS1734;CA1416</NoWarn>
66
<AssemblyName>System.Management.Automation</AssemblyName>
77
</PropertyGroup>
88

9+
<PropertyGroup>
10+
<!-- we persist source generator files under 'gen' folder so that they are visible to IDEs -->
11+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
12+
<CompilerGeneratedFilesOutputPath>gen\SourceGenerated</CompilerGeneratedFilesOutputPath>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<!-- these properties are declared in 'PowerShell.Common.props' and used by source generator -->
17+
<CompilerVisibleProperty Include="ProductVersion" />
18+
<CompilerVisibleProperty Include="PSCoreBuildVersion" />
19+
<CompilerVisibleProperty Include="PowerShellVersion" />
20+
<CompilerVisibleProperty Include="ReleaseTag" />
21+
22+
<ProjectReference Include="SourceGenerators\PSVersionInfoGenerator\PSVersionInfoGenerator.csproj"
23+
OutputItemType="Analyzer"
24+
ReferenceOutputAssembly="false" />
25+
</ItemGroup>
26+
927
<ItemGroup Condition=" '$(IsWindows)' == 'true' ">
1028
<ProjectReference Include="..\Microsoft.PowerShell.CoreCLR.Eventing\Microsoft.PowerShell.CoreCLR.Eventing.csproj" />
1129
</ItemGroup>
@@ -38,6 +56,9 @@
3856
</PropertyGroup>
3957

4058
<ItemGroup>
59+
<!-- exclude code of source generators from compilation and IDEs (e.g. vscode and visual studio) -->
60+
<Compile Remove="SourceGenerators\**\*.cs" />
61+
4162
<Compile Remove="cimSupport\cmdletization\xml\cmdlets-over-objects.objectModel.autogen.cs" />
4263
<Compile Remove="cimSupport\cmdletization\xml\cmdlets-over-objects.xmlSerializer.autogen.cs" />
4364
<Compile Remove="engine\TransactedString.cs" />

src/System.Management.Automation/engine/PSVersionInfo.cs

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections;
55
using System.Globalization;
6-
using System.Reflection;
76
using System.Text;
87
using System.Text.RegularExpressions;
98

@@ -27,7 +26,7 @@ namespace System.Management.Automation
2726
/// The above statement retrieves the PowerShell edition.
2827
/// </para>
2928
/// </summary>
30-
public static class PSVersionInfo
29+
public static partial class PSVersionInfo
3130
{
3231
internal const string PSVersionTableName = "PSVersionTable";
3332
internal const string PSRemotingProtocolVersionName = "PSRemotingProtocolVersion";
@@ -42,6 +41,18 @@ public static class PSVersionInfo
4241

4342
private static readonly PSVersionHashTable s_psVersionTable;
4443

44+
/*
45+
The following constants are generated by the source generator 'PSVersionInfoGenerator':
46+
47+
internal const string ProductVersion;
48+
internal const string GitCommitId;
49+
50+
private const int Version_Major
51+
private const int Version_Minor;
52+
private const int Version_Patch;
53+
private const string Version_Label;
54+
*/
55+
4556
/// <summary>
4657
/// A constant to track current PowerShell Version.
4758
/// </summary>
@@ -78,39 +89,14 @@ static PSVersionInfo()
7889
{
7990
s_psVersionTable = new PSVersionHashTable(StringComparer.OrdinalIgnoreCase);
8091

81-
Assembly currentAssembly = typeof(PSVersionInfo).Assembly;
82-
ProductVersion = currentAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
83-
84-
// Get 'GitCommitId' and 'PSVersion' from the 'productVersion' assembly attribute.
85-
//
86-
// The strings can be one of the following format examples:
87-
// when powershell is built from a commit:
88-
// productVersion = '6.0.0-beta.7 Commits: 29 SHA: 52c6b...' convert to GitCommitId = 'v6.0.0-beta.7-29-g52c6b...'
89-
// PSVersion = '6.0.0-beta.7'
90-
// when powershell is built from a release tag:
91-
// productVersion = '6.0.0-beta.7 SHA: f1ec9...' convert to GitCommitId = 'v6.0.0-beta.7'
92-
// PSVersion = '6.0.0-beta.7'
93-
// when powershell is built from a release tag for RTM:
94-
// productVersion = '6.0.0 SHA: f1ec9...' convert to GitCommitId = 'v6.0.0'
95-
// PSVersion = '6.0.0'
96-
string rawGitCommitId;
97-
string mainVersion = ProductVersion.Substring(0, ProductVersion.IndexOf(' '));
98-
99-
if (ProductVersion.Contains(" Commits: "))
100-
{
101-
rawGitCommitId = ProductVersion.Replace(" Commits: ", "-").Replace(" SHA: ", "-g");
102-
}
103-
else
104-
{
105-
rawGitCommitId = mainVersion;
106-
}
107-
108-
s_psSemVersion = new SemanticVersion(mainVersion);
92+
s_psSemVersion = Version_Label == string.Empty
93+
? new SemanticVersion(Version_Major, Version_Minor, Version_Patch)
94+
: new SemanticVersion(Version_Major, Version_Minor, Version_Patch, Version_Label, buildLabel: null);
10995
s_psVersion = (Version)s_psSemVersion;
11096

11197
s_psVersionTable[PSVersionName] = s_psSemVersion;
11298
s_psVersionTable[PSEditionName] = PSEditionValue;
113-
s_psVersionTable[PSGitCommitIdName] = rawGitCommitId;
99+
s_psVersionTable[PSGitCommitIdName] = GitCommitId;
114100
s_psVersionTable[PSCompatibleVersionsName] = new Version[] { s_psV1Version, s_psV2Version, s_psV3Version, s_psV4Version, s_psV5Version, s_psV51Version, s_psV6Version, s_psV61Version, s_psV62Version, s_psV7Version, s_psV71Version, s_psV72Version, s_psVersion };
115101
s_psVersionTable[SerializationVersionName] = new Version(InternalSerializer.DefaultVersion);
116102
s_psVersionTable[PSRemotingProtocolVersionName] = RemotingConstants.ProtocolVersion;
@@ -183,16 +169,6 @@ public static Version PSVersion
183169
}
184170
}
185171

186-
internal static string ProductVersion { get; }
187-
188-
internal static string GitCommitId
189-
{
190-
get
191-
{
192-
return (string)s_psVersionTable[PSGitCommitIdName];
193-
}
194-
}
195-
196172
/// <summary>
197173
/// Gets the edition of PowerShell.
198174
/// </summary>

0 commit comments

Comments
 (0)