Skip to content

Docker images

The Docker images femforge builds, the femforge-elmer solver image, and the environment probe that checks this machine can mesh and solve.

femforge.execution.docker.image

Images femforge builds itself: apt install scripts from typed sections, version tags guarded by a recipe hash, and the build on first use.

Classes:

  • AptPackage –

    An apt package an image installs and why it needs it.

  • DockerImage –

    An image a tool runs in: a published or built recipe image, or with no recipe, a tag Docker has or pulls.

  • ImageRecipe –

    An image femforge builds itself, with the version and recipe hash it was last released at.

  • Section –

    One titled block of an install script, listing each package it installs with its reason.

Functions:

  • apt_install –

    An apt-get install of packages, one per line.

  • ensure_image –

    Make Docker hold image: reuse a matching local tag, else pull a published one, else build its recipe; checked once per tag per process.

  • install_run –

    A Dockerfile RUN heredoc that runs sections as one strict, non-interactive shell script.

Attributes:

  • REGISTRY –

    The registry namespace femforge publishes its released images under.

REGISTRY module-attribute

REGISTRY = 'ghcr.io/femforge-io'

The registry namespace femforge publishes its released images under.

AptPackage dataclass

AptPackage(name: str, reason: str, build_only: bool = False)

An apt package an image installs and why it needs it.

Attributes:

  • build_only (bool) –

    Whether the install purges the package once it is done with it.

build_only class-attribute instance-attribute

build_only: bool = False

Whether the install purges the package once it is done with it.

DockerImage dataclass

DockerImage(tag: str, dockerfile: str | None = None, probe: tuple[str, ...] = (), published: bool = False, platform: str | None = None, labels: tuple[str, ...] = ())

An image a tool runs in: a published or built recipe image, or with no recipe, a tag Docker has or pulls.

Attributes:

  • probe (tuple[str, ...]) –

    A command run in the fresh image whose output the build logs, such as the installed tool's version.

  • published (bool) –

    Whether a registry serves the tag, so it is pulled before it is built.

  • platform (str | None) –

    The platform the image is pulled, built and run for, such as linux/amd64; None lets Docker choose.

  • labels (tuple[str, ...]) –

    The KEY=VALUE labels a build stamps beside the recipe label.

  • platform_args (list[str]) –

    The --platform option of a docker command, empty without a platform.

probe class-attribute instance-attribute

probe: tuple[str, ...] = ()

A command run in the fresh image whose output the build logs, such as the installed tool's version.

published class-attribute instance-attribute

published: bool = False

Whether a registry serves the tag, so it is pulled before it is built.

platform class-attribute instance-attribute

platform: str | None = None

The platform the image is pulled, built and run for, such as linux/amd64; None lets Docker choose.

labels class-attribute instance-attribute

labels: tuple[str, ...] = ()

The KEY=VALUE labels a build stamps beside the recipe label.

platform_args property

platform_args: list[str]

The --platform option of a docker command, empty without a platform.

ImageRecipe dataclass

ImageRecipe(name: str, version: str, digest: str, dockerfile: str, source: Path, probe: tuple[str, ...] = (), registry: str = REGISTRY, platform: str | None = None, probe_label: str = '')

An image femforge builds itself, with the version and recipe hash it was last released at.

Methods:

  • image –

    The released image under the registry, or while the recipe differs from the released one, a local dev image that is never published.

Attributes:

  • digest (str) –

    The sha256 of the Dockerfile released as version.

  • source (Path) –

    The module whose IMAGE_VERSION and IMAGE_DIGEST lines a bump rewrites.

  • registry (str) –

    The registry namespace a released version is published under.

  • platform (str | None) –

    The platform the image is built for and runs on; None lets Docker choose.

  • probe_label (str) –

    What probe reports the version of, such as elmerfem-csc.

digest instance-attribute

digest: str

The sha256 of the Dockerfile released as version.

source instance-attribute

source: Path

The module whose IMAGE_VERSION and IMAGE_DIGEST lines a bump rewrites.

registry class-attribute instance-attribute

registry: str = REGISTRY

The registry namespace a released version is published under.

platform class-attribute instance-attribute

platform: str | None = None

The platform the image is built for and runs on; None lets Docker choose.

probe_label class-attribute instance-attribute

probe_label: str = ''

What probe reports the version of, such as elmerfem-csc.

image

image() -> DockerImage

The released image under the registry, or while the recipe differs from the released one, a local dev image that is never published.

