Public/New-WinRarSFX.ps1

<#
.SYNOPSIS
    Creates a self-extracting archive (SFX) from a directory with configurable installation options.
 
.DESCRIPTION
    Creates a self-extracting (SFX) archive using WinRAR, with required extraction path, optional
    post-extraction command. Automatically handles PowerShell script
    detection and execution, with flexible naming options.
 
    The function follows this priority for output naming:
    1. User-specified OutPutExe name extension (ex: "Custom_Tool_Name.exe")
    3. Title parameter (with spaces converted to underscores)
    2. Uses detected PowerShell script name
    4. Source directory name as final fallback
 
.PARAMETER FilePath
    Path to the directory that will be compressed into the SFX archive.
 
.PARAMETER OutputExe
    Path where the SFX executable will be created.
 
.PARAMETER WinRarPath
    Path to WinRAR installation directory. Defaults to "C:\Program Files\WinRAR".
    Will attempt to find WinRAR in common installation paths if not found at default location.
 
.PARAMETER ExtractionPath
    Target path where files will be extracted.
    Defaults to "C:\Windows\Temp\{Title}".
 
.PARAMETER SetupCommand
    Command to execute after extraction. Supports:
    - "Script" - Auto-detects and executes a PowerShell script (recommended for .ps1 files)
    - Custom command (e.g., "notepad.exe config.txt")
    - Skip post-extraction execution by omitting this parameter
 
.PARAMETER Title
    Custom title for the SFX and default exe name (spaces converted to underscores for filename).
    If not specified, uses:
    1. Detected PowerShell script name (without extension)
    2. Source directory name as fallback
 
.PARAMETER Overwrite
    Controls file overwrite behavior:
    0 = Prompt user
    1 = Overwrite silently (default)
    2 = Skip existing files
 
.PARAMETER Silent
    Controls interface visibility:
    0 = Show all dialogs and progress
    1 = Silent operation (default)
    2 = Show only errors
 
.EXAMPLE
    New-WinRarSFX -FilePath "C:\Scripts\Tool" -SetupCommand "Script"
 
    # Pipeline input for FilePath
    "C:\Scripts\Tool" | New-WinRarSFX -SetupCommand "Script"
     
    Basic usage that auto-detects a PowerShell script. Uses .Ps1 file name,
    creates "Your_File_Name.exe" that runs the script after extraction.
 
.EXAMPLE
    New-WinRarSFX -FilePath "C:\Config\App" -SetupCommand "cmd.exe /c start config.exe"
     
    Creates an SFX that runs a custom command.
    Useful for non-PowerShell deployments.
 
.EXAMPLE
    New-WinRarSFX -FilePath "C:\Scripts\Tool" -Title "Custom_Tool_Name" -SetupCommand "Script"
 
    Creates an SFX with a specific name regardless of the script name or source directory.
    Useful when you need consistent naming across different deployments.
 
