Bootstrap/Eigenverft.Manifested.Package.Bootstrap.ps1

[CmdletBinding()]
param(
    [ValidateSet('CurrentUser', 'AllUsers')]
    [string]$Scope = 'CurrentUser',

    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3

$requiredArtifacts = @(
    [pscustomobject]@{
        Id                   = 'packageManagementPackage'
        ModuleName           = 'PackageManagement'
        RequiredVersion      = '1.4.8.1'
        FileName             = 'PackageManagement.1.4.8.1.nupkg'
        Sha256               = '7e1f8a75b6bc8a83d8abff79f6690fc1dfbd534fd3e5733d97e19bcb5954c13e'
        AllowClobber         = $true
        SkipPublisherCheck   = $true
        RequireNuGetProvider = $true
    }
    [pscustomobject]@{
        Id                   = 'powerShellGetPackage'
        ModuleName           = 'PowerShellGet'
        RequiredVersion      = '2.2.5'
        FileName             = 'PowerShellGet.2.2.5.nupkg'
        Sha256               = '6b8cebf2a464eaeb31b0a6d627355c30d9d1899dba0ce3bdd0d4e7afca148673'
        AllowClobber         = $true
        SkipPublisherCheck   = $false
        RequireNuGetProvider = $false
    }
    [pscustomobject]@{
        Id                   = 'package'
        ModuleName           = 'Eigenverft.Manifested.Package'
        RequiredVersion      = '1.20264.4323'
        FileName             = 'Eigenverft.Manifested.Package.1.20264.4323.nupkg'
        Sha256               = '59de7e2d2514d80ab917b9e22e369a0379c867c3d56a3f87e9f37ede1a294c89'
        AllowClobber         = $true
        SkipPublisherCheck   = $false
        RequireNuGetProvider = $false
    }
)

function Assert-BootstrapHost {
    if ($PSVersionTable.PSVersion -lt [Version]'5.1') {
        throw "Eigenverft.Manifested.Package bootstrap requires Windows PowerShell 5.1 or newer. Found '$($PSVersionTable.PSVersion)'."
    }
    if (-not [string]::Equals([string]$PSVersionTable.PSEdition, 'Desktop', [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Run this bootstrap through Windows PowerShell (powershell.exe), not '$($PSVersionTable.PSEdition)' PowerShell."
    }
}

function Resolve-BootstrapArtifacts {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ArtifactDirectory,

        [Parameter(Mandatory = $true)]
        [object[]]$Artifacts
    )

    $resolvedArtifacts = New-Object System.Collections.Generic.List[object]
    $validationErrors = New-Object System.Collections.Generic.List[string]
    foreach ($artifact in $Artifacts) {
        $path = [System.IO.Path]::GetFullPath((Join-Path $ArtifactDirectory ([string]$artifact.FileName)))
        if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
            $validationErrors.Add("Missing required artifact '$($artifact.Id)': $path") | Out-Null
            continue
        }

        $actualHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
        if (-not [string]::Equals($actualHash, [string]$artifact.Sha256, [System.StringComparison]::OrdinalIgnoreCase)) {
            $validationErrors.Add("Artifact '$($artifact.Id)' failed SHA-256 verification. Expected '$($artifact.Sha256)', found '$actualHash'.") | Out-Null
            continue
        }

        $resolvedArtifacts.Add([pscustomobject]@{
            Id                   = [string]$artifact.Id
            ModuleName           = [string]$artifact.ModuleName
            RequiredVersion      = [string]$artifact.RequiredVersion
            FileName             = [string]$artifact.FileName
            Path                 = $path
            Sha256               = [string]$artifact.Sha256
            AllowClobber         = [bool]$artifact.AllowClobber
            SkipPublisherCheck   = [bool]$artifact.SkipPublisherCheck
            RequireNuGetProvider = [bool]$artifact.RequireNuGetProvider
        }) | Out-Null
    }

    if ($validationErrors.Count -gt 0) {
        throw "Offline bootstrap artifact validation failed:`r`n - $([string]::Join("`r`n - ", @($validationErrors.ToArray())))"
    }

    return @($resolvedArtifacts.ToArray())
}

function Expand-BootstrapInstallerHelper {
    param(
        [Parameter(Mandatory = $true)]
        [string]$PackagePath,

        [Parameter(Mandatory = $true)]
        [string]$DestinationPath
    )

    $entryPath = 'Support/Package/Execution/Invoke-PackagePowerShellModuleInstall.ps1'
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    $archive = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
    try {
        $entry = $archive.Entries |
            Where-Object { [string]::Equals($_.FullName, $entryPath, [System.StringComparison]::OrdinalIgnoreCase) } |
            Select-Object -First 1
        if (-not $entry) {
            throw "Eigenverft package '$PackagePath' does not contain installer helper '$entryPath'."
        }

        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath)
    }
    finally {
        $archive.Dispose()
    }

    return $DestinationPath
}