Source code in src/femforge/execution/docker/image.py
def image(self) -> DockerImage:
    """The released image under the registry, or while the recipe differs from the released one, a local dev image that is never published."""
    digest: str = recipe_digest(self.dockerfile)
    if digest == self.digest:
        return DockerImage(
            f"{self.registry}/{self.name}:{self.version}",
            self.dockerfile,
            self.probe,
            published=True,
            platform=self.platform,
            labels=(f"{VERSION_LABEL}={self.version}",),
        )

    version: str = f"{self.version}-dev.{digest[:8]}"

    return DockerImage(
        f"{self.name}:{version}",
        self.dockerfile,
        self.probe,
        published=False,
        platform=self.platform,
        labels=(f"{VERSION_LABEL}={version}",),
    )

Section dataclass

Section(title: str, commands: tuple[str, ...], packages: tuple[AptPackage, ...] = ())

One titled block of an install script, listing each package it installs with its reason.

Methods:

  • render –

    The block as shell lines: a dashed banner, the package reasons, then the commands.

render

render() -> str

The block as shell lines: a dashed banner, the package reasons, then the commands.

Source code in src/femforge/execution/docker/image.py
def render(self) -> str:
    """The block as shell lines: a dashed banner, the package reasons, then the commands."""
    label: str = f" {self.title.upper()} "
    dashes: int = BANNER_WIDTH - len("#  #") - len(label)
    banner: str = f"# {'-' * (dashes - dashes // 2)}{label}{'-' * (dashes // 2)} #"

    width: int = max((len(package.name) for package in self.packages), default=0)
    reasons: list[str] = [f"#           {package.name:<{width}}   {package.reason}" for package in self.packages]

    return "\n".join([banner, *reasons, *self.commands])

apt_install

apt_install(packages: tuple[AptPackage, ...]) -> str

An apt-get install of packages, one per line.

Source code in src/femforge/execution/docker/image.py
def apt_install(packages: tuple[AptPackage, ...]) -> str:
    """An ``apt-get install`` of ``packages``, one per line."""
    names: str = " \\\n".join(f"    {package.name}" for package in packages)

    return f"apt-get install -y --no-install-recommends \\\n{names}"

ensure_image

ensure_image(image: DockerImage, docker: DockerCli, *, pull: bool = True) -> None

Make Docker hold image: reuse a matching local tag, else pull a published one, else build its recipe; checked once per tag per process.

A tag without a recipe is pulled when Docker lacks it and never built, so a failed pull raises DockerError.

Parameters:

  • image (DockerImage) –

    The image to make available.

  • docker (DockerCli) –

    The client to run Docker through.

  • pull (bool, default: True ) –

    Try the registry before building a published recipe.

Source code in src/femforge/execution/docker/image.py
def ensure_image(image: DockerImage, docker: DockerCli, *, pull: bool = True) -> None:
    """Make Docker hold ``image``: reuse a matching local tag, else pull a published one, else build its recipe; checked once per tag per process.

    A tag without a recipe is pulled when Docker lacks it and never built, so a failed pull raises `DockerError`.

    Args:
        image: The image to make available.
        docker: The client to run Docker through.
        pull: Try the registry before building a published recipe.
    """

    def pulled_from_recipe(digest: str) -> bool:
        try:
            docker.pull(image.tag, platform=image.platform)
        except DockerError as error:
            log.bind(tag=image.tag, error=str(error)).warning("Could not pull the image; building it instead.")
            return False

        if docker.image_label(image.tag, RECIPE_LABEL) == digest:
            return True

        log.bind(tag=image.tag).warning("The pulled image was built from another recipe; building it instead.")
        return False

    with _BUILD_LOCK:
        if image.tag in _READY:
            return

        if image.dockerfile is None:
            if not docker.has_image(image.tag):
                docker.pull(image.tag, platform=image.platform)

            _READY.add(image.tag)
            return

        digest: str = recipe_digest(image.dockerfile)
        if docker.image_label(image.tag, RECIPE_LABEL) == digest or (pull and image.published and pulled_from_recipe(digest)):
            _READY.add(image.tag)
            return

        log.bind(tag=image.tag).info("Building a Docker image; this takes a few minutes, once.")
        docker.build(
            image.tag,
            image.dockerfile,
            labels=[f"{RECIPE_LABEL}={digest}", *image.labels],
            platform=image.platform,
        )

        probed: str = docker.output(["run", "--rm", *image.platform_args, image.tag, *image.probe]) if image.probe else ""
        log.bind(tag=image.tag, probe=probed).success("Built the Docker image.")
        _READY.add(image.tag)

