2018-01-30 11:30:47 +00:00
|
|
|
"""Module to handle installing requirements."""
|
2021-03-17 16:34:55 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2018-01-30 11:30:47 +00:00
|
|
|
import asyncio
|
2021-04-20 15:40:41 +00:00
|
|
|
from collections.abc import Iterable
|
2018-01-30 11:30:47 +00:00
|
|
|
import os
|
2021-04-20 15:40:41 +00:00
|
|
|
from typing import Any, cast
|
2018-01-30 11:30:47 +00:00
|
|
|
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2019-08-07 22:35:50 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2020-12-19 11:46:27 +00:00
|
|
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
2020-04-08 19:48:20 +00:00
|
|
|
from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration
|
2018-01-30 11:30:47 +00:00
|
|
|
import homeassistant.util.package as pkg_util
|
|
|
|
|
2021-01-18 21:23:25 +00:00
|
|
|
# mypy: disallow-any-generics
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_PIP_LOCK = "pip_lock"
|
|
|
|
DATA_PKG_CACHE = "pkg_cache"
|
2020-04-08 18:42:15 +00:00
|
|
|
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
|
2019-07-31 19:25:30 +00:00
|
|
|
CONSTRAINT_FILE = "package_constraints.txt"
|
2021-03-17 16:34:55 +00:00
|
|
|
DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = {
|
2021-01-14 08:09:08 +00:00
|
|
|
"dhcp": ("dhcp",),
|
2020-10-07 16:30:51 +00:00
|
|
|
"mqtt": ("mqtt",),
|
2019-12-10 08:24:49 +00:00
|
|
|
"ssdp": ("ssdp",),
|
|
|
|
"zeroconf": ("zeroconf", "homekit"),
|
|
|
|
}
|
2018-01-30 11:30:47 +00:00
|
|
|
|
|
|
|
|
2019-08-07 22:35:50 +00:00
|
|
|
class RequirementsNotFound(HomeAssistantError):
|
|
|
|
"""Raised when a component is not found."""
|
|
|
|
|
2021-03-17 16:34:55 +00:00
|
|
|
def __init__(self, domain: str, requirements: list[str]) -> None:
|
2019-08-07 22:35:50 +00:00
|
|
|
"""Initialize a component not found error."""
|
2019-08-23 16:53:33 +00:00
|
|
|
super().__init__(f"Requirements for {domain} not found: {requirements}.")
|
2019-08-07 22:35:50 +00:00
|
|
|
self.domain = domain
|
|
|
|
self.requirements = requirements
|
|
|
|
|
|
|
|
|
|
|
|
async def async_get_integration_with_requirements(
|
2021-03-17 16:34:55 +00:00
|
|
|
hass: HomeAssistant, domain: str, done: set[str] | None = None
|
2019-08-07 22:35:50 +00:00
|
|
|
) -> Integration:
|
2019-12-10 08:24:49 +00:00
|
|
|
"""Get an integration with all requirements installed, including the dependencies.
|
2019-08-07 22:35:50 +00:00
|
|
|
|
|
|
|
This can raise IntegrationNotFound if manifest or integration
|
|
|
|
is invalid, RequirementNotFound if there was some type of
|
|
|
|
failure to install requirements.
|
|
|
|
"""
|
2019-12-05 18:40:05 +00:00
|
|
|
if done is None:
|
|
|
|
done = {domain}
|
|
|
|
else:
|
|
|
|
done.add(domain)
|
|
|
|
|
2019-08-07 22:35:50 +00:00
|
|
|
integration = await async_get_integration(hass, domain)
|
|
|
|
|
2019-10-31 18:39:26 +00:00
|
|
|
if hass.config.skip_pip:
|
2019-08-07 22:35:50 +00:00
|
|
|
return integration
|
|
|
|
|
2020-04-08 18:42:15 +00:00
|
|
|
cache = hass.data.get(DATA_INTEGRATIONS_WITH_REQS)
|
|
|
|
if cache is None:
|
|
|
|
cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {}
|
|
|
|
|
2021-03-17 16:34:55 +00:00
|
|
|
int_or_evt: Integration | asyncio.Event | None | UndefinedType = cache.get(
|
2020-12-19 11:46:27 +00:00
|
|
|
domain, UNDEFINED
|
|
|
|
)
|
2020-04-08 18:42:15 +00:00
|
|
|
|
|
|
|
if isinstance(int_or_evt, asyncio.Event):
|
|
|
|
await int_or_evt.wait()
|
2021-04-22 20:32:38 +00:00
|
|
|
|
2020-12-19 11:46:27 +00:00
|
|
|
int_or_evt = cache.get(domain, UNDEFINED)
|
2020-04-08 18:42:15 +00:00
|
|
|
|
2020-12-19 11:46:27 +00:00
|
|
|
# When we have waited and it's UNDEFINED, it doesn't exist
|
2020-04-08 18:42:15 +00:00
|
|
|
# We don't cache that it doesn't exist, or else people can't fix it
|
|
|
|
# and then restart, because their config will never be valid.
|
2020-12-19 11:46:27 +00:00
|
|
|
if int_or_evt is UNDEFINED:
|
2020-04-08 18:42:15 +00:00
|
|
|
raise IntegrationNotFound(domain)
|
|
|
|
|
2020-12-19 11:46:27 +00:00
|
|
|
if int_or_evt is not UNDEFINED:
|
2020-04-08 18:42:15 +00:00
|
|
|
return cast(Integration, int_or_evt)
|
|
|
|
|
|
|
|
event = cache[domain] = asyncio.Event()
|
|
|
|
|
2021-04-22 20:32:38 +00:00
|
|
|
try:
|
|
|
|
await _async_process_integration(hass, integration, done)
|
2021-04-25 00:39:24 +00:00
|
|
|
except Exception:
|
2021-04-22 20:32:38 +00:00
|
|
|
del cache[domain]
|
|
|
|
event.set()
|
|
|
|
raise
|
|
|
|
|
|
|
|
cache[domain] = integration
|
|
|
|
event.set()
|
|
|
|
return integration
|
|
|
|
|
|
|
|
|
|
|
|
async def _async_process_integration(
|
|
|
|
hass: HomeAssistant, integration: Integration, done: set[str]
|
|
|
|
) -> None:
|
|
|
|
"""Process an integration and requirements."""
|
2019-10-31 18:39:26 +00:00
|
|
|
if integration.requirements:
|
|
|
|
await async_process_requirements(
|
|
|
|
hass, integration.domain, integration.requirements
|
|
|
|
)
|
|
|
|
|
2019-12-05 18:40:05 +00:00
|
|
|
deps_to_check = [
|
|
|
|
dep
|
2019-12-10 08:24:49 +00:00
|
|
|
for dep in integration.dependencies + integration.after_dependencies
|
2019-12-05 18:40:05 +00:00
|
|
|
if dep not in done
|
|
|
|
]
|
2019-11-02 00:21:50 +00:00
|
|
|
|
2019-12-10 08:24:49 +00:00
|
|
|
for check_domain, to_check in DISCOVERY_INTEGRATIONS.items():
|
|
|
|
if (
|
|
|
|
check_domain not in done
|
|
|
|
and check_domain not in deps_to_check
|
|
|
|
and any(check in integration.manifest for check in to_check)
|
|
|
|
):
|
|
|
|
deps_to_check.append(check_domain)
|
|
|
|
|
2021-04-22 20:32:38 +00:00
|
|
|
if not deps_to_check:
|
|
|
|
return
|
2019-08-07 22:35:50 +00:00
|
|
|
|
2021-04-22 20:32:38 +00:00
|
|
|
results = await asyncio.gather(
|
|
|
|
*[
|
|
|
|
async_get_integration_with_requirements(hass, dep, done)
|
|
|
|
for dep in deps_to_check
|
|
|
|
],
|
|
|
|
return_exceptions=True,
|
|
|
|
)
|
|
|
|
for result in results:
|
|
|
|
if not isinstance(result, BaseException):
|
|
|
|
continue
|
|
|
|
if not isinstance(result, IntegrationNotFound) or not (
|
|
|
|
not integration.is_built_in
|
|
|
|
and result.domain in integration.after_dependencies
|
|
|
|
):
|
|
|
|
raise result
|
2019-08-07 22:35:50 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_process_requirements(
|
2021-03-17 16:34:55 +00:00
|
|
|
hass: HomeAssistant, name: str, requirements: list[str]
|
2019-08-07 22:35:50 +00:00
|
|
|
) -> None:
|
2018-01-30 11:30:47 +00:00
|
|
|
"""Install the requirements for a component or platform.
|
|
|
|
|
2019-08-07 22:35:50 +00:00
|
|
|
This method is a coroutine. It will raise RequirementsNotFound
|
|
|
|
if an requirement can't be satisfied.
|
2018-01-30 11:30:47 +00:00
|
|
|
"""
|
|
|
|
pip_lock = hass.data.get(DATA_PIP_LOCK)
|
|
|
|
if pip_lock is None:
|
2019-05-23 04:09:59 +00:00
|
|
|
pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock()
|
2018-01-30 11:30:47 +00:00
|
|
|
|
2019-06-04 18:04:20 +00:00
|
|
|
kwargs = pip_kwargs(hass.config.config_dir)
|
2018-01-30 11:30:47 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async with pip_lock:
|
2018-01-30 11:30:47 +00:00
|
|
|
for req in requirements:
|
2019-05-26 18:58:42 +00:00
|
|
|
if pkg_util.is_installed(req):
|
2018-08-28 10:52:18 +00:00
|
|
|
continue
|
|
|
|
|
2021-03-17 16:34:55 +00:00
|
|
|
def _install(req: str, kwargs: dict[str, Any]) -> bool:
|
2020-08-05 12:58:19 +00:00
|
|
|
"""Install requirement."""
|
|
|
|
return pkg_util.install_package(req, **kwargs)
|
|
|
|
|
|
|
|
ret = await hass.async_add_executor_job(_install, req, kwargs)
|
2018-08-28 10:52:18 +00:00
|
|
|
|
2018-01-30 11:30:47 +00:00
|
|
|
if not ret:
|
2019-08-07 22:35:50 +00:00
|
|
|
raise RequirementsNotFound(name, [req])
|
2018-01-30 11:30:47 +00:00
|
|
|
|
|
|
|
|
2021-03-17 16:34:55 +00:00
|
|
|
def pip_kwargs(config_dir: str | None) -> dict[str, Any]:
|
2018-01-30 11:30:47 +00:00
|
|
|
"""Return keyword arguments for PIP install."""
|
2019-06-01 08:04:12 +00:00
|
|
|
is_docker = pkg_util.is_docker_env()
|
2018-01-30 11:30:47 +00:00
|
|
|
kwargs = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE),
|
|
|
|
"no_cache_dir": is_docker,
|
2018-01-30 11:30:47 +00:00
|
|
|
}
|
2019-07-31 19:25:30 +00:00
|
|
|
if "WHEELS_LINKS" in os.environ:
|
|
|
|
kwargs["find_links"] = os.environ["WHEELS_LINKS"]
|
|
|
|
if not (config_dir is None or pkg_util.is_virtual_env()) and not is_docker:
|
|
|
|
kwargs["target"] = os.path.join(config_dir, "deps")
|
2018-01-30 11:30:47 +00:00
|
|
|
return kwargs
|