public/New-OSDeployBootUSB.ps1
|
function New-OSDeployBootUSB { <# .SYNOPSIS Creates a two-partition bootable USB drive from OSDeploy boot media .DESCRIPTION Displays eligible USB disks and repeatedly prompts for a disk number, then presents a completed OSDeploy boot build and either its bootmedia or bootmedia_ca2023 directory in Out-GridView selectors. Eligible disks have USB bus type and sizes greater than 7 GB and less than 2000 GB. The selected disk is cleared when it has partitions, converted to MBR when needed, and formatted with a 4 GB active FAT32 boot partition plus an NTFS data partition using the remaining space. The selected boot-media tree is copied to FAT32. When local OSDCloud content exists and fits, it is copied to an OSDCloud directory on the NTFS partition. Before media selection, the function sets the machine-wide Explorer NoDriveTypeAutorun policy to 0xFF. ShouldProcess gates clearing existing partitions, creating and formatting partitions, and file copies. The AutoRun policy change is not gated. Initialization of an already empty RAW disk and conversion of an already empty GPT disk to MBR occur before the partition ShouldProcess check and are also not gated. .PARAMETER BootLabel Specifies the FAT32 boot-partition label. The value can contain 0 through 11 characters. The default is OSDEPLOY. .PARAMETER DataLabel Specifies the NTFS data-partition label. The value can contain 0 through 32 characters. The default is OSDCloud. .EXAMPLE PS> New-OSDeployBootUSB Interactively selects the USB disk and boot media, then creates partitions with the default labels. .EXAMPLE PS> New-OSDeployBootUSB -BootLabel 'WinPE' -DataLabel 'WinPE-Data' Interactively prepares the selected USB disk using custom partition labels. .EXAMPLE PS> New-OSDeployBootUSB -WhatIf Runs the selectors and reports gated disk and copy operations. The machine-wide AutoRun policy change still occurs, and an already empty disk can still be converted to MBR. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject. Emits a projected snapshot of the selected disk before preparation and returns the refreshed OSDeploy disk object after preparation. The formatted disk table shown before selection is sent to the host and is not returned. System.String. Native robocopy standard output is written to the success stream during boot-media and optional OSDCloud copies. .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, curl.exe, Administrator rights, Out-GridView, a completed BootImage build, and an eligible USB disk. This command removes all partitions and data from the selected USB disk. It also sets HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\NoDriveTypeAutorun to 0xFF for the system, even if later media selection is cancelled. .LINK https://github.com/RecastSoftware/RecastOSDeploy #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( # FAT32 boot-partition label; 0 through 11 characters. [ValidateLength(0, 11)] [string] $BootLabel = 'OSDEPLOY', # NTFS data-partition label; 0 through 32 characters. [ValidateLength(0, 32)] [string] $DataLabel = 'OSDCloud' ) #================================================= # Stop before preparing USB media 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." } #================================================= # Set Variables $ErrorActionPreference = 'Stop' $MinimumSizeGB = 7 $MaximumSizeGB = 2000 #================================================= # Stop when no supported USB disk is selected. # Select USB disk $SelectDisk = Select-OSDeployDiskUSB -MinimumSizeGB $MinimumSizeGB -MaximumSizeGB $MaximumSizeGB if (-not $SelectDisk) { Write-Warning "[$(Get-Date -Format s)] No USB drive found meeting the size requirements ($MinimumSizeGB GB - $MaximumSizeGB GB)" return } $GetUSBDisk = Get-OSDeployDisk -BusType USB -Number $SelectDisk.Number $GetUSBDisk | Select-Object -Property * -ExcludeProperty Cim*, PS*, Pass* #================================================= # Disable Autorun Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -Name NoDriveTypeAutorun -Type DWord -Value 0xFF -ErrorAction SilentlyContinue #================================================= # Stop when no completed BootImage is selected. # Select a BootImage build $SelectBootImage = Select-OSDeployCoreBootMedia if ($null -eq $SelectBootImage) { Write-Warning "[$(Get-Date -Format s)] No OSDeployCore BootImage build was found or selected" return } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Selected BootImage: $($SelectBootImage.Name)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] BootImage path: $($SelectBootImage.Path)" #================================================= # Stop when the user cancels boot media selection. # Select a bootmedia folder Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Select a bootmedia folder to copy to the USB (Cancel to exit)" $BootMediaObject = Get-ChildItem $SelectBootImage.Path -Directory | Where-Object { ($_.Name -eq 'bootmedia') -or ($_.Name -eq 'bootmedia_ca2023') } | Sort-Object Name, FullName | Select-Object Name, FullName | Out-GridView -Title 'Select a bootmedia folder to copy to the USB (Cancel to exit)' -OutputMode Single if ($null -eq $BootMediaObject) { Write-Warning "[$(Get-Date -Format s)] No bootmedia folder was found or selected" return } #================================================= # Honor WhatIf and Confirm before removing existing disk partitions and data. # Clear the disk if ($GetUSBDisk.NumberOfPartitions -ne 0) { if ($PSCmdlet.ShouldProcess("Disk $($SelectDisk.Number)", 'Clear existing partitions and data')) { $GetUSBDisk | Clear-Disk -RemoveData -RemoveOEM -Confirm:$false -ErrorAction Stop } else { return } } $GetUSBDisk = Get-OSDeployDisk -BusType USB -Number $SelectDisk.Number | Where-Object { $_.NumberOfPartitions -eq 0 } if (-not $GetUSBDisk) { Write-Warning "[$(Get-Date -Format s)] Unable to verify the USB disk was cleared successfully" return } #================================================= # Convert the selected disk to the required MBR layout before creating boot and data partitions. # Initialize and partition (MBR) if ($GetUSBDisk.PartitionStyle -eq 'RAW') { $GetUSBDisk | Initialize-Disk -PartitionStyle MBR -ErrorAction Stop } if ($GetUSBDisk.PartitionStyle -eq 'GPT') { Set-Disk -Number $GetUSBDisk.Number -PartitionStyle MBR -ErrorAction Stop } if ($GetUSBDisk.SizeGB -le 2000) { if ($PSCmdlet.ShouldProcess("Disk $($GetUSBDisk.Number)", "Create FAT32 '$BootLabel' and NTFS '$DataLabel' partitions")) { $BootPartition = $GetUSBDisk | New-Partition -Size 4GB -IsActive -AssignDriveLetter | Format-Volume -FileSystem FAT32 -NewFileSystemLabel $BootLabel -ErrorAction Stop $DataPartition = $GetUSBDisk | New-Partition -UseMaximumSize -AssignDriveLetter | Format-Volume -FileSystem NTFS -NewFileSystemLabel $DataLabel -ErrorAction Stop } else { return } } else { # Error as the drive is too large Write-Warning "[$(Get-Date -Format s)] The selected USB disk is too large to create the required partitions" return } #================================================= # Copy media only when both the selected source and formatted destination are available. # Copy BootMedia to the FAT32 partition $WinpeDestinationPath = "$($BootPartition.DriveLetter):\" if (-not $WinpeDestinationPath) { Write-Warning "[$(Get-Date -Format s)] Unable to determine the destination drive letter" return } if ((Test-Path $BootMediaObject.FullName) -and (Test-Path $WinpeDestinationPath)) { if ($PSCmdlet.ShouldProcess($WinpeDestinationPath, "Copy BootMedia from $($BootMediaObject.FullName)")) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copying BootMedia to $WinpeDestinationPath" robocopy.exe "$($BootMediaObject.FullName)" "$WinpeDestinationPath" *.* /e /ndl /njh /njs /np /r:0 /w:0 /b /zb } } #================================================= # Copy OSDCloud content to USB data partition $OSDCloudSourcePath = Join-Path $script:OSDeployCorePath 'OSDCloud' # Skip optional OSDCloud content when no local source is available. if (-not (Test-Path -Path $OSDCloudSourcePath)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] OSDCloud source path not found, skipping data partition copy: $OSDCloudSourcePath" } else { $DataDrive = $DataPartition.DriveLetter $OSDCloudDestPath = "$DataDrive`:\OSDCloud" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Checking OSDCloud content on $DataDrive`:\" # Calculate total source size for free space check $SourceFiles = Get-ChildItem -Path $OSDCloudSourcePath -File -Recurse $RequiredBytes = ($SourceFiles | Measure-Object -Property Length -Sum).Sum $DataVolumeInfo = Get-Volume -DriveLetter $DataDrive -ErrorAction SilentlyContinue $FreeBytes = $DataVolumeInfo.SizeRemaining if ($null -eq $FreeBytes) { Write-Warning "[$(Get-Date -Format s)] Could not determine free space on $DataDrive`:\ — skipping OSDCloud copy" } elseif ($RequiredBytes -gt $FreeBytes) { $RequiredGB = [Math]::Round($RequiredBytes / 1GB, 2) $FreeGB = [Math]::Round($FreeBytes / 1GB, 2) Write-Warning "[$(Get-Date -Format s)] Insufficient space on $DataDrive`:\ — need $RequiredGB GB, $FreeGB GB free — skipping OSDCloud copy" } else { if ($PSCmdlet.ShouldProcess($OSDCloudDestPath, "Copy OSDCloud content from $OSDCloudSourcePath")) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copying OSDCloud from $OSDCloudSourcePath to $OSDCloudDestPath" robocopy.exe "$OSDCloudSourcePath" "$OSDCloudDestPath" *.* /e /njh /ndl /r:0 /w:0 /xd '$RECYCLE.BIN' 'System Volume Information' /xj } } } #================================================= Write-Verbose "[$($MyInvocation.MyCommand.Name)] End" return (Get-OSDeployDisk -BusType USB -Number $SelectDisk.Number) #================================================= } |