Private/Write-BinWipsExe.ps1

function Write-BinWipsExe
{
   <#
    .SYNOPSIS
       Creates a new PowerShell binary.
    .DESCRIPTION
       Generates a .EXE from a script.
    .EXAMPLE
       New-PSBinary -ScriptBlock {Get-Process}
        
       Creates a file in the current directory named PSBinary.exe which runs get-process
    .EXAMPLE
       New-PsBinary MyScript.ps1
 
       Creates an exe in the current directory named MyScript.exe
    #>

   [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
   [Alias()]
   [OutputType([int])]
   Param
   (
      # The powershell command to convert into a program
      # cannot be combined with `InFile`
      [Parameter(Mandatory = $true,
         ValueFromPipelineByPropertyName = $true,
         Position = 0)]
      $Script,


      # Namespace for the generated program.
      # This parameter is trumped by -Tokens, so placing a value here will be overriden by
      # whatever is in -Tokens
      # So if you did -Namespace A -Tokens @{Namespace='B'} Namespace would be set to B not A
      # Must be a valid C# namespace
      # Defaults to PSBinary
      [string]
      $Namespace = "PSBinary",

      # Class name for the generated program
      # This parameter is trumped by -Tokens, so placing a value here will be overriden
      # by whatever is in -Tokens
      # So if you did -ClassName A -Tokens @{ClassName='B'} ClassName would be set to B not A
      # must be a valid c# class name and cannot be equal to -Namespace
      # Defaults to Program
      $ClassName = "Program",

      # Name of the .exe to generate. Defaults to the -InFile (replaced with .exe) or
      # PSBinary.exe if a script block is inlined
      [string]
      $OutFile,

      <# Hashtable of assembly attributes to apply to the assembly level.
             - list of defaults here: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/global
             - custom attributes can also be aplied.
             - Invalid attributes will throw a c# compiler exception
             - Attributes are applied in addition to the defaults unless -NoDefaultAttributes
        #>

      [string[]]
      $AssemblyAttributes,

      <# Hashtable of assembly attributes to apply to the class.
             - Any valid c# class attribute can be applied
             - Invalid attributes will throw a c# compiler exception
             - Attributes are applied in addition to the defaults unless -NoDefaultAttributes
        #>

      [hashtable]
      $ClassAttributes,
        

      <#
            Override the default class template.
            - BinWips Tokens (a character sequence such as beginning with {# and ending with #}) can be included
              and will be replaced with the matching token either from defaults or the -Tokens paramter.
            - If a BinWips exists with out a matching value in -Tokens an exception is thrown.
            - Example: In the default template there is namespace '{#PSNameSpace#} {...}' when compiled
              {#PSNamespace#} is replaced with PSBinary to produce namesapce PSBinary {...}
        #>

      [string]
      $ClassTemplate,

      
      <#
            Override the default attributes template.
            BinWips Tokens not supported.
        #>

      [string]
      $AttributesTemplate,

      <#
            Hashtable of tokens to replace in the class template.
            Exclude the '{#' and '#}' in the keys.
             
            Example:
            -Tokens @{Namespace='CustomNamespace';ClassName='MyCoolClass'}
 
            Reserved Tokens
            ---------------
            {#Script#} The script content to compile
 
        #>

      [hashtable]
      $Tokens,

      # Directory to place output in, defaults to current directory
      # Dir will be created if it doesn't already exist.
      [string]
      $OutDir,

      # Change the directory where work will be done defaults to 'obj' folder in current directory
      # Use -Clean to clean this directory before building
      # Dir will be created if it doesn't already exist.
      [string]
      $ScratchDir,

      # Clean the scratch directory after building
      [switch]
      $Cleanup,

      # Overrite -OutFile if it already exists
      [switch]
      $Force, 


      [string]
      $CompilerPath,

      [string[]]
      $CompilerArgs
   )

   process
   {

      $hasClassAttributes = $PSBoundParameters.ContainsKey('ClassAttributes')
      $hasAssemblyAttributes = $PSBoundParameters.ContainsKey('AssemblyAttributes')
      $runtimeSetupScript = Get-Content -Raw "$PSScriptRoot\..\files\Setup-Runtime.ps1"
   
      $runtimeSetupScript = $runtimeSetupScript | Set-BinWipsToken -Key AssemblyPath -Value ($OutFile.TrimStart('.')) -Required 
   
      if ($Tokens)
      {
         $Tokens.GetEnumerator() | ForEach-Object {
            $runtimeSetupScript = $runtimeSetupScript | Set-BinWipsToken -Key $_.Key -Value $_.Value 
         } 
      }
   
      Write-Verbose $runtimeSetupScript
      if($PSCmdlet.ShouldProcess('Create Runtime Setup Script')) {
         $runtimeSetupScript | Out-File "$ScratchDir\Setup-Runtime.ps1" -Encoding unicode -Force:$Force
      }
      $encodedRuntimeSetup = [Convert]::ToBase64String(([System.Text.Encoding]::Unicode.GetBytes($runtimeSetupScript)))
    
   
      # 3. Base64 encode script for easy handling (no dealing with quotes)
      # https://stackoverflow.com/questions/15414678/how-to-decode-a-base64-string
      $encodedScript = [Convert]::ToBase64String(([System.Text.Encoding]::Unicode.GetBytes($psScript)))
      
      # 4. Insert script and replace tokens in class template
      $funtionName = [System.IO.Path]::GetFileNameWithoutExtension($OutFile)
      $csProgram = $ClassTemplate | Set-BinWipsToken -Key Script -Value $encodedScript `
      | Set-BinWipsToken -Key RuntimeSetup -Value $encodedRuntimeSetup -Required `
      | Set-BinWipsToken -Key ClassName -Value $ClassName -Required `
      | Set-BinWipsToken -Key Namespace -Value $Namespace -Required `
      | Set-BinWipsToken -Key BinWipsVersion -Value "0.1"
      | Set-BinWipsToken -Key FunctionName -Value $funtionName
   
   
      
      if ($hasAssemblyAttributes)
      {
         # TODO: preformat assembly attributes
         Write-Verbose "Applying Assembly Attribuytes"
         $att = ""
         $AssemblyAttributes | ForEach-Object {
            $att += "$_`r`n"
         }
         if ($att -eq $null)
         {
            Write-Error "Failed to build assembly attributes"
         }
         $csProgram = $csProgram | Set-BinWipsToken -Key AssemblyAttributes -Value $att
      }
      else
      {
         $csProgram = $csProgram | Remove-BinWipsToken -Key AssemblyAttributes
      }
   
      if ($hasClassAttributes)
      {
         Write-Verbose "Applying class attributes"
         $att = ""
         $ClassAttributes | ForEach-Object {
            $att += "$_`r`n"
         }
         if ($att -eq $null)
         {
            Write-Error "Failed to build class attributes"
         }
         $csProgram = $csProgram | Set-BinWipsToken -Key ClassAttributes -Value $att
      }
      else
      {
         $csProgram = $csProgram | Remove-BinWipsToken -Key ClassAttributes
      }
      if ($Tokens)
      {
         $Tokens.GetEnumerator() | ForEach-Object {
            $csProgram = $csProgram | Set-BinWipsToken -Key $_.Key -Value $_.Value 
         } 
      }
   
   
      # 5. Output class + additional files to .cs files in scratch dir
      Write-Verbose "Writing to $ScratchDir"
      if($PSCmdlet.ShouldProcess('Create C# Source File')){
         $csProgram | Out-File "$ScratchDir/PSBinary.cs" -Encoding unicode -Force:$Force
      }
      if($PSCmdlet.ShouldProcess('Create BinWiPS Attribute File')){
         $attributesTemplate | Out-File "$ScratchDir/BinWipsAttr.cs" -Encoding unicode -Force:$Force
      }
     
   
   
      
      $buildCmd = "$CompilerPath $CompilerArgs"
      Write-Verbose $buildCmd
      if ($PSCmdlet.ShouldProcess('Create Binary'))
      {
         #$results = Invoke-Expression $buildCmd

         # Use [System.Diagnostics.Process]::Start() and redirect input to avoid Invoke-Expression
         $psi = [System.Diagnostics.ProcessStartInfo]::new($CompilerPath)
         
         $CompilerArgs | ForEach-Object {
            $psi.ArgumentList.Add($_)
         }
  


         $psi.RedirectStandardOutput = $true
         $psi.RedirectStandardError = $true
         $process = [System.Diagnostics.Process]::Start($psi)
         $process.WaitForExit()
         $results = @($process.StandardOutput.ReadToEnd(), $process.StandardError.ReadToEnd())
         
         if ($results -like '*Error*')
         {
           throw $results
         }
         elseif ($null -ne $results)
         {
            Write-Output $results
         }
      }
      else
      {
         Write-Verbose "Not building because ShouldProcess is false"
         return
      }
     
   
      # 7.
      if ($Cleanup)
      {
         Remove-Item $ScratchDir -Recurse
      }
      
   }
 
}