Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.

---

## [0.6.0] - 2026-04-01

- `.output` is now always populated in the result object -- previously it was
`$null` when `-ShowOutput` was used; output is now captured and streamed
- `.exeOutputDir` and `.dcuOutputDir` are now resolved from the dcc32 compiler
invocation in the build output when not supplied as parameters
- `.warnings` and `.errors` integer counts added to the result object, parsed
from the MSBuild summary line
- Fix: duplicate `/p:DCC_UnitSearchPath` argument when `-UnitSearchPath` was
supplied (the unquoted copy was appended first, then the quoted copy)

## [0.5.0] - 2026-03-19

- `-Define` parameter broken - MSBuild thinks it's a switch
Expand Down
87 changes: 69 additions & 18 deletions source/delphi-msbuild.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ NOTES
-Config is the RAD Studio MSBuild property name (/p:Config); common values
are Debug and Release.

By default MSBuild output is captured and returned in the result object's
.output property. Use -ShowOutput to stream output to stdout in real time;
in that case .output is null and errors are written via Write-Error.
MSBuild output is always captured and returned in the result object's
.output property. Use -ShowOutput to also stream output to stdout in real
time; .output is populated in both cases.

Exit codes:
0 success
Expand All @@ -53,6 +53,8 @@ NOTES
Justification='Script accepts at most one piped installation object; end-block semantics are correct.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Get-RsvarsEnvLines',
Justification='Function returns multiple KEY=VALUE lines from cmd.exe set; plural noun is accurate.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
Justification='Write-Host is intentional: -ShowOutput streams build text directly to the console host.')]
param(
[Parameter(ValueFromPipeline=$true)]
[psobject]$DelphiInstallation,
Expand Down Expand Up @@ -100,7 +102,7 @@ $ExitRootDirError = 3
$ExitProjectNotFound = 4
$ExitBuildFailed = 5

$script:Version = '0.5.0'
$script:Version = '0.6.0'

# Resolve the Delphi root dir from the explicit -RootDir parameter or from a
# piped delphi-inspect result object (.rootDir property).
Expand Down Expand Up @@ -163,21 +165,18 @@ function Invoke-RsvarsEnvironment {
}

# Invoke msbuild.exe with the given arguments.
# Returns [pscustomobject]@{ ExitCode; Output } where Output is $null when
# -ShowOutput is set (output streams to stdout instead of being captured).
# Returns [pscustomobject]@{ ExitCode; Output } where Output is always the
# captured build text. When -ShowOutput is set the text is also written to
# the host so the caller sees it in real time.
# Separated into its own function so tests can mock it.
function Invoke-MsbuildExe {
param(
[string[]]$Arguments,
[switch]$ShowOutput
)

if ($ShowOutput) {
& msbuild.exe @Arguments | Out-Host
return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $null }
}

$output = & msbuild.exe @Arguments 2>&1 | Out-String
if ($ShowOutput) { Write-Host $output }
return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $output }
}

