Using DPM in Continuous Integration
DPM is designed to work well on a build server. Because packages are restored from sources into a machine-local cache and compiled on demand, your build agents don't need third-party components pre-installed, and you avoid the "compiles on my machine" problems that come from manually-managed library paths.
This guide covers both sides of CI:
- Consuming — restoring the packages a project depends on, then building it.
- Publishing — packing, signing, verifying and pushing your own packages from a release pipeline.
Table of Contents
- Why DPM in CI
- Getting dpm.exe onto the build agent
- What to commit and what to ignore
- Configuring sources non-interactively
- Restoring packages (the consume path)
- Caching packages between builds
- Publishing packages (the release path)
- Exit codes
- CI system examples
- Troubleshooting
Why DPM in CI
- No components installed in the IDE.
dpm restoreresolves and compiles packages into the cache and wires the project's search paths. Nothing has to be installed into a RAD Studio instance on the agent. - Reproducible builds. Versions are recorded in the
.dproj, so a clean checkout restores exactly the same dependency graph on every agent. - Runs headless. The CLI is a single executable with no IDE dependency for
restore,pack,sign,verifyandpush— it runs fine in a Windows container / Server Core. - Scriptable. Every command returns a meaningful exit code, so a failed restore or a vulnerable dependency can fail the build.
Getting dpm.exe onto the build agent
dpm.exe is a standalone console application. The simplest approach is to download a known release of the CLI, unzip it to a fixed location on the agent, and put that folder on the PATH. See installing for download locations.
Recommendations for CI:
- Pin a version. Download a specific
dpm.exerelease rather than "latest" so builds are reproducible. Treat the DPM version like any other build tool version. - No IDE required to restore or publish.
restore,pack,sign,verifyandpushdo not need the Delphi IDE. Compiling the restored project does — that step still calls MSBuild /dcc32, so the agent needs RAD Studio (or the command-line compiler) installed for the target compiler version. - Quieter output. Pass
--nobannerto suppress the startup banner and--verbosity=Quiet|Normal|Detailedto control log volume.
dpm --nobanner restore .\MyProject.dprojWhat to commit and what to ignore
| Path | Commit? | Why |
|---|---|---|
*.dproj / *.groupproj | Yes | These carry the resolved DPM dependency graph that restore reads. They are the source of truth for which package versions a clean build will restore. |
A repo-level dpm.config.yaml (optional) | Yes, if used | Lets the build pin its package sources without touching the agent's machine config. |
The package cache (%APPDATA%\.dpm\package_cache) | No | Machine-local and fully regenerated by restore. Cache it in the CI system instead of committing it. |
Configuring sources non-interactively
By default DPM reads its config from %APPDATA%\.dpm\dpm.config.yaml. On a build agent you usually want the build to be self-contained rather than depending on whatever is in the agent user's profile. Two options:
1. Ship a config with the repo and point at it explicitly. Every command accepts --configFile:
dpm --nobanner restore .\MyProject.dproj --configFile=.\ci\dpm.config.yaml2. Add the source as a setup step.
dpm sources add -name=corporate -source=https://packages.example.com -type=DPMServer ^
-userName=%DPM_USER% -password=%DPM_PASSWORD%
dpm sources listdpm sources supports Add, Remove, Enable, Disable, Update, Refresh and List, with -type one of Folder, GitRegistry or DPMServer. For git-registry sources, see git-registry-packages.md.
Keep push API keys in your CI system's secret store and pass them through environment variables — never hard-code them in a committed config or a script.
Restoring packages (the consume path)
dpm restore is the core CI command. It accepts a .dproj, a .groupproj, or a folder (in which case it restores every .dproj it finds):
dpm --nobanner restore .\MyProject.dproj
dpm --nobanner restore .\src :: every .dproj under .\src
dpm --nobanner restore .\MySolution.groupprojUseful options (source):
| Option | Alias | Purpose |
|---|---|---|
-source=<name> | -s | Restrict to specific source(s). Repeat or comma-separate for several. Omit to use all configured sources. |
-compiler=<ver> | -c | Restore for a specific compiler version (e.g. 12.0). |
-debugMode | -dm | Restore/compile the Debug configuration of packages. |
-ignoreHashLocks | -ihl | Refresh manifest hashes from the cache instead of failing on a recorded-hash mismatch. |
A typical pipeline restores, then hands the project to your build tool (FinalBuilder, MSBuild etc):
dpm --nobanner restore .\MyProject.dproj || exit /b 1
call rsvars.bat
fbcmd MyProject.dproj || exit /b 1restore returns 0 on success and non-zero on failure — always gate the build on it (see Exit codes).
Caching packages between builds
DPM downloads and compiles packages into a cache. By default that's %APPDATA%\.dpm\package_cache, but you can redirect it with the DPMPACKAGECACHE environment variable. Pointing it at a folder your CI system persists between runs means packages aren't re-downloaded and re-compiled every build:
set DPMPACKAGECACHE=%CI_WORKSPACE%\.dpmcache
dpm --nobanner restore .\MyProject.dprojThis will speed up builds substantially.
Publishing packages (the release path)
To publish your own packages from CI, run the release pipeline in this order: pack → sign → verify → push.
1. Pack
dpm pack builds a .dpkg from a .dspec.yaml. Override the version from CI (e.g. your build number) so each build produces a uniquely-versioned package:
dpm --nobanner pack .\MyPackage.dspec.yaml -version=%BUILD_VERSION% -outputfolder=.\artifacts2. Sign
dpm sign signs one or more .dpkg files. For CI, always supply secrets via environment variables, never as literal command-line values (they leak into process listings and logs). The provider-specific secret flags all take the name of an environment variable:
:: Local PFX, password from the PFX_PWD environment variable
dpm sign .\artifacts\*.dpkg -pfx=%CODESIGN_PFX% -pfx-password-env=PFX_PWD
:: Azure Key Vault
dpm sign .\artifacts\*.dpkg -provider=keyvault ^
-vault-url=https://my.vault.azure.net -cert-name=codesign ^
-tenant-id=%AZ_TENANT% -client-id=%AZ_CLIENT% -client-secret-env=AAD_SECRET
:: VSoft Signotaur
dpm sign .\artifacts\*.dpkg -provider=signotaur ^
-endpoint=https://signotaur.example.com -api-key-env=SIGNOTAUR_KEY -label=CodeSignNote whilst you can sign using certificates in your windows credential store, those are usually backed by a token which will prompt for a password - not recommended for CI.
The target can be a single file, a folder, or a wildcard. Add -r to recurse and -fail-fast to stop on the first failure. See Package Signing for the full signing guide.
3. Verify
Confirm the signature and package integrity before publishing. --json-output is handy for parsing in a script; --offline skips network calls (revocation / timestamp authority) on locked-down agents:
dpm verify .\artifacts\MyPackage.1.2.3.dpkg --json-outputverify exits 0 when the package is valid and non-zero otherwise.
4. Push
dpm push uploads a .dpkg to a source. -source/-s is required; supply the API key from a secret:
dpm push .\artifacts\MyPackage.1.2.3.dpkg -source=corporate -apiKey=%DPM_APIKEY% ^
-skipDuplicate -retries=3| Option | Purpose |
|---|---|
-source / -s | Required. Target source name from your config. |
-apiKey / -a | API key for an authenticated source. Pass from a CI secret. |
-skipDuplicate | Don't fail if that package + version already exists. Makes re-runs idempotent. |
-retries | Retry on rate-limit (429) / transient (502/503/504) errors. Default 3; 0 disables. |
-retryDelay | Base seconds between retries (exponential backoff with jitter). Default 2. |
-timeout / -t | Upload timeout in seconds. Default 300. |
Optional: SBOM and vulnerability gating
DPM can also generate a Software Bill of Materials and fail the build on known vulnerabilities — useful as a release-pipeline gate:
dpm sbom .\MyProject.dproj -format=cyclonedx -outdir=.\artifacts
dpm scan .\MyProject.dproj -fail-on=highdpm scan -fail-on=high returns a non-zero exit code when a vulnerability of the given severity (or higher) is found. See sbom command and scan command.
Exit codes
Every command returns one of these (source). Gate your pipeline on a non-zero result. You can also run dpm exitcodes to print this list.
| Code | Meaning |
|---|---|
0 | Success |
1 | Error (command failed) |
2 | Missing argument |
100 | Initialization exception |
101 | Invalid arguments |
102 | Invalid command |
200 | Feature not implemented |
9999 | Unhandled exception |
Checking the code in a script:
:: cmd / batch
dpm --nobanner restore .\MyProject.dproj
if errorlevel 1 (
echo DPM restore failed
exit /b 1
)# PowerShell
dpm --nobanner restore .\MyProject.dproj
if ($LASTEXITCODE -ne 0) { throw "DPM restore failed ($LASTEXITCODE)" }CI system examples
All examples assume dpm.exe is on the PATH and a Delphi command-line compiler is available on the agent for the build step.
Generic / CI-agnostic
A minimal restore-then-build, with caching and exit-code checks, in PowerShell:
$env:DPMPACKAGECACHE = Join-Path $PWD '.dpmcache' # persist this folder between runs
dpm --nobanner restore .\MyProject.dproj
if ($LASTEXITCODE -ne 0) { throw "restore failed" }
& "$env:BDS\bin\rsvars.bat"
msbuild MyProject.dproj /t:Build /p:Config=Release /p:Platform=Win32
if ($LASTEXITCODE -ne 0) { throw "build failed" }GitHub Actions
Delphi isn't available on GitHub-hosted runners, so use a self-hosted Windows runner with RAD Studio and dpm.exe installed and (optionally) FinalBuilder. actions/cache persists the package cache between runs, keyed on the project files.
name: build
on: [push]
jobs:
build:
runs-on: [self-hosted, windows, delphi]
env:
DPMPACKAGECACHE: ${{ github.workspace }}\.dpmcache
steps:
- uses: actions/checkout@v4
- name: Cache DPM packages
uses: actions/cache@v4
with:
path: ${{ github.workspace }}\.dpmcache
key: dpm-${{ hashFiles('**/*.dproj', '**/*.groupproj', '**/dpm.config.yaml') }}
- name: Restore
run: dpm --nobanner restore .\MyProject.dproj
- name: Build
run: |
call "%BDS%\bin\rsvars.bat"
msbuild MyProject.dproj /t:Build /p:Config=Release /p:Platform=Win32
shell: cmd
release:
needs: build
runs-on: [self-hosted, windows, delphi]
if: startsWith(github.ref, 'refs/tags/')
env:
PFX_PWD: ${{ secrets.CODESIGN_PFX_PWD }}
DPM_APIKEY: ${{ secrets.DPM_APIKEY }}
steps:
- uses: actions/checkout@v4
- name: Pack, sign, verify, push
run: |
dpm --nobanner pack .\MyPackage.dspec.yaml -version=${{ github.ref_name }} -outputfolder=.\artifacts
dpm sign .\artifacts\*.dpkg -pfx=%CODESIGN_PFX% -pfx-password-env=PFX_PWD
for %%f in (.\artifacts\*.dpkg) do dpm verify "%%f" || exit /b 1
for %%f in (.\artifacts\*.dpkg) do dpm push "%%f" -source=corporate -apiKey=%DPM_APIKEY% -skipDuplicate
shell: cmdFinalBuilder / Continua CI
DPM is itself built with FinalBuilder and Continua CI, so they're a natural fit.
FinalBuilder — there are FinalBuilder Actions for DPM restore and pack (sign action will be added soon).
Set DPMPACKAGECACHE as a project/environment variable so the cache lives in a known location, then follow the restore with your existing Compile Delphi Project action.
Continua CI — run the same commands in an Execute Program action inside your stage. Point DPMPACKAGECACHE at a path under the workspace and use a Continua CI Cache (or a shared agent folder) so the package cache survives between builds. Pass the push API key from a Continua secret/variable into the environment rather than hard-coding it.
:: stage script (Execute Program actions)
set DPMPACKAGECACHE=$Workspace$\.dpmcache
dpm --nobanner restore .\MyProject.dproj
dpm --nobanner pack .\MyPackage.dspec.yaml -version=$Version$ -outputfolder=.\artifacts
dpm push .\artifacts\*.dpkg -source=corporate -apiKey=$DPM_APIKEY$ -skipDuplicateGitLab CI
Use a Windows runner with the shell executor (Delphi can't run in a generic Linux container). cache: persists the package folder between jobs.
stages: [build, release]
variables:
DPMPACKAGECACHE: "$CI_PROJECT_DIR\\.dpmcache"
cache:
key:
files:
- "**/*.dproj"
- dpm.config.yaml
paths:
- .dpmcache/
build:
stage: build
tags: [windows, delphi]
script:
- dpm --nobanner restore .\MyProject.dproj
- cmd /c "call \"%BDS%\bin\rsvars.bat\" && msbuild MyProject.dproj /t:Build /p:Config=Release /p:Platform=Win32"
publish:
stage: release
tags: [windows, delphi]
rules:
- if: $CI_COMMIT_TAG
script:
- dpm --nobanner pack .\MyPackage.dspec.yaml -version=$CI_COMMIT_TAG -outputfolder=.\artifacts
- dpm sign .\artifacts\*.dpkg -pfx=$CODESIGN_PFX -pfx-password-env=PFX_PWD
- dpm push .\artifacts\*.dpkg -source=corporate -apiKey=$DPM_APIKEY -skipDuplicatePFX_PWD, CODESIGN_PFX and DPM_APIKEY are defined as masked/protected CI/CD variables, not in the YAML.
Jenkins
A declarative Jenkinsfile on a Windows agent, using credentials binding so the API key never appears in the log:
pipeline {
agent { label 'windows && delphi' }
environment {
DPMPACKAGECACHE = "${WORKSPACE}\\.dpmcache"
}
stages {
stage('Restore') {
steps {
bat 'dpm --nobanner restore .\\MyProject.dproj'
}
}
stage('Build') {
steps {
bat '''
call "%BDS%\\bin\\rsvars.bat"
msbuild MyProject.dproj /t:Build /p:Config=Release /p:Platform=Win32
'''
}
}
stage('Publish') {
when { tag '*' }
steps {
withCredentials([string(credentialsId: 'dpm-apikey', variable: 'DPM_APIKEY')]) {
bat '''
dpm --nobanner pack .\\MyPackage.dspec.yaml -version=%TAG_NAME% -outputfolder=.\\artifacts
dpm push .\\artifacts\\*.dpkg -source=corporate -apiKey=%DPM_APIKEY% -skipDuplicate
'''
}
}
}
}
}Troubleshooting
- Restore can't find a package / source. Confirm the build is reading the config you expect — pass
--configFileexplicitly and rundpm sources listas a diagnostic step. A source defined only in a developer's profile won't exist on the agent. - Authentication failures on a private feed. Make sure the credential environment variables are actually set in the job (masked secrets are easy to forget to expose) and that the source was added with the matching
-userName/-passwordor that the API key is passed topush. - Packages re-download every build. The cache isn't being persisted. Set
DPMPACKAGECACHEto a stable path and wire it into the CI system's cache mechanism (see Caching packages between builds). - Hash-lock mismatch on restore. If a package was legitimately refreshed in the cache,
-ignoreHashLockslets restore refresh the recorded hashes instead of failing. Investigate first — a mismatch can also indicate a changed package. - Verifying on an offline / locked-down agent. Use
dpm verify ... --offlineto skip revocation and timestamp-authority network calls. - A command returns 200 (not implemented). That feature isn't available in your DPM build — check the version and the command's status in the docs.