Private/Invoke-IntunePackageBuild.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Downloads the latest installer for an App.json definition and creates the .intunewin package using IntuneWin32App's New-IntuneWin32AppPackage. .DESCRIPTION Given a comparison row (containing the DefinitionPath and DefinitionObject) plus a working path, this function: 1. Resolves the latest installer version via Get-IntunePackageLatestVersion 2. Creates working sub-directories under WorkingPath 3. Copies the Source folder from the definition directory if it exists 4. Downloads the installer with Save-EvergreenApp 5. Calls New-IntuneWin32AppPackage to produce the .intunewin file New-IntuneWin32AppPackage requires no remote authentication; it wraps IntuneWinAppUtil.exe locally. The IntuneWin32App module must already be imported in the calling session. All progress messages are written via Write-UILog; pass $SyncHash so messages appear in the UI log panel. .PARAMETER ComparisonRow A comparison row object that must contain DefinitionPath (full path to App.json) and DefinitionObject (the parsed App.json PSCustomObject). .PARAMETER WorkingPath Root working path under which per-app sub-directories are created. .PARAMETER SyncHash The shared UI synchronised hashtable used by Write-UILog. .OUTPUTS PSCustomObject with: Succeeded : bool IntuneWinPath : string - full path to the created .intunewin file SetupFileUsed : string - setup file name passed to New-IntuneWin32AppPackage DownloadedVersion : string - version string of the downloaded artifact ResolvedArtifact : object - the raw artifact row returned by Evergreen AppFolderName : string - sanitized folder name used under WorkingPath SourcePath : string - path to the download/staging folder OutputPath : string - path to the .intunewin output folder Error : string - populated on failure #> function Invoke-IntunePackageBuild { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [PSCustomObject]$ComparisonRow, [Parameter(Mandatory)] [string]$WorkingPath, [Parameter(Mandatory)] [System.Collections.Hashtable]$SyncHash ) $fail = { param([string]$Msg) return [PSCustomObject]@{ Succeeded = $false IntuneWinPath = '' SetupFileUsed = '' DownloadedVersion = '' ResolvedArtifact = $null AppFolderName = '' SourcePath = '' OutputPath = '' Error = $Msg } } # Resolve definition path and object $definitionPath = [string]$ComparisonRow.DefinitionPath $definitionObject = $ComparisonRow.DefinitionObject if ($null -eq $definitionObject -and -not [string]::IsNullOrWhiteSpace($definitionPath) -and (Test-Path -LiteralPath $definitionPath -PathType Leaf)) { try { $definitionObject = Get-Content -LiteralPath $definitionPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { return (& $fail "Failed to load definition from '$definitionPath': $($_.Exception.Message)") } } if ($null -eq $definitionObject) { return (& $fail 'No definition object available and no valid definition path provided.') } # Resolve latest version Write-UILog -Message "Resolving latest version from: $([string]$definitionObject.Application.Filter)" -Level Cmd -SyncHash $SyncHash $latestResult = Get-IntunePackageLatestVersion -DefinitionObject $definitionObject if (-not $latestResult.Succeeded) { return (& $fail "Version resolution failed: $($latestResult.Error)") } Write-UILog -Message "Resolved version: $($latestResult.Version) - $($latestResult.URI)" -Level Info -SyncHash $SyncHash # Build a safe folder name from the definition file path $appFolderName = '' if (-not [string]::IsNullOrWhiteSpace($definitionPath)) { $appFolderName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName($definitionPath)) } if ([string]::IsNullOrWhiteSpace($appFolderName)) { $appFolderName = [string]$definitionObject.Application.Name } $appFolderName = [System.Text.RegularExpressions.Regex]::Replace($appFolderName, '[^\w\-\.]', '_').Trim('_') if ([string]::IsNullOrWhiteSpace($appFolderName)) { $appFolderName = 'IntunePackage' } $sourcePath = Join-Path -Path $WorkingPath -ChildPath "$appFolderName\Source" $outputPath = Join-Path -Path $WorkingPath -ChildPath "$appFolderName\Output" # Create working directories foreach ($dir in @($sourcePath, $outputPath)) { if (-not (Test-Path -LiteralPath $dir)) { $null = New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop } } # Remove any stale .intunewin files from the output folder Get-ChildItem -LiteralPath $outputPath -Filter '*.intunewin' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue # Copy Source folder content from the definition directory if it exists if (-not [string]::IsNullOrWhiteSpace($definitionPath)) { $definitionDir = [System.IO.Path]::GetDirectoryName($definitionPath) $definitionSrcDir = Join-Path -Path $definitionDir -ChildPath 'Source' if (Test-Path -LiteralPath $definitionSrcDir -PathType Container) { Write-UILog -Message "Copying source content from '$definitionSrcDir'..." -Level Info -SyncHash $SyncHash $isPSADT = Test-Path -LiteralPath (Join-Path -Path $definitionSrcDir -ChildPath 'Invoke-AppDeployToolkit.ps1') if ($isPSADT) { # For PSADT packages, exclude the deploy script until the installer is in place $null = Copy-Item -Path "$definitionSrcDir\*" -Destination $sourcePath -Recurse -Force ` -Exclude 'Invoke-AppDeployToolkit.ps1' -ErrorAction Stop } else { $null = Copy-Item -Path "$definitionSrcDir\*" -Destination $sourcePath -Recurse -Force -ErrorAction Stop } } } # Download the latest installer $artifact = $latestResult.ResolvedArtifact if (-not [string]::IsNullOrWhiteSpace($latestResult.URI)) { Write-UILog -Message "Downloading installer to '$sourcePath'..." -Level Info -SyncHash $SyncHash Write-UILog -Message "Save-EvergreenApp -LiteralPath '$sourcePath'" -Level Cmd -SyncHash $SyncHash try { $downloadResults = @($artifact | Save-EvergreenApp -LiteralPath $sourcePath -ErrorAction Stop) if ($downloadResults.Count -eq 0) { return (& $fail 'Save-EvergreenApp completed but returned no results.') } Write-UILog -Message "Downloaded: $($downloadResults[0].FullName)" -Level Info -SyncHash $SyncHash } catch { return (& $fail "Download failed: $($_.Exception.Message)") } } else { Write-UILog -Message 'Artifact has no URI; skipping download (expected pre-staged source).' -Level Warning -SyncHash $SyncHash } # Determine setup file and whether PSADT is involved $setupFile = [string]$definitionObject.PackageInformation.SetupFile if ([string]::IsNullOrWhiteSpace($setupFile)) { # Fall back to SetupType default file names $setupType = [string]$definitionObject.PackageInformation.SetupType $setupFile = switch ($setupType.ToUpper()) { 'MSI' { 'Setup.msi' } 'MSIX' { 'Setup.msix' } default { 'Setup.exe' } } Write-UILog -Message "SetupFile not specified; defaulting to '$setupFile'." -Level Warning -SyncHash $SyncHash } # If PSADT deploy script now exists in source, adjust setup file name if (Test-Path -LiteralPath (Join-Path -Path $sourcePath -ChildPath 'Invoke-AppDeployToolkit.ps1')) { $setupFile = 'Deploy-Application.exe' } # Verify IntuneWin32App module is available for packaging if (-not (Get-Command -Name 'New-IntuneWin32AppPackage' -ErrorAction SilentlyContinue)) { return (& $fail 'New-IntuneWin32AppPackage is not available. Ensure IntuneWin32App module is loaded.') } Write-UILog -Message "Creating .intunewin: source='$sourcePath' setup='$setupFile' output='$outputPath'" -Level Info -SyncHash $SyncHash Write-UILog -Message "New-IntuneWin32AppPackage -SourceFolder `"$sourcePath`" -SetupFile `"$setupFile`" -OutputFolder `"$outputPath`"" -Level Cmd -SyncHash $SyncHash $intuneWinPath = '' try { $packageResult = New-IntuneWin32AppPackage -SourceFolder $sourcePath -SetupFile $setupFile ` -OutputFolder $outputPath -Force -ErrorAction Stop # Different module versions return either: # - a plain string path # - an object with .Path # - an object where .Path can itself be FileInfo-like if ($null -ne $packageResult) { if ($packageResult -is [string]) { $candidatePath = [string]$packageResult if (-not [string]::IsNullOrWhiteSpace($candidatePath)) { $intuneWinPath = $candidatePath } } elseif ($packageResult.PSObject.Properties.Name -contains 'Path' -and $null -ne $packageResult.Path) { $candidatePath = [string]$packageResult.Path if (-not [string]::IsNullOrWhiteSpace($candidatePath)) { $intuneWinPath = $candidatePath } } } if ([string]::IsNullOrWhiteSpace($intuneWinPath)) { $intuneWinFiles = @(Get-ChildItem -LiteralPath $outputPath -Filter '*.intunewin' -ErrorAction SilentlyContinue) if ($intuneWinFiles.Count -eq 0) { return (& $fail 'New-IntuneWin32AppPackage completed but no .intunewin file was found in the output folder.') } # Pick the newest package if multiple files exist. $intuneWinPath = @($intuneWinFiles | Sort-Object -Property LastWriteTimeUtc -Descending)[0].FullName } } catch { return (& $fail "Packaging failed: $($_.Exception.Message)") } if (-not (Test-Path -LiteralPath $intuneWinPath -PathType Leaf)) { return (& $fail "Expected .intunewin file not found at: $intuneWinPath") } Write-UILog -Message "Package created successfully: $intuneWinPath" -Level Info -SyncHash $SyncHash return [PSCustomObject]@{ Succeeded = $true IntuneWinPath = $intuneWinPath SetupFileUsed = $setupFile DownloadedVersion = $latestResult.Version ResolvedArtifact = $latestResult.ResolvedArtifact AppFolderName = $appFolderName SourcePath = $sourcePath OutputPath = $outputPath Error = '' } } |