Private/Logic/RuntimeKernel/Execute/Manifested.Install.Shared.ps1

function Get-ManifestedRuntimeDependencyFacts {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RuntimeName,

        [string]$LocalRoot = (Get-ManifestedLocalRoot),

        [hashtable]$FactsCache = @{}
    )

    $dependencyContext = Get-ManifestedRuntimeContext -RuntimeName $RuntimeName
    if (-not $dependencyContext) {
        return $null
    }

    return (Get-ManifestedRuntimeFactsFromContext -Context $dependencyContext -LocalRoot $LocalRoot -FactsCache $FactsCache)
}

function Invoke-ManifestedPortableArchiveInstallFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$Facts,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $packageInfo = if ($Facts.PSObject.Properties['Package']) { $Facts.Package } else { $null }
    if (-not $packageInfo) {
        throw "The package for '$($Definition.commandName)' was not available for install."
    }

    $factsBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'facts' -BlockName 'portableRuntime'
    $installBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'portableArchive'
    if (-not $factsBlock -or -not $installBlock) {
        throw "The portable archive install blocks for '$($Definition.commandName)' were not available."
    }

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $toolsRoot = $layout.($installBlock.toolsRootLayoutProperty)
    $versionFolderName = Expand-ManifestedDefinitionTemplate -Template $(if ($installBlock.PSObject.Properties.Match('versionFolderTemplate').Count -gt 0) { $installBlock.versionFolderTemplate } else { '{version}' }) -Version $packageInfo.Version -TagName $packageInfo.Version -Flavor $null
    $versionFolderName = $versionFolderName.Replace('{versionNoPrefixV}', $packageInfo.Version.TrimStart('v', 'V'))
    $runtimeHome = Join-Path (Join-Path $toolsRoot $versionFolderName) $Facts.Flavor
    $currentValidation = Test-ManifestedPortableRuntimeHome -Definition $Definition -RuntimeHome $runtimeHome

    if (-not $currentValidation.IsUsable) {
        New-ManifestedDirectory -Path (Split-Path -Parent $runtimeHome) | Out-Null

        $stagePrefix = if ($installBlock.PSObject.Properties.Match('stagePrefix').Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($installBlock.stagePrefix)) { [string]$installBlock.stagePrefix } else { [string]$factsBlock.stagePrefix }
        $stageInfo = $null
        try {
            $stageInfo = Expand-ManifestedArchiveToStage -PackagePath $packageInfo.Path -Prefix $stagePrefix
            if (-not (Test-Path -LiteralPath $stageInfo.ExpandedRoot)) {
                throw "The archive for '$($Definition.commandName)' did not extract as expected."
            }

            if (Test-Path -LiteralPath $runtimeHome) {
                Remove-ManifestedPath -Path $runtimeHome | Out-Null
            }

            New-ManifestedDirectory -Path $runtimeHome | Out-Null
            Get-ChildItem -LiteralPath $stageInfo.ExpandedRoot -Force | ForEach-Object {
                Move-Item -LiteralPath $_.FullName -Destination $runtimeHome -Force
            }

            foreach ($relativeDirectory in @($installBlock.createDirectories)) {
                if (-not [string]::IsNullOrWhiteSpace([string]$relativeDirectory)) {
                    New-ManifestedDirectory -Path (Join-Path $runtimeHome ([string]$relativeDirectory)) | Out-Null
                }
            }
        }
        finally {
            if ($stageInfo) {
                Remove-ManifestedPath -Path $stageInfo.StagePath | Out-Null
            }
        }
    }

    $validation = Test-ManifestedPortableRuntimeHome -Definition $Definition -RuntimeHome $runtimeHome
    if (-not $validation.IsUsable) {
        throw "$($Definition.runtimeName) validation failed after install at $runtimeHome."
    }

    $result = [ordered]@{
        Action         = if ($currentValidation.IsUsable) { 'Skipped' } else { 'Installed' }
        Version        = $packageInfo.Version
        Flavor         = $Facts.Flavor
        RuntimeHome    = $runtimeHome
        ExecutablePath = $validation.ExecutablePath
        Source         = $packageInfo.Source
        DownloadUrl    = if ($packageInfo.PSObject.Properties['DownloadUrl']) { $packageInfo.DownloadUrl } else { $null }
        Sha256         = if ($packageInfo.PSObject.Properties['Sha256']) { $packageInfo.Sha256 } else { $null }
    }
    foreach ($property in @($validation.PSObject.Properties)) {
        if ($result.Contains($property.Name)) {
            continue
        }
        $result[$property.Name] = $property.Value
    }

    return [pscustomobject]$result
}

function Invoke-ManifestedPythonEmbeddableInstallFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$Facts,

        [bool]$RefreshRequested = $false,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $packageInfo = if ($Facts.PSObject.Properties['Package']) { $Facts.Package } else { $null }
    if (-not $packageInfo) {
        throw "The package for '$($Definition.commandName)' was not available for install."
    }

    return (Install-ManifestedPythonEmbeddableRuntime -Definition $Definition -PackageInfo $packageInfo -Flavor $Facts.Flavor -LocalRoot $LocalRoot -ForceInstall:$RefreshRequested)
}

