mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
Optimize hassfest image (#124855)
* Optimize hassfest docker image * Adjust CI * Use dynamic uv version * Remove workaround
This commit is contained in:
parent
98cbd7d8da
commit
bd2be0a763
10
.github/workflows/builder.yml
vendored
10
.github/workflows/builder.yml
vendored
@ -491,7 +491,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
attestations: write
|
attestations: write
|
||||||
id-token: write
|
id-token: write
|
||||||
needs: ["init", "build_base"]
|
needs: ["init"]
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
env:
|
env:
|
||||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||||
@ -510,8 +510,8 @@ jobs:
|
|||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||||
with:
|
with:
|
||||||
context: ./script/hassfest/docker
|
context: . # So action will not pull the repository again
|
||||||
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
load: true
|
load: true
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||||
|
|
||||||
@ -523,8 +523,8 @@ jobs:
|
|||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||||
with:
|
with:
|
||||||
context: ./script/hassfest/docker
|
context: . # So action will not pull the repository again
|
||||||
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
"""Generate and validate the dockerfile."""
|
"""Generate and validate the dockerfile."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
|
from homeassistant.const import Platform
|
||||||
from homeassistant.util import executor, thread
|
from homeassistant.util import executor, thread
|
||||||
|
from script.gen_requirements_all import gather_recursive_requirements
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR
|
from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR
|
||||||
@ -20,7 +25,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv=={uv_version}
|
RUN pip3 install uv=={uv}
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
@ -61,30 +66,105 @@ COPY rootfs /
|
|||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
|
||||||
|
#
|
||||||
|
# To update, run python3 -m script.hassfest -p docker
|
||||||
|
FROM python:alpine3.20
|
||||||
|
|
||||||
def _get_uv_version() -> str:
|
ENV \
|
||||||
with open("requirements_test.txt") as fp:
|
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"
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:{uv} /uv /bin/uv
|
||||||
|
|
||||||
|
COPY . /usr/src/homeassistant
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Required for PyTurboJPEG
|
||||||
|
apk add --no-cache libturbojpeg \
|
||||||
|
&& cd /usr/src/homeassistant \
|
||||||
|
&& uv pip install \
|
||||||
|
--no-build \
|
||||||
|
--no-cache \
|
||||||
|
-c homeassistant/package_constraints.txt \
|
||||||
|
-r 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: str, packages: set[str]) -> dict[str, str]:
|
||||||
|
package_versions: dict[str, str] = {}
|
||||||
|
with open(file, encoding="UTF-8") as fp:
|
||||||
for _, line in enumerate(fp):
|
for _, line in enumerate(fp):
|
||||||
|
if package_versions.keys() == packages:
|
||||||
|
return package_versions
|
||||||
|
|
||||||
if match := PACKAGE_REGEX.match(line):
|
if match := PACKAGE_REGEX.match(line):
|
||||||
pkg, sep, version = match.groups()
|
pkg, sep, version = match.groups()
|
||||||
|
|
||||||
if pkg != "uv":
|
if pkg not in packages:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if sep != "==" or not version:
|
if sep != "==" or not version:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Requirement uv need to be pinned "uv==<version>".'
|
f'Requirement {pkg} need to be pinned "{pkg}==<version>".'
|
||||||
)
|
)
|
||||||
|
|
||||||
for part in version.split(";", 1)[0].split(","):
|
for part in version.split(";", 1)[0].split(","):
|
||||||
version_part = PIP_VERSION_RANGE_SEPARATOR.match(part)
|
version_part = PIP_VERSION_RANGE_SEPARATOR.match(part)
|
||||||
if version_part:
|
if version_part:
|
||||||
return version_part.group(2)
|
package_versions[pkg] = version_part.group(2)
|
||||||
|
break
|
||||||
|
|
||||||
raise RuntimeError("Invalid uv requirement in requirements_test.txt")
|
if package_versions.keys() == packages:
|
||||||
|
return package_versions
|
||||||
|
|
||||||
|
raise RuntimeError("At least one package was not found in the requirements file.")
|
||||||
|
|
||||||
|
|
||||||
def _generate_dockerfile() -> str:
|
@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)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = (
|
timeout = (
|
||||||
core.STOPPING_STAGE_SHUTDOWN_TIMEOUT
|
core.STOPPING_STAGE_SHUTDOWN_TIMEOUT
|
||||||
+ core.STOP_STAGE_SHUTDOWN_TIMEOUT
|
+ core.STOP_STAGE_SHUTDOWN_TIMEOUT
|
||||||
@ -93,27 +173,39 @@ def _generate_dockerfile() -> str:
|
|||||||
+ executor.EXECUTOR_SHUTDOWN_TIMEOUT
|
+ executor.EXECUTOR_SHUTDOWN_TIMEOUT
|
||||||
+ thread.THREADING_SHUTDOWN_TIMEOUT
|
+ thread.THREADING_SHUTDOWN_TIMEOUT
|
||||||
+ 10
|
+ 10
|
||||||
|
) * 1000
|
||||||
|
|
||||||
|
package_versions = _get_package_versions(
|
||||||
|
"requirements_test.txt", {"pipdeptree", "tqdm", "uv"}
|
||||||
)
|
)
|
||||||
return DOCKERFILE_TEMPLATE.format(
|
package_versions |= _get_package_versions(
|
||||||
timeout=timeout * 1000, uv_version=_get_uv_version()
|
"requirements_test_pre_commit.txt", {"ruff"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
File(
|
||||||
|
DOCKERFILE_TEMPLATE.format(timeout=timeout, **package_versions),
|
||||||
|
config.root / "Dockerfile",
|
||||||
|
),
|
||||||
|
_generate_hassfest_dockerimage(config, timeout, package_versions),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
"""Validate dockerfile."""
|
"""Validate dockerfile."""
|
||||||
dockerfile_content = _generate_dockerfile()
|
docker_files = _generate_files(config)
|
||||||
config.cache["dockerfile"] = dockerfile_content
|
config.cache["docker"] = docker_files
|
||||||
|
|
||||||
dockerfile_path = config.root / "Dockerfile"
|
for file in docker_files:
|
||||||
if dockerfile_path.read_text() != dockerfile_content:
|
if file.content != file.path.read_text():
|
||||||
config.add_error(
|
config.add_error(
|
||||||
"docker",
|
"docker",
|
||||||
"File Dockerfile is not up to date. Run python3 -m script.hassfest",
|
f"File {file.path} is not up to date. Run python3 -m script.hassfest",
|
||||||
fixable=True,
|
fixable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate(integrations: dict[str, Integration], config: Config) -> None:
|
def generate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
"""Generate dockerfile."""
|
"""Generate dockerfile."""
|
||||||
dockerfile_path = config.root / "Dockerfile"
|
for file in _generate_files(config):
|
||||||
dockerfile_path.write_text(config.cache["dockerfile"])
|
file.path.write_text(file.content)
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
ARG BASE_IMAGE=ghcr.io/home-assistant/home-assistant:beta
|
# Automatically generated by hassfest.
|
||||||
FROM $BASE_IMAGE
|
#
|
||||||
|
# To update, run python3 -m script.hassfest -p docker
|
||||||
|
FROM python:alpine3.20
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
ENV \
|
||||||
|
UV_SYSTEM_PYTHON=true \
|
||||||
|
UV_EXTRA_INDEX_URL="https://wheels.home-assistant.io/musllinux-index/"
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
|
||||||
|
ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"]
|
||||||
|
WORKDIR "/github/workspace"
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.2.27 /uv /bin/uv
|
||||||
|
|
||||||
|
COPY . /usr/src/homeassistant
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
uv pip install stdlib-list==0.10.0 \
|
# Required for PyTurboJPEG
|
||||||
$(grep -e "^pipdeptree" -e "^tqdm" /usr/src/homeassistant/requirements_test.txt) \
|
apk add --no-cache libturbojpeg \
|
||||||
$(grep -e "^ruff" /usr/src/homeassistant/requirements_test_pre_commit.txt)
|
&& cd /usr/src/homeassistant \
|
||||||
|
&& uv pip install \
|
||||||
WORKDIR "/github/workspace"
|
--no-build \
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
--no-cache \
|
||||||
|
-c homeassistant/package_constraints.txt \
|
||||||
|
-r requirements.txt \
|
||||||
|
stdlib-list==0.10.0 pipdeptree==2.23.1 tqdm==4.66.4 ruff==0.6.2 \
|
||||||
|
PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.0 hassil==1.7.4 home-assistant-intents==2024.8.29 mutagen==1.47.0
|
||||||
|
|
||||||
LABEL "name"="hassfest"
|
LABEL "name"="hassfest"
|
||||||
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
|
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
|
||||||
|
8
script/hassfest/docker/Dockerfile.dockerignore
Normal file
8
script/hassfest/docker/Dockerfile.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Ignore everything except the specified files
|
||||||
|
*
|
||||||
|
|
||||||
|
!homeassistant/
|
||||||
|
!requirements.txt
|
||||||
|
!script/
|
||||||
|
script/hassfest/docker/
|
||||||
|
!script/hassfest/docker/entrypoint.sh
|
@ -1,16 +1,18 @@
|
|||||||
#!/usr/bin/env bashio
|
#!/bin/sh
|
||||||
declare -a integrations
|
|
||||||
declare integration_path
|
|
||||||
|
|
||||||
shopt -s globstar nullglob
|
integrations=""
|
||||||
for manifest in **/manifest.json; do
|
integration_path=""
|
||||||
|
|
||||||
|
# Enable recursive globbing using find
|
||||||
|
for manifest in $(find . -name "manifest.json"); do
|
||||||
manifest_path=$(realpath "${manifest}")
|
manifest_path=$(realpath "${manifest}")
|
||||||
integrations+=(--integration-path "${manifest_path%/*}")
|
integrations="$integrations --integration-path ${manifest_path%/*}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ${#integrations[@]} -eq 0 ]]; then
|
if [ -z "$integrations" ]; then
|
||||||
bashio::exit.nok "No integrations found!"
|
echo "Error: No integrations found!"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd /usr/src/homeassistant
|
cd /usr/src/homeassistant || exit 1
|
||||||
exec python3 -m script.hassfest --action validate "${integrations[@]}" "$@"
|
exec python3 -m script.hassfest --action validate $integrations "$@"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user