Public/New-BinWips.ps1


function New-BinWips
{
   <#
    .SYNOPSIS
       Creates a new .exe from a script block or script file.
    .DESCRIPTION
       Generates a .EXE from a script. Support for parameters, interactive programs,
       cross-platform compiling, resource embedding. See examples for more information.
       Use Get-Help BinWips -Online to open the read me on github.
    .NOTES
      This module is not associated with Bflat or the developers. The license for
      Bflat (if installed through the module), can be found at:
         module_folder/BinWips/files/bflat/platform/License.txt
    .EXAMPLE
       New-BinWips -ScriptBlock {Write-Host "Hello, World!"}
       # ./PSBinary.exe
       # Hello, World!
        
       Creates a file in the current directory named PSBinary.exe which writes "Hello, World!" to the console
    .EXAMPLE
       New-BinWips MyScript.ps1
       # ./MyScript.exe
       Creates an exe in the current directory named MyScript.exe
   .EXAMPLE
      New-BinWips -ScriptBlock {
         param($foo)
         Write-Output "$foo"
      }
      # ./PSBinary.exe -foo "bar"
       
      Creates a program which accepts a parameter and writes it to the console
   .EXAMPLE
      New-BinWips -ScriptBlock {
         $input = Read-Host "What's your name?"
         Write-Host "Hello, $input!"
      }
      # ./PSBinary.exe
      # What's your name?: BinWips
      # Hello, BinWips!
 
      Create an interactive program (Read-Host works the same).
   .EXAMPLE
      New-BinWips -ScriptBlock {
         $fileContent = Get-PsBinaryResource "MyFile.txt"
         $fileContent += Get-PsBinaryResource "MyOtherFile.txt"
         Write-Output $fileContent
      } -Resources "MyFile.txt", "MyOtherFile.txt" -OutFile "MyProgram.exe"
 
      Embeddes the files MyFile.txt and MyOtherFile.txt into the exe and makes them accessible via Get-PsBinaryResource.
   .EXAMPLE
      New-BinWips -ScriptBlock {
         Write-host "done"
      } -ClassTemplate @"
        // use tokens to replace values in the template, see -Tokens for more info
        namespace {#Namespace#} {
           public class MyClass {
              public static void Main(string[] args) {
                 //.. Custom Host class implementation
                 var x = "{#RuntimeSetip#}"; // ignored but required to be in template
                 var y = "{#Script#}"; // ignored but required to be in template
                 var ext = ".exe";
                 var p = System.Diagnostics.Process.Start("pwsh", "-NoProfile -NoLogo -Command \"Write-host 'Ignore Script'\"");
                 p.WaitForExit();
              }
           }
        }
"@
 
      Override the Class Template used for the C# program that runs the script. This example would ignore the passed in scripts
      and print "Ignore Script" to the console.
   .EXAMPLE
      New-BinWips -ScriptBlock {echo "Only Runs on Win x64"} -Platform Windows -Architecture x64
       
      Creates a program which targets Windows x64. Valid Options are Windows/Linux and x86/x64/arm64.
      By default BinWips will target the current platform and architecture.
   .EXAMPLE
      New-BinWips -ScriptBlock {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory=$true)]
                [string]$foo
            )
            Add-Type -AssemblyName System.Windows.Forms
            $form = New-Object System.Windows.Forms.Form
            $form.Text = "Hello World"
            $form.ShowDialog()
        } -Platform Windows -Architecture x64
       
      Creates a program that shows a window on Windows x64.
   .LINK
        https://github.com/d-carrigg/BinWips
    #>

   [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
   [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'BinWips is not plural')]
   Param
   (
      # The powershell command to convert into a program
      # cannot be combined with `InFile`
      [Parameter(Mandatory = $true,
         ValueFromPipelineByPropertyName = $true,
         Position = 0,
         ParameterSetName = 'Inline')]
      $ScriptBlock,

      # Source Script file(s), order is important
      # Files added in order entered
      # Exe name is defaulted to last file in array
      [string[]]
      [Parameter(Mandatory = $true,
         ValueFromPipelineByPropertyName = $true,
         Position = 0,
         ParameterSetName = 'File')]
      $InFile,

      # 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 `.binwips` folder in current directory
      # Use -Cleanup to clean this directory after build
      # Dir will be created if it doesn't already exist.
      [string]
      $ScratchDir,

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


      # Recursively delete the scratch directory after build
      # Disabled by default to prevent accidental deletion of files
      [switch]
      $Cleanup,

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

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


      <# List 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
        #>

      [string[]]
      $AssemblyAttributes,


      <# List of assembly attributes to apply to the class.
            - Any valid c# class attribute can be applied
            - Invalid attributes will throw a c# compiler exception
        #>

      [string[]]
      $ClassAttributes,
        

      <#
         Override the default C# 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 C# attribute template.
            BinWips Tokens are not supported.
 
            Example Template:
 
               using System;
               namespace BinWips {
                  [AttributeUsage(AttributeTargets.Assembly)]
                  public class BinWipsAttribute : Attribute {
                     public string Version { get; set; }
                     public BinWipsAttribute() { }
                     public BinWipsAttribute(string version) { Version = version; }
                  }
               }
        #>

      [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 = @{},

      <#
        List of .NET assemblies for the host .exe to reference. These references will not be accessible from within the powershell script.
      #>

      [string[]]
      $HostReferences,

      <# List of files to include with the app. I.e., `-Resources "MyFirstResource.txt", "MySecondResource.txt"`
 
           To call files in script (if -NoEmbedresources is *not* included):
           `$myFile = Get-PsBinaryResource FileName.ext`
           where `FileName.ext` is the file you want to use.
           This returns the content of the file, not a path to it.
 
           Only use this if you want to package things like settings files
           or images. Otherwise files are accessed normally by the generated exe.
        #>

      [string[]]
      $Resources,

      # Don't embed any resource specifed by -Resources
      # instead they are copied to out dir if they don't already exist
      [switch]
      $NoEmbedResources,

      # The platform to target, valid options include Linux and Windows
      [string]
      [ValidateSet('Linux', 'Windows')]
      $Platform,

      # The architecture to target, valid options include x86, x64, and arm64
      [string]
      [ValidateSet('x86', 'x64', 'arm64')]
      $Architecture,

      # Additional parameters to pass to the bflat compiler
      [string[]]
      $ExtraArguments,

      <#
        Which edition of PowerShell to target:
         - Core: PowerShell Core (pwsh)
         - Desktop: Windows PowerShell (powershell.exe)
 
        If not specified, defaults to the edition of PowerShell that is running the cmdlet.
        So if this function is run from pwsh, it will default to PowerShell Core.
        If this function is run from powershell.exe, it will default to Windows PowerShell.
 
        PowerShellEdition='Desktop' is only supported on Windows PowerShell 5.1 and newer.
        If you try to use PowerShellEdition='Desktop' and Platform='Linux', an error will be thrown.
      #>

      [string]
      [ValidateSet('Core', 'Desktop')]
      $PowerShellEdition = $(Get-PSEdition)
   )

   Begin
   {
   }
   Process
   {

      <#
         Basic procedure is as follows:
         1. Verify params and perform setup (create dirs, clean, etc.)
         2. Read in script file if needed
         3. Base64 encode script for easy handling (no dealing with quotes)
         4. Insert script and replace tokens in class template
         5. Output class + additional files to .cs files in scratch dir
            - Maybe add an additional step here in the future to run
              preprocessing on c# files (allow a script block -PreprocessBlock argument)
         6. Run C# compiler over those files and produce an exe in the out dir
         7. Cleanup
       #>


      # 1. Verify params and perform setup (create dirs, clean, etc.)
      $inline = $PSCmdlet.ParameterSetName -eq 'Inline'
      if (!$inline -and !(Test-Path $InFile))
      {
         throw "Error: $InFile could not be found or you do not have access"
      }

      # flags for later
      $hasOutDir = $PSBoundParameters.ContainsKey('OutDir')
      $hasScratchDir = $PSBoundParameters.ContainsKey('ScratchDir')
      $hasOutFile = $PSBoundParameters.ContainsKey('OutFile')
      $hasClassTemplate = $PSBoundParameters.ContainsKey('ClassTemplate')
      $hasAttributesTemplate = $PSBoundParameters.ContainsKey('AttributesTemplate')
      $multipleFiles = !$inline -and ($InFile.Count -gt 0)
      $target = "exe"
      $outExt = "exe"

      # If the user wants to cross-compile to linux but runs the cmdlet from Windows PowerShell, we nede to change the PowerShellEdition to Core
      # use can override this behavior by specifying -PowerShellEdition
      if ($Platform -eq 'Linux' -and $PowerShellEdition -eq 'Desktop' -and !$PSBoundParameters.ContainsKey('PowerShellEdition'))
      {
         $PowerShellEdition = 'Core'
      }

      if ($PowerShellEdition -eq 'Desktop' -and $Platform -eq 'Linux')
      {
         throw "PowerShellEdition='Desktop' is only supported when Platform='Windows'"
      }

      # Warn if both -Namespace and -Tokens are specified and tokens contains Namespace
      if ($PSBoundParameters.ContainsKey('Namespace') -and $null -ne $Tokens -and $Tokens.ContainsKey('Namespace'))
      {
         Write-Warning "Both -Namespace was specified and -Tokens, containing a value for Namespace. The value passed via -Tokens will be ignored."
      }

      # Warn if both -ClassName and -Tokens are specified and tokens contains ClassName
      if ($PSBoundParameters.ContainsKey('ClassName') -and $null -ne $Tokens -and $Tokens.ContainsKey('ClassName'))
      {
         Write-Warning "Both -ClassName was specified and -Tokens, containing a value for ClassName. The value passed via -Tokens will be ignored."
      }

      if ($ClassName -eq $Namespace)
      {
         throw "ClassName cannot be equal to Namespace"
      }
      
      $currentDir = (Get-Location).Path
      if (!$hasOutDir)
      {
         $OutDir = $currentDir
      }
      if (!$hasScratchDir)
      {
         $ScratchDir = "$currentDir/.binwips"
      }
      if (!$hasOutFile)
      {
         if ($inline)
         {
            $OutFile = "$OutDir/PSBinary.$outExt"
         }
         elseif ($multipleFiles)
         {
            #$OutFile = #$InFile[-1].Replace(".ps1", ".$outExt")
            $OutFile = [System.IO.Path]::ChangeExtension($InFile[-1], $outExt)
         }
         else
         {
            #$OutFile = $InFile.Replace(".ps1", ".$outExt")
            $OutFile = [System.IO.Path]::ChangeExtension($InFile, $outExt)
         }
      } # otherwise if path isn't absolute, make it absolute to out dir
      elseif ([System.IO.Path]::IsPathRooted($OutFile) -eq $false)
      {
         $OutFile = Join-Path $OutDir $OutFile
      }  
 

      if (!$hasClassTemplate -and $Library)
      {
         $ClassTemplate = Get-Content -Raw "$PSScriptRoot/../files/LibraryClassTemplate.cs"
      }
      elseif (!$hasClassTemplate)
      {
         $ClassTemplate = Get-Content -Raw "$PSScriptRoot/../files/ClassTemplate.cs"
      }
      if (!$hasAttributesTemplate)
      {
         $AttributesTemplate = Get-Content -Raw "$PSScriptRoot/../files/AttributesTemplate.cs"
      }

      if ($inline)
      {
         $psScript = $ScriptBlock.ToString()
         
      }
      else
      { 
         # read in content from each input file and merge them into 1 string
         $psScript = $InFile | ForEach-Object { Get-Content -Raw $_ } | Out-String

         
      }

      # If Platform and Architecture are not specified, use the current platform and architecture
      if (!$PSBoundParameters.ContainsKey('Platform') -and $IsWindows)
      {
         $Platform = 'Windows'
        
      }
      elseif (!$PSBoundParameters.ContainsKey('Platform') -and $IsLinux)
      {
         $Platform = 'Linux'
      }
      elseif (!$PSBoundParameters.ContainsKey('Platform'))
      {
         throw "Unsported platform"
      }

      if (!$PSBoundParameters.ContainsKey('Architecture'))
      {
         $Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
      }

      if ($PSCmdlet.ShouldProcess('Create Scratch Directory'))
      {
         New-Item -ItemType Directory -Path $ScratchDir -Force | Out-Null
        
      }
      if ($PSCmdlet.ShouldProcess('Create Output Directory'))
      {
         New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
      }

      # Download Bflat if needed
      Get-BinWipsBFlat


      $funcArgs = @{
         Script             = $psScript
         Namespace          = $Namespace
         ClassName          = $ClassName
         OutFile            = $OutFile
         Target             = $target
         AssemblyAttributes = $AssemblyAttributes
         ClassAttributes    = $ClassAttributes
         ClassTemplate      = $ClassTemplate
         AttributesTemplate = $AttributesTemplate
         Tokens             = $Tokens
         CscArgumentList    = $ExtraArguments
         OutDir             = $OutDir
         ScratchDir         = $ScratchDir
         Cleanup            = $Cleanup
         Force              = $Force
         Resources          = $Resources
         NoEmbedResources   = $NoEmbedResources
         Platform           = $Platform
         Architecture       = $Architecture
         References         = $HostReferences
         PowerShellEdition  = $PowerShellEdition
      }     

      Build-Bflat @funcArgs
      
   }
   End
   {
   }
}