private/Software/Install-7Zip.ps1

#Requires -PSEdition Core

<#
.SYNOPSIS
Downloads, installs, and caches 7-Zip on the host and prepares the WinPE cache.
 
.DESCRIPTION
Ensures 7-Zip is available on the host by:
 
  1. Downloading the 7-Zip installer for both amd64 and arm64 architectures to
     C:\ProgramData\OSDeployCore\Software\7zip.7zip\ using winget download.
  2. Installing 7-Zip on the host via winget when it is not already present
     (skipped when -DownloadOnly is specified).
  3. Pre-populating the WinPE apps cache with 7zr.exe and the extracted 7z-extra
     archive so that subsequent boot image builds find the files already cached.
     This mirrors the logic in Step-BootImageAppZip.
 
.PARAMETER DownloadOnly
Downloads the installer and WinPE cache files without installing 7-Zip on the host.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
 
.EXAMPLE
Install-7Zip
 
Installs 7-Zip if not present and populates the WinPE cache.
 
.EXAMPLE
Install-7Zip -DownloadOnly
 
Downloads the 7-Zip installers and WinPE cache files without installing.
 
.NOTES
Author: David Segura
Company: Recast Software
This function is supported only on Windows and requires winget.
#>

function Install-7Zip {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject])]
    param (
        [Parameter()]
        [switch] $DownloadOnly
    )

    if (-not $IsWindows) {
        throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Install-7Zip 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)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] -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)] [$($MyInvocation.MyCommand.Name)] 7-Zip is already installed: $version" -ForegroundColor Green
        }
        else {
            if ($PSCmdlet.ShouldProcess($packageId, 'Install 7-Zip using winget')) {
                Write-Host "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] 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-BootImageAppZip 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)] [$($MyInvocation.MyCommand.Name)] Using 7-Zip WinPE cache: $CacheZip" -ForegroundColor Green
        }
        else {
            Write-Host "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] Downloading 7zr.exe from $downloadUrl" -ForegroundColor DarkGray
            if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
                & curl.exe --silent --location --output $sevenZrPath $downloadUrl
            }
            else {
                Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $sevenZrPath
            }
        }
        else {
            Write-Host "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] 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)] [$($MyInvocation.MyCommand.Name)] Downloading 7-Zip extra archive: $extraFileName" -ForegroundColor DarkGray
            if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
                & curl.exe --silent --location --output $downloadPath $downloadUrl
            }
            else {
                Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $downloadPath
            }

            Write-Host "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Extracting $extraFileName to $extraDir" -ForegroundColor DarkGray
            $null = & $sevenZrPath x $downloadPath -o"$extraDir" -y
        }
        else {
            Write-Host "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] 7-Zip extra archive already extracted: $extraDir" -ForegroundColor Green
        }
    }
    else {
        Write-Verbose "[$(Get-Date -format s)] [$($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
    }
}