Writing tests is an important way of validating the correctness of the software you are building and is a common practice for Erlang and Elixir projects. Type checking is also an important way of validating the correctness of the software you are working on. Dialyzer is still a valuable tool that is good at finding type errors in Erlang and Elixir source code. If you aren’t running Dialyzer on your Erlang and Elixir code your build is likely not catching bugs that could easily be found and fixed. Running Dialyzer automatically during Jenkins builds is a good way of ensuring obvious type errors are found and fixed.
In this blog post I will explain how I set up Jenkins to run Dialyzer during a Docker build for an Elixir project. I will also show how to cache Dialyzer’s PLT (persistent lookup table) files between builds using Jenkins artifacts and a Docker volume to speed up Dialyzer runs.
The instructions in this article are for Elixir 1.10 projects running in Docker, but the same steps can be applied to Erlang projects with minimal changes. The primary difference between the Elixir and Erlang builds would be the build paths.
Running Dialyzer
Running Dialyzer is pretty easy.
Add the dialyxir package to your project’s mix.exs
file. Make sure to specify the environments the command will be needed in. In my case I needed the dialyzer
command in dev
and test
environments:
defp deps do
[
...
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}
]
end
Running Dialyzer in Docker is as simple as adding the Dialyzer command right below the place where you run your Elixir tests:
RUN mix test
RUN MIX_ENV=test mix dialyzer
While this works, running Dialyzer during the Docker build like this isn’t going to allow us to reuse PLT files. There is a better way.
Reusing PLT Files
Building the type information is time consuming so Dialyzer writes the data to PLT files on disk. If you run Dialyzer during the Docker build in Jenkins the PLT files will not be present and Dialyzer will recreate them for every build. Building the PLT files will likely add 10-15 minutes to your build time. To speed up builds it is possible to cache files with Jenkins artifacts and reuse them on subsequent builds. Since we are using Docker the PLT files will be located inside the Docker container, so we will need a way to get the PLT files in and out of the Docker container.
Sharing PLT Files Between the Container and the Host
Putting files into a Docker container is easy. A simple COPY <source> <destination>
command in the Dockerfile is all that’s needed. But we need to access the same PLT files both inside and outside of the container. Inside the container the PLT files will be used by Dialyzer to speed up the type analysis. After Dialyzer has finished inside the container the updated files will be needed outside of the container in the Jenkins build directory so they can be cached as Jenkins artifacts.
The easiest way of sharing files between the container and the host file system is a Docker volume. In order to use a Docker volume, you’ll need to first build the container with the application code, and then run the Dialyzer command with a volume mounted in the container.
Dockerfile
When running a container using docker run
, you’ll need to use the --mount
flag like this:
$ docker run ... --mount 'type=volume,src=/dialyxir,dst=/app/dialyxir,read_only=false'
Docker Compose
My Jenkins build actually ran mix dialyzer
in a container that was started with Docker Compose. Creating a volume is also easy with Docker Compose:
volumes:
- type: bind
source: /dialyxir
target: /app/dialyxir
read_only: false
Putting PLT Files in the Volume
When Dialyzer runs in Elixir, it’s not going to write the PLT files to dialyxir/
, it’s going to write them _build/test/
. After Dialyzer runs we will need to copy the PLT files into the volume so they can be accessed on the host file system. We need to add two commands after the mix dialyxir
command for this. The easiest way to do all this is to create a Bash script in the container that you can invoke with docker run
. The script should look something like this. I named my script run_checks.sh
:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\t\n' # Stricter IFS settings
# Run tests as usual if you want
mix test
# Run Dialyzer
MIX_ENV=test mix dialyzer
# Create the dialyxir directory if it doesn't already exist
mkdir -p dialyxir
# Copy the PLT files into the dialyxir directory
cp -f _build/dev/dialyxir* dialyxir
Note
|
In the past I have encountered permissions issues with files shared with the host through a Docker volume. Sometimes the host does not have permission to read the files, preventing Jenkins from uploading the PLT files as artifacts. You can work around this issue by copying the files into a new directory, and setting permission on that directory and its contents to 777:
Then outside the container in the Jenkins build copy the files from |
Now you can run the script using the docker run
command after you have built the docker image:
$ docker run <container>:<tag> run_checks.sh ... --mount 'type=volume,src=/dialyxir,dst=/app/dialyxir,read_only=false'
After running this command you should see the PLT files in a dialyxir
directory inside the same directory that you ran the command in. You now have something that you can cache using Jenkins artifacts.
Saving PLT Files as Jenkins Build Artifacts
If you are using Jenkins pipelines uploading the PLT files as Jenkins artifacts can be done as a stage that runs after the docker run
that generates the PLT files. You only want to save PLT files for builds of the master branch. Feature branches may generate PLT files that contain new dependencies that are not used by the master branch or other feature branches and may result in PLT files aren’t suitable for use in later builds.
stage('Upload Build Artifacts') {
when {
branch 'master'
}
stage('Upload Artifacts') {
steps {
// Upload the PLT files as Jenkins artifacts
archiveArtifacts artifacts: 'dialyxir/*', onlyIfSuccessful: true
}
}
}
Downloading PLT File Artifacts for Reuse
Now that the build is storing PLT files in Jenkins artifacts subsequent builds can download and use them instead of rebuilding the PLT files from scratch. This can be easily done with a Jenkins stage that downloads the files before running the run_checks.sh
script in Docker:
stages {
stage('Download Artifacts') {
steps {
// download dialyzer artifacts from jenkins
copyArtifacts(projectName: '<project name>/master', target: 'dialyxir', optional: true, flatten: true)
}
}
...
Summary
With these two new stages in place your builds should now be able to upload generated PLT files after running Dialyzer on your project’s master branch and download the cached PLT files on subsequent builds of any branch. PLT files will never need to be re-created and will only need to be updated when dependencies change. With PLT file caching my Elixir build times improved from an average of 15-20 minutes to around 4 minutes.
Example Files
Below is a Dockerfile and a Jenkinsfile so you can see what the final files might look like after all these changes.
Example Dockerfile:
FROM elixir:1.10.2-alpine AS base
LABEL project="<project name>"
ENV APP_ROOT=/app
WORKDIR $APP_ROOT
# Add dependencies
FROM base AS deps
RUN apk add coreutils
# Copy in application code
FROM deps AS application
COPY . $APP_ROOT
# Generate final image
FROM application AS build
# Fetch the Elixir dependencies and compile and release the application
RUN MIX_ENV=prod mix do deps.get, compile, release
# Copy release into the rel directory for convenience
COPY --from=build $APP_ROOT/_build/prod/rel rel
ENTRYPOINT ["/app/rel/<app name>/bin/<app name>"]
CMD ["start"]
Example Jenkinsfile:
pipeline {
stages {
stage('Download Artifacts') {
steps {
// Download PLT files from Jenkins
copyArtifacts(projectName: '<project name>/master', target: 'dialyxir', optional: true, flatten: true)
}
}
stage('Build Docker Image') {
sh 'docker build .'
}
stage('Run Tests and Dialyzer') {
// Run Dialyzer with shared volume so we can share PLT files between Jenkins and the container
sh 'docker run --mount "type=volume,src=/dialyxir,dst=/app/dialyxir,read_only=false" <project name> ./run_checks.sh'
}
stage('Upload Build Artifacts') {
when {
branch 'master'
}
stage('Upload Artifacts') {
steps {
// Upload the PLT files as Jenkins artifacts
archiveArtifacts artifacts: 'dialyxir/*', onlyIfSuccessful: true
}
}
}
}
}