Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

198 lines
6.6 KiB
Python
Raw Permalink Normal View History

2023-11-29 11:26:50 +01:00
"""Generate and validate the dockerfile."""
2024-08-30 13:09:10 +02:00
from dataclasses import dataclass
from pathlib import Path
2026-05-19 10:10:17 +02:00
import re
2024-08-30 13:09:10 +02:00
2023-11-29 11:26:50 +01:00
from homeassistant import core
from homeassistant.util import executor, thread
from .model import Config, Integration
# Don't forget to update also Dockerfile.dev when updating this.
_DOCKERFILE_SYNTAX_SHA = (
"2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769" # 1.23.0
)
2026-05-19 10:10:17 +02:00
_DOCKERFILE_SYNTAX_PATTERN = re.compile(r"# syntax=docker/dockerfile@sha256:[0-9a-f]+")
_DOCKERFILE_S6_GRACETIME_PATTERN = re.compile(r"S6_SERVICES_GRACETIME=\d+")
2025-11-24 12:52:58 +01:00
2023-11-29 11:26:50 +01:00
2026-05-19 10:10:17 +02:00
def _update_dockerfile(content: str, timeout: int) -> str:
"""Update the hassfest-managed parts of the Dockerfile."""
content = _DOCKERFILE_SYNTAX_PATTERN.sub(
f"# syntax=docker/dockerfile@sha256:{_DOCKERFILE_SYNTAX_SHA}",
content,
count=1,
)
return _DOCKERFILE_S6_GRACETIME_PATTERN.sub(
f"S6_SERVICES_GRACETIME={timeout}", content, count=1
)
2023-11-29 11:26:50 +01:00
@dataclass(frozen=True)
class _MachineConfig:
"""Machine-specific Dockerfile configuration."""
arch: str
packages: tuple[str, ...] = ()
_MACHINES = {
"generic-x86-64": _MachineConfig(arch="amd64", packages=("libva-intel-driver",)),
"green": _MachineConfig(arch="aarch64"),
"khadas-vim3": _MachineConfig(arch="aarch64"),
"odroid-c2": _MachineConfig(arch="aarch64"),
"odroid-c4": _MachineConfig(arch="aarch64"),
"odroid-m1": _MachineConfig(arch="aarch64"),
"odroid-n2": _MachineConfig(arch="aarch64"),
"qemuarm-64": _MachineConfig(arch="aarch64"),
"qemux86-64": _MachineConfig(arch="amd64"),
"raspberrypi3-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi4-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi5-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"yellow": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
}
_MACHINE_DOCKERFILE_TEMPLATE = r"""# syntax=docker/dockerfile@sha256:{dockerfile_syntax}
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/{arch}-homeassistant:latest
FROM ${{BUILD_FROM}}
{extra_packages}
LABEL io.hass.machine="{machine}"
"""
def _generate_machine_dockerfile(
machine_name: str, machine_config: _MachineConfig
) -> str:
"""Generate a machine Dockerfile from configuration."""
if machine_config.packages:
pkg_lines = " \\\n ".join(machine_config.packages)
extra_packages = f"\nRUN apk --no-cache add \\\n {pkg_lines}\n"
else:
extra_packages = ""
return _MACHINE_DOCKERFILE_TEMPLATE.format(
dockerfile_syntax=_DOCKERFILE_SYNTAX_SHA,
arch=machine_config.arch,
extra_packages=extra_packages,
machine=machine_name,
)
_HASSFEST_TEMPLATE = r"""# syntax=docker/dockerfile@sha256:{dockerfile_syntax}
# Automatically generated by hassfest.
2024-08-30 13:09:10 +02:00
#
# To update, run python3 -m script.hassfest -p docker
FROM python:{python_version}-alpine
2023-11-29 11:26:50 +01:00
2024-08-30 13:09:10 +02:00
ENV \
UV_SYSTEM_PYTHON=true \
UV_EXTRA_INDEX_URL="https://wheels.home-assistant.io/musllinux-index/"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"]
WORKDIR "/github/workspace"
COPY --parents requirements.txt homeassistant/ script /usr/src/homeassistant/
2024-08-30 13:09:10 +02:00
# Uv creates a lock file in /tmp
RUN --mount=type=tmpfs,target=/tmp \
--mount=type=bind,source=requirements_test.txt,target=/tmp/requirements_test.txt,readonly \
--mount=type=bind,source=requirements_test_pre_commit.txt,target=/tmp/requirements_test_pre_commit.txt,readonly \
2024-08-30 13:09:10 +02:00
# Required for PyTurboJPEG
apk add --no-cache libturbojpeg \
# Install uv at the version pinned in the requirements file
&& pip install --no-cache-dir "uv==$(awk -F'==' '/^uv==/{{print $2}}' /usr/src/homeassistant/requirements.txt)" \
2024-08-30 13:09:10 +02:00
&& uv pip install \
--no-build \
--no-cache \
2024-09-03 14:59:01 +02:00
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
"pipdeptree==$(awk -F'==' '/^pipdeptree==/{{print $2}}' /tmp/requirements_test.txt)" \
"tqdm==$(awk -F'==' '/^tqdm==/{{print $2}}' /tmp/requirements_test.txt)" \
"ruff==$(awk -F'==' '/^ruff==/{{print $2}}' /tmp/requirements_test_pre_commit.txt)"
2024-08-30 13:09:10 +02:00
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
LABEL "com.github.actions.name"="hassfest"
LABEL "com.github.actions.description"="Run hassfest to validate standalone integration repositories"
LABEL "com.github.actions.icon"="terminal"
LABEL "com.github.actions.color"="gray-dark"
"""
def _get_python_version(root: Path) -> str:
"""Extract the Python version from .python-version."""
return (root / ".python-version").read_text(encoding="UTF-8").strip()
2024-08-30 13:09:10 +02:00
@dataclass
class File:
"""File."""
content: str
path: Path
def _generate_files(config: Config) -> list[File]:
2023-11-29 11:26:50 +01:00
timeout = (
core.STOPPING_STAGE_SHUTDOWN_TIMEOUT
+ core.STOP_STAGE_SHUTDOWN_TIMEOUT
+ core.FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT
+ core.CLOSE_STAGE_SHUTDOWN_TIMEOUT
2023-11-29 11:26:50 +01:00
+ executor.EXECUTOR_SHUTDOWN_TIMEOUT
+ thread.THREADING_SHUTDOWN_TIMEOUT
+ 10
2024-08-30 13:09:10 +02:00
) * 1000
2026-05-19 10:10:17 +02:00
dockerfile_path = config.root / "Dockerfile"
files = [
2024-08-30 13:09:10 +02:00
File(
2026-05-19 10:10:17 +02:00
_update_dockerfile(dockerfile_path.read_text(encoding="UTF-8"), timeout),
dockerfile_path,
2024-08-30 13:09:10 +02:00
),
File(
_HASSFEST_TEMPLATE.format(
dockerfile_syntax=_DOCKERFILE_SYNTAX_SHA,
python_version=_get_python_version(config.root),
),
config.root / "script/hassfest/docker/Dockerfile",
),
2024-08-30 13:09:10 +02:00
]
for machine_name, machine_config in sorted(_MACHINES.items()):
files.append(
File(
_generate_machine_dockerfile(machine_name, machine_config),
config.root / "machine" / machine_name,
)
)
return files
2023-11-29 11:26:50 +01:00
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate dockerfile."""
2024-08-30 13:09:10 +02:00
docker_files = _generate_files(config)
config.cache["docker"] = docker_files
2023-11-29 11:26:50 +01:00
2024-08-30 13:09:10 +02:00
for file in docker_files:
if file.content != file.path.read_text():
config.add_error(
"docker",
f"File {file.path} is not up to date. Run python3 -m script.hassfest",
fixable=True,
)
2023-11-29 11:26:50 +01:00
def generate(integrations: dict[str, Integration], config: Config) -> None:
"""Generate dockerfile."""
2024-08-30 13:09:10 +02:00
for file in _generate_files(config):
file.path.write_text(file.content)