psbundle.psm1

$psBundleLoader = @'
# psbundle loader
Set-ExecutionPolicy Bypass -Force -Scope Process
 
if($null -eq $env:PSBundlePath){
    $env:PSModulePath = (Resolve-Path 'psbundle_modules').Path + ';' + $env:PSModulePath
}else{
    $env:PSModulePath = (Resolve-Path (Join-Path $env:PSBundlePath 'psbundle_modules')).Path + ';' + $env:PSModulePath
}
 
'@


$psBundleExeLdr = @'
using System.IO;
using System.Reflection;
using System.Diagnostics;
 
class _PSBundleAutoGeneratedExeLdr {{
    static readonly string tempDir = Path.Combine(Path.GetTempPath(), "psb{0}");
    static readonly Assembly executingAssembly = Assembly.GetExecutingAssembly();
 
    static void ExtractFile(string fileName){{
        Directory.CreateDirectory(Path.Combine(tempDir, Path.GetDirectoryName(fileName)));
 
        Stream stream = executingAssembly.GetManifestResourceStream(fileName);
        byte[] data = new byte[stream.Length];
        stream.Read(data, 0, (int)stream.Length);
        stream.Close();
 
        File.WriteAllBytes(Path.Combine(tempDir, fileName), data);
    }}
 
    public static void Main(){{
        if(!Directory.Exists(tempDir)){{
            {1}
        }}
 
        ProcessStartInfo psi = new ProcessStartInfo();
        psi.FileName = "cmd.exe";
        psi.Arguments = "/c {2}";
        psi.EnvironmentVariables.Add("PSBundlePath", tempDir);
        psi.UseShellExecute = false;
        psi.CreateNoWindow = {3};
 
        Process.Start(psi).WaitForExit();
    }}
}}
'@


$alphabet = @('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9')

Function Get-RandomString {
    Param(
        [Parameter(Position=0)]
        [int] $Count = 8
    )

    return '_' + ((Get-Random -Count ($Count - 1) -InputObject $alphabet) -join '')
}

Function Invoke-Csc {
    Param(
        [Parameter(Mandatory, Position=0)]
        [string] $Arguments,

        [Parameter(Position=1)]
        [string] $WorkingDirectory = (Resolve-Path '.')
    )

    $cscDirName = Get-ChildItem -Path "$Env:SYSTEMROOT\Microsoft.NET\Framework64\v*" -Name | Sort-Object { [version]$_.Substring(1) } -Descending | Select-Object -First 1
    $cscExeName = Join-Path "$Env:SYSTEMROOT\Microsoft.NET\Framework64" $cscDirName 'csc.exe'

    $psi = [System.Diagnostics.ProcessStartInfo]::new()
    $psi.FileName = $cscExeName
    $psi.Arguments = $Arguments
    $psi.WorkingDirectory = $WorkingDirectory

    $process = [System.Diagnostics.Process]::Start($psi)
    $process.WaitForExit()
}

