NipkgHelper.psm1
#Requires -Version 5.1 function ConvertTo-PackageSpecifications { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject[]]$PackageDefinitions ) $packageSpecs = @() foreach ($packageDef in $PackageDefinitions) { # Validate package definition if (-not $packageDef.Package) { Write-Warning "Package definition missing 'Package' property, skipping" continue } if (-not $packageDef.Version) { Write-Warning "Package definition for '$($packageDef.Package)' missing 'Version' property, skipping" continue } $packageName = $packageDef.Package $packageVersion = $packageDef.Version $packageSpec = "$packageName=$packageVersion" $packageSpecs += $packageSpec Write-Verbose "Added package specification: $packageSpec" } return $packageSpecs } function Install-Packages { [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [PSCustomObject[]]$PackageDefinitions, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [switch]$AcceptEulas, [Parameter()] [switch]$Yes, [Parameter()] [switch]$Simulate, [Parameter()] [switch]$ForceLocked, [Parameter()] [switch]$AllowDowngrade, [Parameter()] [switch]$AllowUninstall, [Parameter()] [switch]$InstallAlsoUpgrades, [Parameter()] [switch]$IncludeRecommended, [Parameter()] [switch]$SuppressIncompatibilityErrors ) begin { try { $null = Get-Command $NipkgCmdPath -ErrorAction Stop } catch { throw "NIPKG command not found: $NipkgCmdPath. Please ensure NI Package Manager is installed and accessible." } } process { if (-not $PackageDefinitions -or $PackageDefinitions.Count -eq 0) { Write-Warning "No package definitions provided, skipping installation" return } $packageSpecs = ConvertTo-PackageSpecifications -PackageDefinitions $PackageDefinitions if ($packageSpecs.Count -eq 0) { Write-Warning "No valid package definitions found, skipping installation" return } Write-Information "Installing $($packageSpecs.Count) packages in batch mode..." -InformationAction Continue Write-Information "Package specifications: $($packageSpecs -join ', ')" -InformationAction Continue $arguments = @("install") + $packageSpecs if ($AcceptEulas) { $arguments += "--accept-eulas" } if ($Yes) { $arguments += "-y" } if ($Simulate) { $arguments += "--simulate" } if ($ForceLocked) { $arguments += "--force-locked" } $arguments += "--verbose" if ($SuppressIncompatibilityErrors) { $arguments += "--suppress-incompatibility-errors" } if ($AllowDowngrade) { $arguments += "--allow-downgrade" } if ($AllowUninstall) { $arguments += "--allow-uninstall" } if ($InstallAlsoUpgrades) { $arguments += "--install-also-upgrades" } if ($IncludeRecommended) { $arguments += "--include-recommended" } Write-Verbose "Executing NIPKG command: $NipkgCmdPath $($arguments -join ' ')" # Execute NIPKG command with interactive output & $NipkgCmdPath @arguments $exitCode = $LASTEXITCODE Write-Verbose "NIPKG installation completed with exit code: $exitCode" switch ($exitCode) { 0 { Write-Information "Successfully installed $($packageSpecs.Count) packages" -InformationAction Continue } -125071 { Write-Warning "Installation completed successfully but requires system restart (exit code: $exitCode)" $global:LASTEXITCODE = 0 } default { throw "Packages Installation failed with exit code: $exitCode" } } } end { } } function Add-FeedDirectories { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$FeedsDirectory, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg" ) begin { Write-Verbose "Starting Add-FeedDirectories operation" } process { try { if (-not (Test-Path -LiteralPath $FeedsDirectory)) { throw "Feeds directory not found: $FeedsDirectory" } $directories = Get-ChildItem -Path $FeedsDirectory -Directory -ErrorAction Stop if ($directories.Count -eq 0) { Write-Warning "No subdirectories found in: $FeedsDirectory" return } Write-Information "Adding $($directories.Count) feed directories..." -InformationAction Continue foreach ($dir in $directories) { if ($PSCmdlet.ShouldProcess($dir.FullName, "Add NIPKG feed")) { # Convert folder name to valid feed name $feedName = ConvertTo-FeedName -FolderName $dir.Name Write-Verbose "Adding feed directory: $($dir.Name) as feed name: $feedName" Write-Information "Adding feed: $feedName (from folder: $($dir.Name))" -InformationAction Continue & $NipkgCmdPath feed-add "$($dir.FullName)" --name="$feedName" if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to add feed: $feedName" } } } if ($PSCmdlet.ShouldProcess("NIPKG", "Update package database")) { Write-Information "Updating package database..." -InformationAction Continue & $NipkgCmdPath update if ($LASTEXITCODE -eq 0) { Write-Information "Package database updated successfully" -InformationAction Continue } else { Write-Warning "Package database update may have failed" } } } catch { $errorMessage = "Failed to add feed directories - Path: '$FeedsDirectory'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $FeedsDirectory throw } } end { Write-Verbose "Completed Add-FeedDirectories operation" } } function Get-FeedDefinitionsFromSystem { <# .SYNOPSIS Retrieves information about configured NIPKG feeds. .DESCRIPTION Gets a list of all currently configured package feeds from NI Package Manager, including feed names and their associated paths. Can filter feeds by type (local vs online). .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .PARAMETER ExcludeLocalFeeds Exclude local file system feeds from the results. Only online feeds will be returned. .PARAMETER ExcludeOnlineFeeds Exclude online feeds from the results. Only local file system feeds will be returned. .EXAMPLE Get-FeedDefinitionsFromSystem .EXAMPLE Get-FeedDefinitionsFromSystem -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" .EXAMPLE # Get only online feeds Get-FeedDefinitionsFromSystem -ExcludeLocalFeeds .EXAMPLE # Get only local feeds Get-FeedDefinitionsFromSystem -ExcludeOnlineFeeds .NOTES Returns PSCustomObject with Name, Path, and Type properties for each feed. Feed types are determined as follows: - Local: file:// paths or local file system paths (C:\, D:\, etc.) - Online: http://, https://, ftp://, or other network protocols If neither ExcludeLocalFeeds nor ExcludeOnlineFeeds is specified, all feeds are returned. #> [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [switch]$ExcludeLocalFeeds, [Parameter()] [switch]$ExcludeOnlineFeeds ) begin { Write-Verbose "Starting Get-FeedDefinitionsFromSystem operation" # Validate parameter combination if ($ExcludeLocalFeeds -and $ExcludeOnlineFeeds) { throw "Cannot exclude both local and online feeds. Please specify only one exclusion parameter or none." } } process { try { Write-Verbose "Executing NIPKG feed-list command" $lines = & $NipkgCmdPath feed-list if ($LASTEXITCODE -ne 0) { throw "NIPKG feed-list command failed with exit code: $LASTEXITCODE" } $allResults = @() foreach ($line in $lines) { $line = $line.Trim() # Parse feed lines - handle both local paths and URIs if ($line -match '^(.*\S)\s+(.+)$') { $feedName = $matches[1].Trim() $feedPath = $matches[2].Trim() # Determine feed type based on path $feedType = if (Test-FeedIsLocal -FeedPath $feedPath) { "Local" } else { "Online" } $feedObj = [PSCustomObject]@{ Name = $feedName Path = $feedPath Type = $feedType } $allResults += $feedObj Write-Verbose "Found feed: $feedName ($feedType) -> $feedPath" } } Write-Verbose "Retrieved $($allResults.Count) total feeds from NIPKG" # Apply filtering based on parameters $filteredResults = $allResults if ($ExcludeLocalFeeds) { $filteredResults = $filteredResults | Where-Object { $_.Type -eq "Online" } Write-Verbose "Filtered to $($filteredResults.Count) online feeds (excluded local)" } elseif ($ExcludeOnlineFeeds) { $filteredResults = $filteredResults | Where-Object { $_.Type -eq "Local" } Write-Verbose "Filtered to $($filteredResults.Count) local feeds (excluded online)" } else { Write-Verbose "Returning all $($filteredResults.Count) feeds (no filtering applied)" } return $filteredResults } catch { $errorMessage = "Failed to retrieve feeds information from NIPKG. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $NipkgCmdPath throw } } end { Write-Verbose "Completed Get-FeedDefinitionsFromSystem operation" } } function Remove-Feeds { <# .SYNOPSIS Removes specified feeds from NI Package Manager. .DESCRIPTION Removes one or more package feeds from NIPKG configuration based on provided feed information. .PARAMETER FeedsInfo PSCustomObject containing feed information with Name and Path properties. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .EXAMPLE $feeds = Get-FeedDefinitionsFromSystem Remove-Feeds -FeedsInfo $feeds .EXAMPLE Remove-Feeds -FeedsInfo $feedsToRemove -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" .NOTES Use Get-FeedDefinitionsFromSystem to get the required FeedsInfo object. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [PSCustomObject]$FeedsInfo, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg" ) begin { Write-Verbose "Starting Remove-Feeds operation" } process { try { foreach ($feed in $FeedsInfo) { if ($PSCmdlet.ShouldProcess($feed.Name, "Remove NIPKG feed")) { Write-Verbose "Removing feed: $($feed.Name)" Write-Information "Removing feed: $($feed.Name)" -InformationAction Continue & $NipkgCmdPath feed-remove "$($feed.Name)" if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to remove feed: $($feed.Name)" } } } } catch { Write-Error "Failed to remove feeds: $_" throw } } end { Write-Verbose "Completed Remove-Feeds operation" } } function Get-NipkgCommandOutput { <# .SYNOPSIS Executes NIPKG command and returns raw output lines. .DESCRIPTION Executes the specified NIPKG command and returns the raw output as an array of lines. This function handles only command execution, not parsing. .PARAMETER Type Specifies whether to get 'installed' or 'available' packages. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .EXAMPLE $output = Get-NipkgCommandOutput -Type "installed" .EXAMPLE $output = Get-NipkgCommandOutput -Type "available" -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" .NOTES Returns an array of strings representing the raw command output. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet("installed", "available")] [string]$Type, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg" ) begin { Write-Verbose "Starting Get-NipkgCommandOutput operation" # Check if NIPKG command is available try { $null = Get-Command $NipkgCmdPath -ErrorAction Stop } catch { throw "NIPKG command not found: $NipkgCmdPath. Please ensure NI Package Manager is installed and accessible." } } process { try { $subCmd = if ($Type -eq "installed") { "info-installed" } else { "info" } Write-Verbose "Executing NIPKG command: $NipkgCmdPath $subCmd" $lines = & $NipkgCmdPath $subCmd # Handle empty or null output if (-not $lines) { Write-Warning "NIPKG command returned no output" return @() } # Ensure we have an array and filter out any null/empty entries that might cause binding issues $filteredLines = @() foreach ($line in $lines) { if ($null -ne $line) { $filteredLines += $line.ToString() } } if ($filteredLines.Count -eq 0) { Write-Warning "No valid output lines after filtering" return @() } Write-Verbose "Command executed successfully, returned $($filteredLines.Count) valid lines" return $filteredLines } catch { $errorMessage = "Failed to execute NIPKG command - Type: '$Type', Command: '$NipkgCmdPath'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $Type throw } } end { Write-Verbose "Completed Get-NipkgCommandOutput operation" } } function ConvertFrom-NipkgOutput { <# .SYNOPSIS Parses NIPKG command output into structured package information. .DESCRIPTION Takes raw NIPKG command output lines and parses them into structured PSCustomObjects containing package properties. Handles package separation and key-value parsing. .PARAMETER OutputLines Array of strings representing the raw NIPKG command output. .EXAMPLE $output = Get-NipkgCommandOutput -Type "installed" $packages = ConvertFrom-NipkgOutput -OutputLines $output .EXAMPLE $lines = @("Package: example", "Version: 1.0.0", "", "", "", "Package: another", "Version: 2.0.0") $packages = ConvertFrom-NipkgOutput -OutputLines $lines .NOTES Returns an array of PSCustomObjects with package properties parsed from the output. Uses 3+ consecutive blank lines as package separators. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]]$OutputLines ) begin { Write-Verbose "Starting ConvertFrom-NipkgOutput operation" } process { try { # Handle empty input if (-not $OutputLines -or $OutputLines.Count -eq 0) { Write-Warning "No output lines provided for parsing" return @() } # Variables to hold packages $packages = @() $currentPackage = @() $blankLineCount = 0 Write-Verbose "Processing $($OutputLines.Count) output lines" foreach ($line in $OutputLines) { if ([string]::IsNullOrWhiteSpace($line)) { # Increment count for blank lines $blankLineCount++ # If 3 or more consecutive blank lines, start a new package block if ($blankLineCount -ge 3) { if ($currentPackage.Count -gt 0) { $packages += ,@($currentPackage) $currentPackage = @() } # Reset blank line count after splitting $blankLineCount = 0 } } else { # Non-blank line, reset blank line count and add to current package $blankLineCount = 0 $currentPackage += $line } } # Add last package if any lines remain if ($currentPackage.Count -gt 0) { $packages += ,@($currentPackage) } Write-Verbose "Split output into $($packages.Count) package blocks" # Parse each package block into key-value pairs $parsedPackages = @() foreach ($packageLines in $packages) { $packageProps = @{} foreach ($line in $packageLines) { # Split on first colon $key, $value = $line -split ":\s*", 2 if ($key -and $value) { $packageProps[$key] = $value } } if ($packageProps.Count -gt 0) { $parsedPackages += [PSCustomObject]$packageProps } } Write-Verbose "Parsed $($parsedPackages.Count) packages from output" return $parsedPackages } catch { $errorMessage = "Failed to parse NIPKG output. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidData -TargetObject $OutputLines throw } } end { Write-Verbose "Completed ConvertFrom-NipkgOutput operation" } } function ConvertFrom-NipkgPackagesFile { <# .SYNOPSIS Parses NIPKG Packages file content into structured package information. .DESCRIPTION Takes content from NIPKG Packages files and parses it into structured PSCustomObjects. This function is specifically designed for NIPKG feed Packages files which use single blank line separation between packages (unlike command output which uses 3+ blank lines). .PARAMETER OutputLines Array of strings representing the content from a NIPKG Packages file. .EXAMPLE $lines = Get-Content "Packages" -Encoding UTF8 $packages = ConvertFrom-NipkgPackagesFile -OutputLines $lines .NOTES Returns an array of PSCustomObjects with package properties parsed from the file content. Uses single blank lines as package separators (NIPKG feed file format). #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]]$OutputLines ) begin { Write-Verbose "Starting ConvertFrom-NipkgPackagesFile operation" } process { try { # Handle empty input if (-not $OutputLines -or $OutputLines.Count -eq 0) { Write-Warning "No output lines provided for parsing" return @() } # Variables to hold packages $packages = @() $currentPackage = @() Write-Verbose "Processing $($OutputLines.Count) output lines" foreach ($line in $OutputLines) { if ([string]::IsNullOrWhiteSpace($line)) { # Single blank line indicates end of package block in Packages files if ($currentPackage.Count -gt 0) { $packages += ,@($currentPackage) $currentPackage = @() } } else { # Non-blank line, add to current package $currentPackage += $line } } # Add last package if any lines remain if ($currentPackage.Count -gt 0) { $packages += ,@($currentPackage) } Write-Verbose "Split output into $($packages.Count) package blocks" # Parse each package block into key-value pairs $parsedPackages = @() foreach ($packageLines in $packages) { $packageProps = @{} foreach ($line in $packageLines) { # Split on first colon $key, $value = $line -split ":\s*", 2 if ($key -and $value) { $packageProps[$key] = $value } } if ($packageProps.Count -gt 0) { $parsedPackages += [PSCustomObject]$packageProps } } Write-Verbose "Parsed $($parsedPackages.Count) packages from Packages file" return $parsedPackages } catch { $errorMessage = "Failed to parse NIPKG Packages file content. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidData -TargetObject $OutputLines throw } } end { Write-Verbose "Completed ConvertFrom-NipkgPackagesFile operation" } } function Get-PackagesInfoFromNipkgCmd { <# .SYNOPSIS Retrieves package information from NI Package Manager. .DESCRIPTION Gets detailed information about either installed or available packages from NIPKG, parsing the output into structured PSCustomObject format. This function combines command execution and output parsing for convenience. .PARAMETER Type Specifies whether to get 'installed' or 'available' packages. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .EXAMPLE Get-PackagesInfoFromNipkgCmd -Type "installed" .EXAMPLE Get-PackagesInfoFromNipkgCmd -Type "available" -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" .NOTES Returns an array of PSCustomObjects with package properties parsed from NIPKG output. This function is a convenience wrapper around Get-NipkgCommandOutput and ConvertFrom-NipkgOutput. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet("installed", "available")] [string]$Type, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg" ) begin { Write-Verbose "Starting Get-PackagesInfoFromNipkgCmd operation" } process { try { # Get raw command output $outputLines = Get-NipkgCommandOutput -Type $Type -NipkgCmdPath $NipkgCmdPath # Handle empty output if (-not $outputLines -or $outputLines.Count -eq 0) { Write-Warning "No package information available for type: $Type" return @() } # Parse the output into structured format $parsedPackages = ConvertFrom-NipkgOutput -OutputLines $outputLines Write-Verbose "Retrieved and parsed $($parsedPackages.Count) packages" return $parsedPackages } catch { $errorMessage = "Failed to get packages info - Type: '$Type'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $Type throw } } end { Write-Verbose "Completed Get-PackagesInfoFromNipkgCmd operation" } } function Get-PackagesInfoFromPackagesFile { <# .SYNOPSIS Retrieves package information from a NIPKG Packages file. .DESCRIPTION Reads and parses a NIPKG Packages file (typically found in feeds) and converts it into structured PSCustomObject format. This file format is the same as NIPKG command output but stored in a file, commonly used in NIPKG feeds and repositories. .PARAMETER InputPath Path to the NIPKG Packages file to parse. .EXAMPLE Get-PackagesInfoFromPackagesFile -InputPath "C:\feeds\my-feed\Packages" .EXAMPLE $packages = Get-PackagesInfoFromPackagesFile -InputPath ".\test-data\Packages" $packages | Where-Object { $_.Section -eq "Programming Environments" } .NOTES Returns an array of PSCustomObjects with package properties parsed from the NIPKG Packages file. The file format is the same as 'nipkg info' command output, with packages separated by blank lines and properties in key: value format. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$InputPath ) begin { Write-Verbose "Starting Get-PackagesInfoFromPackagesFile operation" } process { try { # Validate input file exists if (-not (Test-Path -Path $InputPath)) { throw "NIPKG Packages file not found: $InputPath" } Write-Information "Reading NIPKG Packages file..." -InformationAction Continue Write-Verbose "Reading file: $InputPath" # Read file content as array of lines $outputLines = Get-Content -Path $InputPath -Encoding UTF8 Write-Verbose "Read $($outputLines.Count) lines from NIPKG Packages file" # Parse the output using the specialized Packages file parser $parsedPackages = ConvertFrom-NipkgPackagesFile -OutputLines $outputLines Write-Information "Parsed packages from NIPKG file successfully: $InputPath" -InformationAction Continue Write-Verbose "Parsed $($parsedPackages.Count) packages from NIPKG Packages file" return $parsedPackages } catch { $errorMessage = "Failed to get packages info from NIPKG file - InputPath: '$InputPath'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $InputPath throw } } end { Write-Verbose "Completed Get-PackagesInfoFromPackagesFile operation" } } function Get-DriverPackages { return { $packages = @($input) $packages | Where-Object { $_.Section -eq 'Drivers' } } } function Get-ProgrammingEnvironmentsPackages { return { $packages = @($input) $packages | Where-Object { $_.Section -eq 'Programming Environments' } } } function Get-UtilitiesPackages { return { $packages = @($input) $packages | Where-Object { $_.Section -eq 'Utilities' } } } function Get-ApplicationSoftwarePackages { return { $packages = @($input) $packages | Where-Object { $_.Section -eq 'Application Software' } } } function Get-StoreProductPackages { return { $packages = @($input) $packages | Where-Object { $_.StoreProduct -eq 'yes' } } } function Get-UserVisiblePackages { return { $packages = @($input) $packages | Where-Object { $_.UserVisible -eq 'yes' } } } function Install-NipkgManager { <# .SYNOPSIS Installs NI Package Manager using provided installer. .DESCRIPTION Executes the NI Package Manager installer with quiet installation parameters, accepting EULAs and preventing reboots during installation. .PARAMETER InstallerPath Path to the NI Package Manager installer executable. .EXAMPLE Install-NipkgManager -InstallerPath "C:\Installers\nipkg-manager.exe" .NOTES Installation runs with timeout of 5 minutes and captures exit codes and output. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$InstallerPath ) begin { Write-Verbose "Starting Install-NipkgManager operation" } process { try { if (-not (Test-Path -Path $InstallerPath)) { throw "Installer not found: $InstallerPath" } if ($PSCmdlet.ShouldProcess($InstallerPath, "Install NI Package Manager")) { Write-Information "Installing NI Package Manager..." -InformationAction Continue $timeout = 300000 # 5 minutes # Set up the process start info $process = New-Object System.Diagnostics.Process $process.StartInfo.FileName = $InstallerPath $process.StartInfo.Arguments = "--quiet --accept-eulas --prevent-reboot" $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true $process.StartInfo.UseShellExecute = $false $process.StartInfo.CreateNoWindow = $false # Start the process Write-Verbose "Starting installer process: $InstallerPath" $process.Start() # Wait for it to exit $process.WaitForExit($timeout) # Capture the exit code $exitCode = $process.ExitCode # Optionally capture output or error text $output = $process.StandardOutput.ReadToEnd() $errorOutput = $process.StandardError.ReadToEnd() # Display results Write-Information "Installation completed with exit code: $exitCode" -InformationAction Continue Write-Verbose "Installer output: $output" if ($errorOutput) { Write-Warning "Installer error output: $errorOutput" } if ($exitCode -ne 0) { Write-Warning "Installation may have failed with exit code: $exitCode" } return [PSCustomObject]@{ ExitCode = $exitCode Output = $output ErrorOutput = $errorOutput InstallerPath = $InstallerPath } } } catch { $errorMessage = "Failed to install NI Package Manager - InstallerPath: '$InstallerPath'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $InstallerPath throw } } end { Write-Verbose "Completed Install-NipkgManager operation" } } function New-Snapshot { <# .SYNOPSIS Creates a snapshot object from provided NIPKG feeds and packages information. .DESCRIPTION Formats feeds and packages information from provided PSObjects into a structured object. This object can be serialized to JSON or used for other purposes. When exactly one ni-package-manager package is found in the PackagesInfo, the function automatically resolves all its dependencies using Resolve-PackageDependencies and creates a separate packages-nipkg section containing these dependencies ordered for installation using Get-PackageInstallationOrder. This ensures that NIPKG package dependencies are properly tracked and can be installed in the correct order when applying snapshots. If more than one ni-package-manager package is found, the function will throw an error and stop processing, as only exactly one ni-package-manager package is allowed per snapshot. .PARAMETER FeedsInfo PSCustomObject containing feed information from Get-FeedDefinitionsFromSystem. .PARAMETER PackagesInfo PSCustomObject containing package information from Get-PackagesInfoFromNipkgCmd. .PARAMETER FilterFunction Optional scriptblock or function name to filter packages. If not provided, all packages are included. The filter function should accept pipeline input and return filtered packages. .EXAMPLE $feeds = Get-FeedDefinitionsFromSystem $packages = Get-PackagesInfoFromNipkgCmd -Type "installed" $snapshot = New-Snapshot -FeedsInfo $feeds -PackagesInfo $packages .EXAMPLE # Create snapshot with only user-visible packages using filter function $snapshot = New-Snapshot -FeedsInfo $feeds -PackagesInfo $packages -FilterFunction { Get-UserVisiblePackages } .EXAMPLE # Create snapshot with only store products $snapshot = New-Snapshot -FeedsInfo $feeds -PackagesInfo $packages -FilterFunction { Get-StoreProductPackages } .EXAMPLE # Create snapshot with chained filters $snapshot = New-Snapshot -FeedsInfo $feeds -PackagesInfo $packages -FilterFunction { Get-UserVisiblePackages | Get-StoreProductPackages } .NOTES Returns a hashtable with: - feeds: Array of feeds with name and uri - packages: Array of packages with name and version (filtered based on FilterFunction) - packages-nipkg: Array of ni-package-manager packages and their resolved dependencies ordered for installation (only present when exactly one ni-package-manager package is found) The packages-nipkg section includes all dependencies for the ni-package-manager package found in the PackagesInfo, ordered for optimal installation sequence. This ensures that when snapshots are applied, all NIPKG dependencies are available for installation in the correct order. NIPKG packages will also appear in the regular packages section (duplicated), which is expected behavior. If more than one ni-package-manager package is found in the input, an error will be thrown and processing will stop. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowEmptyCollection()] [PSCustomObject[]]$FeedsInfo, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [PSCustomObject[]]$PackagesInfo, [Parameter()] [scriptblock]$FilterFunction ) begin { Write-Verbose "Starting New-Snapshot operation" } process { try { $filteredPackages = $PackagesInfo if ($FilterFunction) { Write-Verbose "Applying custom filter function..." $originalCount = $filteredPackages.Count try { $filteredPackages = $filteredPackages | & $FilterFunction Write-Verbose "Filtered packages using custom function: $($filteredPackages.Count) (was $originalCount)" } catch { Write-Error "Filter function failed, using unfiltered packages: $($_.Exception.Message)" throw } } else { Write-Verbose "No filter function provided, using all packages: $($filteredPackages.Count)" } # Convert feeds to desired format $feeds = @() foreach ($feed in $FeedsInfo) { $feeds += [ordered]@{ name = $feed.Name uri = $feed.Path } } Write-Verbose "Formatting packages information" # Convert packages to desired format $packages = @() foreach ($package in $filteredPackages) { $packages += [ordered]@{ Package = $package.Package Version = $package.Version Depends = $package.Depends Description = $package.Description Section = $package.Section UserVisible = $package.UserVisible StoreProduct = $package.StoreProduct } } # Resolve dependencies for ni-package-manager and create packages-nipkg section Write-Verbose "Resolving dependencies for ni-package-manager packages..." $packagesNipkg = @() # Find ni-package-manager packages in the filtered packages (already validated above) $nipkgPackages = $PackagesInfo | Where-Object { $_.Package -eq "ni-package-manager" } if ($nipkgPackages.Count -eq 1) { Write-Verbose "Found exactly one ni-package-manager package" foreach ($nipkgPackage in $nipkgPackages) { try { Write-Verbose "Resolving dependencies for ni-package-manager package: $($nipkgPackage.Package) v$($nipkgPackage.Version)" # Resolve all dependencies for this ni-package-manager package $resolvedDependencies = Resolve-PackageDependencies -PackageName $nipkgPackage.Package -PackagesInfo $PackagesInfo -IncludeRootPackage $true -AllowCircularDependencies $true Write-Verbose "Resolved $($resolvedDependencies.Count) dependencies for $($nipkgPackage.Package)" # Apply installation order to the resolved dependencies try { Write-Verbose "Ordering resolved dependencies for optimal installation sequence..." $orderedDependencies = Get-PackageInstallationOrder -PackagesInfo $resolvedDependencies -AllowCircularDependencies Write-Verbose "Successfully ordered $($orderedDependencies.Count) dependencies (circular dependencies allowed)" } catch { Write-Warning "Dependency ordering failed for $($nipkgPackage.Package), using original order: $($_.Exception.Message)" $orderedDependencies = $resolvedDependencies } # Add ordered dependencies to packages-nipkg section foreach ($dependency in $orderedDependencies) { # Check if not already added (avoid duplicates) $existingPackage = $packagesNipkg | Where-Object { $_.Package -eq $dependency.Package } if (-not $existingPackage) { $packagesNipkg += [ordered]@{ Package = $dependency.Package Version = $dependency.Version Depends = $dependency.Depends Description = $dependency.Description Section = $dependency.Section UserVisible = $dependency.UserVisible StoreProduct = $dependency.StoreProduct } } } } catch { Write-Warning "Failed to resolve dependencies for ni-package-manager package '$($nipkgPackage.Package)': $($_.Exception.Message)" # Still add the ni-package-manager package itself even if dependency resolution fails $existingPackage = $packagesNipkg | Where-Object { $_.Package -eq $nipkgPackage.Package } if (-not $existingPackage) { $packagesNipkg += [ordered]@{ Package = $nipkgPackage.Package Version = $nipkgPackage.Version } } } } Write-Verbose "Created packages-nipkg section with $($packagesNipkg.Count) package(s)" } else { Write-Verbose "No ni-package-manager packages found in the package list" Write-Verbose "Packages-nipkg section will not be created" } # Create the snapshot object $snapshot = @{ feeds = $feeds packages = $packages } # Only add packages-nipkg section if ni-package-manager packages were found if ($packagesNipkg.Count -gt 0) { $snapshot["packages-nipkg"] = $packagesNipkg Write-Verbose "Added packages-nipkg section with $($packagesNipkg.Count) package(s)" } else { Write-Verbose "Skipped packages-nipkg section (no ni-package-manager packages found)" } if ($packagesNipkg.Count -gt 0) { Write-Verbose "Snapshot object contains $($feeds.Count) feeds, $($packages.Count) packages, and $($packagesNipkg.Count) NIPKG packages with dependencies" } else { Write-Verbose "Snapshot object contains $($feeds.Count) feeds and $($packages.Count) packages" } return $snapshot } catch { $errorMessage = "Failed to create NIPKG snapshot object. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation throw } } end { Write-Verbose "Completed New-Snapshot operation" } } function Export-NipkgSnapshotToJson { <# .SYNOPSIS Exports a snapshot object to a JSON file. .DESCRIPTION Serializes a snapshot object (from New-Snapshot) to a JSON file. Creates the output directory if it doesn't exist. .PARAMETER Snapshot Hashtable containing the snapshot data to serialize. .PARAMETER OutputPath Path where the JSON snapshot file will be created. .EXAMPLE $snapshot = New-Snapshot -FeedsInfo $feeds -PackagesInfo $packages Export-NipkgSnapshotToJson -Snapshot $snapshot -OutputPath "C:\Snapshots\nipkg-snapshot.json" .NOTES Creates the output directory if it doesn't exist. Returns summary information about the exported file. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [hashtable]$Snapshot, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$OutputPath ) begin { Write-Verbose "Starting Export-NipkgSnapshotToJson operation" } process { try { if ($PSCmdlet.ShouldProcess($OutputPath, "Export NIPKG snapshot to JSON")) { Write-Information "Exporting NIPKG snapshot to JSON..." -InformationAction Continue # Convert to JSON $jsonContent = $Snapshot | ConvertTo-Json -Depth 10 -Compress:$false # Ensure output directory exists $outputDir = Split-Path -Path $OutputPath -Parent if ($outputDir -and -not (Test-Path -Path $outputDir)) { Write-Verbose "Creating output directory: $outputDir" New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } # Write JSON to file $jsonContent | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Information "Snapshot exported successfully: $OutputPath" -InformationAction Continue $feedsCount = if ($Snapshot.feeds) { $Snapshot.feeds.Count } else { 0 } $packagesCount = if ($Snapshot.packages) { $Snapshot.packages.Count } else { 0 } $nipkgPackagesCount = if ($Snapshot."packages-nipkg") { $Snapshot."packages-nipkg".Count } else { 0 } Write-Verbose "Exported snapshot contains type '$snapshotType', $feedsCount feeds, $packagesCount packages, and $nipkgPackagesCount NIPKG packages" # Return summary information return [PSCustomObject]@{ SnapshotPath = $OutputPath FeedsCount = $feedsCount PackagesCount = $packagesCount NipkgPackagesCount = $nipkgPackagesCount FileSizeBytes = (Get-Item -Path $OutputPath).Length } } } catch { $errorMessage = "Failed to export NIPKG snapshot to JSON - OutputPath: '$OutputPath'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $OutputPath throw } } end { Write-Verbose "Completed Export-NipkgSnapshotToJson operation" } } function Import-NipkgSnapshotFromJson { <# .SYNOPSIS Imports a NIPKG snapshot from a JSON file. .DESCRIPTION Reads and parses a JSON snapshot file created by Export-NipkgSnapshotToJson. Returns the snapshot object with feeds and packages information. .PARAMETER InputPath Path to the JSON snapshot file to import. .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "C:\Snapshots\nipkg-snapshot.json" .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "C:\Snapshots\snapshot.json" Write-Host "Imported $($snapshot.packages.Count) packages and $($snapshot.feeds.Count) feeds" .NOTES Returns a hashtable with: - type: Snapshot type (Full, UserVisible, StoreProduct, or Unknown if not specified) - feeds: Array of feeds with name and uri properties - packages: Array of packages with name and version properties - packages-nipkg: Array of NIPKG packages with name and version properties (if present) #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$InputPath ) begin { Write-Verbose "Starting Import-NipkgSnapshotFromJson operation" } process { try { # Validate input file exists if (-not (Test-Path -Path $InputPath)) { throw "Snapshot file not found: $InputPath" } Write-Information "Importing NIPKG snapshot from JSON..." -InformationAction Continue Write-Verbose "Reading JSON file: $InputPath" # Read and parse JSON content $jsonContent = Get-Content -Path $InputPath -Raw -Encoding UTF8 $snapshotData = $jsonContent | ConvertFrom-Json # Validate JSON structure if (-not $snapshotData) { throw "Invalid or empty JSON content" } # Convert back to hashtable format $snapshot = @{ feeds = @() packages = @() } # Process feeds if they exist if ($snapshotData.feeds) { Write-Verbose "Processing $($snapshotData.feeds.Count) feeds" foreach ($feed in $snapshotData.feeds) { $snapshot.feeds += [ordered]@{ name = $feed.name uri = $feed.uri } } } # Process packages if they exist if ($snapshotData.packages) { Write-Verbose "Processing $($snapshotData.packages.Count) packages" foreach ($package in $snapshotData.packages) { $snapshot.packages += [ordered]@{ Package = $package.Package Version = $package.Version Depends = $package.Depends Description = $package.Description Section = $package.Section UserVisible = $package.UserVisible StoreProduct = $package.StoreProduct } } } # Process packages-nipkg if they exist if ($snapshotData."packages-nipkg") { Write-Verbose "Processing $($snapshotData.'packages-nipkg'.Count) NIPKG packages" $snapshot."packages-nipkg" = @() foreach ($package in $snapshotData."packages-nipkg") { $snapshot."packages-nipkg" += [ordered]@{ Package = $package.Package Version = $package.Version Depends = $package.Depends Description = $package.Description Section = $package.Section UserVisible = $package.UserVisible StoreProduct = $package.StoreProduct } } } Write-Information "Snapshot imported successfully: $InputPath" -InformationAction Continue $nipkgPackagesCount = if ($snapshot."packages-nipkg") { $snapshot."packages-nipkg".Count } else { 0 } $snapshotType = $snapshot.type if ($nipkgPackagesCount -gt 0) { Write-Verbose "Imported snapshot contains type '$snapshotType', $($snapshot.feeds.Count) feeds, $($snapshot.packages.Count) packages, and $nipkgPackagesCount NIPKG packages" } else { Write-Verbose "Imported snapshot contains type '$snapshotType', $($snapshot.feeds.Count) feeds and $($snapshot.packages.Count) packages" } return $snapshot } catch { $errorMessage = "Failed to import NIPKG snapshot from JSON - InputPath: '$InputPath'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $InputPath throw } } end { Write-Verbose "Completed Import-NipkgSnapshotFromJson operation" } } function Test-NipkgAvailability { <# .SYNOPSIS Tests if NI Package Manager is available and accessible. .DESCRIPTION Checks if the NIPKG command is available and can be executed. This is useful for troubleshooting connection issues. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .EXAMPLE Test-NipkgAvailability .EXAMPLE Test-NipkgAvailability -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" .NOTES Returns $true if NIPKG is available, $false otherwise. #> [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg" ) begin { Write-Verbose "Starting Test-NipkgAvailability operation" } process { try { # Test if command exists $command = Get-Command $NipkgCmdPath -ErrorAction Stop Write-Verbose "NIPKG command found at: $($command.Source)" # Test if command can execute (try getting version) $versionOutput = & $NipkgCmdPath --version 2>&1 if ($LASTEXITCODE -eq 0) { Write-Information "NIPKG is available and working" -InformationAction Continue Write-Verbose "NIPKG version output: $versionOutput" return $true } else { Write-Warning "NIPKG command exists but failed to execute properly (exit code: $LASTEXITCODE)" Write-Verbose "NIPKG error output: $versionOutput" return $false } } catch { Write-Warning "NIPKG command not found or not accessible: $NipkgCmdPath" Write-Verbose "Error details: $($_.Exception.Message)" return $false } } end { Write-Verbose "Completed Test-NipkgAvailability operation" } } function New-SnapshotFromInstallerDirectory { <# .SYNOPSIS Creates a NIPKG snapshot from an installer directory structure. .DESCRIPTION Scans an installer directory for feeds and packages, then creates a snapshot object containing all available feeds and packages from the installer. This is useful for creating snapshots from offline installers before installation. The function automatically detects and includes: - Standard feeds in the 'feeds' subdirectory - Special NIPKG feed in 'bin/ni-package-manager-packages' (for offline installers) Feed names are generated from folder names using ConvertTo-FeedName to ensure compatibility with feed naming requirements (dots are replaced with underscores). Duplicate packages (same name, different versions) are automatically removed by selecting the highest version of each package before creating the snapshot. .PARAMETER InstallerDirectory Path to the installer directory containing the feeds subdirectory. .PARAMETER IncludeFeeds Whether to include feed information in the snapshot. Defaults to $true. .PARAMETER FilterFunction Optional scriptblock or function name to filter packages. If not provided, defaults to Get-StoreProductPackages. The filter function should accept pipeline input and return filtered packages. .EXAMPLE $snapshot = New-SnapshotFromInstallerDirectory -InstallerDirectory "C:\Installers\ni-labview-2025-community-x86_25.1.3_offline" .EXAMPLE $snapshot = New-SnapshotFromInstallerDirectory -InstallerDirectory ".\installers\labview-2025" -IncludeFeeds $false .EXAMPLE # Create snapshot with only user-visible packages from installer $snapshot = New-SnapshotFromInstallerDirectory -InstallerDirectory ".\installers\labview-2025" -FilterFunction { Get-UserVisiblePackages } .EXAMPLE # Create snapshot with chained filters from installer $snapshot = New-SnapshotFromInstallerDirectory -InstallerDirectory ".\installers\labview-2025" -FilterFunction { Get-UserVisiblePackages | Get-ProgrammingEnvironmentsPackages } .NOTES Returns a hashtable with: - feeds: Array of feeds with name and uri (if IncludeFeeds is true) - packages: Array of packages with name and version from all feeds Feed names are automatically converted using ConvertTo-FeedName to ensure compatibility (e.g., "ni-488.2" becomes "ni-488_2", "ni-vivado-2021.1-cg" becomes "ni-vivado-2021_1-cg"). The function looks for: 1. A 'feeds' subdirectory and scans all subdirectories for Packages files 2. A special 'bin/ni-package-manager-packages' directory for NIPKG packages (offline installers) Duplicate packages are automatically removed by selecting the highest version of each package. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$InstallerDirectory, [Parameter()] [bool]$IncludeFeeds = $true, [Parameter()] [scriptblock]$FilterFunction = { @($input) | & (Get-StoreProductPackages)} ) begin { Write-Verbose "Starting New-SnapshotFromInstallerDirectory operation" } process { try { # Validate installer directory exists if (-not (Test-Path -Path $InstallerDirectory -PathType Container)) { throw "Installer directory not found: $InstallerDirectory" } Write-Information "Scanning installer directory for feeds and packages..." -InformationAction Continue Write-Verbose "Installer directory: $InstallerDirectory" # Look for feeds directory $feedsDirectory = Join-Path -Path $InstallerDirectory -ChildPath "feeds" if (-not (Test-Path -Path $feedsDirectory -PathType Container)) { throw "Feeds directory not found in installer: $feedsDirectory" } Write-Verbose "Found feeds directory: $feedsDirectory" # Get all feed subdirectories $feedSubdirectories = Get-ChildItem -Path $feedsDirectory -Directory -ErrorAction Stop if ($feedSubdirectories.Count -eq 0) { Write-Warning "No feed subdirectories found in: $feedsDirectory" return @{ feeds = @() packages = @() } } Write-Information "Found $($feedSubdirectories.Count) feed(s) in installer" -InformationAction Continue # Initialize collections $allPackages = @() $allFeeds = @() # Check for special NIPKG feed in bin directory (for offline installers) $nipkgFeedDirectory = Join-Path -Path $InstallerDirectory -ChildPath "bin\ni-package-manager-packages" if (Test-Path -Path $nipkgFeedDirectory -PathType Container) { Write-Information "Found special NIPKG feed in bin directory" -InformationAction Continue Write-Verbose "Processing special NIPKG feed: $nipkgFeedDirectory" $nipkgPackagesFile = Join-Path -Path $nipkgFeedDirectory -ChildPath "Packages" if (Test-Path -Path $nipkgPackagesFile -PathType Leaf) { try { # Parse packages from the NIPKG feed $nipkgPackages = Get-PackagesInfoFromPackagesFile -InputPath $nipkgPackagesFile Write-Verbose "Found $($nipkgPackages.Count) packages in NIPKG feed" # Add packages to the combined collection $allPackages += $nipkgPackages # Add feed information if requested if ($IncludeFeeds) { $nipkgFeedInfo = [PSCustomObject]@{ Name = "local-installer-ni-package-manager-packages" Path = $nipkgFeedDirectory } $allFeeds += $nipkgFeedInfo } Write-Information "Successfully processed NIPKG feed with $($nipkgPackages.Count) packages" -InformationAction Continue } catch { Write-Warning "Failed to parse packages from NIPKG feed: $($_.Exception.Message)" } } else { Write-Warning "No Packages file found in NIPKG feed directory: $nipkgFeedDirectory" } } else { Write-Verbose "No special NIPKG feed found in bin directory" } # Process each feed subdirectory foreach ($feedDir in $feedSubdirectories) { Write-Verbose "Processing feed: $($feedDir.Name)" # Look for Packages file in this feed $packagesFile = Join-Path -Path $feedDir.FullName -ChildPath "Packages" if (Test-Path -Path $packagesFile -PathType Leaf) { Write-Information "Processing packages from feed: $($feedDir.Name)" -InformationAction Continue try { # Parse packages from this feed $feedPackages = Get-PackagesInfoFromPackagesFile -InputPath $packagesFile Write-Verbose "Found $($feedPackages.Count) packages in feed: $($feedDir.Name)" # Add packages to the combined collection $allPackages += $feedPackages # Add feed information if requested if ($IncludeFeeds) { $feedName = ConvertTo-FeedName -FolderName $feedDir.Name $feedInfo = [PSCustomObject]@{ Name = $feedName Path = $feedDir.FullName } $allFeeds += $feedInfo } } catch { Write-Warning "Failed to parse packages from feed '$($feedDir.Name)': $($_.Exception.Message)" } } else { Write-Warning "No Packages file found in feed directory: $($feedDir.FullName)" } } Write-Information "Successfully processed installer: $($allFeeds.Count) feeds, $($allPackages.Count) total packages" -InformationAction Continue # Remove duplicate packages by selecting highest version Write-Verbose "Removing duplicate packages by selecting highest versions" $uniquePackages = Remove-DuplicatePackagesByVersion -PackagesInfo $allPackages Write-Information "After duplicate removal: $($uniquePackages.Count) unique packages (removed $($allPackages.Count - $uniquePackages.Count) duplicates)" -InformationAction Continue $snapshot = New-Snapshot -FeedsInfo $allFeeds -PackagesInfo $uniquePackages -FilterFunction $FilterFunction Write-Verbose "Created snapshot with $($snapshot.feeds.Count) feeds and $($snapshot.packages.Count) packages" return $snapshot } catch { $errorMessage = "Failed to create snapshot from installer - InstallerDirectory: '$InstallerDirectory'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $InstallerDirectory throw } } end { Write-Verbose "Completed New-SnapshotFromInstallerDirectory operation" } } function New-SnapshotFromSystem { <# .SYNOPSIS Creates a NIPKG snapshot from the current system state. .DESCRIPTION Creates a snapshot by capturing the current NIPKG configuration including configured feeds and installed packages. This function combines Get-FeedDefinitionsFromSystem, Get-PackagesInfo, and New-Snapshot to create a complete system state snapshot. .PARAMETER PackageType Specifies whether to capture 'installed' or 'available' packages. Defaults to 'installed'. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .PARAMETER IncludeFeeds Whether to include current feed configuration in the snapshot. Defaults to $true. #> [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [bool]$IncludeFeeds = $true, [Parameter()] [scriptblock]$FilterFunction ) begin { Write-Verbose "Starting New-SnapshotFromSystem operation" } process { try { Write-Information "Creating NIPKG snapshot from current system state..." -InformationAction Continue # Initialize collections $feedsInfo = @() $packagesInfo = @() # Get current feeds if requested if ($IncludeFeeds) { Write-Information "Retrieving current feed configuration..." -InformationAction Continue Write-Verbose "Getting feeds info from NIPKG command" try { $feedsInfo = Get-FeedDefinitionsFromSystem -NipkgCmdPath $NipkgCmdPath -ExcludeLocalFeeds Write-Verbose "Retrieved $($feedsInfo.Count) feeds from system" Write-Information "Found $($feedsInfo.Count) configured feed(s)" -InformationAction Continue } catch { Write-Warning "Failed to retrieve feeds information: $($_.Exception.Message)" $feedsInfo = @() } } else { Write-Information "Skipping feed configuration (IncludeFeeds=$IncludeFeeds)" -InformationAction Continue } try { $packagesInfo = Get-PackagesInfoFromNipkgCmd -Type "Installed" -NipkgCmdPath $NipkgCmdPath Write-Verbose "Retrieved $($packagesInfo.Count) packages from system" Write-Information "Found $($packagesInfo.Count) $PackageType package(s)" -InformationAction Continue } catch { Write-Error "Failed to retrieve packages information: $($_.Exception.Message)" throw } # Create the snapshot using the retrieved information Write-Information "Creating snapshot object..." -InformationAction Continue Write-Verbose "Creating snapshot with type '$Type', $($feedsInfo.Count) feeds and $($packagesInfo.Count) packages" $snapshot = New-Snapshot -FeedsInfo $feedsInfo -PackagesInfo $packagesInfo -FilterFunction $FilterFunction Write-Information "System snapshot created successfully" -InformationAction Continue Write-Verbose "Snapshot contains $($snapshot.feeds.Count) feeds and $($snapshot.packages.Count) packages" return $snapshot } catch { $errorMessage = "Failed to create system snapshot - PackageType: '$PackageType'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $PackageType throw } } end { Write-Verbose "Completed New-SnapshotFromSystem operation" } } function Install-Snapshot { [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [hashtable]$Snapshot, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [switch]$SkipFeedsInstallation, [Parameter()] [switch]$SkipPackagesInstallation, [Parameter()] [switch]$RemoveExistingFeeds, [Parameter()] [switch]$RemoveSnapshotFeeds, [Parameter()] [switch]$AcceptEulas, [Parameter()] [switch]$Yes, [Parameter()] [switch]$Simulate, [Parameter()] [switch]$ForceLocked, [Parameter()] [switch]$AllowDowngrade, [Parameter()] [switch]$AllowUninstall, [Parameter()] [switch]$InstallAlsoUpgrades, [Parameter()] [switch]$IncludeRecommended, [Parameter()] [switch]$SuppressIncompatibilityErrors, [Parameter()] [scriptblock]$FilterFunction ) begin { Write-Verbose "Starting Install-Snapshot operation" } process { try { Write-Information "Applying NIPKG snapshot..." -InformationAction Continue Write-Information "Snapshot contains $($Snapshot.feeds.Count) feeds and $($Snapshot.packages.Count) packages" -InformationAction Continue if($RemoveExistingFeeds) { Write-Information "Removing existing feeds..." Get-FeedDefinitionsFromSystem -NipkgCmdPath $NipkgCmdPath | Remove-Feeds -NipkgCmdPath $NipkgCmdPath } $snapshotFeedsAdded = @() if (-not $SkipFeedsInstallation -and $Snapshot.feeds.Count -gt 0) { Write-Information "Adding feeds from snapshot..." $snapshotFeedsAdded = $Snapshot.feeds | Install-Feeds -NipkgCmdPath $NipkgCmdPath } if (-not $SkipPackagesInstallation -and $Snapshot.packages.Count -gt 0) { Write-Information "Installing Packages..." $packagesToInstall = Get-PackageDefinitionsFromSnapshot -Snapshot $Snapshot $filteredPackages = $packagesToInstall if ($FilterFunction) { Write-Verbose "Applying custom filter function..." $originalCount = $filteredPackages.Count try { $filteredPackages = $filteredPackages | & $FilterFunction $filteredPackages = Remove-DuplicatePackagesByVersion -PackagesInfo $filteredPackages Write-Verbose "Filtered packages using custom function: $($filteredPackages.Count) (was $originalCount)" } catch { Write-Error "Filter function failed, using unfiltered packages: $($_.Exception.Message)" throw } } else { Write-Verbose "No filter function provided, using all packages: $($filteredPackages.Count)" } $installSwitches = @{ NipkgCmdPath = $NipkgCmdPath AcceptEulas = $AcceptEulas.IsPresent Yes = $Yes.IsPresent Simulate = $Simulate.IsPresent ForceLocked = $ForceLocked.IsPresent AllowDowngrade = $AllowDowngrade.IsPresent AllowUninstall = $AllowUninstall.IsPresent InstallAlsoUpgrades = $InstallAlsoUpgrades.IsPresent IncludeRecommended = $IncludeRecommended.IsPresent PackageDefinitions = $filteredPackages SuppressIncompatibilityErrors = $SuppressIncompatibilityErrors.IsPresent } Install-Packages @installSwitches } if ($RemoveSnapshotFeeds -and $snapshotFeedsAdded.Count -gt 0) { Write-Information "Removing feeds added by snapshot..." $snapshotFeedsAdded | Remove-Feeds -NipkgCmdPath $NipkgCmdPath } Write-Information "Snapshot application completed successfully" -InformationAction Continue } catch { $errorMessage = "Failed to apply NIPKG snapshot. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $Snapshot throw } } end { Write-Verbose "Completed Install-Snapshot operation" # Return the snapshot for pipeline continuation return $Snapshot } } function Test-Snapshot { <# .SYNOPSIS Validates that the current system state matches the desired snapshot state. .DESCRIPTION Compares the current NIPKG configuration (feeds and/or packages) against a snapshot to verify compliance. This function is useful for validating that Install-Snapshot achieved the desired state or for ongoing compliance monitoring. .PARAMETER Snapshot The snapshot object containing packages and their versions. Can be passed via pipeline from Install-Snapshot. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .PARAMETER ValidateFeeds Whether to validate feed configuration compliance. Defaults to $true. .PARAMETER ValidatePackages Whether to validate package installation compliance. Defaults to $true. .PARAMETER FailOnNonCompliance Whether to throw an error if the system is not compliant. If false, returns compliance status without failing. Defaults to $false. .PARAMETER AllowExtraFeeds Allow extra feeds beyond those in the snapshot. If false, extra feeds cause non-compliance. Defaults to $false. .PARAMETER AllowExtraPackages Allow extra packages beyond those in the snapshot. If false, extra packages cause non-compliance. Defaults to $false. .OUTPUTS PSCustomObject with compliance validation results including: - IsCompliant: Boolean indicating overall compliance - FeedsCompliant: Boolean indicating feed compliance - PackagesCompliant: Boolean indicating package compliance - ComplianceSummary: String summary of compliance status .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "snapshot.json" Test-Snapshot -Snapshot $snapshot .EXAMPLE # Pipeline usage with Install-Snapshot Import-NipkgSnapshotFromJson -InputPath "snapshot.json" | Install-Snapshot | Test-Snapshot .EXAMPLE # Validate only packages, allow extra feeds $snapshot | Test-Snapshot -ValidateFeeds $false -AllowExtraFeeds $true .EXAMPLE # Strict validation - fail build if not compliant $snapshot | Test-Snapshot -FailOnNonCompliance .NOTES Returns a compliance report object with detailed information about differences. Can be used in CI/CD pipelines to ensure environment consistency. Accepts snapshot objects from pipeline or directly as parameter. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] $Snapshot, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [bool]$ValidateFeeds = $true, [Parameter()] [bool]$ValidatePackages = $true, [Parameter()] [switch]$FailOnNonCompliance, [Parameter()] [bool]$AllowExtraFeeds = $false, [Parameter()] [bool]$AllowExtraPackages = $false ) begin { Write-Verbose "Starting Test-Snapshot operation" } process { try { Write-Information "Validating NIPKG snapshot compliance..." -InformationAction Continue Write-Verbose "Snapshot contains $($Snapshot.feeds.Count) feeds and $($Snapshot.packages.Count) packages" # Initialize compliance report $complianceReport = [PSCustomObject]@{ IsCompliant = $true FeedsCompliant = $true PackagesCompliant = $true ComplianceSummary = "System is compliant with snapshot" FeedsDifferences = @() PackagesDifferences = @() ValidationTimestamp = Get-Date } # Validate feeds if requested if ($ValidateFeeds -and $Snapshot.feeds.Count -gt 0) { Write-Information "Validating feed compliance..." -InformationAction Continue try { $currentFeeds = Get-FeedDefinitionsFromSystem -NipkgCmdPath $NipkgCmdPath # Check for missing feeds foreach ($snapshotFeed in $Snapshot.feeds) { $matchingFeed = $currentFeeds | Where-Object { $_.name -eq $snapshotFeed.name -and $_.uri -eq $snapshotFeed.uri } if (-not $matchingFeed) { $complianceReport.FeedsDifferences += "Missing feed: $($snapshotFeed.name) ($($snapshotFeed.uri))" $complianceReport.FeedsCompliant = $false } } # Check for extra feeds if not allowed if (-not $AllowExtraFeeds) { foreach ($currentFeed in $currentFeeds) { $matchingFeed = $Snapshot.feeds | Where-Object { $_.name -eq $currentFeed.name -and $_.uri -eq $currentFeed.uri } if (-not $matchingFeed) { $complianceReport.FeedsDifferences += "Extra feed: $($currentFeed.name) ($($currentFeed.uri))" $complianceReport.FeedsCompliant = $false } } } } catch { Write-Warning "Failed to validate feeds: $($_.Exception.Message)" $complianceReport.FeedsCompliant = $false $complianceReport.FeedsDifferences += "Error validating feeds: $($_.Exception.Message)" } } # Validate packages if requested if ($ValidatePackages -and $Snapshot.packages.Count -gt 0) { Write-Information "Validating package compliance..." -InformationAction Continue try { $installedPackages = Get-PackagesInfoFromNipkgCmd -Type "installed" -NipkgCmdPath $NipkgCmdPath # Check for missing or incorrect versions foreach ($snapshotPackage in $Snapshot.packages) { $matchingPackage = $installedPackages | Where-Object { $_.Package -eq $snapshotPackage.Package } if (-not $matchingPackage) { $complianceReport.PackagesDifferences += "Missing package: $($snapshotPackage.Package) $($snapshotPackage.Version)" $complianceReport.PackagesCompliant = $false } elseif ($matchingPackage.Version -ne $snapshotPackage.version) { $complianceReport.PackagesDifferences += "Version mismatch: $($snapshotPackage.Package) (expected: $($snapshotPackage.version), actual: $($matchingPackage.Version))" $complianceReport.PackagesCompliant = $false } } # Check for extra packages if not allowed if (-not $AllowExtraPackages) { foreach ($installedPackage in $installedPackages) { $matchingPackage = $Snapshot.packages | Where-Object { $_.Package -eq $installedPackage.Package } if (-not $matchingPackage) { $complianceReport.PackagesDifferences += "Extra package: $($installedPackage.Package) $($installedPackage.Version)" $complianceReport.PackagesCompliant = $false } } } } catch { Write-Warning "Failed to validate packages: $($_.Exception.Message)" $complianceReport.PackagesCompliant = $false $complianceReport.PackagesDifferences += "Error validating packages: $($_.Exception.Message)" } } # Update overall compliance $complianceReport.IsCompliant = $complianceReport.FeedsCompliant -and $complianceReport.PackagesCompliant if (-not $complianceReport.IsCompliant) { $totalDifferences = $complianceReport.FeedsDifferences.Count + $complianceReport.PackagesDifferences.Count $complianceReport.ComplianceSummary = "System is NOT compliant with snapshot ($totalDifferences difference(s) found)" if ($FailOnNonCompliance) { $errorMessage = $complianceReport.ComplianceSummary if ($complianceReport.FeedsDifferences.Count -gt 0) { $errorMessage += ". Feed differences: " + ($complianceReport.FeedsDifferences -join "; ") } if ($complianceReport.PackagesDifferences.Count -gt 0) { $errorMessage += ". Package differences: " + ($complianceReport.PackagesDifferences -join "; ") } throw $errorMessage } } Write-Information $complianceReport.ComplianceSummary -InformationAction Continue return $complianceReport } catch { $errorMessage = "Failed to validate NIPKG snapshot compliance. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $Snapshot throw } } end { Write-Verbose "Completed Test-Snapshot operation" } } function Install-PackagesFromSystemSnapshot { [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [hashtable]$Snapshot, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [switch]$SkipFeedsInstallation, [Parameter()] [switch]$SkipPackagesInstallation, [Parameter()] [switch]$RemoveExistingFeeds, [Parameter()] [switch]$RemoveSnapshotFeeds, [Parameter()] [switch]$AcceptEulas, [Parameter()] [switch]$Yes, [Parameter()] [switch]$Simulate, [Parameter()] [switch]$ForceLocked, [Parameter()] [switch]$AllowDowngrade, [Parameter()] [switch]$AllowUninstall, [Parameter()] [switch]$InstallAlsoUpgrades, [Parameter()] [switch]$IncludeRecommended, [Parameter()] [switch]$SuppressIncompatibilityErrors, [Parameter()] [scriptblock]$FilterFunction ) $installParams = @{ NipkgCmdPath = $NipkgCmdPath Snapshot = $Snapshot AcceptEulas = $AcceptEulas.IsPresent Simulate = $Simulate.IsPresent Yes = $true ForceLocked = $true AllowDowngrade = $true AllowUninstall = $true InstallAlsoUpgrades = $true IncludeRecommended = $false RemoveExistingFeeds = $RemoveExistingFeeds.IsPresent RemoveSnapshotFeeds = $false SuppressIncompatibilityErrors = $SuppressIncompatibilityErrors.IsPresent FilterFunction = $FilterFunction } Install-Snapshot @installParams Test-Snapshot ` -NipkgCmdPath $NipkgCmdPath ` -Snapshot $Snapshot ` -AllowExtraFeeds $true ` -AllowExtraPackages $true ` -ValidateFeeds $false } function Test-FeedIsLocal { <# .SYNOPSIS Tests whether a feed path represents a local or online feed. .DESCRIPTION Determines if a feed path is local (file system) or online (network) based on the URI scheme or path format. .PARAMETER FeedPath The feed path/URI to test. .NOTES Returns $true for local feeds, $false for online feeds. Local feeds include: - file:// URIs - Windows drive paths (C:\, D:\, etc.) - UNC paths (\\server\share) - Relative paths Online feeds include: - http://, https:// URIs - ftp://, ftps:// URIs - Other network protocols #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$FeedPath ) if ([string]::IsNullOrWhiteSpace($FeedPath)) { return $false } $FeedPath = $FeedPath.Trim() # Check for online protocols $onlineProtocols = @('http://', 'https://', 'ftp://', 'ftps://', 'sftp://') foreach ($protocol in $onlineProtocols) { if ($FeedPath.StartsWith($protocol, [System.StringComparison]::OrdinalIgnoreCase)) { return $false } } # Check for file:// protocol (local) if ($FeedPath.StartsWith('file://', [System.StringComparison]::OrdinalIgnoreCase)) { return $true } # Check for Windows drive letters (C:\, D:\, etc.) if ($FeedPath -match '^[a-zA-Z]:\\') { return $true } # Check for UNC paths (\\server\share) if ($FeedPath.StartsWith('\\')) { return $true } # Check for relative paths or paths without protocol # If it doesn't start with a protocol, assume it's local if (-not ($FeedPath -match '^[a-zA-Z][a-zA-Z0-9+.-]*://')) { return $true } # Default to online for unknown protocols return $false } function Compare-PackageVersions { <# .SYNOPSIS Compares two package version strings. .DESCRIPTION Compares two version strings and returns: - 1 if Version1 is greater than Version2 - 0 if versions are equal - -1 if Version1 is less than Version2 .PARAMETER Version1 First version string to compare. .PARAMETER Version2 Second version string to compare. .EXAMPLE Compare-PackageVersions -Version1 "2.0.0" -Version2 "1.0.0" Returns: 1 (Version1 is greater) .EXAMPLE Compare-PackageVersions -Version1 "1.0.0" -Version2 "1.0.0" Returns: 0 (versions are equal) .NOTES Handles NIPKG version formats including build metadata (e.g., 24.0.0.49212-0+f60). Attempts to parse as System.Version first, then strips build metadata and retries. Falls back to string comparison as last resort. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Version1, [Parameter(Mandatory)] [string]$Version2 ) begin { Write-Verbose "Starting Compare-PackageVersions operation" Write-Verbose "Comparing versions: '$Version1' vs '$Version2'" } process { # Quick equality check first if ($Version1 -eq $Version2) { Write-Verbose "Versions are identical strings" return 0 } try { # Try to parse as System.Version first (works for standard formats) $v1 = [System.Version]::Parse($Version1) $v2 = [System.Version]::Parse($Version2) Write-Verbose "Successfully parsed as System.Version" return $v1.CompareTo($v2) } catch { Write-Verbose "Failed to parse as System.Version: $($_.Exception.Message)" # Strip build metadata (+xxx) and pre-release info (-xxx) for NIPKG versions $cleanVersion1 = $Version1 -replace '\+.*$', '' -replace '-.*$', '' $cleanVersion2 = $Version2 -replace '\+.*$', '' -replace '-.*$', '' Write-Verbose "Cleaned versions: '$cleanVersion1' vs '$cleanVersion2'" try { $v1 = [System.Version]::Parse($cleanVersion1) $v2 = [System.Version]::Parse($cleanVersion2) Write-Verbose "Successfully parsed cleaned versions as System.Version" $result = $v1.CompareTo($v2) # If core versions are equal, compare the original strings to handle pre-release/build info if ($result -eq 0 -and $Version1 -ne $Version2) { Write-Verbose "Core versions equal, comparing full version strings" if ($Version1 -gt $Version2) { return 1 } else { return -1 } } return $result } catch { # Fall back to string comparison if all version parsing fails Write-Verbose "Failed to parse cleaned versions, using string comparison" if ($Version1 -gt $Version2) { return 1 } else { return -1 } } } } end { Write-Verbose "Completed Compare-PackageVersions operation" } } function Get-PackageInstallationOrder { <# .SYNOPSIS Sorts packages into correct installation order based on dependencies. .DESCRIPTION Analyzes package dependencies from NIPKG Packages file data and returns packages in the correct installation order. Ensures that all dependencies are installed with exact versions before dependent packages, preventing NIPKG from selecting random compatible versions during installation. .PARAMETER PackagesInfo Array of package objects from Get-PackagesInfoFromPackagesFile containing package information including dependencies. .PARAMETER IncludeDependencyTypes Array of dependency types to consider for ordering. Valid values are: 'Depends', 'Recommends', 'Suggests', 'Enhances', 'Supplements'. Defaults to @('Depends', 'Recommends') for critical dependencies. .PARAMETER AllowCircularDependencies Allow circular dependencies by breaking cycles at arbitrary points. If false, throws an error when circular dependencies are detected. .EXAMPLE $packages = Get-PackagesInfoFromPackagesFile -InputPath "C:\feeds\Packages" $orderedPackages = Get-PackageInstallationOrder -PackagesInfo $packages -AllowCircularDependencies .EXAMPLE # Include all dependency types for ordering $packages = Get-PackagesInfoFromPackagesFile -InputPath ".\Packages" $orderedPackages = Get-PackageInstallationOrder -PackagesInfo $packages -IncludeDependencyTypes @('Depends', 'Recommends', 'Suggests') .EXAMPLE # Only consider hard dependencies $orderedPackages = Get-PackageInstallationOrder -PackagesInfo $packages -IncludeDependencyTypes @('Depends') .NOTES Returns packages ordered from least dependent (no dependencies) to most dependent. Packages with the same dependency level are sorted alphabetically by package name. The function performs topological sorting on the dependency graph to ensure that all dependencies are installed before their dependents. Dependency strings are parsed to extract package names and version constraints. Only packages present in the input list are considered for ordering. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowEmptyCollection()] [PSCustomObject[]]$PackagesInfo, [Parameter()] [ValidateSet('Depends', 'Recommends', 'Suggests', 'Enhances', 'Supplements')] [string[]]$IncludeDependencyTypes = @('Depends', 'Recommends'), [Parameter()] [switch]$AllowCircularDependencies ) begin { Write-Verbose "Starting Get-PackageInstallationOrder operation" } process { try { if (-not $PackagesInfo -or $PackagesInfo.Count -eq 0) { Write-Warning "No packages provided for ordering" return @() } Write-Information "Analyzing dependencies for $($PackagesInfo.Count) packages..." -InformationAction Continue Write-Verbose "Including dependency types: $($IncludeDependencyTypes -join ', ')" # Create a lookup table for packages by name $packageLookup = @{} foreach ($package in $PackagesInfo) { $packageLookup[$package.Package] = $package } # Parse dependencies for each package $dependencyGraph = @{} $allPackageNames = @($PackagesInfo | ForEach-Object { $_.Package }) foreach ($package in $PackagesInfo) { $packageName = $package.Package $dependencies = @() # Extract dependencies from all specified dependency types foreach ($depType in $IncludeDependencyTypes) { if ($package.PSObject.Properties.Name -contains $depType -and $package.$depType) { $depString = $package.$depType $parsedDeps = Get-ParsedDependencies -DependencyString $depString -AvailablePackages $allPackageNames $dependencies += $parsedDeps } } $dependencyGraph[$packageName] = @{ Package = $package Dependencies = $dependencies | Sort-Object -Unique Visited = $false InStack = $false } Write-Verbose "Package '$packageName' depends on: $($dependencies -join ', ')" } Write-Verbose "Built dependency graph for $($dependencyGraph.Count) packages" # Perform topological sort using DFS $sortedPackages = @() $visitStack = @() foreach ($packageName in $dependencyGraph.Keys) { if (-not $dependencyGraph[$packageName].Visited) { $result = Invoke-TopologicalSort -DependencyGraph $dependencyGraph -PackageName $packageName -SortedList ([ref]$sortedPackages) -VisitStack ([ref]$visitStack) -AllowCircular $AllowCircularDependencies if (-not $result.Success) { if ($AllowCircularDependencies) { Write-Warning "Circular dependency detected: $($result.ErrorMessage). Continuing with partial ordering." } else { throw "Circular dependency detected: $($result.ErrorMessage)" } } } } # Return the packages in installation order (DFS post-order is correct) # Dependencies are added first, then dependents Write-Information "Successfully ordered $($sortedPackages.Count) packages for installation" -InformationAction Continue Write-Verbose "Installation order: $($sortedPackages | ForEach-Object { $_.Package } | Join-String -Separator ', ')" return $sortedPackages } catch { $errorMessage = "Failed to determine package installation order. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $PackagesInfo throw } } end { Write-Verbose "Completed Get-PackageInstallationOrder operation" } } function Get-ParsedDependencies { <# .SYNOPSIS Parses NIPKG dependency strings into package names. .DESCRIPTION Internal helper function that parses NIPKG dependency strings and extracts package names that are available in the provided package list. .PARAMETER DependencyString The dependency string from a NIPKG package (e.g., "pkg1 (>= 1.0), pkg2, pkg3 | pkg4"). .PARAMETER AvailablePackages Array of available package names to filter dependencies against. .NOTES Handles various NIPKG dependency formats: - Simple: "package-name" - Versioned: "package-name (>= 1.0.0)" - Multiple: "pkg1, pkg2, pkg3" - Alternatives: "pkg1 | pkg2 | pkg3" (returns first available) #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$DependencyString, [Parameter(Mandatory)] [string[]]$AvailablePackages ) $dependencies = @() if ([string]::IsNullOrWhiteSpace($DependencyString)) { return $dependencies } # Split by comma for multiple dependencies $depParts = $DependencyString -split ',' foreach ($depPart in $depParts) { $depPart = $depPart.Trim() # Skip empty parts if ([string]::IsNullOrWhiteSpace($depPart)) { continue } # Handle alternative dependencies (pkg1 | pkg2 | pkg3) if ($depPart -match '\|') { $alternatives = $depPart -split '\|' foreach ($alt in $alternatives) { $altTrimmed = $alt.Trim() if ([string]::IsNullOrWhiteSpace($altTrimmed)) { continue } $cleanAlt = Get-CleanPackageName -PackageSpec $altTrimmed if ($cleanAlt -and $cleanAlt -in $AvailablePackages) { $dependencies += $cleanAlt break # Only take the first available alternative } } } else { # Single dependency $cleanDep = Get-CleanPackageName -PackageSpec $depPart if ($cleanDep -and $cleanDep -in $AvailablePackages) { $dependencies += $cleanDep } } } return $dependencies } function Get-CleanPackageName { <# .SYNOPSIS Extracts clean package name from package specification. .DESCRIPTION Internal helper function that removes version constraints and other specifications from package names. .PARAMETER PackageSpec Package specification that may include version constraints. .NOTES Handles formats like: - "package-name" - "package-name (>= 1.0.0)" - "package-name (<< 2.0)" #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$PackageSpec ) if ([string]::IsNullOrWhiteSpace($PackageSpec)) { return $null } # Remove version constraints in parentheses $cleanName = $PackageSpec -replace '\s*\([^)]*\)\s*', '' # Remove any remaining whitespace $cleanName = $cleanName.Trim() # Return null if empty after cleaning if ([string]::IsNullOrWhiteSpace($cleanName)) { return $null } return $cleanName } function Invoke-TopologicalSort { <# .SYNOPSIS Performs depth-first search topological sorting. .DESCRIPTION Internal helper function that performs DFS-based topological sorting to determine dependency order. .PARAMETER DependencyGraph Hashtable representing the dependency graph. .PARAMETER PackageName Current package name being processed. .PARAMETER SortedList Reference to the sorted list being built. .PARAMETER VisitStack Reference to the current visit stack for cycle detection. .PARAMETER AllowCircular Whether to allow circular dependencies. .NOTES Returns a result object with Success boolean and ErrorMessage if applicable. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [hashtable]$DependencyGraph, [Parameter(Mandatory)] [string]$PackageName, [Parameter(Mandatory)] [ref]$SortedList, [Parameter(Mandatory)] [ref]$VisitStack, [Parameter()] [bool]$AllowCircular = $false ) $node = $DependencyGraph[$PackageName] # Check for circular dependency if ($node.InStack) { $cycleStart = $VisitStack.Value.IndexOf($PackageName) $cycle = $VisitStack.Value[$cycleStart..($VisitStack.Value.Count - 1)] + @($PackageName) return @{ Success = $false ErrorMessage = "Circular dependency detected: $($cycle -join ' -> ')" } } # Skip if already visited if ($node.Visited) { return @{ Success = $true } } # Mark as in current path and add to stack $node.InStack = $true $VisitStack.Value += $PackageName # Visit all dependencies first foreach ($depName in $node.Dependencies) { if ($DependencyGraph.ContainsKey($depName)) { $result = Invoke-TopologicalSort -DependencyGraph $DependencyGraph -PackageName $depName -SortedList $SortedList -VisitStack $VisitStack -AllowCircular $AllowCircularDependencies if (-not $result.Success -and -not $AllowCircular) { return $result } } } # Mark as visited and remove from current path $node.Visited = $true $node.InStack = $false $VisitStack.Value = $VisitStack.Value[0..($VisitStack.Value.Count - 2)] # Remove last element # Add to sorted list (will be reversed later) $SortedList.Value += $node.Package return @{ Success = $true } } function Sync-NipkgVersionWithSnapshot { <# .SYNOPSIS Installs packages from the packages-nipkg section in order. .DESCRIPTION Installs all packages from the packages-nipkg section of a snapshot in the correct order. This ensures that NIPKG-related packages and their dependencies are installed properly without version checking - simply installs all packages in the specified order. .PARAMETER Snapshot The snapshot object containing the packages-nipkg section with ordered packages. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .PARAMETER Force This parameter is retained for compatibility but is not used in the current implementation. .OUTPUTS PSCustomObject with: - PackagesInstalled: Number of packages successfully installed - PackagesFailed: Number of packages that failed to install - InstalledPackages: Array of successfully installed package names - FailedPackages: Array of failed package names with error details .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "snapshot.json" $result = Sync-NipkgVersionWithSnapshot -Snapshot $snapshot -NipkgCmdPath "nipkg" Write-Host "Installed $($result.PackagesInstalled) packages successfully" .NOTES This function installs packages from the packages-nipkg section in the order they appear. No version checking is performed - all packages are installed with their specified versions. Requires administrative privileges for package installation. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [PSCustomObject]$Snapshot, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NipkgCmdPath = "nipkg", [Parameter()] [switch]$Force ) begin { Write-Verbose "Starting Sync-NipkgVersionWithSnapshot operation" } process { try { $result = [PSCustomObject]@{ PackagesInstalled = 0 PackagesFailed = 0 InstalledPackages = @() FailedPackages = @() } # Check if the snapshot has a packages-nipkg section if (-not $Snapshot."packages-nipkg" -or $Snapshot."packages-nipkg".Count -eq 0) { Write-Information "No packages-nipkg section found in snapshot or section is empty - no packages to install" -InformationAction Continue return $result } $packagesToInstall = $Snapshot."packages-nipkg" Write-Information "Installing $($packagesToInstall.Count) packages from packages-nipkg section in order..." -InformationAction Continue # Install packages in order foreach ($package in $packagesToInstall) { $packageName = $package.Package $packageVersion = $package.Version $packageSpec = "$packageName=$packageVersion" Write-Information "Installing package: $packageSpec" -InformationAction Continue if ($PSCmdlet.ShouldProcess($packageSpec, "Install NIPKG package")) { try { Write-Verbose "Executing: & $NipkgCmdPath install $packageSpec --accept-eulas -y --allow-downgrade --allow-uninstall --force-locked" & $NipkgCmdPath install $packageSpec --accept-eulas -y --allow-downgrade --allow-uninstall if ($LASTEXITCODE -eq 0) { Write-Information "Successfully installed: $packageSpec" -InformationAction Continue $result.PackagesInstalled++ $result.InstalledPackages += $packageSpec } else { Write-Warning "Failed to install: $packageSpec (exit code: $LASTEXITCODE)" $result.PackagesFailed++ $result.FailedPackages += [PSCustomObject]@{ Package = $packageSpec ExitCode = $LASTEXITCODE Error = "NIPKG install command failed" } } } catch { Write-Warning "Exception installing: $packageSpec - $($_.Exception.Message)" $result.PackagesFailed++ $result.FailedPackages += [PSCustomObject]@{ Package = $packageSpec ExitCode = $null Error = $_.Exception.Message } } } } # Summary Write-Information "Package installation completed:" -InformationAction Continue Write-Information " Successfully installed: $($result.PackagesInstalled) packages" -InformationAction Continue if ($result.PackagesFailed -gt 0) { Write-Information " Failed to install: $($result.PackagesFailed) packages" -InformationAction Continue } return $result } catch { $errorMessage = "Failed to install packages from packages-nipkg section. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation throw } } end { Write-Verbose "Completed Sync-NipkgVersionWithSnapshot operation" } } function Resolve-PackageDependencies { <# .SYNOPSIS Resolves all dependencies for a given package name using provided package information. .DESCRIPTION Takes a package name and a collection of package information (as returned by Get-PackagesInfoFromPackagesFile or similar functions) and recursively resolves all dependencies for the specified package. Only uses the provided package information and does not make external queries. .PARAMETER PackageName The name of the package for which to resolve dependencies. .PARAMETER PackagesInfo Array of package objects containing package information including dependencies. Typically obtained from Get-PackagesInfoFromPackagesFile, Get-PackagesInfoFromNipkgCmd, or similar functions. .PARAMETER IncludeDependencyTypes Array of dependency types to consider for resolution. Valid values are: 'Depends', 'Recommends', 'Suggests', 'Enhances', 'Supplements'. Defaults to @('Depends', 'Recommends') for critical dependencies. .PARAMETER IncludeRootPackage Whether to include the root package in the returned dependency list. Defaults to $true. .PARAMETER AllowCircularDependencies Allow circular dependencies by breaking cycles when detected. If false, throws an error when circular dependencies are detected. Defaults to $true for practical real-world scenarios. .EXAMPLE # Get package info from a Packages file $packages = Get-PackagesInfoFromPackagesFile -InputPath "C:\feeds\Packages" # Resolve all dependencies for a specific package $dependencies = Resolve-PackageDependencies -PackageName "ni-labview-2025" -PackagesInfo $packages .EXAMPLE # Get package info from installer and resolve dependencies $snapshot = New-SnapshotFromInstallerDirectory -InstallerDirectory "C:\Installers\labview-2025" $allPackages = $snapshot.packages | ForEach-Object { # Convert snapshot format back to package info format [PSCustomObject]@{ Package = $_.name Version = $_.version # Note: Dependencies would need to be included in the package info } } $dependencies = Resolve-PackageDependencies -PackageName "my-package" -PackagesInfo $allPackages .EXAMPLE # Only resolve hard dependencies (Depends), exclude root package $dependencies = Resolve-PackageDependencies -PackageName "my-package" -PackagesInfo $packages -IncludeDependencyTypes @('Depends') -IncludeRootPackage $false .EXAMPLE # Strict mode - fail on circular dependencies $dependencies = Resolve-PackageDependencies -PackageName "my-package" -PackagesInfo $packages -AllowCircularDependencies $false .NOTES Returns an array of package objects representing all resolved dependencies. The returned packages are in dependency order (dependencies before dependents). The function performs recursive dependency resolution: 1. Finds the specified package in the provided package info 2. Extracts its dependencies based on the specified dependency types 3. Recursively resolves dependencies of those dependencies 4. Returns all unique dependencies in proper installation order Dependency strings are parsed to extract package names and version constraints. Only packages present in the provided PackagesInfo are considered. If a dependency is not found in PackagesInfo, it is silently skipped with a warning. This allows for partial dependency resolution when not all packages are available in the provided data set. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$PackageName, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [PSCustomObject[]]$PackagesInfo, [Parameter()] [ValidateSet('Depends', 'Recommends', 'Suggests', 'Enhances', 'Supplements')] [string[]]$IncludeDependencyTypes = @('Depends', 'Recommends'), [Parameter()] [bool]$IncludeRootPackage = $true, [Parameter()] [bool]$AllowCircularDependencies = $true ) begin { Write-Verbose "Starting Resolve-PackageDependencies operation" } process { try { if (-not $PackagesInfo -or $PackagesInfo.Count -eq 0) { Write-Warning "No package information provided" return @() } Write-Verbose "Resolving dependencies for package: $PackageName" Write-Verbose "Including dependency types: $($IncludeDependencyTypes -join ', ')" Write-Verbose "Searching in $($PackagesInfo.Count) available packages" # Create a lookup table for packages by name $packageLookup = @{} foreach ($package in $PackagesInfo) { $packageLookup[$package.Package] = $package } # Check if the root package exists if (-not $packageLookup.ContainsKey($PackageName)) { throw "Package '$PackageName' not found in provided package information" } # Initialize tracking collections $resolvedPackages = @{} # Track resolved packages to avoid duplicates $resolutionStack = @() # Track current resolution path for circular dependency detection $allPackageNames = @($PackagesInfo | ForEach-Object { $_.Package }) # Internal function to recursively resolve dependencies function Resolve-PackageRecursive { param ( [string]$CurrentPackageName, [hashtable]$PackageLookup, [hashtable]$ResolvedPackages, [System.Collections.Generic.List[string]]$ResolutionStack, [string[]]$AllPackageNames, [string[]]$DependencyTypes, [bool]$AllowCircular ) Write-Verbose "Resolving dependencies for: $CurrentPackageName" # Check for circular dependency if ($ResolutionStack.Contains($CurrentPackageName)) { $cyclePath = ($ResolutionStack + $CurrentPackageName) -join ' -> ' $errorMessage = "Circular dependency detected: $cyclePath" if ($AllowCircular) { Write-Warning $errorMessage return # Break the cycle by returning early } else { throw $errorMessage } } # Check if already resolved if ($ResolvedPackages.ContainsKey($CurrentPackageName)) { Write-Verbose "Package '$CurrentPackageName' already resolved" return } # Get the package information $currentPackage = $PackageLookup[$CurrentPackageName] if (-not $currentPackage) { Write-Warning "Package '$CurrentPackageName' not found in package information - skipping" return } # Add to resolution stack for circular dependency detection $null = $ResolutionStack.Add($CurrentPackageName) try { # Extract dependencies for this package $dependencies = @() foreach ($depType in $DependencyTypes) { if ($currentPackage.PSObject.Properties.Name -contains $depType -and $currentPackage.$depType) { $depString = $currentPackage.$depType $parsedDeps = Get-ParsedDependencies -DependencyString $depString -AvailablePackages $AllPackageNames $dependencies += $parsedDeps } } # Remove duplicates and sort $dependencies = $dependencies | Sort-Object -Unique Write-Verbose "Package '$CurrentPackageName' has dependencies: $($dependencies -join ', ')" # Recursively resolve each dependency first foreach ($dependency in $dependencies) { if ($PackageLookup.ContainsKey($dependency)) { $null = Resolve-PackageRecursive -CurrentPackageName $dependency -PackageLookup $PackageLookup -ResolvedPackages $ResolvedPackages -ResolutionStack $ResolutionStack -AllPackageNames $allPackageNames -DependencyTypes $DependencyTypes -AllowCircular $AllowCircularDependencies } else { Write-Warning "Dependency '$dependency' of package '$CurrentPackageName' not found in package information - skipping" } } # Add current package to resolved list (after its dependencies) $ResolvedPackages[$CurrentPackageName] = $currentPackage Write-Verbose "Resolved package: $CurrentPackageName" } finally { # Remove from resolution stack $null = $ResolutionStack.Remove($CurrentPackageName) } } # Start recursive resolution $stackList = New-Object System.Collections.Generic.List[string] $null = Resolve-PackageRecursive -CurrentPackageName $PackageName -PackageLookup $packageLookup -ResolvedPackages $resolvedPackages -ResolutionStack $stackList -AllPackageNames $allPackageNames -DependencyTypes $IncludeDependencyTypes -AllowCircular $AllowCircularDependencies # Prepare result - create an ordered list of packages $result = [System.Collections.ArrayList]::new() # Add dependencies in resolution order (dependencies come first) foreach ($packageName in $resolvedPackages.Keys) { if ($IncludeRootPackage -or $packageName -ne $PackageName) { $null = $result.Add($resolvedPackages[$packageName]) } } Write-Information "Successfully resolved $($result.Count) dependencies for package '$PackageName'" -InformationAction Continue Write-Verbose "Resolved packages: $($result | ForEach-Object { $_.Package } | Join-String -Separator ', ')" # Return as array to ensure proper type return $result.ToArray() } catch { $errorMessage = "Failed to resolve dependencies for package '$PackageName'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $PackageName throw } } end { Write-Verbose "Completed Resolve-PackageDependencies operation" } } function Remove-DuplicatePackagesByVersion { <# .SYNOPSIS Removes duplicate packages by selecting the highest version of each package. .DESCRIPTION Takes an array of package objects and removes duplicates based on the Package name, keeping only the package with the highest version for each unique package name. Uses the Compare-PackageVersions function for consistent version comparison logic. .PARAMETER PackagesInfo Array of PSCustomObject containing package information with Package and Version properties. .EXAMPLE $uniquePackages = Remove-DuplicatePackagesByVersion -PackagesInfo $allPackages .NOTES Returns an array of unique packages with the highest version for each package name. Preserves all other properties of the package objects. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [PSCustomObject[]]$PackagesInfo ) begin { Write-Verbose "Starting Remove-DuplicatePackagesByVersion operation" } process { try { if ($PackagesInfo.Count -eq 0) { Write-Verbose "No packages provided, returning empty array" return @() } Write-Verbose "Processing $($PackagesInfo.Count) packages for duplicate removal" # Group packages by name $packageGroups = $PackagesInfo | Group-Object -Property Package $uniquePackages = @() foreach ($group in $packageGroups) { if ($group.Count -eq 1) { # Only one version, add it directly $uniquePackages += $group.Group[0] Write-Verbose "Package '$($group.Name)': single version $($group.Group[0].Version)" } else { # Multiple versions, select the highest Write-Verbose "Package '$($group.Name)': found $($group.Count) versions" $highestVersionPackage = $group.Group[0] foreach ($package in $group.Group[1..($group.Count-1)]) { # Use existing Compare-PackageVersions function for consistency $comparisonResult = Compare-PackageVersions -Version1 $package.Version -Version2 $highestVersionPackage.Version if ($comparisonResult -gt 0) { Write-Verbose " Selected higher version: $($package.Version) > $($highestVersionPackage.Version)" $highestVersionPackage = $package } } $uniquePackages += $highestVersionPackage Write-Verbose "Package '$($group.Name)': selected version $($highestVersionPackage.Version) from $($group.Count) versions" } } Write-Verbose "Removed $($PackagesInfo.Count - $uniquePackages.Count) duplicate packages, returning $($uniquePackages.Count) unique packages" return $uniquePackages } catch { $errorMessage = "Failed to remove duplicate packages: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation throw } } end { Write-Verbose "Completed Remove-DuplicatePackagesByVersion operation" } } function ConvertTo-FeedName { <# .SYNOPSIS Converts a folder name to a valid feed name. .DESCRIPTION Transforms a folder name into a valid feed name by applying naming conventions. Currently replaces dots (.) with underscores (_) to ensure compatibility with feed naming requirements. This function can be extended in the future to apply additional transformations as needed. .PARAMETER FolderName The folder name to convert to a feed name. .EXAMPLE $feedName = ConvertTo-FeedName -FolderName "ni-software-2025.q1" # Returns: "ni-software-2025_q1" .EXAMPLE $feedName = ConvertTo-FeedName -FolderName "drivers.24.8" # Returns: "drivers_24_8" .NOTES Current transformations: - Replaces dots (.) with underscores (_) This function is designed to be easily extensible for future naming convention requirements. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$FolderName ) begin { Write-Verbose "Starting ConvertTo-FeedName operation" } process { try { Write-Verbose "Converting folder name '$FolderName' to feed name" $feedName = $FolderName $feedName = $feedName -replace '\.', '-' $feedName = "local-installer-$feedName" Write-Verbose "Converted '$FolderName' to feed name '$feedName'" return $feedName } catch { $errorMessage = "Failed to convert folder name to feed name - FolderName: '$FolderName'. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation throw } } end { Write-Verbose "Completed ConvertTo-FeedName operation" } } function Get-PackageDefinitionsFromSnapshot { <# .SYNOPSIS Converts snapshot packages section into package definitions for use with Install-Packages. .DESCRIPTION Takes a snapshot object and converts its packages section into an array of package definitions with Package and Version properties that can be used with Install-Packages function for batch installation. .PARAMETER Snapshot Hashtable containing the snapshot data with packages section. .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "snapshot.json" $packageDefinitions = Get-PackageDefinitionsFromSnapshot -Snapshot $snapshot Install-Packages -PackageDefinitions $packageDefinitions -AcceptEulas -Yes .EXAMPLE # Direct pipeline usage $snapshot | Get-PackageDefinitionsFromSnapshot | Install-Packages -AcceptEulas -Yes -AllowDowngrade .NOTES Returns an array of PSCustomObjects with Package and Version properties. This function is designed to be a bridge between snapshot format and Install-Packages function. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [hashtable]$Snapshot ) begin { Write-Verbose "Starting Get-PackageDefinitionsFromSnapshot operation" } process { try { # Validate snapshot structure if (-not $Snapshot.packages) { Write-Warning "Snapshot does not contain a packages section" return @() } if ($Snapshot.packages.Count -eq 0) { Write-Verbose "Snapshot packages section is empty" return @() } Write-Verbose "Converting $($Snapshot.packages.Count) snapshot packages to package definitions" # Convert snapshot packages (name/version) to package definitions (Package/Version) $packageDefinitions = @() foreach ($snapshotPackage in $Snapshot.packages) { # Validate snapshot package structure if (-not $snapshotPackage.Package) { Write-Warning "Snapshot package missing 'name' property, skipping" continue } if (-not $snapshotPackage.Version) { Write-Warning "Snapshot package '$($snapshotPackage.name)' missing 'version' property, skipping" continue } # Create package definition for Install-Packages $packageDef = [PSCustomObject]@{ Package = $snapshotPackage.Package Version = $snapshotPackage.Version Depends = $snapshotPackage.Depends Description = $snapshotPackage.Description Section = $snapshotPackage.Section UserVisible = $snapshotPackage.UserVisible StoreProduct = $snapshotPackage.StoreProduct } $packageDefinitions += $packageDef Write-Verbose "Converted: $($snapshotPackage.Package) v$($snapshotPackage.Version) -> Package: $($packageDef.Package), Version: $($packageDef.Version)" } Write-Verbose "Successfully converted $($packageDefinitions.Count) package definitions" return $packageDefinitions } catch { $errorMessage = "Failed to convert snapshot packages to package definitions. Error: $($_.Exception.Message)" Write-Error -Message $errorMessage -Category InvalidOperation -TargetObject $Snapshot throw } } end { Write-Verbose "Completed Get-PackageDefinitionsFromSnapshot operation" } } function Install-Feeds { <# .SYNOPSIS Adds feeds to the system, avoiding duplicates. .DESCRIPTION This function adds feeds to the system. It checks for existing feeds by comparing both name and URI to avoid duplicates. If a feed with the same name but different URI already exists, it will show a warning but still attempt to add the new feed. After adding feeds, it updates the package database to ensure the latest package information is available. .PARAMETER FeedDefinitions Array of feed objects containing 'name' and 'uri' properties to be added. .PARAMETER NipkgCmdPath Path to the NIPKG command-line tool. Defaults to 'nipkg' if in PATH. .OUTPUTS Returns an array of feeds that were successfully added to the system. .EXAMPLE $feeds = @( @{ name = "my-feed"; uri = "C:\feeds\my-feed" }, @{ name = "online-feed"; uri = "https://example.com/feed" } ) $addedFeeds = Install-Feeds -FeedDefinitions $feeds .EXAMPLE $snapshot = Import-NipkgSnapshotFromJson -InputPath "snapshot.json" $addedFeeds = Install-Feeds -FeedDefinitions $snapshot.feeds -NipkgCmdPath "C:\Program Files\NI\nipkg.exe" #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$FeedDefinition, [Parameter()] [string]$NipkgCmdPath = 'nipkg' ) begin { Write-Verbose "Starting Install-Feeds operation" $script:feedsAdded = @() # Get currently configured feeds for duplicate checking Write-Information "Retrieving currently configured feeds..." -InformationAction Continue try { $script:currentFeeds = Get-FeedDefinitionsFromSystem -NipkgCmdPath $NipkgCmdPath -ExcludeLocalFeeds Write-Verbose "Found $($script:currentFeeds.Count) currently configured feeds" } catch { Write-Warning "Failed to get current feeds list: $($_.Exception.Message)" $script:currentFeeds = @() } # Create lookup table for current feeds (by name and path) $script:currentFeedsLookup = @{} foreach ($currentFeed in $script:currentFeeds) { # Create a key combining name and normalized path for comparison $normalizedPath = $currentFeed.Path -replace '\\', '/' -replace '/$', '' $lookupKey = "$($currentFeed.Name)|$normalizedPath" $script:currentFeedsLookup[$lookupKey] = $currentFeed } } process { # Process the individual feed definition $feed = $FeedDefinition # Validate feed definition if ([string]::IsNullOrWhiteSpace($feed.name)) { Write-Warning "Feed definition has null or empty name - skipping" return } if ([string]::IsNullOrWhiteSpace($feed.uri)) { Write-Warning "Feed definition has null or empty URI - skipping" return } # Normalize the feed URI for comparison $normalizedUri = $feed.uri -replace '\\', '/' -replace '/$', '' $feedLookupKey = "$($feed.name)|$normalizedUri" # Check if this feed already exists with the same name and URI if ($script:currentFeedsLookup.ContainsKey($feedLookupKey)) { Write-Verbose "Feed '$($feed.name)' already exists with URI '$($feed.uri)' - skipping" Write-Information "Skipping feed '$($feed.name)' - already configured" -InformationAction Continue # Still track it as an added feed in case we need to remove it later # $script:feedsAdded += $feed // do not remove feed that was allready present } else { # Check if a feed with the same name but different URI exists $existingFeedWithSameName = $script:currentFeeds | Where-Object { $_.Name -eq $feed.name } if ($existingFeedWithSameName) { Write-Warning "Feed with name '$($feed.name)' already exists but with different URI: '$($existingFeedWithSameName.Path)' vs '$($feed.uri)'" } # Feed doesn't exist - add it try { Write-Verbose "Adding new feed: $($feed.name) -> $($feed.uri)" Write-Information "Adding feed: $($feed.name)" -InformationAction Continue $output = & $NipkgCmdPath feed-add "$($feed.uri)" --name="$($feed.name)" 2>&1 if ($LASTEXITCODE -eq 0) { $script:feedsAdded += $feed Write-Verbose "Successfully added feed: $($feed.name)" } else { Write-Warning "Failed to add feed '$($feed.name)' (exit code: $LASTEXITCODE) - nipkg output: $output" } } catch { Write-Warning "Failed to add feed '$($feed.name)': $($_.Exception.Message)" } } } end { if ($script:feedsAdded.Count -gt 0) { Write-Information "Updating package database to ensure latest package information..." -InformationAction Continue $output = & $NipkgCmdPath update 2>&1 if ($LASTEXITCODE -eq 0) { Write-Information "Package database updated successfully" -InformationAction Continue } else { Write-Warning "Package database update failed (exit code: $LASTEXITCODE)" Write-Warning "This may cause package installation failures due to missing package information" Write-Warning "NIPKG output: $output" } } Write-Verbose "Completed Install-Feeds operation" return $script:feedsAdded } } Export-ModuleMember -Function ` Add-FeedDirectories ` , Test-NipkgAvailability ` , Get-NipkgCommandOutput ` , ConvertFrom-NipkgOutput ` , ConvertFrom-NipkgPackagesFile ` , ConvertTo-PackageSpecifications ` , Get-PackagesInfoFromNipkgCmd ` , Get-PackagesInfoFromPackagesFile ` , Get-PackageDefinitionsFromSnapshot ` , Get-PackageInstallationOrder ` , Get-DriverPackages ` , Get-ProgrammingEnvironmentsPackages ` , Get-UtilitiesPackages ` , Get-ApplicationSoftwarePackages ` , Get-StoreProductPackages ` , Get-FeedDefinitionsFromSystem ` , Install-Feeds ` , Remove-Feeds ` , Install-Packages ` , Install-NipkgManager ` , New-Snapshot ` , New-SnapshotFromInstallerDirectory ` , New-SnapshotFromSystem ` , Export-NipkgSnapshotToJson ` , Import-NipkgSnapshotFromJson ` , Install-Snapshot ` , Install-PackagesFromSystemSnapshot ` , Compare-PackageVersions ` , Sync-NipkgVersionWithSnapshot ` , Test-Snapshot ` , Test-NipkgSnapshotDependencies ` , Resolve-PackageDependencies ` , Remove-DuplicatePackagesByVersion ` , ConvertTo-FeedName |