Public/New-7zSFX.ps1

Function New-7zSfx {
<#
    .SYNOPSIS
        Create a new 7-Zip self extracting archive
    .DESCRIPTION
        Create self-extracting archives using 7-Zip
    .EXAMPLE
        New-7zsfx app-sfx app.exe,app.exe.config app.exe
 
        Simply create a self-extracting exe from an executable file app.exe
        with its configuration file app.exe.config:
    .NOTES
        This might be omitted in later revisions as this sets off my
        sense of DSC flow and its old in its concept. Since WinRM and
        DSC self expanding objects are a non starter. I have included
        this for now but im reviewing security articles to determine
        use in different environments.
 
    .LINK
        https://documentation.help/7-Zip/sfx.htm
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Low')]
    Param(
        # The name of the exe-file to produce, without extension
        [Parameter(Mandatory=$true, Position=0)]
        [string]$Path,

        # The files to include in the archive
        [Parameter(Mandatory=$true, Position=1)]
        [string[]]$Include,

        # The command to run when the sfx archive is started
        [Parameter(Mandatory=$true, Position=2)]
        [string]$CommandToRun,

        # Title for messages
        [Parameter(Mandatory=$false)]
        [string]$Title,

        # Begin Prompt message
        [Parameter(Mandatory=$false)]
        [string]$BeginPrompt,

        # Title of extraction dialog
        [Parameter(Mandatory=$false)]
        [string]$ExtractTitle,

        # Text in dialog
        [Parameter(Mandatory=$false)]
        [string]$ExtractDialogText,

        # Button text of cancel button
        [Parameter(Mandatory=$false)]
        [string]$ExtractCancelText,

        # A list of additional options, of the form "key=value"
        [Parameter(Mandatory=$false)]
        [string[]]$ConfigOptions,

        # Include subdirectories
        [switch]$Recurse,

        # Additional switches to pass to 7za when creating the archive
        [string]$Switches = ''
    )

    Begin {

        # Escape a variable for the config file
        Function Esc([string]$t) {
            # Prefix \ and " with \, replace CRLF with \n and TAB with \t
            Return $t.Replace('\', '\\').Replace('"', '\"').Replace("`r`n", '\n').Replace("`t", '\t')
        }

        # Get the base name of the specified path in Name
        if (-not [IO.Path]::IsPathRooted($Path)) {
            $Path = Join-Path "." $Path
        }
        # Then join the directory name with the file name exluding the extension
        [string]$Name = Join-Path ([IO.Path]::GetDirectoryName($Path)) ([IO.Path]::GetFileNameWithoutExtension($Path))

        [string]$tmpfile = "$Name.sfx.tmp"
        [Collections.ArrayList]$cfg = @()

        [string]$exefile = "$Name.exe"
        if (Test-Path -PathType Leaf "$exefile") { Remove-Item "$exefile" -Force }

    }

    Process {
        if ($PSCmdlet.ShouldProcess('Create Executable Archive')) {

            $null = New-7zArchive -ArchivePath $tmpfile -FilesToInclude $Include -FilesToExclude @() -ArchiveType 7z -Recurse:$Recurse -Switches $Switches

            # Copy sfx + archive + config to exe via bytestream

            #SFX Configuration File Header
            [void]$cfg.Add(";!@Install@!UTF-8!")
            #Title - title for messages
            [void]$cfg.Add('Title="{0}"' -f $Title)
            #RunProgram - Command for executing. Default value is "setup.exe". Substring %%T will be replaced with path to temporary folder, where files were extracted
            [void]$cfg.Add('RunProgram="{0}"' -f $(Esc($CommandToRun)))
            #BeginPrompt - Begin Prompt message
            if ($BeginPrompt -ne "")       { [void]$cfg.Add('BeginPrompt="{0}"' -f $(Esc($BeginPrompt))) }
            #ExtractTitle - title of extraction dialog
            if ($ExtractTitle -ne "")      { [void]$cfg.Add('ExtractTitle="{0}"' -f $(Esc($ExtractTitle))) }
            #ExtractDialogText - text in dialog
            if ($ExtractDialogText -ne "") { [void]$cfg.Add('ExtractDialogText="{0}"' -f $(Esc($ExtractDialogText))) }
            #ExtractCancelText - button text of cancel button
            if ($ExtractCancelText -ne "") { [void]$cfg.Add('ExtractCancelText="{0}"' -f $(Esc($ExtractCancelText))) }

            #Progress - Value can be "yes" or "no". Default value is "yes".
            #Directory - Directory prefix for "RunProgram". Default value is ".\\"
            #ExecuteFile - Name of file for executing
            #ExecuteParameters - Parameters for "ExecuteFile"
            if ($null -ne $ConfigOptions) {
                $ConfigOptions | ForEach-Object {
                    [string[]]$parts = $_.Split('=')
                    if ($parts.Count -lt 2) {
                        throw "Invalid configuration option '$($_)': missing '='"
                    } else {
                        [void]$cfg.Add('{0}="{1}"' -f $($parts[0]), $(Esc($parts[1])))
                    }
                }
            }

            #SFX Configuration File Ending Suffix
            [void]$cfg.Add(';!@InstallEnd@!')


            Write-Verbose ('Creating sfx "{0}"...' -f $exefile)
            Write-Debug ($cfg | Join-String -Separator '`r`n')

            [string]$cfgfile = ( '{0}.sfx.cfg' -f $Name )

            Set-Content "$cfgfile" -Value $cfg
            Get-Content "$($7zSettings.Path7zSfx)","$cfgfile","$tmpfile" -AsByteStream -Raw | Set-Content "$exefile" -AsByteStream

        }
    }

    End {

        Remove-Item "$tmpfile"
        Remove-Item "$cfgfile"
    }
}