install_run

install_run(sections: Sequence[Section]) -> str

A Dockerfile RUN heredoc that runs sections as one strict, non-interactive shell script.

Source code in src/femforge/execution/docker/image.py
def install_run(sections: Sequence[Section]) -> str:
    """A Dockerfile ``RUN`` heredoc that runs ``sections`` as one strict, non-interactive shell script."""
    script: str = "\n\n".join(section.render() for section in sections)

    return f"RUN <<EOF\nset -eux\nexport DEBIAN_FRONTEND=noninteractive\n\n{script}\nEOF"

femforge.elmer.runtime.docker_image

The femforge-elmer Docker image: headless Elmer from CSC's PPA on Ubuntu 24.04, with Open MPI and MUMPS.

Functions:

  • dockerfile –

    The image's Dockerfile, generated from INSTALL.

Attributes:

  • ELMER_IMAGE –

    The image Elmer runs in under the Docker backend.

ELMER_IMAGE module-attribute

ELMER_IMAGE = ImageRecipe(name='femforge-elmer', version=IMAGE_VERSION, digest=IMAGE_DIGEST, dockerfile=dockerfile(), source=Path(__file__), probe=('dpkg-query', '--show', '--showformat=${Version}', ELMER_PACKAGE.name), platform=PLATFORM, probe_label=ELMER_PACKAGE.name)

The image Elmer runs in under the Docker backend.

dockerfile

dockerfile() -> str

The image's Dockerfile, generated from INSTALL.

Source code in src/femforge/elmer/runtime/docker_image.py
def dockerfile() -> str:
    """The image's Dockerfile, generated from `INSTALL`."""
    return f"""\
# syntax=docker/dockerfile:1
# ^ Enables RUN heredocs (BuildKit).

# Elmer FEM (headless) on Ubuntu 24.04, installed from CSC's official PPA.
FROM {BASE_IMAGE}

# The repository this recipe comes from, and the licenses of the Elmer it installs.
LABEL {SOURCE_LABEL}="{SOURCE}" \\
      {LICENSES_LABEL}="{LICENSES}"

# Installs headless Elmer FEM from CSC's official package archive.
# One RUN, so the cleanup at the end actually shrinks the image.
{install_run(INSTALL)}

# Open MPI refuses to launch as root by default, and containers run as root.
# These allow parallel runs, e.g.:  mpirun -np 4 ElmerSolver_mpi
ENV OMPI_ALLOW_RUN_AS_ROOT=1 \\
    OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1

# Working directory for the simulation case (case.sif + mesh directory).
# Typically bind-mounted:  docker run -v "$PWD":/case <image> ElmerSolver case.sif
WORKDIR /case
"""

femforge.doctor

femforge doctor: check that this machine can mesh and solve, and name the fix for whatever it cannot.

Classes:

  • Check –

    One doctor check: what it looked at, how it went, and how to fix it.

  • CheckName –

    The checks, in the order the doctor runs them.

  • CheckStatus –

    How a check went.

  • DoctorReport –

    Every doctor check in order, with the count of blockers and warnings.

Functions:

  • add_arguments –

    The femforge doctor options.

  • diagnose –

    Run every check against this machine.

  • render –

    Print the checks as a table, then the numbered fixes and the count of blockers and warnings.

  • run –

    Run the checks and report them; the exit code is 1 when any check failed.

Attributes:

  • COMMAND –

    The femforge subcommand.

COMMAND module-attribute

COMMAND = 'doctor'

The femforge subcommand.

Check pydantic-model

Bases: BaseModel

One doctor check: what it looked at, how it went, and how to fix it.

Config:

  • use_attribute_docstrings: True

Fields:

name pydantic-field

name: CheckName

What was checked.

status pydantic-field

status: CheckStatus

How the check went.

detail pydantic-field

detail: str

What the check found.

fix pydantic-field

fix: str | None = None

What to do about it, when there is something to do.

CheckName

Bases: StrEnum

The checks, in the order the doctor runs them.

CheckStatus

Bases: StrEnum

How a check went.

Attributes:

  • style (str) –

    The Rich style the status shows in.

style property

style: str

The Rich style the status shows in.

DoctorReport pydantic-model

Bases: BaseModel

Every doctor check in order, with the count of blockers and warnings.

Config:

  • use_attribute_docstrings: True
  • json_schema_mode_override: serialization

Fields:

checks pydantic-field

checks: list[Check]

The checks, in the order they ran.

blockers property

