mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
- Added Apps\Orchestration folder with new orchestration workflow to replace InstallAppsAndSysprep.cmd file.
- Updated BuildFFUUnattend files to point to the new Orchestrator.ps1 file. - Added new common and FFUUI.Core directories that house common/shared files between the UI and PS1 script. This breaks up each of the PS1 scripts to keep things smaller and more organized. Still a lot of work to do here to pull some stuff out of the PS1 scripts. - Modified the CaptureFFU.ps1 file to include more info during the capture process to help with troubleshooting - Too many functional changes to list here.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"VMWareTools": true,
|
||||
"foo": "bar"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
$basePath = "D:\MSStore"
|
||||
# Check if the base path exists
|
||||
Write-Host "Installing Store Apps: Checking for $basePath"
|
||||
if (-not (Test-Path -Path $basePath)) {
|
||||
Write-Host "Installing Store Apps: $basePath does not exist."
|
||||
exit
|
||||
}
|
||||
Write-Host "Installing Store Apps: $basePath exists, installing apps."
|
||||
|
||||
# Process each app folder in the base path
|
||||
foreach ($appFolder in Get-ChildItem -Path $basePath -Directory) {
|
||||
$folderPath = $appFolder.FullName
|
||||
$dependenciesFolder = Join-Path -Path $folderPath -ChildPath "Dependencies"
|
||||
|
||||
# Find main package - exclude Dependencies folder items and xml/yaml files
|
||||
$mainPackage = Get-ChildItem -Path $folderPath -File |
|
||||
Where-Object {
|
||||
$_.DirectoryName -ne $dependenciesFolder -and
|
||||
$_.Extension -ne ".xml" -and
|
||||
$_.Extension -ne ".yaml"
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($mainPackage) {
|
||||
# Build DISM command with main package
|
||||
$dismParams = @(
|
||||
"/Online"
|
||||
"/Add-ProvisionedAppxPackage"
|
||||
"/PackagePath:`"$($mainPackage.FullName)`""
|
||||
"/Region:all"
|
||||
"/StubPackageOption:installfull"
|
||||
)
|
||||
|
||||
# Add dependency packages if they exist
|
||||
if (Test-Path -Path $dependenciesFolder) {
|
||||
$dependencies = Get-ChildItem -Path $dependenciesFolder -File
|
||||
foreach ($dependency in $dependencies) {
|
||||
$dismParams += "/DependencyPackagePath:`"$($dependency.FullName)`""
|
||||
}
|
||||
}
|
||||
|
||||
# Look for license file and add appropriate parameter
|
||||
$licenseFile = Get-ChildItem -Path $folderPath -Filter "*.xml" -File | Select-Object -First 1
|
||||
if ($licenseFile) {
|
||||
$dismParams += "/LicensePath:`"$($licenseFile.FullName)`""
|
||||
} else {
|
||||
$dismParams += "/SkipLicense"
|
||||
}
|
||||
|
||||
# Construct final command
|
||||
$dismCommand = "DISM " + ($dismParams -join " ")
|
||||
|
||||
# Output and execute the command
|
||||
Write-Output $dismCommand
|
||||
Invoke-Expression -Command $dismCommand
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
function Invoke-Process {
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param
|
||||
(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string[]]$ArgumentList,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[bool]$Wait = $true
|
||||
)
|
||||
|
||||
$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 = $($Wait);
|
||||
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 -and $wait -eq $true) {
|
||||
if ($cmdError) {
|
||||
throw $cmdError.Trim()
|
||||
}
|
||||
if ($cmdOutput) {
|
||||
throw $cmdOutput.Trim()
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
||||
# WriteLog $cmdOutput
|
||||
Write-Host $cmdOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
#$PSCmdlet.ThrowTerminatingError($_)
|
||||
# WriteLog $_
|
||||
# Write-Host "Script failed - $Logfile for more info"
|
||||
throw $_
|
||||
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore
|
||||
}
|
||||
return $cmd
|
||||
}
|
||||
|
||||
# Define paths for the JSON files
|
||||
$wingetAppsJsonFile = "$PSScriptRoot\WinGetWin32Apps.json"
|
||||
# Look for UserAppList.json one directory level up from the script's location. This keeps the user specific json files (AppList.json and UserAppList.json in the Apps dir)
|
||||
$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json"
|
||||
|
||||
# Initialize an empty array to hold all apps
|
||||
$allApps = @()
|
||||
|
||||
# Read the WinGetWin32Apps.json file if it exists
|
||||
if (Test-Path -Path $wingetAppsJsonFile) {
|
||||
Write-Host "Processing WinGetWin32Apps.json..."
|
||||
try {
|
||||
$wingetApps = Get-Content -Path $wingetAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
if ($wingetApps -is [array]) {
|
||||
$allApps += $wingetApps
|
||||
Write-Host "Found $(($wingetApps | Measure-Object).Count) WinGet Win32 apps."
|
||||
} elseif ($wingetApps) {
|
||||
$allApps += @($wingetApps) # Ensure it's added as an array element
|
||||
Write-Host "Found 1 WinGet Win32 app."
|
||||
} else {
|
||||
Write-Host "WinGetWin32Apps.json is empty or invalid."
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to read or parse WinGetWin32Apps.json file: $_"
|
||||
# Decide if execution should stop or continue
|
||||
# exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "WinGetWin32Apps.json file not found. Skipping."
|
||||
}
|
||||
|
||||
# Read the UserAppList.json file if it exists
|
||||
if (Test-Path -Path $userAppsJsonFile) {
|
||||
Write-Host "Processing UserAppList.json..."
|
||||
try {
|
||||
$userApps = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
if ($userApps -is [array]) {
|
||||
$allApps += $userApps
|
||||
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
|
||||
} elseif ($userApps) {
|
||||
$allApps += @($userApps) # Ensure it's added as an array element
|
||||
Write-Host "Found 1 user-defined app."
|
||||
} else {
|
||||
Write-Host "UserAppList.json is empty or invalid."
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to read or parse UserAppList.json file: $_"
|
||||
# Decide if execution should stop or continue
|
||||
# exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "UserAppList.json file not found. Skipping."
|
||||
}
|
||||
|
||||
# Check if there are any apps to install
|
||||
if ($allApps.Count -eq 0) {
|
||||
Write-Host "No Win32 apps found in either WinGetWin32Apps.json or UserAppList.json. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Total apps to install: $($allApps.Count)"
|
||||
|
||||
# Sort all apps by priority
|
||||
$sortedApps = $allApps | Sort-Object -Property Priority
|
||||
|
||||
# Install each app
|
||||
foreach ($app in $sortedApps) {
|
||||
# Check if required properties exist
|
||||
if (-not $app.PSObject.Properties['Name'] -or -not $app.PSObject.Properties['CommandLine'] -or -not $app.PSObject.Properties['Arguments']) {
|
||||
Write-Warning "Skipping app due to missing required properties (Name, CommandLine, Arguments): $($app | ConvertTo-Json -Depth 1 -Compress)"
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Installing $($app.Name)..."
|
||||
|
||||
# Wait until no MSIExec installation is running
|
||||
while ($true) {
|
||||
try {
|
||||
# Try to open the MSIExec global mutex
|
||||
$Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute")
|
||||
# Dispose releases the handle from our script only.
|
||||
$Mutex.Dispose()
|
||||
Write-Host "Another MSIExec installer is running. Waiting for 5 seconds before rechecking..."
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
catch [System.Threading.WaitHandleCannotBeOpenedException] {
|
||||
# If we can't open the mutex, it means no MSIExec installation is running
|
||||
break
|
||||
}
|
||||
catch {
|
||||
# Handle other potential errors when checking the mutex
|
||||
Write-Warning "Error checking MSIExec mutex: $_. Proceeding with caution."
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
# Construct the argument list properly, handling potential array vs string
|
||||
$argumentsToPass = if ($app.Arguments -is [array]) { $app.Arguments } else { @($app.Arguments) }
|
||||
|
||||
Write-Host "Running command: $($app.CommandLine) $($argumentsToPass -join ' ')"
|
||||
$result = Invoke-Process -FilePath $($app.CommandLine) -ArgumentList $argumentsToPass
|
||||
Write-Host "$($app.Name) exited with exit code: $($result.ExitCode)`r`n"
|
||||
} catch {
|
||||
Write-Error "Error occurred while installing $($app.Name): $_"
|
||||
# Decide if execution should stop or continue after an error
|
||||
# exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "All Win32 app installations attempted."
|
||||
@@ -0,0 +1,59 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This script uses the variables from the AppsScriptVariables hashtable passed to BuildFFUVM.ps1 to run application deployment tasks.
|
||||
|
||||
.DESCRIPTION
|
||||
By defining the variables in the AppsScriptVariables hashtable, you can customize the application deployment tasks that are run by this script.
|
||||
The BuildFFUVM.ps1 script will export the AppsScriptVariables hashtable to a JSON file in the Orchestration folder.
|
||||
Include your own custom script here if you want to run it as part of the application deployment tasks.
|
||||
Alternatively, you can pass the AppsScriptVariables hashtable directly to this script.
|
||||
#>
|
||||
|
||||
param (
|
||||
[hashtable]$AppsScriptVariables
|
||||
)
|
||||
|
||||
# Try to read from the JSON file if it exists and AppsScriptVariables is not provided
|
||||
$appsScriptVarsJsonPath = Join-Path -Path $PSScriptRoot -ChildPath "AppsScriptVariables.json"
|
||||
if ((-not $AppsScriptVariables -or $AppsScriptVariables.Count -eq 0) -and (Test-Path -Path $appsScriptVarsJsonPath)) {
|
||||
try {
|
||||
$jsonContent = Get-Content -Path $appsScriptVarsJsonPath -Raw -ErrorAction Stop
|
||||
$jsonObject = $jsonContent | ConvertFrom-Json -ErrorAction Stop
|
||||
|
||||
# Convert PSCustomObject to hashtable
|
||||
$AppsScriptVariables = @{}
|
||||
foreach ($prop in $jsonObject.PSObject.Properties) {
|
||||
$AppsScriptVariables[$prop.Name] = $prop.Value
|
||||
}
|
||||
|
||||
Write-Host "Successfully loaded AppsScriptVariables from $appsScriptVarsJsonPath"
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to load AppsScriptVariables from JSON file: $_"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "AppsScriptVariables provided directly, skipping JSON file load."
|
||||
}
|
||||
|
||||
# Example of how to use the AppsScriptVariables hashtable to control script execution
|
||||
|
||||
# Example: Check if a variable named 'foo' is set to string 'true' and run a script accordingly
|
||||
# if ($AppsScriptVariables['foo'] -eq 'true') {
|
||||
# Write-Host "Foo would have installed"
|
||||
# }
|
||||
# else {
|
||||
# Write-Host "Foo would not have installed"
|
||||
# }
|
||||
|
||||
# Example: Check if a variable named 'foo' is set to boolean $true and run a script accordingly
|
||||
# if ($AppsScriptVariables['foo'] -eq $true) {
|
||||
# Write-Host "Foo would have been installed"
|
||||
# }
|
||||
# else {
|
||||
# Write-Host "Foo would not have installed"
|
||||
# }
|
||||
|
||||
# Your code below here
|
||||
|
||||
Write-Host 'Invoke-AppsScript.ps1 finished'
|
||||
@@ -0,0 +1,84 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Orchestration script for FFU VM deployment tasks
|
||||
|
||||
.DESCRIPTION
|
||||
This script orchestrates the following deployment tasks:
|
||||
- Install-Office.ps1
|
||||
- Update-Defender.ps1
|
||||
- Update-MSRT.ps1
|
||||
- Update-OneDrive.ps1
|
||||
- Update-Edge.ps1
|
||||
- Install-Win32Apps.ps1
|
||||
- Invoke-AppsScript.ps1
|
||||
- Install-UserApps.ps1
|
||||
- Install-StoreApps.ps1
|
||||
- Run-DiskCleanup.ps1
|
||||
- Run-Sysprep.ps1
|
||||
|
||||
The script will check for the presence of each of these files and if they exist, will run the script
|
||||
#>
|
||||
|
||||
# Header
|
||||
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
Write-Host " FFU Builder Orchestrator " -ForegroundColor Yellow
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
|
||||
# Define the path to the scripts
|
||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
# Define the list of scripts to run, order doesn't matter - if you have a custom script, add it here
|
||||
$scriptList = @(
|
||||
"Install-Office.ps1",
|
||||
"Update-Defender.ps1",
|
||||
"Update-MSRT.ps1",
|
||||
"Update-OneDrive.ps1",
|
||||
"Update-Edge.ps1",
|
||||
"Install-Win32Apps.ps1",
|
||||
"Install-StoreApps.ps1",
|
||||
"Invoke-AppsScript.ps1",
|
||||
"Install-UserApps.ps1"
|
||||
)
|
||||
# Check if each script exists and run it if it does
|
||||
foreach ($script in $scriptList) {
|
||||
$scriptFile = Join-Path -Path $scriptPath -ChildPath $script
|
||||
if (Test-Path -Path $scriptFile) {
|
||||
Write-Host "`n" # Add a newline for spacing
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
Write-Host " Running script: $script" -ForegroundColor Yellow
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
# Run script and wait for it to finish
|
||||
# pause
|
||||
& $scriptFile
|
||||
}
|
||||
}
|
||||
|
||||
# Run-DiskCleanup.ps1 must run before Run-Sysprep.ps1
|
||||
$diskCleanupScript = Join-Path -Path $scriptPath -ChildPath "Run-DiskCleanup.ps1"
|
||||
if (Test-Path -Path $diskCleanupScript) {
|
||||
Write-Host "`n" # Add a newline for spacing
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
Write-Host " Running script: Run-DiskCleanup.ps1" -ForegroundColor Yellow
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
# Run script and wait for it to finish
|
||||
& $diskCleanupScript
|
||||
|
||||
} else {
|
||||
Write-Host "Run-DiskCleanup.ps1 not found!"
|
||||
}
|
||||
|
||||
# Run-Sysprep.ps1 must run last
|
||||
$sysprepScript = Join-Path -Path $scriptPath -ChildPath "Run-Sysprep.ps1"
|
||||
if (Test-Path -Path $sysprepScript) {
|
||||
Write-Host "`n" # Add a newline for spacing
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
Write-Host " Running script: Run-Sysprep.ps1" -ForegroundColor Yellow
|
||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||
# Run script and wait for it to finish
|
||||
& $sysprepScript
|
||||
} else {
|
||||
Write-Host "Run-Sysprep.ps1 not found!"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Run disk cleanup (cleanmgr.exe) with all options enabled
|
||||
# Reference: https://learn.microsoft.com/en-us/troubleshoot/windows-server/backup-and-storage/automating-disk-cleanup-tool
|
||||
|
||||
$rootKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
|
||||
|
||||
# Set StateFlags0000 to 2 for all subkeys except "Offline Pages Files"
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Set-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Type DWord -Value 2 -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Run the disk cleanup tool with the specified flags
|
||||
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:0" -Wait
|
||||
|
||||
# Remove the StateFlags0000 registry values that were added
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Remove-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#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.
|
||||
#Also kills the sysprep process in order to automate sysprep generalize
|
||||
# Convert these commands to native powershell
|
||||
# del c:\windows\panther\unattend\unattend.xml /F /Q
|
||||
# del c:\windows\panther\unattend.xml /F /Q
|
||||
# taskkill /IM sysprep.exe
|
||||
# timeout /t 10
|
||||
# & c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
|
||||
|
||||
Remove-Item -Path "C:\windows\panther\unattend\unattend.xml" -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "C:\windows\panther\unattend.xml" -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name "sysprep" -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 10
|
||||
& "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe
|
||||
Reference in New Issue
Block a user