functions/invoke-d365modulebuild.ps1
|
<# .SYNOPSIS Build a package / module (X++ compile + labels, reports optional) .DESCRIPTION Build a package / module using the builtin "xppc.exe" executable to compile source code and "labelc.exe" to compile label files Specify -IncludeReports to also compile reports using "ReportsC.exe" Prints a per module summary to the console and exits with a terminating error when any module fails .PARAMETER Module The package to build .PARAMETER OutputDir The path to the folder to save assemblies .PARAMETER LogPath Path where you want to store the log outputs generated from the compiler Also used as the path where the log file(s) will be saved When running without the ShowOriginalProgress parameter, the log files will be the standard output and the error output from the underlying tool executed .PARAMETER MetaDataDir The path to the meta data directory for the environment .PARAMETER ReferenceDir The full path of one or more folders containing all assemblies referenced from X++ code Accepts multiple folders, one "-referencefolder" argument is passed to the compiler per folder Default path is the same as the aos service PackagesLocalDirectory .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the aos service PackagesLocalDirectory\bin .PARAMETER IncludeReports Instruct the cmdlet to also compile the reports of the module using "ReportsC.exe" Default is $false which will skip the reports compile .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .PARAMETER OutputCommandOnly Instruct the cmdlet to only output the command that you would have to execute by hand Will include full path to the executable and the needed parameters based on your selection .EXAMPLE PS C:\> Invoke-D365ModuleBuild -Module Essence-Temp This will use the default paths and start the xppc.exe with the needed parameters to compile the Essence-Temp package. When the X++ compile succeeds it will start the labelc.exe to compile the labels. A summary with error and warning counts is written to the console. The default output from all the different steps will be silenced. .EXAMPLE PS C:\> Invoke-D365ModuleBuild -Module Essence-Temp -IncludeReports -ShowOriginalProgress This will compile X++, labels and reports for the Essence-Temp package. The output from the different steps will be written to the console / host. .NOTES Tags: Compile, Model, Servicing, Build, X++ Author: Mötz Jensen (@Splaxi) #> function Invoke-D365ModuleBuild { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Alias("ModuleName")] [string] $Module, [Alias('Output')] [string] $OutputDir = $Script:MetaDataDir, [Alias('LogDir')] [string] $LogPath = $(Join-Path -Path $Script:DefaultTempPath -ChildPath "Logs\ModuleCompile"), [string] $MetaDataDir = $Script:MetaDataDir, [string[]] $ReferenceDir = @($Script:MetaDataDir), [string] $BinDir = $Script:BinDirTools, [switch] $IncludeReports, [switch] $ShowOriginalProgress, [switch] $OutputCommandOnly ) begin { Invoke-TimeSignal -Start $moduleInputs = [System.Collections.Generic.List[string]]::new() $failedModules = [System.Collections.Generic.List[string]]::new() if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) { return } if (-not (Test-PathExists -Path @($ReferenceDir) -Type Container)) { return } if (-not (Test-PathExists -Path $LogPath -Type Container -Create)) { return } $xppcExecutable = Join-Path -Path $BinDir -ChildPath "xppc.exe" $labelExecutable = Join-Path -Path $BinDir -ChildPath "labelc.exe" $reportsExecutable = Join-Path -Path $BinDir -ChildPath "ReportsC.exe" if (-not (Test-PathExists -Path $xppcExecutable, $labelExecutable -Type Leaf)) { return } if ($IncludeReports) { if (-not (Test-PathExists -Path $reportsExecutable -Type Leaf)) { return } } if ([string]::IsNullOrWhiteSpace($OutputDir)) { Stop-PSFFunction -Message "Stopping because OutputDir was either null or empty string. It defaults to the registered MetaDataDir, which is not available on this machine. Please supply -OutputDir explicitly." return } } process { if (Test-PSFFunctionInterrupt) { return } if ([string]::IsNullOrWhiteSpace($Module)) { Write-PSFMessage -Level Warning -Message "Module parameter is empty. Skipping." return } $moduleInputs.Add($Module.Trim()) } end { if (Test-PSFFunctionInterrupt) { return } foreach ($moduleName in $moduleInputs) { $logDirModule = Join-Path -Path $LogPath -ChildPath $moduleName $outputDirModule = Join-Path -Path $OutputDir -ChildPath $moduleName if (-not (Test-PathExists -Path $logDirModule -Type Container -Create)) { continue } $logFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.xppc.log" $logXmlFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.xppc.xml" $labelLogFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.labelc.log" $labelErrorFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.labelc.err" $reportsLogFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.ReportsC.log" $reportsXmlLogFile = Join-Path -Path $logDirModule -ChildPath "Dynamics.AX.$moduleName.ReportsC.xml" if ($OutputCommandOnly) { $referenceParams = @($ReferenceDir | ForEach-Object { "-referencefolder=`"$_`"" }) $xppcParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-output=`"$outputDirModule\bin`"") + $referenceParams + @( "-log=`"$logFile`"", "-xmlLog=`"$logXmlFile`"", "-verbose" ) Invoke-BuildTool -Executable $xppcExecutable -Params $xppcParams -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly $labelParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-output=`"$outputDirModule\Resources`"", "-outlog=`"$labelLogFile`"", "-errlog=`"$labelErrorFile`"" ) Invoke-BuildTool -Executable $labelExecutable -Params $labelParams -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly if ($IncludeReports) { $reportsParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-LabelsPath=`"$MetaDataDir`"", "-output=`"$outputDirModule\Reports`"", "-log=`"$reportsLogFile`"", "-xmlLog=`"$reportsXmlLogFile`"" ) Invoke-BuildTool -Executable $reportsExecutable -Params $reportsParams -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly } continue } # Remove stale logs from previous runs, so a successful parse always reflects this run foreach ($staleLog in @($logFile, $logXmlFile, $labelLogFile, $labelErrorFile)) { if (Test-Path -LiteralPath $staleLog -PathType Leaf) { Remove-Item -LiteralPath $staleLog -Force -ErrorAction SilentlyContinue } } if ($IncludeReports) { foreach ($staleLog in @($reportsLogFile, $reportsXmlLogFile)) { if (Test-Path -LiteralPath $staleLog -PathType Leaf) { Remove-Item -LiteralPath $staleLog -Force -ErrorAction SilentlyContinue } } } # Stage 1: X++ compile $referenceParams = @($ReferenceDir | ForEach-Object { "-referencefolder=`"$_`"" }) $xppcParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-output=`"$outputDirModule\bin`"") + $referenceParams + @( "-log=`"$logFile`"", "-xmlLog=`"$logXmlFile`"", "-verbose" ) $xppcExitCode = Invoke-BuildTool -Executable $xppcExecutable -Params $xppcParams -ShowOriginalProgress:$ShowOriginalProgress $xppcFailed = $xppcExitCode -ne 0 $errors = @() $warnings = @() $logFailure = $null if (-not (Test-Path -LiteralPath $logXmlFile -PathType Leaf)) { if (-not $xppcFailed) { $logFailure = "The compiler did not create its XML log: [$logXmlFile]" } } else { try { $xml = [xml](Get-Content -LiteralPath $logXmlFile -Raw -ErrorAction Stop) $diagnostics = @($xml.SelectNodes('//Diagnostic')) $errors = @($diagnostics | Where-Object { $_.Severity -eq 'Error' }) $warnings = @($diagnostics | Where-Object { $_.Severity -eq 'Warning' }) } catch { $logFailure = "The compiler XML log is invalid: [$logXmlFile]" } } if ($null -eq $logFailure -and (Test-Path -LiteralPath $logFile -PathType Leaf) -and ($errors.Count -eq 0) -and ($xppcExitCode -eq 0)) { # Fall back to the text log totals when the XML log carries no diagnostics $textResult = Get-CompilerResult -Path $logFile if ($null -ne $textResult -and $textResult.Errors -gt 0) { $errors = @($textResult.Errors) } } $errorCount = $errors.Count if ($errors.Count -eq 1 -and ($errors[0] -is [int])) { $errorCount = $errors[0] } $warningCount = $warnings.Count if ($warnings.Count -eq 1 -and ($warnings[0] -is [int])) { $warningCount = $warnings[0] } # Stage 2: labels, only when the X++ compile succeeded $labelExitCode = $null $labelFailure = $null $labelsCompiled = $false $xppcOk = (-not $xppcFailed) -and ($null -eq $logFailure) -and ($errorCount -eq 0) -and ($xppcExitCode -eq 0) if ($xppcOk) { $labelParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-output=`"$outputDirModule\Resources`"", "-outlog=`"$labelLogFile`"", "-errlog=`"$labelErrorFile`"" ) $labelExitCode = Invoke-BuildTool -Executable $labelExecutable -Params $labelParams -ShowOriginalProgress:$ShowOriginalProgress $labelsCompiled = $true if ($labelExitCode -ne 0) { $labelFailure = "[$moduleName] label compilation failed with exit code $labelExitCode. Review the label log shown below." } } # Stage 3: reports, only when -IncludeReports was specified and everything before succeeded $reportsExitCode = $null $reportsFailure = $null $reportsCompiled = $false $reportsSkipped = (-not $IncludeReports) if ($IncludeReports -and $xppcOk -and ($null -eq $labelFailure)) { $reportsParams = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$moduleName`"", "-LabelsPath=`"$MetaDataDir`"", "-output=`"$outputDirModule\Reports`"", "-log=`"$reportsLogFile`"", "-xmlLog=`"$reportsXmlLogFile`"" ) $reportsExitCode = Invoke-BuildTool -Executable $reportsExecutable -Params $reportsParams -ShowOriginalProgress:$ShowOriginalProgress $reportsCompiled = $true if ($reportsExitCode -ne 0) { $reportsFailure = "[$moduleName] reports compilation failed with exit code $reportsExitCode. Review the reports log shown below." } } $failed = $xppcFailed -or ($null -ne $logFailure) -or ($errorCount -gt 0) -or ($null -ne $labelFailure) -or ($null -ne $reportsFailure) if ((-not $xppcOk) -and (-not $failed)) { # X++ stage did not succeed, but no explicit failure was recorded (e.g. live output mode with unparsable logs) $failed = $true } $displayErrorCount = "$errorCount" $displayWarningCount = "$warningCount" if ($null -ne $logFailure) { $displayErrorCount = 'Unavailable' $displayWarningCount = 'Unavailable' } $summary = @( "Model: $moduleName" "Errors: $displayErrorCount" "Warnings: $displayWarningCount" "Log: $logFile" "XML log: $logXmlFile" ) if ($labelsCompiled) { $summary += @( "Labels: exit code $labelExitCode" "Label log: $labelLogFile" ) } else { $summary += "Labels: skipped (X++ compile did not succeed)" } if ($reportsCompiled) { $summary += @( "Reports: exit code $reportsExitCode" "Reports log: $reportsLogFile" ) } elseif ($reportsSkipped) { $summary += "Reports: skipped (-IncludeReports not specified)" } else { $summary += "Reports: skipped (previous stage did not succeed)" } Write-PSFMessage -Level Host -Message ($summary -join [Environment]::NewLine) if ($logFailure) { Write-PSFHostColor -String $logFailure } if ($failed -and (Test-Path -LiteralPath $logFile -PathType Leaf)) { $errorLines = @(Select-String -LiteralPath $logFile -Pattern "error", "warning" -SimpleMatch -CaseSensitive:$false -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Line -First 25) foreach ($errorLine in $errorLines) { Write-PSFHostColor -String $errorLine } } if ($labelFailure) { Write-PSFHostColor -String $labelFailure if (Test-Path -LiteralPath $labelErrorFile -PathType Leaf) { $labelErrorLines = @(Get-Content -LiteralPath $labelErrorFile -ErrorAction SilentlyContinue | Select-Object -First 25) foreach ($labelErrorLine in $labelErrorLines) { Write-PSFHostColor -String $labelErrorLine } } } if ($reportsFailure) { Write-PSFHostColor -String $reportsFailure } if ($failed) { $failedModules.Add($moduleName) | Out-Null Stop-PSFFunction -Message "[$moduleName] build failed. Review the compiler logs shown above." -Category InvalidResult -Continue continue } [PSCustomObject]@{ Module = $moduleName Success = $true XppcExitCode = $xppcExitCode LabelExitCode = $labelExitCode ReportsExitCode = $reportsExitCode Errors = $errorCount Warnings = $warningCount LogFile = $logFile XmlLogFile = $logXmlFile LabelOutLogFile = $labelLogFile LabelErrorFile = $labelErrorFile ReportsLogFile = $(if ($reportsCompiled) { $reportsLogFile } else { $null }) PSTypeName = 'D365FO.TOOLS.ModuleBuildOutput' } } if ($failedModules.Count -gt 0) { $names = $failedModules -join ', ' $message = "D365 module build failed for: $names" $global:LASTEXITCODE = 1 $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new($message), 'D365ModuleBuildFailed', [System.Management.Automation.ErrorCategory]::InvalidResult, $names ) $PSCmdlet.ThrowTerminatingError($errorRecord) } if ($failedModules.Count -eq 0 -and $moduleInputs.Count -gt 0 -and -not $OutputCommandOnly) { $global:LASTEXITCODE = 0 } Invoke-TimeSignal -End } } |