public/Build-OSDeployBoot.ps1
|
#Requires -PSEdition Core #Requires -Version 7.4 function Build-OSDeployBoot { <# .SYNOPSIS Builds customized WinPE boot media from a WinRE or Windows ADK source .DESCRIPTION Creates boot media under %ProgramData%\OSDeployCore\boot from an imported WinRE image or the Windows ADK winpe.wim. In the Default parameter set, the command selects an imported WinRE source interactively unless Auto is specified, and falls back to ADK WinPE when no WinRE source is selected or available. The ADK parameter set requires an explicit architecture and UseAdkWinPE. ProfileName tab-completes saved architecture-specific profiles from flat <Name>-<Architecture> profile directories. It requires an exact match and supplies the architecture and content paths without displaying shared-content or wallpaper selectors. Recovery-image selection remains available and is filtered to the profile architecture. Languages, international settings, timezone, and options passed on the command line override saved values for that build only. The selected profile JSON and profile-local files are not changed. When ProfileName is omitted, shared-content selections are written to recent-amd64.json or recent-arm64.json under Boot-Assets. The matching architecture snapshot is overwritten after configuration, before build confirmation. Selected wallpaper and other profile-local companion content remain temporary and are discarded after the build. Use New-OSDeployBootProfilePreview and Update-OSDeployBootProfilePreview to manage named profiles. The build validates selected and profile-local WinPEStartup profile JSON before showing the build configuration. It then services boot.wim, creates boot media and ISO content, writes build metadata and logs, and can copy completed media to USB partitions labeled USB-WinPE. Before creating the build directories, it displays the configuration and waits five seconds; WhatIf and Confirm gate the build-directory operation and stop the build when declined. .PARAMETER ProfileName Specifies the exact canonical name of an existing profile, such as Contoso-amd64. Values tab-complete from saved profiles. Saved content is loaded without content selectors, command-line configuration values apply only to the current build, and the source profile is not modified. .PARAMETER Architecture Specifies amd64 or arm64. In the Default parameter set, this value limits WinRE source selection; when omitted, a selected WinRE source determines the architecture. Auto derives an omitted value from PROCESSOR_ARCHITECTURE. A matching saved profile supplies this value. The value must otherwise be specified when UseAdkWinPE is used. .PARAMETER Languages Specifies zero or more validated Windows ADK language identifiers to add. The en-us base packages are processed independently. Specify * to enumerate all additional language directories. When ProfileName is specified, an explicitly passed value applies only to the current build. .PARAMETER SetAllIntl Specifies the value used by the WinPE international-settings build step. A saved profile supplies this value when omitted. When ProfileName is specified, an explicitly passed value applies only to the current build. .PARAMETER SetInputLocale Specifies the WinPE input locale used by the servicing steps. A saved profile supplies this value when omitted. When ProfileName is specified, an explicitly passed value applies only to the current build. .PARAMETER SetTimeZone Specifies a timezone validated against tzutil /l. The default is the current system timezone returned by tzutil /g when no profile supplies it. When ProfileName is specified, an explicitly passed value applies only to the current build. .PARAMETER SkipAdkPackages Skips installation of the configured Windows ADK optional-component and language packages. Other image customization steps still run. .PARAMETER UseAdkWinPE Uses the Windows ADK winpe.wim instead of an imported WinRE source. This switch is mandatory in the ADK parameter set and cannot be combined with Auto. Architecture must be specified unless it is supplied by a matching saved profile. .PARAMETER UpdateUSB Runs the final USB update build step, which targets USB partitions labeled USB-WinPE. .PARAMETER Auto Selects the newest imported WinRE source for the resolved architecture without showing the WinRE picker. When none is available, uses ADK WinPE. ProfileName still loads saved content without selectors; temporary builds show shared-content and wallpaper selectors. .PARAMETER Options Specifies optional WinPE features to include. Valid values are pwsh and dart, and multiple values are allowed. Selecting pwsh installs PowerShell 7, and selecting dart installs available Microsoft DaRT content in the mounted image. When ProfileName is specified, an explicitly passed value applies only to the current build. .EXAMPLE PS> Build-OSDeployBoot -ProfileName 'MyPE-amd64' -Options pwsh Loads MyPE-amd64 without content selectors, uses pwsh for this build only, and prompts for a matching WinRE source without changing the profile JSON. .EXAMPLE PS> Build-OSDeployBoot Prompts for a WinRE source and build content, writes the matching recent architecture profile, builds media named OSDeploy, then removes temporary companion content. .INPUTS None. This function does not accept pipeline input. .OUTPUTS None. This function does not intentionally return the build context. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires Windows 11 25H2 or later, PowerShell 7.4 or later installed from MSI, Windows ADK with the WinPE add-on, curl.exe, OSDCloud 26.7.25.2 or later, and Administrator rights. The function populates $global:BuildMedia for build steps and later inspection. That global value is process state, not pipeline output. It also changes process-wide web, TLS, and progress preferences during the build and restores the TLS and progress values only at normal completion. Recent architecture profile JSON remains after normal completion, cancellation, a declined build operation, or a terminating error after configuration. Temporary companion content is removed. Generated build output is not removed with the companion content. .LINK New-OSDeployBootProfilePreview .LINK Update-OSDeployBootProfilePreview .LINK https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install #> [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)] param ( [Parameter(Mandatory, ParameterSetName = 'Profile')] [Parameter(Mandatory, ParameterSetName = 'ADKProfile')] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete) $profilePath = Join-Path $env:ProgramData 'OSDeployCore\boot-assets\osdeployboot-profiles' Get-ChildItem -LiteralPath $profilePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "$wordToComplete*" -and (Test-Path -LiteralPath (Join-Path $_.FullName 'osdeployboot.json') -PathType Leaf) } | Sort-Object Name | ForEach-Object { $completionText = if ($_.Name -match '\s') { "'$($_.Name.Replace("'", "''"))'" } else { $_.Name } [System.Management.Automation.CompletionResult]::new( $completionText, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.FullName ) } })] [System.String] $ProfileName, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ADK')] [Parameter(ParameterSetName = 'Profile')] [Parameter(ParameterSetName = 'ADKProfile')] [ValidateSet('amd64', 'arm64')] [System.String] $Architecture, [ValidateSet( '*', 'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr', 'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', 'fi-fi', 'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it', 'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', 'nb-no', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sl-si', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr', 'uk-ua', 'zh-cn', 'zh-tw' )] [System.String[]] $Languages, [System.String] $SetAllIntl, [System.String] $SetInputLocale, [ValidateScript({ $tz = (tzutil /l) $validOptions = foreach ($t in $tz) { if (($tz.IndexOf($t) - 1) % 3 -eq 0) { $t.Trim() } } $validOptions -contains $_ })] [System.String] $SetTimeZone = (tzutil /g), [System.Management.Automation.SwitchParameter] $SkipAdkPackages, [Parameter(Mandatory, ParameterSetName = 'ADK')] [Parameter(Mandatory, ParameterSetName = 'ADKProfile')] [System.Management.Automation.SwitchParameter] $UseAdkWinPE, [System.Management.Automation.SwitchParameter] $UpdateUSB, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Profile')] [System.Management.Automation.SwitchParameter] $Auto, [ValidateSet('pwsh', 'dart')] [System.String[]] $Options ) #================================================= $BuildDateTime = (Get-Date).ToString('yyMMdd-HHmm') Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Starting $($MyInvocation.MyCommand.Name) at $BuildDateTime" #================================================= # Stop before preparing build content when a required host capability is missing. if (-not (Test-IsWindows11)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 is required." } if (-not (Test-IsWindows1125H2)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 25H2 (build 26200) is required." } if (-not (Test-PwshVersionMin)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] PowerShell 7.4 or higher is required." } if (-not (Test-PwshPSHome)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] The MSI installation of PowerShell 7 is required." } if (-not (Test-CommandCurl)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] curl.exe is required but was not found in the current PATH. curl.exe ships with Windows 10 1803+." } if (-not (Test-IsAdministrator)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Administrator rights are required. Re-run PowerShell as Administrator and try again." } #================================================= # Stop when the required OSDCloud version is unavailable. #region OSDCloud Requirements $RequiredOSDCloudVersion = [System.Version]'26.7.25.2' if (-not (Get-Command -Name 'Get-OSDCloudModuleVersion' -ErrorAction SilentlyContinue)) { Write-Warning "[$(Get-Date -Format s)] OSDCloud module $RequiredOSDCloudVersion or newer is required." Write-Warning "[$(Get-Date -Format s)] Install-Module -Name OSDCloud -Force -SkipPublisherCheck" return } $OSDCloudVersion = Get-OSDCloudModuleVersion if ($OSDCloudVersion -lt $RequiredOSDCloudVersion) { Write-Warning "[$(Get-Date -Format s)] OSDCloud module $RequiredOSDCloudVersion or newer is required. Loaded version: $OSDCloudVersion" Write-Warning "[$(Get-Date -Format s)] Install-Module -Name OSDCloud -Force -SkipPublisherCheck" return } #endregion #================================================= Initialize-OSDCoreLicense #================================================= $ProfileNameSpecified = $PSBoundParameters.ContainsKey('ProfileName') -and -not [System.String]::IsNullOrWhiteSpace($ProfileName) if ($PSBoundParameters.ContainsKey('ProfileName') -and [System.String]::IsNullOrWhiteSpace($ProfileName)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] ProfileName cannot be empty." } # Profile mode is read-only; Boot-Assets initialization can migrate and rewrite saved profiles. if (-not $ProfileNameSpecified) { Initialize-OSDeployCorePaths } $MyBuildProfile = $null $ProfileArchitecture = $null if ($ProfileNameSpecified) { $MyBuildProfile = Select-BuildOSDeployBootProfile -Name $ProfileName -SkipTokenMigration if ($MyBuildProfile) { $ProfileArchitecture = (Get-Content -LiteralPath $MyBuildProfile.FullName -Raw | ConvertFrom-Json).Architecture } else { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Build profile was not found or could not be read: $ProfileName" } if ($Architecture -and $ProfileArchitecture -and $Architecture -ne $ProfileArchitecture) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture '$Architecture' does not match build profile architecture '$ProfileArchitecture'." } if ($ProfileArchitecture) { $Architecture = $ProfileArchitecture } } if ($UseAdkWinPE -and -not $Architecture) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Architecture is required when no saved build profile supplies it." } $BuildName = if ($ProfileNameSpecified) { $ProfileName -replace '(?i)-(?:amd64|arm64)$', '' } else { 'OSDeploy' } #region TLS and Proxy $PSDefaultParameterValues['Invoke-WebRequest:UseBasicParsing'] = $true $currentProgressPref = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $regProxy = Get-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue if ($regProxy -and $regProxy.PSObject.Properties['ProxyServer'] -and $regProxy.ProxyServer -and -not ([System.Net.WebRequest]::DefaultWebProxy).Address -and $regProxy.ProxyEnable) { [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy $regProxy.ProxyServer [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials } $currentVersionTls = [Net.ServicePointManager]::SecurityProtocol $currentSupportableTls = [Math]::Max($currentVersionTls.value__, [Net.SecurityProtocolType]::Tls.value__) $availableTls = [enum]::GetValues('Net.SecurityProtocolType') | Where-Object { $_ -gt $currentSupportableTls } $availableTls | ForEach-Object { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor $_ } #endregion # Stop before source selection when the ADK required to build media is unavailable. #region ADK Detection $AdkInfo = Get-OSDeployWindowsAdkInstall if (-not $AdkInfo.IsInstalled) { Write-Warning "[$(Get-Date -Format s)] Windows ADK is not installed" Write-Warning "[$(Get-Date -Format s)] Install the Windows ADK from https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install" return } $AdkRootPath = $AdkInfo.InstallPath Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Windows ADK $($AdkInfo.InstallVersion)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Windows ADK: $AdkRootPath" # Select an explicit ADK source or a compatible WinRE source, with ADK as the fallback. #region Select Boot Image Source if ($Auto -and -not $Architecture) { $Architecture = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Auto mode: detected architecture '$Architecture'" } if ($UseAdkWinPE) { $WimSourceType = 'WinPE' } else { $WimSourceType = 'WinRE' if ($Auto) { $GetWindowsImage = Get-OSDeployCoreWindowsRE -Architecture $Architecture | Sort-Object OSVersion -Descending | Select-Object -First 1 if ($GetWindowsImage) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Auto mode: selected latest WinRE '$($GetWindowsImage.Name)' version $($GetWindowsImage.OSVersion)" } } elseif ($Architecture) { $GetWindowsImage = Select-OSDeployCoreCacheWindowsRE -Architecture $Architecture } else { $GetWindowsImage = Select-OSDeployCoreCacheWindowsRE } if (-not $GetWindowsImage -or $GetWindowsImage.Count -eq 0) { Write-Warning "[$(Get-Date -Format s)] No WinRE source selected. Defaulting to Windows ADK WinPE." $WimSourceType = 'WinPE' if (-not $Architecture) { $Architecture = ($env:PROCESSOR_ARCHITECTURE).ToLower() -replace 'x86_', '' if ($Architecture -notin @('amd64', 'arm64')) { Write-Warning "[$(Get-Date -Format s)] Unsupported architecture '$Architecture'. Only 'amd64' and 'arm64' are supported." return } } } else { $Architecture = $GetWindowsImage.Architecture $ImportImageCorePath = Join-Path $GetWindowsImage.Path '.core' $ImportImageOSFilesPath = Join-Path $GetWindowsImage.Path '.core' 'os-files' Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Recovery Image: $($GetWindowsImage.ImagePath)" } } #endregion #region ADK Paths if (($Architecture -ne 'amd64') -and ($Architecture -ne 'arm64')) { Write-Warning "[$(Get-Date -Format s)] Unknown architecture: $Architecture" return } $WindowsAdkPaths = Get-OSDeployWindowsAdkPaths -Architecture $Architecture -AdkRoot $AdkRootPath if (-not $WindowsAdkPaths) { Write-Warning "[$(Get-Date -Format s)] Unable to resolve Windows ADK paths for architecture $Architecture" return } if ($WimSourceType -eq 'WinPE') { $GetWindowsImage = Get-WindowsImage -ImagePath $WindowsAdkPaths.WimSourcePath -Index 1 $ImportImageWimPath = $GetWindowsImage.ImagePath $sourceVersion = $GetWindowsImage.Version.ToString() } elseif ($WimSourceType -eq 'WinRE') { $ImportImageWimPath = $GetWindowsImage.ImagePath $sourceVersion = $GetWindowsImage.Version } # Build the media name as {build}.{revision}-{architecture}-{name} $versionParts = $sourceVersion.Split('.') $trimmedVersion = if ($versionParts.Count -ge 4) { "$($versionParts[2]).$($versionParts[3])" } else { $sourceVersion } $MediaName = "$trimmedVersion-$Architecture-$BuildName" $MediaIsoLabel = "$trimmedVersion-$BuildName" $WindowsAdkPaths.WimSourcePath = $ImportImageWimPath #endregion #region Build Paths $BuildsPath = Join-Path $Script:OSDeployCorePath 'boot' # Handle duplicate build names by appending -001, -002, etc. $MediaRootPath = Join-Path $BuildsPath $MediaName if (Test-Path -Path $MediaRootPath) { $suffix = 1 do { $candidateName = '{0}-{1:d3}' -f $MediaName, $suffix $candidatePath = Join-Path $BuildsPath $candidateName $suffix++ } while (Test-Path -Path $candidatePath) $MediaName = $candidateName $MediaRootPath = $candidatePath } $CorePath = Join-Path $MediaRootPath '.core' $TempPath = Join-Path $MediaRootPath '.temp' $LogsPath = Join-Path $TempPath 'logs' $MediaPath = Join-Path $MediaRootPath 'bootmedia' $SourcesPath = Join-Path $MediaPath 'sources' #endregion $TemporaryBuildProfilePath = $null try { #region Select Profile, Drivers, Scripts if ($ProfileNameSpecified) { $SavedBuildProfile = Get-Content $MyBuildProfile.FullName -Raw | ConvertFrom-Json $WinPEDriver = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEDriver if ($WimSourceType -eq 'WinPE' -and $WinPEDriver) { Write-HostDateTimeDarkGray 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers' $WinPEDriver = $WinPEDriver | Where-Object { (Split-Path $_ -Leaf) -notmatch 'wifi|wireless' } } $WinPEScript = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEScript $BuildMediaScript = Expand-OSDeployBuildProfileToken $SavedBuildProfile.MediaScript $WinPEStartupProfile = Expand-OSDeployBuildProfileToken $SavedBuildProfile.WinPEStartupProfile if (-not $PSBoundParameters.ContainsKey('Languages')) { [System.String[]]$Languages = $SavedBuildProfile.Languages } if (-not $PSBoundParameters.ContainsKey('SetAllIntl')) { $SetAllIntl = $SavedBuildProfile.SetAllIntl } if (-not $PSBoundParameters.ContainsKey('SetInputLocale')) { $SetInputLocale = $SavedBuildProfile.SetInputLocale } if (-not $PSBoundParameters.ContainsKey('SetTimeZone')) { $SetTimeZone = $SavedBuildProfile.SetTimeZone } if (-not $PSBoundParameters.ContainsKey('Options')) { [System.String[]]$Options = $SavedBuildProfile.Options } $BuildProfile = [ordered]@{ Name = [System.String]$MyBuildProfile.Name Architecture = [System.String]$Architecture Languages = [System.String[]]$Languages SetAllIntl = [System.String]$SetAllIntl SetInputLocale = [System.String]$SetInputLocale SetTimeZone = [System.String]$SetTimeZone Options = [System.String[]]$Options WinPEStartupProfile = $SavedBuildProfile.WinPEStartupProfile WinPEDriver = $SavedBuildProfile.WinPEDriver WinPEScript = $SavedBuildProfile.WinPEScript MediaScript = $SavedBuildProfile.MediaScript } $MyBuildProfilePath = $MyBuildProfile.FullName } else { $skipWifi = $WimSourceType -eq 'WinPE' if ($skipWifi) { Write-HostDateTimeDarkGray 'ADK WinPE does not support wireless hardware - excluding Wi-Fi drivers' } $SelectedContent = Select-OSDeployBootProfileContent -Architecture $Architecture -SkipWifiDrivers:$skipWifi $WinPEDriver = $SelectedContent.WinPEDriver $WinPEScript = $SelectedContent.WinPEScript $BuildMediaScript = $SelectedContent.MediaScript $WinPEStartupProfile = $SelectedContent.WinPEStartupProfile $BuildProfileName = "$BuildName-$($Architecture.ToLowerInvariant())" $BuildProfile = [ordered]@{ Name = [System.String]$BuildProfileName Architecture = [System.String]$Architecture Languages = [System.String[]]$Languages SetAllIntl = [System.String]$SetAllIntl SetInputLocale = [System.String]$SetInputLocale SetTimeZone = [System.String]$SetTimeZone Options = [System.String[]]$Options WinPEStartupProfile = ConvertTo-OSDeployBuildProfileToken $WinPEStartupProfile WinPEDriver = ConvertTo-OSDeployBuildProfileToken $WinPEDriver WinPEScript = ConvertTo-OSDeployBuildProfileToken $WinPEScript MediaScript = ConvertTo-OSDeployBuildProfileToken $BuildMediaScript } $BuildProfileContentPath = Join-Path ([System.IO.Path]::GetTempPath()) "OSDeployBootProfile-$([System.Guid]::NewGuid().ToString('N'))" $TemporaryBuildProfilePath = $BuildProfileContentPath New-Item -Path $BuildProfileContentPath -ItemType Directory -ErrorAction Stop | Out-Null Initialize-OSDeployCoreBuildProfilePaths -Path $BuildProfileContentPath $BuildProfilesPath = Join-Path $Script:OSDeployBootAssetsPath 'osdeployboot-profiles' $MyBuildProfilePath = Join-Path $BuildProfilesPath "recent-$($Architecture.ToLowerInvariant()).json" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Profile to $MyBuildProfilePath" $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File -LiteralPath $MyBuildProfilePath -Encoding utf8 -Force } $BuildProfileDirectory = if ($ProfileNameSpecified) { Split-Path -Path $MyBuildProfilePath -Parent } else { $BuildProfileContentPath } if (-not $ProfileNameSpecified) { Set-OSDeployBootProfileWallpaper -ProfilePath $BuildProfileDirectory } $WinPEStartupProfilesToValidate = @($WinPEStartupProfile | Where-Object { $_ }) $ProfileStartupProfilePath = Join-Path $BuildProfileDirectory 'WinPEStartup' 'profiles' if (Test-Path -LiteralPath $ProfileStartupProfilePath -PathType Container) { $WinPEStartupProfilesToValidate += Get-ChildItem -LiteralPath $ProfileStartupProfilePath -Filter '*.json' -File -ErrorAction Stop | Sort-Object FullName | Select-Object -ExpandProperty FullName } $SeenProfiles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($StartupProfilePath in $WinPEStartupProfilesToValidate) { if ($SeenProfiles.Add([System.String]$StartupProfilePath)) { Test-OSDeployWinPEStartupProfile -Path $StartupProfilePath } } #endregion #region BuildMedia $global:BuildMedia = [ordered]@{ AdkRootPath = $AdkRootPath AdkPaths = $WindowsAdkPaths SkipAdkPackages = [System.Boolean]$SkipAdkPackages Architecture = [System.String]$Architecture BootGuid = [System.Guid]::NewGuid().ToString() BuildProfile = $MyBuildProfilePath BuildProfileContentPath = $BuildProfileDirectory ContentStartnet = [System.String]'' ContentWinpeshl = [System.String]'' CorePath = $CorePath InstalledApps = @() ImportImageWimPath = $ImportImageWimPath Languages = [System.String[]]$Languages LogsPath = $LogsPath MediaIsoLabel = $MediaIsoLabel MediaName = $MediaName MediaPath = $MediaPath MediaPathEX = $null MediaRootPath = $MediaRootPath MountPath = $null Name = [System.String]$BuildName SetAllIntl = [System.String]$SetAllIntl SetInputLocale = [System.String]$SetInputLocale SetTimeZone = [System.String]$SetTimeZone SourcesPath = $SourcesPath SourcesPathEX = $null UpdateUSB = [System.Boolean]$UpdateUSB WimSourceType = $WimSourceType WindowsImage = $null WinPEAppsPath = Join-Path $Script:OSDeployCorePath 'cache' 'winpe-apps' WinPEDriver = $WinPEDriver MediaScript = $BuildMediaScript WinPEScript = $WinPEScript WinPEStartupProfile = $WinPEStartupProfile } #endregion # Present the final configuration before creating the build output. #region Point of No Return Write-HostDateTimeDarkGray 'Build Configuration' $global:BuildMedia | Out-Host Write-Host -ForegroundColor DarkCyan 'Proceeding with build in 5 seconds. Use Ctrl+C to cancel before the next operation starts.' Start-Sleep -Seconds 5 $BuildStartTime = Get-Date #endregion # Send build telemetry with selected license identity fields. $EventLicense = Get-OSDCoreLicense Send-RecastOSDeployEvent -EventName 'Build-OSDeployBoot' -Properties @{ BootGuid = $global:BuildMedia.BootGuid Email = if ($EventLicense) { [System.String]$EventLicense.Email } else { 'Unregistered' } LicenseGuid = if ($EventLicense) { [System.String]$EventLicense.LicenseGuid } else { 'Unregistered' } } # Honor WhatIf and Confirm before creating the build directory tree. #region Create Build Directories if (-not $PSCmdlet.ShouldProcess($MediaRootPath, 'Create build directories')) { return } foreach ($dir in @($MediaRootPath, $CorePath, $TempPath, $LogsPath)) { if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } } $Transcript = "$((Get-Date).ToString('yyMMdd-HHmmss'))-Build-BootImage.log" Start-Transcript -Path (Join-Path $LogsPath $Transcript) -ErrorAction SilentlyContinue #endregion #region Hydrate Core Metadata if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath -and (Test-Path $ImportImageCorePath)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $CorePath" $null = robocopy.exe "$ImportImageCorePath" "$CorePath" *.json /nfl /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\core.log" $null = robocopy.exe "$ImportImageCorePath" "$CorePath" *.xml /nfl /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\core.log" } $ImportId = @{ id = $MediaName } if (-not (Test-Path $CorePath)) { New-Item -Path $CorePath -ItemType Directory -Force | Out-Null } $ImportId | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\id.json" -Encoding utf8 -Force #endregion #region Hydrate Media Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $MediaPath" $null = robocopy.exe "$($WindowsAdkPaths.PathWinPEMedia)" "$MediaPath" *.* /mir /b /ndl /np /r:0 /w:0 /xj /njs /mt:128 /LOG+:"$LogsPath\media.log" if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath) { Copy-Item -Path "$ImportImageCorePath\os-boot\DVD\EFI\en-US\efisys.bin" -Destination "$MediaPath\EFI\Microsoft\Boot\efisys.bin" -Force -ErrorAction SilentlyContinue Copy-Item -Path "$ImportImageCorePath\os-boot\DVD\EFI\en-US\efisys_noprompt.bin" -Destination "$MediaPath\EFI\Microsoft\Boot\efisys_noprompt.bin" -Force -ErrorAction SilentlyContinue $Fonts = @('malgunn_boot.ttf', 'meiryon_boot.ttf', 'msjhn_boot.ttf', 'msyhn_boot.ttf', 'segoen_slboot.ttf') foreach ($Font in $Fonts) { if (Test-Path "$ImportImageCorePath\os-boot\Fonts\$Font") { Copy-Item -Path "$ImportImageCorePath\os-boot\Fonts\$Font" -Destination "$MediaPath\EFI\Microsoft\Boot\Fonts\$Font" -Force -ErrorAction SilentlyContinue } } } #endregion #region Build MediaEX (BlackLotus CVE-2022-21894 Mitigation) $MediaPathEX = $null if ($WimSourceType -eq 'WinRE' -and $ImportImageCorePath -and (Test-Path "$ImportImageCorePath\os-boot\EFI_EX")) { $MediaPathEX = Join-Path $MediaRootPath 'bootmedia_ca2023' $global:BuildMedia.MediaPathEX = $MediaPathEX Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $MediaPathEX" $null = robocopy.exe "$($WindowsAdkPaths.PathWinPEMedia)" "$MediaPathEX" *.* /mir /b /ndl /np /r:0 /w:0 /xj /mt:128 /LOG+:"$LogsPath\mediaex.log" Write-HostDateTimeDarkGray 'Mitigate CVE-2022-21894 Secure Boot Security Feature Bypass Vulnerability' Remove-Item -Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts" -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts")) { New-Item -Path "$MediaPathEX\EFI\Microsoft\Boot\Fonts" -ItemType Directory -Force | Out-Null } $ExFiles = @( @{ Src = "$ImportImageCorePath\os-boot\EFI_EX\bootmgr_ex.efi"; Dst = "$MediaPathEX\bootmgr.efi" }, @{ Src = "$ImportImageCorePath\os-boot\EFI_EX\bootmgfw_ex.efi"; Dst = "$MediaPathEX\EFI\Boot\bootx64.efi" } ) foreach ($f in $ExFiles) { Copy-Item -Path $f.Src -Destination $f.Dst -Force -ErrorAction SilentlyContinue } $ExFonts = @( 'chs_boot', 'cht_boot', 'jpn_boot', 'kor_boot', 'malgun_boot', 'malgunn_boot', 'meiryo_boot', 'meiryon_boot', 'msjh_boot', 'msjhn_boot', 'msyh_boot', 'msyhn_boot', 'segmono_boot', 'segoe_slboot', 'segoen_slboot', 'wgl4_boot' ) foreach ($fontBase in $ExFonts) { $srcFont = "$ImportImageCorePath\os-boot\Fonts_EX\${fontBase}_EX.ttf" $dstFont = "$MediaPathEX\EFI\Microsoft\Boot\Fonts\$fontBase.ttf" Copy-Item -Path $srcFont -Destination $dstFont -Force -ErrorAction SilentlyContinue } Copy-Item -Path "$ImportImageCorePath\os-boot\DVD_EX\EFI\en-US\efisys_EX.bin" -Destination "$MediaPathEX\EFI\Microsoft\Boot\efisys.bin" -Force -ErrorAction SilentlyContinue Copy-Item -Path "$ImportImageCorePath\os-boot\DVD_EX\EFI\en-US\efisys_noprompt_EX.bin" -Destination "$MediaPathEX\EFI\Microsoft\Boot\efisys_noprompt.bin" -Force -ErrorAction SilentlyContinue } #endregion #region Build Sources (boot.wim) if (-not (Test-Path $SourcesPath)) { New-Item -Path $SourcesPath -ItemType Directory -Force -ErrorAction Stop | Out-Null } $bootWimPath = Join-Path $SourcesPath 'boot.wim' Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hydrate $bootWimPath" Copy-Item -Path $WindowsAdkPaths.WimSourcePath -Destination $bootWimPath -Force -ErrorAction Stop | Out-Null if (-not (Test-Path $bootWimPath)) { Write-Warning "[$(Get-Date -Format s)] Failed to copy boot.wim to $bootWimPath" Stop-Transcript return } attrib -s -h -r $SourcesPath attrib -s -h -r $bootWimPath if ($MediaPathEX) { $SourcesPathEX = Join-Path $MediaPathEX 'sources' $global:BuildMedia.SourcesPathEX = $SourcesPathEX if (-not (Test-Path $SourcesPathEX)) { New-Item -Path $SourcesPathEX -ItemType Directory -Force -ErrorAction Stop | Out-Null } attrib -s -h -r $SourcesPathEX } #endregion #region Mount Image Write-HostDateTimeDarkGray 'Mount Windows Image' $MountPath = Join-Path $TempPath 'mount' if (-not (Test-Path $MountPath)) { New-Item -Path $MountPath -ItemType Directory -Force | Out-Null } $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Mount-WindowsImage.log" $WindowsImage = Mount-WindowsImage -ImagePath $bootWimPath -Index 1 -Path $MountPath -LogPath $CurrentLog $global:BuildMedia.MountPath = $MountPath $global:BuildMedia.WindowsImage = $WindowsImage Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] MountPath: $MountPath" #endregion #region Recast Licenses $RecastLicensesPath = Join-Path $env:ProgramData 'Recast Software\Licenses' if (Test-Path $RecastLicensesPath) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copy Recast Software License files from $RecastLicensesPath to WinPE image" $DestinationPath = Join-Path $MountPath 'ProgramData\Recast Software\Licenses' if (-not (Test-Path $DestinationPath)) { New-Item -Path $DestinationPath -ItemType Directory -Force | Out-Null } Copy-Item -Path "$RecastLicensesPath\*" -Destination $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue | Out-Null } #endregion #region Registry Information Write-HostDateTimeDarkGray 'Get WinPE Registry CurrentVersion' $softwareHivePath = Join-Path $MountPath 'Windows\System32\Config\SOFTWARE' if (Test-Path $softwareHivePath) { $hiveName = 'OSDeployCoreBoot' try { reg.exe LOAD "HKLM\$hiveName" "$softwareHivePath" 2>&1 | Out-Null $regPath = "HKLM:\$hiveName\Microsoft\Windows NT\CurrentVersion" if (Test-Path $regPath) { Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue | Select-Object -Property CurrentBuild, BuildLabEx, EditionID, InstallationType, ProductName | Out-Host } } finally { Start-Sleep -Seconds 3 reg.exe UNLOAD "HKLM\$hiveName" 2>&1 | Out-Null } } #endregion #region Export Initial Packages Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Export initial Get-WindowsPackage to $CorePath" $WindowsPackage = $WindowsImage | Get-WindowsPackage if ($WindowsPackage) { $WindowsPackage | Select-Object * | Export-Clixml -Path "$CorePath\winpe-windowspackage-initial.xml" -Force } #endregion #region Adding OS Files if ($WimSourceType -eq 'WinRE' -and $ImportImageOSFilesPath -and (Test-Path $ImportImageOSFilesPath)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Adding OS Files from $ImportImageOSFilesPath" $null = robocopy.exe "$ImportImageOSFilesPath" "$MountPath" *.* /s /b /ndl /nfl /np /ts /r:0 /w:0 /xf bcp47*.dll /xx /xj /mt:128 /LOG+:"$LogsPath\os-files.log" } #endregion #region Adding OA3Tool $OA3ToolPath = $WindowsAdkPaths.oa3toolexe if ($OA3ToolPath -and (Test-Path $OA3ToolPath)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Adding OA3Tool from $OA3ToolPath" Copy-Item -Path $OA3ToolPath -Destination "$MountPath\Windows\System32\oa3tool.exe" -Force -ErrorAction SilentlyContinue | Out-Null } #endregion #region ADK Optional Component Packages if (-not $SkipAdkPackages) { $WinPEOCs = $WindowsAdkPaths.WinPEOCs $WindowsAdkWinpePackages = $global:OSDeployModule.BootImage.adkwinpepackages # Install default en-us packages Write-HostDateTimeDarkGray 'Adding ADK Packages for Language en-us' $Lang = 'en-us' foreach ($Package in $WindowsAdkWinpePackages) { $PackageFile = "$WinPEOCs\WinPE-$Package.cab" if (Test-Path $PackageFile) { Write-Host -ForegroundColor Gray "$PackageFile" $PackageName = "Add-WindowsPackage-WinPE-$Package" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-$PackageName.log" try { $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null } catch { if ($_.Exception.ErrorCode -eq '-2148468766') { Write-Warning "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE The Windows ADK version does not support this WinPE version" } if ($_.Exception.ErrorCode -eq '-2146498512') { Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE The image may be corrupted. Discard and start again" } } } } # Verify PowerShell was installed if (-not ($WindowsImage | Get-WindowsPackage | Where-Object { $_.PackageName -match 'PowerShell' })) { Write-Warning "[$(Get-Date -Format s)] PowerShell Optional Component did not install. Required ADK Packages did not install properly." Write-Warning "[$(Get-Date -Format s)] Ensure the Windows ADK version supports the WinRE version being serviced." Write-Warning "[$(Get-Date -Format s)] Build will continue so you can review the logs." } # Install en-us language pack $PackageFile = "$WinPEOCs\$Lang\lp.cab" if (Test-Path $PackageFile) { Write-Host -ForegroundColor Gray "$PackageFile" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-lp_$Lang.log" try { $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null } catch { if ($_.Exception.ErrorCode -eq '-2148468766') { Write-Warning "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE" } if ($_.Exception.ErrorCode -eq '-2146498512') { Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE" } } } # Install en-us language-specific OCs foreach ($Package in $WindowsAdkWinpePackages) { $PackageFile = "$WinPEOCs\$Lang\WinPE-${Package}_$Lang.cab" if (Test-Path $PackageFile) { Write-Host -ForegroundColor Gray "$PackageFile" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-${Package}_$Lang.log" try { $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null } catch { if ($_.Exception.ErrorCode -eq '-2148468766') { Write-Warning "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE" } if ($_.Exception.ErrorCode -eq '-2146498512') { Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE" } } } } # Save after default language Step-BuildBootSaveWindowsImage # Install additional selected languages if ($Languages -contains '*') { $Languages = Get-ChildItem $WinPEOCs -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'en-us' } | Select-Object -ExpandProperty Name } foreach ($Lang in $Languages) { if ($Lang -eq 'en-us') { continue } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Adding ADK Packages for Language $Lang" $PackageFile = "$WinPEOCs\$Lang\lp.cab" if (Test-Path $PackageFile) { Write-Host -ForegroundColor Gray "$PackageFile" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-lp_$Lang.log" try { $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null } catch { if ($_.Exception.ErrorCode -eq '-2148468766') { Write-Warning "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE" } if ($_.Exception.ErrorCode -eq '-2146498512') { Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE" } } } foreach ($Package in $WindowsAdkWinpePackages) { $PackageFile = "$WinPEOCs\$Lang\WinPE-${Package}_$Lang.cab" if (Test-Path $PackageFile) { Write-Host -ForegroundColor Gray "$PackageFile" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Add-WindowsPackage-WinPE-${Package}_$Lang.log" try { $WindowsImage | Add-WindowsPackage -PackagePath $PackageFile -LogPath "$CurrentLog" -ErrorAction Stop | Out-Null } catch { if ($_.Exception.ErrorCode -eq '-2148468766') { Write-Warning "[$(Get-Date -Format s)] 0x800f081e CBS_E_NOT_APPLICABLE" } if ($_.Exception.ErrorCode -eq '-2146498512') { Write-Warning "[$(Get-Date -Format s)] 0x800f0830 CBS_E_IMAGE_UNSERVICEABLE" } } } } # Update lang.ini if (Test-Path "$MountPath\sources\lang.ini") { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Updating lang.ini for $Lang" $CurrentLog = "$LogsPath\$((Get-Date).ToString('yyMMdd-HHmmss'))-Gen-LangINI.log" dism.exe /image:"$MountPath" /Gen-LangINI /distribution:"$MountPath" /LogPath:"$CurrentLog" } Step-BuildBootSaveWindowsImage } } #endregion #region Step Functions Step-BuildBootSetDismSettings Step-BuildBootAddWinpeJpg Step-BuildBootCoreWinPEWallpaper Step-BuildBootUpdatePowerShell Step-BuildBootInstallWinPEAppAzCopy Step-BuildBootInstallWinPEAppSysinternals Step-BuildBootInstallWinPEAppCurl Step-BuildBootInstallWinPEAppZip if ($Options -contains 'pwsh') { Step-BuildBootInstallWinPEAppPwsh } if ($Options -contains 'dart') { Step-BuildBootInstallWinPEAppDaRT } # Step-BuildBootCoreWinPEApp Step-BuildBootSaveWindowsImage Step-BuildBootRemoveWinpeshl Step-BuildBootConsoleSettings Step-BuildBootSetEnvironmentVariables Step-BuildBootCopyOSDModule Step-BuildBootCopyOSDCloudModule Step-BuildBootCoreWinPEScript Step-BuildBootSetContentStartnet Step-BuildBootCoreWinPEStartupAssets Step-BuildBootCoreWinPEStartupProfiles Step-BuildBootCoreWinPEDrivers Step-BuildBootLogWindowsDriver Step-BuildBootLogWindowsPackage Step-BuildBootLogRegCurrentVersion Step-BuildBootLogDismGetIntl Step-BuildBootGetContentStartnet Step-BuildBootGetContentWinpeshl Step-BuildBootDismountWindowsImage Step-BuildBootExportWindowsImage Step-BuildBootCoreMediaScript Step-BuildBootMediaIso Step-BuildBootUpdateUsbDrive #endregion #region Complete Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Profile to $CorePath\osdeployboot.json" $BuildProfile | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\osdeployboot.json" -Encoding utf8 -Force Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Exporting Build Context to $CorePath\buildcontext.json" $global:BuildMedia | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File "$CorePath\buildcontext.json" -Encoding utf8 -Force #region Write properties.json Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Writing properties.json to $MediaRootPath" $bootWimFinal = Join-Path $SourcesPath 'boot.wim' $wimInfo = Get-WindowsImage -ImagePath $bootWimFinal -Index 1 $buildProperties = [ordered]@{ Type = 'WinPE' Id = $MediaName Name = $BuildName ModifiedTime = $wimInfo.ModifiedTime InstallationType = $wimInfo.InstallationType Version = $wimInfo.Version.ToString() Architecture = $Architecture Languages = [System.String[]]$Languages SetAllIntl = $SetAllIntl InputLocale = $SetInputLocale TimeZone = $SetTimeZone ContentStartnet = $global:BuildMedia.ContentStartnet ContentWinpeshl = $global:BuildMedia.ContentWinpeshl InstalledApps = $global:BuildMedia.InstalledApps AdkVersion = $AdkInfo.InstallVersion BuildProfile = $MyBuildProfilePath WinPEScript = $WinPEScript WinPEDriver = $WinPEDriver MediaScript = $BuildMediaScript CreatedTime = $wimInfo.CreatedTime ImageName = $wimInfo.ImageName ImagePath = $bootWimFinal ImageIndex = 1 ImageSize = $wimInfo.ImageSize DirectoryCount = $wimInfo.DirectoryCount FileCount = $wimInfo.FileCount Path = $MediaRootPath } # Add OS source info if WinRE-based if ($WimSourceType -eq 'WinRE' -and $GetWindowsImage.OSCreatedTime) { $buildProperties.OSCreatedTime = $GetWindowsImage.OSCreatedTime $buildProperties.OSModifiedTime = $GetWindowsImage.OSModifiedTime $buildProperties.OSImageName = $GetWindowsImage.OSImageName $buildProperties.OSEditionId = $GetWindowsImage.OSEditionId $buildProperties.OSVersion = $GetWindowsImage.OSVersion } $buildProperties | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File (Join-Path $MediaRootPath 'properties.json') -Encoding utf8 -Force #endregion # Restore settings [Net.ServicePointManager]::SecurityProtocol = $currentVersionTls $ProgressPreference = $currentProgressPref $BuildEndTime = Get-Date $BuildTimeSpan = New-TimeSpan -Start $BuildStartTime -End $BuildEndTime Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Build-OSDeployBoot completed in $($BuildTimeSpan.ToString("mm' minutes 'ss' seconds'"))" Stop-Transcript #endregion } finally { if ($TemporaryBuildProfilePath -and (Test-Path -LiteralPath $TemporaryBuildProfilePath)) { Remove-Item -LiteralPath $TemporaryBuildProfilePath -Recurse -Force -ErrorAction SilentlyContinue } } } |