diff --git a/FFUDevelopment/Apps/AppList.json b/FFUDevelopment/Apps/AppList.json new file mode 100644 index 0000000..a33c2b9 --- /dev/null +++ b/FFUDevelopment/Apps/AppList.json @@ -0,0 +1,19 @@ +{ + "apps": [ + { + "name": "7-Zip", + "id": "7zip.7zip", + "source": "winget" + }, + { + "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/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..8b39b03 100644 --- a/FFUDevelopment/Apps/InstallAppsandSysprep.cmd +++ b/FFUDevelopment/Apps/InstallAppsandSysprep.cmd @@ -33,24 +33,29 @@ 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 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 ef6da11..45711c2 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1660,102 +1660,72 @@ function Get-Office { function Install-WinGet { param ( - [bool]$InstallWithDependencies + [string]$Architecture ) - $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 - } - 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 + $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 "WinGet installation complete." } -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 - 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 "WinGet's settings.json file is already configured to enable the storeDownload feature." - } +function Confirm-WinGetInstallation { + $wingetPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" + $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 } - else { - WriteLog "Creating WinGet settings.json file to allow the storeDownload feature. Writing file to $wingetSettingsFile" - $wingetSettingsContent | Out-File -FilePath $wingetSettingsFile -Encoding utf8 -Force + 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 } } function Add-Win32SilentInstallCommand { param ( [string]$AppFolder, - [string]$AppFolderPath + [string]$AppFolderPath, + [int]$LineNumber ) $appName = $AppFolder - $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 + $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction Stop + 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" } $cmdFile = "$AppsPath\InstallAppsandSysprep.cmd" @@ -1765,132 +1735,145 @@ function Add-Win32SilentInstallCommand { Set-Content -Path $cmdFile -Value $cmdContent } -function Get-WinGetApp { - param ( - [string]$WinGetApp, - [int]$LineNumber - ) - $wingetSearchResult = & winget.exe search --name "$WinGetApp" --exact --accept-source-agreements --source winget - if ($wingetSearchResult -contains "No package found matching input criteria.") { - WriteLog "$WinGetApp not found in WinGet repository. Skipping download." - return - } - $appFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $WinGetApp - 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 - if ($wingetDownloadResult -match "No applicable installer found") { - $wingetDownloadResult = & winget.exe download --name "$WinGetApp" --exact --download-directory "$appFolderPath" --scope machine --source winget | Out-String - } - if ($wingetDownloadResult -notmatch "Installer downloaded") { - WriteLog "$WinGetApp did not successfully download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } - WriteLog "$WinGetApp has completed downloading." - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath -} - -function Get-StoreApp { - param ( - [string]$StoreApp - ) - $wingetSearchResult = & winget.exe search --name --exact "$StoreApp" --accept-source-agreements --source msstore - if ($wingetSearchResult -contains "No package found matching input criteria.") { - WriteLog "$StoreApp 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 $StoreApp 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 - 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 - 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." - } - else { - WriteLog "No installer found for $StoreApp. Skipping download." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } - } - Add-Win32SilentInstallCommand -AppFolder $appFolder -AppFolderPath $appFolderPath - return - } - $appFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $StoreApp - 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 '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 - # 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." - # 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 $StoreApp. 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." - Remove-Item -Path $appFolderPath -Recurse -Force - return - } - $cmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" +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 } - WriteLog "$StoreApp has completed downloading. Identifying the latest version of $StoreApp." - $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 +} + +function Get-WinGetApp { + param ( + [string]$WinGetAppName, + [string]$WinGetAppId, + [int]$LineNumber + ) + $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 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 + WriteLog "Downloading $WinGetAppName..." + $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..." + $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." + } + } + if ($wingetDownloadResult -notmatch "Installer downloaded") { + WriteLog "No installer found for $WinGetAppName. Skipping download." + Remove-Item -Path $appFolderPath -Recurse -Force + return $false + } + WriteLog "$WinGetAppName has completed downloading to $appFolderPath" + $installerPath = Get-ChildItem -Path "$appFolderPath\*" -Exclude "*.yaml", "*.xml" -File -ErrorAction Stop + $uwpExtensions = @(".appx", ".appxbundle", ".msix", ".msixbundle") + 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 + Set-InstallStoreAppsFlag + return $false + } + else { + Add-Win32SilentInstallCommand -AppFolder $WinGetAppName -AppFolderPath $appFolderPath -LineNumber $LineNumber + } +} + +function Get-StoreApp { + param ( + [string]$StoreAppName, + [string]$StoreAppId, + [int]$LineNumber + ) + $wingetSearchResult = & winget.exe search "$StoreAppId" --accept-source-agreements --source msstore + if ($wingetSearchResult -contains "No package found matching input criteria.") { + WriteLog "$StoreAppName not found in WinGet repository. Skipping download." + return + } + WriteLog "Checking if $StoreAppName is a win32 app..." + $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 + 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 + } + 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 + } } } } + 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 = $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 $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 { @@ -1907,39 +1890,16 @@ function Get-StoreApp { function Get-Apps { param ( - [string]$AppsList + [string]$AppList ) - $apps = Get-Content -Path $AppsList + $apps = Get-Content -Path $AppList -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 = @() - $apps | ForEach-Object { - if ($_ -like 'winget:*') { - $wingetApps += $_.Substring(7).Trim() - } - elseif ($_ -like 'store:*') { - $storeApps += $_.Substring(6).Trim() - } - } - $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" @@ -1949,8 +1909,11 @@ 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++ + WriteLog "Line number incremented to $lineNumber" + } } catch { WriteLog "Error occurred while processing $wingetApp : $_" @@ -1959,13 +1922,16 @@ function Get-Apps { } } if ($storeApps) { - New-WinGetSettings if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) { New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null } foreach ($storeApp in $storeApps) { try { - Get-StoreApp -StoreApp $storeApp + $result = Get-StoreApp -StoreAppName $storeApp.Name -StoreAppId $storeApp.Id -LineNumber $lineNumber + if ($result -eq $false) { + $lineNumber++ + WriteLog "Line number incremented to $lineNumber" + } } catch { WriteLog "Error occurred while processing $storeApp : $_" @@ -3383,7 +3349,7 @@ if ($InstallApps) { exit } WriteLog "$AppsPath\InstallAppsandSysprep.cmd found" - Get-Apps -AppsList "$AppsPath\AppsList.txt" + 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" diff --git a/FFUDevelopment/USBImagingToolCreator.ps1 b/FFUDevelopment/USBImagingToolCreator.ps1 new file mode 100644 index 0000000..dd3f36d --- /dev/null +++ b/FFUDevelopment/USBImagingToolCreator.ps1 @@ -0,0 +1,223 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $True, Position = 0)] + $DeployISOPath, + [Switch]$DisableAutoPlay +) +$Host.UI.RawUI.WindowTitle = 'USB Imaging Tool Creator' + +if($DeployISOPath){ +$DevelopmentPath = $DeployISOPath | Split-Path +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 Get-USBDrive { + $USBDrives = (Get-WmiObject -Class Win32_DiskDrive -Filter "MediaType='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" + exit 1 + } + return $USBDrives, $USBDrivesCount +} +Function Build-DeploymentUSB{ + param( + [Array]$Drives + ) + 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 + Writelog "Creating partitions..." + 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 + ":\" + $jobScriptBlock = { + param ( + [string]$SFolder, + [string]$DFolder + ) + 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 $FFUPath, $Destination | 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){ +Writelog "Building $DrivesCount drives concurrently...Please be patient..." +}else{ +Writelog "Building the imaging tool on $model...Please be patient..." +} +Get-Job | Wait-Job | Out-Null + +Dismount-DiskImage -ImagePath $DeployISOPath | Out-Null +Writelog "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 + if($Last){ + writelog "All drives selected" + }else{ + 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) + + $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 + } + 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){ + 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!" +} +#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-USBDrive +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 +} diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index ec131e6..201fb07 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