The Docker images used to build and deploy OpenModelica with Jenkins.
Images are published to:
ghcr.io/openmodelica/build-deps(GitHub Container Registry)docker.openmodelica.org/build-deps(Nexus)
Every image lives on main, keyed by operating system and OS version rather
than by OpenModelica version. Each image is a base plus optional, layered
add-ons, so the heavy common tooling is built once and reused.
main
├── apt/
│ └── Dockerfile # multi-stage: all Ubuntu + Debian versions + add-ons
├── apk/
│ └── Dockerfile # multi-stage: Alpine Linux + add-ons
├── rpm/
│ └── Dockerfile # multi-stage: AlmaLinux, RHEL, Fedora
├── pacman/
│ └── Dockerfile # placeholder (not implemented yet)
└── .ci/
├── matrix.yml # source of truth: which images exist
├── matrix.py # matrix.yml -> CI matrix / tag lookup
├── lib.sh # recipe hash + registry hash label
├── registry-status.sh # does this image need building at all?
├── build.sh # build one image (base + add-ons), write GHA cache
└── publish.sh # push + sign one image to every registry
- Base image — one per OS/OS-version. Contains everything needed to build OpenModelica (distro packages + common tooling: TeX, Qt, Python venv, ccache, …). This is what most CI jobs use.
- Add-on image — the base plus one thing the distro package manager can't
provide or that needs a pinned version (e.g. CMake 4). Realized as an extra
build stage (
FROMthe base stage) in the same Dockerfile, so shared layers are reused from cache.
Ubuntu and Debian share apt/Dockerfile. The DISTRO and
VERSION build-args select the base image; the Qt package set is picked from
${ID}:${VERSION_ID} at build time. The base image is the full stage; each
add-on is a further stage (e.g. --target cmake-4).
Each image's context, dockerfile, target, build_args and addons
(add-on stage names) are declared in .ci/matrix.yml.
To add a new image, create or extend the OS's Dockerfile and list it in .ci/matrix.yml (a new version of an existing OS needs only a matrix entry).
One image repository per registry; OS, version and variant are encoded in the tag:
| Tag | Mutable? | Meaning |
|---|---|---|
ubuntu-24.04 |
moving | Latest base image for Ubuntu 24.04 |
ubuntu-24.04-2.1.0 |
immutable | Pinned base, synthesized from git tag v2.1.0 |
ubuntu-24.04-cmake-4 |
moving | Latest CMake 4 add-on on the 24.04 base |
ubuntu-24.04-cmake-4-2.1.0 |
immutable | Pinned add-on, synthesized from git tag v2.1.0 |
Releasing is done by pushing a single repo-wide git tag v<MAJOR>.<MINOR>.<PATCH>
(e.g. v2.1.0). CI synthesizes the per-image immutable Docker tags from it and
publishes all images in one run. Day-to-day CI uses the moving tag; when an
OpenModelica release needs a frozen environment it pins the immutable tag.
| OS / version | Base tag | Add-ons | Dockerfile |
|---|---|---|---|
| Ubuntu 26.04 (Resolute) | ubuntu-26.04 |
rust, rust-qt-wasm, rust-qt-win-x86_64, rust-qt-win-aarch64, rust-qt-mac, debug, omsimulator |
apt/Dockerfile |
| Ubuntu 24.04 (Noble) | ubuntu-24.04 |
cmake-4, debug, omsimulator |
apt/Dockerfile |
| Ubuntu 22.04 (Jammy) | ubuntu-22.04 |
debug, newer-qt, qt-aarch64-linux, omsimulator |
apt/Dockerfile |
| Debian 13 (Trixie) | debian-13 |
cmake-4, debug |
apt/Dockerfile |
| Debian 12 (Bookworm) | debian-12 |
cmake-4, debug |
apt/Dockerfile |
| Alpine 3.24 | alpine-3.24 |
omsimulator |
apk/Dockerfile |
| AlmaLinux 10 | almalinux-10 |
– | rpm/Dockerfile |
| AlmaLinux 9 | almalinux-9 |
– | rpm/Dockerfile |
| Fedora 44 | fedora-44 |
– | rpm/Dockerfile |
| Fedora 43 | fedora-43 |
– | rpm/Dockerfile |
Base image — pick the distro and version with DISTRO and VERSION:
# Ubuntu
docker build --pull --no-cache \
--target full \
--build-arg DISTRO=ubuntu --build-arg VERSION=24.04 \
--tag build-deps:ubuntu-24.04 \
apt
# Debian
docker build --pull --no-cache \
--target full \
--build-arg DISTRO=debian --build-arg VERSION=13 --build-arg INTEL_OCL_PKGS= \
--tag build-deps:debian-13 \
aptAdd-on image — build the add-on's stage with --target. It reuses the
base's cached layers, so it only adds the extra step:
docker build --pull \
--target cmake-4 \
--build-arg DISTRO=ubuntu --build-arg VERSION=24.04 \
--tag build-deps:ubuntu-24.04-cmake-4 \
aptAlpine uses the separate apk/Dockerfile context. Alpine has
no OpenModelica apk repo, so the build dependencies are installed directly; the
only build-arg is VERSION (the Alpine release):
# Base image (the `full` stage)
docker build --pull --no-cache \
--target full \
--build-arg VERSION=3.24 \
--tag build-deps:alpine-3.24 \
apk
# OMSimulator add-on (reuses the base's cached layers)
docker build --pull \
--target omsimulator \
--build-arg VERSION=3.24 \
--tag build-deps:alpine-3.24-omsimulator \
apkEnterprise Linux (AlmaLinux, RHEL) and Fedora share
rpm/Dockerfile. DISTRO and VERSION select the base image;
EPEL/CRB, Qt5/Qt6, autoconf 2.7x and gcc-toolset are picked from ID/PLATFORM_ID
at build time:
# AlmaLinux
docker build --pull --no-cache \
--target full \
--build-arg DISTRO=almalinux --build-arg VERSION=10 \
--tag build-deps:almalinux-10 \
rpm
# Fedora
docker build --pull --no-cache \
--target full \
--build-arg DISTRO=fedora --build-arg VERSION=44 \
--tag build-deps:fedora-44 \
rpmThe values to pass (
context,--file,--target,--build-arg) for any image are exactly its fields in .ci/matrix.yml.
A single workflow, build.yml, runs the whole pipeline so
that a release it creates can publish in the same run (a release created
with GITHUB_TOKEN cannot trigger a separate workflow):
pull request discover ─▶ build (changed images only, no push)
push main discover ─▶ publish ─▶ cleanup
push tag discover ─▶ publish ─▶ release
- build — on a pull request, builds the base + add-ons whose recipe the PR changed (no push). Unchanged images cost one registry lookup.
- publish — builds each image once and pushes it to GHCR and Nexus
from that single build, so both registries share a digest. Moving tags
(
<os>-<version>) are updated on every push tomain, the weekly schedule, andworkflow_dispatch. Immutable tags (<os>-<version>-<semver>) are pushed on a release tag, and can also be republished viaworkflow_dispatchwhen a globalv<semver>is supplied. On GHCR the immutable tags are cosign-signed — only on releases (and optionally onworkflow_dispatchre-publishes of a release); the moving tag of a released image points at the same digest, so it verifies with the same signature. - release — on a repo-wide release tag, creates/updates the GitHub Release once every image has been published.
Every published image carries the hash of the recipe it was built from as the
org.openmodelica.build-deps.hash label — the same label
apt-build's Jenkinsfile uses for its own build-deps images. The
hash covers the build-args, the stage that is built, the instructions building
that stage would run, and the rest of the build context, so it changes exactly
when the image would come out different.
Before building anything, a job reads that label from the registry and skips
the image when every tag it would push already matches, so a change to one
Dockerfile does not rebuild the others. Within a Dockerfile the granularity is
the target's stage closure — everything reachable from it through FROM and
--from= — so editing one stage rebuilds that stage and its descendants, not
every image the Dockerfile serves.
Within a run no image is built more than once: a push to main skips the
build job (the pull request already proved the build) and goes straight to
publish, which pushes to both registries from a single docker buildx build.
An add-on is built on top of the image its parent stage published, pinned by
digest (--build-context <parent>=docker-image://…@sha256:…). FROM full
names a stage rather than the published base image, so without that the add-on
would re-run the base's apt-get upgrade and could end up on a different
package set than the base image it extends. The recipe hash still covers the
parent's instructions, so a change to the base rebuilds every add-on.
A release of an image whose recipe is already published is not rebuilt
either: its immutable tag is added to the digest that the moving tag already
points at (docker buildx imagetools create --prefer-index=false, a
digest-preserving carbon copy). The release is then exactly the image CI has
been using, and one cosign signature still covers both tags.
The hash cannot see upstream changes (a new ubuntu:24.04 base image, updated
distro packages), so two triggers ignore it and rebuild unconditionally: the
weekly schedule, and a workflow_dispatch with its force checkbox left on.
A registry that answers "no such tag" means the image has to be built. A registry we cannot reach at all (after three attempts) fails the job instead: an image we cannot see is not an image we may assume is up to date, and rebuilding it blindly would paper over a broken registry.
A second workflow, cleanup.yml, deletes
stale versions of the GHCR package: untagged digests left behind by re-pushed
moving tags and orphaned cosign sha256-* signature tags whose image is gone.
It keeps all tagged images, anything still referenced by them, and signatures
of existing images. It runs automatically after every successful GHCR publish
and can also be triggered manually, where it defaults to a dry run — inspect
the log, then re-run it with dry-run unchecked. Note that superseded digests
are pruned right after each publish, so pulling a moving tag's previous image
by digest is not supported.
See RELEASING.md for the step-by-step process.
The original Dockerfile was taken from OpenModelica/OpenModelicaBuildScripts. See LICENSE.md.