private/Install-Software7Zip.ps1

#Requires -PSEdition Core
function Install-Software7Zip {
    <#
    .SYNOPSIS
        Downloads and installs 7-Zip and prepares its WinPE cache
 
    .DESCRIPTION
        Downloads amd64 and arm64 7-Zip installers with winget to the OSDeployCore software
        cache. When 7-Zip is not installed, the function can install it on the host with
        winget. DownloadOnly skips the host installation.
 
        The function also removes obsolete 7-Zip WinPE cache versions and populates the
        current cache with 7zr.exe and the extracted 7z-extra archive when module metadata is
        available. WhatIf and Confirm apply only to the host installation; downloads, cache
        directory changes, and WinPE cache preparation are not gated by ShouldProcess.
 
    .PARAMETER DownloadOnly
        Downloads the installers and prepares the WinPE cache without installing 7-Zip on the
        host.
 
    .EXAMPLE
        PS> Install-Software7Zip
 
        Downloads the installers, installs 7-Zip when needed, and prepares the WinPE cache.
 
    .EXAMPLE
        PS> Install-Software7Zip -DownloadOnly
 
        Downloads and caches 7-Zip content without installing the host application.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.String. Returns native winget output written to the success stream.
 
        System.Management.Automation.PSCustomObject. Returns installation and cache status.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Requires Windows and winget. WinPE cache downloads require curl.exe or
        Invoke-WebRequest. The function reads 7-Zip package and WinPE app settings from
        $global:OSDeployModule when available.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject])]
    param (
        [Parameter()]
        [switch] $DownloadOnly
    )

    if (-not $IsWindows) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Install-Software7Zip is supported only on Windows."
    }

    $winget = Get-Command -Name 'winget' -ErrorAction SilentlyContinue
    if (-not $winget) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] winget is required but was not found. Install App Installer from Microsoft Store and try again."
    }

    $packageId = '7zip.7zip'
    if ($global:OSDeployModule -and $global:OSDeployModule.Software.'7zip'.id) {
        $packageId = [string]$global:OSDeployModule.Software.'7zip'.id
    }

    #region Download installer for both architectures
    $softwarePath = Join-Path $script:OSDeployCoreSoftwarePath $packageId
    $amd64Path    = Join-Path $softwarePath 'amd64'
    $arm64Path    = Join-Path $softwarePath 'arm64'

    foreach ($archEntry in @(
        @{ Path = $amd64Path; Arch = 'x64';   Label = 'amd64' }
        @{ Path = $arm64Path; Arch = 'arm64'; Label = 'arm64' }
    )) {
        $archPath  = $archEntry.Path
        $archArg   = $archEntry.Arch
        $archLabel = $archEntry.Label

        $alreadyCached = (Test-Path -Path $archPath) -and
            (Get-ChildItem -Path $archPath -File -ErrorAction SilentlyContinue | Select-Object -First 1)

        if ($alreadyCached) {
            Write-Host "[$(Get-Date -Format s)] 7-Zip installer already cached ($archLabel): $archPath" -ForegroundColor Green
        }
        else {
            if (-not (Test-Path -Path $archPath)) {
                New-Item -Path $archPath -ItemType Directory -Force | Out-Null
            }

            Write-Host "[$(Get-Date -Format s)] Downloading 7-Zip installer ($archLabel) to $archPath..." -ForegroundColor DarkGray
            & $winget.Source download --id $packageId --exact --download-directory $archPath --architecture $archArg --accept-source-agreements --accept-package-agreements
            if ($LASTEXITCODE -ne 0) {
                Write-Warning "[$(Get-Date -Format s)] winget download exited with code $LASTEXITCODE for architecture $archLabel."
            }
        }
    }
    #endregion

    #region Install 7-Zip on host (skip when -DownloadOnly)
    $wasInstalled = $false
    $skippedInstall = $false

    if ($DownloadOnly) {
        Write-Host "[$(Get-Date -Format s)] -DownloadOnly specified — skipping host installation." -ForegroundColor DarkGray
        $skippedInstall = $true
    }
    else {
        $sevenZipExe = Join-Path $env:ProgramFiles '7-Zip\7z.exe'

        if (Test-Path -Path $sevenZipExe) {
            $version = (& $sevenZipExe i 2>&1 | Select-String -Pattern '^\d' | Select-Object -First 1).ToString().Trim()
            Write-Host "[$(Get-Date -Format s)] 7-Zip is already installed: $version" -ForegroundColor Green
        }
        else {
            if ($PSCmdlet.ShouldProcess($packageId, 'Install 7-Zip using winget')) {
                Write-Host "[$(Get-Date -Format s)] 7-Zip is not installed. Installing with winget..." -ForegroundColor DarkGray
                & $winget.Source install --id $packageId --exact -e -h --accept-source-agreements --accept-package-agreements
                if ($LASTEXITCODE -ne 0) {
                    throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] 7-Zip installation failed with exit code $LASTEXITCODE."
                }
                $wasInstalled = $true

                if (-not (Test-Path -Path $sevenZipExe)) {
                    Write-Warning "[$(Get-Date -Format s)] 7-Zip was installed but 7z.exe was not found at '$sevenZipExe'."
                }
            }
        }
    }
    #endregion

    #region Populate WinPE apps cache (7zr.exe + 7z-extra archive)
    # Mirrors Step-BuildBootInstallWinPEAppZip so the boot image build finds the files already cached.
    if ($global:OSDeployModule -and $global:OSDeployModule.BootImage.winpeapps.sevenzip) {
        $zipConfig  = $global:OSDeployModule.BootImage.winpeapps.sevenzip
        $zipVersion = $zipConfig.version

        $CacheZipRoot = Join-Path $Script:OSDeployCorePath 'cache' 'winpe-apps' '7zip'
        $CacheZip     = Join-Path $CacheZipRoot $zipVersion

        # Cleanup old version directories
        if (Test-Path -Path $CacheZipRoot) {
            Get-ChildItem -Path $CacheZipRoot -File -ErrorAction SilentlyContinue | Remove-Item -Force
            Get-ChildItem -Path $CacheZipRoot -Directory -ErrorAction SilentlyContinue |
                Where-Object { $_.Name -ne $zipVersion } |
                Remove-Item -Recurse -Force
        }

        if (Test-Path -Path $CacheZip) {
            Write-Host "[$(Get-Date -Format s)] Using 7-Zip WinPE cache: $CacheZip" -ForegroundColor Green
        }
        else {
            Write-Host "[$(Get-Date -Format s)] Creating 7-Zip WinPE cache: $CacheZip" -ForegroundColor DarkGray
            New-Item -Path $CacheZip -ItemType Directory -Force | Out-Null
        }

        # Download 7zr.exe (standalone, used to extract the extra archive)
        $sevenZrPath = Join-Path $CacheZip '7zr.exe'
        if (-not (Test-Path -Path $sevenZrPath)) {
            $downloadUrl = [string]$zipConfig.standalone
            Write-Host "[$(Get-Date -Format s)] Downloading 7zr.exe from $downloadUrl" -ForegroundColor DarkGray
            if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
                & curl.exe --silent --location --retry 5 --continue-at - --output $sevenZrPath $downloadUrl
            }
            else {
                Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $sevenZrPath
            }
        }
        else {
            Write-Host "[$(Get-Date -Format s)] 7zr.exe already cached: $sevenZrPath" -ForegroundColor Green
        }

        # Download and extract the 7z-extra archive
        $extraDir = Join-Path $CacheZip '7za'
        if (-not (Test-Path -Path $extraDir)) {
            $downloadUrl    = [string]$zipConfig.extra
            $extraFileName  = Split-Path $downloadUrl -Leaf
            $downloadPath   = Join-Path $CacheZip $extraFileName

            Write-Host "[$(Get-Date -Format s)] Downloading 7-Zip extra archive: $extraFileName" -ForegroundColor DarkGray
            if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
                & curl.exe --silent --location --retry 5 --continue-at - --output $downloadPath $downloadUrl
            }
            else {
                Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $downloadPath
            }

            Write-Host "[$(Get-Date -Format s)] Extracting $extraFileName to $extraDir" -ForegroundColor DarkGray
            $null = & $sevenZrPath x $downloadPath -o"$extraDir" -y
        }
        else {
            Write-Host "[$(Get-Date -Format s)] 7-Zip extra archive already extracted: $extraDir" -ForegroundColor Green
        }
    }
    else {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] OSDeployModule or BootImage.winpeapps.sevenzip not available — skipping WinPE cache preparation."
    }
    #endregion

    [pscustomobject]@{
        Component    = '7-Zip'
        PackageId    = $packageId
        WasInstalled = $wasInstalled
        SkippedInstall = $skippedInstall
        DownloadOnly = $DownloadOnly.IsPresent
        SoftwarePath = $softwarePath
    }
}