diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 3a1081e4b87..ca34a4bbae4 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -47,6 +47,9 @@ def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: kwargs = { 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) } - if not (config_dir is None or pkg_util.is_virtual_env()): + 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 pkg_util.is_docker_env(): kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 3a34ab0a365..272a097b24c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -6,6 +6,7 @@ from subprocess import PIPE, Popen import sys from typing import Optional from urllib.parse import urlparse +from pathlib import Path import pkg_resources from importlib_metadata import version, PackageNotFoundError @@ -21,6 +22,11 @@ def is_virtual_env() -> bool: hasattr(sys, 'real_prefix')) +def is_docker_env() -> bool: + """Return True if we run in a docker env.""" + return Path("/.dockerenv").exists() + + def is_installed(package: str) -> bool: """Check if a package is installed and will be loaded when we import it. @@ -42,7 +48,8 @@ def is_installed(package: str) -> bool: def install_package(package: str, upgrade: bool = True, target: Optional[str] = None, - constraints: Optional[str] = None) -> bool: + constraints: Optional[str] = None, + find_links: Optional[str] = None) -> bool: """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successful. @@ -55,6 +62,8 @@ def install_package(package: str, upgrade: bool = True, args.append('--upgrade') if constraints is not None: args += ['--constraint', constraints] + if find_links is not None: + args += ['--find-links', find_links] if target: assert not is_virtual_env() # This only works if not running in venv diff --git a/tests/test_requirements.py b/tests/test_requirements.py index c061e37ca0a..35264c2e1b4 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -27,9 +27,10 @@ class TestRequirements: @patch('os.path.dirname') @patch('homeassistant.util.package.is_virtual_env', return_value=True) + @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_dirname): + self, mock_install, mock_venv, mock_denv, mock_dirname): """Test requirement installed in virtual environment.""" mock_venv.return_value = True mock_dirname.return_value = 'ha_package_path' @@ -45,9 +46,10 @@ class TestRequirements: @patch('os.path.dirname') @patch('homeassistant.util.package.is_virtual_env', return_value=False) + @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_dirname): + self, mock_install, mock_venv, mock_denv, mock_dirname): """Test requirement installed in deps directory.""" mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False @@ -77,3 +79,60 @@ async def test_install_existing_package(hass): hass, 'test_component', ['hello==1.0.0']) assert len(mock_inst.mock_calls) == 0 + + +async def test_install_with_wheels_index(hass): + """Test an install attempt with wheels index URL.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule('comp', requirements=['hello==1.0.0'])) + + with patch( + 'homeassistant.util.package.is_installed', return_value=False + ), \ + patch( + 'homeassistant.util.package.is_docker_env', return_value=True + ), \ + patch( + 'homeassistant.util.package.install_package' + ) as mock_inst, \ + patch.dict( + os.environ, {'WHEELS_LINKS': "https://wheels.hass.io/test"} + ), \ + patch( + 'os.path.dirname' + ) as mock_dir: + mock_dir.return_value = 'ha_package_path' + assert await setup.async_setup_component(hass, 'comp', {}) + assert 'comp' in hass.config.components + print(mock_inst.call_args) + assert mock_inst.call_args == call( + 'hello==1.0.0', find_links="https://wheels.hass.io/test", + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + +async def test_install_on_docker(hass): + """Test an install attempt on an docker system env.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule('comp', requirements=['hello==1.0.0'])) + + with patch( + 'homeassistant.util.package.is_installed', return_value=False + ), \ + patch( + 'homeassistant.util.package.is_docker_env', return_value=True + ), \ + patch( + 'homeassistant.util.package.install_package' + ) as mock_inst, \ + patch( + 'os.path.dirname' + ) as mock_dir: + mock_dir.return_value = 'ha_package_path' + assert await setup.async_setup_component(hass, 'comp', {}) + assert 'comp' in hass.config.components + print(mock_inst.call_args) + assert mock_inst.call_args == call( + 'hello==1.0.0', + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 41af56265d2..3751c056907 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -167,6 +167,23 @@ def test_install_constraint(mock_sys, mock_popen, mock_env_copy, mock_venv): assert mock_popen.return_value.communicate.call_count == 1 +def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv): + """Test install with find-links on not installed package.""" + env = mock_env_copy() + link = 'https://wheels-repository' + assert package.install_package( + TEST_NEW_REQ, False, find_links=link) + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call([ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ, '--find-links', link + ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + @asyncio.coroutine def test_async_get_user_site(mock_env_copy): """Test async get user site directory."""