Refactored Get-USBDrives and New-DeploymentUSB to use tables when displaying multiple drives or FFUs to the user. Fixed an issue with cleaning up InstallAppsandSysprep.cmd. Re-wrote Readme.md.

This commit is contained in:
rbalsleyMSFT
2024-08-06 15:43:23 -07:00
parent 02d858f27f
commit e1ab74e5a3
2 changed files with 265 additions and 132 deletions
+201 -125
View File
@@ -2911,15 +2911,22 @@ Function Get-WindowsVersionInfo {
} }
} }
Function Get-USBDrive { Function Get-USBDrive {
# $USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media'") # Log the start of the USB drive check
WriteLog 'Checking for USB drives' WriteLog 'Checking for USB drives'
# Check if external hard disk media is allowed
If ($AllowExternalHardDiskMedia) { If ($AllowExternalHardDiskMedia) {
$USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media' OR MediaType='External hard disk media'") # Get all removable and external hard disk media drives
if ($PromptExternalHardDiskMedia){ [array]$USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media' OR MediaType='External hard disk media'")
# List all drives with MediaType='External hard disk media' and have the end user pick which one to use [array]$ExternalHardDiskDrives = $USBDrives | Where-Object { $_.MediaType -eq 'External hard disk media' }
[array]$ExternalHardDiskDrives = $USBDrives | Where-Object { $_.MediaType -eq 'External hard disk media' } $ExternalCount = $ExternalHardDiskDrives.Count
$USBDrivesCount = $USBDrives.Count
# Check if user should be prompted for external hard disk media
if ($PromptExternalHardDiskMedia) {
if ($ExternalHardDiskDrives) { if ($ExternalHardDiskDrives) {
if ($VerbosePreference -ne 'Continue'){ # Log and warn about found external hard disk media drives
if ($VerbosePreference -ne 'Continue') {
Write-Warning 'Found external hard disk media drives' Write-Warning 'Found external hard disk media drives'
Write-Warning 'Will prompt for user input to select the drive to use to prevent accidental data loss' Write-Warning 'Will prompt for user input to select the drive to use to prevent accidental data loss'
Write-Warning 'If you do not want to be prompted for this in the future, set -PromptExternalHardDiskMedia to $false' Write-Warning 'If you do not want to be prompted for this in the future, set -PromptExternalHardDiskMedia to $false'
@@ -2928,71 +2935,105 @@ Function Get-USBDrive {
WriteLog 'Will prompt for user input to select the drive to use to prevent accidental data loss' WriteLog 'Will prompt for user input to select the drive to use to prevent accidental data loss'
WriteLog 'If you do not want to be prompted for this in the future, set -PromptExternalHardDiskMedia to $false' WriteLog 'If you do not want to be prompted for this in the future, set -PromptExternalHardDiskMedia to $false'
# Prepare output for user selection
$Output = @()
for ($i = 0; $i -lt $ExternalHardDiskDrives.Count; $i++) { for ($i = 0; $i -lt $ExternalHardDiskDrives.Count; $i++) {
$ExternalDiskNumber = $ExternalHardDiskDrives[$i].DeviceID.Replace("\\.\PHYSICALDRIVE", "") $ExternalDiskNumber = $ExternalHardDiskDrives[$i].Index
$ExternalDisk = Get-Disk -Number $ExternalDiskNumber $ExternalDisk = Get-Disk -Number $ExternalDiskNumber
if ($VerbosePreference -ne 'Continue'){ $Index = $i + 1
# Write-Host ("{0}: {1}" -f ($i + 1), $ExternalHardDiskDrives[$i].Model) $Name = $ExternalDisk.FriendlyName
Write-Host ("Drive {0}: {1} SN/{2} PartitionStyle={3} Status={4}" -f ($i + 1), $ExternalDisk.FriendlyName , $ExternalHardDiskDrives[$i].serialnumber, $ExternalDisk.PartitionStyle,$ExternalDisk.OperationalStatus ) -ForegroundColor Green $SerialNumber = $ExternalHardDiskDrives[$i].serialnumber
$PartitionStyle = $ExternalDisk.PartitionStyle
$Status = $ExternalDisk.OperationalStatus
$Properties = [ordered]@{
'Drive Number' = $Index
'Drive Name' = $Name
'Serial Number' = $SerialNumber
'Partition Style' = $PartitionStyle
'Status' = $Status
} }
WriteLog ("Drive {0}: {1} SN/{2} PartitionStyle={3} Status={4}" -f ($i + 1), $ExternalDisk.FriendlyName , $ExternalHardDiskDrives[$i].serialnumber, $ExternalDisk.PartitionStyle,$ExternalDisk.OperationalStatus ) $Output += New-Object PSObject -Property $Properties
} }
while ($true) {
try {
# Ask the user for input
#$userInput = Read-Host "Please enter a number"
$inputChoice = $(Write-Host "Enter the number corresponding to the external hard disk media drive you want to use: " -ForegroundColor DarkYellow -NoNewline; Read-Host)
# Convert the input to a float # Format and display the output
$ISnumber = [float]$inputChoice $FormattedOutput = $Output | Format-Table -AutoSize -Property 'Drive Number', 'Drive Name', 'Serial Number', 'Partition Style', 'Status' | Out-String
if ($VerbosePreference -ne 'Continue') {
$FormattedOutput | Out-Host
}
WriteLog $FormattedOutput
# Display the entered number used for Debugging # Prompt user to select a drive
Write-Host "You selected Disk: $ISnumber" do {
$selectedIndex = $inputChoice - 1 $inputChoice = Read-Host "Enter the number corresponding to the external hard disk media drive you want to use"
break if ($inputChoice -match '^\d+$') {
$inputChoice = [int]$inputChoice
if ($inputChoice -ge 1 -and $inputChoice -le $ExternalCount) {
$SelectedIndex = $inputChoice - 1
$ExternalDiskNumber = $ExternalHardDiskDrives[$SelectedIndex].Index
$ExternalDisk = Get-Disk -Number $ExternalDiskNumber
$USBDrives = $ExternalHardDiskDrives[$SelectedIndex]
$USBDrivesCount = $USBDrives.Count
if ($VerbosePreference -ne 'Continue') {
Write-Host "Drive $inputChoice was selected"
}
WriteLog "Drive $inputChoice was selected"
} }
catch { else {
# If the input is not a valid number, display an error message # Handle invalid selection
Write-Host "Invalid input. Please try again." if ($VerbosePreference -ne 'Continue') {
Write-Host "Invalid selection. Please try again."
}
WriteLog "Invalid selection. Please try again."
}
# Check if the selected drive is offline
if ($ExternalDisk.OperationalStatus -eq 'Offline') {
if ($VerbosePreference -ne 'Continue') {
Write-Error "Selected Drive is in an Offline State. Please check the drive status in Disk Manager and try again."
}
WriteLog "Selected Drive is in an Offline State. Please check the drive status in Disk Manager and try again."
exit 1
} }
}
if ($selectedIndex -ge 0 -and $selectedIndex -lt $ExternalHardDiskDrives.Count) {
#Check if Selected Drive is in an Offline State. Useful when presenting the FFU Driv to Hyper-V VMs and forget to Online Again
if ($ExternalDisk.OperationalStatus -eq 'Offline') {
Write-Warning "Selected Drive is in an Offline State. Please check the drive status in Disk Manager and try again."
exit 1
} }
else { else {
$USBDrives = $ExternalHardDiskDrives[$selectedIndex] # Handle invalid input
if ($VerbosePreference -ne 'Continue') {
Write-Host "Invalid selection. Please try again."
}
WriteLog "Invalid selection. Please try again."
} }
} while ($null -eq $selectedIndex)
} else { }
Write-Warning "Invalid selection. Exiting." | Out-Null }
exit 1 else {
} # Log the count of found USB drives
if ($VerbosePreference -ne 'Continue') {
Write-Host "Found $USBDrivesCount total USB drives"
If ($ExternalCount -gt 0) {
Write-Host "$ExternalCount are external drives"
}
}
WriteLog "Found $USBDrivesCount total USB drives"
If ($ExternalCount -gt 0) {
WriteLog "$ExternalCount are external drives"
} }
} }
}
else {
$USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media'")
}
If ($USBDrives -and ($null -eq $USBDrives.count)) {
$USBDrivesCount = 1
} }
else { else {
# Get only removable media drives
[array]$USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media'")
$USBDrivesCount = $USBDrives.Count $USBDrivesCount = $USBDrives.Count
WriteLog "Found $USBDrivesCount Removable USB drives"
} }
WriteLog "Found $USBDrivesCount USB drives"
# Check if any USB drives were found
if ($null -eq $USBDrives) { if ($null -eq $USBDrives) {
WriteLog "No removable USB drive found. Exiting" WriteLog "No removable USB drive found. Exiting"
Write-Error "No removable USB drive found. Exiting" Write-Error "No removable USB drive found. Exiting"
exit 1 exit 1
} }
# Return the found USB drives and their count
return $USBDrives, $USBDrivesCount return $USBDrives, $USBDrivesCount
} }
Function New-DeploymentUSB { Function New-DeploymentUSB {
@@ -3004,75 +3045,88 @@ Function New-DeploymentUSB {
WriteLog "BuildUSBPath is $BuildUSBPath" WriteLog "BuildUSBPath is $BuildUSBPath"
$SelectedFFUFile = $null $SelectedFFUFile = $null
# Check if the CopyFFU switch is present
if ($CopyFFU.IsPresent) { if ($CopyFFU.IsPresent) {
# Get all FFU files in the specified directory
$FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu" $FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu"
$FFUCount = $FFUFiles.count
# If there is exactly one FFU file, select it
if (-not $FFUFiles) { if ($FFUCount -eq 1) {
# WriteLog "No FFU files found in the current directory." $SelectedFFUFile = $FFUFiles.FullName
Write-Error "No FFU files found in the current directory." }
Return # If there are multiple FFU files, prompt the user to select one
} elseif ($FFUCount -gt 1) {
WriteLog "Found $FFUCount FFU files"
elseif ($FFUFiles.Count -eq 1) { if($VerbosePreference -ne 'Continue'){
$SelectedFFUFile = $FFUFiles.FullName Write-Host "Found $FFUCount FFU files"
} }
elseif ($FFUFiles.Count -gt 1) { $output = @()
WriteLog 'Found multiple FFU files' # Create a table of FFU files with their index, name, and last modified date
Write-Warning 'Found multiple FFU files' for ($i = 0; $i -lt $FFUCount; $i++) {
for ($i = 0; $i -lt $FFUFiles.Count; $i++) { $index = $i + 1
WriteLog ("FFU {0}: {1} Modified: {2}" -f ($i + 1), $FFUFiles[$i].Name, $FFUFiles[$i].LastWriteTime) $name = $FFUFiles[$i].Name
if ($VerbosePreference -ne 'Continue') { $modified = $FFUFiles[$i].LastWriteTime
Write-Host ("FFU {0}: {1} Modified: {2}" -f ($i + 1), $FFUFiles[$i].Name, $FFUFiles[$i].LastWriteTime) -ForegroundColor Green $Properties = [ordered]@{
} 'FFU Number' = $index
'FFU Name' = $name
} 'Last Modified' = $modified
#$inputChoice = Read-Host "Enter the number corresponding to the FFU file you want to copy or 'A' to copy all FFU files" }
$inputChoice = $(Write-Host "Enter the number corresponding to the FFU file you want to copy or 'A' to copy all FFU files: " -ForegroundColor DarkYellow -NoNewline; Read-Host) $output += New-Object PSObject -Property $Properties
}
Write-Host "You selected FFU: $inputChoice" $output | Format-Table -AutoSize -Property 'FFU Number', 'FFU Name', 'Last Modified'
WriteLog "You selected FFU: $inputChoice"
# Loop until a valid FFU file is selected
while ($true) { do {
#If 'A' is selected copy all the FFUs found $inputChoice = Read-Host "Enter the number corresponding to the FFU file you want to copy or 'A' to copy all FFU files"
if ($inputChoice -eq 'A') { # Check if the input is a valid number or 'A'
$SelectedFFUFile = $FFUFiles.FullName if ($inputChoice -match '^\d+$' -or $inputChoice -eq 'A') {
Write-Host "You selected $inputChoice" if ($inputChoice -eq 'A') {
break # Select all FFU files
} $SelectedFFUFile = $FFUFiles.FullName
if ($VerbosePreference -ne 'Continue') {
try { Write-Host 'Will copy all FFU files'
}
# Try to Convert the inputChoice to a float WriteLog 'Will copy all FFU Files'
$ISnumber = [float]$inputChoice }
else {
# Display the entered number for debuggin # Convert input to integer and validate the selection
#Write-Host "You selected Disk: $ISnumber" $inputChoice = [int]$inputChoice
if ($inputChoice -ge 1 -and $inputChoice -le $FFUCount) {
} $selectedIndex = $inputChoice - 1
catch { $SelectedFFUFile = $FFUFiles[$selectedIndex].FullName
# If inputChoice is not a valid number, must have been a character, so display an error message if ($VerbosePreference -ne 'Continue') {
Write-Host "Invalid input. Please try again." Write-Host "$SelectedFFUFile was selected"
}
} WriteLog "$SelectedFFUFile was selected"
# If InputChoice is a number check that its withing the range of }
if ($inputChoice -ge 1 -and $inputChoice -le $FFUFiles.Count) { else {
$selectedIndex = $inputChoice - 1 # Handle invalid selection
Write-Host "You Selected FFU $selectedIndex" if ($VerbosePreference -ne 'Continue') {
$SelectedFFUFile = $FFUFiles[$selectedIndex].FullName Write-Host "Invalid selection. Please try again."
break }
} WriteLog "Invalid selection. Please try again."
else{ }
#No correct input for FFU selection, so prompt again and repeat Checks. }
Write-Host "Invalid FFU Number. Please try again." }
$inputChoice = $(Write-Host "Enter the number corresponding to the FFU file you want to copy or 'A' to copy all FFU files: " -ForegroundColor DarkYellow -NoNewline; Read-Host) else {
} # Handle invalid input
} if ($VerbosePreference -ne 'Continue') {
WriteLog "$SelectedFFUFile was selected" Write-Host "Invalid selection. Please try again."
Write-Host "$SelectedFFUFile was selected" }
} WriteLog "Invalid selection. Please try again."
} }
} while ($null -eq $SelectedFFUFile)
}
else {
# Handle case where no FFU files are found
WriteLog "No FFU files found in the current directory."
Write-Error "No FFU files found in the current directory."
Return
}
}
$counter = 0 $counter = 0
foreach ($USBDrive in $USBDrives) { foreach ($USBDrive in $USBDrives) {
@@ -3363,7 +3417,7 @@ if (Test-Path -Path $Logfile) {
Remove-item -Path $LogFile -Force Remove-item -Path $LogFile -Force
} }
$startTime = Get-Date $startTime = Get-Date
Write-Host "FFU build process has begun at" $startTime -ForegroundColor DarkYellow Write-Host "FFU build process started at" $startTime
Write-Host "This process can take 20 minutes or more. Please do not close this window or any additional windows that pop up" Write-Host "This process can take 20 minutes or more. Please do not close this window or any additional windows that pop up"
Write-Host "To track progress, please open the log file $Logfile or use the -Verbose parameter next time" Write-Host "To track progress, please open the log file $Logfile or use the -Verbose parameter next time"
@@ -3874,7 +3928,7 @@ Catch {
throw $_ throw $_
} }
#Clean up ffu_user and Share #Clean up ffu_user and Share and clean up apps
If ($InstallApps) { If ($InstallApps) {
try { try {
Remove-FFUUserShare Remove-FFUUserShare
@@ -3885,6 +3939,30 @@ If ($InstallApps) {
Remove-FFUVM -VMName $VMName Remove-FFUVM -VMName $VMName
throw $_ throw $_
} }
#Clean up InstallAppsandSysprep.cmd
try {
WriteLog "Cleaning up $AppsPath\InstallAppsandSysprep.cmd"
Clear-InstallAppsandSysprep
}
catch {
Write-Host 'Cleaning up InstallAppsandSysprep.cmd failed'
Writelog "Cleaning up InstallAppsandSysprep.cmd failed with error $_"
throw $_
}
try {
if (Test-Path -Path "$AppsPath\Win32" -PathType Container) {
WriteLog "Cleaning up Win32 folder"
Remove-Item -Path "$AppsPath\Win32" -Recurse -Force
}
if (Test-Path -Path "$AppsPath\MSStore" -PathType Container) {
WriteLog "Cleaning up MSStore folder"
Remove-Item -Path "$AppsPath\MSStore" -Recurse -Force
}
}
catch {
WriteLog "$_"
throw $_
}
} }
#Clean up VM or VHDX #Clean up VM or VHDX
try { try {
@@ -4020,17 +4098,15 @@ if ($VerbosePreference -ne 'Continue'){
} }
# Record the end time # Record the end time
$endTime = Get-Date $endTime = Get-Date
Write-Host "FFU build process has completed at" $endTime -ForegroundColor DarkYellow Write-Host "FFU build process completed at" $endTime
# Calculate the total run time # Calculate the total run time
$runTime = $endTime - $startTime $runTime = $endTime - $startTime
$runTimeInMinutes = [math]::Round($runTime.TotalMinutes, 2)
# Format the runtime as minutes and seconds # Format the runtime as minutes and seconds
$runTimeFormatted = 'Duration: {0:mm} min {0:ss} sec' -f $runTime $runTimeFormatted = 'Duration: {0:mm} min {0:ss} sec' -f $runTime
Write-Host $runTimeFormatted -ForegroundColor DarkGreen if ($VerbosePreference -ne 'Continue'){
Write-Host $runTimeFormatted
}
WriteLog 'Script complete: ' + $runTimeFormatted
WriteLog 'Script complete'
+64 -7
View File
@@ -1,19 +1,76 @@
# Using Full Flash Update (FFU) files to speed up Windows deployment # Using Full Flash Update (FFU) files to speed up Windows deployment
This repo contains the full FFU process that we use in US Education at Microsoft to help customers with large deployments of Windows as they prepare for the new school year. This process isn't limited to only large deployments at the start of the year, but is the most common. What if you could have a Windows image that has:
This process will copy Windows in about 2-3 minutes to the target device, optionally copy drivers, provisioning packages, Autopilot, etc. School technicians have even given the USB sticks to teachers and teachers calling them their "Magic USB sticks" to quickly get student devices reimaged in the event of an issue with their Windows PC. - The latest Windows cumulative update
- The latest .NET cumulative update
- The latest Windows Defender Platform and Definition Updates
- The latest version of Microsoft Edge
- The latest version of OneDrive (Per-Machine)
- The latest version of Microsoft 365 Apps/Office
- The latest drivers from any of the major OEMs (Dell, HP, Lenovo, Microsoft) (yes, the latest, not some out of date enterprise CAB file from years ago)
- Winget support so you can integrate any app available from Winget directly in your image
- ARM64 support for the latest Copilot+ PCs
- The ability to bring your own drivers and apps if necessary
- Custom WinRE support
While we use this in Education at Microsoft, other industries can use it as well. We esepcially see a need for something like this with partners who do re-imaging on behalf of customers. The difference in Education is that they typically have large deployments that tend to happen at the beginning of the school year and any amount of time saved is helpful. Microsoft Deployment Toolkit, Configuration Manager, and other community solutions are all great solutions, but are typically slower due to WIM deployments being file-based while FFU files are sector-based. And the best part: it takes less than two minutes to apply the image, even with all of these updates added to the media. After setting Windows up and going through Autopilot or a provisioning package, total elapsed time ~10 minutes (depending on what Intune or your device management tool is deploying).
The Full-Flash update (FFU) process can automatically download the latest release of Windows 11, the updates mentioned above, and creates a USB drive that can be used to quickly reimage a machine.
# Updates # Updates
2407.1 has been released! Check out the changes in the new [Change Log](ChangeLog.md) 2408.1 has been released! Check out the changes in the [Change Log](ChangeLog.md)
# Getting Started # Getting Started
If you're not familiar with Github, you can click the Green code button above and select download zip. Extract the zip file and make sure to copy the FFUDevelopment folder to the root of your C: drive. That will make it easy to follow the guide and allow the scripts to work properly. - Download the latest [release](https://github.com/rbalsleyMSFT/FFU/releases)
- Extract the FFUDevelopment folder from the ZIP file (recommend to C:\FFUDevelopment)
- Follow the doc: C:\FFUDevelopment\Docs\BuildDeployFFU.docx
If extracted correctly, your c:\FFUDevelopment folder should look like the following. If it does, go to c:\FFUDevelopment\Docs\BuildDeployFFU.docx to get started. ## YouTube Detailed Walkthrough
![image](https://github.com/rbalsleyMSFT/FFU/assets/53497092/5400a203-9c2e-42b2-b24c-ab8dfd922ba1) The first 15 minutes of the following video includes a quick start demo to get started. Below the video are a list of chapters
[![Reimage Windows Fast with Full-Flash Update (FFU))](https://img.youtube.com/vi/rqXRbgeeKSQ/maxresdefault.jpg)](https://www.youtube.com/watch?v=rqXRbgeeKSQ "Reimage Windows Fast with Full-Flash Update (FFU))")
Chapters:
[00:00](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=0s) Begin
[03:21](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=201s) Quick Start Prereqs
[07:19](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=439s) Quick Start Demo
[14:12](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=852s) Script Parameters
[17:22](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=1042s) Obtaining Windows Media
[25:55](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=1555s) Adding Applications
[26:59](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=1619s) Adding M365 Apps/Office
[29:21](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=1761s) Adding Applications via Winget
[34:59](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=2099s) Bring your own Applications
[36:01](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=2161s) Customizing InstallAppsAndSysprep.cmd
[38:34](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=2314s) Demo - Application Configuration
[49:43](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=2983s) Drivers
[55:39](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3339s) Automatically downloading drivers
[57:28](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3448s) Microsoft Surface drivers
[58:55](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3535s) Dell drivers
[01:01:45](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3705s) Lenovo drivers
[01:03:16](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3796s) HP drivers
[01:05:25](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3925s) Bring your own drivers
[01:06:24](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=3984s) Demo - Drivers
[01:11:55](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=4315s) Multi-model driver support
[01:13:21](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=4401s) Device naming
[01:18:30](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=4710s) Device enrollment
[01:21:43](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=4903s) Autopilot
[01:24:57](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5097s) Provisioning packages
[01:26:54](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5214s) Custom WinRE
[01:29:59](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5399s) Demo - Putting it all together (Deep dive)
[01:32:06](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5526s) Downloading Lenovo 500w drivers
[01:33:28](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5608s) Downloading apps via Winget
[01:36:54](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5814s) Downloading Office, Defender, Edge, OneDrive
[01:38:15](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5895s) Building the Apps.iso
[01:39:08](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=5948s) Applying Windows to the VHDX
[01:40:16](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=6016s) Downloading and applying cumulative updates
[01:41:44](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=6104s) Building the VM
[01:48:13](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=6493s) Capturing the FFU
[01:53:38](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=6818s) Creating USB drive
[01:58:41](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=7121s) Deploying FFU
[02:11:48](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=7908s) Troubleshooting
[02:14:30](https://www.youtube.com/watch?v=rqXRbgeeKSQ&t=8070s) EDU Endpoint Office Hours