diff --git a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd
new file mode 100644
index 0000000..9e35570
--- /dev/null
+++ b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd
@@ -0,0 +1,13 @@
+REM Put each app install on a separate line
+REM M365 Apps/Office ProPlus
+d:\Office\setup.exe /configure d:\Office\DeployFFU.xml
+REM Add additional apps below here
+REM Contoso App (Example)
+REM msiexec /i d:\Contoso\setup.msi /qn /norestart
+REM The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time.
+REM Also kills the sysprep process in order to automate sysprep generalize
+del c:\windows\panther\unattend\unattend.xml /F /Q
+del c:\windows\panther\unattend.xml /F /Q
+taskkill /IM sysprep.exe
+timeout /t 5
+c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
diff --git a/FFUDevelopment/Office/FFU.xml b/FFUDevelopment/Apps/Office/DeployFFU.xml
similarity index 90%
rename from FFUDevelopment/Office/FFU.xml
rename to FFUDevelopment/Apps/Office/DeployFFU.xml
index 6977fb3..115876c 100644
--- a/FFUDevelopment/Office/FFU.xml
+++ b/FFUDevelopment/Apps/Office/DeployFFU.xml
@@ -6,6 +6,8 @@
+
+
diff --git a/FFUDevelopment/Office/DownloadFFU.xml b/FFUDevelopment/Apps/Office/DownloadFFU.xml
similarity index 84%
rename from FFUDevelopment/Office/DownloadFFU.xml
rename to FFUDevelopment/Apps/Office/DownloadFFU.xml
index d433f43..95fb4c5 100644
--- a/FFUDevelopment/Office/DownloadFFU.xml
+++ b/FFUDevelopment/Apps/Office/DownloadFFU.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/FFUDevelopment/BuildFFUUnattend/unattend.xml b/FFUDevelopment/BuildFFUUnattend/unattend.xml
new file mode 100644
index 0000000..9590b1e
--- /dev/null
+++ b/FFUDevelopment/BuildFFUUnattend/unattend.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ 1
+ d:\InstallAppsandSysprep.cmd
+
+
+
+
+
+
+
+ Audit
+
+
+
+
+
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 025528c..ccf2606 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -1,60 +1,1339 @@
-#Modify variables
-$VMPath = "c:\VM\$VMName"
-$ISOPath = "E:\software\ISOs\Windows\Windows 11\en-us_windows_11_consumer_editions_version_22h2_updated_feb_2023_x64_dvd_4fa87138.iso"
-$memory = 8GB
-$disksize = 30GB
-$processors = 4
-$rand = get-random
-$VMName = "_FFU-$rand"
-$VHDPath = "$VMPath\$VMName.vhdx"
+#Requires -Modules Hyper-V, Storage
+#Requires -PSEdition Desktop
+#Requires -RunAsAdministrator
-# 0. Delete old VMs and remove old certs
+<#
+.SYNOPSIS
+A PowerShell script to create a Windows 10/11 FFU file.
-$certPath = 'Cert:\LocalMachine\Shielded VM Local Certificates\'
-$vms = get-vm _ffu-* | ? {$_.state -ne 'running'}
+.DESCRIPTION
+This script creates a Windows 10/11 FFU and USB drive to help quickly get a Windows device reimaged. FFU can be customized with drivers, apps, and additional settings.
-If($null -ne $vms){
- Foreach ($vm in $vms){
- $OldVMName = $vm.VMName
- Remove-VM -Name $vm.name -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "C:\VM\$OldVMName" -Force -Recurse -ErrorAction SilentlyContinue
- Remove-HgsGuardian -Name $OldVMName
- $certs = Get-ChildItem -Path $certPath -Recurse | Where-Object { $_.Subject -like "*$OldVMName*" }
- foreach ($cert in $Certs){
- Remove-item -Path $cert.PSPath -force
+.PARAMETER ISOPath
+Path to the Windows 10/11 ISO file.
+
+.PARAMETER WindowsSKU
+Edition of Windows 10/11 to be installed, e.g., 'Home', 'Home_N', 'Home_SL', 'EDU', 'EDU_N', 'Pro', 'Pro_N', 'Pro_EDU', 'Pro_Edu_N', 'Pro_WKS', 'Pro_WKS_N'
+
+.PARAMETER FFUDevelopmentPath
+Path to the FFU development folder (default is C:\FFUDevelopment).
+
+.PARAMETER InstallApps
+When set to $true, the script will create an Apps.iso file from the $FFUDevelopmentPath\Apps folder. It will also create a VM, mount the Apps.ISO, install the Apps, sysprep, and capture the VM. When set to $False, the FFU is created from a VHDX file. No VM is created.
+
+.PARAMETER InstallOffice
+Install Microsoft Office if set to $true. The script will download the latest ODT and Office files in the $FFUDevelopmentPath\Apps\Office folder and install Office in the FFU via VM
+
+.PARAMETER InstallDrivers
+Install device drivers from the specified $FFUDevelopmentPath\Drivers folder if set to $true. Download the drivers and put them in the Drivers folder. The script will recurse the drivers folder and add the drivers to the FFU.
+
+.PARAMETER Memory
+Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Use 4GB if necesary.
+
+.PARAMETER Disksize
+Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk.
+
+.PARAMETER Processors
+Number of virtual processors for the virtual machine. Recommended to use at least 4.
+
+.PARAMETER VMSwitchName
+Name of the Hyper-V virtual switch. If $InstallApps is set to $true, this must be set. This is required to capture the FFU from the VM. The default is *external*, but you will likely need to change this.
+
+.PARAMETER VMLocation
+Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to.
+
+.PARAMETER FFUPrefix
+Prefix for the generated FFU file. Default is _FFU
+
+.PARAMETER FFUCaptureLocation
+Path to the folder where the captured FFU will be stored. Default is $FFUDevelopmentPath\FFU
+
+.PARAMETER ShareName
+Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed.
+
+.PARAMETER Username
+Username for accessing the shared folder. The default is ffu_user. The script will auto create the account and password. When finished, it will remove the account.
+
+.PARAMETER VMHostIPAddress
+IP address of the Hyper-V host for FFU capture. If $InstallApps is set to $true, this parameter must be configured. You must manually configure this. The script will not auto detect your IP (depending on your network adapters, it may not find the correct IP).
+
+.PARAMETER CreateCaptureMedia
+When set to $true, this will create WinPE capture media for use when $InstallApps is set to $true. This capture media will be automatically attached to the VM and the boot order will be changed to automate the capture of the FFU.
+
+.PARAMETER CreateDeploymentMedia
+When set to $true, this will create WinPE deployment media for use when deploying to a physical device.
+
+.PARAMETER OptionalFeatures
+Provide a semi-colon separated list of Windows optional features you want to include in the FFU (e.g. netfx3;TFTP)
+
+.PARAMETER ProductKey
+Product key for the Windows 10/11 edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here.
+
+.PARAMETER BuildUSBDrive
+When set to $true, will partition and format a USB drive and copy the captured FFU to the drive. If you'd like to customize the drive to add drivers, provisioning packages, name prefix, etc. You'll need to do that afterward.
+
+.EXAMPLE
+Command line for most people who want to create an FFU with Office and drivers and have never done it before. This assumes you have copied this script and associated files to the C:\FFUDevelopment folder. If you need to use another drive or folder, change the -FFUDevelopment parameter (e.g. -FFUDevelopment 'D:\FFUDevelopment')
+
+.\BuildFFUVMv3.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $true -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
+
+Command line for those who just want a FFU with no drivers, apps, or Office
+.\BuildFFUVMv3.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $false -InstallOffice $false -InstallDrivers $false -CreateCaptureMedia $false -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
+
+Command line for those who just want a FFU with Apps and drivers, no Office
+.\BuildFFUVMv3.ps1 -ISOPath 'C:\path_to_iso\Windows.iso' -WindowsSKU 'Pro' -Installapps $true -InstallOffice $false -InstallDrivers $true -VMSwitchName 'Name of your VM Switch in Hyper-V' -VMHostIPAddress 'Your IP Address' -CreateCaptureMedia $true -CreateDeploymentMedia $true -BuildUSBDrive $true -verbose
+
+Command line with all parameters for reference
+.\BuildFFUVMv3.ps1 -ISOPath "C:\path_to_iso\Windows.iso" -WindowsSKU "Pro" -FFUDevelopmentPath "C:\FFUDevelopment" -InstallApps $true -InstallOffice $true -InstallDrivers $true -Memory 8GB -Disksize 30GB -Processors 4 -VMSwitchName "Your VM Switch Name" -VMLocation "C:\VMs" -FFUPrefix "_FFU" -FFUCaptureLocation "C:\FFUDevelopment\FFU" -ShareName "FFUCaptureShare" -Username "ffu_user" -VMHostIPAddress "Your IP Address" -CreateCaptureMedia $true -CreateDeploymentMedia $false -OptionalFeatures "NetFx3;TFTP" -ProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX -BuildUSBDrive $true -verbose"
+
+.NOTES
+ Additional notes about your script.
+
+.LINK
+ https://github.com/rbalsleyMSFT/FFU
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $false, Position = 0)]
+ [ValidateScript({ Test-Path $_ })]
+ [string]$ISOPath,
+ [ValidateScript({
+ $allowedSKUs = @('Home', 'Home_N', 'Home_SL', 'EDU', 'EDU_N', 'Pro', 'Pro_N', 'Pro_EDU', 'Pro_Edu_N', 'Pro_WKS', 'Pro_WKS_N')
+ if ($allowedSKUs -contains $_) { $true } else { throw "Invalid WindowsSKU value. Allowed values: $($allowedSKUs -join ', ')" }
+ })]
+ [string]$WindowsSKU = 'Pro',
+ [ValidateScript({ Test-Path $_ })]
+ [string]$FFUDevelopmentPath = 'C:\FFUDevelopment',
+ [bool]$InstallApps,
+ [bool]$InstallOffice,
+ [Parameter(Mandatory = $false)]
+ [ValidateScript({
+ if ($_ -and (!(Test-Path -Path '.\Drivers') -or ((Get-ChildItem -Path '.\Drivers' -Recurse | Measure-Object -Property Length -Sum).Sum -lt 1MB))) {
+ throw "InstallDrivers is set to `$true, but either the Drivers folder is missing or empty"
+ }
+ return $true
+ })]
+ [bool]$InstallDrivers,
+ [uint64]$Memory = 4GB,
+ [uint64]$Disksize = 30GB,
+ [int]$Processors = 4,
+ [string]$VMSwitchName,
+ [string]$VMLocation,
+ [string]$FFUPrefix = '_FFU',
+ [string]$FFUCaptureLocation,
+ [String]$ShareName = "FFUCaptureShare",
+ [string]$Username = "ffu_user",
+ [Parameter(Mandatory = $false)]
+ [ValidateScript({
+ if ($InstallApps -and ($_ -eq $null)) {
+ throw "If variable InstallApps is set to `$true, VMHostIPAddress must also be set to capture the FFU"
+ }
+ return $true
+ })]
+ [string]$VMHostIPAddress,
+ [bool]$CreateCaptureMedia = $true,
+ [bool]$CreateDeploymentMedia,
+ [ValidateScript({
+ $allowedFeatures = @("Windows-Defender-Default-Definitions","Printing-PrintToPDFServices-Features","Printing-XPSServices-Features","TelnetClient","TFTP",
+ "TIFFIFilter","LegacyComponents","DirectPlay","MSRDC-Infrastructure","Windows-Identity-Foundation","MicrosoftWindowsPowerShellV2Root","MicrosoftWindowsPowerShellV2",
+ "SimpleTCP","NetFx4-AdvSrvs","NetFx4Extended-ASPNET45","WCF-Services45","WCF-HTTP-Activation45","WCF-TCP-Activation45","WCF-Pipe-Activation45","WCF-MSMQ-Activation45",
+ "WCF-TCP-PortSharing45","IIS-WebServerRole","IIS-WebServer","IIS-CommonHttpFeatures","IIS-HttpErrors","IIS-HttpRedirect","IIS-ApplicationDevelopment","IIS-Security",
+ "IIS-RequestFiltering","IIS-NetFxExtensibility","IIS-NetFxExtensibility45","IIS-HealthAndDiagnostics","IIS-HttpLogging","IIS-LoggingLibraries","IIS-RequestMonitor",
+ "IIS-HttpTracing","IIS-URLAuthorization","IIS-IPSecurity","IIS-Performance","IIS-HttpCompressionDynamic","IIS-WebServerManagementTools","IIS-ManagementScriptingTools",
+ "IIS-IIS6ManagementCompatibility","IIS-Metabase","WAS-WindowsActivationService","WAS-ProcessModel","WAS-NetFxEnvironment","WAS-ConfigurationAPI","IIS-HostableWebCore",
+ "WCF-HTTP-Activation","WCF-NonHTTP-Activation","IIS-StaticContent","IIS-DefaultDocument","IIS-DirectoryBrowsing","IIS-WebDAV","IIS-WebSockets","IIS-ApplicationInit",
+ "IIS-ISAPIFilter","IIS-ISAPIExtensions","IIS-ASPNET","IIS-ASPNET45","IIS-ASP","IIS-CGI","IIS-ServerSideIncludes","IIS-CustomLogging","IIS-BasicAuthentication",
+ "IIS-HttpCompressionStatic","IIS-ManagementConsole","IIS-ManagementService","IIS-WMICompatibility","IIS-LegacyScripts","IIS-LegacySnapIn","IIS-FTPServer","IIS-FTPSvc",
+ "IIS-FTPExtensibility","MSMQ-Container","MSMQ-DCOMProxy","MSMQ-Server","MSMQ-ADIntegration","MSMQ-HTTP","MSMQ-Multicast","MSMQ-Triggers","IIS-CertProvider",
+ "IIS-WindowsAuthentication","IIS-DigestAuthentication","IIS-ClientCertificateMappingAuthentication","IIS-IISCertificateMappingAuthentication","IIS-ODBCLogging",
+ "NetFx3","SMB1Protocol-Deprecation","MediaPlayback","WindowsMediaPlayer","Client-DeviceLockdown","Client-EmbeddedShellLauncher","Client-EmbeddedBootExp",
+ "Client-EmbeddedLogon","Client-KeyboardFilter","Client-UnifiedWriteFilter","HostGuardian","MultiPoint-Connector","MultiPoint-Connector-Services","MultiPoint-Tools"
+ ,"AppServerClient","SearchEngine-Client-Package","WorkFolders-Client","Printing-Foundation-Features","Printing-Foundation-InternetPrinting-Client",
+ "Printing-Foundation-LPDPrintService","Printing-Foundation-LPRPortMonitor","HypervisorPlatform","VirtualMachinePlatform","Microsoft-Windows-Subsystem-Linux",
+ "Client-ProjFS","Containers-DisposableClientVM",'Containers-DisposableClientVM','Microsoft-Hyper-V-All','Microsoft-Hyper-V','Microsoft-Hyper-V-Tools-All',
+ 'Microsoft-Hyper-V-Management-PowerShell','Microsoft-Hyper-V-Hypervisor','Microsoft-Hyper-V-Services','Microsoft-Hyper-V-Management-Clients','DataCenterBridging',
+ 'DirectoryServices-ADAM-Client','Windows-Defender-ApplicationGuard','ServicesForNFS-ClientOnly','ClientForNFS-Infrastructure','NFS-Administration','Containers','Containers-HNS',
+ 'Containers-SDN','SMB1Protocol','SMB1Protocol-Client','SMB1Protocol-Server','SmbDirect')
+ $inputFeatures = $_ -split ';'
+ foreach ($feature in $inputFeatures) {
+ if (-not ($allowedFeatures -contains $feature)) {
+ throw "Invalid optional feature '$feature'. Allowed values: $($allowedFeatures -join ', ')"
+ }
+ }
+ $true
+ })]
+ [string]$OptionalFeatures,
+ [string]$ProductKey,
+ [bool]$BuildUSBDrive
+)
+
+if (($InstallOffice -eq $true) -and ($InstallApps -eq $false)) {
+ throw "If variable InstallOffice is set to `$true, InstallApps must also be set to `$true."
+}
+
+#Check if Hyper-V feature is installed (requires only checks the module)
+$osInfo = Get-WmiObject -Class Win32_OperatingSystem
+$isServer = $osInfo.Caption -match 'server'
+
+if ($isServer) {
+ $hyperVFeature = Get-WindowsFeature -Name Hyper-V
+ if ($hyperVFeature.InstallState -ne "Installed") {
+ Write-Host "Hyper-V feature is not installed. Please install it before running this script."
+ exit
+ }
+}
+else {
+ $hyperVFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All
+ if ($hyperVFeature.State -ne "Enabled") {
+ Write-Host "Hyper-V feature is not enabled. Please enable it before running this script."
+ exit
+ }
+}
+
+# Set default values for variables that depend on other parameters
+if (-not $AppsISO) { $AppsISO = "$FFUDevelopmentPath\Apps.iso" }
+if (-not $AppsPath) { $AppsPath = "$FFUDevelopmentPath\Apps" }
+if (-not $OfficePath) { $OfficePath = "$AppsPath\Office" }
+if (-not $rand) { $rand = Get-Random }
+if (-not $VMLocation) { $VMLocation = "$FFUDevelopmentPath\VM" }
+if (-not $VMName) { $VMName = "$FFUPrefix-$rand" }
+if (-not $VMPath) { $VMPath = "$VMLocation\$VMName" }
+if (-not $VHDXPath) { $VHDXPath = "$VMPath\$VMName.vhdx" }
+if (-not $FFUCaptureLocation) { $FFUCaptureLocation = "$FFUDevelopmentPath\FFU" }
+if (-not $LogFile) { $LogFile = "$FFUDevelopmentPath\FFUDevelopment.log" }
+
+#FUNCTIONS
+function WriteLog($LogText) {
+ Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" -Force -ErrorAction SilentlyContinue
+ Write-Verbose $LogText
+}
+
+function LogVariableValues {
+ $excludedVariables = @(
+ 'PSBoundParameters',
+ 'PSScriptRoot',
+ 'PSCommandPath',
+ 'MyInvocation',
+ '?',
+ 'ConsoleFileName',
+ 'ExecutionContext',
+ 'false',
+ 'HOME',
+ 'Host',
+ 'hyperVFeature',
+ 'input',
+ 'MaximumAliasCount',
+ 'MaximumDriveCount',
+ 'MaximumErrorCount',
+ 'MaximumFunctionCount',
+ 'MaximumVariableCount',
+ 'null',
+ 'PID',
+ 'PSCmdlet',
+ 'PSCulture',
+ 'PSUICulture',
+ 'PSVersionTable',
+ 'ShellId',
+ 'true'
+ )
+
+ $allVariables = Get-Variable -Scope Script | Where-Object { $_.Name -notin $excludedVariables }
+ WriteLog 'Logging variables'
+ foreach ($variable in $allVariables) {
+ $variableName = $variable.Name
+ $variableValue = $variable.Value
+ if ($null -ne $variableValue) {
+ WriteLog "[VAR]$variableName`: $variableValue"
+ }
+ else {
+ WriteLog "[VAR]Variable $variableName not found or not set"
+ }
+ }
+ WriteLog 'End logging variables'
+}
+
+function Invoke-Process {
+ [CmdletBinding(SupportsShouldProcess)]
+ param
+ (
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string]$FilePath,
+
+ [Parameter()]
+ [ValidateNotNullOrEmpty()]
+ [string]$ArgumentList
+ )
+
+ $ErrorActionPreference = 'Stop'
+
+ try {
+ $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)"
+ $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)"
+
+ $startProcessParams = @{
+ FilePath = $FilePath
+ ArgumentList = $ArgumentList
+ RedirectStandardError = $stdErrTempFile
+ RedirectStandardOutput = $stdOutTempFile
+ Wait = $true;
+ PassThru = $true;
+ NoNewWindow = $true;
+ }
+ if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
+ $cmd = Start-Process @startProcessParams
+ $cmdOutput = Get-Content -Path $stdOutTempFile -Raw
+ $cmdError = Get-Content -Path $stdErrTempFile -Raw
+ if ($cmd.ExitCode -ne 0) {
+ if ($cmdError) {
+ throw $cmdError.Trim()
+ }
+ if ($cmdOutput) {
+ throw $cmdOutput.Trim()
+ }
+ }
+ else {
+ if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
+ WriteLog $cmdOutput
+ }
+ }
+ }
+ }
+ catch {
+ #$PSCmdlet.ThrowTerminatingError($_)
+ WriteLog $_
+ Write-Host "Script failed - $Logfile for more info"
+ throw $_
+
+ }
+ finally {
+ Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore
+
+ }
+
+}
+Function Get-ADK {
+ Writelog 'Get ADK Path'
+ # Define the registry key and value name to query
+ $adkRegKey = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots"
+ $adkRegValueName = "KitsRoot10"
+
+ # Check if the registry key exists
+ if (Test-Path $adkRegKey) {
+ # Get the registry value for the Windows ADK installation path
+ $adkPath = (Get-ItemProperty -Path $adkRegKey -Name $adkRegValueName).$adkRegValueName
+
+ if ($adkPath) {
+ WriteLog "ADK located at $adkPath"
+ return $adkPath
+ }
+ }
+ else {
+ throw "Windows ADK is not installed or the installation path could not be found."
+ }
+}
+function Get-ODTURL {
+
+ [String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
+
+ $MSWebPage | ForEach-Object {
+ if ($_ -match 'url=(https://.*officedeploymenttool.*\.exe)') {
+ $matches[1]
}
}
}
-# 1. Create Dynamic Hard Disk
-mkdir -Path $VMPath -Force
-New-VHD -Path $VHDPath -Fixed -SizeBytes $disksize
-#New-VHD -path $VHDPath -SizeBytes 128000000000 -LogicalSectorSizeBytes 512 -Dynamic
+function Get-Office {
+ #Download ODT
+ $ODTUrl = Get-ODTURL
+ $ODTInstallFile = "$env:TEMP\odtsetup.exe"
+ WriteLog "Downloading Office Deployment Toolkit from $ODTUrl to $ODTInstallFile"
+ Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile
-# 2. Create VM
-# - Name: _FFU
-# - Location: C:\VM\_FFU
-# - Generation: 2
-# - Memory: 4GB static
-# - Networking: Not connected
-# - Connect to existing VHD: c:\VM\_FFU\
-# - Mount ISO
-$VM = New-VM -Name $VMName -Path $VMPath -MemoryStartupBytes $memory -VHDPath $VHDPath -Generation 2
-Set-VMProcessor -VMName $VMName -Count $processors
-Add-VMDvdDrive -VMName $VMName -Path $ISOPath
-$VMDVDDrive = Get-VMDvdDrive -VMName $VMName
-Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
+ # Extract ODT
+ WriteLog "Extracting ODT to $OfficePath"
+ # Start-Process -FilePath $ODTInstallFile -ArgumentList "/extract:$OfficePath /quiet" -Wait
+ Invoke-Process $ODTInstallFile "/extract:$OfficePath /quiet"
-#Configure TPM
-New-HgsGuardian -Name $VMName -GenerateCertificates
-$owner = get-hgsguardian -Name $VMName
-$kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot
-Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData
-Enable-VMTPM -VMName $VMName
+ # Run setup.exe with config.xml and modify xml file to download to $OfficePath
+ $ConfigXml = "$OfficePath\DownloadFFU.xml"
+ $xmlContent = [xml](Get-Content $ConfigXml)
+ $xmlContent.Configuration.Add.SourcePath = $OfficePath
+ $xmlContent.Save($ConfigXml)
+ WriteLog "Downloading M365 Apps/Office to $OfficePath"
+ # Start-Process -FilePath "$OfficePath\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
+ Invoke-Process $OfficePath\setup.exe "/download $ConfigXml"
-#Connect to VM
-vmconnect $VM.ComputerName $VMName
+ WriteLog "Cleaning up ODT default config files and checking InstallAppsandSysprep.cmd file for proper command line"
+ #Clean up default configuration files
+ Remove-Item -Path "$OfficePath\configuration*" -Force
-#Use if creating for MDT/SCCM Builds
-#Get-VM $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName "Intel(R) I350 Gigabit Network Connection - Virtual Switch"
-#$networkAdapter = Get-VMNetworkAdapter -VMName $VMName
\ No newline at end of file
+ #Read the contents of the InstallAppsandSysprep.cmd file
+ $content = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd"
+
+ #Update the InstallAppsandSysprep.cmd file with the Office install command
+ $officeCommand = "d:\Office\setup.exe /configure d:\Office\DeployFFU.xml"
+
+ # Check if Office command is not commented out or missing and fix it if it is
+ if ($content[2] -ne $officeCommand) {
+ $content[2] = $officeCommand
+
+ # Write the modified content back to the file
+ Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $content
+ }
+}
+
+function New-AppsISO {
+ #Create Apps ISO file
+ $OSCDIMG = "$adkpath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe"
+ #Start-Process -FilePath $OSCDIMG -ArgumentList "-n -m -d $Appspath $AppsISO" -wait
+ Invoke-Process $OSCDIMG "-n -m -d $Appspath $AppsISO"
+
+ #Remove the Office Download and ODT
+ if ($InstallOffice) {
+ $ODTPath = "$AppsPath\Office"
+ $OfficeDownloadPath = "$ODTPath\Office"
+ WriteLog 'Cleaning up Office and ODT download'
+ Remove-Item -Path $OfficeDownloadPath -Recurse -Force
+ Remove-Item -Path "$ODTPath\setup.exe"
+ }
+
+}
+function Get-WimFromISO {
+ #Mount ISO, get Wim file
+ $mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
+ $sourcesFolder = ($mountResult | Get-Volume).DriveLetter + ":\sources\"
+
+ # Check for install.wim or install.esd
+ $wimPath = (Get-ChildItem $sourcesFolder\install.* | Where-Object { $_.Name -match "install\.(wim|esd)" }).FullName
+
+ if($wimPath) {
+ WriteLog "The path to the install file is: $wimPath"
+ }
+ else {
+ WriteLog "No install.wim or install.esd file found in: $sourcesFolder"
+ }
+
+ return $wimPath
+}
+
+
+function Get-WimIndex {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$WindowsSKU
+ )
+ WriteLog "Getting WIM Index for Windows SKU: $WindowsSKU"
+
+ $wimindex = switch ($WindowsSKU) {
+ 'Home' { 1 }
+ 'Home_N' { 2 }
+ 'Home_SL' { 3 }
+ 'EDU' { 4 }
+ 'EDU_N' { 5 }
+ 'Pro' { 6 }
+ 'Pro_N' { 7 }
+ 'Pro_EDU' { 8 }
+ 'Pro_Edu_N' { 9 }
+ 'Pro_WKS' { 10 }
+ 'Pro_WKS_N' { 11 }
+ Default { 6 }
+ }
+
+ Writelog "WIM Index: $wimindex"
+ return $WimIndex
+}
+
+#Create VHDX
+function New-ScratchVhdx {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$VhdxPath,
+ [uint64]$SizeBytes = 30GB,
+ [ValidateSet(512, 4096)]
+ [uint32]$LogicalSectorSizeBytes = 512,
+ [switch]$Dynamic,
+ [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT
+ )
+
+ WriteLog "Creating new Scratch VHDX..."
+
+ $newVHDX = New-VHD -Path $VhdxPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes -Dynamic:($Dynamic.IsPresent)
+ $toReturn = $newVHDX | Mount-VHD -Passthru | Initialize-Disk -PassThru -PartitionStyle GPT
+
+ #Remove auto-created partition so we can create the correct partition layout
+ remove-partition $toreturn.DiskNumber -PartitionNumber 1 -Confirm:$False
+
+ Writelog "Done."
+ return $toReturn
+}
+#Add System Partition
+function New-SystemPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [uint64]$SystemPartitionSize = 256MB
+ )
+
+ WriteLog "Creating System partition..."
+
+ $sysPartition = $VhdxDisk | New-Partition -DriveLetter 'S' -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
+ $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"
+
+ WriteLog 'Done.'
+ return $sysPartition.DriveLetter
+}
+#Add MSRPartition
+function New-MSRPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk
+ )
+
+ WriteLog "Creating MSR partition..."
+
+ # $toReturn = $VhdxDisk | New-Partition -AssignDriveLetter -Size 16MB -GptType "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" -IsHidden | Out-Null
+ $toReturn = $VhdxDisk | New-Partition -Size 16MB -GptType "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" -IsHidden | Out-Null
+
+ WriteLog "Done."
+
+ return $toReturn
+}
+#Add OS Partition
+function New-OSPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [Parameter(Mandatory = $true)]
+ [string]$WimPath,
+ [uint32]$WimIndex,
+ [uint64]$OSPartitionSize = 0
+ )
+
+ WriteLog "Creating OS partition..."
+
+ if ($OSPartitionSize -gt 0) {
+ $osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -Size $OSPartitionSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
+ }
+ else {
+ $osPartition = $vhdxDisk | New-Partition -DriveLetter 'W' -UseMaximumSize -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
+ }
+
+ $osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"
+ WriteLog 'Done'
+ Writelog "OS partition at drive $($osPartition.DriveLetter):"
+
+ WriteLog "Writing Windows at $WimPath to OS partition at drive $($osPartition.DriveLetter):..."
+
+ #Server 2019 is missing the Windows Overlay Filter (wof.sys), likely other Server SKUs are missing it as well. Script will error if trying to use the -compact switch on Server OSes
+ if ((Get-CimInstance Win32_OperatingSystem).Caption -match "Server") {
+ WriteLog (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\")
+ }
+ else {
+ WriteLog (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\" -Compact)
+ }
+
+ WriteLog 'Done'
+ return $osPartition
+}
+#Add Recovery partition
+function New-RecoveryPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [Parameter(Mandatory = $true)]
+ $OsPartition,
+ [uint64]$RecoveryPartitionSize = 0,
+ [ciminstance]$DataPartition
+ )
+
+ WriteLog "Creating empty Recovery partition (to be filled on first boot automatically)..."
+
+ $calculatedRecoverySize = 0
+ $recoveryPartition = $null
+
+ if ($RecoveryPartitionSize -gt 0) {
+ $calculatedRecoverySize = $RecoveryPartitionSize
+ }
+ else {
+ $winReWim = Get-ChildItem "$($OsPartition.DriveLetter):\Windows\System32\Recovery\Winre.wim"
+
+ if (($null -ne $winReWim) -and ($winReWim.Count -eq 1)) {
+ # Wim size + 52MB is minimum WinRE partition size.
+ # NTFS and other partitioning size differences account for about 17MB of space that's unavailable.
+ # Adding 32MB as a buffer to ensure there's enough space.
+ $calculatedRecoverySize = $winReWim.Length + 52MB + 32MB
+
+ WriteLog "Calculated space needed for recovery in bytes: $calculatedRecoverySize"
+
+ if ($null -ne $DataPartition) {
+ $DataPartition | Resize-Partition -Size ($DataPartition.Size - $calculatedRecoverySize)
+ WriteLog "Data partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
+ }
+ else {
+ $newOsPartitionSize = [math]::Floor(($OsPartition.Size - $calculatedRecoverySize) / 4096) * 4096
+ $OsPartition | Resize-Partition -Size $newOsPartitionSize
+ WriteLog "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
+ }
+
+ $recoveryPartition = $VhdxDisk | New-Partition -DriveLetter 'R' -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
+ | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel 'Recovery'
+
+ WriteLog "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"
+ }
+ else {
+ WriteLog "No WinRE.WIM found in the OS partition under \Windows\System32\Recovery."
+ WriteLog "Skipping creating the Recovery partition."
+ WriteLog "If a Recovery partition is desired, please re-run the script setting the -RecoveryPartitionSize flag as appropriate."
+ }
+ }
+
+ return $recoveryPartition
+}
+#Add boot files
+function Add-BootFiles {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$OsPartitionDriveLetter,
+ [Parameter(Mandatory = $true)]
+ [string]$SystemPartitionDriveLetter,
+ [string]$FirmwareType = 'UEFI'
+ )
+
+ WriteLog "Adding boot files for `"$($OsPartitionDriveLetter):\Windows`" to System partition `"$($SystemPartitionDriveLetter):`"..."
+ Invoke-Process bcdboot "$($OsPartitionDriveLetter):\Windows /S $($SystemPartitionDriveLetter): /F $FirmwareType"
+ WriteLog "Done."
+}
+
+function Enable-WindowsFeaturesByName {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$FeatureNames,
+ [Parameter(Mandatory = $true)]
+ [string]$Source
+ )
+
+ $FeaturesArray = $FeatureNames.Split(';')
+
+ # Looping through each feature and enabling it
+ foreach ($FeatureName in $FeaturesArray) {
+ WriteLog "Enabling Windows Optional feature: $FeatureName"
+ Enable-WindowsOptionalFeature -Path $WindowsPartition -FeatureName $FeatureName -All -Source $Source | Out-Null
+ WriteLog "Done"
+ }
+}
+
+#Dismount VHDX
+function Dismount-ScratchVhdx {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$VhdxPath
+ )
+
+ if (Test-Path $VhdxPath) {
+ WriteLog "Dismounting scratch VHDX..."
+ Dismount-VHD -Path $VhdxPath
+ WriteLog "Done."
+ }
+}
+
+function New-FFUVM {
+ #Create new Gen2 VM
+ $VM = New-VM -Name $VMName -Path $VMPath -MemoryStartupBytes $memory -VHDPath $VHDXPath -Generation 2
+ Set-VMProcessor -VMName $VMName -Count $processors
+
+ #Mount AppsISO
+ Add-VMDvdDrive -VMName $VMName -Path $AppsISO
+
+ #Set Hard Drive as boot device
+ $VMHardDiskDrive = Get-VMHarddiskdrive -VMName $VMName
+ Set-VMFirmware -VMName $VMName -FirstBootDevice $VMHardDiskDrive
+ Set-VM -Name $VMName -AutomaticCheckpointsEnabled $false -StaticMemory
+
+ #Configure TPM
+ New-HgsGuardian -Name $VMName -GenerateCertificates
+ $owner = get-hgsguardian -Name $VMName
+ $kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot
+ Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData
+ Enable-VMTPM -VMName $VMName
+
+ #Connect to VM
+ WriteLog "Starting vmconnect localhost $VMName"
+ & vmconnect localhost "$VMName"
+
+ #Start VM
+ Start-VM -Name $VMName
+
+ return $VM
+}
+
+Function Set-CaptureFFU {
+ $CaptureFFUScriptPath = "$FFUDevelopmentPath\WinPECaptureFFUFiles\CaptureFFU.ps1"
+
+ If (-not (Test-Path -Path $FFUCaptureLocation)) {
+ WriteLog "Creating FFU capture location at $FFUCaptureLocation"
+ New-Item -Path $FFUCaptureLocation -ItemType Directory -Force
+ WriteLog "Successfully created FFU capture location at $FFUCaptureLocation"
+ }
+
+ # Create a standard user
+ $UserExists = Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue
+ if (-not $UserExists) {
+ WriteLog "Creating FFU_User account as standard user"
+ New-LocalUser -Name $UserName -AccountNeverExpires -NoPassword | Out-null
+ WriteLog "Successfully created FFU_User account"
+ }
+
+ # Create a random password for the standard user
+ $Password = New-Guid | Select-Object -ExpandProperty Guid
+ $SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
+ Set-LocalUser -Name $UserName -Password $SecurePassword -PasswordNeverExpires:$true
+
+ # Create a share of the $FFUCaptureLocation variable
+ $ShareExists = Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue
+ if (-not $ShareExists) {
+ WriteLog "Creating $ShareName and giving access to $UserName"
+ New-SmbShare -Name $ShareName -Path $FFUCaptureLocation -FullAccess $UserName | Out-Null
+ WriteLog "Share created"
+ }
+
+ # Return the share path in the format of \\\ /user:
+ $SharePath = "\\$VMHostIPAddress\$ShareName /user:$UserName $Password"
+ $SharePath = "net use W: " + $SharePath
+
+ # Update CaptureFFU.ps1 script
+ if (Test-Path -Path $CaptureFFUScriptPath) {
+ $ScriptContent = Get-Content -Path $CaptureFFUScriptPath
+ $UpdatedContent = $ScriptContent -replace '(net use).*', ("$SharePath")
+ WriteLog 'Updating share command in CaptureFFU.ps1 script with new share information'
+ Set-Content -Path $CaptureFFUScriptPath -Value $UpdatedContent
+ WriteLog 'Update complete'
+ }
+ else {
+ throw "CaptureFFU.ps1 script not found at $CaptureFFUScriptPath"
+ }
+}
+
+function New-PEMedia {
+ param (
+ [Parameter()]
+ [bool]$Capture,
+ [Parameter()]
+ [bool]$Deploy
+ )
+ #Need to use the Demployment and Imaging tools environment to create winPE media
+ $DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
+ $WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
+
+ If (Test-path -Path "$WinPEFFUPath") {
+ WriteLog "Removing old WinPE path at $WinPEFFUPath"
+ Remove-Item -Path "$WinPEFFUPath" -Recurse -Force | out-null
+ }
+
+ WriteLog "Copying WinPE files to $WinPEFFUPath"
+ & cmd /c """$DandIEnv"" && copype amd64 $WinPEFFUPath" | Out-Null
+ #Invoke-Process cmd "/c ""$DandIEnv"" && copype amd64 $WinPEFFUPath"
+ WriteLog 'Files copied successfully'
+
+ WriteLog 'Mounting WinPE media to add WinPE optional components'
+ Mount-WindowsImage -ImagePath "$WinPEFFUPath\media\sources\boot.wim" -Index 1 -Path "$WinPEFFUPath\mount" | Out-Null
+ WriteLog 'Mounting complete'
+
+ $Packages = @(
+ "WinPE-WMI.cab",
+ "en-us\WinPE-WMI_en-us.cab",
+ "WinPE-NetFX.cab",
+ "en-us\WinPE-NetFX_en-us.cab",
+ "WinPE-Scripting.cab",
+ "en-us\WinPE-Scripting_en-us.cab",
+ "WinPE-PowerShell.cab",
+ "en-us\WinPE-PowerShell_en-us.cab",
+ "WinPE-StorageWMI.cab",
+ "en-us\WinPE-StorageWMI_en-us.cab",
+ "WinPE-DismCmdlets.cab",
+ "en-us\WinPE-DismCmdlets_en-us.cab"
+ )
+
+ $PackagePathBase = "$adkPath`Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\WinPE_OCs\"
+
+ foreach ($Package in $Packages) {
+ $PackagePath = Join-Path $PackagePathBase $Package
+ WriteLog "Adding Package $Package"
+ Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
+ WriteLog "Adding package complete"
+ }
+ If ($Capture) {
+ WriteLog "Copying $FFUDevelopmentPath\WinPECaptureFFUFiles\* to WinPE capture media"
+ Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | out-null
+ WriteLog "Copy complete"
+ #Remove Bootfix.bin - for BIOS systems, shouldn't be needed, but doesn't hurt to remove for our purposes
+ Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force | Out-null
+ $WinPEISOName = 'WinPE_FFU_Capture.iso'
+ $Capture = $false
+ }
+ If ($Deploy) {
+ WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
+ Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
+ WriteLog 'Copy complete'
+ # If you need to add drivers (storage/keyboard most likely), remove the '#' from the below line and change the /Driver:Path to a folder of drivers
+ # & dism /image:$WinPEFFUPath\mount /Add-Driver /Driver: /Recurse
+ $WinPEISOName = 'WinPE_FFU_Deploy.iso'
+ $Deploy = $false
+ }
+ WriteLog 'Dismounting WinPE media'
+ Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save | Out-Null
+ WriteLog 'Dismount complete'
+ #Make ISO
+ $OSCDIMGPath = "$adkPath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg"
+ $OSCDIMG = "$OSCDIMGPath\oscdimg.exe"
+ WriteLog "Creating WinPE ISO at $FFUDevelopmentPath\$WinPEISOName"
+ # & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\$WinPEISOName | Out-null
+ Invoke-Process $OSCDIMG "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$FFUDevelopmentPath\$WinPEISOName`""
+ WriteLog "ISO created successfully"
+ WriteLog "Cleaning up $WinPEFFUPath"
+ Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
+ WriteLog 'Cleanup complete'
+}
+function New-FFU {
+ param (
+ [Parameter(Mandatory = $false)]
+ [string]$VMName
+ )
+ #If $InstallApps = $true, configure the VM
+ If ($InstallApps) {
+ WriteLog 'Creating FFU from VM'
+ #Mount the Capture ISO to the VM
+ $CaptureISOPath = "$FFUDevelopmentPath\WinPE_FFU_Capture.iso"
+
+ WriteLog "Setting $CaptureISOPath as first boot device"
+ $VMDVDDrive = Get-VMDvdDrive -VMName $VMName
+ Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
+ Set-VMDvdDrive -VMName $VMName -Path $CaptureISOPath
+ $VMSwitch = Get-VMSwitch -name $VMSwitchName
+ WriteLog "Setting $($VMSwitch.Name) as VMSwitch"
+ get-vm $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $VMSwitch.Name
+ WriteLog "Configuring VM complete"
+
+ #Start VM
+ WriteLog "Starting VM"
+ Start-VM -Name $VMName
+
+ # Wait for the VM to turn off
+ do {
+ $FFUVM = Get-VM -Name $VMName
+ Start-Sleep -Seconds 5
+ } while ($FFUVM.State -ne 'Off')
+ WriteLog "VM Shutdown"
+ # Check for .ffu files in the FFUDevelopment folder
+ WriteLog "Checking for FFU Files"
+ $FFUFiles = Get-ChildItem -Path $FFUCaptureLocation -Filter "*.ffu" -File
+
+ # If there's more than one .ffu file, get the most recent and store its path in $FFUFile
+ if ($FFUFiles.Count -gt 0) {
+ WriteLog 'Getting the most recent FFU file'
+ $FFUFile = ($FFUFiles | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).FullName
+ WriteLog "Most recent .ffu file: $FFUFile"
+ }
+ else {
+ WriteLog "No .ffu files found in $FFUFolderPath"
+ throw $_
+ }
+ }
+ elseif (-not $InstallApps) {
+ #Get Windows Version Information from the VHDX
+ $winverinfo = Get-WindowsVersionInfo
+ $FFUFileName = "$($winverinfo.Name)`_$($winverinfo.DisplayVersion)`_$($winverinfo.SKU)`_$($winverinfo.BuildDate).ffu"
+ WriteLog "FFU file name: $FFUFileName"
+ $FFUFile = "$FFUCaptureLocation\$FFUFileName"
+ #Capture the FFU
+ WriteLog 'Capturing FFU from VHDX file'
+ Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($vhdxDisk.DiskNumber) /Name:$($winverinfo.Name)$($winverinfo.DisplayVersion)$($winverinfo.SKU) /Compress:Default"
+ WriteLog 'FFU Capture complete'
+ WriteLog 'Sleeping 60 seconds before dismount of VHDX'
+ Dismount-ScratchVhdx -VhdxPath $VHDXPath
+ }
+
+ #Add drivers
+ If ($InstallDrivers) {
+ WriteLog 'Adding drivers'
+ WriteLog "Creating $FFUDevelopmentPath\Mount directory"
+ New-Item -Path "$FFUDevelopmentPath\Mount" -ItemType Directory -Force | Out-Null
+ WriteLog "Created $FFUDevelopmentPath\Mount directory"
+ #Without this 120 second sleep, we sometimes see an error when mounting the FFU due to a file handle lock
+ WriteLog 'Sleeping 2 minutes to prevent file handle lock'
+ Start-Sleep 120
+ WriteLog "Mounting $FFUFile to $FFUDevelopmentPath\Mount"
+ Mount-WindowsImage -ImagePath $FFUFile -Index 1 -Path "$FFUDevelopmentPath\Mount" | Out-null
+ WriteLog 'Mounting complete'
+ WriteLog 'Adding drivers - This will take a few minutes, please be patient'
+ Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$FFUDevelopmentPath\Drivers" -Recurse | Out-null
+ WriteLog 'Adding drivers complete'
+ WriteLog "Dismount $FFUDevelopmentPath\Mount"
+ Dismount-WindowsImage -Path "$FFUDevelopmentPath\Mount" -Save | Out-Null
+ WriteLog 'Dismount complete'
+ WriteLog "Remove $FFUDevelopmentPath\Mount folder"
+ Remove-Item -Path "$FFUDevelopmentPath\Mount" -Recurse -Force | Out-null
+ WriteLog 'Folder removed'
+ }
+ #Optimize FFU
+ WriteLog 'Optimizing FFU - This will take a few minutes, please be patient'
+ Invoke-Process cmd "/c ""$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile"
+ WriteLog 'Optimizing FFU complete'
+
+}
+function Remove-FFUVM {
+ param (
+ [Parameter(Mandatory = $false)]
+ [string]$VMName
+ )
+ #Get the VM object and remove the VM, the HGSGuardian, and the certs
+ If ($VMName) {
+ $FFUVM = get-vm $VMName | Where-Object { $_.state -ne 'running' }
+ }
+ If ($null -ne $FFUVM) {
+ WriteLog 'Cleaning up VM'
+ $certPath = 'Cert:\LocalMachine\Shielded VM Local Certificates\'
+ $VMName = $FFUVM.Name
+ WriteLog "Removing VM: $VMName"
+ Remove-VM -Name $VMName -Force
+ WriteLog 'Removal complete'
+ WriteLog "Removing $VMPath"
+ Remove-Item -Path $VMPath -Force -Recurse
+ WriteLog 'Removal complete'
+ WriteLog "Removing HGSGuardian for $VMName"
+ Remove-HgsGuardian -Name $VMName -WarningAction SilentlyContinue
+ WriteLog 'Removal complete'
+ WriteLog 'Cleaning up HGS Guardian certs'
+ $certs = Get-ChildItem -Path $certPath -Recurse | Where-Object { $_.Subject -like "*$VMName*" }
+ foreach ($cert in $Certs) {
+ Remove-item -Path $cert.PSPath -force | Out-Null
+ }
+ WriteLog 'Cert removal complete'
+ }
+ #If just building the FFU from vhdx, remove the vhdx path
+ If (-not $InstallApps -and $vhdxDisk) {
+ WriteLog 'Cleaning up VHDX'
+ WriteLog "Removing $VMPath"
+ Remove-Item -Path $VMPath -Force -Recurse | Out-Null
+ WriteLog 'Removal complete'
+ }
+
+ #Remove orphaned mounted images
+ $mountedImages = Get-WindowsImage -Mounted
+ if ($mountedImages) {
+ foreach ($image in $mountedImages) {
+ $mountPath = $image.Path
+ WriteLog "Dismounting image at $mountPath"
+ Dismount-WindowsImage -Path $mountPath -discard
+ WriteLog "Successfully dismounted image at $mountPath"
+ }
+ }
+ #Remove Mount folder if it exists
+ If (Test-Path -Path $FFUDevelopmentPath\Mount) {
+ WriteLog "Remove $FFUDevelopmentPath\Mount folder"
+ Remove-Item -Path "$FFUDevelopmentPath\Mount" -Recurse -Force
+ WriteLog 'Folder removed'
+ }
+ #Remove unused mountpoints
+ WriteLog 'Remove unused mountpoints'
+ Invoke-Process cmd "/c mountvol /r"
+ WriteLog 'Removal complete'
+}
+Function Remove-FFUUserShare {
+ WriteLog "Removing $ShareName"
+ Remove-SmbShare -Name $ShareName -Force | Out-null
+ WriteLog 'Removal complete'
+ WriteLog "Removing $Username"
+ Remove-LocalUser -Name $Username | Out-Null
+ WriteLog 'Removal complete'
+}
+
+Function Get-WindowsVersionInfo {
+ WriteLog "Getting Windows Version info"
+ #Load Registry Hive
+ $Software = "$osPartitionDriveLetter`:\Windows\System32\config\software"
+ WriteLog "Loading Software registry hive"
+ Invoke-Process reg "load HKLM\FFU $Software"
+
+ #Find Windows version values
+ $SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID'
+ WriteLog "Windows SKU: $SKU"
+ [int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild'
+ WriteLog "Windows Build: $CurrentBuild"
+ $DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion'
+ WriteLog "Windows Version: $DisplayVersion"
+ $BuildDate = Get-Date -uformat %b%Y
+
+ $SKU = switch ($SKU) {
+ Core { 'Home' }
+ Professional { 'Pro' }
+ ProfessionalEducation { 'Pro_Edu' }
+ Enterprise { 'Ent' }
+ Education { 'Edu' }
+ ProfessionalWorkstation { 'Pro_Wks' }
+ }
+ WriteLog "Windows SKU Modified to: $SKU"
+
+ if ($CurrentBuild -ge 22000) {
+ $Name = 'Win11'
+ }
+ else {
+ $Name = 'Win10'
+ }
+
+ WriteLog "Unloading registry"
+ Invoke-Process reg "unload HKLM\FFU"
+
+ return @{
+
+ DisplayVersion = $DisplayVersion
+ BuildDate = $buildDate
+ Name = $Name
+ SKU = $SKU
+ }
+}
+Function New-DeploymentUSB {
+ param(
+ [switch]$CopyFFU
+ )
+ WriteLog "CopyFFU is set to $CopyFFU"
+ # Set your FFUDevelopmentPath here
+ $BuildUSBPath = $FFUDevelopmentPath
+
+ # Get the first removable USB drive
+ $USBDrive = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media'")
+
+ if ($null -eq $USBDrive) {
+ Writelog "No USB drive found"
+ exit 1
+ }
+
+ # Format the USB drive
+ $DiskNumber = $USBDrive.DeviceID.Replace("\\.\PHYSICALDRIVE", "")
+ $ScriptBlock = {
+ param($DiskNumber)
+ Clear-Disk -Number $DiskNumber -RemoveData -Confirm:$false
+ Clear-Disk -Number $DiskNumber -RemoveData -RemoveOEM -Confirm:$false
+ #Check for other partitions since, apparently, Clear-Disk doesn't remove all of them
+ Get-Disk $disknumber | Get-Partition | Remove-Partition -Confirm:$false
+ $Disk = Get-Disk -Number $DiskNumber
+ $Disk | Set-Disk -PartitionStyle MBR
+ $BootPartition = $Disk | New-Partition -Size 2GB -IsActive -AssignDriveLetter
+ $DeployPartition = $Disk | New-Partition -UseMaximumSize -AssignDriveLetter
+ Format-Volume -Partition $BootPartition -FileSystem FAT32 -NewFileSystemLabel "Boot" -Confirm:$false
+ Format-Volume -Partition $DeployPartition -FileSystem NTFS -NewFileSystemLabel "Deploy" -Confirm:$false
+ }
+ WriteLog 'Partitioning USB Drive'
+ Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $DiskNumber | Out-null
+ WriteLog 'Done'
+
+ # Mount the ISO and copy the contents to the boot partition
+ $BootPartitionDriveLetter = (get-volume -FileSystemLabel Boot).DriveLetter + ":\"
+ $ISOMountPoint = (Mount-DiskImage -ImagePath "$BuildUSBPath\WinPE_FFU_Deploy.iso" -PassThru | Get-Volume).DriveLetter + ":\"
+ WriteLog "Copying WinPE files to $BootPartitionDriveLetter"
+ Copy-Item -Path "$ISOMountPoint\*" -Destination $BootPartitionDriveLetter -Recurse -Force | Out-Null
+ Dismount-DiskImage -ImagePath "$BuildUSBPath\WinPE_FFU_Deploy.iso" | Out-Null
+
+ # Copy FFU files if switch is provided
+ if ($CopyFFU.IsPresent) {
+ WriteLog 'Copying FFU files'
+ $DeployPartitionDriveLetter = (get-volume -FileSystemLabel Deploy).DriveLetter + ":\"
+ $FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu"
+
+ if ($FFUFiles.Count -eq 1) {
+ WriteLog "Copying $($FFUFiles.FullName) to $DeployPartitionDriveLetter this could take a few minutes"
+ Copy-Item -Path $FFUFiles.FullName -Destination $DeployPartitionDriveLetter -Force | Out-Null
+ Writelog 'Copy complete'
+ }
+ elseif ($FFUFiles.Count -gt 1) {
+ WriteLog "Multiple FFU files found:"
+ Write-Host "Multiple FFU files found:"
+ for ($i = 0; $i -lt $FFUFiles.Count; $i++) {
+ WriteLog ("{0}: {1}" -f ($i + 1), $FFUFiles[$i].Name)
+ Write-Host ("{0}: {1}" -f ($i + 1), $FFUFiles[$i].Name)
+ }
+ WriteLog "A: Copy all FFU files"
+ Write-Host "A: Copy all FFU files"
+ $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') {
+ WriteLog "Copying All FFU files to $DeployPartitionDriveLetter this could take a few minutes"
+ Write-Host "Copying All FFU files to $DeployPartitionDriveLetter this could take a few minutes"
+ Copy-Item -Path $FFUFiles.FullName -Destination $DeployPartitionDriveLetter -Force | Out-Null
+ Writelog 'Copy complete'
+ Write-Host 'Copy complete'
+ }
+ elseif ($inputChoice -ge 1 -and $inputChoice -le $FFUFiles.Count) {
+ $selectedIndex = $inputChoice - 1
+ WriteLog "Copying $($FFUFiles[$selectedIndex].FullName) to $DeployPartitionDriveLetter this could take a few minutes"
+ Write-Host "Copying $($FFUFiles[$selectedIndex].FullName) to $DeployPartitionDriveLetter this could take a few minutes"
+ Copy-Item -Path $FFUFiles[$selectedIndex].FullName -Destination $DeployPartitionDriveLetter -Force | Out-Null
+ Writelog 'Copy complete'
+ Write-Host 'Copy complete'
+ }
+ else {
+ WriteLog "Invalid choice. No FFU file copied"
+ Write-Host 'Invalid choice. No FFU file copied'
+ }
+ }
+ else {
+ WriteLog "No FFU files found in the current directory."
+ }
+ }
+
+ WriteLog "USB drive prepared successfully."
+}
+
+###END FUNCTIONS
+
+#Remove old log file if found
+if (Test-Path -Path $Logfile) {
+ Remove-item -Path $LogFile -Force
+}
+Write-Host "FFU build process has begun. 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"
+
+WriteLog 'Begin Logging'
+#Get script variable values
+LogVariableValues
+
+#Get Windows ADK
+try {
+ $adkPath = Get-ADK
+ #Need to use the Deployment and Imaging tools environment to use dism from the Insider ADK to optimize the FFU. This is only needed until Windows 23H2
+ $DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
+}
+catch {
+ WriteLog 'ADK not found'
+ throw $_
+}
+
+#Create apps ISO for Office and/or 3rd party apps
+if ($InstallApps) {
+ try {
+ #Make sure InstallAppsandSysprep.cmd file exists
+ WriteLog "InstallApps variable set to true, verifying $AppsPath\InstallAppsandSysprep.cmd exists"
+ if (-not (Test-Path -Path "$AppsPath\InstallAppsandSysprep.cmd")) {
+ Write-Host "$AppsPath\InstallAppsandSysprep.cmd is missing, exiting script"
+ WriteLog "$AppsPath\InstallAppsandSysprep.cmd is missing, exiting script"
+ exit
+ }
+ WriteLog "$AppsPath\InstallAppsandSysprep.cmd found"
+
+ if (-not $InstallOffice) {
+ #Modify InstallAppsandSysprep.cmd to REM out the office install command
+ $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd"
+ $UpdatedcmdContent = $cmdContent -replace '^(d:\\Office\\setup.exe /configure d:\\office\\DeployFFU.xml)', ("REM d:\Office\setup.exe /configure d:\office\DeployFFU.xml")
+ Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent
+ }
+
+ if ($InstallOffice) {
+ WriteLog 'Downloading M365 Apps/Office'
+ Get-Office
+ WriteLog 'Downloading M365 Apps/Office completed successfully'
+ }
+
+ #Create Apps ISO
+ WriteLog "Creating $AppsISO file"
+ New-AppsISO
+ WriteLog "$AppsISO created successfully"
+ }
+ catch {
+ Write-Host "Creating Apps ISO Failed"
+ WriteLog "Creating Apps ISO Failed with error $_"
+ throw $_
+ }
+}
+
+#Create VHDX
+try {
+ $wimPath = Get-WimFromISO
+
+ $WimIndex = Get-WimIndex -WindowsSKU $WindowsSKU
+
+ $vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -Dynamic
+
+ $systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk
+
+ New-MSRPartition -VhdxDisk $vhdxDisk
+
+ $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $WimIndex
+ $osPartitionDriveLetter = $osPartition[1].DriveLetter
+ $WindowsPartition = $osPartitionDriveLetter + ":\"
+
+ $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
+
+ WriteLog "All necessary partitions created."
+
+ Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1]
+
+ #Enable Windows Optional Features (e.g. .Net3, etc)
+ If ($OptionalFeatures) {
+ $Source = Join-Path (Split-Path $wimpath) "sxs"
+ Enable-WindowsFeaturesByName -FeatureNames $OptionalFeatures -Source $Source
+ }
+
+ #Set Product key
+ If ($ProductKey) {
+ WriteLog "Setting Windows Product Key"
+ Set-WindowsProductKey -Path $WindowsPartition -ProductKey $ProductKey
+ }
+
+ WriteLog 'Dismounting Windows ISO'
+ Dismount-DiskImage -ImagePath $ISOPath | Out-null
+ WriteLog 'Done'
+
+ If ($InstallApps) {
+ #Copy Unattend file so VM Boots into Audit Mode
+ WriteLog 'Copying unattend file to boot to audit mode'
+ New-Item -Path "$($osPartitionDriveLetter):\Windows\Panther\unattend" -ItemType Directory | Out-Null
+ Copy-Item -Path "$FFUDevelopmentPath\BuildFFUUnattend\unattend.xml" -Destination "$($osPartitionDriveLetter):\Windows\Panther\Unattend\Unattend.xml" -Force | Out-Null
+ WriteLog 'Copy completed'
+ Dismount-ScratchVhdx -VhdxPath $VHDXPath
+ }
+}
+catch {
+ Write-Host 'Creating VHDX Failed'
+ WriteLog "Creating VHDX Failed with error $_"
+ WriteLog "Dismounting $VHDXPath"
+ Dismount-ScratchVhdx -VhdxPath $VHDXPath
+ WriteLog "Removing $VMPath"
+ Remove-Item -Path $VMPath -Force -Recurse | Out-Null
+ WriteLog 'Removal complete'
+ WriteLog 'Dismounting Windows ISO'
+ Dismount-DiskImage -ImagePath $ISOPath | Out-null
+ WriteLog 'Dismounting complete'
+ throw $_
+
+}
+
+#If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file
+if ($InstallApps) {
+ #Create VM and attach VHDX
+ try {
+ WriteLog 'Creating new FFU VM'
+ $FFUVM = New-FFUVM
+ WriteLog 'FFU VM Created'
+ }
+ catch {
+ Write-Host 'VM creation failed'
+ Writelog "VM creation failed with error $_"
+ Remove-FFUVM -VMName $VMName
+ throw $_
+
+ }
+ #Create ffu user and share to capture FFU to
+ try {
+ Set-CaptureFFU
+ }
+ catch {
+ Write-Host 'Set-CaptureFFU function failed'
+ WriteLog "Set-CaptureFFU function failed with error $_"
+ Remove-FFUVM -VMName $VMName
+ throw $_
+
+ }
+ If ($CreateCaptureMedia) {
+ #Create Capture Media
+ try {
+ #This should happen while the FFUVM is building
+ New-PEMedia -Capture $true
+ }
+ catch {
+ Write-Host 'Creating capture media failed'
+ WriteLog "Creating capture media failed with error $_"
+ Remove-FFUVM -VMName $VMName
+ throw $_
+
+ }
+ }
+}
+#Capture FFU file
+try {
+ #Check for FFU Folder and create it if it's missing
+ If (-not (Test-Path -Path $FFUCaptureLocation)) {
+ WriteLog "Creating FFU capture location at $FFUCaptureLocation"
+ New-Item -Path $FFUCaptureLocation -ItemType Directory -Force
+ WriteLog "Successfully created FFU capture location at $FFUCaptureLocation"
+ }
+ #Check if VM is done provisioning
+ If ($InstallApps) {
+ do {
+ $FFUVM = Get-VM -Name $FFUVM.Name
+ Start-Sleep -Seconds 10
+ WriteLog 'Waiting for VM to shutdown'
+ } while ($FFUVM.State -ne 'Off')
+ WriteLog 'VM Shutdown'
+ #Capture FFU file
+ New-FFU $FFUVM.Name
+ }
+ else {
+ New-FFU
+ }
+}
+Catch {
+ Write-Host 'Capturing FFU file failed'
+ Writelog "Capturing FFU file failed with error $_"
+ If ($InstallApps) {
+ Remove-FFUVM -VMName $VMName
+ }
+ else {
+ Remove-FFUVM
+ }
+
+ throw $_
+
+}
+#Clean up ffu_user and Share
+If ($InstallApps) {
+ try {
+ Remove-FFUUserShare
+ }
+ catch {
+ Write-Host 'Cleaning up FFU User and/or share failed'
+ WriteLog "Cleaning up FFU User and/or share failed with error $_"
+ Remove-FFUVM -VMName $VMName
+ throw $_
+ }
+}
+#Clean up VM or VHDX
+try {
+ Remove-FFUVM
+ WriteLog 'FFU build complete!'
+}
+catch {
+ Write-Host 'VM or vhdx cleanup failed'
+ Writelog "VM or vhdx cleanup failed with error $_"
+ throw $_
+}
+#Create Deployment Media
+If ($CreateDeploymentMedia) {
+ try {
+ New-PEMedia -Deploy $true
+ }
+ catch {
+ Write-Host 'Creating deployment media failed'
+ WriteLog "Creating deployment media failed with error $_"
+ throw $_
+
+ }
+}
+If($BuildUSBDrive){
+ try{
+ If(Test-Path -Path "$FFUDevelopmentPath\WinPE_FFU_Deploy.iso"){
+ New-DeploymentUSB -CopyFFU
+ }
+ else{
+ WriteLog "$BuildUSBDrive set to true, however unable to find WinPE_FFU_Deploy.iso. USB drive not built."
+ }
+
+ }
+ catch{
+ Write-Host 'Building USB deployment drive failed'
+ Writelog "Building USB deployment drive failed with error $_"
+ throw $_
+ }
+}
+Write-Host "Script complete"
+WriteLog "Script complete"
\ No newline at end of file
diff --git a/FFUDevelopment/Docs/BuildDeployFFU.docx b/FFUDevelopment/Docs/BuildDeployFFU.docx
index f951565..7cc7f83 100644
Binary files a/FFUDevelopment/Docs/BuildDeployFFU.docx and b/FFUDevelopment/Docs/BuildDeployFFU.docx differ
diff --git a/FFUDevelopment/Office/InstallOfficeandSysprep.cmd b/FFUDevelopment/Office/InstallOfficeandSysprep.cmd
deleted file mode 100644
index d13072b..0000000
--- a/FFUDevelopment/Office/InstallOfficeandSysprep.cmd
+++ /dev/null
@@ -1,4 +0,0 @@
-d:\setup.exe /configure d:\DeployFFU.xml
-taskkill /IM sysprep.exe
-timeout /t 5
-c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
\ No newline at end of file
diff --git a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1
index 135e6cd..dba0c81 100644
--- a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1
+++ b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1
@@ -1,5 +1,5 @@
-#Modify the net use path to map the W: drive to the location you want to copy the FFU file to
-net use W: \\192.168.1.2\c$\FFUDevelopment /user:administrator p@ssw0rd
+#Modify the net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user 62bffb7c-4350-426c-8151-58093bb90117
+net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user 62bffb7c-4350-426c-8151-58093bb90117
$AssignDriveLetter = 'x:\AssignDriveLetter.txt'
Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null
@@ -14,11 +14,13 @@ $SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersio
$DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion'
$BuildDate = Get-Date -uformat %b%Y
-$SKU = switch ($SKU){
- Home {'Home'}
- Professional {'Pro'}
- ProfessionalEducation {'Pro_Edu'}
- Enterprise {'Ent'}
+$SKU = switch ($SKU) {
+ Core { 'Home' }
+ Professional { 'Pro' }
+ ProfessionalEducation { 'Pro_Edu' }
+ Enterprise { 'Ent' }
+ Education { 'Edu' }
+ ProfessionalWorkstation { 'Pro_Wks' }
}
if($CurrentBuild -ge 22000){
@@ -30,7 +32,7 @@ else{
#If Office is installed, modify the file name of the FFU
#$Office = Get-childitem -Path 'M:\Program Files\Microsoft Office' -ErrorAction SilentlyContinue | Out-Null
-$Office = Get-childitem -Path 'M:\Program Files\Microsoft Office'
+$Office = Get-childitem -Path 'M:\Program Files\Microsoft Office' -ErrorAction SilentlyContinue
if($Office){
$ffuFilePath = "W:\$Name`_$DisplayVersion`_$SKU`_Office`_$BuildDate.ffu"
$dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$Name$DisplayVersion$SKU /Compress:Default"
@@ -38,7 +40,7 @@ if($Office){
}
else{
- $ffuFilePath = "W:\$Name`_$DisplayVersion`_$SKU`_$BuildDate.ffu"
+ $ffuFilePath = "W:\$Name`_$DisplayVersion`_$SKU`_Apps`_$BuildDate.ffu"
$dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$Name$DisplayVersion$SKU /Compress:Default"
}
@@ -54,4 +56,7 @@ reg unload "HKLM\FFU"
Start-Process -FilePath dism.exe -ArgumentList $dismArgs -Wait -PassThru -ErrorAction Stop | Out-Null
#Copy DISM log to Host
xcopy X:\Windows\logs\dism\dism.log W:\ /Y | Out-Null
+
+#Remvove W: drive
+net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user 62bffb7c-4350-426c-8151-58093bb90117
wpeutil Shutdown
diff --git a/README.md b/README.md
index 7ad46c3..8d8b3ee 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ This process will copy Windows in about 2-3 minutes to the target device, option
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.
# Updates
+2023-05-22
+- Automated most of the process
2023-03-03
- Added script convert-wimToFFU.ps1 which will convert any WIM file to a FFU. If you don't want to capture apps in your FFU, this is a much quicker way to get a FFU file to deploy. Check the ConvertWimToFFU.docx file for more info.
diff --git a/archive/FFUDevelopment/Apps/InstallAppsandSysprep.cmd b/archive/FFUDevelopment/Apps/InstallAppsandSysprep.cmd
new file mode 100644
index 0000000..e3533f1
--- /dev/null
+++ b/archive/FFUDevelopment/Apps/InstallAppsandSysprep.cmd
@@ -0,0 +1,13 @@
+REM Put each app install on a separate line
+REM M365 Apps/Office ProPlus
+d:\Office\setup.exe /configure d:\office\DeployFFU.xml
+REM Add additional apps below here
+
+
+REM The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time.
+REM Also kills the sysprep process in order to automate sysprep generalize
+del c:\windows\panther\unattend\unattend.xml /F /Q
+del c:\windows\panther\unattend.xml /F /Q
+taskkill /IM sysprep.exe
+timeout /t 5
+c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
\ No newline at end of file
diff --git a/FFUDevelopment/Office/DeployFFU.xml b/archive/FFUDevelopment/Apps/Office/DeployFFU.xml
similarity index 100%
rename from FFUDevelopment/Office/DeployFFU.xml
rename to archive/FFUDevelopment/Apps/Office/DeployFFU.xml
diff --git a/archive/FFUDevelopment/Apps/Office/DownloadFFU.xml b/archive/FFUDevelopment/Apps/Office/DownloadFFU.xml
new file mode 100644
index 0000000..9054148
--- /dev/null
+++ b/archive/FFUDevelopment/Apps/Office/DownloadFFU.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/archive/FFUDevelopment/BuildFFUUnattend/unattend.xml b/archive/FFUDevelopment/BuildFFUUnattend/unattend.xml
new file mode 100644
index 0000000..9590b1e
--- /dev/null
+++ b/archive/FFUDevelopment/BuildFFUUnattend/unattend.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ 1
+ d:\InstallAppsandSysprep.cmd
+
+
+
+
+
+
+
+ Audit
+
+
+
+
+
diff --git a/archive/FFUDevelopment/BuildFFUVM.ps1 b/archive/FFUDevelopment/BuildFFUVM.ps1
new file mode 100644
index 0000000..025528c
--- /dev/null
+++ b/archive/FFUDevelopment/BuildFFUVM.ps1
@@ -0,0 +1,60 @@
+#Modify variables
+$VMPath = "c:\VM\$VMName"
+$ISOPath = "E:\software\ISOs\Windows\Windows 11\en-us_windows_11_consumer_editions_version_22h2_updated_feb_2023_x64_dvd_4fa87138.iso"
+$memory = 8GB
+$disksize = 30GB
+$processors = 4
+$rand = get-random
+$VMName = "_FFU-$rand"
+$VHDPath = "$VMPath\$VMName.vhdx"
+
+# 0. Delete old VMs and remove old certs
+
+$certPath = 'Cert:\LocalMachine\Shielded VM Local Certificates\'
+$vms = get-vm _ffu-* | ? {$_.state -ne 'running'}
+
+If($null -ne $vms){
+ Foreach ($vm in $vms){
+ $OldVMName = $vm.VMName
+ Remove-VM -Name $vm.name -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path "C:\VM\$OldVMName" -Force -Recurse -ErrorAction SilentlyContinue
+ Remove-HgsGuardian -Name $OldVMName
+ $certs = Get-ChildItem -Path $certPath -Recurse | Where-Object { $_.Subject -like "*$OldVMName*" }
+ foreach ($cert in $Certs){
+ Remove-item -Path $cert.PSPath -force
+ }
+ }
+}
+
+# 1. Create Dynamic Hard Disk
+mkdir -Path $VMPath -Force
+New-VHD -Path $VHDPath -Fixed -SizeBytes $disksize
+#New-VHD -path $VHDPath -SizeBytes 128000000000 -LogicalSectorSizeBytes 512 -Dynamic
+
+# 2. Create VM
+# - Name: _FFU
+# - Location: C:\VM\_FFU
+# - Generation: 2
+# - Memory: 4GB static
+# - Networking: Not connected
+# - Connect to existing VHD: c:\VM\_FFU\
+# - Mount ISO
+$VM = New-VM -Name $VMName -Path $VMPath -MemoryStartupBytes $memory -VHDPath $VHDPath -Generation 2
+Set-VMProcessor -VMName $VMName -Count $processors
+Add-VMDvdDrive -VMName $VMName -Path $ISOPath
+$VMDVDDrive = Get-VMDvdDrive -VMName $VMName
+Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
+
+#Configure TPM
+New-HgsGuardian -Name $VMName -GenerateCertificates
+$owner = get-hgsguardian -Name $VMName
+$kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot
+Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData
+Enable-VMTPM -VMName $VMName
+
+#Connect to VM
+vmconnect $VM.ComputerName $VMName
+
+#Use if creating for MDT/SCCM Builds
+#Get-VM $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName "Intel(R) I350 Gigabit Network Connection - Virtual Switch"
+#$networkAdapter = Get-VMNetworkAdapter -VMName $VMName
\ No newline at end of file
diff --git a/archive/FFUDevelopment/BuildFFUVMv2.ps1 b/archive/FFUDevelopment/BuildFFUVMv2.ps1
new file mode 100644
index 0000000..5337759
--- /dev/null
+++ b/archive/FFUDevelopment/BuildFFUVMv2.ps1
@@ -0,0 +1,563 @@
+#Requires -Modules Hyper-V, Storage
+#Requires -PSEdition Desktop
+
+# To do list (will get around to this stuff sometime next week)
+
+# Change from using variables to parameters - this way you don't even need to open the script to edit it.
+# Clean up the output it sends to powershell so you know what step you're on
+# Do some real logging incase folks get in trouble and need my help
+# Edit the WinPECaptureFFUFiles\CaptureFFU.ps1 file within this script instead of doing it manually.
+# Make Capture/Deploy media creation optional
+# Change DownloadFFU.xml file to match C:\FFUDevelopment path
+# If drivers = true, check if drivers folder exists
+# C:\VM\VMFolder isn't being deleted
+
+#Modify Required variables
+$ISOPath = "E:\software\ISOs\Windows\Windows 11\en-us_windows_11_consumer_editions_version_22h2_updated_feb_2023_x64_dvd_4fa87138.iso"
+$WindowsSKU = 'Pro'
+$FFUDevelopmentPath = 'C:\FFUDevelopment'
+$AppsISO = "$FFUDevelopmentPath\Apps.iso"
+$AppsPath = "$FFUDevelopmentPath\Apps"
+$InstallOffice = $true
+$InstallApps = $true
+$InstallDrivers = $true
+$memory = 8GB
+$disksize = 30GB
+$processors = 4
+$VMSwitchName = '*intel*'
+
+#Optional variables
+$rand = get-random
+$VMName = "_FFU-$rand"
+$VMLocation = "c:\VM"
+$VMPath = $VMLocation + $VMName
+$VHDXPath = "$VMPath\$VMName.vhdx"
+
+#FUNCTIONS
+Function Get-ADK {
+ # Define the registry key and value name to query
+ $adkRegKey = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots"
+ $adkRegValueName = "KitsRoot10"
+
+ # Check if the registry key exists
+ if (Test-Path $adkRegKey) {
+ # Get the registry value for the Windows ADK installation path
+ $adkPath = (Get-ItemProperty -Path $adkRegKey -Name $adkRegValueName).$adkRegValueName
+
+ if ($adkPath) {
+ return $adkPath
+ }
+ }
+ else {
+ throw "Windows ADK is not installed or the installation path could not be found."
+ }
+}
+function Get-ODTURL {
+
+ [String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
+
+ $MSWebPage | ForEach-Object {
+ if ($_ -match 'url=(https://.*officedeploymenttool.*\.exe)') {
+ $matches[1]
+ }
+ }
+}
+
+function Get-Office {
+ #Download ODT
+ $ODTUrl = Get-ODTURL
+ $ODTInstallFile = "$env:TEMP\odtsetup.exe"
+ Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile
+
+ # Extract ODT
+ $ODTPath = "$AppsPath\Office"
+ Start-Process -FilePath $ODTInstallFile -ArgumentList "/extract:$ODTPath /quiet" -Wait
+
+ # Run setup.exe with config.xml
+ $ConfigXml = "$ODTPath\DownloadFFU.xml"
+ #Set-Location $ODTPath
+ Start-Process -FilePath "$ODTPath\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
+
+ #Clean up default configuration files
+ Remove-Item -Path "$ODTPath\configuration*" -Force
+}
+
+function New-AppsISO {
+ #Create Apps ISO file
+ $OSCDIMG = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe'
+ Start-Process -FilePath $OSCDIMG -ArgumentList "-n -m -d $Appspath $AppsISO" -wait
+
+ #Remove the Office Download and ODT
+ if ($InstallOffice) {
+ $ODTPath = "$AppsPath\Office"
+ $OfficeDownloadPath = "$ODTPath\Office"
+ Remove-Item -Path $OfficeDownloadPath -Recurse -Force
+ Remove-Item -Path "$ODTPath\setup.exe"
+ }
+
+}
+
+
+
+
+function Get-WimFromISO {
+ # Mount the ISO file using Mount-DiskImage cmdlet
+ $mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
+
+ # Get the drive letter of the mounted ISO
+ $driveLetter = ($mountResult | Get-Volume).DriveLetter
+
+ # Construct the path to the install.wim file
+ $wimPath = $driveLetter + ":\sources\install.wim"
+
+ # Display the path to the install.wim file
+ Write-Host "The path to the install.wim file is: $wimPath"
+
+ return $wimpath
+
+}
+
+function Get-WimIndex {
+ [Parameter(Mandatory = $true)]
+ [string]$WindowsSKU
+
+ $wimindex = switch ($WindowsSKU) {
+ Home { 1 }
+ Home_N { 2 }
+ Home_SL { 3 }
+ EDU { 4 }
+ EDU_N { 5 }
+ Pro { 6 }
+ Pro_N { 7 }
+ Pro_EDU { 8 }
+ Pro_Edu_N { 9 }
+ Pro_WKS { 10 }
+ Pro_WKS_N { 11 }
+ Default { 6 }
+ }
+ Return $WimIndex
+}
+
+#Build VHDX
+#Create VHDX
+function New-ScratchVhdx {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$VhdxPath,
+ [uint64]$SizeBytes = 30GB,
+ [ValidateSet(512, 4096)]
+ [uint32]$LogicalSectorSizeBytes = 512,
+ [switch]$Dynamic,
+ [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT
+ )
+
+ Write-Host "Creating new Scratch VHDX..."
+
+ $newVHDX = New-VHD -Path $VhdxPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes -Fixed:$true
+ $toReturn = $newVHDX | Mount-VHD -Passthru | Initialize-Disk -PassThru -PartitionStyle GPT
+
+ #Remove auto-created system partition so we can create the correct partition layout
+ remove-partition $toreturn.DiskNumber -PartitionNumber 1 -Confirm:$False
+
+ Write-Host "Done."
+ return $toReturn
+}
+#Add System Partition
+function New-SystemPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [uint64]$SystemPartitionSize = 256MB
+ )
+
+ Write-Host "Creating System partition..."
+
+ $sysPartition = $VhdxDisk | New-Partition -AssignDriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
+ $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"
+
+ Write-Host "Done. System partition at drive $($sysPartition.DriveLetter):"
+ return $sysPartition.DriveLetter
+}
+#Add MSRPartition - skip this initially unless needed
+function New-MSRPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk
+ )
+
+ Write-Host "Creating MSR partition..."
+
+ $toReturn = $VhdxDisk | New-Partition -AssignDriveLetter -Size 16MB -GptType "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" -IsHidden
+
+ Write-Host "Done."
+
+ return $toReturn
+}
+#Add OS Partition
+function New-OSPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [Parameter(Mandatory = $true)]
+ [string]$WimPath,
+ [uint32]$WimIndex,
+ [uint64]$OSPartitionSize = 0
+ )
+
+ Write-Host "Creating OS partition..."
+
+ if ($OSPartitionSize -gt 0) {
+ $osPartition = $vhdxDisk | New-Partition -AssignDriveLetter -Size $OSPartitionSize
+ }
+ else {
+ $osPartition = $vhdxDisk | New-Partition -AssignDriveLetter -UseMaximumSize
+ }
+
+ $osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"
+ Write-Host "Done. OS partition at drive $($osPartition.DriveLetter):"
+
+ Write-Host "Writing WIM at $WimPath to OS partition at drive $($osPartition.DriveLetter):..."
+
+ #Server 2019 is missing the Windows Overlay Filter (wof.sys), likely other Server SKUs are missing it as well. Script will error if trying to use the -compact switch on Server OSes
+ if ((Get-CimInstance Win32_OperatingSystem).Caption -match "Server") {
+ Write-Host (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\")
+ }
+ else {
+ Write-Host (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\" -Compact)
+ }
+
+ Write-Host "Done."
+
+ return $osPartition
+}
+
+#Add Recovery partition
+function New-RecoveryPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$VhdxDisk,
+ [Parameter(Mandatory = $true)]
+ $OsPartition,
+ [uint64]$RecoveryPartitionSize = 0,
+ [ciminstance]$DataPartition
+ )
+
+ Write-Host "Creating empty Recovery partition (to be filled on first boot automatically)..."
+
+ $calculatedRecoverySize = 0
+ $recoveryPartition = $null
+
+ if ($RecoveryPartitionSize -gt 0) {
+ $calculatedRecoverySize = $RecoveryPartitionSize
+ }
+ else {
+ $winReWim = Get-ChildItem "$($OsPartition.DriveLetter):\Windows\System32\Recovery\Winre.wim"
+
+ if (($null -ne $winReWim) -and ($winReWim.Count -eq 1)) {
+ # Wim size + 52MB is minimum WinRE partition size.
+ # NTFS and other partitioning size differences account for about 17MB of space that's unavailable.
+ # Adding 32MB as a buffer to ensure there's enough space.
+ $calculatedRecoverySize = $winReWim.Length + 52MB + 32MB
+
+ Write-Host "Calculated space needed for recovery in bytes: $calculatedRecoverySize"
+
+ if ($null -ne $DataPartition) {
+ $DataPartition | Resize-Partition -Size ($DataPartition.Size - $calculatedRecoverySize)
+ Write-Host "Data partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
+ }
+ else {
+ $OsPartition | Resize-Partition -Size ($OsPartition.Size - $calculatedRecoverySize)
+ Write-Host "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."
+ }
+
+ $recoveryPartition = $VhdxDisk | New-Partition -AssignDriveLetter -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" `
+ | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "WinRE"
+
+ Write-Host "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"
+ }
+ else {
+ Write-Host "No WinRE.WIM found in the OS partition under \Windows\System32\Recovery."
+ Write-Host "Skipping creating the Recovery partition."
+ Write-Host "If a Recovery partition is desired, please re-run the script setting the -RecoveryPartitionSize flag as appropriate."
+ }
+ }
+
+ return $recoveryPartition
+}
+#Add boot files
+function Add-BootFiles {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$OsPartitionDriveLetter,
+ [Parameter(Mandatory = $true)]
+ [string]$SystemPartitionDriveLetter,
+ [string]$FirmwareType = 'UEFI'
+ )
+
+ Write-Host "Adding boot files for `"$($OsPartitionDriveLetter):\Windows`" to System partition `"$($SystemPartitionDriveLetter):`"..."
+
+ bcdboot "$($OsPartitionDriveLetter):\Windows" /S "$($SystemPartitionDriveLetter):" /F "$FirmwareType"
+
+ Write-Host "Done."
+}
+
+#Dismount VHDX
+function Dismount-ScratchVhdx {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$VhdxPath
+ )
+
+ if (Test-Path $VhdxPath) {
+ Write-Host "Dismounting scratch VHDX..."
+ Dismount-VHD -Path $VhdxPath
+ Write-Host "Done."
+ }
+}
+
+#Delete old VMs and remove old certs
+function Remove-FFUVM {
+ $certPath = 'Cert:\LocalMachine\Shielded VM Local Certificates\'
+ $OLDFFUVMs = get-vm _ffu-* | Where-Object { $_.state -ne 'running' }
+
+ If ($null -ne $OLDFFUVMs) {
+ Foreach ($OLDFFUVM in $OLDFFUVMs) {
+ $OldVMName = $OLDFFUVM.VMName
+ Remove-VM -Name $OLDFFUVM.name -Force -ErrorAction SilentlyContinue
+ #Remove-Item -Path "C:\VM\$OldVMName" -Force -Recurse -ErrorAction SilentlyContinue
+ Remove-Item -Path "$VMLocation\$OLDVMName" -Force -Recurse -ErrorAction SilentlyContinue
+ Remove-HgsGuardian -Name $OldVMName
+ $certs = Get-ChildItem -Path $certPath -Recurse | Where-Object { $_.Subject -like "*$OldVMName*" }
+ foreach ($cert in $Certs) {
+ Remove-item -Path $cert.PSPath -force
+ }
+ }
+ }
+}
+
+function New-FFUVM {
+ #Create new Gen2 VM
+ $VM = New-VM -Name $VMName -Path $VMPath -MemoryStartupBytes $memory -VHDPath $VHDXPath -Generation 2
+ Set-VMProcessor -VMName $VMName -Count $processors
+ #Mount Office ISO
+ Add-VMDvdDrive -VMName $VMName -Path $AppsISO
+ $VMHardDiskDrive = Get-VMHarddiskdrive -VMName $VMName
+ Set-VMFirmware -VMName $VMName -FirstBootDevice $VMHardDiskDrive
+ Set-VM -Name $VMName -AutomaticCheckpointsEnabled $false -StaticMemory
+
+ #Configure TPM
+ New-HgsGuardian -Name $VMName -GenerateCertificates
+ $owner = get-hgsguardian -Name $VMName
+ $kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot
+ Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData
+ Enable-VMTPM -VMName $VMName
+
+ #Connect to VM
+ vmconnect $VM.ComputerName $VMName
+
+ #Start VM
+ Start-VM -Name $VMName
+ return $VM
+}
+function New-PEMedia {
+ param (
+ [Parameter()]
+ [switch]
+ $Capture,
+ [Parameter()]
+ [switch]
+ $Deploy
+ )
+ #Need to use the Demployment and Imaging tools environment to create winPE media
+ $DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
+ $WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
+
+ If (Test-path -Path "$WinPEFFUPath") {
+ #Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -discard
+ Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
+ }
+
+ & cmd /c """$DandIEnv"" && copype amd64 $WinPEFFUPath"
+ Mount-WindowsImage -ImagePath "$WinPEFFUPath\media\sources\boot.wim" -Index 1 -Path "$WinPEFFUPath\mount"
+
+ $Packages = @(
+ "WinPE-WMI.cab",
+ "en-us\WinPE-WMI_en-us.cab",
+ "WinPE-NetFX.cab",
+ "en-us\WinPE-NetFX_en-us.cab",
+ "WinPE-Scripting.cab",
+ "en-us\WinPE-Scripting_en-us.cab",
+ "WinPE-PowerShell.cab",
+ "en-us\WinPE-PowerShell_en-us.cab",
+ "WinPE-StorageWMI.cab",
+ "en-us\WinPE-StorageWMI_en-us.cab",
+ "WinPE-DismCmdlets.cab",
+ "en-us\WinPE-DismCmdlets_en-us.cab"
+ )
+
+ $PackagePathBase = "$adkPath`Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\WinPE_OCs\"
+
+ foreach ($Package in $Packages) {
+ $PackagePath = Join-Path $PackagePathBase $Package
+ Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
+ }
+ If($Capture){
+ Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force
+ #Remove Bootfix.bin if capturing from BIOS systems
+ Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force
+ }
+ If($Deploy){
+ Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force
+ # If you need to add drivers (storage/keyboard most likely), remove the '#' from the below line and change the /Driver:Path to a folder of drivers
+ # & dism /image:$WinPEFFUPath\mount /Add-Driver /Driver: /Recurse
+ }
+ Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save
+ #Make ISO
+ $OSCDIMGPath = "$adkPath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg"
+ $OSCDIMG = "$OSCDIMGPath\oscdimg.exe"
+ & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\WinPE_FFU_Capture.iso
+ Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
+}
+function New-FFU {
+ #Need to use the Demployment and Imaging tools environment to use dism from the Insider ADK to optimize the FFU. This is only needed until Windows 23H2.
+ $DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
+ #Mount the Capture ISO to the VM
+
+ $CaptureISOPath = "$FFUDevelopmentPath\WinPE_FFU_Capture.iso"
+ $FFUVMs = get-vm _ffu-* | Where-Object { $_.state -ne 'running' }
+
+ If ($null -ne $FFUVMs) {
+ Foreach ($FFUVM in $FFUVMs) {
+ $VMName = $FFUVM.name
+ $VMDVDDrive = Get-VMDvdDrive -VMName $VMName
+ Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
+ Set-VMDvdDrive -VMName $VMName -Path $CaptureISOPath
+ $VMSwitch = Get-VMSwitch -name $VMSwitchName
+ get-vm $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $VMSwitch.Name
+ #vmconnect $FFUVM.ComputerName $VMName
+ }
+ }
+ #Start VM
+ Start-VM -Name $VMName
+
+ # Wait for the VM to turn off
+ do {
+ $FFUVM = Get-VM -Name $VMName
+ Start-Sleep -Seconds 5
+ } while ($FFUVM.State -ne 'Off')
+
+ # Check for .ffu files in the FFUDevelopment folder
+ $FFUFiles = Get-ChildItem -Path $FFUDevelopmentPath -Filter "*.ffu" -File
+
+ # If there's more than one .ffu file, get the most recent and store its path in $FFUFile
+ if ($FFUFiles.Count -gt 0) {
+ $FFUFile = ($FFUFiles | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).FullName
+ Write-Host "Most recent .ffu file: $FFUFile"
+ }
+ else {
+ Write-Host "No .ffu files found in $FFUFolderPath"
+ }
+ #Add drivers
+ If ($InstallDrivers){
+ New-Item -Path "$FFUDevelopmentPath\Mount" -ItemType Directory -Force
+ Mount-WindowsImage -ImagePath $FFUFile -Index 1 -Path "$FFUDevelopmentPath\Mount"
+ Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$FFUDevelopmentPath\Drivers" -Recurse
+ Dismount-WindowsImage -Path "$FFUDevelopmentPath\Mount" -Save
+ Remove-Item -Path "$FFUDevelopmentPath\Mount" -Recurse -Force
+ }
+ #Optimize FFU
+ & cmd /c """$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile"
+}
+
+
+
+
+try {
+ #Check if the Windows ADK is installed
+ $adkPath = Get-ADK
+}
+catch {
+ throw $_
+}
+
+#Build ISO for Office and Apps
+try{
+ if($InstallOffice){
+ Get-Office
+ }
+ if($InstallApps){
+ New-AppsISO
+ }
+}
+catch {
+ Write-Host "Getting Office and/or building the AppsISO failed"
+ throw $_
+}
+
+#Create VHDX
+try {
+ $wimPath = Get-WimFromISO
+
+ $WimIndex = Get-WimIndex
+
+ $vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -Dynamic:$true
+
+ $systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk
+
+ New-MSRPartition -VhdxDisk $vhdxDisk
+
+ $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $WimIndex[1]
+ $osPartitionDriveLetter = $osPartition[1].DriveLetter
+
+ $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
+
+ Write-Host "All necessary partitions created."
+
+ Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1]
+ New-Item -Path "$($osPartitionDriveLetter):\Windows\Panther\unattend" -ItemType Directory
+ Copy-Item -Path "$FFUDevelopmentPath\BuildFFUUnattend\unattend.xml" -Destination "$($osPartitionDriveLetter):\Windows\Panther\Unattend\Unattend.xml" -Force
+}
+finally {
+ Dismount-ScratchVhdx -VhdxPath $VHDXPath
+ Dismount-DiskImage -ImagePath $ISOPath
+}
+
+#Clean up old VMs
+try {
+ Remove-FFUVM
+}
+catch {
+ Write-Host "VM cleanup failed"
+ throw $_
+}
+
+#Create VM and attach VHDX
+try {
+ $FFUVM = New-FFUVM
+}
+catch {
+ Write-Host 'VM creation failed'
+ throw $_
+}
+#Create Capture Media
+try{
+ #This should happen while the FFUVM is building
+ New-PEMedia -Capture
+}
+catch{
+ throw $_
+}
+#Capture FFU file
+try {
+ #Check if VM is done provisioning
+ do {
+ $FFUVM = Get-VM -Name $FFUVM.Name
+ Start-Sleep -Seconds 10
+ } while ($FFUVM.State -ne 'Off')
+
+ #Capture FFU file
+ New-FFU
+}
+Catch {
+ throw $_
+}
\ No newline at end of file
diff --git a/FFUDevelopment/BuildWinPECaptureMedia.cmd b/archive/FFUDevelopment/BuildWinPECaptureMedia.cmd
similarity index 100%
rename from FFUDevelopment/BuildWinPECaptureMedia.cmd
rename to archive/FFUDevelopment/BuildWinPECaptureMedia.cmd
diff --git a/FFUDevelopment/BuildWinPEDeploymentMedia.cmd b/archive/FFUDevelopment/BuildWinPEDeploymentMedia.cmd
similarity index 100%
rename from FFUDevelopment/BuildWinPEDeploymentMedia.cmd
rename to archive/FFUDevelopment/BuildWinPEDeploymentMedia.cmd
diff --git a/FFUDevelopment/CaptureFFU.ps1 b/archive/FFUDevelopment/CaptureFFU.ps1
similarity index 100%
rename from FFUDevelopment/CaptureFFU.ps1
rename to archive/FFUDevelopment/CaptureFFU.ps1
diff --git a/FFUDevelopment/CreateOfficeISO.ps1 b/archive/FFUDevelopment/CreateOfficeISO.ps1
similarity index 66%
rename from FFUDevelopment/CreateOfficeISO.ps1
rename to archive/FFUDevelopment/CreateOfficeISO.ps1
index 4cee4c3..e5829a9 100644
--- a/FFUDevelopment/CreateOfficeISO.ps1
+++ b/archive/FFUDevelopment/CreateOfficeISO.ps1
@@ -9,25 +9,25 @@ function Get-ODTURL {
}
}
-$FFUDevPath = 'C:\FFUDevelopment'
+$FFUDevelopmentPath = 'C:\FFUDevelopment'
$ODTUrl = Get-ODTURL
$ODTInstallFile = "$env:TEMP\odtsetup.exe"
Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile
# Extract Office Deployment Tool
-$ODTPath = "$FFUDevPath\Office"
+$ODTPath = "$FFUDevelopmentPath\Apps\Office"
Start-Process -FilePath $ODTInstallFile -ArgumentList "/extract:$ODTPath /quiet" -Wait
# Run setup.exe with config.xml
-$ConfigXml = "$FFUDevPath\Office\DownloadFFU.xml"
+$ConfigXml = "$FFUDevelopmentPath\Apps\Office\DownloadFFU.xml"
#Set-Location $ODTPath
-Start-Process -FilePath "$FFUDevPath\Office\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
+Start-Process -FilePath "$FFUDevelopmentPath\Apps\Office\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
#Make Office ISO
Remove-Item -Path "$ODTPath\configuration*" -Force
$OSCDIMG = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe'
-$OfficeISO = "$FFUDevPath\office.iso"
-Start-Process -FilePath $OSCDIMG -ArgumentList "-n -m $ODTPath $OfficeISO" -wait
+$AppsISO = "$FFUDevelopmentPath\Apps.iso"
+Start-Process -FilePath $OSCDIMG -ArgumentList "-n -m -d $ODTPath $AppsISO" -wait
#Mount Office ISO to FFU VM
$VMS = get-vm _ffu-* | Where-Object {$_.state -eq 'running'}
@@ -37,16 +37,15 @@ foreach ($VM in $VMs) {
$DVD = Get-VMDvdDrive -VMName $VM.Name
if ($DVD) {
# Attach ISO to existing DVD drive
- Set-VMDvdDrive -VMName $VM.Name -Path $OfficeISO
+ Set-VMDvdDrive -VMName $VM.Name -Path $AppsISO
}
else {
# Add DVD drive and attach ISO
- Add-VMDvdDrive -VMName $VM.Name -Path $OfficeISO
+ Add-VMDvdDrive -VMName $VM.Name -Path $AppsISO
}
}
#Remove the Office Download and ODT
-$OfficeDownloadPath = "$FFUDevPath\Office\Office"
+$OfficeDownloadPath = "$FFUDevelopmentPath\Apps\Office\Office"
Remove-Item -Path $OfficeDownloadPath -Recurse -Force
-Remove-Item -Path "$ODTPath\setup.exe"
-
+Remove-Item -Path "$ODTPath\setup.exe"
\ No newline at end of file
diff --git a/archive/FFUDevelopment/Docs/BuildDeployFFU.docx b/archive/FFUDevelopment/Docs/BuildDeployFFU.docx
new file mode 100644
index 0000000..f951565
Binary files /dev/null and b/archive/FFUDevelopment/Docs/BuildDeployFFU.docx differ
diff --git a/archive/FFUDevelopment/Docs/BuildDeployFFUv2.docx b/archive/FFUDevelopment/Docs/BuildDeployFFUv2.docx
new file mode 100644
index 0000000..8b6155e
Binary files /dev/null and b/archive/FFUDevelopment/Docs/BuildDeployFFUv2.docx differ
diff --git a/FFUDevelopment/Docs/ConvertWimToFFU.docx b/archive/FFUDevelopment/Docs/ConvertWimToFFU.docx
similarity index 96%
rename from FFUDevelopment/Docs/ConvertWimToFFU.docx
rename to archive/FFUDevelopment/Docs/ConvertWimToFFU.docx
index 6990858..d19c9e4 100644
Binary files a/FFUDevelopment/Docs/ConvertWimToFFU.docx and b/archive/FFUDevelopment/Docs/ConvertWimToFFU.docx differ
diff --git a/archive/FFUDevelopment/Docs/~$ildDeployFFU.docx b/archive/FFUDevelopment/Docs/~$ildDeployFFU.docx
new file mode 100644
index 0000000..97a147c
Binary files /dev/null and b/archive/FFUDevelopment/Docs/~$ildDeployFFU.docx differ
diff --git a/archive/FFUDevelopment/Docs/~$ildDeployFFUv2.docx b/archive/FFUDevelopment/Docs/~$ildDeployFFUv2.docx
new file mode 100644
index 0000000..a91bc36
Binary files /dev/null and b/archive/FFUDevelopment/Docs/~$ildDeployFFUv2.docx differ
diff --git a/FFUDevelopment/ModifyVMForCapture.ps1 b/archive/FFUDevelopment/ModifyVMForCapture.ps1
similarity index 100%
rename from FFUDevelopment/ModifyVMForCapture.ps1
rename to archive/FFUDevelopment/ModifyVMForCapture.ps1
diff --git a/archive/FFUDevelopment/WinPECaptureFFUFiles/AssignDriveLetter.txt b/archive/FFUDevelopment/WinPECaptureFFUFiles/AssignDriveLetter.txt
new file mode 100644
index 0000000..a18948f
--- /dev/null
+++ b/archive/FFUDevelopment/WinPECaptureFFUFiles/AssignDriveLetter.txt
@@ -0,0 +1,4 @@
+select disk 0
+select partition 3
+Assign letter="M"
+exit
diff --git a/archive/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/archive/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1
new file mode 100644
index 0000000..28145ec
--- /dev/null
+++ b/archive/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1
@@ -0,0 +1,60 @@
+#Modify the net use path to map the W: drive to the location you want to copy the FFU file to
+net use W: \\192.168.1.2\c$\FFUDevelopment /user:administrator p@ssw0rd
+
+$AssignDriveLetter = 'x:\AssignDriveLetter.txt'
+Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null
+#Load Registry Hive
+$Software = 'M:\Windows\System32\config\software'
+reg load "HKLM\FFU" $Software
+
+#Find Windows version values
+
+$SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID'
+[int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild'
+$DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion'
+$BuildDate = Get-Date -uformat %b%Y
+
+$SKU = switch ($SKU){
+ Home {'Home'}
+ Professional {'Pro'}
+ ProfessionalEducation {'Pro_Edu'}
+ Enterprise {'Ent'}
+ Education {'Edu'}
+}
+
+if($CurrentBuild -ge 22000){
+ $Name = 'Win11'
+}
+else{
+ $Name = 'Win10'
+}
+
+#If Office is installed, modify the file name of the FFU
+#$Office = Get-childitem -Path 'M:\Program Files\Microsoft Office' -ErrorAction SilentlyContinue | Out-Null
+$Office = Get-childitem -Path 'M:\Program Files\Microsoft Office'
+if($Office){
+ $ffuFilePath = "W:\$Name`_$DisplayVersion`_$SKU`_Office`_$BuildDate.ffu"
+ $dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$Name$DisplayVersion$SKU /Compress:Default"
+
+
+}
+else{
+ $ffuFilePath = "W:\$Name`_$DisplayVersion`_$SKU`_$BuildDate.ffu"
+ $dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$Name$DisplayVersion$SKU /Compress:Default"
+
+}
+
+#Unload Registry
+Set-Location X:\
+Remove-Variable SKU
+Remove-Variable CurrentBuild
+Remove-Variable DisplayVersion
+Remove-Variable Office
+reg unload "HKLM\FFU"
+
+Start-Process -FilePath dism.exe -ArgumentList $dismArgs -Wait -PassThru -ErrorAction Stop | Out-Null
+#Copy DISM log to Host
+xcopy X:\Windows\logs\dism\dism.log W:\ /Y | Out-Null
+#Remvove W: drive
+net use W: /delete
+wpeutil Shutdown
diff --git a/archive/FFUDevelopment/WinPECaptureFFUFiles/Windows/System32/startnet.cmd b/archive/FFUDevelopment/WinPECaptureFFUFiles/Windows/System32/startnet.cmd
new file mode 100644
index 0000000..ed54781
--- /dev/null
+++ b/archive/FFUDevelopment/WinPECaptureFFUFiles/Windows/System32/startnet.cmd
@@ -0,0 +1,5 @@
+wpeinit
+powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
+powershell -Noprofile -ExecutionPolicy Bypass -File x:\CaptureFFU.ps1
+exit
+
diff --git a/archive/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/archive/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
new file mode 100644
index 0000000..6d57e45
--- /dev/null
+++ b/archive/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1
@@ -0,0 +1,582 @@
+function Get-USBDrive(){
+ $USBDriveLetter = (Get-Volume | Where-Object {$_.DriveType -eq 'Removable' -and $_.FileSystemType -eq 'NTFS'}).DriveLetter
+ if ($null -eq $USBDriveLetter){
+ #Must be using a fixed USB drive - difficult to grab drive letter from win32_diskdrive. Assume user followed instructions and used Deploy as the friendly name for partition
+ $USBDriveLetter = (Get-Volume | Where-Object {$_.DriveType -eq 'Fixed' -and $_.FileSystemType -eq 'NTFS' -and $_.FileSystemLabel -eq 'Deploy'}).DriveLetter
+ #If we didn't get the drive letter, stop the script.
+ if ($null -eq $USBDriveLetter){
+ WriteLog 'Cannot find USB drive letter - most likely using a fixed USB drive. Name the 2nd partition with the FFU files as Deploy so the script can grab the drive letter. Exiting'
+ Exit
+ }
+
+ }
+ $USBDriveLetter = $USBDriveLetter + ":\"
+ return $USBDriveLetter
+}
+
+function Get-HardDrive(){
+ $DeviceID = (Get-WmiObject -Class 'Win32_DiskDrive' | Where-Object {$_.MediaType -eq 'Fixed hard disk media' -and $_.Model -ne 'Microsoft Virtual Disk'}).DeviceID
+ return $DeviceID
+}
+
+function WriteLog($LogText){
+ Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText"
+}
+
+function Set-DiskpartAnswerFiles($DiskpartFile,$DiskID){
+ (Get-Content $DiskpartFile).Replace('disk 0', "disk $DiskID") | Set-Content -Path $DiskpartFile
+}
+
+function Set-Computername($computername){
+ [xml]$xml = Get-Content $UnattendFile
+ if($xml.unattend.settings.component.Count -ge 2){
+ #Assumes that Computername is the first component element
+ $xml.unattend.settings.component[0].ComputerName = $computername
+ }else{
+ $xml.unattend.settings.component.ComputerName = $computername
+ }
+ $xml.Save($UnattendFile)
+ return $computername
+}
+
+function Invoke-Process {
+ [CmdletBinding(SupportsShouldProcess)]
+ param
+ (
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string]$FilePath,
+
+ [Parameter()]
+ [ValidateNotNullOrEmpty()]
+ [string]$ArgumentList
+ )
+
+ $ErrorActionPreference = 'Stop'
+
+ try {
+ $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)"
+ $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)"
+
+ $startProcessParams = @{
+ FilePath = $FilePath
+ ArgumentList = $ArgumentList
+ RedirectStandardError = $stdErrTempFile
+ RedirectStandardOutput = $stdOutTempFile
+ Wait = $true;
+ PassThru = $true;
+ NoNewWindow = $false;
+ }
+ if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
+ $cmd = Start-Process @startProcessParams
+ $cmdOutput = Get-Content -Path $stdOutTempFile -Raw
+ $cmdError = Get-Content -Path $stdErrTempFile -Raw
+ if ($cmd.ExitCode -ne 0) {
+ if ($cmdError) {
+ throw $cmdError.Trim()
+ }
+ if ($cmdOutput) {
+ throw $cmdOutput.Trim()
+ }
+ } else {
+ if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
+ WriteLog $cmdOutput
+ }
+ }
+ }
+ } catch {
+ #$PSCmdlet.ThrowTerminatingError($_)
+ WriteLog $_
+ Write-Host 'Script failed - check scriptlog.txt on the USB drive for more info'
+ throw $_
+
+ } finally {
+ Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore
+
+ }
+
+}
+
+# This function can be used in instances where battery level might matter (e.g. installing firmware for Surface). The problem is that WinPE doesn't have
+# a driver for the battery installed, so you'll need to inject drivers, which can be tricky because just injecting the battery driver might not be enough,
+# you might also need other drivers that the battery driver is dependent on.
+# function Get-Battery(){
+# while (($BattLev = (Get-CimInstance win32_battery).EstimatedChargeRemaining) -lt "35")
+# {
+# WriteLog "Battery is currently at $BattLev`%. Waiting for 35`% to proceed..."
+# Write-Host "Battery is currently at $BattLev`%. Waiting for 35`% to proceed..."
+# Start-Sleep 60
+# }
+
+# WriteLog "Battery level is $BattLev `%, which is greater than 35'% applying FFU"
+# Write-Host "Battery level is $BattLev `%, which is greater than 35'% applying FFU"
+# }
+
+#Get USB Drive and create log file
+$LogFileName = 'ScriptLog.txt'
+$USBDrive = Get-USBDrive
+New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
+$LogFile = $USBDrive + $LogFilename
+WriteLog 'Begin Logging'
+
+#Find PhysicalDrive
+$PhysicalDeviceID = Get-HardDrive
+WriteLog "Physical DeviceID is $PhysicalDeviceID"
+
+#Parse DiskID Number
+$DiskID = $PhysicalDeviceID.substring($PhysicalDeviceID.length - 1,1)
+WriteLog "DiskID is $DiskID"
+
+#Modify diskpart answer files if DiskID not 0
+# $UEFIFFUPartitions = 'x:\CreateUEFI-FFU-Partitions.txt'
+# $ExtendPartition = 'x:\ExtendPartition-UEFI.txt'
+
+# If ($DiskID -ne '0'){
+# WriteLog 'DiskID is not 0. Need to modify diskpart answer files'
+# try {
+# Set-DiskpartAnswerFiles $UEFIFFUPartitions $DiskID
+# }
+# catch {
+# WriteLog "Modifying $UEFIFFUPartitions failed with error: $_"
+# }
+
+# try {
+# Set-DiskpartAnswerFiles $ExtendPartition $DiskID
+# }
+# catch {
+# WriteLog "Modifying $ExtendPartition failed with error: $_"
+# }
+# }
+
+#Find FFU Files
+[array]$FFUFiles = @(Get-ChildItem -Path $USBDrive*.ffu)
+$FFUCount = $FFUFiles.Count
+
+#If multiple FFUs found, ask which to install
+If ($FFUCount -gt 1) {
+ WriteLog "Found $FFUCount FFU Files"
+ $array = @()
+
+ for($i=0;$i -le $FFUCount -1;$i++){
+ $Properties = [ordered]@{Number = $i + 1 ; FFUFile = $FFUFiles[$i].FullName}
+ $array += New-Object PSObject -Property $Properties
+ }
+ $array | Format-Table -AutoSize -Property Number, FFUFile
+ do {
+ try {
+ $var = $true
+ [int]$FFUSelected = Read-Host 'Enter the FFU number to install'
+ $FFUSelected = $FFUSelected -1
+ }
+
+ catch {
+ Write-Host 'Input was not in correct format. Please enter a valid FFU number'
+ $var = $false
+ }
+ } until (($FFUSelected -le $FFUCount -1) -and $var)
+
+ $FFUFileToInstall = $array[$FFUSelected].FFUFile
+ WriteLog "$FFUFileToInstall was selected"
+}
+elseif ($FFUCount -eq 1) {
+ WriteLog "Found $FFUCount FFU File"
+ $FFUFileToInstall = $FFUFiles[0].FullName
+ WriteLog "$FFUFileToInstall will be installed"
+}
+else {
+ Writelog 'No FFU files found'
+ Write-Host 'No FFU files found'
+ Exit
+}
+
+#FindAP
+$APFolder = $USBDrive + "Autopilot\"
+If (Test-Path -Path $APFolder){
+ [array]$APFiles = @(Get-ChildItem -Path $APFolder*.json)
+ $APFilesCount = $APFiles.Count
+ if ($APFilesCount -ge 1){
+ $autopilot = $true
+ }
+}
+
+
+#FindPPKG
+$PPKGFolder = $USBDrive + "PPKG\"
+if (Test-Path -Path $PPKGFolder){
+ [array]$PPKGFiles = @(Get-ChildItem -Path $PPKGFolder*.ppkg)
+ $PPKGFilesCount = $PPKGFiles.Count
+ if ($PPKGFilesCount -ge 1){
+ $PPKG = $true
+ }
+}
+
+#FindUnattend
+$UnattendFolder = $USBDrive + "unattend\"
+$UnattendFilePath = $UnattendFolder + "unattend.xml"
+$UnattendPrefixPath = $UnattendFolder + "prefixes.txt"
+If (Test-Path -Path $UnattendFilePath){
+ $UnattendFile = Get-ChildItem -Path $UnattendFilePath
+ If ($UnattendFile){
+ $Unattend = $true
+ }
+}
+If (Test-Path -Path $UnattendPrefixPath){
+ $UnattendPrefixFile = Get-ChildItem -Path $UnattendPrefixPath
+ If ($UnattendPrefixFile){
+ $UnattendPrefix = $true
+ }
+}
+
+#Ask for device name if unattend exists
+if ($Unattend -and $UnattendPrefix){
+ Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
+ $UnattendPrefixes = @(Get-content $UnattendPrefixFile)
+ $UnattendPrefixCount = $UnattendPrefixes.Count
+ If ($UnattendPrefixCount -gt 1) {
+ WriteLog "Found $UnattendPrefixCount Prefixes"
+ $array = @()
+ for($i=0;$i -le $UnattendPrefixCount -1;$i++){
+ $Properties = [ordered]@{Number = $i + 1 ; DeviceNamePrefix = $UnattendPrefixes[$i]}
+ $array += New-Object PSObject -Property $Properties
+ }
+ $array | Format-Table -AutoSize -Property Number, DeviceNamePrefix
+ do {
+ try {
+ $var = $true
+ [int]$PrefixSelected = Read-Host 'Enter the prefix number to use for the device name'
+ $PrefixSelected = $PrefixSelected -1
+ }
+ catch {
+ Write-Host 'Input was not in correct format. Please enter a valid prefix number'
+ $var = $false
+ }
+ } until (($PrefixSelected -le $UnattendPrefixCount -1) -and $var)
+ $PrefixToUse = $array[$PrefixSelected].DeviceNamePrefix
+ WriteLog "$PrefixToUse was selected"
+ }
+ elseif ($UnattendPrefixCount -eq 1) {
+ WriteLog "Found $UnattendPrefixCount Prefix"
+ $PrefixToUse = $UnattendPrefixes[0]
+ WriteLog "Will use $PrefixToUse as device name prefix"
+ }
+ #Get serial number to append. This can make names longer than 15 characters. Trim any leading or trailing whitespace
+ $serial = (Get-CimInstance -ClassName win32_bios).SerialNumber.Trim()
+ #Combine prefix with serial
+ $computername = $PrefixToUse + $serial
+ #If computername is longer than 15 characters, reduce to 15. Sysprep/unattend doesn't like ComputerName being longer than 15 characters even though Windows accepts it
+ If ($computername.Length -gt 15){
+ $computername = $computername.substring(0,15)
+ }
+ $computername = Set-Computername($computername)
+ Writelog "Computer name set to $computername"
+}
+elseif($Unattend){
+ Writelog 'Unattend file found with no prefixes.txt, asking for name'
+ [string]$computername = Read-Host 'Enter device name'
+ Set-Computername($computername)
+ Writelog "Computer name set to $computername"
+}
+else {
+ WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
+}
+
+#If both AP and PPKG folder found with files, ask which to use.
+If($autopilot -eq $true -and $PPKG -eq $true){
+ WriteLog 'Both PPKG and Autopilot json files found'
+ Write-Host 'Both Autopilot JSON files and Provisioning packages were found.'
+ do {
+ try {
+ $var = $true
+ [int]$APorPPKG = Read-Host 'Enter 1 for Autopilot or 2 for Provisioning Package'
+ }
+
+ catch {
+ Write-Host 'Incorrect value. Please enter 1 for Autopilot or 2 for Provisioning Package'
+ $var = $false
+ }
+ } until (($APorPPKG -gt 0 -and $APorPPKG -lt 3) -and $var)
+ If ($APorPPKG -eq 1){
+ $PPKG = $false
+ }
+ else{
+ $autopilot = $false
+ }
+}
+
+#If multiple AP json files found, ask which to install
+If ($APFilesCount -gt 1 -and $autopilot -eq $true) {
+ WriteLog "Found $APFilesCount Autopilot json Files"
+ $array = @()
+
+ for($i=0;$i -le $APFilesCount -1;$i++){
+ $Properties = [ordered]@{Number = $i + 1 ; APFile = $APFiles[$i].FullName; APFileName = $APFiles[$i].Name}
+ $array += New-Object PSObject -Property $Properties
+ }
+ $array | Format-Table -AutoSize -Property Number, APFileName
+ do {
+ try {
+ $var = $true
+ [int]$APFileSelected = Read-Host 'Enter the AP json file number to install'
+ $APFileSelected = $APFileSelected - 1
+ }
+
+ catch {
+ Write-Host 'Input was not in correct format. Please enter a valid AP json file number'
+ $var = $false
+ }
+ } until (($APFileSelected -le $APFilesCount -1) -and $var)
+
+ $APFileToInstall = $array[$APFileSelected].APFile
+ $APFileName = $array[$APFileSelected].APFileName
+ WriteLog "$APFileToInstall was selected"
+}
+elseif ($APFilesCount -eq 1 -and $autopilot -eq $true) {
+ WriteLog "Found $APFilesCount AP File"
+ $APFileToInstall = $APFiles[0].FullName
+ $APFileName = $APFiles[0].Name
+ WriteLog "$APFileToInstall will be copied"
+}
+else {
+ Writelog 'No AP files found or AP was not selected'
+}
+
+#If multiple PPKG files found, ask which to install
+If ($PPKGFilesCount -gt 1 -and $PPKG -eq $true) {
+ WriteLog "Found $PPKGFilesCount PPKG Files"
+ $array = @()
+
+ for($i=0;$i -le $PPKGFilesCount -1;$i++){
+ $Properties = [ordered]@{Number = $i + 1 ; PPKGFile = $PPKGFiles[$i].FullName; PPKGFileName = $PPKGFiles[$i].Name}
+ $array += New-Object PSObject -Property $Properties
+ }
+ $array | Format-Table -AutoSize -Property Number, PPKGFileName
+ do {
+ try {
+ $var = $true
+ [int]$PPKGFileSelected = Read-Host 'Enter the PPKG file number to install'
+ $PPKGFileSelected = $PPKGFileSelected - 1
+ }
+
+ catch {
+ Write-Host 'Input was not in correct format. Please enter a valid PPKG file number'
+ $var = $false
+ }
+ } until (($PPKGFileSelected -le $PPKGFilesCount -1) -and $var)
+
+ $PPKGFileToInstall = $array[$PPKGFileSelected].PPKGFile
+ WriteLog "$PPKGFileToInstall was selected"
+}
+elseif ($PPKGFilesCount -eq 1 -and $PPKG -eq $true) {
+ WriteLog "Found $PPKGFilesCount PPKG File"
+ $PPKGFileToInstall = $PPKGFiles[0].FullName
+ WriteLog "$PPKGFileToInstall will be used"
+}
+else {
+ Writelog 'No PPKG files found or PPKG not selected.'
+}
+
+#Find Drivers
+$Drivers = $USBDrive + "Drivers"
+If (Test-Path -Path $Drivers)
+{
+ #Check if multiple driver folders found, if so, just select one folder to save time/space
+ $DriverFolders = Get-ChildItem -Path $Drivers
+ $DriverFoldersCount = $DriverFolders.count
+ If ($DriverFoldersCount -gt 1)
+ {
+ WriteLog "Found $DriverFoldersCount driver folders"
+ $array = @()
+
+ for($i=0; $i -le $DriverFoldersCount -1; $i++){
+ $Properties = [ordered]@{Number = $i + 1; Drivers = $DriverFolders[$i].FullName}
+ $array += New-Object PSObject -Property $Properties
+ }
+ $array | Format-Table -AutoSize -Property Number, Drivers
+ do {
+ try {
+ $var = $true
+ [int]$DriversSelected = Read-Host 'Enter the set of drivers to install'
+ $DriversSelected = $DriversSelected - 1
+ }
+
+ catch {
+ Write-Host 'Input was not in correct format. Please enter a valid driver folder number'
+ $var = $false
+ }
+ } until (($DriversSelected -le $DriverFoldersCount -1) -and $var)
+
+ $Drivers = $array[$DriversSelected].Drivers
+ WriteLog "$Drivers was selected"
+ }
+ elseif ($DriverFoldersCount -eq 1) {
+ WriteLog "Found $DriverFoldersCount driver folder"
+ $Drivers = $DriverFolders.FullName
+ WriteLog "$Drivers will be installed"
+ }
+ else {
+ Writelog 'No driver folders found'
+ }
+}
+
+#If you want to enable battery level checking, uncomment the line below as well as the Get-Battery function near the top of the script
+#Get-Battery
+
+#Partition drive
+Writelog 'Clean Disk'
+#Start-Process -FilePath diskpart.exe -ArgumentList "/S $UEFIFFUPartitions" -Wait -ErrorAction Stop | Out-File $Logfile -Append
+#Invoke-Process diskpart.exe "/S $UEFIFFUPartitions"
+try {
+ $Disk = Get-Disk -Number $DiskID
+ $Disk | clear-disk -RemoveData -RemoveOEM -Confirm:$false
+}
+catch {
+ WriteLog 'Cleaning disk failed. Exiting'
+ throw $_
+}
+
+Writelog 'Cleaning Disk succeeded'
+
+#Apply FFU
+WriteLog "Applying FFU to $PhysicalDeviceID"
+WriteLog "Running command dism /apply-ffu /ImageFile:$FFUFileToInstall /ApplyDrive:$PhysicalDeviceID"
+#In order for Applying Image progress bar to show up, need to call dism directly. Might be a better way to handle, but must have progress bar show up on screen.
+dism /apply-ffu /ImageFile:$FFUFileToInstall /ApplyDrive:$PhysicalDeviceID
+if($LASTEXITCODE -eq 0){
+ WriteLog 'Successfully applied FFU'
+}
+else{
+ Writelog "Failed to apply FFU - LastExitCode = $LASTEXITCODE also check dism.log on the USB drive for more info"
+ #Copy DISM log to USBDrive
+ invoke-process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y"
+ exit
+}
+
+#Remove recovery partition - this is needed in order to extend the Windows partition so it uses the full disk size. If dism /optimize-ffu worked, this wouldn't be needed
+# $disk = get-disk -Number $DiskID
+# $RecoveryPartition = $disk | get-partition | Where-Object {$_.type -eq 'Recovery'}
+# if ($RecoveryPartition){
+# $RecoveryPartitionNumber = $RecoveryPartition.PartitionNumber
+# if ($RecoveryPartitionNumber -eq 4){
+# try {
+# WriteLog 'Removing recovery partition'
+# Remove-partition -DiskNumber $DiskID -PartitionNumber $RecoveryPartitionNumber -Confirm:$false
+# }
+# catch {
+# WriteLog 'Error removing recovery partition, exiting'
+# throw $_
+# }
+# }
+# else{
+# WriteLog 'Recovery partition not partition 4. Script will exit. Please create the FFU with the recovery partition as the last partition. This is the default and recommended way.'
+# exit
+# }
+# }
+
+#Extend Windows partition and create recovery partition
+# Writelog 'Extending Windows partition'
+# Invoke-Process diskpart.exe "/S $ExtendPartition"
+# if($LASTEXITCODE -eq 0){
+# WriteLog 'Successfully extended Windows partition and created recovery partition'
+# }
+# else{
+# Writelog "Failed to extend Windows partition and/or create recovery partition - LastExitCode = $LASTEXITCODE"
+# }
+
+#Set W: drive letter to Windows partition
+Get-Disk | Where-Object Number -eq $DiskID | Get-Partition | Where-Object PartitionNumber -eq 3 | Set-Partition -NewDriveLetter W
+
+#Copy modified WinRE if folder exists, else copy inbox WinRE
+$WinRE = $USBDrive + "WinRE\winre.wim"
+If (Test-Path -Path $WinRE)
+{
+ WriteLog 'Copying modified WinRE to Recovery directory'
+ Invoke-Process xcopy.exe "/h $WinRE R:\Recovery\WindowsRE\ /Y"
+ WriteLog 'Copying WinRE to Recovery directory succeeded'
+ WriteLog 'Registering location of recovery tools'
+ Invoke-Process W:\Windows\System32\Reagentc.exe "/Setreimage /Path R:\Recovery\WindowsRE /Target W:\Windows"
+ WriteLog 'Registering location of recovery tools succeeded'
+}
+# else
+# {
+# WriteLog 'Copying default WinRE to Recovery directory'
+# Invoke-Process xcopy.exe "/h W:\Windows\System32\Recovery\Winre.wim R:\Recovery\WindowsRE\ /Y"
+# WriteLog 'Copying WinRE to Recovery directory succeeded'
+# WriteLog 'Registering location of recovery tools'
+# Invoke-process W:\Windows\System32\Reagentc.exe "/Setreimage /Path R:\Recovery\WindowsRE /Target W:\Windows"
+# WriteLog 'Registering location of recovery tools succeeded'
+# }
+
+#Autopilot JSON
+If ($APFileToInstall){
+ WriteLog "Copying $APFileToInstall to W:\windows\provisioning\autopilot"
+ Invoke-process xcopy.exe "$APFileToInstall W:\Windows\provisioning\autopilot\"
+ WriteLog "Copying $APFileToInstall to W:\windows\provisioning\autopilot succeeded"
+ # Rename file in W:\Windows\Provisioning\Autopilot to AutoPilotConfigurationFile.json
+ try {
+ Rename-Item -Path "W:\Windows\Provisioning\Autopilot\$APFileName" -NewName 'W:\Windows\Provisioning\Autopilot\AutoPilotConfigurationFile.json'
+ WriteLog "Renamed W:\Windows\Provisioning\Autopilot\$APFilename to W:\Windows\Provisioning\Autopilot\AutoPilotConfigurationFile.json"
+ }
+
+ catch{
+ Writelog "Copying $APFileToInstall to W:\windows\provisioning\autopilot failed with error: $_"
+ throw $_
+ }
+}
+#Apply PPKG
+If ($PPKGFileToInstall){
+ try {
+ #Make sure to delete any existing PPKG on the USB drive
+ Get-Childitem -Path $USBDrive\*.ppkg | ForEach-Object {
+ Remove-item -Path $_.FullName
+ }
+ WriteLog "Copying $PPKGFileToInstall to $USBDrive"
+ Invoke-process xcopy.exe "$PPKGFileToInstall $USBDrive"
+ WriteLog "Copying $PPKGFileToInstall to $USBDrive succeeded"
+ }
+
+ catch{
+ Writelog "Copying $PPKGFileToInstall to $USBDrive failed with error: $_"
+ throw $_
+ }
+}
+#Set DeviceName
+If ($PrefixToUse){
+ try{
+ $PantherDir = 'w:\windows\panther'
+ If (Test-Path -Path $PantherDir){
+ Writelog "Copying $UnattendFile to $PantherDir"
+ Invoke-process xcopy "$UnattendFile $PantherDir /Y"
+ WriteLog "Copying $UnattendFile to $PantherDir succeeded"
+ }
+ else{
+ Writelog "$PantherDir doesn't exist, creating it"
+ New-Item -Path $PantherDir -ItemType Directory -Force
+ Writelog "Copying $UnattendFile to $PantherDir"
+ Invoke-Process xcopy.exe "$UnattendFile $PantherDir"
+ WriteLog "Copying $UnattendFile to $PantherDir succeeded"
+ }
+ }
+ catch{
+ WriteLog "Copying Unattend.xml to name device failed"
+ throw $_
+ }
+}
+
+#Add Drivers
+#Some drivers can sometimes fail to copy and dism ends up with a non-zero error code. Invoke-process will throw and terminate in these instances.
+If (Test-Path -Path $Drivers)
+{
+ WriteLog 'Copying drivers'
+ Write-Warning 'Copying Drivers - dism will pop a window with no progress. This can take a few minutes to complete. This is done so drivers are logged to the scriptlog.txt file. Please be patient.'
+ Invoke-process dism.exe "/image:W:\ /Add-Driver /Driver:""$Drivers"" /Recurse"
+ WriteLog 'Copying drivers succeeded'
+}
+
+#Copy DISM log to USBDrive
+WriteLog "Copying dism log to $USBDrive"
+invoke-process xcopy "X:\Windows\logs\dism\dism.log $USBDrive /Y"
+WriteLog "Copying dism log to $USBDrive succeeded"
+
+
+
+
diff --git a/archive/FFUDevelopment/WinPEDeployFFUFiles/Windows/System32/startnet.cmd b/archive/FFUDevelopment/WinPEDeployFFUFiles/Windows/System32/startnet.cmd
new file mode 100644
index 0000000..12519ba
--- /dev/null
+++ b/archive/FFUDevelopment/WinPEDeployFFUFiles/Windows/System32/startnet.cmd
@@ -0,0 +1,5 @@
+wpeinit
+powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
+powershell -Noprofile -ExecutionPolicy Bypass -File x:\ApplyFFU.ps1
+exit
+
diff --git a/FFUDevelopment/unattend/prefixes.txt b/archive/FFUDevelopment/unattend/prefixes.txt
similarity index 100%
rename from FFUDevelopment/unattend/prefixes.txt
rename to archive/FFUDevelopment/unattend/prefixes.txt
diff --git a/FFUDevelopment/unattend/unattend.xml b/archive/FFUDevelopment/unattend/unattend.xml
similarity index 100%
rename from FFUDevelopment/unattend/unattend.xml
rename to archive/FFUDevelopment/unattend/unattend.xml
diff --git a/FFUDevelopment/wimToFFU/Convert-WimToFfu.ps1 b/archive/FFUDevelopment/wimToFFU/Convert-WimToFfu.ps1
similarity index 100%
rename from FFUDevelopment/wimToFFU/Convert-WimToFfu.ps1
rename to archive/FFUDevelopment/wimToFFU/Convert-WimToFfu.ps1