Create a bootable USB drive

Discussion in 'Scripting' started by GodHand, Oct 2, 2018.

  1. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    #1 GodHand, Oct 2, 2018
    Last edited: Aug 1, 2019
    08-01-19 Updated

    This advanced PowerShell function leverages a disk object to create GPT/MBR/UEFI/BIOS bootable Windows Installation USB drives. By using a disk object, the function does not require visible variable values for preparing the disk or creating its file structure. This removes any possibility of the incorrect disk being used during the function's process when there are no detectable access paths to the disk itself.

    If it detects the USB drive exceeds 32GB, it will allocate only that amount of space.

    After the function clears all data from the USB disk it will create a partition structure based on the parameters passed (UEFI/BIOS/GPT/MBR). It then formats the partitions accordingly. After the USB disk is set up, it will automatically mount the ISO and then measure the length of the install.wim to ensure it does not exceed the FAT32 filesystem's maximum 4GB per-file limit. If the WIM file does exceed this limit, the function will automatically split the WIM into SWM files, create the file and directory structure on the USB disk and copy the OS installation media contents to the USB disk and make it bootable. It will then automatically dismount the ISO and return a custom object displaying the name of the USB, its access path, the filesystem used to format the disk volumes and return a boolean value to verify that the disk is indeed bootable.

    Though I have another function that performs a USB creation like Rufus does (creating two partitions - the "tiny" one retaining only the EFI/boot files and the NTFS partition retaining all the OS files), I use this function more often since it works across all versions of Windows. I have never had any issues booting multiple USB disks across all types of device firmwares.

    To create a disk object, with your USB disk inserted in the device, you type Get-Disk and locate the USB disk and take note of the number assigned to it. Then you can assign that to a variable: $USB = Get-Disk -Number 6, for example.

    To run the function, simply enter New-WinBootUSB -Disk $USB -MediaPath "E:\Windows 10 Pro.iso" -DiskLayout UEFI. You can add an optional Volume Label to identify the disk, too, if so desired.

    The -MediaPath parameter can either be the path to a Windows installation ISO, or an already extracted Windows installation ISO. This way you can create a bootable disk if the Windows installation ISO has already been extracted.

    [​IMG]

    [​IMG]

    Code:
    Function New-WinBootUSB
    {
    <#
        .SYNOPSIS
            Create a bootable USB drive.
       
        .DESCRIPTION
            Create a bootable USB drive using using a disk object.
           
            Create a Windows installation file structure and copy the full installation media directly to the disk making it bootable on both GPT/UEFI and MBR/BIOS systems
       
        .PARAMETER Disk
            Create a disk object using the Get-Disk cmdlet that identifies the USB drive by its index number.
            Example: $USB = Get-Disk -Number 6
       
        .PARAMETER MediaPath
            The full path to a Windows installation media. This can either be an ISO or directory.
       
        .PARAMETER PartitionStyle
            The partition style of the disk. Options are:
            GPT (Default)
            MBR
       
        .PARAMETER VolumeLabel
            An optional volume label to easily identify the USB drive.
       
        .PARAMETER DiskLabel
            An optional volume label to easily identify the USB drive.
       
        .EXAMPLE
            PS C:\> New-WinBootUSB -Disk $USB -MediaPath "E:\Windows 10\WIN_10_ENTERPRISE_LTSC.iso"
            PS C:\> New-WinBootUSB -Disk $USB -MediaPath "E:\Windows 10\WIN_10_ENTERPRISE_LTSC" -PartitionStyle GPT -VolumeLabel "WIN10LTSC"
    #>
       
        [CmdletBinding(ConfirmImpact = 'High',
                      SupportsShouldProcess = $true)]
        [OutputType([PSCustomObject])]
        Param
        (
            [Parameter(Mandatory = $true,
                      HelpMessage = 'A disk object that identifies the USB drive.')]
            [ValidateNotNullOrEmpty()]
            [object]$Disk,
            [Parameter(Mandatory = $true,
                      HelpMessage = 'The full path to a Windows installation media. This can either be an ISO or directory.')]
            [ValidateScript({ Test-Path $(Resolve-Path -Path $_) })]
            [IO.FileInfo]$MediaPath,
            [Parameter(Mandatory = $false,
                      HelpMessage = 'The partition style of the disk.')]
            [ValidateNotNullOrEmpty()]
            [ValidateSet('GPT', 'MBR')]
            [string]$PartitionStyle = 'GPT',
            [Parameter(Mandatory = $false,
                      HelpMessage = 'An optional volume label to easily identify the USB drive.')]
            [string]$VolumeLabel
        )
       
        Begin
        {
            $ErrorActionPreference = 'Stop'
        }
        Process
        {
            If ($PSCmdlet.ShouldProcess("$($Disk.FriendlyName), All data will be lost"))
            {
                Try
                {
                    If ($Disk.BusType -ne 'USB') { Write-Warning "Disk must be a USB."; Break }
                    Write-Host "Preparing the disk..." -NoNewline -ForegroundColor Cyan
                    $Disk = Get-Disk -Number $Disk.Number | Get-Disk
                    $Disk | Set-Disk -IsReadOnly $false
                    $Disk | Clear-Disk -RemoveData -Confirm:$false
                    If ($Disk.PartitionStyle -eq 'RAW') { $Disk | Initialize-Disk -PartitionStyle $PartitionStyle }
                    Else { $Disk | Set-Disk -PartitionStyle $PartitionStyle }
                    Switch ($PartitionStyle)
                    {
                        'GPT'
                        {
                            If ($Disk.Size -gt 32GB) { $Partition = $Disk | New-Partition -Size 32GB }
                            Else { $Partition = $Disk | New-Partition -UseMaximumSize }
                        }
                        'MBR'
                        {
                            If ($Disk.Size -gt 32GB) { $Partition = $Disk | New-Partition -Size 32GB -IsActive }
                            Else { $Partition = $Disk | New-Partition -UseMaximumSize -IsActive }
                        }
                    }
                    If ($VolumeLabel) { $Format = Format-Volume -Partition $Partition -NewFileSystemLabel $VolumeLabel -FileSystem FAT32 -Force -Confirm:$false }
                    Else { $Format = Format-Volume -Partition $Partition -FileSystem FAT32 -Force -Confirm:$false }
                    $Partition | Add-PartitionAccessPath -AssignDriveLetter
                    $Disk = Get-Disk -Number $Disk.Number | Get-Disk
                    $DiskPath = (($Disk | Get-Partition).AccessPaths[0]).SubString(0, 2)
                    If ($null -eq $DiskPath) { $DiskPath = (($Disk | Get-Partition).AccessPaths[1]).SubString(0, 2) }
                    Write-Host "[Complete]" -ForegroundColor Cyan
                }
                Catch
                {
                    Write-Warning $($_.Exception.Message)
                    Break
                }
                Try
                {
                    If ($MediaPath.Extension -eq '.ISO') { $SourcePath = (Mount-DiskImage -ImagePath $MediaPath.FullName -StorageType ISO -PassThru | Get-Volume).DriveLetter + ':' }
                    Else { If (Test-Path -Path $MediaPath -PathType Container) { $SourcePath = $MediaPath.FullName } }
                    If ((Get-ChildItem -Path "$SourcePath\sources\install.wim").Length -gt 4GB)
                    {
                        Write-Host "Splitting the WIM to individual SWM files. This will take some time..." -NoNewline -ForegroundColor Cyan
                        [void](New-Item -Path $DiskPath -Name sources -ItemType Directory -Force)
                        Start-Process -FilePath DISM -ArgumentList ('/Split-Image /ImageFile:"{0}" /SWMFile:"{1}" /FileSize:4096' -f "$($SourcePath)\sources\install.wim", "$($DiskPath)\sources\install.swm") -WindowStyle Hidden -Wait
                        $FileList = Get-ChildItem -Path $SourcePath -Recurse -Exclude install.wim; $Progress = 0
                        Write-Host "[Complete]" -ForegroundColor Cyan
                    }
                    Else { $FileList = Get-ChildItem -Path $SourcePath -Recurse; $Progress = 0 }
                    Write-Host "Copying the installation media file structure..." -NoNewline -ForegroundColor Cyan
                    ForEach ($File In $FileList)
                    {
                        $FileName = $File.FullName.Replace($SourcePath, $null)
                        $DestinationFilePath = Join-Path -Path $DiskPath -ChildPath $FileName
                        Switch ($File.Length)
                        {
                            { $_ -lt 1MB } { $FileSize = ($_ / 1KB).ToString("0.00 KB"); Break }
                            { $_ -lt 1GB } { $FileSize = ($_ / 1MB).ToString("0.00 MB"); Break }
                            { $_ -lt 1TB } { $FileSize = ($_ / 1GB).ToString("0.00 GB"); Break }
                        }
                        Write-Progress -Activity "Copying media from $(Split-Path -Path $MediaPath -Leaf)" -Status "Copying File: $($File.FullName), Size: $FileSize" -PercentComplete (($Progress / $($FileList.Count)) * 100)
                        Copy-Item $($File.FullName) -Destination $DestinationFilePath -Force
                        $Progress++
                    }
                    Write-Host "[Complete]" -ForegroundColor Cyan
                    Write-Host "Making drive bootable..." -NoNewline -ForegroundColor Cyan
                    $Bootsect = Join-Path -Path $SourcePath -ChildPath 'boot\bootsect.exe'
                    If (!(Test-Path -Path $Bootsect)) { $Bootsect = 'bootsect.exe' }
                    $RunProcess = Start-Process -FilePath $Bootsect -ArgumentList ('/NT60 "{0}"' -f $DiskPath) -WindowStyle Hidden -Wait -PassThru
                    If ($RunProcess.ExitCode -eq 0) { Write-Host "[Complete]" -ForegroundColor Cyan; $IsBootable = $true }
                    Else { Write-Host "[Failed]" -ForegroundColor Red; $IsBootable = $false }
                    $Result = [PSCustomObject]@{
                        'USB Name'    = $Disk.FriendlyName
                        'Access Path' = $DiskPath + '\'
                        'File System' = $Format.FileSystem
                        'Is Bootable' = $IsBootable.ToString()
                    } | Format-List
                }
                Catch
                {
                    Write-Warning $($_.Exception.Message)
                    Break
                }
                Finally
                {
                    [void](Dismount-DiskImage -ImagePath $MediaPath.FullName)
                }
            }
        }
        End
        {
            If ($Result) { Return $Result }
        }
    }
    
    08/01/19 - Optimized its disk preparation process and added output verbosity.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  2. TheStutch

    TheStutch MDL Novice

    Apr 14, 2018
    41
    73
    0
    Can you please design this script to also ingest already unpacked image folders like the unpacked directory produced by your Optimize-Offline tool?
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  3. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    #3 GodHand, Oct 7, 2018
    Last edited: Oct 8, 2018
    (OP)
    Yeah I can add that option and post it.

    *EDIT*

    Done.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  4. TheStutch

    TheStutch MDL Novice

    Apr 14, 2018
    41
    73
    0
    #4 TheStutch, Nov 13, 2018
    Last edited: Nov 13, 2018
    I haven't had any success using this tool at all. I know that doesn't provide you with a deep degree of troubleshooting insight, but basically it just doesn't work.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  5. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    Right :rolleyes:
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  6. TheStutch

    TheStutch MDL Novice

    Apr 14, 2018
    41
    73
    0
    Thanks, I'm not the only one having issues, but whatever ...
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  7. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    [​IMG]

    [​IMG]

    Works fine with both ISO files and expanded ISO directories. The problem on your end is, as usual, between the keyboard and seat; not with the function.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  8. TheStutch

    TheStutch MDL Novice

    Apr 14, 2018
    41
    73
    0
    #8 TheStutch, Nov 13, 2018
    Last edited: Nov 13, 2018
    Thanks for posting the clarifying photos. That does actually help!
    Your absolutely correct about where the problem lies. I wasn't getting the results you've shown and gave up.
    In any event my, my 'whining' was rewarded here https://forums.mydigitallife.net/th...rprise-n-ltsc-2019.76325/page-98#post-1481070 with a PowerShell GUI script that freed me from having to use the confusing keyboard and let me do nearly everything I wanted by pushing buttons on the less confusing mouse.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  9. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    #9 GodHand, Nov 13, 2018
    Last edited: Nov 13, 2018
    (OP)
    What does creating an ISO have to do with this function whatsoever?

    As a sidenote, that ISO Creator has no boot image stream, so any ISO file made with it will not be bootable.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  10. TheStutch

    TheStutch MDL Novice

    Apr 14, 2018
    41
    73
    0
    #10 TheStutch, Nov 14, 2018
    Last edited: Nov 14, 2018
    I figured out my mistake and got your script to work. Thanks for sharing it and for improving it to work with fully expanded ISO media! Create a bootable USB drive.jpg
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  11. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    Always best to consider a parameter or value is incorrect before dismissing an entire function/project/whatever and telling others it does not work when it does. I do not write, and especially would not post, functions, scripts, projects, or the like that I am not complete certain are functional unless it's for a debugging reason (which I would not do on this forum) or to theorycraft with others.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  12. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    I can also upload the function I use that works similar to how Rufus works if so desired.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  13. BluSmoke

    BluSmoke MDL Novice

    Jan 14, 2010
    4
    0
    0
    That would be very much appreciated GodHand. I love your code - succinct and easy to read.

    I'm curious if you have a function already that allows creation of batches of USB keys by ejecting each at the end of the write process. I recall seeing something a year or two ago but forget where on the web. I think the code then used the shell32.dll function to eject USB media but may be mistaken.

    Thanks again,

    BluSmoke
     
  14. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    534
    926
    30
    Easiest way to do safe removable drive ejecting is using the InvokeVerb method on the Shell.Application COM object which can be made a PowerShell one-liner:

    Code:
    (New-Object -ComObject Shell.Application).NameSpace(17).ParseName($DriveLetter).InvokeVerb('Eject')
    
    Where the variable $DriveLetter is the removable drive you want to eject.

    You can easily make a function for that:

    Code:
    Function Eject-Disk
    {
        [CmdletBinding()]
        Param
        (
            [Parameter(Mandatory = $true,
                       ValueFromPipeline = $true,
                       ValueFromPipelineByPropertyName = $true)]
            [Object]$Disk
        )
        
        Process
        {
            $Disk = Get-Disk -Number $Disk.Number | Get-Disk
            $DriveType = Get-CimInstance -ClassName Win32_LogicalDisk -Filter DriveType=2
            If ($DriveType)
            {
                If ($DriveType.Caption -eq ($Disk | Get-Partition).DriveLetter + ':')
                {
                    $DriveLetter = ($Disk | Get-Partition).DriveLetter + ':'
                    (New-Object -ComObject Shell.Application).NameSpace(17).ParseName($DriveLetter).InvokeVerb('Eject')
                }
            }
        }
    }
    
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...