public/New-OSDeployUSB.ps1

function New-OSDeployUSB {
    <#
    .SYNOPSIS
        Creates a new OSDeploy USB bootable drive from an OSDeployCore BootImage build.
 
    .DESCRIPTION
        Prepares a USB drive for use as an OSDeploy bootable device. The function:
        - Prompts for selection of a completed BootImage from %ProgramData%\OSDeployCore\boot-media
        - Prompts for the specific bootmedia folder (bootmedia or bootmedia_ca2023)
        - Selects a suitable USB disk (7 GB - 2 TB)
        - Clears, partitions (MBR), and formats the disk:
            - FAT32 4 GB active boot partition (labeled $BootLabel)
            - NTFS remaining-space data partition (labeled $DataLabel)
        - Copies the selected BootMedia to the FAT32 partition
 
    .PARAMETER BootLabel
        Volume label for the FAT32 boot partition. Maximum 11 characters (FAT32 limit).
        Default is 'USB-WinPE'.
 
    .PARAMETER DataLabel
        Volume label for the NTFS data partition. Maximum 32 characters.
        Default is 'USB-DATA'.
 
    .EXAMPLE
        New-OSDeployUSB
        Creates a new OSDeploy USB using default labels 'USB-WinPE' and 'USB-DATA'.
 
    .EXAMPLE
        New-OSDeployUSB -BootLabel 'OSDEPLOY' -DataLabel 'OSD-DATA'
        Creates a new OSDeploy USB using custom partition labels.
 
    .OUTPUTS
        Microsoft.Management.Infrastructure.CimInstance#root/Microsoft/Windows/Storage/MSFT_Disk
        Returns the prepared USB disk object.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 0.1.0
        Date: April 2026
 
        Prerequisites:
            - PowerShell 5.0 or higher
            - Windows 10 or higher
            - Run as Administrator
            - At least one completed BootImage build in %ProgramData%\OSDeployCore\boot-media
            - A USB drive of at least 7 GB
 
    .LINK
        https://github.com/OSDeploy/OSDeploy
    #>


    [CmdletBinding()]
    param (
        # Label for the FAT32 boot partition. Default is 'USB-WinPE'.
        [ValidateLength(0, 11)]
        [string]
        $BootLabel = 'USB-WinPE',

        # Label for the NTFS data partition. Default is 'USB-DATA'.
        [ValidateLength(0, 32)]
        [string]
        $DataLabel = 'USB-DATA'
    )
    #=================================================
    Write-OSDeployBanner
    $Error.Clear()
    Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Start"
    #=================================================
    # Requires Run as Administrator
    $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $IsAdmin) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] This function must be Run as Administrator"
        return
    }
    #=================================================
    # Set Variables
    $ErrorActionPreference = 'Stop'
    $MinimumSizeGB = 7
    $MaximumSizeGB = 2000
    #=================================================
    # Block
    Block-StandardUser
    Block-WindowsVersionNe10
    Block-PowerShellVersionLt5
    Block-WindowsReleaseIdLt1703
    #=================================================
    # Select a BootImage build
    $SelectBootImage = Select-OSDeployCoreBootImage
    if ($null -eq $SelectBootImage) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No OSDeployCore BootImage build was found or selected"
        return
    }
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Selected BootImage: $($SelectBootImage.Name)"
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] BootImage path: $($SelectBootImage.Path)"
    #=================================================
    # Select a bootmedia folder
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] 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 G)] [$($MyInvocation.MyCommand.Name)] No bootmedia folder was found or selected"
        return
    }
    #=================================================
    # Disable Autorun
    Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -Name NoDriveTypeAutorun -Type DWord -Value 0xFF -ErrorAction SilentlyContinue
    #=================================================
    # Select USB disk
    $SelectDisk = Invoke-SelectUSBDisk -MinimumSizeGB $MinimumSizeGB -MaximumSizeGB $MaximumSizeGB
    if (-not $SelectDisk) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No USB drive found meeting the size requirements ($MinimumSizeGB GB - $MaximumSizeGB GB)"
        break
    }

    $GetUSBDisk = Get-OSDDisk -BusType USB -Number $SelectDisk.Number
    $GetUSBDisk | Select-Object -Property * -ExcludeProperty Cim*, PS*, Pass*
    #=================================================
    # Clear the disk
    if ($GetUSBDisk.NumberOfPartitions -ne 0) {
        $GetUSBDisk | Clear-Disk -RemoveData -RemoveOEM -Confirm:$true -ErrorAction Stop
    }

    $GetUSBDisk = Get-OSDDisk -BusType USB -Number $SelectDisk.Number | Where-Object { $_.NumberOfPartitions -eq 0 }
    if (-not $GetUSBDisk) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Unable to verify the USB disk was cleared successfully"
        break
    }
    #=================================================
    # 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) {
        $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
    }
    #=================================================
    # Copy BootMedia to the FAT32 partition
    $WinpeDestinationPath = "$($BootPartition.DriveLetter):\"
    if (-not $WinpeDestinationPath) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Unable to determine the destination drive letter"
        break
    }

    if ((Test-Path $BootMediaObject.FullName) -and (Test-Path $WinpeDestinationPath)) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Copying BootMedia to $WinpeDestinationPath"
        robocopy.exe "$($BootMediaObject.FullName)" "$WinpeDestinationPath" *.* /e /ndl /njh /njs /np /r:0 /w:0 /b /zb
    }
    #=================================================
    Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] End"
    return (Get-OSDDisk -BusType USB -Number $SelectDisk.Number)
    #=================================================
}