function Invoke-BootstrapInstallerHelper {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Check', 'Install')]
        [string]$Operation,

        [Parameter(Mandatory = $true)]
        [psobject]$Artifact,

        [Parameter(Mandatory = $true)]
        [string]$HelperPath,

        [Parameter(Mandatory = $true)]
        [string]$WorkDirectory,

        [Parameter(Mandatory = $true)]
        [string]$NugetDirectory,

        [Parameter(Mandatory = $true)]
        [string]$ProviderDirectory,

        [Parameter(Mandatory = $true)]
        [string]$InstallScope
    )

    $requestId = '{0}-{1}' -f (($Artifact.ModuleName -replace '[^A-Za-z0-9.-]', '_')), $Operation.ToLowerInvariant()
    $requestPath = Join-Path $WorkDirectory ($requestId + '-request.json')
    $resultPath = Join-Path $WorkDirectory ($requestId + '-result.json')
    $request = [ordered]@{
        operation             = $Operation
        definitionId          = [string]$Artifact.ModuleName
        packageId             = [string]$Artifact.Id
        moduleName            = [string]$Artifact.ModuleName
        requiredVersion       = [string]$Artifact.RequiredVersion
        scope                 = $InstallScope
        allowClobber          = [bool]$Artifact.AllowClobber
        skipPublisherCheck    = [bool]$Artifact.SkipPublisherCheck
        requireNuGetProvider  = [bool]$Artifact.RequireNuGetProvider
        repositoryName        = 'EVFBootstrap_{0}' -f ([Guid]::NewGuid().ToString('N').Substring(0, 12))
        stageDirectory        = $WorkDirectory
        nugetDirectory        = $NugetDirectory
        providerDirectory     = $ProviderDirectory
    }
    $request | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $requestPath -Encoding UTF8

    $powerShellPath = Join-Path $PSHOME 'powershell.exe'
    if (-not (Test-Path -LiteralPath $powerShellPath -PathType Leaf)) {
        throw "Windows PowerShell executable '$powerShellPath' was not found."
    }

    $helperOutput = @(
        & $powerShellPath -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $HelperPath -RequestPath $requestPath -ResultPath $resultPath 2>&1
    )
    $exitCode = $LASTEXITCODE
    if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
        throw "PowerShell module helper did not return a result for '$($Artifact.ModuleName)'. Exit code: $exitCode. Output: $([string]::Join(' | ', @($helperOutput)))"
    }

    $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
    if ($exitCode -ne 0 -or -not [bool]$result.success) {
        $message = if ($result.PSObject.Properties['errorMessage']) { [string]$result.errorMessage } else { [string]::Join(' | ', @($helperOutput)) }
        throw "PowerShell module helper $Operation failed for '$($Artifact.ModuleName)' version '$($Artifact.RequiredVersion)': $message"
    }

    return $result
}

Assert-BootstrapHost
$artifactDirectory = [System.IO.Path]::GetFullPath($PSScriptRoot)
$artifacts = @(Resolve-BootstrapArtifacts -ArtifactDirectory $artifactDirectory -Artifacts $requiredArtifacts)
$tempBase = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
$workDirectory = [System.IO.Path]::GetFullPath((Join-Path $tempBase ('Eigenverft.Manifested.Package.Bootstrap-' + [Guid]::NewGuid().ToString('N'))))
if (-not $workDirectory.StartsWith($tempBase, [System.StringComparison]::OrdinalIgnoreCase)) {
    throw "Refusing to create bootstrap work directory outside '$tempBase'."
}

