diff --git a/FFUDevelopment/Apps/Orchestration/Install-StoreApps.ps1 b/FFUDevelopment/Apps/Orchestration/Install-StoreApps.ps1 index 253ca23..36ef897 100644 --- a/FFUDevelopment/Apps/Orchestration/Install-StoreApps.ps1 +++ b/FFUDevelopment/Apps/Orchestration/Install-StoreApps.ps1 @@ -1,57 +1,271 @@ +#Requires -RunAsAdministrator + +# --- CONFIGURATION --- +# Base path where application folders are located. Each subfolder represents one application. $basePath = "D:\MSStore" +# Path for temporary files (e.g., for extracting archives). This will be created and cleaned up automatically. +$tempBasePath = Join-Path -Path $env:TEMP -ChildPath "StoreAppInstall" + +# --- SCRIPT --- + +# Helper function to clean up temporary files on exit or error +function Remove-TemporaryFiles { + if (Test-Path -Path $tempBasePath) { + Write-Host "Cleaning up temporary directory: $tempBasePath" + Remove-Item -Path $tempBasePath -Recurse -Force + } +} + +# Ensure temp directory is clean before starting +Remove-TemporaryFiles +New-Item -Path $tempBasePath -ItemType Directory -Force | Out-Null + +# 1. Determine applicable dependency architectures based on the OS architecture +$osArchitecture = $env:PROCESSOR_ARCHITECTURE +$applicableArchitectures = switch ($osArchitecture) { + "AMD64" { 'x64', 'x86' } + "x86" { 'x86' } + "ARM64" { 'arm64', 'arm' } + default { $osArchitecture.ToLower() } +} +Write-Host "Installing Store Apps: Detected OS Architecture: $osArchitecture." +Write-Host "Applicable dependency architectures: $($applicableArchitectures -join ', ')" + # Check if the base path exists -Write-Host "Installing Store Apps: Checking for $basePath" if (-not (Test-Path -Path $basePath)) { - Write-Host "Installing Store Apps: $basePath does not exist." + Write-Host "Installing Store Apps: Base path '$basePath' does not exist. Exiting." exit } -Write-Host "Installing Store Apps: $basePath exists, installing apps." +Write-Host "Installing Store Apps: Base path '$basePath' exists." -# Process each app folder in the base path -foreach ($appFolder in Get-ChildItem -Path $basePath -Directory) { - $folderPath = $appFolder.FullName - $dependenciesFolder = Join-Path -Path $folderPath -ChildPath "Dependencies" - - # Find main package - exclude Dependencies folder items and xml/yaml files - $mainPackage = Get-ChildItem -Path $folderPath -File | - Where-Object { - $_.DirectoryName -ne $dependenciesFolder -and - $_.Extension -ne ".xml" -and - $_.Extension -ne ".yaml" - } | Select-Object -First 1 - - if ($mainPackage) { - # Build DISM command with main package - $dismParams = @( - "/Online" - "/Add-ProvisionedAppxPackage" - "/PackagePath:`"$($mainPackage.FullName)`"" - "/Region:all" - "/StubPackageOption:installfull" - ) - - # Add dependency packages if they exist - if (Test-Path -Path $dependenciesFolder) { - $dependencies = Get-ChildItem -Path $dependenciesFolder -File - foreach ($dependency in $dependencies) { - $dismParams += "/DependencyPackagePath:`"$($dependency.FullName)`"" +# 2. Pre-scan and catalog all available dependencies from all app folders +Write-Host "Scanning for all available application dependencies..." +$allAvailableDependencies = @{} +$dependencyFoldersToScan = [System.Collections.Generic.List[string]]::new() + +# Find all 'Dependencies' subfolders +Get-ChildItem -Path $basePath -Directory | ForEach-Object { + $dependenciesPath = Join-Path -Path $_.FullName -ChildPath "Dependencies" + if (Test-Path -Path $dependenciesPath) { + $dependencyFoldersToScan.Add($dependenciesPath) + } +} + +# Handle zipped dependencies by extracting them to a temp location +$dependencyFoldersToScan.ToArray() | ForEach-Object { + $folder = $_ + Get-ChildItem -Path $folder -Filter "*.zip" -File | ForEach-Object { + $zipFile = $_ + $extractPath = Join-Path -Path $tempBasePath -ChildPath $zipFile.BaseName + Write-Host "Extracting zipped dependencies from '$($zipFile.FullName)' to '$extractPath'..." + try { + Expand-Archive -Path $zipFile.FullName -DestinationPath $extractPath -Force + $script:dependencyFoldersToScan.Add($extractPath) + } + catch { + Write-Error "Failed to extract '$($zipFile.FullName)'. Error: $($_.Exception.Message)" + } + } +} + +# Regex to parse package filenames: Name_Version_Architecture__PublisherID (PublisherID and other tags are optional) +$packageFileRegex = '^(?.+?)_(?(?:\d+\.){2,3}\d+)_(?:[^_]+_)*(?x64|x86|arm|arm64|neutral)(?:__.*)?$' + +# Catalog all package files found in the dependency folders +foreach ($folder in $dependencyFoldersToScan.ToArray() | Select-Object -Unique) { + Get-ChildItem -Path $folder -Recurse -File | Where-Object { $_.Extension -in '.appx', '.msix', '.appxbundle' } | ForEach-Object { + $file = $_ + $match = $file.BaseName -imatch $packageFileRegex + if ($match) { + $dependencyName = $matches.Name + try { + $dependencyVersion = [System.Version]$matches.Version + $dependencyArch = $matches.Arch + + if (-not $allAvailableDependencies.ContainsKey($dependencyName)) { + $allAvailableDependencies[$dependencyName] = [System.Collections.Generic.List[object]]::new() + } + $allAvailableDependencies[$dependencyName].Add([pscustomobject]@{ + Name = $dependencyName + Version = $dependencyVersion + Arch = $dependencyArch + Path = $file.FullName + }) + } + catch { + Write-Warning "Could not parse version for file '$($file.Name)'. Skipping." } } - - # Look for license file and add appropriate parameter - $licenseFile = Get-ChildItem -Path $folderPath -Filter "*.xml" -File | Select-Object -First 1 - if ($licenseFile) { - $dismParams += "/LicensePath:`"$($licenseFile.FullName)`"" - } else { - $dismParams += "/SkipLicense" - } - - # Construct final command - $dismCommand = "DISM " + ($dismParams -join " ") - - # Output and execute the command - Write-Output $dismCommand - Invoke-Expression -Command $dismCommand - Write-Output "" } -} \ No newline at end of file +} +Write-Host "Dependency scan complete. Found $($allAvailableDependencies.Keys.Count) unique dependency packages." +Write-Output "" + +# 3. Process and install each main application +Write-Host "Starting main application installation process..." +foreach ($appFolder in Get-ChildItem -Path $basePath -Directory) { + Write-Host "--- Processing application in folder: $($appFolder.Name) ---" + + # Find the main application package (.appx/.msix/.appxbundle) in the app's root folder + $mainPackage = Get-ChildItem -Path $appFolder.FullName -File | + Where-Object { $_.Extension -in '.appx', '.msix', '.appxbundle', '.msixbundle' } | + Select-Object -First 1 + + if (-not $mainPackage) { + Write-Warning "No main application package found in '$($appFolder.Name)'. Skipping." + Write-Output "" + continue + } + Write-Host "Found main package: $($mainPackage.Name)" + + # Extract and parse AppxManifest.xml from the main package + $manifestTempPath = Join-Path -Path $tempBasePath -ChildPath "AppxManifest.xml" + if (Test-Path $manifestTempPath) { Remove-Item $manifestTempPath -Force } + + $requiredDependencies = $null + try { + [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null + + # Logic for handling bundles vs. single packages + if ($mainPackage.Extension -in '.appxbundle', '.msixbundle') { + Write-Host "Processing bundle. Searching for architecture-specific package..." + $bundleArchive = [System.IO.Compression.ZipFile]::OpenRead($mainPackage.FullName) + try { + # Find the best matching .appx/.msix package inside the bundle + $primaryArch = if ($osArchitecture -eq 'AMD64') { 'x64' } else { $osArchitecture.ToLower() } + $packageEntries = $bundleArchive.Entries | Where-Object { ($_.Name.EndsWith('.appx') -or $_.Name.EndsWith('.msix')) -and $_.Name -notlike "*_language-*" } + + # Prioritize the primary architecture, then x86 (on x64), then neutral + $bestPackageEntry = $packageEntries | Where-Object { $_.Name -imatch "[._]${primaryArch}\.(appx|msix)$" } | Select-Object -First 1 + if (-not $bestPackageEntry -and $primaryArch -eq 'x64') { + $bestPackageEntry = $packageEntries | Where-Object { $_.Name -imatch "[._]x86\.(appx|msix)$" } | Select-Object -First 1 + } + if (-not $bestPackageEntry) { + $bestPackageEntry = $packageEntries | Where-Object { $_.Name -imatch "[._]neutral\.(appx|msix)$" } | Select-Object -First 1 + } + + if ($bestPackageEntry) { + Write-Host "Found inner package: $($bestPackageEntry.Name). Extracting to read its manifest." + $innerPackageTempPath = Join-Path -Path $tempBasePath -ChildPath $bestPackageEntry.Name + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($bestPackageEntry, $innerPackageTempPath, $true) + + $innerPackageArchive = [System.IO.Compression.ZipFile]::OpenRead($innerPackageTempPath) + try { + $manifestEntry = $innerPackageArchive.Entries | Where-Object { $_.Name -eq 'AppxManifest.xml' } | Select-Object -First 1 + if ($manifestEntry) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($manifestEntry, $manifestTempPath, $true) + } + } finally { + $innerPackageArchive.Dispose() + } + } else { + Write-Error "Could not find a suitable architecture-specific package inside '$($mainPackage.Name)'." + } + } finally { + $bundleArchive.Dispose() + } + } else { # It's a regular .appx or .msix + $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($mainPackage.FullName) + try { + $manifestEntry = $zipArchive.Entries | Where-Object { $_.Name -eq 'AppxManifest.xml' } | Select-Object -First 1 + if ($manifestEntry) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($manifestEntry, $manifestTempPath, $true) + } + } finally { + $zipArchive.Dispose() + } + } + + # Common manifest parsing logic + if (Test-Path $manifestTempPath) { + [xml]$manifest = Get-Content -Path $manifestTempPath + $nsm = [System.Xml.XmlNamespaceManager]::new($manifest.NameTable) + $nsm.AddNamespace("def", "http://schemas.microsoft.com/appx/manifest/foundation/windows10") + + $dependenciesNode = $manifest.SelectSingleNode("//def:Dependencies", $nsm) + if ($dependenciesNode) { + $requiredDependencies = $dependenciesNode.SelectNodes("def:PackageDependency", $nsm) + } + } else { + Write-Error "Could not find or extract AppxManifest.xml from '$($mainPackage.FullName)'." + } + } catch { + Write-Error "Failed to read or parse manifest from '$($mainPackage.FullName)'. Error: $($_.Exception.Message)" + } + + if (-not $requiredDependencies) { + Write-Warning "Could not read dependencies from manifest for '$($mainPackage.Name)'. Proceeding without explicit dependencies." + } + + # Resolve all required dependencies + $resolvedDependencyPaths = [System.Collections.Generic.List[string]]::new() + foreach ($req in $requiredDependencies) { + $reqName = $req.Name + $reqMinVersion = [System.Version]$req.MinVersion + Write-Host "Resolving dependency: $reqName (MinVersion: $reqMinVersion)" + + if ($allAvailableDependencies.ContainsKey($reqName)) { + # Find all available packages that meet the minimum version and architecture requirements + $candidates = $allAvailableDependencies[$reqName] | Where-Object { + $_.Version -ge $reqMinVersion -and + $_.Arch -in ($applicableArchitectures + 'neutral') + } + + if ($candidates) { + # Group by architecture and find the single latest version for each applicable arch + $bestCandidates = $candidates | Group-Object -Property Arch | ForEach-Object { + $_.Group | Sort-Object -Property Version -Descending | Select-Object -First 1 + } + + foreach($best in $bestCandidates) { + Write-Host " - Found best match: $($best.Path.Replace($basePath, '...'))" + $resolvedDependencyPaths.Add($best.Path) + } + } else { + Write-Warning " - No suitable package found for dependency '$reqName' with MinVersion '$reqMinVersion' for applicable architectures." + } + } else { + Write-Warning " - Dependency '$reqName' not found in any scanned dependency folders." + } + } + + # Build the DISM command + $dismParams = @( + "/Online" + "/Add-ProvisionedAppxPackage" + "/PackagePath:`"$($mainPackage.FullName)`"" + "/Region:all" + "/StubPackageOption:installfull" + ) + + # Add resolved dependencies, ensuring no duplicates + $resolvedDependencyPaths.ToArray() | Select-Object -Unique | ForEach-Object { + $dismParams += "/DependencyPackagePath:`"$_`"" + } + + # Find and add the license file, or skip if not found + $licenseFile = Get-ChildItem -Path $appFolder.FullName -Filter "*.xml" -File | Select-Object -First 1 + if ($licenseFile) { + $dismParams += "/LicensePath:`"$($licenseFile.FullName)`"" + } else { + $dismParams += "/SkipLicense" + } + + # Execute the DISM command + $dismCommand = "DISM.exe " + ($dismParams -join " ") + Write-Host "Constructed DISM command:" + Write-Output $dismCommand + + try { + Invoke-Expression -Command $dismCommand -ErrorAction Stop + Write-Host "Successfully installed $($mainPackage.Name)." + } catch { + Write-Error "DISM command failed for $($mainPackage.Name). Error: $($_.Exception.Message)" + } + Write-Output "" +} + +# Final cleanup +Write-Host "Installation process finished." +pause +Remove-TemporaryFiles \ No newline at end of file diff --git a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 index 09f2711..838123a 100644 --- a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 +++ b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 @@ -73,7 +73,7 @@ $allApps = @() # Read the WinGetWin32Apps.json file if it exists if (Test-Path -Path $wingetAppsJsonFile) { - Write-Host "Processing WinGetWin32Apps.json..." + Write-Host "Processing WinGetWin32Apps.json first..." try { $wingetApps = Get-Content -Path $wingetAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json if ($wingetApps -is [array]) { @@ -87,8 +87,6 @@ if (Test-Path -Path $wingetAppsJsonFile) { } } catch { Write-Error "Failed to read or parse WinGetWin32Apps.json file: $_" - # Decide if execution should stop or continue - # exit 1 } } else { Write-Host "WinGetWin32Apps.json file not found. Skipping." @@ -96,7 +94,7 @@ if (Test-Path -Path $wingetAppsJsonFile) { # Read the UserAppList.json file if it exists if (Test-Path -Path $userAppsJsonFile) { - Write-Host "Processing UserAppList.json..." + Write-Host "Processing UserAppList.json next..." try { $userApps = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json if ($userApps -is [array]) { @@ -110,8 +108,6 @@ if (Test-Path -Path $userAppsJsonFile) { } } catch { Write-Error "Failed to read or parse UserAppList.json file: $_" - # Decide if execution should stop or continue - # exit 1 } } else { Write-Host "UserAppList.json file not found. Skipping." @@ -168,8 +164,6 @@ foreach ($app in $sortedApps) { Write-Host "$($app.Name) exited with exit code: $($result.ExitCode)`r`n" } catch { Write-Error "Error occurred while installing $($app.Name): $_" - # Decide if execution should stop or continue after an error - # exit 1 } } diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 index d0aad83..a9f3c8c 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 @@ -345,9 +345,6 @@ function Save-HPDriversTask { New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null $arguments = "/s /e /f `"$extractFolder`"" WriteLog "Extracting driver $driverFilePath with args: $arguments" - #DEBUG - # wrap $driverFilePath in quotes to handle spaces - # $driverFilePath = "`"$driverFilePath`"" WriteLog "Running HP Driver Extraction Command: $driverFilePath $arguments" Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -ErrorAction Stop | Out-Null # Start-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait -NoNewWindow -ErrorAction Stop | Out-Null