10

I am trying to compile source code in by using this command:

Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $source

But features are not working, for example:

Add-Type : c:\Users\...\AppData\Local\Temp\2\d2q5hn5b.0.cs(101) : Unexpected character '$'

For code:

new Problem($"... ({identifier})", node)

I am using

Is there a way to fix this?

Maximilian Burszley
  • 18,243
  • 4
  • 34
  • 63
Zergatul
  • 1,957
  • 1
  • 18
  • 28

3 Answers3

14

Powershell uses CodeDomProvider to compile their assemblies. The version provided with the framework just supports C# 5, so no new features are available by default.

However, if you provide another CodeDomProvider, you can compile any language, also C#6. There is a CodeDomProvider available for Roslyn (the new .NET compiler). You can download it from NuGet and include the assembly using Add-Type. Then create an instance of the compiler and pass that in the -CodeDomProvider attribute.

Community
  • 1
  • 1
Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
6

To expand upon Patrick Hoffmans's solution, I was a bit uncomfortable about using the reflection method in unbob's solution as that could potentially break in the future.

I worked out the following powershell code instead which uses .NET named classes and interfaces:

#requires -Version 5

# download https://www.nuget.org/packages/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/ and extract with 7-zip to a location, enter that location on the next line
$DotNetCodeDomLocation = 'C:\Utils\microsoft.codedom.providers.dotnetcompilerplatform'
Add-Type -Path "$DotNetCodeDomLocation\lib\net475\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll"

# using Invoke-Expression moves this class definition to runtime, so it will work after the add-type and the ps5 class interface implementation will succeed
# This uses the public interface ICompilerSettings instead of the private class CompilerSettings
Invoke-Expression -Command @"
class RoslynCompilerSettings : Microsoft.CodeDom.Providers.DotNetCompilerPlatform.ICompilerSettings
{
    [string] get_CompilerFullPath()
    {
        // This path may need to be updated for newer versions
        return "$DotNetCodeDomLocation\tools\Roslyn-4.1.0\csc.exe"
    }
    [int] get_CompilerServerTimeToLive()
    {
        return 10
    }
}
"@
$DotNetCodeDomProvider = [Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider]::new([RoslynCompilerSettings]::new())

This can then be used as in the following examples:

  • To directly add a type to the powershell instance, with assembly reference example (requires the roslyn compiler and the above code to be bundled with your script):

      Add-Type -CodeDomProvider $DotNetCodeDomProvider -TypeDefinition $your_source_code_block -ReferencedAssemblies @([System.Reflection.Assembly]::GetAssembly([hashtable]).Location)
    
  • To compile the code to a dll for loading in future/other scripts (only requires the resulting dll file to be bundled with your script):

      $DotNetAssemblyParameters = [System.CodeDom.Compiler.CompilerParameters]::new(
          @([System.Reflection.Assembly]::GetAssembly([hashtable]).Location),
          'path_and_name_for_saved.dll',
          $false
      )
      # you can adjust more compilation settings here if you want, see
      # https://learn.microsoft.com/en-us/dotnet/api/system.codedom.compiler.compilerparameters?view=netframework-4.7.2
      $compilationResults = $DotNetCodeDomProvider.CompileAssemblyFromSource(
          $DotNetAssemblyParameters,
          $your_source_code_block
      )
    

The compiled dll can then be used with a simple Add-Type:

Add-Type -Path 'path_and_name_for_saved.Dll'

This allows for you to use the latest .NET compiler in powershell either inline with your main script if you bundle the CodeDomProvider dll and the roslyn compiler, or you can compile the C# code out to a dll so it doesn't have to be recompiled each time the script is run, allowing for easier portability and a faster script run time.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
taelnia
  • 61
  • 1
  • 1
1

When I try Mr. Hoffman's approach, I get the following error:

Add-Type : Could not find a part of the path 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\bin\roslyn\csc.exe'.

user1676558 mentions two solutions:

  1. A NuGet package that contains a fix for this specific problem
  2. A snippet of C# that reflects out the offending private field and fixes it

Being a (little league) PowerShell hacker, I came up with my own PowerShellian solution, based on examination of the source code:

$dncpTypes = Add-Type -Path C:\<path where I put the dll>\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll -PassThru
$dncpTypTab = [ordered]@{}
$dncpTypes | %{$dncpTypTab[$_.Name] = $_}

$compSetCtor    = $dncpTypTab.CompilerSettings.GetConstructor(@([string],[int]))
$compSettings   = $compSetCtor.Invoke(@('C:\Program Files (x86)\MSBuild\14.0\Bin\csc.exe', 10))
$cscpOtherCtor  = $dncpTypTab.CSharpCodeProvider.GetConstructor('NonPublic,Instance', $null, @($dncpTypTab.ICompilerSettings), $null)
$roslynProvider = $cscpOtherCtor.Invoke($compSettings)

Needless to say, there's some discussion on the innertubes about whether or not this is a bug. Looks like the provider was targeted at ASP.NET and does the right thing there. People also disagree on where to pick up csc.exe. I suspect this may continue to be in flux.

[later edit: in VS2017, csc appears to live at ${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\Roslyn\csc.exe.]

[Edit VS2022] The API seems to have change a bit with VS2022, now I had to use the following code to set it up:

$pathToCodeDomProvider = 'C:\<WhereEver>\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll'
$pathToCompiler = 'C:\<WhereEver>\csc.exe'

$dncpTypes = Add-Type -LiteralPath $pathToCodeDomProvider -PassThru
$dncpTypTab = [ordered]@{}
$dncpTypes | %{$dncpTypTab[$_.Name] = $_}

$compSetCtor    = $dncpTypTab.ProviderOptions.GetConstructor(@([string],[int]))
$compSettings   = $compSetCtor.Invoke(@($pathToCompiler, 10))
$cscpOtherCtor  = $dncpTypTab.CSharpCodeProvider.GetConstructor(@([Microsoft.CodeDom.Providers.DotNetCompilerPlatform.IProviderOptions]))
$roslynProvider = $cscpOtherCtor.Invoke($compSettings)
Christoph
  • 3,322
  • 2
  • 19
  • 28
unbob
  • 331
  • 3
  • 7