1 # Copyright (c) 2018 Ansible Project
2 # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
3 
Add-CSharpTypenull4 Function Add-CSharpType {
5     <#
6     .SYNOPSIS
7     Compiles one or more C# scripts similar to Add-Type. This exposes
8     more configuration options that are useable within Ansible and it
9     also allows multiple C# sources to be compiled together.
10 
11     .PARAMETER References
12     [String[]] A collection of C# scripts to compile together.
13 
14     .PARAMETER IgnoreWarnings
15     [Switch] Whether to compile code that contains compiler warnings, by
16     default warnings will cause a compiler error.
17 
18     .PARAMETER PassThru
19     [Switch] Whether to return the loaded Assembly
20 
21     .PARAMETER AnsibleModule
22     [Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values.
23         TempPath is set to the Tmpdir property of the class
24         IncludeDebugInfo is set when the Ansible verbosity is >= 3
25 
26     .PARAMETER TempPath
27     [String] The temporary directory in which the dynamic assembly is
28     compiled to. This file is deleted once compilation is complete.
29     Cannot be used when AnsibleModule is set. This is a no-op when
30     running on PSCore.
31 
32     .PARAMETER IncludeDebugInfo
33     [Switch] Whether to include debug information in the compiled
34     assembly. Cannot be used when AnsibleModule is set. This is a no-op
35     when running on PSCore.
36 
37     .PARAMETER CompileSymbols
38     [String[]] A list of symbols to be defined during compile time. These are
39     added to the existing symbols, 'CORECLR', 'WINDOWS', 'UNIX' that are set
40     conditionalls in this cmdlet.
41 
42     .NOTES
43     The following features were added to control the compiling options from the
44     code itself.
45 
46     * Predefined compiler SYMBOLS
47 
48         * CORECLR - Added when running on PowerShell Core.
49         * WINDOWS - Added when running on Windows.
50         * UNIX - Added when running on non-Windows.
51         * X86 - Added when running on a 32-bit process (Ansible 2.10+)
52         * AMD64 - Added when running on a 64-bit process (Ansible 2.10+)
53 
54     * Ignore compiler warnings inline with the following comment inline
55 
56         //NoWarn -Name <rule code> [-CLR Core|Framework]
57 
58     * Specify custom assembly references inline
59 
60         //AssemblyReference -Name Dll.Location.dll [-CLR Core|Framework]
61 
62         # Added in Ansible 2.10
63         //AssemblyReference -Type System.Type.Name [-CLR Core|Framework]
64 
65     * Create automatic type accelerators to simplify long namespace names (Ansible 2.9+)
66 
67         //TypeAccelerator -Name <AcceleratorName> -TypeName <Name of compiled type>
68     #>
69     param(
70         [Parameter(Mandatory=$true)][AllowEmptyCollection()][String[]]$References,
71         [Switch]$IgnoreWarnings,
72         [Switch]$PassThru,
73         [Parameter(Mandatory=$true, ParameterSetName="Module")][Object]$AnsibleModule,
74         [Parameter(ParameterSetName="Manual")][String]$TempPath = $env:TMP,
75         [Parameter(ParameterSetName="Manual")][Switch]$IncludeDebugInfo,
76         [String[]]$CompileSymbols = @()
77     )
78     if ($null -eq $References -or $References.Length -eq 0) {
79         return
80     }
81 
82     # define special symbols CORECLR, WINDOWS, UNIX if required
83     # the Is* variables are defined on PSCore, if absent we assume an
84     # older version of PowerShell under .NET Framework and Windows
85     $defined_symbols = [System.Collections.ArrayList]$CompileSymbols
86 
87     if ([System.IntPtr]::Size -eq 4) {
88         $defined_symbols.Add('X86') > $null
89     } else {
90         $defined_symbols.Add('AMD64') > $null
91     }
92 
93     $is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue
94     if ($null -ne $is_coreclr) {
95         if ($is_coreclr.Value) {
96             $defined_symbols.Add("CORECLR") > $null
97         }
98     }
99     $is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue
100     if ($null -ne $is_windows) {
101         if ($is_windows.Value) {
102             $defined_symbols.Add("WINDOWS") > $null
103         } else {
104             $defined_symbols.Add("UNIX") > $null
105         }
106     } else {
107         $defined_symbols.Add("WINDOWS") > $null
108     }
109 
110     # Store any TypeAccelerators shortcuts the util wants us to set
111     $type_accelerators = [System.Collections.Generic.List`1[Hashtable]]@()
112 
113     # pattern used to find referenced assemblies in the code
114     $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-(?<Parameter>(Name)|(Type))\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
115     $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
116     $type_pattern = [Regex]"//\s*TypeAccelerator\s+-Name\s+(?<Name>[\w.]*)\s+-TypeName\s+(?<TypeName>[\w.]*)"
117 
118     # PSCore vs PSDesktop use different methods to compile the code,
119     # PSCore uses Roslyn and can compile the code purely in memory
120     # without touching the disk while PSDesktop uses CodeDom and csc.exe
121     # to compile the code. We branch out here and run each
122     # distribution's method to add our C# code.
123     if ($is_coreclr) {
124         # compile the code using Roslyn on PSCore
125 
126         # Include the default assemblies using the logic in Add-Type
127         # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs
128         $assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@(
129             [Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location)
130         )
131         $netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref")
132         $lib_assembly_location = [System.IO.Path]::GetDirectoryName([object].Assembly.Location)
133         foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) {
134             $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null
135         }
136 
137         # loop through the references, parse as a SyntaxTree and get
138         # referenced assemblies
139         $ignore_warnings = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[String], [Microsoft.CodeAnalysis.ReportDiagnostic]]'
140         $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols)
141         $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@()
142         foreach ($reference in $References) {
143             # scan through code and add any assemblies that match
144             # //AssemblyReference -Name ... [-CLR Core]
145             # //NoWarn -Name ... [-CLR Core]
146             # //TypeAccelerator -Name ... -TypeName ...
147             $assembly_matches = $assembly_pattern.Matches($reference)
148             foreach ($match in $assembly_matches) {
149                 $clr = $match.Groups["CLR"].Value
150                 if ($clr -and $clr -ne "Core") {
151                     continue
152                 }
153 
154                 $parameter_type = $match.Groups["Parameter"].Value
155                 $assembly_path = $match.Groups["Name"].Value
156                 if ($parameter_type -eq "Type") {
157                     $assembly_path = ([Type]$assembly_path).Assembly.Location
158                 } else {
159                     if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) {
160                         $assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path
161                     }
162                 }
163                 $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null
164             }
165             $warn_matches = $no_warn_pattern.Matches($reference)
166             foreach ($match in $warn_matches) {
167                 $clr = $match.Groups["CLR"].Value
168                 if ($clr -and $clr -ne "Core") {
169                     continue
170                 }
171                 $ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress)
172             }
173             $syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
174 
175             $type_matches = $type_pattern.Matches($reference)
176             foreach ($match in $type_matches) {
177                 $type_accelerators.Add(@{Name=$match.Groups["Name"].Value; TypeName=$match.Groups["TypeName"].Value})
178             }
179         }
180 
181         # Release seems to contain the correct line numbers compared to
182         # debug,may need to keep a closer eye on this in the future
183         $compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @(
184             [Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary
185         )).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release)
186 
187         # set warnings to error out if IgnoreWarnings is not set
188         if (-not $IgnoreWarnings.IsPresent) {
189             $compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error)
190             $compiler_options = $compiler_options.WithSpecificDiagnosticOptions($ignore_warnings)
191         }
192 
193         # create compilation object
194         $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create(
195             [System.Guid]::NewGuid().ToString(),
196             $syntax_trees,
197             $assemblies,
198             $compiler_options
199         )
200 
201         # Load the compiled code and pdb info, we do this so we can
202         # include line number in a stracktrace
203         $code_ms = New-Object -TypeName System.IO.MemoryStream
204         $pdb_ms = New-Object -TypeName System.IO.MemoryStream
205         try {
206             $emit_result = $compilation.Emit($code_ms, $pdb_ms)
207             if (-not $emit_result.Success) {
208                 $errors = [System.Collections.ArrayList]@()
209 
210                 foreach ($e in $emit_result.Diagnostics) {
211                     # builds the error msg, based on logic in Add-Type
212                     # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239
213                     if ($null -eq $e.Location.SourceTree) {
214                         $errors.Add($e.ToString()) > $null
215                         continue
216                     }
217 
218                     $cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false
219                     $text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines
220                     $line_span = $e.Location.GetLineSpan()
221 
222                     $diagnostic_message = $e.ToString()
223                     $error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString()
224                     $error_position = $line_span.StartLinePosition.Character
225 
226                     $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4)
227                     $sb.AppendLine($diagnostic_message)
228                     $sb.AppendLine($error_line_string)
229 
230                     for ($i = 0; $i -lt $error_line_string.Length; $i++) {
231                         if ([System.Char]::IsWhiteSpace($error_line_string[$i])) {
232                             continue
233                         }
234                         $sb.Append($error_line_string, 0, $i)
235                         $sb.Append(' ', [Math]::Max(0, $error_position - $i))
236                         $sb.Append("^")
237                         break
238                     }
239 
240                     $errors.Add($sb.ToString()) > $null
241                 }
242 
243                 throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")"
244             }
245 
246             $code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
247             $pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
248             $compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms)
249         } finally {
250             $code_ms.Close()
251             $pdb_ms.Close()
252         }
253     } else {
254         # compile the code using CodeDom on PSDesktop
255 
256         # configure compile options based on input
257         if ($PSCmdlet.ParameterSetName -eq "Module") {
258             $temp_path = $AnsibleModule.Tmpdir
259             $include_debug = $AnsibleModule.Verbosity -ge 3
260         } else {
261             $temp_path = $TempPath
262             $include_debug = $IncludeDebugInfo.IsPresent
263         }
264         $compiler_options = [System.Collections.ArrayList]@("/optimize")
265         if ($defined_symbols.Count -gt 0) {
266             $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null
267         }
268 
269         $compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters
270         $compile_parameters.GenerateExecutable = $false
271         $compile_parameters.GenerateInMemory = $true
272         $compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent)
273         $compile_parameters.IncludeDebugInformation = $include_debug
274         $compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false)
275 
276         # Add-Type automatically references System.dll, System.Core.dll,
277         # and System.Management.Automation.dll which we replicate here
278         $assemblies = [System.Collections.Generic.HashSet`1[String]]@(
279             "System.dll",
280             "System.Core.dll",
281             ([System.Reflection.Assembly]::GetAssembly([PSObject])).Location
282         )
283 
284         # create a code snippet for each reference and check if we need
285         # to reference any extra assemblies
286         $ignore_warnings = [System.Collections.ArrayList]@()
287         $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
288         foreach ($reference in $References) {
289             # scan through code and add any assemblies that match
290             # //AssemblyReference -Name ... [-CLR Framework]
291             # //NoWarn -Name ... [-CLR Framework]
292             # //TypeAccelerator -Name ... -TypeName ...
293             $assembly_matches = $assembly_pattern.Matches($reference)
294             foreach ($match in $assembly_matches) {
295                 $clr = $match.Groups["CLR"].Value
296                 if ($clr -and $clr -ne "Framework") {
297                     continue
298                 }
299 
300                 $parameter_type = $match.Groups["Parameter"].Value
301                 $assembly_path = $match.Groups["Name"].Value
302                 if ($parameter_type -eq "Type") {
303                     $assembly_path = ([Type]$assembly_path).Assembly.Location
304                 }
305                 $assemblies.Add($assembly_path) > $null
306             }
307             $warn_matches = $no_warn_pattern.Matches($reference)
308             foreach ($match in $warn_matches) {
309                 $clr = $match.Groups["CLR"].Value
310                 if ($clr -and $clr -ne "Framework") {
311                     continue
312                 }
313                 $warning_id = $match.Groups["Name"].Value
314                 # /nowarn should only contain the numeric part
315                 if ($warning_id.StartsWith("CS")) {
316                     $warning_id = $warning_id.Substring(2)
317                 }
318                 $ignore_warnings.Add($warning_id) > $null
319             }
320             $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
321 
322             $type_matches = $type_pattern.Matches($reference)
323             foreach ($match in $type_matches) {
324                 $type_accelerators.Add(@{Name=$match.Groups["Name"].Value; TypeName=$match.Groups["TypeName"].Value})
325             }
326         }
327         if ($ignore_warnings.Count -gt 0) {
328             $compiler_options.Add("/nowarn:" + ([String]::Join(",", $ignore_warnings.ToArray()))) > $null
329         }
330         $compile_parameters.ReferencedAssemblies.AddRange($assemblies)
331         $compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray())
332 
333         # compile the code together and check for errors
334         $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
335 
336         # This calls csc.exe which can take compiler options from environment variables. Currently these env vars
337         # are known to have problems so they are unset:
338         #   LIB - additional library paths will fail the compilation if they are invalid
339         $originalEnv = @{}
340         try {
341             'LIB' | ForEach-Object -Process {
342                 $value = Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue
343                 if ($value) {
344                     $originalEnv[$_] = $value
345                     Remove-Item -LiteralPath "Env:\$_"
346                 }
347             }
348 
349             $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units)
350         }
351         finally {
352             foreach ($kvp in $originalEnv.GetEnumerator()) {
353                 [System.Environment]::SetEnvironmentVariable($kvp.Key, $kvp.Value, "Process")
354             }
355         }
356 
357         if ($compile.Errors.HasErrors) {
358             $msg = "Failed to compile C# code: "
359             foreach ($e in $compile.Errors) {
360                 $msg += "`r`n" + $e.ToString()
361             }
362             throw [InvalidOperationException]$msg
363         }
364         $compiled_assembly = $compile.CompiledAssembly
365     }
366 
367     $type_accelerator = [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")
368     foreach ($accelerator in $type_accelerators) {
369         $type_name = $accelerator.TypeName
370         $found = $false
371 
372         foreach ($assembly_type in $compiled_assembly.GetTypes()) {
373             if ($assembly_type.Name -eq $type_name) {
374                 $type_accelerator::Add($accelerator.Name, $assembly_type)
375                 $found = $true
376                 break
377             }
378         }
379         if (-not $found) {
380             throw "Failed to find compiled class '$type_name' for custom TypeAccelerator."
381         }
382     }
383 
384     # return the compiled assembly if PassThru is set.
385     if ($PassThru) {
386         return $compiled_assembly
387     }
388 }
389 
390 Export-ModuleMember -Function Add-CSharpType
391 
392