Public/Sync-OSDAppMicrosoft365Apps.ps1

function Sync-OSDAppMicrosoft365Apps {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [ValidateSet('Current','MonthlyEnterprise','SemiAnnual','CurrentPreview','SemiAnnualPreview','BetaChannel')]
        [string]$Channel = 'Current',

        [ValidateSet('x64','x86')]
        [string]$Architecture = 'x64',

        [ValidateSet('O365ProPlusRetail','O365BusinessRetail')]
        [string]$ProductId = 'O365ProPlusRetail',

        [string[]]$Language = @('en-us'),
        [bool]$AcceptEula = $true,
        [bool]$SharedComputerLicensing = $false,
        [bool]$DeviceBasedLicensing = $false,
        [bool]$UpdatesEnabled = $true,
        [switch]$IncludeVisio,
        [switch]$IncludeProject,

        [ValidateSet('Access','Excel','Groove','Lync','OneDrive','OneNote','Outlook','OutlookForWindows','PowerPoint','Publisher','Teams','Word')]
        [string[]]$ExcludeApp,

        [string]$ConfigurationXml,
        [string]$OfficeDeploymentToolUri = 'https://officecdn.microsoft.com/pr/wsus/setup.exe',
        [double]$MinimumFreeSpaceGB = 8
    )

    if (Test-OSDAppWinPE) {
        throw 'Sync-OSDAppMicrosoft365Apps is intended for full Windows. Pre-cache Microsoft 365 Apps from full Windows before deployment.'
    }

    if ($SharedComputerLicensing -and $DeviceBasedLicensing) {
        throw 'SharedComputerLicensing and DeviceBasedLicensing cannot both be enabled.'
    }

    if (-not $ConfigurationXml -and (-not $Language -or @($Language).Count -eq 0)) {
        throw 'At least one Office language must be specified.'
    }

    $cachePath = Get-OSDAppCachePath
    $clientLogPath = Join-Path $cachePath 'Logs\Client.log'
    $root = Join-Path $cachePath 'BuiltIn\Microsoft365Apps'
    $setupPath = Join-Path $root 'setup.exe'
    $configPath = Join-Path $root 'configuration.xml'
    $cacheInfoPath = Join-Path $root 'CacheInfo.json'

    New-Item -ItemType Directory -Path $root -Force | Out-Null

    if (-not $PSCmdlet.ShouldProcess($root, 'Configure and synchronize Microsoft 365 Apps built-in cache')) { return }

    Assert-OSDAppCacheFreeSpace -CachePath $cachePath -MinimumFreeSpaceGB $MinimumFreeSpaceGB -Operation 'Microsoft 365 Apps cache synchronization' -LogPath $clientLogPath | Out-Null
    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'BuiltInSyncStart' -Message 'Synchronizing Microsoft 365 Apps built-in cache.'

    Save-OSDAppDownload -Uri $OfficeDeploymentToolUri -DestinationPath $setupPath -Activity 'Downloading Office Deployment Tool' | Out-Null

    if ($ConfigurationXml) {
        Test-OSDAppOfficeConfigurationXml -Path $ConfigurationXml | Out-Null
        Copy-Item -LiteralPath $ConfigurationXml -Destination $configPath -Force
    }
    else {
        New-OSDAppOfficeConfiguration -Path $configPath `
            -Channel $Channel -Architecture $Architecture -ProductId $ProductId `
            -Language $Language -AcceptEula $AcceptEula -UpdatesEnabled $UpdatesEnabled `
            -SharedComputerLicensing $SharedComputerLicensing -DeviceBasedLicensing $DeviceBasedLicensing `
            -ExcludeApp $ExcludeApp -IncludeVisio:$IncludeVisio -IncludeProject:$IncludeProject
    }

    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'OfficeCacheDownloadStart' -Message 'Running Office Deployment Tool in download mode.' -Data @{ Configuration = $configPath }

    $process = Start-Process -FilePath $setupPath -ArgumentList @('/download', $configPath) -WorkingDirectory $root -PassThru
    try {
        while (-not $process.HasExited) {
            $downloadedBytes = 0L
            $officeRoot = Join-Path $root 'Office'
            if (Test-Path -LiteralPath $officeRoot -PathType Container) {
                $downloadedBytes = (Get-ChildItem -LiteralPath $officeRoot -File -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
            }
            $downloadedGB = [math]::Round(([double]$downloadedBytes / 1GB), 2)
            Write-Progress -Activity 'Downloading Microsoft 365 Apps content' -Status "$downloadedGB GB cached" -PercentComplete -1
            Start-Sleep -Seconds 2
            $process.Refresh()
        }
    }
    finally { Write-Progress -Activity 'Downloading Microsoft 365 Apps content' -Completed }

    if ($process.ExitCode -ne 0) { throw "Office Deployment Tool download failed with exit code $($process.ExitCode)." }

    $officeData = Join-Path $root 'Office\Data'
    if (-not (Test-Path -LiteralPath $officeData -PathType Container)) { throw "Office Deployment Tool completed but Office\Data was not found: $officeData" }

    $versionFolders = @(Get-ChildItem -LiteralPath $officeData -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | ForEach-Object { try { [pscustomobject]@{ Name = $_.Name; Version = [version]$_.Name } } catch { } } | Sort-Object Version -Descending)
    $resolvedVersion = if ($versionFolders.Count -gt 0) { $versionFolders[0].Name } else { 'Unknown' }

    [ordered]@{
        Id = 'Microsoft365Apps'
        Cached = $true
        Version = $resolvedVersion
        Architecture = $Architecture
        SourcePolicy = 'Evergreen'
        SyncMethod = 'OfficeDeploymentTool'
        Updated = $true
        Channel = $Channel
        ProductId = $ProductId
        Language = @($Language)
        AcceptEula = $AcceptEula
        SharedComputerLicensing = $SharedComputerLicensing
        DeviceBasedLicensing = $DeviceBasedLicensing
        UpdatesEnabled = $UpdatesEnabled
        IncludeVisio = [bool]$IncludeVisio
        IncludeProject = [bool]$IncludeProject
        ExcludeApp = @($ExcludeApp)
        ConfigurationSource = if ($ConfigurationXml) { 'CustomXml' } else { 'Generated' }
        SyncedAt = (Get-Date).ToUniversalTime().ToString('o')
    } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8

    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'BuiltInSyncComplete' -Message 'Microsoft 365 Apps built-in cache synchronized.' -Data @{ Version = $resolvedVersion; Path = $root; Channel = $Channel; Architecture = $Architecture; SourcePolicy='Evergreen'; SyncMethod='OfficeDeploymentTool' }

    [pscustomobject]@{ PSTypeName='OSDApps.BuiltInCache'; Id='Microsoft365Apps'; Version=$resolvedVersion; Architecture=$Architecture; Channel=$Channel; CachePath=$root; Cached=$true }
}