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