Expand Down Expand Up @@ -208,11 +207,6 @@ function Invoke-MsbuildProject {
if (-not [string]::IsNullOrWhiteSpace($ExeOutputDir)) { $msbuildArgs += "/p:DCC_ExeOutput=$ExeOutputDir" }
if (-not [string]::IsNullOrWhiteSpace($DcuOutputDir)) { $msbuildArgs += "/p:DCC_DcuOutput=$DcuOutputDir" }

if ($UnitSearchPath.Count -gt 0) {
$unitSearchValue = '$(DCC_UnitSearchPath);' + ($UnitSearchPath -join ';')
$msbuildArgs += "/p:DCC_UnitSearchPath=$unitSearchValue"
}

if ($UnitSearchPath.Count -gt 0) {
$unitSearchValue = '$(DCC_UnitSearchPath);' + ($UnitSearchPath -join ';')
$msbuildArgs += "/p:DCC_UnitSearchPath=`"$unitSearchValue`""
Expand All @@ -226,6 +220,56 @@ function Invoke-MsbuildProject {
return Invoke-MsbuildExe -Arguments $msbuildArgs -ShowOutput:$ShowOutput
}

# Parse the dcc32.exe invocation line from captured msbuild output and extract
# the exe output dir (-E flag) and DCU output dir (-NO flag).
# Paths are resolved to absolute using the project file directory as base.
# Returns [pscustomobject]@{ ExeOutputDir; DcuOutputDir } -- either may be $null.
function Get-BuildOutputDir {
param(
[string]$Output,
[string]$ProjectFileDir
)

$result = [pscustomobject]@{ ExeOutputDir = $null; DcuOutputDir = $null }
if ([string]::IsNullOrWhiteSpace($Output)) { return $result }

$dcc32Line = ($Output -split "`n") |
Where-Object { $_ -match '[/\\]dcc32\.exe\s' } |
Select-Object -First 1
if (-not $dcc32Line) { return $result }

if ($dcc32Line -match '\s-E(\S+)') {
$result.ExeOutputDir = [System.IO.Path]::GetFullPath(
[System.IO.Path]::Combine($ProjectFileDir, $Matches[1]))
}

if ($dcc32Line -match '\s-NO(\S+)') {
$result.DcuOutputDir = [System.IO.Path]::GetFullPath(
[System.IO.Path]::Combine($ProjectFileDir, $Matches[1]))
}

return $result
}

# Parse the MSBuild summary block from captured output and return warning and
# error counts as integers.
# Returns [pscustomobject]@{ Warnings; Errors }.
function Get-BuildCount {
param([string]$Output)

$warnings = 0
$errors = 0
if (-not [string]::IsNullOrWhiteSpace($Output)) {
$wMatch = [regex]::Match($Output, '^\s*(\d+)\s+Warning\(s\)',
[System.Text.RegularExpressions.RegexOptions]::Multiline)
$eMatch = [regex]::Match($Output, '^\s*(\d+)\s+Error\(s\)',
[System.Text.RegularExpressions.RegexOptions]::Multiline)
if ($wMatch.Success) { $warnings = [int]$wMatch.Groups[1].Value }
if ($eMatch.Success) { $errors = [int]$eMatch.Groups[1].Value }
}
return [pscustomobject]@{ Warnings = $warnings; Errors = $errors }
}

# Guard: skip top-level execution when the script is dot-sourced for testing.
if ($MyInvocation.InvocationName -eq '.') { return }

Expand Down Expand Up @@ -277,6 +321,11 @@ try {
-Define $Define `
-ShowOutput:$ShowOutput

$parsedDirs = Get-BuildOutputDir `
-Output $buildResult.Output `
-ProjectFileDir (Split-Path $resolvedProjectFile -Parent)
$counts = Get-BuildCount -Output $buildResult.Output

$resultObj = [pscustomobject]@{
scriptVersion = $script:Version
projectFile = $resolvedProjectFile
Expand All @@ -286,11 +335,13 @@ try {
define = $Define
rootDir = $resolvedRootDir
rsvarsPath = $rsvarsPath
exeOutputDir = if ([string]::IsNullOrWhiteSpace($ExeOutputDir)) { $null } else { $ExeOutputDir }
dcuOutputDir = if ([string]::IsNullOrWhiteSpace($DcuOutputDir)) { $null } else { $DcuOutputDir }
exeOutputDir = if (-not [string]::IsNullOrWhiteSpace($ExeOutputDir)) { $ExeOutputDir } else { $parsedDirs.ExeOutputDir }
dcuOutputDir = if (-not [string]::IsNullOrWhiteSpace($DcuOutputDir)) { $DcuOutputDir } else { $parsedDirs.DcuOutputDir }
unitSearchPath = if ($UnitSearchPath.Count -eq 0) { $null } else { $UnitSearchPath }
exitCode = $buildResult.ExitCode
success = ($buildResult.ExitCode -eq 0)
warnings = $counts.Warnings
errors = $counts.Errors
output = $buildResult.Output
}

Expand Down
8 changes: 4 additions & 4 deletions tests/pwsh/delphi-msbuild.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ Describe 'Invoke-MsbuildProject' {
-UnitSearchPath @('C:\Libs\MyLib')
}

It 'includes /p:DCC_UnitSearchPath=$(DCC_UnitSearchPath);C:\Libs\MyLib' {
$script:capturedArgs | Should -Contain '/p:DCC_UnitSearchPath=$(DCC_UnitSearchPath);C:\Libs\MyLib'
It 'includes /p:DCC_UnitSearchPath="$(DCC_UnitSearchPath);C:\Libs\MyLib"' {
$script:capturedArgs | Should -Contain '/p:DCC_UnitSearchPath="$(DCC_UnitSearchPath);C:\Libs\MyLib"'
}

}
Expand All @@ -437,8 +437,8 @@ Describe 'Invoke-MsbuildProject' {
-UnitSearchPath @('C:\Libs\A', 'C:\Libs\B')
}

It 'includes /p:DCC_UnitSearchPath=$(DCC_UnitSearchPath);C:\Libs\A;C:\Libs\B' {
$script:capturedArgs | Should -Contain '/p:DCC_UnitSearchPath=$(DCC_UnitSearchPath);C:\Libs\A;C:\Libs\B'
It 'includes /p:DCC_UnitSearchPath="$(DCC_UnitSearchPath);C:\Libs\A;C:\Libs\B"' {
$script:capturedArgs | Should -Contain '/p:DCC_UnitSearchPath="$(DCC_UnitSearchPath);C:\Libs\A;C:\Libs\B"'
}

}
Expand Down
Loading