Public/Sync-OSDStageMicrosoftCompanyPortal.ps1
|
function Sync-OSDStageMicrosoftCompanyPortal { [CmdletBinding(SupportsShouldProcess)] param( [string]$KitPath, [ValidateSet('x64','arm64')][string]$Architecture = 'x64' ) if (Test-OSDStageWinPE) { throw 'Prepare the Company Portal cache on full Windows, not in WinPE.' } $cachePath = Get-OSDStageCachePath $target = Join-Path $cachePath ('BuiltIn\MicrosoftCompanyPortal\' + $Architecture) if (-not $PSCmdlet.ShouldProcess($target, 'Download or import the official Microsoft Preinstall Kit')) { return } $automatic = [string]::IsNullOrWhiteSpace($KitPath) $downloadUri = 'https://download.microsoft.com/download/ac93b367-7b17-4838-a079-c6f3377bf582/CompanyPortal-Universal-Production_x64_x86_ARM_ARM64.appxupload_Windows10_PreinstallKit.zip' $kit = $null $kitDirectory = $null Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop $lock = Enter-OSDStageCacheLock -CachePath $cachePath -Operation 'Microsoft Company Portal cache synchronization' $candidate = $target + '.osdstage-new-' + [guid]::NewGuid().ToString('N') $backup = $target + '.osdstage-old-' + [guid]::NewGuid().ToString('N') try { if ($automatic) { $kitDirectory = Join-Path $cachePath ('.osdstage-kit-' + [guid]::NewGuid().ToString('N')) $download = Save-OSDStageDownload -Uri $downloadUri -DestinationDirectory $kitDirectory -ExpectedExtension '.zip' -Activity 'Downloading Microsoft Company Portal Preinstall Kit' $kit = [string]$download.FullName } else { $kit = (Resolve-Path -LiteralPath $KitPath -ErrorAction Stop).Path } if ([IO.Path]::GetExtension($kit) -ne '.zip') { throw 'Expected the official Microsoft Preinstall Kit ZIP.' } $zip = [IO.Compression.ZipFile]::OpenRead($kit) try { # Only select vendor files at the ZIP root. Reject ambiguous versions. $entries = @($zip.Entries | Where-Object { $_.Name -eq $_.FullName -and $_.Length -gt 0 }) $bundles = @($entries | Where-Object { $_.Name -match '^[a-zA-Z0-9_-]+\.appxbundle$' }) $licenses = @($entries | Where-Object { $_.Name -match '^[a-zA-Z0-9_-]+_License[0-9]+\.xml$' }) if ($bundles.Count -ne 1 -or $licenses.Count -ne 1) { throw 'The Preinstall Kit must contain exactly one root appxbundle and one offline license.' } $families = @( 'Microsoft.NET.Native.Framework.2.2_', 'Microsoft.NET.Native.Runtime.2.2_', 'Microsoft.Services.Store.Engagement_', 'Microsoft.UI.Xaml.2.7_', 'Microsoft.VCLibs.140.00_' ) $dependencies = @() foreach ($family in $families) { $matches = @($entries | Where-Object { $_.Name -like ($family + '*_' + $Architecture + '__*.appx') }) if ($matches.Count -ne 1) { throw ('Expected one {0} dependency for {1}; found {2}.' -f $family,$Architecture,$matches.Count) } $dependencies += $matches[0] } New-Item -ItemType Directory -Path $candidate -Force -ErrorAction Stop | Out-Null New-Item -ItemType Directory -Path (Join-Path $candidate 'Dependencies') -Force -ErrorAction Stop | Out-Null $selected = @( @{ Entry=$bundles[0]; Relative=[string]$bundles[0].Name }, @{ Entry=$licenses[0]; Relative=[string]$licenses[0].Name } ) foreach ($dep in $dependencies) { $selected += @{ Entry=$dep; Relative=('Dependencies\' + $dep.Name) } } $files = @() foreach ($entry in $selected) { $destination = Join-Path $candidate $entry.Relative [IO.Compression.ZipFileExtensions]::ExtractToFile($entry.Entry, $destination, $true) $files += [pscustomobject]@{ Path = [string]$entry.Relative Sha256 = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash } } } finally { $zip.Dispose() } # Read app identity and version from the actual vendor-signed bundle metadata. $bundle = [IO.Compression.ZipFile]::OpenRead((Join-Path $candidate ([string]$bundles[0].Name))) try { $manifestEntry = $bundle.GetEntry('AppxMetadata/AppxBundleManifest.xml') if (-not $manifestEntry) { throw 'AppxBundleManifest.xml was not found.' } $reader = [IO.StreamReader]::new($manifestEntry.Open()) try { [xml]$bundleXml = $reader.ReadToEnd() } finally { $reader.Dispose() } $identity = $bundleXml.SelectSingleNode("//*[local-name()='Identity']") if (-not $identity -or $identity.GetAttribute('Name') -ne 'Microsoft.CompanyPortal' -or $identity.GetAttribute('Publisher') -notmatch 'Microsoft Corporation') { throw 'Unexpected Microsoft Company Portal bundle identity or publisher.' } $version = $identity.GetAttribute('Version') if ($version -notmatch '^\d+\.\d+\.\d+\.\d+$') { throw 'Invalid Company Portal version.' } } finally { $bundle.Dispose() } [ordered]@{ Id='MicrosoftCompanyPortal'; Version=$version; Architecture=$Architecture PayloadFileName=[string]$bundles[0].Name; LicenseFileName=[string]$licenses[0].Name Sha256=[string]$files[0].Sha256; Size=(Get-Item -LiteralPath (Join-Path $candidate ([string]$bundles[0].Name))).Length Source=$(if($automatic){'Microsoft Download Center'}else{'Microsoft Preinstall Kit (operator supplied)'}) SourceUri=$(if($automatic){$downloadUri}else{$null}) SourceKitSha256=(Get-FileHash -LiteralPath $kit -Algorithm SHA256).Hash Files=@($files); DependencyCount=$dependencies.Count SyncedAt=(Get-Date).ToUniversalTime().ToString('o') } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $candidate 'CacheInfo.json') -Encoding UTF8 -ErrorAction Stop $candidateInfo = Get-Content -LiteralPath (Join-Path $candidate 'CacheInfo.json') -Raw -Encoding UTF8 | ConvertFrom-Json New-OSDStageCompanyPortalManifestEntry -CacheInfo $candidateInfo -Architecture $Architecture | Out-Null if (Test-Path -LiteralPath $target) { Move-Item -LiteralPath $target -Destination $backup -ErrorAction Stop } try { Move-Item -LiteralPath $candidate -Destination $target -ErrorAction Stop } catch { if (Test-Path -LiteralPath $backup) { Move-Item -LiteralPath $backup -Destination $target -ErrorAction SilentlyContinue } throw } if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction Stop } [pscustomobject]@{ Id='MicrosoftCompanyPortal'; Version=$version; Architecture=$Architecture Cached=$true; Updated=$true; CacheReused=$false; CachePath=$target; Files=$files.Count } } catch { $originalError = $_ if ($automatic -and (Test-Path -LiteralPath (Join-Path $target 'CacheInfo.json') -PathType Leaf)) { try { $previous = Get-Content (Join-Path $target 'CacheInfo.json') -Raw | ConvertFrom-Json $entry = New-OSDStageCompanyPortalManifestEntry -CacheInfo $previous -Architecture $Architecture Assert-OSDStageCompanyPortalFiles -App $entry -StagedPath $cachePath $healthy = $true if ($healthy) { Write-Warning ('Company Portal download failed; verified cache reused: {0}' -f $originalError.Exception.Message) return [pscustomobject]@{Id='MicrosoftCompanyPortal';Version=$previous.Version;Architecture=$Architecture;Cached=$true;Updated=$false;CacheReused=$true;CachePath=$target;Files=7} } } catch { } } throw $originalError } finally { if ($kitDirectory -and (Test-Path -LiteralPath $kitDirectory)) { Remove-Item -LiteralPath $kitDirectory -Recurse -Force -ErrorAction SilentlyContinue } if (Test-Path -LiteralPath $candidate) { Remove-Item -LiteralPath $candidate -Recurse -Force -ErrorAction SilentlyContinue } if ($lock) { $lock.Dispose() } } } |