From eb4a40c6e81d16a7b48eba8f5aac375582f43f8c Mon Sep 17 00:00:00 2001 From: Darian Miller Date: Mon, 16 Mar 2026 22:52:43 -0500 Subject: [PATCH] For Ticket #11 exe/dcu output directories and UnitSearchPath --- CHANGELOG.md | 18 +- README.md | 114 +++++++++++-- source/delphi-msbuild.ps1 | 67 ++++++-- tests/pwsh/delphi-msbuild.Tests.ps1 | 245 ++++++++++++++++++++++++++++ 4 files changed, 416 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 690dd50..5998439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,32 @@ All notable changes to this project will be documented in this file. --- +## [0.3.0] - Unreleased + +- Add `-ExeOutputDir` parameter to set the compiled executable output directory + via `/p:DCC_ExeOutput` +- Add `-DcuOutputDir` parameter to set the compiled DCU output directory + via `/p:DCC_DcuOutput` +- Add `-UnitSearchPath` parameter to append additional unit search paths + via `/p:DCC_UnitSearchPath=$(DCC_UnitSearchPath);...`, preserving paths + already set by the project's PropertyGroups +[#11](https://github.com/continuous-delphi/delphi-msbuild/issues/11) + + +- Add support for passing compiler defines to MSBUILD + [#9](https://github.com/continuous-delphi/delphi-msbuild/issues/9) ## [0.2.0] - 2026-03-16 - Added `delphi-msbuild.ps1` to be a direct download on the release page -[#5](https://github.com/continuous-delphi/delphi-msbuild/issues/5) + [#5](https://github.com/continuous-delphi/delphi-msbuild/issues/5) ## [0.1.0] - 2026-03-16 - Initial release of `delphi-msbuild.ps1` -- build Delphi `.dproj` projects via MSBuild from the command line, with support for piped output from `delphi-inspect` and automatic `rsvars.bat` environment sourcing. -[#1](https://github.com/continuous-delphi/delphi-msbuild/issues/1) + [#1](https://github.com/continuous-delphi/delphi-msbuild/issues/1)
diff --git a/README.md b/README.md index bd15253..78a0f59 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,95 @@ The MSBuild verbosity level. Passed to MSBuild as `/v:`. Valid values: `quiet`, `minimal`, `normal`, `detailed`, `diagnostic` +## -ExeOutputDir + +```text +-ExeOutputDir +``` + +Output directory for the compiled executable or library. Passed to MSBuild as +`/p:DCC_ExeOutput=`. + +When omitted, MSBuild uses the output location defined in the project's +PropertyGroups. The result object's `.exeOutputDir` is `$null` when this +parameter is not supplied. + +## -DcuOutputDir + +```text +-DcuOutputDir +``` + +Output directory for compiled `.dcu` files. Passed to MSBuild as +`/p:DCC_DcuOutput=`. + +When omitted, MSBuild uses the DCU location from the project's PropertyGroups. +The result object's `.dcuOutputDir` is `$null` when this parameter is not +supplied. + +## -UnitSearchPath + +```text +-UnitSearchPath +``` + +Additional unit search paths appended to the project's existing unit path. +Accepts an array of path strings. Multiple paths are joined with semicolons +and passed as: + +```text +/p:DCC_UnitSearchPath="$(DCC_UnitSearchPath);path1;path2" +``` + +The `$(DCC_UnitSearchPath)` prefix preserves the paths already set in the +project's PropertyGroups. Without it, the assignment would replace them +entirely. + +When omitted (or an empty array), no `/p:DCC_UnitSearchPath` argument is added. +The result object's `.unitSearchPath` is `$null` when no paths are supplied. + +Example: + +```powershell +-UnitSearchPath @('C:\Libs\A', 'C:\Libs\B') +``` + +## -Define + +```text +-Define +``` + +One or more additional MSBuild defines to pass to the compiler. When at least +one value is supplied, the script appends the following to the MSBuild command +line: + +```text +/p:DCC_Define="$(DCC_Define);DEFINE1;DEFINE2" +``` + +The `$(DCC_Define)` prefix preserves the defines already set by the project's +PropertyGroups (e.g. `DEBUG`, `RELEASE`). Without it, the property assignment +would replace them entirely. + +When no `-Define` values are supplied (the default), the `/p:DCC_Define` +argument is omitted entirely. + +Examples: + +```powershell +# Single define +delphi-msbuild.ps1 -ProjectFile .\src\MyApp.dproj -RootDir $root -Define CI + +# Multiple defines +delphi-msbuild.ps1 -ProjectFile .\src\MyApp.dproj -RootDir $root ` + -Define MYFLAG, USE_JEDI_JCL + +# Via pipeline with defines +delphi-inspect.ps1 -DetectLatest -Platform Win32 -BuildSystem MSBuild | + delphi-msbuild.ps1 -ProjectFile .\src\MyApp.dproj -Define CI, MYFLAG +``` + ## -ShowOutput (switch) ```text @@ -155,17 +244,20 @@ On success or build failure (exit codes 0 and 5), a single `pscustomobject` is written to the pipeline before the script exits. This allows downstream pipeline steps to consume the build result. -| Property | Type | Description | -|---------------|---------|------------------------------------------------------| -| `projectFile` | string | Absolute path to the project file | -| `platform` | string | Platform value used (e.g. `Win32`) | -| `config` | string | Config value used (e.g. `Debug`) | -| `target` | string | Target used (e.g. `Build`) | -| `rootDir` | string | Resolved Delphi installation root | -| `rsvarsPath` | string | Derived path to `rsvars.bat` | -| `exitCode` | int | MSBuild process exit code | -| `success` | bool | `$true` when `exitCode` is 0 | -| `output` | string | Captured MSBuild output; `$null` when `-ShowOutput` | +| Property | Type | Description | +|------------------|----------|----------------------------------------------------------| +| `projectFile` | string | Absolute path to the project file | +| `platform` | string | Platform value used (e.g. `Win32`) | +| `config` | string | Config value used (e.g. `Debug`) | +| `target` | string | Target used (e.g. `Build`) | +| `rootDir` | string | Resolved Delphi installation root | +| `rsvarsPath` | string | Derived path to `rsvars.bat` | +| `exeOutputDir` | string | Value of `-ExeOutputDir`; `$null` when not supplied | +| `dcuOutputDir` | string | Value of `-DcuOutputDir`; `$null` when not supplied | +| `unitSearchPath` | string[] | Value of `-UnitSearchPath`; `$null` when not supplied | +| `exitCode` | int | MSBuild process exit code | +| `success` | bool | `$true` when `exitCode` is 0 | +| `output` | string | Captured MSBuild output; `$null` when `-ShowOutput` | Note: On fata errors before MSBuild is invoked (exit codes 2, 3, 4) no result object is emitted. diff --git a/source/delphi-msbuild.ps1 b/source/delphi-msbuild.ps1 index 83ecf61..21c442f 100644 --- a/source/delphi-msbuild.ps1 +++ b/source/delphi-msbuild.ps1 @@ -74,6 +74,19 @@ param( [ValidateSet('quiet','minimal','normal','detailed','diagnostic')] [string]$Verbosity = 'normal', + # Output directory for the compiled executable or DLL (/p:DCC_ExeOutput property). + [string]$ExeOutputDir, + + # Output directory for compiled DCU files (/p:DCC_DcuOutput property). + [string]$DcuOutputDir, + + # Additional unit search paths (/p:DCC_UnitSearchPath property). Multiple paths are + # joined with semicolons and appended to the paths already set by the project's + # PropertyGroups. + [string[]]$UnitSearchPath = @(), + + [string[]]$Define = @(), + [switch]$ShowOutput ) @@ -177,6 +190,10 @@ function Invoke-MsbuildProject { [string]$Config, [string]$Target, [string]$Verbosity, + [string]$ExeOutputDir, + [string]$DcuOutputDir, + [string[]]$UnitSearchPath = @(), + [string[]]$Define = @(), [switch]$ShowOutput ) @@ -188,6 +205,19 @@ function Invoke-MsbuildProject { "/v:$Verbosity" ) + 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 ($Define.Count -gt 0) { + $defineValue = '$(DCC_Define);' + ($Define -join ';') + $msbuildArgs += "/p:DCC_Define=$defineValue" + } + return Invoke-MsbuildExe -Arguments $msbuildArgs -ShowOutput:$ShowOutput } @@ -231,24 +261,31 @@ try { Invoke-RsvarsEnvironment -RsvarsPath $rsvarsPath $buildResult = Invoke-MsbuildProject ` - -ProjectFile $resolvedProjectFile ` - -Platform $Platform ` - -Config $Config ` - -Target $Target ` - -Verbosity $Verbosity ` + -ProjectFile $resolvedProjectFile ` + -Platform $Platform ` + -Config $Config ` + -Target $Target ` + -Verbosity $Verbosity ` + -ExeOutputDir $ExeOutputDir ` + -DcuOutputDir $DcuOutputDir ` + -UnitSearchPath $UnitSearchPath ` + -Define $Define ` -ShowOutput:$ShowOutput $resultObj = [pscustomobject]@{ - scriptVersion = $script:Version - projectFile = $resolvedProjectFile - platform = $Platform - config = $Config - target = $Target - rootDir = $resolvedRootDir - rsvarsPath = $rsvarsPath - exitCode = $buildResult.ExitCode - success = ($buildResult.ExitCode -eq 0) - output = $buildResult.Output + scriptVersion = $script:Version + projectFile = $resolvedProjectFile + platform = $Platform + config = $Config + target = $Target + rootDir = $resolvedRootDir + rsvarsPath = $rsvarsPath + exeOutputDir = if ([string]::IsNullOrWhiteSpace($ExeOutputDir)) { $null } else { $ExeOutputDir } + dcuOutputDir = if ([string]::IsNullOrWhiteSpace($DcuOutputDir)) { $null } else { $DcuOutputDir } + unitSearchPath = if ($UnitSearchPath.Count -eq 0) { $null } else { $UnitSearchPath } + exitCode = $buildResult.ExitCode + success = ($buildResult.ExitCode -eq 0) + output = $buildResult.Output } Write-Output $resultObj diff --git a/tests/pwsh/delphi-msbuild.Tests.ps1 b/tests/pwsh/delphi-msbuild.Tests.ps1 index c8eb9ed..29d315e 100644 --- a/tests/pwsh/delphi-msbuild.Tests.ps1 +++ b/tests/pwsh/delphi-msbuild.Tests.ps1 @@ -26,6 +26,14 @@ Passes correct MSBuild arguments to Invoke-MsbuildExe. Forwards -ShowOutput switch to Invoke-MsbuildExe. Returns the result object from Invoke-MsbuildExe. + ExeOutputDir adds /p:DCC_ExeOutput; omitted adds nothing. + DcuOutputDir adds /p:DCC_DcuOutput; omitted adds nothing. + UnitSearchPath single entry appends with $(DCC_UnitSearchPath) prefix. + UnitSearchPath multiple entries joined with semicolons. + UnitSearchPath omitted adds no /p:DCC_UnitSearchPath argument. + Omits /p:DCC_Define when no defines are supplied. + Appends /p:DCC_Define with $(DCC_Define) prefix for a single define. + Appends /p:DCC_Define with $(DCC_Define) prefix for multiple defines. Describe 5 - Main flow (via Invoke-ToolProcess, no MSBuild calls): Exits 3 when no rootDir is provided (no pipeline, no -RootDir). @@ -293,6 +301,243 @@ Describe 'Invoke-MsbuildProject' { } + Context 'ExeOutputDir adds /p:DCC_ExeOutput' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -ExeOutputDir 'C:\Build\bin' + } + + It 'includes /p:DCC_ExeOutput=C:\Build\bin' { + $script:capturedArgs | Should -Contain '/p:DCC_ExeOutput=C:\Build\bin' + } + + } + + Context 'ExeOutputDir omitted adds no /p:DCC_ExeOutput argument' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' + } + + It 'no argument contains DCC_ExeOutput' { + ($script:capturedArgs | Where-Object { $_ -like '*DCC_ExeOutput*' }) | Should -BeNullOrEmpty + } + + } + + Context 'DcuOutputDir adds /p:DCC_DcuOutput' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -DcuOutputDir 'C:\Build\dcu' + } + + It 'includes /p:DCC_DcuOutput=C:\Build\dcu' { + $script:capturedArgs | Should -Contain '/p:DCC_DcuOutput=C:\Build\dcu' + } + + } + + Context 'DcuOutputDir omitted adds no /p:DCC_DcuOutput argument' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' + } + + It 'no argument contains DCC_DcuOutput' { + ($script:capturedArgs | Where-Object { $_ -like '*DCC_DcuOutput*' }) | Should -BeNullOrEmpty + } + + } + + Context 'UnitSearchPath single entry appends with $(DCC_UnitSearchPath) prefix' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -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' + } + + } + + Context 'UnitSearchPath multiple entries are joined with semicolons' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -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' + } + + } + + Context 'UnitSearchPath omitted adds no /p:DCC_UnitSearchPath argument' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' + } + + It 'no argument contains DCC_UnitSearchPath' { + ($script:capturedArgs | Where-Object { $_ -like '*DCC_UnitSearchPath*' }) | Should -BeNullOrEmpty + } + + } + + Context 'omits /p:DCC_Define when no -Define values are supplied' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' + } + + It 'does not include any /p:DCC_Define argument' { + $script:capturedArgs | Should -Not -Contain { $_ -like '/p:DCC_Define=*' } + ($script:capturedArgs | Where-Object { $_ -like '/p:DCC_Define=*' }) | Should -BeNullOrEmpty + } + + } + + Context 'appends /p:DCC_Define with $(DCC_Define) prefix for a single define' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -Define @('MYFLAG') + } + + It 'includes /p:DCC_Define=$(DCC_Define);MYFLAG' { + $script:capturedArgs | Should -Contain '/p:DCC_Define=$(DCC_Define);MYFLAG' + } + + } + + Context 'appends /p:DCC_Define with $(DCC_Define) prefix for multiple defines' { + + BeforeAll { + $script:capturedArgs = $null + Mock Invoke-MsbuildExe { + $script:capturedArgs = $Arguments + return [pscustomobject]@{ ExitCode = 0; Output = '' } + } + + Invoke-MsbuildProject ` + -ProjectFile 'C:\Projects\MyApp.dproj' ` + -Platform 'Win32' ` + -Config 'Debug' ` + -Target 'Build' ` + -Verbosity 'normal' ` + -Define @('MYFLAG', 'USE_JEDI_JCL') + } + + It 'includes /p:DCC_Define=$(DCC_Define);MYFLAG;USE_JEDI_JCL' { + $script:capturedArgs | Should -Contain '/p:DCC_Define=$(DCC_Define);MYFLAG;USE_JEDI_JCL' + } + + } + } Describe 'Main flow -- pre-MSBuild validation (no MSBuild invoked)' {