Skip to content

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

  • No components installed in the IDE. dpm restore resolves 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, verify and push — 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.exe release 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, verify and push do 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 --nobanner to suppress the startup banner and --verbosity=Quiet|Normal|Detailed to control log volume.
cmd
dpm --nobanner restore .\MyProject.dproj

What to commit and what to ignore

PathCommit?Why
*.dproj / *.groupprojYesThese 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 usedLets the build pin its package sources without touching the agent's machine config.
The package cache (%APPDATA%\.dpm\package_cache)NoMachine-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:

cmd
dpm --nobanner restore .\MyProject.dproj --configFile=.\ci\dpm.config.yaml

2. Add the source as a setup step.

cmd
dpm sources add -name=corporate -source=https://packages.example.com -type=DPMServer ^
  -userName=%DPM_USER% -password=%DPM_PASSWORD%
dpm sources list

dpm 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):

cmd
dpm --nobanner restore .\MyProject.dproj
dpm --nobanner restore .\src                       :: every .dproj under .\src
dpm --nobanner restore .\MySolution.groupproj

Useful options (source):

OptionAliasPurpose
-source=<name>-sRestrict to specific source(s). Repeat or comma-separate for several. Omit to use all configured sources.
-compiler=<ver>-cRestore for a specific compiler version (e.g. 12.0).
-debugMode-dmRestore/compile the Debug configuration of packages.
-ignoreHashLocks-ihlRefresh 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):

cmd
dpm --nobanner restore .\MyProject.dproj || exit /b 1
call rsvars.bat
fbcmd MyProject.dproj || exit /b 1

restore 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:

cmd
set DPMPACKAGECACHE=%CI_WORKSPACE%\.dpmcache
dpm --nobanner restore .\MyProject.dproj

This 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:

cmd
dpm --nobanner pack .\MyPackage.dspec.yaml -version=%BUILD_VERSION% -outputfolder=.\artifacts

2. 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:

cmd
:: 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=CodeSign

Note 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:

cmd
dpm verify .\artifacts\MyPackage.1.2.3.dpkg --json-output

verify 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:

cmd
dpm push .\artifacts\MyPackage.1.2.3.dpkg -source=corporate -apiKey=%DPM_APIKEY% ^
  -skipDuplicate -retries=3
OptionPurpose
-source / -sRequired. Target source name from your config.
-apiKey / -aAPI key for an authenticated source. Pass from a CI secret.
-skipDuplicateDon't fail if that package + version already exists. Makes re-runs idempotent.
-retriesRetry on rate-limit (429) / transient (502/503/504) errors. Default 3; 0 disables.
-retryDelayBase seconds between retries (exponential backoff with jitter). Default 2.
-timeout / -tUpload 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:

cmd
dpm sbom .\MyProject.dproj -format=cyclonedx -outdir=.\artifacts
dpm scan .\MyProject.dproj -fail-on=high

dpm 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.

CodeMeaning
0Success
1Error (command failed)
2Missing argument
100Initialization exception
101Invalid arguments
102Invalid command
200Feature not implemented
9999Unhandled exception

Checking the code in a script:

cmd
:: cmd / batch
dpm --nobanner restore .\MyProject.dproj
if errorlevel 1 (
  echo DPM restore failed
  exit /b 1
)
powershell
# 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:

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.

yaml
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: cmd

FinalBuilder / 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.

text
:: 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$ -skipDuplicate

GitLab 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.

yaml
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 -skipDuplicate

PFX_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:

groovy
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 --configFile explicitly and run dpm sources list as 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 / -password or that the API key is passed to push.
  • Packages re-download every build. The cache isn't being persisted. Set DPMPACKAGECACHE to 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, -ignoreHashLocks lets 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 ... --offline to 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.