mirror of
https://github.com/home-assistant/core.git
synced 2026-05-23 17:00:13 +01:00
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
"""Generate and validate the dockerfile."""
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import re
|
|
|
|
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
|
|
)
|
|
|
|
_DOCKERFILE_SYNTAX_PATTERN = re.compile(r"# syntax=docker/dockerfile@sha256:[0-9a-f]+")
|
|
_DOCKERFILE_S6_GRACETIME_PATTERN = re.compile(r"S6_SERVICES_GRACETIME=\d+")
|
|
|
|
|
|
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
|
|
)
|
|
|
|
|
|
@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.
|
|
#
|
|
# To update, run python3 -m script.hassfest -p docker
|
|
FROM python:{python_version}-alpine
|
|
|
|
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/
|
|
|
|
# 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 \
|
|
# 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)" \
|
|
&& uv pip install \
|
|
--no-build \
|
|
--no-cache \
|
|
-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)"
|
|
|
|
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()
|
|
|
|
|
|
@dataclass
|
|
class File:
|
|
"""File."""
|
|
|
|
content: str
|
|
path: Path
|
|
|
|
|
|
def _generate_files(config: Config) -> list[File]:
|
|
timeout = (
|
|
core.STOPPING_STAGE_SHUTDOWN_TIMEOUT
|
|
+ core.STOP_STAGE_SHUTDOWN_TIMEOUT
|
|
+ core.FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT
|
|
+ core.CLOSE_STAGE_SHUTDOWN_TIMEOUT
|
|
+ executor.EXECUTOR_SHUTDOWN_TIMEOUT
|
|
+ thread.THREADING_SHUTDOWN_TIMEOUT
|
|
+ 10
|
|
) * 1000
|
|
|
|
dockerfile_path = config.root / "Dockerfile"
|
|
|
|
files = [
|
|
File(
|
|
_update_dockerfile(dockerfile_path.read_text(encoding="UTF-8"), timeout),
|
|
dockerfile_path,
|
|
),
|
|
File(
|
|
_HASSFEST_TEMPLATE.format(
|
|
dockerfile_syntax=_DOCKERFILE_SYNTAX_SHA,
|
|
python_version=_get_python_version(config.root),
|
|
),
|
|
config.root / "script/hassfest/docker/Dockerfile",
|
|
),
|
|
]
|
|
|
|
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
|
|
|
|
|
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
|
"""Validate dockerfile."""
|
|
docker_files = _generate_files(config)
|
|
config.cache["docker"] = docker_files
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
def generate(integrations: dict[str, Integration], config: Config) -> None:
|
|
"""Generate dockerfile."""
|
|
for file in _generate_files(config):
|
|
file.path.write_text(file.content)
|