From 8c897e93fe5fee62cd6b7dfa5f4f06c6841bc6aa Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:57:14 -0400 Subject: [PATCH 01/16] Added support for AppList to be in JSON format. WinGet searches now use app ID. Modified InstallAppsandSysprep.cmd to handle packages with no dependencies --- FFUDevelopment/Apps/AppList.json | 14 ++ FFUDevelopment/Apps/AppsList.txt | 2 - FFUDevelopment/Apps/InstallAppsandSysprep.cmd | 18 +-- FFUDevelopment/BuildFFUVM.ps1 | 121 +++++++++++------- 4 files changed, 98 insertions(+), 57 deletions(-) create mode 100644 FFUDevelopment/Apps/AppList.json delete mode 100644 FFUDevelopment/Apps/AppsList.txt diff --git a/FFUDevelopment/Apps/AppList.json b/FFUDevelopment/Apps/AppList.json new file mode 100644 index 0000000..ea7e1d7 --- /dev/null +++ b/FFUDevelopment/Apps/AppList.json @@ -0,0 +1,14 @@ +{ + "apps": [ + { + "name": "7-Zip", + "id": "7zip.7zip", + "source": "winget" + }, + { + "name": "Company Portal", + "id": "9WZDNCRFJ3PZ", + "source": "msstore" + } + ] +} \ No newline at end of file diff --git a/FFUDevelopment/Apps/AppsList.txt b/FFUDevelopment/Apps/AppsList.txt deleted file mode 100644 index b4903ae..0000000 --- a/FFUDevelopment/Apps/AppsList.txt +++ /dev/null @@ -1,2 +0,0 @@ -winget:7-Zip -store:Company Portal \ No newline at end of file diff --git a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd index fde1977..1fefc88 100644 --- a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd +++ b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd @@ -33,20 +33,20 @@ for /d %%D in ("%basepath%\*") do ( set "licensefile=%%F" ) if defined mainpackage ( + set "dism_command=DISM /Online /Add-ProvisionedAppxPackage /PackagePath:"!mainpackage!"" if exist "!dependenciesfolder!" ( - set "dism_command=DISM /Online /Add-ProvisionedAppxPackage /PackagePath:"!mainpackage!"" for %%G in ("!dependenciesfolder!\*") do ( set "dism_command=!dism_command! /DependencyPackagePath:"%%G"" ) - if defined licensefile ( - set "dism_command=!dism_command! /LicensePath:"!licensefile!"" - ) else ( - set "dism_command=!dism_command! /SkipLicense" - ) - set "dism_command=!dism_command! /Region:All" - echo !dism_command! - !dism_command! ) + if defined licensefile ( + set "dism_command=!dism_command! /LicensePath:"!licensefile!"" + ) else ( + set "dism_command=!dism_command! /SkipLicense" + ) + set "dism_command=!dism_command! /Region:All" + echo !dism_command! + !dism_command! ) ) :remaining diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index ef6da11..f0dadac 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1737,12 +1737,13 @@ function New-WinGetSettings { function Add-Win32SilentInstallCommand { param ( [string]$AppFolder, - [string]$AppFolderPath + [string]$AppFolderPath, + [string]$LineNumber ) $appName = $AppFolder - $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include *.exe, *.msi -File -ErrorAction Stop + $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction Stop $installer = Split-Path -Path $installerPath -Leaf - $yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include *.yaml -File -ErrorAction Stop + $yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop $yamlContent = Get-Content -Path $yamlFile -Raw $silentInstallSwitch = [regex]::Match($yamlContent, 'Silent:\s*(.+)').Groups[1].Value $silentInstallSwitch = $silentInstallSwitch.Replace("'", "").Trim() @@ -1758,6 +1759,11 @@ function Add-Win32SilentInstallCommand { elseif ($installerFileExtension -eq ".msi") { $silentInstallCommand = "msiexec /i `"D:\win32\$appFolder\$installer`" $silentInstallSwitch" } + else { + WriteLog "No win32 app installers were found. Skipping the inclusion of $appName" + Remove-Item -Path $AppFolderPath -Recurse -Force + return + } $cmdFile = "$AppsPath\InstallAppsandSysprep.cmd" $cmdContent = Get-Content -Path $cmdFile $cmdContent = $cmdContent[0..($lineNumber - 2)] + $silentInstallCommand.Trim() + $cmdContent[($lineNumber - 1)..($cmdContent.Length - 1)] @@ -1767,38 +1773,59 @@ function Add-Win32SilentInstallCommand { function Get-WinGetApp { param ( - [string]$WinGetApp, + [string]$WinGetAppName, + [string]$WinGetAppId, [int]$LineNumber ) - $wingetSearchResult = & winget.exe search --name "$WinGetApp" --exact --accept-source-agreements --source winget + $wingetSearchResult = & winget.exe search --id "$WinGetAppId" --exact --accept-source-agreements --source winget if ($wingetSearchResult -contains "No package found matching input criteria.") { - WriteLog "$WinGetApp not found in WinGet repository. Skipping download." + WriteLog "$WinGetAppName not found in WinGet repository. Skipping download." return } - $appFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $WinGetApp + $appFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $WinGetAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null $appFolder = Split-Path -Path $appFolderPath -Leaf - WriteLog "Downloading $WinGetApp..." - $wingetDownloadResult = & winget.exe download --name "$WinGetApp" --exact --download-directory "$appFolderPath" --scope machine --source winget --architecture "$WindowsArch" | Out-String + WriteLog "Downloading $WinGetAppName..." + $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget --architecture "$WindowsArch" | Out-String if ($wingetDownloadResult -match "No applicable installer found") { - $wingetDownloadResult = & winget.exe download --name "$WinGetApp" --exact --download-directory "$appFolderPath" --scope machine --source winget | Out-String + $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget | Out-String } if ($wingetDownloadResult -notmatch "Installer downloaded") { - WriteLog "$WinGetApp did not successfully download." + WriteLog "$WinGetAppName did not successfully download." Remove-Item -Path $appFolderPath -Recurse -Force return } - WriteLog "$WinGetApp has completed downloading." - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath + WriteLog "$WinGetAppName has completed downloading to $appFolderPath" + $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Exclude "*.yaml", "*.xml" -File -ErrorAction Stop + $installer = Split-Path -Path $installerPath -Leaf + $installerFileExtension = [System.IO.Path]::GetExtension($installer) + $uwpExtensions = @(".appx", ".appxbundle", ".msix", ".msixbundle") + if ($uwpExtensions -contains $installerFileExtension) { + New-Item -Path "$AppsPath\MSStore\$WinGetAppName" -ItemType Directory -Force | Out-Null + Move-Item -Path "$appFolderPath\*" -Destination "$AppsPath\MSStore\$WinGetAppName" -Force + Remove-Item -Path $appFolderPath -Force + $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" + if ($cmdContent -match 'set "INSTALL_STOREAPPS=false"') { + WriteLog "Setting INSTALL_STOREAPPS flag to true in InstallAppsandSysprep.cmd file." + $updatedcmdContent = $cmdContent -replace 'set "INSTALL_STOREAPPS=false"', 'set "INSTALL_STOREAPPS=true"' + Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $updatedcmdContent + } + # Since a Win32 app was not received, returning false to not increment line number for silent install command + return $false + } + else { + Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath -LineNumber $LineNumber + } } function Get-StoreApp { param ( - [string]$StoreApp + [string]$StoreAppName, + [string]$StoreAppId ) - $wingetSearchResult = & winget.exe search --name --exact "$StoreApp" --accept-source-agreements --source msstore + $wingetSearchResult = & winget.exe search "$StoreAppId" --accept-source-agreements --source msstore if ($wingetSearchResult -contains "No package found matching input criteria.") { - WriteLog "$StoreApp not found in WinGet repository. Skipping download." + WriteLog "$StoreAppName not found in WinGet repository. Skipping download." return } # Skip the header lines and get the line with the app information @@ -1806,22 +1833,22 @@ function Get-StoreApp { # Split the line by whitespace and get the second-to-last item (the Id) $appID = ($appResult -split '\s+')[-2] # Checking app ID to determine if store app is a win32 app - WriteLog "Checking if $StoreApp is a win32 app..." + WriteLog "Checking if $StoreAppName is a win32 app..." if ($appID.StartsWith("XP")) { - WriteLog "$StoreApp is a win32 app. Adding to $AppsPath\win32 folder" - $appFolderPath = Join-Path -Path "$AppsPath\win32" -ChildPath $StoreApp + WriteLog "$StoreAppName is a win32 app. Adding to $AppsPath\win32 folder" + $appFolderPath = Join-Path -Path "$AppsPath\win32" -ChildPath $StoreAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null $appFolder = Split-Path -Path $appFolderPath -Leaf - WriteLog "Downloading $StoreApp for $WindowsArch architecture..." - $wingetDownloadResult = & winget.exe download --name --exact "$StoreApp" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String + WriteLog "Downloading $StoreAppName for $WindowsArch architecture..." + $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." - $wingetDownloadResult = & winget.exe download --name --exact "$StoreApp" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match $StoreApp){ - WriteLog "Downloaded $StoreApp without specifying architecture." + $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String + if ($wingetDownloadResult -match $StoreAppName){ + WriteLog "Downloaded $StoreAppName without specifying architecture." } else { - WriteLog "No installer found for $StoreApp. Skipping download." + WriteLog "No installer found for $StoreAppName. Skipping download." Remove-Item -Path $appFolderPath -Recurse -Force return } @@ -1829,19 +1856,19 @@ function Get-StoreApp { Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath return } - $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreApp + $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null # Invoke-Process is not used here because it terminates the script if the exit code of the process is not zero. # WinGet's download command will return a non-zero exit code when downloading store apps, as attempting to download the license file always appears to cause an error. - WriteLog "Downloading $StoreApp and dependencies..." + WriteLog "Downloading $StoreAppName and dependencies..." WriteLog 'MSStore app downloads require authentication with an Entra ID account. You may be prompted twice for credentials, once for the app and another for the license file.' - $wingetDownloadResult = & winget.exe download --name --exact "$StoreApp" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String + $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String # For some apps, specifying the architecture leads to no results found for the app. In those cases, the command will be run without the architecture parameter. if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." - $wingetDownloadResult = & winget.exe download --name --exact "$StoreApp" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match $StoreApp){ - WriteLog "Downloaded $StoreApp without specifying architecture." + $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String + if ($wingetDownloadResult -match $StoreAppName){ + WriteLog "Downloaded $StoreAppName without specifying architecture." # If $WindowsArch -eq 'ARM64', remove all dependency files that are not ARM64 if ($WindowsArch -eq 'ARM64') { WriteLog 'Windows architecture is ARM64. Removing dependencies that are not ARM64.' @@ -1857,14 +1884,14 @@ function Get-StoreApp { } } else { - WriteLog "No installer found for $StoreApp. Skipping download." + WriteLog "No installer found for $StoreAppName. Skipping download." Remove-Item -Path $appFolderPath -Recurse -Force return } } # Many store apps can be found by winget search, but the download of the apps are unsupported. if ($wingetDownloadResult -match "No applicable Microsoft Store package download information found.") { - WriteLog "No applicable Microsoft Store package download information found for $StoreApp. Skipping download." + WriteLog "No applicable Microsoft Store package download information found for $StoreAppName. Skipping download." Remove-Item -Path $appFolderPath -Recurse -Force return } @@ -1874,7 +1901,7 @@ function Get-StoreApp { $updatedcmdContent = $cmdContent -replace 'set "INSTALL_STOREAPPS=false"', 'set "INSTALL_STOREAPPS=true"' Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $updatedcmdContent } - WriteLog "$StoreApp has completed downloading. Identifying the latest version of $StoreApp." + WriteLog "$StoreAppName has completed downloading. Identifying the latest version of $StoreAppName." $packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop # WinGet downloads multiple versions of certain store apps. The latest version of the package will be determined based on the date of the file signature. $latestPackage = "" @@ -1890,7 +1917,7 @@ function Get-StoreApp { } } # Removing all packages that are not the latest version - WriteLog "Latest version of $StoreApp has been identified as $latestPackage. Removing old versions of $StoreApp that may have downloaded." + WriteLog "Latest version of $StoreAppName has been identified as $latestPackage. Removing old versions of $StoreAppName that may have downloaded." foreach ($package in $packages) { if ($package.FullName -ne $latestPackage) { try { @@ -1909,19 +1936,19 @@ function Get-Apps { param ( [string]$AppsList ) - $apps = Get-Content -Path $AppsList + $apps = Get-Content -Path $AppsList -Raw | ConvertFrom-Json if (-not $apps) { WriteLog "No apps were specified in AppsList.txt file." return } $wingetApps = @() $storeApps = @() - $apps | ForEach-Object { - if ($_ -like 'winget:*') { - $wingetApps += $_.Substring(7).Trim() - } - elseif ($_ -like 'store:*') { - $storeApps += $_.Substring(6).Trim() + foreach ($app in $apps.apps) { + if ($app.source -eq "winget") { + $wingetApps += $app + } + elseif ($app.source -eq "msstore") { + $storeApps += $app } } $wingetInstalled = Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" -ErrorAction SilentlyContinue @@ -1949,8 +1976,10 @@ function Get-Apps { } foreach ($wingetApp in $wingetApps) { try { - Get-WinGetApp -WinGetApp $wingetApp -LineNumber $lineNumber - $lineNumber++ + $result = Get-WinGetApp -WinGetAppName $wingetApp.Name -WinGetAppId $wingetApp.Id -LineNumber $lineNumber + if ($null -eq $result) { + $lineNumber++ + } } catch { WriteLog "Error occurred while processing $wingetApp : $_" @@ -1965,7 +1994,7 @@ function Get-Apps { } foreach ($storeApp in $storeApps) { try { - Get-StoreApp -StoreApp $storeApp + Get-StoreApp -StoreAppName $storeApps.Name -StoreAppId $storeApps.Id } catch { WriteLog "Error occurred while processing $storeApp : $_" @@ -3383,7 +3412,7 @@ if ($InstallApps) { exit } WriteLog "$AppsPath\InstallAppsandSysprep.cmd found" - Get-Apps -AppsList "$AppsPath\AppsList.txt" + Get-Apps -AppsList "$AppsPath\AppList.json" if (-not $InstallOffice) { #Modify InstallAppsandSysprep.cmd to REM out the office install command $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" From 146c1601bd47474b2d9997d8603f2a98ea5f764d Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:01:06 -0400 Subject: [PATCH 02/16] Improved handling of store apps that can't be downloaded --- FFUDevelopment/BuildFFUVM.ps1 | 38 ++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f0dadac..d176963 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1738,7 +1738,7 @@ function Add-Win32SilentInstallCommand { param ( [string]$AppFolder, [string]$AppFolderPath, - [string]$LineNumber + [int]$LineNumber ) $appName = $AppFolder $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction Stop @@ -1821,7 +1821,8 @@ function Get-WinGetApp { function Get-StoreApp { param ( [string]$StoreAppName, - [string]$StoreAppId + [string]$StoreAppId, + [int]$LineNumber ) $wingetSearchResult = & winget.exe search "$StoreAppId" --accept-source-agreements --source msstore if ($wingetSearchResult -contains "No package found matching input criteria.") { @@ -1853,21 +1854,33 @@ function Get-StoreApp { return } } - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath - return + Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath -LineNumber $LineNumber + # Since a Win32 app was received, returning false to increment line number for silent install command + return $false } $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null # Invoke-Process is not used here because it terminates the script if the exit code of the process is not zero. # WinGet's download command will return a non-zero exit code when downloading store apps, as attempting to download the license file always appears to cause an error. - WriteLog "Downloading $StoreAppName and dependencies..." + WriteLog "Attempting to download $StoreAppName and dependencies..." WriteLog 'MSStore app downloads require authentication with an Entra ID account. You may be prompted twice for credentials, once for the app and another for the license file.' $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String + if ($wingetDownloadResult -match "The request is not supported") { + WriteLog "The download request is not supported for $StoreAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return + } + # Many store apps can be found by winget search, but the download of the apps are unsupported. + if ($wingetDownloadResult -match "No applicable Microsoft Store package download information found.") { + WriteLog "No applicable Microsoft Store package download information found for $StoreAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return + } # For some apps, specifying the architecture leads to no results found for the app. In those cases, the command will be run without the architecture parameter. if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match $StoreAppName){ + if ($wingetDownloadResult -match "Installer downloaded") { WriteLog "Downloaded $StoreAppName without specifying architecture." # If $WindowsArch -eq 'ARM64', remove all dependency files that are not ARM64 if ($WindowsArch -eq 'ARM64') { @@ -1884,17 +1897,11 @@ function Get-StoreApp { } } else { - WriteLog "No installer found for $StoreAppName. Skipping download." + WriteLog "No installer found for $StoreAppName from the msstore source. Skipping download." Remove-Item -Path $appFolderPath -Recurse -Force return } } - # Many store apps can be found by winget search, but the download of the apps are unsupported. - if ($wingetDownloadResult -match "No applicable Microsoft Store package download information found.") { - WriteLog "No applicable Microsoft Store package download information found for $StoreAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" if ($cmdContent -match 'set "INSTALL_STOREAPPS=false"') { WriteLog "Setting INSTALL_STOREAPPS flag to true in InstallAppsandSysprep.cmd file." @@ -1994,7 +2001,10 @@ function Get-Apps { } foreach ($storeApp in $storeApps) { try { - Get-StoreApp -StoreAppName $storeApps.Name -StoreAppId $storeApps.Id + $result = Get-StoreApp -StoreAppName $storeApp.Name -StoreAppId $storeApp.Id -LineNumber $lineNumber + if ($result -eq $false) { + $lineNumber++ + } } catch { WriteLog "Error occurred while processing $storeApp : $_" From 174c16ecb67b86cc3751273959726bf07351f225 Mon Sep 17 00:00:00 2001 From: Mike Kelly <167896478+MKellyCBSD@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:48:39 -0400 Subject: [PATCH 03/16] Add files via upload Upload BuildUSBDrives.ps1 --- FFUDevelopment/BuildUSBDrives.ps1 | 245 ++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 FFUDevelopment/BuildUSBDrives.ps1 diff --git a/FFUDevelopment/BuildUSBDrives.ps1 b/FFUDevelopment/BuildUSBDrives.ps1 new file mode 100644 index 0000000..a797701 --- /dev/null +++ b/FFUDevelopment/BuildUSBDrives.ps1 @@ -0,0 +1,245 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $True, Position = 0)] + $DeployISOPath, + [Switch]$DisableAutoPlay +) +$Host.UI.RawUI.WindowTitle = 'Imaging Tool USB Creator' + +if($DeployISOPath){ +$DevelopmentPath = $DeployISOPath | Split-Path +$ImagesPath = "$DevelopmentPath\FFU" +function WriteLog($LogText) { +$LogFileName = '\Script.log' +$LogFile = $DevelopmentPath + $LogFilename + Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" -Force -ErrorAction SilentlyContinue + Write-Verbose $LogText +} + +function Write-ProgressLog { + param( + [string]$Activity, + [string]$Status + ) + Write-Progress -Activity $Activity -Status $Status -PercentComplete (($currentStep / $totalSteps) * 100) + WriteLog $Status + $script:currentStep++ + + } +Function Get-RemovableDrive { +writelog "Get information for all removable drives" +$USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media"} +If($USBDrives -and ($null -eq $USBDrives.count)) { + $USBDrivesCount = 1 + } + else { + $USBDrivesCount = $USBDrives.Count + } + WriteLog "Found $USBDrivesCount USB drives" + + if ($null -eq $USBDrives) { + WriteLog "No removable USB drive found. Exiting" + Write-Error "No removable USB drive found. Exiting" + Pause + exit 1 + } + return $USBDrives, $USBDrivesCount + } + +Function Build-DeploymentUSB{ + param( + [Array]$Drives + ) + writelog "Creating list of FFU image files" + $Images = Get-ChildItem -Path $FFUPath -Filter "*.ffu" -File -Recurse + writelog "Checking if drivers are present in the drivers folder" + $Drivers = Get-ChildItem -Path $DriversPath -Recurse + $DrivesCount = $Drives.Count + Write-ProgressLog "Create Imaging Tool" "Creating partitions..." + writelog "Create job to partition each usb drive" + foreach ($USBDrive in $Drives) { + $DriveNumber = $USBDrive.DeviceID.Replace("\\.\PHYSICALDRIVE", "") + $Model = $USBDrive.model + $ScriptBlock = { + param($DriveNumber) + Clear-Disk -Number $DriveNumber -RemoveData -RemoveOEM -Confirm:$false + $Disk = Get-Disk -Number $DriveNumber + $PartitionStyle = $Disk.PartitionStyle + if($PartitionStyle -ne 'MBR'){ + $Disk | Set-Disk -PartitionStyle MBR + } + $BootPartition = New-Partition -DiskNumber $DriveNumber -Size 2GB -IsActive -AssignDriveLetter + $DeployPartition = New-Partition -DiskNumber $DriveNumber -UseMaximumSize -AssignDriveLetter + Format-Volume -Partition $BootPartition -FileSystem FAT32 -NewFileSystemLabel "Boot" -Confirm:$false + Format-Volume -Partition $DeployPartition -FileSystem NTFS -NewFileSystemLabel "Deploy" -Confirm:$false + } + WriteLog "Start job to create BOOT and Deploy partitions on drive number $DriveNumber" + Start-Job -ScriptBlock $ScriptBlock -ArgumentList $DriveNumber | Out-Null + } + writelog "Wait for partitioning jobs to complete" + Get-Job | Wait-Job | Out-Null + if($DrivesCount -gt 1){ + writelog "Get file system information for all drives" + $Partitions = Get-Partition | Get-Volume + }else{ + writelog "Get file system information for drive number $DiskNumber" + $Partitions = Get-Partition -DiskNumber $DriveNumber | Get-Volume + } +writelog "Get drive letter for all volumes labeled:BOOT" +$BootDrives = ($Partitions | Where-Object { $_.FileSystemLabel -eq "BOOT"}).DriveLetter +writelog "Get drive letter for all volumes labeled:Deploy" +$DeployDrives = ($Partitions | Where-Object { $_.FileSystemLabel -eq "Deploy"}).DriveLetter +writelog "Mount Deployment .iso image" +$ISOMountPoint = (Mount-DiskImage -ImagePath "$DeployISOPath" -PassThru | Get-Volume).DriveLetter + ":\" +writelog "Copying boot files to all drives labeled BOOT concurrently" +foreach ($Drive in $BootDrives) { +$Destination = $Drive + ":\" + $jobScriptBlock = { + param ( + [string]$SFolder, + [string]$DFolder + ) + Robocopy $SFolder $DFolder /E /COPYALL /R:5 /W:5 /J + } + WriteLog "Start job to copy all boot files to $Destination" + Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ISOMountPoint, $Destination | Out-Null +} +if($Images){ +writelog "Copying FFU image files to all drives labeled deploy concurrently" +foreach ($Drive in $DeployDrives) { +$Destination = $Drive + ":\Images" + $jobScriptBlock = { + param ( + [string]$SFolder, + [string]$DFolder + ) + New-Item -Path $DFolder -ItemType Directory -Force -Confirm: $false | Out-Null + Robocopy $SFolder $DFolder /E /COPYALL /R:5 /W:5 /J + } + + WriteLog "Start job to copy all FFU files to $Destination" + Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ImagesPath, $Destination | Out-Null + } +} +if(!($Images)){ + foreach ($Drive in $DeployDrives) { + WriteLog "Create images directory" + $drivepath = $Drive + ":\" + New-Item -Path "$drivepath" -Name Images -ItemType Directory -Force -Confirm: $false | Out-Null + } +} +if($Drivers){ +writelog "Copying driver files to all drives labeled deploy concurrently" +foreach ($Drive in $DeployDrives) { +$Destination = $Drive + ":\Drivers" + $jobScriptBlock = { + param ( + [string]$SFolder, + [string]$DFolder + ) + New-Item -Path $DFolder -ItemType Directory -Force -Confirm: $false | Out-Null + Robocopy $SFolder $DFolder /E /COPYALL /R:5 /W:5 /J + } + WriteLog "Start job to copy all drivers to $Destination" + Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $DriversPath, $Destination | Out-Null +} +} +if(!($Drivers)){ + foreach ($Drive in $DeployDrives) { + WriteLog "Create drivers directory" + $drivepath = $Drive + ":\" + New-Item -Path "$drivepath" -Name Drivers -ItemType Directory -Force -Confirm: $false | Out-Null + } +} +if($DrivesCount -gt 1){ +Write-ProgressLog "Create Imaging Tool" "Building $DrivesCount drives concurrently...Please be patient..." +}else{ +Write-ProgressLog "Create Imaging Tool" "Building the imaging tool on $model...Please be patient..." +} +Get-Job | Wait-Job | Out-Null + +Dismount-DiskImage -ImagePath $DeployISOPath | Out-Null +Write-ProgressLog "Create Imaging Tool" "Drive creation jobs completed..." +} + +Function New-DeploymentUSB { + param( + [Array]$Drives, + [int]$Count, + [String]$FFUPath = "$DevelopmentPath\FFU", + [String]$DriversPath = "$DevelopmentPath\Drivers" + + ) + + $Drivelist = @() + writelog "Creating a USB drive selection list" + for($i=0;$i -le $Count -1;$i++){ + $DriveModel = $Drives[$i].Model + $DriveSize = [math]::round($Drives[$i].size/1GB, 2) + $DiskNumber = $Drives[$i].DeviceID.Replace("\\.\PHYSICALDRIVE", "") + $Properties = [ordered]@{Number = $i + 1 ; DriveNumber = $DiskNumber ; DriveModel = $driveModel ; 'Size (GB)' = $DriveSize} + + $Drivelist += New-Object PSObject -Property $Properties + } + if($Count -gt 1){ + $Last = $Count+1 + $Drivelist += New-Object -TypeName PSObject -Property @{ Number = "$last"; DriveModel = "Select this option to use all ($count) inserted USB Drives" } + } + $Drivelist | Format-Table -AutoSize -Property Number, DriveModel , 'Size (GB)' + do { + try { + $var = $true + $DriveSelected = Read-Host 'Enter the drive number to apply the .iso to' + $DriveSelected = ($DriveSelected -as [int]) -1 + writelog "Drive $DriveSelected selected" + } + + catch { + Write-Host 'Input was not in correct format. Please enter a valid FFU number' + $var = $false + } + } until (($DriveSelected -le $Count -1 -or $last) -and $var) + if($DisableAutoPlay){ + WriteLog "Setting the registry key to disable autoplay for all drives" + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value 1 -Type DWORD + } + WriteLog "Closing all MMC windows to prevent drive lock errors" + Stop-Process -Name mmc -ErrorAction SilentlyContinue + WriteLog "Closing all Diskpart windows to prevent drive lock errors" + Stop-Process -Name diskpart -ErrorAction SilentlyContinue + $Selection = $Drivelist[$DriveSelected].Number + $totalSteps = 5 + if($Selection -eq $last){ + Read-Host -Prompt "ALL DRIVES SELECTED! WILL ERASE ALL CURRENTLY CONNECTED USB DRIVES!! Press ENTER to continue" + Build-DeploymentUSB -Drives $Drives + }else{ + Read-Host -Prompt "Drive number $Selection was selected. Press ENTER to continue" + Build-DeploymentUSB -Drives $Drives[$DriveSelected] + } + WriteLog "Setting the registry key to re-enable autoplay for all drives" + if($DisableAutoPlay){ + Write-ProgressLog "Create Imaging Tool" "Enabling Autoplay" + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value 0 -Type DWORD + } + Write-ProgressLog "Create Imaging Tool" "Completed!" +} +#Get USB Drive and create log file +if(Test-Path "$DevelopmentPath\Script.log"){ +Remove-Item -Path "$DevelopmentPath\Script.log" -Force -Confirm:$false +New-item -Path $DevelopmentPath -Name 'Script.log' -ItemType "file" -Force | Out-Null +} +WriteLog 'Begin Logging' +WriteLog 'Getting USB drive information and usb drive count' +$USBDrives,$USBDrivesCount = Get-RemovableDrive +WriteLog 'Setting first step for percentage progress bar' +$currentStep = 1 +New-DeploymentUSB -Drives $USBDrives -Count $USBDrivesCount + +read-host -Prompt "USB drive creation complete. Press ENTER to exit" + +Exit +}else{ +Write-Host "No .ISO file selected..." +read-host "Press ENTER to Exit..." +Exit +} From 325413de13a09bf1f4a4acbf62744e3b806e5798 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:27:27 -0400 Subject: [PATCH 04/16] Moved code into separate functions, refactored existing functions, fixed logical errors in if-statements --- FFUDevelopment/BuildFFUVM.ps1 | 142 ++++++++++++++-------------------- 1 file changed, 57 insertions(+), 85 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index d176963..0b88fbc 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1695,6 +1695,22 @@ function Install-WinGet { } } +function Confirm-WinGetInstallation { + $wingetPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" + if (-not (Test-Path $wingetPath)) { + WriteLog "WinGet is not installed. Downloading preview version of WinGet and its dependencies..." + Install-WinGet -InstallWithDependencies $true + } + elseif (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + WriteLog "WinGet is not on the path. Downloading preview version of WinGet without dependencies..." + Install-WinGet -InstallWithDependencies $false + } + elseif (-not ((& winget.exe --version) -like "*preview*")) { + WriteLog "The preview version of WinGet is not installed. Downloading preview version of WinGet without dependencies..." + Install-WinGet -InstallWithDependencies $false + } +} + function New-WinGetSettings { $wingetSettingsFile = "$env:LOCALAPPDATA\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json" $wingetSettings = @( @@ -1742,28 +1758,26 @@ function Add-Win32SilentInstallCommand { ) $appName = $AppFolder $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction Stop - $installer = Split-Path -Path $installerPath -Leaf + if (-not $installerPath) { + WriteLog "No win32 app installers were found. Skipping the inclusion of $AppFolder" + Remove-Item -Path $AppFolderPath -Recurse -Force + return $false + } $yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop $yamlContent = Get-Content -Path $yamlFile -Raw - $silentInstallSwitch = [regex]::Match($yamlContent, 'Silent:\s*(.+)').Groups[1].Value - $silentInstallSwitch = $silentInstallSwitch.Replace("'", "").Trim() + $silentInstallSwitch = [regex]::Match($yamlContent, 'Silent:\s*(.+)').Groups[1].Value.Replace("'", "").Trim() if (-not $silentInstallSwitch) { WriteLog "Silent install switch for $appName could not be found. Skipping the inclusion of $appName." Remove-Item -Path $appFolderPath -Recurse -Force - return + return $false } - $installerFileExtension = [System.IO.Path]::GetExtension($installer) - if ($installerFileExtension -eq ".exe") { + $installer = Split-Path -Path $installerPath -Leaf + if ($installerPath.Extension -eq ".exe") { $silentInstallCommand = "`"D:\win32\$appFolder\$installer`" $silentInstallSwitch" } - elseif ($installerFileExtension -eq ".msi") { + elseif ($installerPath.Extension -eq ".msi") { $silentInstallCommand = "msiexec /i `"D:\win32\$appFolder\$installer`" $silentInstallSwitch" } - else { - WriteLog "No win32 app installers were found. Skipping the inclusion of $appName" - Remove-Item -Path $AppFolderPath -Recurse -Force - return - } $cmdFile = "$AppsPath\InstallAppsandSysprep.cmd" $cmdContent = Get-Content -Path $cmdFile $cmdContent = $cmdContent[0..($lineNumber - 2)] + $silentInstallCommand.Trim() + $cmdContent[($lineNumber - 1)..($cmdContent.Length - 1)] @@ -1771,6 +1785,16 @@ function Add-Win32SilentInstallCommand { Set-Content -Path $cmdFile -Value $cmdContent } +function Set-InstallStoreAppsFlag { + $cmdPath = "$AppsPath\InstallAppsandSysprep.cmd" + $cmdContent = Get-Content -Path $cmdPath + if ($cmdContent -match 'set "INSTALL_STOREAPPS=false"') { + WriteLog "Setting INSTALL_STOREAPPS flag to true in InstallAppsandSysprep.cmd file." + $updatedcmdContent = $cmdContent -replace 'set "INSTALL_STOREAPPS=false"', 'set "INSTALL_STOREAPPS=true"' + Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $updatedcmdContent + } +} + function Get-WinGetApp { param ( [string]$WinGetAppName, @@ -1780,11 +1804,11 @@ function Get-WinGetApp { $wingetSearchResult = & winget.exe search --id "$WinGetAppId" --exact --accept-source-agreements --source winget if ($wingetSearchResult -contains "No package found matching input criteria.") { WriteLog "$WinGetAppName not found in WinGet repository. Skipping download." - return + # Return false to not increment line number for silent install command. + return $false } $appFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $WinGetAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null - $appFolder = Split-Path -Path $appFolderPath -Leaf WriteLog "Downloading $WinGetAppName..." $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget --architecture "$WindowsArch" | Out-String if ($wingetDownloadResult -match "No applicable installer found") { @@ -1793,28 +1817,20 @@ function Get-WinGetApp { if ($wingetDownloadResult -notmatch "Installer downloaded") { WriteLog "$WinGetAppName did not successfully download." Remove-Item -Path $appFolderPath -Recurse -Force - return + return $false } WriteLog "$WinGetAppName has completed downloading to $appFolderPath" $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Exclude "*.yaml", "*.xml" -File -ErrorAction Stop - $installer = Split-Path -Path $installerPath -Leaf - $installerFileExtension = [System.IO.Path]::GetExtension($installer) $uwpExtensions = @(".appx", ".appxbundle", ".msix", ".msixbundle") - if ($uwpExtensions -contains $installerFileExtension) { + if ($uwpExtensions -contains $installerPath.Extension) { New-Item -Path "$AppsPath\MSStore\$WinGetAppName" -ItemType Directory -Force | Out-Null Move-Item -Path "$appFolderPath\*" -Destination "$AppsPath\MSStore\$WinGetAppName" -Force Remove-Item -Path $appFolderPath -Force - $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" - if ($cmdContent -match 'set "INSTALL_STOREAPPS=false"') { - WriteLog "Setting INSTALL_STOREAPPS flag to true in InstallAppsandSysprep.cmd file." - $updatedcmdContent = $cmdContent -replace 'set "INSTALL_STOREAPPS=false"', 'set "INSTALL_STOREAPPS=true"' - Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $updatedcmdContent - } - # Since a Win32 app was not received, returning false to not increment line number for silent install command + Set-InstallStoreAppsFlag return $false } else { - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath -LineNumber $LineNumber + Add-Win32SilentInstallCommand -AppFolder $WinGetAppName -AppFolderPath $appFolderPath -LineNumber $LineNumber } } @@ -1839,13 +1855,12 @@ function Get-StoreApp { WriteLog "$StoreAppName is a win32 app. Adding to $AppsPath\win32 folder" $appFolderPath = Join-Path -Path "$AppsPath\win32" -ChildPath $StoreAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null - $appFolder = Split-Path -Path $appFolderPath -Leaf WriteLog "Downloading $StoreAppName for $WindowsArch architecture..." $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match $StoreAppName){ + if ($wingetDownloadResult -match "Installer downloaded"){ WriteLog "Downloaded $StoreAppName without specifying architecture." } else { @@ -1854,7 +1869,7 @@ function Get-StoreApp { return } } - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath -LineNumber $LineNumber + Add-Win32SilentInstallCommand -AppFolder $StoreAppName -AppFolderPath $appFolderPath -LineNumber $LineNumber # Since a Win32 app was received, returning false to increment line number for silent install command return $false } @@ -1865,22 +1880,11 @@ function Get-StoreApp { WriteLog "Attempting to download $StoreAppName and dependencies..." WriteLog 'MSStore app downloads require authentication with an Entra ID account. You may be prompted twice for credentials, once for the app and another for the license file.' $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String - if ($wingetDownloadResult -match "The request is not supported") { - WriteLog "The download request is not supported for $StoreAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } - # Many store apps can be found by winget search, but the download of the apps are unsupported. - if ($wingetDownloadResult -match "No applicable Microsoft Store package download information found.") { - WriteLog "No applicable Microsoft Store package download information found for $StoreAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } # For some apps, specifying the architecture leads to no results found for the app. In those cases, the command will be run without the architecture parameter. if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match "Installer downloaded") { + if ($wingetDownloadResult -match "Microsoft Store package download completed") { WriteLog "Downloaded $StoreAppName without specifying architecture." # If $WindowsArch -eq 'ARM64', remove all dependency files that are not ARM64 if ($WindowsArch -eq 'ARM64') { @@ -1902,27 +1906,16 @@ function Get-StoreApp { return } } - $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" - if ($cmdContent -match 'set "INSTALL_STOREAPPS=false"') { - WriteLog "Setting INSTALL_STOREAPPS flag to true in InstallAppsandSysprep.cmd file." - $updatedcmdContent = $cmdContent -replace 'set "INSTALL_STOREAPPS=false"', 'set "INSTALL_STOREAPPS=true"' - Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $updatedcmdContent + elseif ($wingetDownloadResult -notmatch "Microsoft Store package download completed") { + WriteLog "Download not supported for $StoreAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return } + Set-InstallStoreAppsFlag WriteLog "$StoreAppName has completed downloading. Identifying the latest version of $StoreAppName." $packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop # WinGet downloads multiple versions of certain store apps. The latest version of the package will be determined based on the date of the file signature. - $latestPackage = "" - $latestDate = [datetime]::MinValue - foreach ($package in $packages) { - $signature = Get-AuthenticodeSignature -FilePath $package.FullName - if ($signature.Status -eq 'Valid') { - $signatureDate = $signature.SignerCertificate.NotBefore - if ($signatureDate -gt $latestDate) { - $latestPackage = $package.FullName - $latestDate = $signatureDate - } - } - } + $latestPackage = $packages | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1 # Removing all packages that are not the latest version WriteLog "Latest version of $StoreAppName has been identified as $latestPackage. Removing old versions of $StoreAppName that may have downloaded." foreach ($package in $packages) { @@ -1945,35 +1938,12 @@ function Get-Apps { ) $apps = Get-Content -Path $AppsList -Raw | ConvertFrom-Json if (-not $apps) { - WriteLog "No apps were specified in AppsList.txt file." + WriteLog "No apps were specified in AppList.json file." return } - $wingetApps = @() - $storeApps = @() - foreach ($app in $apps.apps) { - if ($app.source -eq "winget") { - $wingetApps += $app - } - elseif ($app.source -eq "msstore") { - $storeApps += $app - } - } - $wingetInstalled = Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" -ErrorAction SilentlyContinue - if (-not $wingetInstalled) { - WriteLog "WinGet is not installed. Downloading preview version of WinGet and its dependencies..." - Install-WinGet -InstallWithDependencies $true - } - $wingetOnPath = Get-Command winget -ErrorAction SilentlyContinue - if (-not $wingetOnPath) { - WriteLog "WinGet is not on the path. Downloading preview version of WinGet without dependencies..." - Install-WinGet -InstallWithDependencies $false - } - $wingetVersion = & winget.exe --version - # Preview release is needed to enable storeDownload experimental feature - if (-not ($wingetVersion -like "*preview*")) { - WriteLog "The preview version of WinGet is not installed. Downloading preview version of WinGet without dependencies..." - Install-WinGet -InstallWithDependencies $false - } + $wingetApps = $apps.apps | Where-Object { $_.source -eq "winget" } + $storeApps = $apps.apps | Where-Object { $_.source -eq "msstore" } + Confirm-WinGetInstallation $lineNumber = 13 $win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32" $storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore" @@ -1986,6 +1956,7 @@ function Get-Apps { $result = Get-WinGetApp -WinGetAppName $wingetApp.Name -WinGetAppId $wingetApp.Id -LineNumber $lineNumber if ($null -eq $result) { $lineNumber++ + WriteLog "Line number incremented to $lineNumber" } } catch { @@ -2004,6 +1975,7 @@ function Get-Apps { $result = Get-StoreApp -StoreAppName $storeApp.Name -StoreAppId $storeApp.Id -LineNumber $lineNumber if ($result -eq $false) { $lineNumber++ + WriteLog "Line number incremented to $lineNumber" } } catch { From 3f0377fbf99481af440a16c49c75f5f9a31ff826 Mon Sep 17 00:00:00 2001 From: Matthew Haley <30245+mhaley@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:45:44 -0700 Subject: [PATCH 05/16] Assign drive letter to recovery partition before modification Assign drive letter so copy succeeds, remove when finished. --- FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index c6b9491..4c9c69d 100644 --- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 +++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 @@ -495,10 +495,12 @@ $WinRE = $USBDrive + "WinRE\winre.wim" If (Test-Path -Path $WinRE) { WriteLog 'Copying modified WinRE to Recovery directory' + Get-Disk | Where-Object Number -eq $DiskID | Get-Partition | Where-Object Type -eq Recovery | Set-Partition -NewDriveLetter R 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" + Get-Disk | Where-Object Number -eq $DiskID | Get-Partition | Where-Object Type -eq Recovery | Remove-PartitionAccessPath -AccessPath R: WriteLog 'Registering location of recovery tools succeeded' } # else From 67cc8c122546d7f45b7c41f2d242ea1fdb167d9c Mon Sep 17 00:00:00 2001 From: Mike Kelly <167896478+MKellyCBSD@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:29:58 -0400 Subject: [PATCH 06/16] Update BuildUSBDrives.ps1 Update code --- FFUDevelopment/BuildUSBDrives.ps1 | 67 ++++++++++--------------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/FFUDevelopment/BuildUSBDrives.ps1 b/FFUDevelopment/BuildUSBDrives.ps1 index a797701..514acce 100644 --- a/FFUDevelopment/BuildUSBDrives.ps1 +++ b/FFUDevelopment/BuildUSBDrives.ps1 @@ -8,28 +8,15 @@ $Host.UI.RawUI.WindowTitle = 'Imaging Tool USB Creator' if($DeployISOPath){ $DevelopmentPath = $DeployISOPath | Split-Path -$ImagesPath = "$DevelopmentPath\FFU" function WriteLog($LogText) { $LogFileName = '\Script.log' $LogFile = $DevelopmentPath + $LogFilename Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" -Force -ErrorAction SilentlyContinue Write-Verbose $LogText } - -function Write-ProgressLog { - param( - [string]$Activity, - [string]$Status - ) - Write-Progress -Activity $Activity -Status $Status -PercentComplete (($currentStep / $totalSteps) * 100) - WriteLog $Status - $script:currentStep++ - - } -Function Get-RemovableDrive { -writelog "Get information for all removable drives" -$USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media"} -If($USBDrives -and ($null -eq $USBDrives.count)) { +Function Get-USBDrive { + $USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='Removable Media'") + If ($USBDrives -and ($null -eq $USBDrives.count)) { $USBDrivesCount = 1 } else { @@ -40,23 +27,20 @@ If($USBDrives -and ($null -eq $USBDrives.count)) { if ($null -eq $USBDrives) { WriteLog "No removable USB drive found. Exiting" Write-Error "No removable USB drive found. Exiting" - Pause exit 1 } - return $USBDrives, $USBDrivesCount - } - + return $USBDrives, $USBDrivesCount +} Function Build-DeploymentUSB{ param( [Array]$Drives ) - writelog "Creating list of FFU image files" + writelog "Checking if ffu files are present in the ffu folder" $Images = Get-ChildItem -Path $FFUPath -Filter "*.ffu" -File -Recurse writelog "Checking if drivers are present in the drivers folder" $Drivers = Get-ChildItem -Path $DriversPath -Recurse $DrivesCount = $Drives.Count - Write-ProgressLog "Create Imaging Tool" "Creating partitions..." - writelog "Create job to partition each usb drive" + Writelog "Creating partitions..." foreach ($USBDrive in $Drives) { $DriveNumber = $USBDrive.DeviceID.Replace("\\.\PHYSICALDRIVE", "") $Model = $USBDrive.model @@ -107,27 +91,19 @@ $Destination = $Drive + ":\" if($Images){ writelog "Copying FFU image files to all drives labeled deploy concurrently" foreach ($Drive in $DeployDrives) { -$Destination = $Drive + ":\Images" +$Destination = $Drive + ":\" $jobScriptBlock = { param ( [string]$SFolder, [string]$DFolder ) - New-Item -Path $DFolder -ItemType Directory -Force -Confirm: $false | Out-Null Robocopy $SFolder $DFolder /E /COPYALL /R:5 /W:5 /J } WriteLog "Start job to copy all FFU files to $Destination" - Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ImagesPath, $Destination | Out-Null + Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $FFUPath, $Destination | Out-Null } } -if(!($Images)){ - foreach ($Drive in $DeployDrives) { - WriteLog "Create images directory" - $drivepath = $Drive + ":\" - New-Item -Path "$drivepath" -Name Images -ItemType Directory -Force -Confirm: $false | Out-Null - } -} if($Drivers){ writelog "Copying driver files to all drives labeled deploy concurrently" foreach ($Drive in $DeployDrives) { @@ -152,14 +128,14 @@ if(!($Drivers)){ } } if($DrivesCount -gt 1){ -Write-ProgressLog "Create Imaging Tool" "Building $DrivesCount drives concurrently...Please be patient..." +Writelog "Building $DrivesCount drives concurrently...Please be patient..." }else{ -Write-ProgressLog "Create Imaging Tool" "Building the imaging tool on $model...Please be patient..." +Writelog "Building the imaging tool on $model...Please be patient..." } Get-Job | Wait-Job | Out-Null Dismount-DiskImage -ImagePath $DeployISOPath | Out-Null -Write-ProgressLog "Create Imaging Tool" "Drive creation jobs completed..." +Writelog "Drive creation jobs completed..." } Function New-DeploymentUSB { @@ -178,7 +154,7 @@ Function New-DeploymentUSB { $DriveSize = [math]::round($Drives[$i].size/1GB, 2) $DiskNumber = $Drives[$i].DeviceID.Replace("\\.\PHYSICALDRIVE", "") $Properties = [ordered]@{Number = $i + 1 ; DriveNumber = $DiskNumber ; DriveModel = $driveModel ; 'Size (GB)' = $DriveSize} - + $Drivelist += New-Object PSObject -Property $Properties } if($Count -gt 1){ @@ -191,10 +167,12 @@ Function New-DeploymentUSB { $var = $true $DriveSelected = Read-Host 'Enter the drive number to apply the .iso to' $DriveSelected = ($DriveSelected -as [int]) -1 - writelog "Drive $DriveSelected selected" + if($Last){ + writelog "All drives selected" + }else{ + writelog "Drive $DriveSelected selected"} } - - catch { + catch { Write-Host 'Input was not in correct format. Please enter a valid FFU number' $var = $false } @@ -218,10 +196,10 @@ Function New-DeploymentUSB { } WriteLog "Setting the registry key to re-enable autoplay for all drives" if($DisableAutoPlay){ - Write-ProgressLog "Create Imaging Tool" "Enabling Autoplay" + Writelog "Enabling Autoplay" Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value 0 -Type DWORD } - Write-ProgressLog "Create Imaging Tool" "Completed!" + Writelog "Completed!" } #Get USB Drive and create log file if(Test-Path "$DevelopmentPath\Script.log"){ @@ -230,11 +208,8 @@ New-item -Path $DevelopmentPath -Name 'Script.log' -ItemType "file" -Force | Out } WriteLog 'Begin Logging' WriteLog 'Getting USB drive information and usb drive count' -$USBDrives,$USBDrivesCount = Get-RemovableDrive -WriteLog 'Setting first step for percentage progress bar' -$currentStep = 1 +$USBDrives,$USBDrivesCount = Get-USBDrive New-DeploymentUSB -Drives $USBDrives -Count $USBDrivesCount - read-host -Prompt "USB drive creation complete. Press ENTER to exit" Exit From 6d85e3ef624469fb556b9192714fb7c281eea9ab Mon Sep 17 00:00:00 2001 From: Mike Kelly <167896478+MKellyCBSD@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:38:33 -0400 Subject: [PATCH 07/16] Rename BuildUSBDrives.ps1 to USBImagingToolCreator.ps1 Update Script name --- FFUDevelopment/{BuildUSBDrives.ps1 => USBImagingToolCreator.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename FFUDevelopment/{BuildUSBDrives.ps1 => USBImagingToolCreator.ps1} (100%) diff --git a/FFUDevelopment/BuildUSBDrives.ps1 b/FFUDevelopment/USBImagingToolCreator.ps1 similarity index 100% rename from FFUDevelopment/BuildUSBDrives.ps1 rename to FFUDevelopment/USBImagingToolCreator.ps1 From ab0b7f67eca7bb9904bd05ff8a2cc20894de350a Mon Sep 17 00:00:00 2001 From: Mike Kelly <167896478+MKellyCBSD@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:40:22 -0400 Subject: [PATCH 08/16] Update USBImagingToolCreator.ps1 --- FFUDevelopment/USBImagingToolCreator.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFUDevelopment/USBImagingToolCreator.ps1 b/FFUDevelopment/USBImagingToolCreator.ps1 index 514acce..622e30c 100644 --- a/FFUDevelopment/USBImagingToolCreator.ps1 +++ b/FFUDevelopment/USBImagingToolCreator.ps1 @@ -4,7 +4,7 @@ param( $DeployISOPath, [Switch]$DisableAutoPlay ) -$Host.UI.RawUI.WindowTitle = 'Imaging Tool USB Creator' +$Host.UI.RawUI.WindowTitle = 'USB Imaging Tool Creator' if($DeployISOPath){ $DevelopmentPath = $DeployISOPath | Split-Path From c0bcfd8bf2f14a9bdc467a39034265579e06ec3f Mon Sep 17 00:00:00 2001 From: Mike Kelly <167896478+MKellyCBSD@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:52:09 -0400 Subject: [PATCH 09/16] Update USBImagingToolCreator.ps1 Changed the way the DisableAutoplay switch runs. Now it only changes the registry setting if it the current value is set to "0". then it restores the setting back to the previous value --- FFUDevelopment/USBImagingToolCreator.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/FFUDevelopment/USBImagingToolCreator.ps1 b/FFUDevelopment/USBImagingToolCreator.ps1 index 622e30c..dd3f36d 100644 --- a/FFUDevelopment/USBImagingToolCreator.ps1 +++ b/FFUDevelopment/USBImagingToolCreator.ps1 @@ -177,7 +177,10 @@ Function New-DeploymentUSB { $var = $false } } until (($DriveSelected -le $Count -1 -or $last) -and $var) - if($DisableAutoPlay){ + + $DisableAutoPlayCurrentSetting = (Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name DisableAutoplay).DisableAutoplay + if($DisableAutoPlay -and $DisableAutoPlayCurrentSetting -ne 1){ + writelog "Disable autoPlay current setting is $DisableAutoPlayCurrentSetting" WriteLog "Setting the registry key to disable autoplay for all drives" Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value 1 -Type DWORD } @@ -196,8 +199,8 @@ Function New-DeploymentUSB { } WriteLog "Setting the registry key to re-enable autoplay for all drives" if($DisableAutoPlay){ - Writelog "Enabling Autoplay" - Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value 0 -Type DWORD + Writelog "Setting disable autoplay setting back to $DisableAutoPlayCurrentSetting" + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers" -Name "DisableAutoplay" -Value $DisableAutoPlayCurrentSetting -Type DWORD } Writelog "Completed!" } From a9afba918557c816ee00088e5f108b77c25dce5a Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Thu, 11 Jul 2024 23:32:58 -0400 Subject: [PATCH 10/16] Refactor Install-WinGet and New-WinGetSettings for improved readability --- FFUDevelopment/BuildFFUVM.ps1 | 103 +++++++++++++++++----------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 0b88fbc..d64bcbc 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1659,55 +1659,48 @@ function Get-Office { } function Install-WinGet { - param ( - [bool]$InstallWithDependencies - ) $wingetPreviewLink = "https://aka.ms/getwingetpreview" $wingetPackageDestination = "$env:TEMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" - if ($InstallWithDependencies) { - $dependencies = @( - @{ - Source = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx" - Destination = "$env:TEMP\Microsoft.VCLibs.x64.14.00.Desktop.appx" - }, - @{ - Source = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx" - Destination = "$env:TEMP\Microsoft.UI.Xaml.2.8.x64.appx" - } - ) - Start-BitsTransferWithRetry -Source $wingetPreviewLink -Destination $wingetPackageDestination - foreach ($dependency in $dependencies) { - Start-BitsTransferWithRetry -Source $dependency.Source -Destination $dependency.Destination - Add-AppxPackage -Path $dependency.Destination - Remove-Item -Path $dependency.Destination -Force -ErrorAction SilentlyContinue + $dependencies = @( + @{ + Source = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx" + Destination = "$env:TEMP\Microsoft.VCLibs.x64.14.00.Desktop.appx" + }, + @{ + Source = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx" + Destination = "$env:TEMP\Microsoft.UI.Xaml.2.8.x64.appx" } - Add-AppxPackage -Path $wingetPackageDestination - Remove-Item -Path $wingetPackageDestination -Force -ErrorAction SilentlyContinue - } - else { - # If WinGet was already installed, then installing the dependencies can cause an error if the system has a newer version of the dependencies than the ones downloaded. - WriteLog "Downloading WinGet..." - Start-BitsTransferWithRetry -Source $wingetPreviewLink -Destination $wingetPackageDestination - WriteLog "Installing WinGet..." - Add-AppxPackage -Path $wingetPackageDestination - WriteLog "Removing WinGet installer..." - Remove-Item -Path $wingetPackageDestination -Force -ErrorAction SilentlyContinue + ) + foreach ($dependency in $dependencies) { + $dependencyName = [System.IO.Path]::GetFileName($dependency.Source) + WriteLog "Downloading $dependencyName..." + Start-BitsTransferWithRetry -Source $dependency.Source -Destination $dependency.Destination + WriteLog "Installing $dependencyName..." + Add-AppxPackage -Path $dependency.Destination -ErrorAction SilentlyContinue + WriteLog "Removing $dependencyName..." + Remove-Item -Path $dependency.Destination -Force -ErrorAction SilentlyContinue } + WriteLog "Downloading WinGet..." + Start-BitsTransferWithRetry -Source $wingetPreviewLink -Destination $wingetPackageDestination + WriteLog "Installing WinGet..." + Add-AppxPackage -Path $wingetPackageDestination -ErrorAction SilentlyContinue + WriteLog "Removing WinGet installer..." + Remove-Item -Path $wingetPackageDestination -Force -ErrorAction SilentlyContinue } function Confirm-WinGetInstallation { $wingetPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" if (-not (Test-Path $wingetPath)) { - WriteLog "WinGet is not installed. Downloading preview version of WinGet and its dependencies..." - Install-WinGet -InstallWithDependencies $true + WriteLog "WinGet is not installed. Downloading preview version of WinGet..." + Install-WinGet } elseif (-not (Get-Command winget -ErrorAction SilentlyContinue)) { - WriteLog "WinGet is not on the path. Downloading preview version of WinGet without dependencies..." - Install-WinGet -InstallWithDependencies $false + WriteLog "WinGet is not on the path. Downloading preview version of WinGet..." + Install-WinGet } elseif (-not ((& winget.exe --version) -like "*preview*")) { - WriteLog "The preview version of WinGet is not installed. Downloading preview version of WinGet without dependencies..." - Install-WinGet -InstallWithDependencies $false + WriteLog "The preview version of WinGet is not installed. Downloading preview version of WinGet..." + Install-WinGet } } @@ -1730,24 +1723,21 @@ function New-WinGetSettings { if (Test-Path -Path $wingetSettingsFile -PathType Leaf) { $jsonContent = Get-Content -Path $wingetSettingsFile -Raw # Check if storeDownload feature is already enabled - if ($jsonContent -notmatch '"storeDownload"\s*:\s*true') { - # Back up existing settings.json file - $backupWingetSettingsFile = $wingetSettingsFile + ".bak" - if (-not (Test-Path -Path $backupWingetSettingsFile -PathType Leaf)) { - WriteLog "Backing up existing WinGet settings.json file to $backupWingetSettingsFile" - Copy-Item -Path $wingetSettingsFile -Destination $backupWingetSettingsFile -Force | Out-Null - } - WriteLog "Creating WinGet settings.json file to allow the storeDownload feature. Writing file to $wingetSettingsFile" - $wingetSettingsContent | Out-File -FilePath $wingetSettingsFile -Encoding utf8 -Force - } - else { + WriteLog "Checking if storeDownload feature is enabled in WinGet configuration." + if ($jsonContent -match '"storeDownload"\s*:\s*true') { WriteLog "WinGet's settings.json file is already configured to enable the storeDownload feature." + return + } + # Back up existing settings.json file + WriteLog "The storeDownload feature is not enabled in WinGet configuration." + $backupWingetSettingsFile = $wingetSettingsFile + ".bak" + if (-not (Test-Path -Path $backupWingetSettingsFile -PathType Leaf)) { + WriteLog "Backing up existing WinGet settings.json file to $backupWingetSettingsFile" + Copy-Item -Path $wingetSettingsFile -Destination $backupWingetSettingsFile -Force | Out-Null } } - else { - WriteLog "Creating WinGet settings.json file to allow the storeDownload feature. Writing file to $wingetSettingsFile" - $wingetSettingsContent | Out-File -FilePath $wingetSettingsFile -Encoding utf8 -Force - } + WriteLog "Creating WinGet settings.json file to allow the storeDownload feature. Writing file to $wingetSettingsFile" + $wingetSettingsContent | Out-File -FilePath $wingetSettingsFile -Encoding utf8 -Force } function Add-Win32SilentInstallCommand { @@ -1812,10 +1802,19 @@ function Get-WinGetApp { WriteLog "Downloading $WinGetAppName..." $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget --architecture "$WindowsArch" | Out-String if ($wingetDownloadResult -match "No applicable installer found") { + WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget | Out-String + if ($wingetDownloadResult -match "Installer downloaded") { + WriteLog "Downloaded $WinGetAppName without specifying architecture." + } + else { + WriteLog "No installer found for $WinGetAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return $false + } } if ($wingetDownloadResult -notmatch "Installer downloaded") { - WriteLog "$WinGetAppName did not successfully download." + WriteLog "No installer found for $WinGetAppName. Skipping download." Remove-Item -Path $appFolderPath -Recurse -Force return $false } From f7f52903a44563af92713eb518d9442babf2b56d Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:38:48 -0400 Subject: [PATCH 11/16] Refactor Get-StoreApp for improved readability --- FFUDevelopment/BuildFFUVM.ps1 | 94 ++++++++++++++++------------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index d64bcbc..407e9a2 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1850,67 +1850,61 @@ function Get-StoreApp { $appID = ($appResult -split '\s+')[-2] # Checking app ID to determine if store app is a win32 app WriteLog "Checking if $StoreAppName is a win32 app..." - if ($appID.StartsWith("XP")) { + $appIsWin32 = $appID.StartsWith("XP") + if ($appIsWin32) { WriteLog "$StoreAppName is a win32 app. Adding to $AppsPath\win32 folder" $appFolderPath = Join-Path -Path "$AppsPath\win32" -ChildPath $StoreAppName - New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null - WriteLog "Downloading $StoreAppName for $WindowsArch architecture..." - $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String - if ($wingetDownloadResult -match "No applicable installer found") { - WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." - $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match "Installer downloaded"){ - WriteLog "Downloaded $StoreAppName without specifying architecture." - } - else { - WriteLog "No installer found for $StoreAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } + } + else { + $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreAppName + } + New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null + WriteLog "Downloading $StoreAppName for $WindowsArch architecture..." + $downloadParams = @( + "download", "$StoreAppId", + "--download-directory", "$appFolderPath", + "--accept-package-agreements", + "--accept-source-agreements", + "--source", "msstore", + "--scope", "machine", + "--architecture", "$WindowsArch" + ) + WriteLog 'MSStore app downloads require authentication with an Entra ID account. You may be prompted twice for credentials, once for the app and another for the license file.' + WriteLog "Attempting to download $StoreAppName and dependencies for $WindowsArch architecture..." + $wingetDownloadResult = & winget.exe @downloadParams | Out-String + # For some apps, specifying the architecture leads to no results found for the app. In those cases, the command will be run without the architecture parameter. + if ($wingetDownloadResult -match "No applicable installer found") { + WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." + $downloadParams = $downloadParams | Where-Object { $_ -notmatch "--architecture" -and $_ -notmatch "$WindowsArch" } + $wingetDownloadResult = & winget.exe @downloadParams | Out-String + if ($wingetDownloadResult -match "Microsoft Store package download completed") { + WriteLog "Downloaded $StoreAppName without specifying architecture." } + } + if ($wingetDownloadResult -notmatch "Installer downloaded|Microsoft Store package download completed") { + WriteLog "Download not supported for $StoreAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return + } + if ($appIsWin32) { Add-Win32SilentInstallCommand -AppFolder $StoreAppName -AppFolderPath $appFolderPath -LineNumber $LineNumber # Since a Win32 app was received, returning false to increment line number for silent install command return $false } - $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreAppName - New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null - # Invoke-Process is not used here because it terminates the script if the exit code of the process is not zero. - # WinGet's download command will return a non-zero exit code when downloading store apps, as attempting to download the license file always appears to cause an error. - WriteLog "Attempting to download $StoreAppName and dependencies..." - WriteLog 'MSStore app downloads require authentication with an Entra ID account. You may be prompted twice for credentials, once for the app and another for the license file.' - $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --architecture "$WindowsArch" --scope machine | Out-String - # For some apps, specifying the architecture leads to no results found for the app. In those cases, the command will be run without the architecture parameter. - if ($wingetDownloadResult -match "No applicable installer found") { - WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." - $wingetDownloadResult = & winget.exe download "$StoreAppId" --download-directory "$appFolderPath" --accept-package-agreements --accept-source-agreements --source msstore --scope machine | Out-String - if ($wingetDownloadResult -match "Microsoft Store package download completed") { - WriteLog "Downloaded $StoreAppName without specifying architecture." - # If $WindowsArch -eq 'ARM64', remove all dependency files that are not ARM64 - if ($WindowsArch -eq 'ARM64') { - WriteLog 'Windows architecture is ARM64. Removing dependencies that are not ARM64.' - $dependencies = Get-ChildItem -Path "$appFolderPath\Dependencies" -ErrorAction SilentlyContinue - if ($dependencies) { - foreach ($dependency in $dependencies) { - if ($dependency.Name -notmatch 'ARM64') { - WriteLog "Removing dependency file $($dependency.FullName)" - Remove-Item -Path $dependency.FullName -Recurse -Force - } - } + Set-InstallStoreAppsFlag + # If $WindowsArch -eq 'ARM64', remove all dependency files that are not ARM64 + if ($WindowsArch -eq 'ARM64') { + WriteLog 'Windows architecture is ARM64. Removing dependencies that are not ARM64.' + $dependencies = Get-ChildItem -Path "$appFolderPath\Dependencies" -ErrorAction SilentlyContinue + if ($dependencies) { + foreach ($dependency in $dependencies) { + if ($dependency.Name -notmatch 'ARM64') { + WriteLog "Removing dependency file $($dependency.FullName)" + Remove-Item -Path $dependency.FullName -Recurse -Force } } } - else { - WriteLog "No installer found for $StoreAppName from the msstore source. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } } - elseif ($wingetDownloadResult -notmatch "Microsoft Store package download completed") { - WriteLog "Download not supported for $StoreAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } - Set-InstallStoreAppsFlag WriteLog "$StoreAppName has completed downloading. Identifying the latest version of $StoreAppName." $packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop # WinGet downloads multiple versions of certain store apps. The latest version of the package will be determined based on the date of the file signature. From 1a444d8e0faa0cde465fe886df24a9d0556ec2b5 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:40:43 -0400 Subject: [PATCH 12/16] Refactor Install-WinGet function and add architecture parameter and stable WinGet download, modify Confirm-WinGetInstallation to check for stable release, refactor Get-WinGetApp for improved maintainability and readability --- FFUDevelopment/BuildFFUVM.ps1 | 87 ++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 407e9a2..f7e108c 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1659,48 +1659,44 @@ function Get-Office { } function Install-WinGet { - $wingetPreviewLink = "https://aka.ms/getwingetpreview" - $wingetPackageDestination = "$env:TEMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" - $dependencies = @( - @{ - Source = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx" - Destination = "$env:TEMP\Microsoft.VCLibs.x64.14.00.Desktop.appx" - }, - @{ - Source = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx" - Destination = "$env:TEMP\Microsoft.UI.Xaml.2.8.x64.appx" - } + param ( + [string]$Architecture ) - foreach ($dependency in $dependencies) { - $dependencyName = [System.IO.Path]::GetFileName($dependency.Source) - WriteLog "Downloading $dependencyName..." - Start-BitsTransferWithRetry -Source $dependency.Source -Destination $dependency.Destination - WriteLog "Installing $dependencyName..." - Add-AppxPackage -Path $dependency.Destination -ErrorAction SilentlyContinue - WriteLog "Removing $dependencyName..." - Remove-Item -Path $dependency.Destination -Force -ErrorAction SilentlyContinue + $packages = @( + @{Name = "VCLibs"; Url = "https://aka.ms/Microsoft.VCLibs.$Architecture.14.00.Desktop.appx"; File = "Microsoft.VCLibs.$Architecture.14.00.Desktop.appx"}, + @{Name = "UIXaml"; Url = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.$Architecture.appx"; File = "Microsoft.UI.Xaml.2.8.$Architecture.appx"}, + @{Name = "WinGet"; Url = "https://aka.ms/getwinget"; File = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"} + ) + foreach ($package in $packages) { + $destination = Join-Path -Path $env:TEMP -ChildPath $package.File + WriteLog "Downloading $($package.Name) from $($package.Url) to $destination" + Start-BitsTransferWithRetry -Source $package.Url -Destination $destination + WriteLog "Installing $($package.Name)..." + Add-AppxPackage -Path $destination -ErrorAction SilentlyContinue + WriteLog "Removing $($package.Name)..." + Remove-Item -Path $destination -Force -ErrorAction SilentlyContinue } - WriteLog "Downloading WinGet..." - Start-BitsTransferWithRetry -Source $wingetPreviewLink -Destination $wingetPackageDestination - WriteLog "Installing WinGet..." - Add-AppxPackage -Path $wingetPackageDestination -ErrorAction SilentlyContinue - WriteLog "Removing WinGet installer..." - Remove-Item -Path $wingetPackageDestination -Force -ErrorAction SilentlyContinue + WriteLog "WinGet installation complete." } function Confirm-WinGetInstallation { $wingetPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" - if (-not (Test-Path $wingetPath)) { - WriteLog "WinGet is not installed. Downloading preview version of WinGet..." - Install-WinGet + $minVersion = [version]"1.8.1911" + if (-not (Test-Path -Path $wingetPath -PathType Leaf)) { + WriteLog "WinGet is not installed. Downloading WinGet..." + Install-WinGet -Architecture $WindowsArch + return } - elseif (-not (Get-Command winget -ErrorAction SilentlyContinue)) { - WriteLog "WinGet is not on the path. Downloading preview version of WinGet..." - Install-WinGet - } - elseif (-not ((& winget.exe --version) -like "*preview*")) { - WriteLog "The preview version of WinGet is not installed. Downloading preview version of WinGet..." - Install-WinGet + if (-not (Get-Command -Name winget -ErrorAction SilentlyContinue)) { + WriteLog "WinGet is not on the path. Downloading WinGet..." + Install-WinGet -Architecture $WindowsArch + return + } + $wingetVersion = & winget.exe --version + if ($wingetVersion -match 'v?(\d+\.\d+\.\d+)' -and [version]$matches[1] -lt $minVersion) { + WriteLog "The installed version of WinGet $($matches[1]) does not support downloading MSStore apps. Downloading the latest version of WinGet..." + Install-WinGet -Architecture $WindowsArch + return } } @@ -1800,18 +1796,25 @@ function Get-WinGetApp { $appFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $WinGetAppName New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null WriteLog "Downloading $WinGetAppName..." - $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget --architecture "$WindowsArch" | Out-String + $downloadParams = @( + "download", + "--id", "$WinGetAppId", + "--exact", + "--download-directory", "$appFolderPath", + "--accept-package-agreements", + "--accept-source-agreements", + "--source", "winget", + "--scope", "machine", + "--architecture", "$WindowsArch" + ) + $wingetDownloadResult = & winget.exe @downloadParams | Out-String if ($wingetDownloadResult -match "No applicable installer found") { WriteLog "No installer found for $WindowsArch architecture. Attempting to download without specifying architecture..." - $wingetDownloadResult = & winget.exe download --id "$WinGetAppId" --exact --download-directory "$appFolderPath" --scope machine --source winget | Out-String + $downloadParams = $downloadParams | Where-Object { $_ -notmatch "--architecture" -and $_ -notmatch "$WindowsArch" } + $wingetDownloadResult = & winget.exe @downloadParams | Out-String if ($wingetDownloadResult -match "Installer downloaded") { WriteLog "Downloaded $WinGetAppName without specifying architecture." } - else { - WriteLog "No installer found for $WinGetAppName. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return $false - } } if ($wingetDownloadResult -notmatch "Installer downloaded") { WriteLog "No installer found for $WinGetAppName. Skipping download." From 191c30dd659ec83e629c6a40dcc05db6f8ab4c27 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:43:10 -0400 Subject: [PATCH 13/16] Remove New-WinGetSettings, since stable release of WinGet now supports MSStore app downloads --- FFUDevelopment/BuildFFUVM.ps1 | 37 ----------------------------------- 1 file changed, 37 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f7e108c..8578c47 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1700,42 +1700,6 @@ function Confirm-WinGetInstallation { } } -function New-WinGetSettings { - $wingetSettingsFile = "$env:LOCALAPPDATA\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json" - $wingetSettings = @( - '{' - ' "$schema": "https://aka.ms/winget-settings.schema.json",' - ' ' - ' // For documentation on these settings, see: https://aka.ms/winget-settings' - ' "experimentalFeatures": {' - ' "storeDownload": true' - ' },' - ' "logging": {' - ' "level": "verbose"' - ' }' - '}' - ) - $wingetSettingsContent = $wingetSettings -join "`n" - if (Test-Path -Path $wingetSettingsFile -PathType Leaf) { - $jsonContent = Get-Content -Path $wingetSettingsFile -Raw - # Check if storeDownload feature is already enabled - WriteLog "Checking if storeDownload feature is enabled in WinGet configuration." - if ($jsonContent -match '"storeDownload"\s*:\s*true') { - WriteLog "WinGet's settings.json file is already configured to enable the storeDownload feature." - return - } - # Back up existing settings.json file - WriteLog "The storeDownload feature is not enabled in WinGet configuration." - $backupWingetSettingsFile = $wingetSettingsFile + ".bak" - if (-not (Test-Path -Path $backupWingetSettingsFile -PathType Leaf)) { - WriteLog "Backing up existing WinGet settings.json file to $backupWingetSettingsFile" - Copy-Item -Path $wingetSettingsFile -Destination $backupWingetSettingsFile -Force | Out-Null - } - } - WriteLog "Creating WinGet settings.json file to allow the storeDownload feature. Writing file to $wingetSettingsFile" - $wingetSettingsContent | Out-File -FilePath $wingetSettingsFile -Encoding utf8 -Force -} - function Add-Win32SilentInstallCommand { param ( [string]$AppFolder, @@ -1962,7 +1926,6 @@ function Get-Apps { } } if ($storeApps) { - New-WinGetSettings if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) { New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null } From ab58b27a1dc4639e5025c76bd38ac9275f703991 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 12 Jul 2024 23:40:59 -0400 Subject: [PATCH 14/16] Removed checking of appId using result parsing, since appId is already a parameter, added command to remove unprovisioned Notepad++ package, which breaks Sysprep --- FFUDevelopment/Apps/AppList.json | 5 +++++ FFUDevelopment/Apps/InstallAppsandSysprep.cmd | 5 +++++ FFUDevelopment/BuildFFUVM.ps1 | 8 ++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/FFUDevelopment/Apps/AppList.json b/FFUDevelopment/Apps/AppList.json index ea7e1d7..a33c2b9 100644 --- a/FFUDevelopment/Apps/AppList.json +++ b/FFUDevelopment/Apps/AppList.json @@ -9,6 +9,11 @@ "name": "Company Portal", "id": "9WZDNCRFJ3PZ", "source": "msstore" + }, + { + "name": "Microsoft Teams", + "id": "Microsoft.Teams", + "source": "winget" } ] } \ No newline at end of file diff --git a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd index 1fefc88..8b39b03 100644 --- a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd +++ b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd @@ -51,6 +51,11 @@ for /d %%D in ("%basepath%\*") do ( ) :remaining endlocal +for /r "D:\" %%G in (.) do ( + if exist "%%G\Notepad++" ( + powershell -Command "Remove-AppxPackage -Package NotepadPlusPlus_1.0.0.0_neutral__7njy0v32s6xk6" + ) +) 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 diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 8578c47..b09e4d9 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1811,18 +1811,14 @@ function Get-StoreApp { WriteLog "$StoreAppName not found in WinGet repository. Skipping download." return } - # Skip the header lines and get the line with the app information - $appResult = $wingetSearchResult | Select-Object -Skip 2 | Select-Object -First 1 - # Split the line by whitespace and get the second-to-last item (the Id) - $appID = ($appResult -split '\s+')[-2] - # Checking app ID to determine if store app is a win32 app WriteLog "Checking if $StoreAppName is a win32 app..." - $appIsWin32 = $appID.StartsWith("XP") + $appIsWin32 = $StoreAppId.StartsWith("XP") if ($appIsWin32) { WriteLog "$StoreAppName is a win32 app. Adding to $AppsPath\win32 folder" $appFolderPath = Join-Path -Path "$AppsPath\win32" -ChildPath $StoreAppName } else { + WriteLog "$StoreAppName is not a win32 app." $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreAppName } New-Item -Path $appFolderPath -ItemType Directory -Force | Out-Null From ddf9c1f98664e96731491c9a34bd9d2c4279a6a0 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 12 Jul 2024 23:46:01 -0400 Subject: [PATCH 15/16] Fix Get-Apps function parameter name --- FFUDevelopment/BuildFFUVM.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index b09e4d9..45711c2 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1890,9 +1890,9 @@ function Get-StoreApp { function Get-Apps { param ( - [string]$AppsList + [string]$AppList ) - $apps = Get-Content -Path $AppsList -Raw | ConvertFrom-Json + $apps = Get-Content -Path $AppList -Raw | ConvertFrom-Json if (-not $apps) { WriteLog "No apps were specified in AppList.json file." return @@ -3349,7 +3349,7 @@ if ($InstallApps) { exit } WriteLog "$AppsPath\InstallAppsandSysprep.cmd found" - Get-Apps -AppsList "$AppsPath\AppList.json" + Get-Apps -AppList "$AppsPath\AppList.json" if (-not $InstallOffice) { #Modify InstallAppsandSysprep.cmd to REM out the office install command $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" From f6c3c0b6c3d7dc5c18803141dc157f03f054698c Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT Date: Sat, 13 Jul 2024 08:38:09 -0700 Subject: [PATCH 16/16] create unattend files for x64 and arm64 depending on windows architecture --- .../BuildFFUUnattend/unattend_arm64.xml | 21 +++++++++++++++++++ .../{unattend.xml => unattend_x64.xml} | 0 2 files changed, 21 insertions(+) create mode 100644 FFUDevelopment/BuildFFUUnattend/unattend_arm64.xml rename FFUDevelopment/BuildFFUUnattend/{unattend.xml => unattend_x64.xml} (100%) diff --git a/FFUDevelopment/BuildFFUUnattend/unattend_arm64.xml b/FFUDevelopment/BuildFFUUnattend/unattend_arm64.xml new file mode 100644 index 0000000..ca11e8b --- /dev/null +++ b/FFUDevelopment/BuildFFUUnattend/unattend_arm64.xml @@ -0,0 +1,21 @@ + + + + + + + 1 + d:\InstallAppsandSysprep.cmd + + + + + + + + Audit + + + + + diff --git a/FFUDevelopment/BuildFFUUnattend/unattend.xml b/FFUDevelopment/BuildFFUUnattend/unattend_x64.xml similarity index 100% rename from FFUDevelopment/BuildFFUUnattend/unattend.xml rename to FFUDevelopment/BuildFFUUnattend/unattend_x64.xml