Function New-BundledExe {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [string] $Command,

        [Parameter(Mandatory)]
        [string] $OutputPath,

        [hashtable] $IncludeFiles = @(),
        [switch] $HideConsole
    )

    $codeTmpFile = (New-TemporaryFile).FullName
    $cmdTmpFile = (New-TemporaryFile).FullName

    $sbCode = [System.Text.StringBuilder]::new()
    $sbCmd = [System.Text.StringBuilder]::new()

    $null = $sbCmd.AppendLine('/nologo')
    $null = $sbCmd.AppendLine("`"/out:$OutputPath`"")
    if($HideConsole.IsPresent){ $null = $sbCmd.AppendLine("/target:winexe") }

    $IncludeFiles.Keys | ForEach-Object {
        $name = $_.Replace('\', '\\')
        $null = $sbCode.AppendLine("ExtractFile(`"$name`");")
        $null = $sbCmd.AppendLine("`"/resource:$($IncludeFiles[$_]),$_`"")
    }

    $null = $sbCmd.AppendLine("`"$codeTmpFile`"")

    $code = [string]::Format($psBundleExeLdr, (Get-RandomString -Count 10), $sbCode.ToString(), $Command, $HideConsole.IsPresent.ToString().ToLower())

    $code | Out-File $codeTmpFile
    $sbCmd.ToString() | Out-File $cmdTmpFile

    Invoke-Csc -Arguments "`"@$cmdTmpFile`""

    Remove-Item -Force $cmdTmpFile
    Remove-Item -Force $codeTmpFile
}

Function New-PSBundle {
    [CmdletBinding()]
    Param(
        [Parameter(Position=0)]
        [string] $Path = '.'
    )

    $null = New-Item -ItemType Directory -Path (Join-Path $Path 'psbundle_modules')
    $null = New-Item -ItemType Directory -Path (Join-Path $Path 'dist')
}

Function Install-PSBundleModule {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, Position=0)]
        [string[]] $ModuleName,

        [string] $Path = '.'
    )

    Save-Module -Name $ModuleName -Path (Join-Path $Path 'psbundle_modules') -Force -AcceptLicense
}

Function Build-PSBundle {
    [CmdletBinding()]
    Param(
        [string] $Path = '.',

        [ValidateSet('ps1', 'bat', 'exe')]
        [string] $Target = 'ps1',

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

        [switch] $BundlePSCore,
        [switch] $HideConsole
    )

    $sb = [System.Text.StringBuilder]::new()
    $null = $sb.AppendLine($psBundleLoader)

    Get-ChildItem -Path $Path -Include '*.ps1' -Recurse -Name | `
    Where-Object { -not $_.StartsWith('psbundle_modules') -and -not $_.StartsWith('dist') } | `
    ForEach-Object { $null = $sb.AppendLine((Get-Content -Raw -Path $_)) }

    Set-Content -Path (Join-Path $Path 'dist' 'bundle.ps1') -Value $sb.ToString()

    switch($Target){
        'exe' {
            Write-Verbose 'Selected target: EXE'
            $psFileName = 'powershell.exe'

            if($PSEdition -eq 'Core'){
                if($BundlePSCore.IsPresent){
                    $psFileName = '..\\pwsh\\pwsh.exe'
                }else{
                    $psFileName = 'pwsh.exe'
                }
            }

            $fileList = @{}

            Get-ChildItem (Join-Path $Path 'dist') -Recurse -File -Name | `
            ForEach-Object {
                $file = Join-Path 'dist' $_
                $fileList.Add($file, $file)
            }

            Get-ChildItem (Join-Path $Path 'psbundle_modules') -Recurse -File -Name | `
            ForEach-Object {
                $file = Join-Path 'psbundle_modules' $_
                $fileList.Add($file, $file)
            }

            if($BundlePSCore.IsPresent){
                Get-ChildItem $PSHome -Recurse -File -Name | ForEach-Object {
                    $fileList.Add((Join-Path 'pwsh' $_), (Join-Path $PSHome $_))
                }
            }

            $fileList | ForEach-Object { Write-Verbose "Adding file: $_" }

            New-BundledExe -OutputPath (Join-Path $Path 'dist' 'bundle.exe') -Command "cd /d \`"%PSBundlePath%\\dist\`" & $psFileName -ExecutionPolicy bypass -File bundle.ps1" -IncludeFiles $fileList -HideConsole:$HideConsole.IsPresent
        }
    }
}

Function Invoke-PSBundle {
    [CmdletBinding()]
    [Alias('psbundle')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [string] $Command,

        [Parameter(Position=1)]
        [string] $Name = '.',

        [string] $Path = '.',

        [ValidateSet('ps1', 'bat', 'exe')]
        [string] $Target = 'ps1',

        [ValidateSet('Desktop', 'Core')]
        [string] $PSEdition = $PSVersionTable.PSEdition,

        [switch] $BundlePSCore,
        [switch] $HideConsole
    )

    switch($Command){
        'new' {
            New-PSBundle -Path $Name
        }

        'install' {
            Install-PSBundleModule -ModuleName $Name -Path $Path
        }

        'build' {
            Build-PSBundle -Path $Name -Target $Target -PSEdition $PSEdition -BundlePSCore:$BundlePSCore.IsPresent -HideConsole:$HideConsole.IsPresent
        }
    }
}

# Export-ModuleMember -Function New-PSBundle,Install-PSBundleModule,Build-PSBundle,Invoke-PSBundle,New-BundledExe,Get-RandomString -Alias psbundle