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)
    #=================================================
}