mirror of https://github.com/home-assistant/core
227 lines
6.9 KiB
Python
227 lines
6.9 KiB
Python
"""Generate and validate the dockerfile."""
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from homeassistant import core
|
|
from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION as GO2RTC_VERSION
|
|
from homeassistant.const import Platform
|
|
from homeassistant.util import executor, thread
|
|
from script.gen_requirements_all import gather_recursive_requirements
|
|
|
|
from .model import Config, Integration
|
|
from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR
|
|
|
|
DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest.
|
|
#
|
|
# To update, run python3 -m script.hassfest -p docker
|
|
ARG BUILD_FROM
|
|
FROM ${{BUILD_FROM}}
|
|
|
|
# Synchronize with homeassistant/core.py:async_stop
|
|
ENV \
|
|
S6_SERVICES_GRACETIME={timeout} \
|
|
UV_SYSTEM_PYTHON=true \
|
|
UV_NO_CACHE=true
|
|
|
|
ARG QEMU_CPU
|
|
|
|
# Install uv
|
|
RUN pip3 install uv=={uv}
|
|
|
|
WORKDIR /usr/src
|
|
|
|
## Setup Home Assistant Core dependencies
|
|
COPY requirements.txt homeassistant/
|
|
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
|
RUN \
|
|
uv pip install \
|
|
--no-build \
|
|
-r homeassistant/requirements.txt
|
|
|
|
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
|
|
RUN \
|
|
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
|
|
uv pip install homeassistant/home_assistant_*.whl; \
|
|
fi \
|
|
&& uv pip install \
|
|
--no-build \
|
|
-r homeassistant/requirements_all.txt
|
|
|
|
## Setup Home Assistant Core
|
|
COPY . homeassistant/
|
|
RUN \
|
|
uv pip install \
|
|
-e ./homeassistant \
|
|
&& python3 -m compileall \
|
|
homeassistant/homeassistant
|
|
|
|
# Home Assistant S6-Overlay
|
|
COPY rootfs /
|
|
|
|
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
|
ARG BUILD_ARCH
|
|
# Get go2rtc binary
|
|
RUN \
|
|
case "${{BUILD_ARCH}}" in \
|
|
"aarch64") go2rtc_suffix='arm64' ;; \
|
|
"armhf") go2rtc_suffix='armv6' ;; \
|
|
"armv7") go2rtc_suffix='arm' ;; \
|
|
*) go2rtc_suffix=${{BUILD_ARCH}} ;; \
|
|
esac \
|
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v{go2rtc}/go2rtc_linux_${{go2rtc_suffix}} --output /bin/go2rtc \
|
|
&& chmod +x /bin/go2rtc \
|
|
# Verify go2rtc can be executed
|
|
&& go2rtc --version
|
|
|
|
WORKDIR /config
|
|
"""
|
|
|
|
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
|
|
#
|
|
# To update, run python3 -m script.hassfest -p docker
|
|
FROM python:3.13-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 . /usr/src/homeassistant
|
|
|
|
# Uv is only needed during build
|
|
RUN --mount=from=ghcr.io/astral-sh/uv:{uv},source=/uv,target=/bin/uv \
|
|
# Required for PyTurboJPEG
|
|
apk add --no-cache libturbojpeg \
|
|
&& uv pip install \
|
|
--no-build \
|
|
--no-cache \
|
|
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
|
-r /usr/src/homeassistant/requirements.txt \
|
|
stdlib-list==0.10.0 pipdeptree=={pipdeptree} tqdm=={tqdm} ruff=={ruff} \
|
|
{required_components_packages}
|
|
|
|
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_package_versions(file: Path, packages: set[str]) -> dict[str, str]:
|
|
package_versions: dict[str, str] = {}
|
|
with file.open(encoding="UTF-8") as fp:
|
|
for _, line in enumerate(fp):
|
|
if package_versions.keys() == packages:
|
|
return package_versions
|
|
|
|
if match := PACKAGE_REGEX.match(line):
|
|
pkg, sep, version = match.groups()
|
|
|
|
if pkg not in packages:
|
|
continue
|
|
|
|
if sep != "==" or not version:
|
|
raise RuntimeError(
|
|
f'Requirement {pkg} need to be pinned "{pkg}==<version>".'
|
|
)
|
|
|
|
for part in version.split(";", 1)[0].split(","):
|
|
version_part = PIP_VERSION_RANGE_SEPARATOR.match(part)
|
|
if version_part:
|
|
package_versions[pkg] = version_part.group(2)
|
|
break
|
|
|
|
if package_versions.keys() == packages:
|
|
return package_versions
|
|
|
|
raise RuntimeError("At least one package was not found in the requirements file.")
|
|
|
|
|
|
@dataclass
|
|
class File:
|
|
"""File."""
|
|
|
|
content: str
|
|
path: Path
|
|
|
|
|
|
def _generate_hassfest_dockerimage(
|
|
config: Config, timeout: int, package_versions: dict[str, str]
|
|
) -> File:
|
|
packages = set()
|
|
already_checked_domains = set()
|
|
for platform in Platform:
|
|
packages.update(
|
|
gather_recursive_requirements(platform.value, already_checked_domains)
|
|
)
|
|
# Add go2rtc requirements as this file needs the go2rtc integration
|
|
packages.update(gather_recursive_requirements("go2rtc", already_checked_domains))
|
|
|
|
return File(
|
|
_HASSFEST_TEMPLATE.format(
|
|
timeout=timeout,
|
|
required_components_packages=" ".join(sorted(packages)),
|
|
**package_versions,
|
|
),
|
|
config.root / "script/hassfest/docker/Dockerfile",
|
|
)
|
|
|
|
|
|
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
|
|
|
|
package_versions = _get_package_versions(Path("requirements.txt"), {"uv"})
|
|
package_versions |= _get_package_versions(
|
|
Path("requirements_test.txt"), {"pipdeptree", "tqdm"}
|
|
)
|
|
package_versions |= _get_package_versions(
|
|
Path("requirements_test_pre_commit.txt"), {"ruff"}
|
|
)
|
|
|
|
return [
|
|
File(
|
|
DOCKERFILE_TEMPLATE.format(
|
|
timeout=timeout,
|
|
**package_versions,
|
|
go2rtc=GO2RTC_VERSION,
|
|
),
|
|
config.root / "Dockerfile",
|
|
),
|
|
_generate_hassfest_dockerimage(config, timeout, package_versions),
|
|
]
|
|
|
|
|
|
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)
|