.EXAMPLE
    $params = @{
        FilePath = "C:\Scripts\Tool"
        OutputExe = "C:\Scripts\Tool\CustomDeployment.exe"
        ExtractionPath = "C:\Windows\Temp\Tool"
        SetupCommand = "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"C:\Windows\Temp\GPUninstall\Install.ps1`""
        Title = "Custom Title"
        Silent = 1
        Overwrite = 1
        Verbose = $true
    }
    New-WinRarSFX @params
     
    Advanced usage with full parameter control.
 
.EXAMPLE
    # Process multiple script directories for deployment
    Get-ChildItem -Path "C:\DeploymentScripts" -Directory | New-WinRarSFX -SetupCommand "Script"
     
    Creates an SFX for each subdirectory in DeploymentScripts, auto-detecting PowerShell scripts.
    Useful for batch processing multiple deployment packages.
 
.NOTES
    Name: New-WinRarSFX
    Author: AutomateSilent
    Version: 1.0.0
    Last Updated: 2025-02-06
     
    Requires WinRAR to be installed on the system.
    Automatically handles spaces in paths and file names.
    Best used with PowerShell scripts using the "Script" template.
#>


function New-WinRarSFX {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to the directory that will be compressed into the SFX archive"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,
        
        [Parameter(
            Position = 1,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path where the SFX executable will be created"
        )]
        [string]$OutputExe,
        
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to WinRAR installation directory"
        )]
        [string]$WinRarPath = "C:\Program Files\WinRAR",
        
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Target path where files will be extracted"
        )]
        [string]$ExtractionPath,
        
        [Parameter(
            Position = 2,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Command to execute after extraction. Use 'Script' for automatic PowerShell script detection"
        )]
        [string]$SetupCommand,
        
        [Parameter(
            Position = 3,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Custom title for the SFX and default exe name (spaces converted to underscores)"
        )]
        [string]$Title,
        
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Controls file overwrite behavior (0=Prompt, 1=Overwrite, 2=Skip)"
        )]
        [ValidateRange(0, 2)]
        [int]$Overwrite = 1,
        
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Controls interface visibility (0=Show all, 1=Silent, 2=Show errors)"
        )]
        [ValidateRange(0, 2)]
        [int]$Silent = 1
    )

    begin {
        Write-Verbose "Checking elevation status..."
        $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = New-Object Security.Principal.WindowsPrincipal $identity
        if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            throw "This function requires elevated privileges. Please run PowerShell as Administrator."
        }

        Write-Verbose "Initializing SFX creation process..."
        
        # Check WinRAR installation first
        $alternativePaths = @(
            $WinRarPath,
            "${env:ProgramFiles}\WinRAR",
            "${env:ProgramFiles(x86)}\WinRAR"
        )
        
        $validPath = $null
        foreach ($path in $alternativePaths) {
            if (Test-Path $path) {
                $validPath = $path
                break
            }
        }
        
        if (-not $validPath) {
            Write-Host "`nWinRAR not found in standard locations." -ForegroundColor Yellow
            Write-Host "Download WinRAR from: https://www.win-rar.com/download.html?&L=0" -ForegroundColor Cyan
            Write-Host "After installing WinRAR, you can continue with the installation.`n" -ForegroundColor Yellow
            
            do {
                $customPath = Read-Host "Please specify WinRAR installation path (or 'Q' to quit)"
                
                if ($customPath -eq 'Q') {
                    throw "Operation cancelled by user"
                }
                
                if (Test-Path $customPath) {
                    $validPath = $customPath
                    break
                } else {
                    Write-Warning "Invalid path. Please try again."
                }
            } while (-not $validPath)
        }
        
        $WinRarPath = $validPath
        Write-Verbose "Using WinRAR from: $WinRarPath"
        
        # Now validate source path
        try {
            $FilePath = [System.IO.Path]::GetFullPath($FilePath)
        } catch {
            throw "Invalid file path format: $FilePath"
        }
        
        if (-not (Test-Path $FilePath)) {
            do {
                Write-Warning "Source path not found: $FilePath"
                $newPath = Read-Host "Please specify a valid source path (or 'Q' to quit)"
                
                if ($newPath -eq 'Q') {
                    throw "Operation cancelled by user"
                }
                
                if (Test-Path $newPath) {
                    $FilePath = [System.IO.Path]::GetFullPath($newPath)
                    break
                } else {
                    Write-Warning "Invalid path. Please try again."
                }
            } while ($true)
        }

        # If Title isn't specified, try to use setup script name, then fall back to directory name
        if (-not $Title) {
            $psScript = Get-ChildItem -Path $FilePath -Filter "*.ps1" | Select-Object -First 1
            if ($psScript) {
                $Title = [System.IO.Path]::GetFileNameWithoutExtension($psScript.Name)
            } else {
                $Title = (Split-Path -Leaf $FilePath)
            }
        }
        
        # Set default extraction path
        if (-not $ExtractionPath) {
            $ExtractionPath = "C:\Windows\Temp\$Title"
        }
        
        # Set OutputExe based on Title if not explicitly specified
        if (-not $OutputExe) {
            $exeName = $Title -replace '\s+', '_'
            $OutputExe = Join-Path $FilePath "$exeName.exe"
        }
        
        # Ensure output directory exists
        $outputDir = Split-Path -Parent $OutputExe
        if (-not (Test-Path $outputDir)) {
            New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
        }
    }

    process {
        try {
            Write-Verbose "Creating temporary workspace..."
            $workDir = Join-Path $env:TEMP ([Guid]::NewGuid().ToString())
            New-Item -ItemType Directory -Path $workDir -Force | Out-Null

            # Handle SetupCommand processing
            $finalSetupCommand = ""
            if ($SetupCommand) {
                if ($SetupCommand -eq "Script") {
                    Write-Verbose "Detecting PowerShell scripts in source directory..."
                    $psScripts = Get-ChildItem -Path $FilePath -Filter "*.ps1"
                    
                    if ($psScripts.Count -eq 0) {
                        throw "No PowerShell scripts found in source directory"
                    } elseif ($psScripts.Count -eq 1) {
                        $scriptName = $psScripts[0].Name
                        $finalSetupCommand = "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ExtractionPath\$scriptName`""
                    } else {
                        Write-Host "`nMultiple PowerShell scripts found:"
                        for ($i = 0; $i -lt $psScripts.Count; $i++) {
                            Write-Host "$($i + 1). $($psScripts[$i].Name)" -ForegroundColor Cyan
                        }
                        
                        $selection = Read-Host "`nSelect script number"
                        if ($selection -match '^\d+$' -and [int]$selection -le $psScripts.Count) {
                            $scriptName = $psScripts[[int]$selection - 1].Name
                            $finalSetupCommand = "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ExtractionPath\$scriptName`""
                        } else {
                            throw "Invalid script selection"
                        }
                    }
                } else {
                    $finalSetupCommand = $SetupCommand
                }
            }

            # Generate SFX configuration
            Write-Verbose "Generating SFX configuration..."
            $configContent = @"
;The comment below contains SFX script commands
Path="$ExtractionPath"
"@

            
            if ($finalSetupCommand) {
                $configContent += "`nSetup=$finalSetupCommand"
            }
            
            $configContent += @"
 
Overwrite=$Overwrite
Silent=$Silent
Title="$Title"
"@


            $configPath = Join-Path $workDir "config.txt"
            [System.IO.File]::WriteAllText($configPath, $configContent, [System.Text.Encoding]::ASCII)

            # Select appropriate SFX module
            Write-Verbose "Selecting SFX module..."
            $sfxModule = Join-Path $WinRarPath "Default.SFX"
            if (-not (Test-Path $sfxModule)) {
                $sfxModule = Join-Path $WinRarPath "WinCon.SFX"
                if (-not (Test-Path $sfxModule)) {
                    throw "No suitable SFX module found in WinRAR directory"
                }
            }

            # Build WinRAR command
            Write-Verbose "Building WinRAR command..."
            $winrarExe = Join-Path $WinRarPath "WinRAR.exe"
            $startInfo = New-Object System.Diagnostics.ProcessStartInfo
            $startInfo.FileName = $winrarExe
            $startInfo.Arguments = "a -cfg- -ep1 -inul -iadm -m5 -r -s `"-sfx$sfxModule`" -y `"-z$configPath`" `"$OutputExe`" `"$FilePath\*`""
            $startInfo.UseShellExecute = $false
            $startInfo.RedirectStandardOutput = $true
            $startInfo.RedirectStandardError = $true
            $startInfo.CreateNoWindow = $true

            # Execute WinRAR process
            Write-Verbose "Executing WinRAR command..."
            $process = Start-Process -FilePath $startInfo.FileName -ArgumentList $startInfo.Arguments -NoNewWindow -Wait -PassThru

            if ($process.ExitCode -ne 0) {
                throw "WinRAR process failed with exit code: $($process.ExitCode)"
            }

            # Verify output file
            if (Test-Path $OutputExe) {
                Write-Host "SFX archive created successfully:" -ForegroundColor Green
                Write-Host "Location: $OutputExe" -ForegroundColor Cyan
                Write-Host "Size: $([math]::Round((Get-Item $OutputExe).Length / 1MB, 2)) MB" -ForegroundColor Cyan
            } else {
                throw "SFX creation failed - output file not found"
            }
        }
        catch {
            Write-Error "Failed to create SFX: $_"
            throw
        }
        finally {
            # Cleanup
            if (Test-Path $workDir) {
                Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    }

    end {
        Write-Verbose "SFX creation process completed"
    }
}