Private/Invoke-M365AppPackageBuild.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Downloads setup.exe for Microsoft 365 Apps via Evergreen, updates the configuration XML, and builds an .intunewin package. .DESCRIPTION Uses Get-EvergreenApp -Name Microsoft365Apps to retrieve the latest setup.exe for the specified channel. Copies the configuration XML (renamed to Install-Microsoft365Apps.xml) and Uninstall-Microsoft365Apps.xml to a working source folder, updates the XML with the caller-supplied Channel, TenantId, and CompanyName, then calls New-IntuneWin32AppPackage to produce the .intunewin file. .PARAMETER ConfigRow A configuration row object returned by Get-M365AppConfigurations, containing FileName, FilePath, DisplayName, and ConfigId. .PARAMETER ConfigDirectoryPath Path to the directory containing the M365 XML configuration files (must include Uninstall-Microsoft365Apps.xml for the uninstall command to function). .PARAMETER Channel The update channel to set in the XML (e.g. MonthlyEnterprise, Current). .PARAMETER CompanyName The company name written to the AppSettings/Setup element in the XML. .PARAMETER TenantId The Entra ID tenant ID written to the TenantId Property element in the XML. .PARAMETER WorkingPath Root working directory. A subdirectory named after the package is created here. .PARAMETER SyncHash Synchronized hashtable used by Write-UILog for thread-safe UI log output. .OUTPUTS PSCustomObject with properties: Succeeded, IntuneWinPath, SetupFileUsed, Version, SourcePath, OutputPath, Error. #> function Invoke-M365AppPackageBuild { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [PSCustomObject]$ConfigRow, [Parameter(Mandatory)] [string]$ConfigDirectoryPath, [Parameter(Mandatory)] [string]$Channel, [Parameter(Mandatory)] [string]$CompanyName, [Parameter(Mandatory)] [string]$TenantId, [Parameter(Mandatory)] [string]$WorkingPath, [Parameter()] [string]$AppJsonTemplatePath = '', [Parameter(Mandatory)] [System.Collections.Hashtable]$SyncHash ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $result = [PSCustomObject]@{ Succeeded = $false IntuneWinPath = '' SetupFileUsed = 'setup.exe' Version = '' SourcePath = '' OutputPath = '' AppJsonPath = '' Error = '' } try { # Resolve latest Evergreen entry for the requested channel Write-UILog -SyncHash $SyncHash -Message "M365: Querying Evergreen for Microsoft365Apps channel '$Channel'..." -Level Info $evergreenRows = Get-EvergreenApp -Name 'Microsoft365Apps' -ErrorAction Stop $channelRow = $evergreenRows | Where-Object { $_.Channel -eq $Channel } | Sort-Object -Property { [System.Version]$_.Version } -Descending | Select-Object -First 1 if ($null -eq $channelRow) { throw "No Evergreen entry found for Microsoft365Apps channel '$Channel'." } $version = [string]$channelRow.Version Write-UILog -SyncHash $SyncHash -Message "M365: Latest version for '$Channel': $version" -Level Info # Sanitise display name for use as a folder name $safeName = ($ConfigRow.DisplayName -replace '[\\/:*?"<>|]', '_').Trim('_') $sourcePath = Join-Path -Path $WorkingPath -ChildPath "$safeName\Source" $outputPath = Join-Path -Path $WorkingPath -ChildPath "$safeName\Output" foreach ($dir in @($sourcePath, $outputPath)) { if (-not (Test-Path -LiteralPath $dir -PathType Container)) { $null = New-Item -Path $dir -ItemType Directory -Force -ErrorAction Stop } } $result.SourcePath = $sourcePath $result.OutputPath = $outputPath # Download setup.exe via Evergreen Write-UILog -SyncHash $SyncHash -Message 'M365: Downloading setup.exe via Evergreen...' -Level Info $null = $channelRow | Save-EvergreenApp -CustomPath $sourcePath -ErrorAction Stop # Verify setup.exe was downloaded $setupFile = Join-Path -Path $sourcePath -ChildPath 'setup.exe' if (-not (Test-Path -LiteralPath $setupFile -PathType Leaf)) { throw "setup.exe was not found in '$sourcePath' after download." } Write-UILog -SyncHash $SyncHash -Message "M365: setup.exe downloaded to '$sourcePath'." -Level Info # Copy and rename the install config XML $installXmlDest = Join-Path -Path $sourcePath -ChildPath 'Install-Microsoft365Apps.xml' Copy-Item -LiteralPath $ConfigRow.FilePath -Destination $installXmlDest -Force -ErrorAction Stop # Copy the uninstall XML if present $uninstallSrc = Join-Path -Path $ConfigDirectoryPath -ChildPath 'Uninstall-Microsoft365Apps.xml' if (Test-Path -LiteralPath $uninstallSrc -PathType Leaf) { $uninstallDest = Join-Path -Path $sourcePath -ChildPath 'Uninstall-Microsoft365Apps.xml' Copy-Item -LiteralPath $uninstallSrc -Destination $uninstallDest -Force -ErrorAction Stop } # Update Install-Microsoft365Apps.xml with caller-supplied values Write-UILog -SyncHash $SyncHash -Message 'M365: Updating configuration XML with channel, tenant ID, and company name...' -Level Info [xml]$xml = Get-Content -LiteralPath $installXmlDest -Raw -ErrorAction Stop # Channel $xml.Configuration.Add.Channel = $Channel # TenantId Property $tenantProp = $xml.Configuration.Property | Where-Object { $_.Name -eq 'TenantId' } | Select-Object -First 1 if ($null -ne $tenantProp) { $tenantProp.Value = $TenantId } # CompanyName via AppSettings/Setup if ($null -ne $xml.Configuration.AppSettings -and $null -ne $xml.Configuration.AppSettings.Setup) { $xml.Configuration.AppSettings.Setup.Value = $CompanyName } $xml.Save($installXmlDest) Write-UILog -SyncHash $SyncHash -Message 'M365: Configuration XML updated.' -Level Info # Build .intunewin package Write-UILog -SyncHash $SyncHash -Message 'M365: Building .intunewin package...' -Level Info $null = New-IntuneWin32AppPackage ` -SourceFolder $sourcePath ` -SetupFile 'setup.exe' ` -OutputFolder $outputPath ` -Force ` -ErrorAction Stop # Locate the produced .intunewin file $intuneWinFile = Get-ChildItem -LiteralPath $outputPath -Filter '*.intunewin' -File | Select-Object -First 1 if ($null -eq $intuneWinFile) { throw "No .intunewin file was found in '$outputPath' after packaging." } Write-UILog -SyncHash $SyncHash -Message "M365: Package built: $($intuneWinFile.Name)" -Level Info $result.IntuneWinPath = $intuneWinFile.FullName $result.Version = $version # Copy and update App.json from the template if (-not [string]::IsNullOrWhiteSpace($AppJsonTemplatePath) -and (Test-Path -LiteralPath $AppJsonTemplatePath -PathType Leaf)) { Write-UILog -SyncHash $SyncHash -Message 'M365: Writing App.json from template...' -Level Info $appJsonDest = Join-Path -Path $sourcePath -ChildPath 'App.json' Copy-Item -LiteralPath $AppJsonTemplatePath -Destination $appJsonDest -Force -ErrorAction Stop $appJson = Get-Content -LiteralPath $appJsonDest -Raw -ErrorAction Stop | ConvertFrom-Json # Package information $appJson.PackageInformation.Version = $version # App information $appJson.Information.DisplayName = $ConfigRow.DisplayName $appJson.Information.PSPackageFactoryGuid = $ConfigRow.ConfigId # Program commands $appJson.Program.InstallCommand = 'setup.exe /configure Install-Microsoft365Apps.xml' $appJson.Program.UninstallCommand = 'setup.exe /configure Uninstall-Microsoft365Apps.xml' # Requirement rule architecture $appJson.RequirementRule.Architecture = $ConfigRow.Architecture # Detection rules — normalise product IDs (strip spaces around commas) $normalizedProducts = (([string]$ConfigRow.Products) -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join ',' $sclValue = if ([bool]$ConfigRow.IsVdi) { '1' } else { '0' } foreach ($rule in $appJson.DetectionRule) { switch ([string]$rule.ValueName) { 'VersionToReport' { $rule.Value = $version } 'ProductReleaseIds' { $rule.Value = $normalizedProducts } 'SharedComputerLicensing' { $rule.Value = $sclValue } } } $appJson | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $appJsonDest -Encoding UTF8 -ErrorAction Stop $result.AppJsonPath = $appJsonDest Write-UILog -SyncHash $SyncHash -Message "M365: App.json written to '$appJsonDest'." -Level Info } $result.Succeeded = $true } catch { $result.Error = $_.Exception.Message Write-UILog -SyncHash $SyncHash -Message "M365: Package build failed: $($_.Exception.Message)" -Level Error } return $result } |