function Invoke-ManifestedMachineInstallerProcess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$InstallerPath,

        [int]$TimeoutSec = 300
    )

    $logDirectory = Split-Path -Parent $InstallerPath
    if (-not [string]::IsNullOrWhiteSpace($logDirectory)) {
        New-ManifestedDirectory -Path $logDirectory | Out-Null
    }

    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $logPrefix = [System.IO.Path]::GetFileNameWithoutExtension($InstallerPath)
    if ([string]::IsNullOrWhiteSpace($logPrefix)) {
        $logPrefix = 'installer'
    }

    $logPath = Join-Path $logDirectory ($logPrefix + ".install.$timestamp.log")
    $quotedLogPath = if ($logPath.IndexOfAny([char[]]@(' ', "`t", '"')) -ge 0) {
        '"' + ($logPath -replace '"', '\"') + '"'
    }
    else {
        $logPath
    }

    $argumentList = @(
        '/install',
        '/quiet',
        '/norestart',
        '/log',
        $quotedLogPath
    )

    $process = Start-Process -FilePath $InstallerPath -ArgumentList $argumentList -PassThru
    if (-not $process.WaitForExit($TimeoutSec * 1000)) {
        try {
            Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
        }
        catch {
        }

        throw "Installer execution exceeded the timeout of $TimeoutSec seconds. Check $logPath."
    }

    return [pscustomobject]@{
        ExitCode        = $process.ExitCode
        LogPath         = $logPath
        RestartRequired = ($process.ExitCode -eq 3010)
    }
}

function Invoke-ManifestedMachineInstallerFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$Facts,

        [hashtable]$CommandOptions = @{},

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $installerInfo = if ($Facts.PSObject.Properties['Artifact'] -and $Facts.Artifact) { $Facts.Artifact } elseif ($Facts.PSObject.Properties['Installer'] -and $Facts.Installer) { $Facts.Installer } else { $null }
    if (-not $installerInfo) {
        throw "The installer for '$($Definition.commandName)' was not available."
    }
    $installerInfo = Get-ManifestedMachineInstallerInfoFromDefinition -Definition $Definition -Artifact $installerInfo -LocalRoot $LocalRoot
    if ([string]::IsNullOrWhiteSpace($installerInfo.Path) -or -not (Test-Path -LiteralPath $installerInfo.Path)) {
        throw "The installer for '$($Definition.commandName)' was not available on disk."
    }

    $timeoutSec = 300
    if ($CommandOptions.ContainsKey('InstallTimeoutSec') -and $CommandOptions['InstallTimeoutSec']) {
        $timeoutSec = [int]$CommandOptions['InstallTimeoutSec']
    }

    $installed = Get-ManifestedInstalledMachinePrerequisiteRuntime -Definition $Definition
    if ($installed.Installed) {
        if (-not $installerInfo.VersionObject -or ($installed.VersionObject -and $installed.VersionObject -ge $installerInfo.VersionObject)) {
            return [pscustomobject]@{
                Action           = 'Skipped'
                Installed        = $true
                Architecture     = $installed.Architecture
                Version          = $installed.Version
                VersionObject    = $installed.VersionObject
                InstallerVersion = $installerInfo.Version
                InstallerPath    = $installerInfo.Path
                InstallerSource  = $installerInfo.Source
                ExitCode         = 0
                RestartRequired  = $false
                LogPath          = $null
            }
        }
    }

    Write-Host ('Installing machine prerequisites for ' + $Definition.runtimeName + '...')
    $installResult = Invoke-ManifestedMachineInstallerProcess -InstallerPath $installerInfo.Path -TimeoutSec $timeoutSec
    $refreshed = Get-ManifestedInstalledMachinePrerequisiteRuntime -Definition $Definition

    if (-not $refreshed.Installed) {
        throw "$($Definition.runtimeName) installation exited with code $($installResult.ExitCode), but the prerequisite was not detected afterwards. Check $($installResult.LogPath)."
    }

    if ($installerInfo.VersionObject -and $refreshed.VersionObject -and $refreshed.VersionObject -lt $installerInfo.VersionObject) {
        throw "$($Definition.runtimeName) installation completed, but version $($refreshed.Version) is still older than installer version $($installerInfo.Version). Check $($installResult.LogPath)."
    }

    if ($installResult.ExitCode -notin @(0, 3010, 1638)) {
        throw "$($Definition.runtimeName) installation failed with exit code $($installResult.ExitCode). Check $($installResult.LogPath)."
    }

    return [pscustomobject]@{
        Action           = 'Installed'
        Installed        = $true
        Architecture     = $refreshed.Architecture
        Version          = $refreshed.Version
        VersionObject    = $refreshed.VersionObject
        InstallerVersion = $installerInfo.Version
        InstallerPath    = $installerInfo.Path
        InstallerSource  = $installerInfo.Source
        ExitCode         = $installResult.ExitCode
        RestartRequired  = $installResult.RestartRequired
        LogPath          = $installResult.LogPath
    }
}

function Install-ManifestedRuntime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [pscustomobject]$Facts,

        [bool]$RefreshRequested = $false,

        [hashtable]$CommandOptions = @{},

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if (Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'portableArchive') {
        return (Invoke-ManifestedPortableArchiveInstallFromDefinition -Definition $Definition -Facts $Facts -LocalRoot $LocalRoot)
    }
    if (Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'pythonEmbeddableZip') {
        return (Invoke-ManifestedPythonEmbeddableInstallFromDefinition -Definition $Definition -Facts $Facts -RefreshRequested:$RefreshRequested -LocalRoot $LocalRoot)
    }
    if (Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'machineInstaller') {
        return (Invoke-ManifestedMachineInstallerFromDefinition -Definition $Definition -Facts $Facts -CommandOptions $CommandOptions -LocalRoot $LocalRoot)
    }
    if (Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'npmGlobalPackage') {
        return (Invoke-ManifestedNpmGlobalPackageInstallFromDefinition -Definition $Definition -Facts $Facts -LocalRoot $LocalRoot)
    }

    throw "No install function is defined for '$($Definition.commandName)'."
}