try {
    $nugetDirectory = Join-Path $workDirectory 'Nuget'
    $providerDirectory = Join-Path $workDirectory 'Provider'
    New-Item -ItemType Directory -Path $nugetDirectory -Force | Out-Null
    New-Item -ItemType Directory -Path $providerDirectory -Force | Out-Null
    foreach ($artifact in $artifacts) {
        Copy-Item -LiteralPath $artifact.Path -Destination (Join-Path $nugetDirectory $artifact.FileName) -Force
    }

    $packageArtifact = $artifacts | Where-Object { $_.Id -eq 'package' } | Select-Object -First 1
    if (-not $packageArtifact) {
        throw "Offline bootstrap artifact set does not contain the Eigenverft package artifact."
    }
    $helperPath = Expand-BootstrapInstallerHelper -PackagePath $packageArtifact.Path -DestinationPath (Join-Path $workDirectory 'Invoke-PackagePowerShellModuleInstall.ps1')

    if ($ValidateOnly.IsPresent) {
        Write-Host "Eigenverft offline bootstrap bundle is valid. Verified $($artifacts.Count) module packages."
        return [pscustomobject]@{
            Status            = 'Valid'
            ArtifactDirectory = $artifactDirectory
            ArtifactCount     = $artifacts.Count
            Scope             = $Scope
        }
    }

    $installedModules = New-Object System.Collections.Generic.List[object]
    foreach ($artifact in $artifacts) {
        Write-Host "Checking $($artifact.ModuleName) $($artifact.RequiredVersion)..."
        $check = Invoke-BootstrapInstallerHelper -Operation Check -Artifact $artifact -HelperPath $helperPath -WorkDirectory $workDirectory -NugetDirectory $nugetDirectory -ProviderDirectory $providerDirectory -InstallScope $Scope
        if (-not [bool]$check.installed) {
            Write-Host "Installing $($artifact.ModuleName) $($artifact.RequiredVersion) from the offline bundle..."
            $install = Invoke-BootstrapInstallerHelper -Operation Install -Artifact $artifact -HelperPath $helperPath -WorkDirectory $workDirectory -NugetDirectory $nugetDirectory -ProviderDirectory $providerDirectory -InstallScope $Scope
            if (-not [bool]$install.installed) {
                throw "PowerShell module '$($artifact.ModuleName)' version '$($artifact.RequiredVersion)' was not installed."
            }
            $check = $install
        }
        else {
            Write-Host "Found $($artifact.ModuleName) $($artifact.RequiredVersion)."
        }
        $installedModules.Add([pscustomobject]@{
            Name       = [string]$artifact.ModuleName
            Version    = [string]$artifact.RequiredVersion
            ModuleBase = if ($check.PSObject.Properties['moduleBase']) { [string]$check.moduleBase } else { $null }
            Status     = [string]$check.status
        }) | Out-Null
    }

    $packageCheck = Invoke-BootstrapInstallerHelper -Operation Check -Artifact $packageArtifact -HelperPath $helperPath -WorkDirectory $workDirectory -NugetDirectory $nugetDirectory -ProviderDirectory $providerDirectory -InstallScope $Scope
    if (-not [bool]$packageCheck.installed) {
        throw "Eigenverft.Manifested.Package $($packageArtifact.RequiredVersion) was not discoverable after installation."
    }

    Write-Host "Eigenverft.Manifested.Package $($packageArtifact.RequiredVersion) offline bootstrap completed successfully."
    return [pscustomobject]@{
        Status            = 'Installed'
        ArtifactDirectory = $artifactDirectory
        Scope             = $Scope
        Modules           = @($installedModules.ToArray())
    }
}
finally {
    if ((Test-Path -LiteralPath $workDirectory -PathType Container) -and
        $workDirectory.StartsWith($tempBase, [System.StringComparison]::OrdinalIgnoreCase)) {
        Remove-Item -LiteralPath $workDirectory -Recurse -Force -ErrorAction SilentlyContinue
    }
}