blockers: int

How many checks failed.

warnings property

warnings: int

How many checks warned.

add_arguments

add_arguments(parser: ArgumentParser) -> None

The femforge doctor options.

Source code in src/femforge/doctor.py
def add_arguments(parser: argparse.ArgumentParser) -> None:
    """The ``femforge doctor`` options."""
    _ = parser.add_argument("--json", action="store_true", help="Print the report as JSON instead of a table.")
    _ = parser.add_argument("--runs-dir", default="runs", help="The runs directory whose disk is checked (default: runs).")

diagnose

diagnose(runs_dir: Path, artifacts: Path | None = None, *, docker: DockerCli | None = None, recipe: ImageRecipe = ELMER_IMAGE) -> DoctorReport

Run every check against this machine.

Parameters:

  • runs_dir (Path) –

    The runs directory whose disk is checked.

  • artifacts (Path | None, default: None ) –

    The artifact store whose disk is checked; None skips that check.

  • docker (DockerCli | None, default: None ) –

    The Docker client to probe through; None makes one.

  • recipe (ImageRecipe, default: ELMER_IMAGE ) –

    The image recipe whose image is checked.

Source code in src/femforge/doctor.py
def diagnose(runs_dir: Path, artifacts: Path | None = None, *, docker: DockerCli | None = None, recipe: ImageRecipe = ELMER_IMAGE) -> DoctorReport:
    """Run every check against this machine.

    Args:
        runs_dir: The runs directory whose disk is checked.
        artifacts: The artifact store whose disk is checked; None skips that check.
        docker: The Docker client to probe through; None makes one.
        recipe: The image recipe whose image is checked.
    """
    client: DockerCli = docker if docker is not None else DockerCli()
    docker_check, info = _check_docker(client)
    docker_usable: bool = info is not None
    image_check: Check = _check_elmer_image(client, recipe) if docker_usable else Check(name=CheckName.ELMER_IMAGE, status=CheckStatus.SKIP, detail="Needs Docker.")
    native_check, tools = _check_native_elmer(docker_usable)

    return DoctorReport(
        checks=[
            _check_python(),
            _check_gmsh(),
            _check_cad_kernel(),
            docker_check,
            image_check,
            native_check,
            _check_disk(CheckName.RUNS_DISK, runs_dir.resolve(), "Free space, for example with `femforge purge`."),
            _check_disk(CheckName.ARTIFACTS_DISK, artifacts.resolve() if artifacts is not None else None, "Free space, for example with `femforge purge`."),
            _check_docker_disk(info),
            _check_solve_path(tools is not None, docker_usable),
        ]
    )

render

render(report: DoctorReport, console: Console) -> None

Print the checks as a table, then the numbered fixes and the count of blockers and warnings.

Source code in src/femforge/doctor.py
def render(report: DoctorReport, console: Console) -> None:
    """Print the checks as a table, then the numbered fixes and the count of blockers and warnings."""
    table = Table("Check", "Status", "Detail")
    for check in report.checks:
        table.add_row(check.name.value, Text(check.status.value.upper(), style=check.status.style), Text(check.detail))

    console.print(table)

    fixes: list[Check] = [check for check in report.checks if check.fix is not None]
    for number, check in enumerate(fixes, start=1):
        console.print(Text(f"{number}. {check.name.value}: {check.fix}"))

    blockers: str = f"{report.blockers} blocker{'' if report.blockers == 1 else 's'}"
    warnings: str = f"{report.warnings} warning{'' if report.warnings == 1 else 's'}"
    console.print(f"{blockers}, {warnings}")

run

run(args: Namespace, console: Console, artifacts: str | None) -> int

Run the checks and report them; the exit code is 1 when any check failed.

Parameters:

  • args (Namespace) –

    The parsed femforge doctor options.

  • console (Console) –

    Where the table goes.

  • artifacts (str | None) –

    The artifact store root from the environment, or None.

Source code in src/femforge/doctor.py
def run(args: argparse.Namespace, console: Console, artifacts: str | None) -> int:
    """Run the checks and report them; the exit code is 1 when any check failed.

    Args:
        args: The parsed ``femforge doctor`` options.
        console: Where the table goes.
        artifacts: The artifact store root from the environment, or None.
    """
    report: DoctorReport = diagnose(Path(cast("str", args.runs_dir)), Path(artifacts) if artifacts else None)
    if cast("bool", args.json):
        print(report.model_dump_json(indent=2))
    else:
        render(report, console)

    return 1 if report.blockers else 0