Generate requirements per supported architecture (#115708)
* Generate requirements per supported architecture * Don't store wheels requirements in the repo * Dry run * Set Python version * Install base packages * Fix * Fix * Fix * Fix typo Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Genarate requirements_all_pytest.txt * Fix hassfest * Reenable building wheels * Remove unneeded code * Address review comment * Fix lying comment * Add tests, address review comments * Deduplicate * Fix file name * Add comment --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/115991/head
parent
124eca4d53
commit
2caca7fbe3
|
@ -97,7 +97,8 @@ jobs:
|
||||||
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
||||||
hashFiles('requirements.txt') }}-${{
|
hashFiles('requirements.txt') }}-${{
|
||||||
hashFiles('requirements_all.txt') }}-${{
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT
|
hashFiles('homeassistant/package_constraints.txt') }}-${{
|
||||||
|
hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT
|
||||||
- name: Generate partial pre-commit restore key
|
- name: Generate partial pre-commit restore key
|
||||||
id: generate_pre-commit_cache_key
|
id: generate_pre-commit_cache_key
|
||||||
run: >-
|
run: >-
|
||||||
|
@ -497,8 +498,9 @@ jobs:
|
||||||
python --version
|
python --version
|
||||||
pip install "$(grep '^uv' < requirements_test.txt)"
|
pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
uv pip install -U "pip>=21.3.1" setuptools wheel
|
uv pip install -U "pip>=21.3.1" setuptools wheel
|
||||||
uv pip install -r requirements_all.txt
|
uv pip install -r requirements.txt
|
||||||
uv pip install "$(grep 'python-gammu' < requirements_all.txt | sed -e 's|# python-gammu|python-gammu|g')"
|
python -m script.gen_requirements_all ci
|
||||||
|
uv pip install -r requirements_all_pytest.txt
|
||||||
uv pip install -r requirements_test.txt
|
uv pip install -r requirements_test.txt
|
||||||
uv pip install -e . --config-settings editable_mode=compat
|
uv pip install -e . --config-settings editable_mode=compat
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,10 @@ on:
|
||||||
- "homeassistant/package_constraints.txt"
|
- "homeassistant/package_constraints.txt"
|
||||||
- "requirements_all.txt"
|
- "requirements_all.txt"
|
||||||
- "requirements.txt"
|
- "requirements.txt"
|
||||||
|
- "script/gen_requirements_all.py"
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: "3.12"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||||
|
@ -30,6 +34,21 @@ jobs:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.3
|
uses: actions/checkout@v4.1.3
|
||||||
|
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v5.1.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Create Python virtual environment
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
python --version
|
||||||
|
pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Get information
|
- name: Get information
|
||||||
id: info
|
id: info
|
||||||
uses: home-assistant/actions/helpers/info@master
|
uses: home-assistant/actions/helpers/info@master
|
||||||
|
@ -76,6 +95,17 @@ jobs:
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Generate requirements
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
|
- name: Upload requirements_all_wheels
|
||||||
|
uses: actions/upload-artifact@v4.3.1
|
||||||
|
with:
|
||||||
|
name: requirements_all_wheels
|
||||||
|
path: ./requirements_all_wheels_*.txt
|
||||||
|
|
||||||
core:
|
core:
|
||||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
|
@ -138,30 +168,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: (Un)comment packages
|
- name: Download requirements_all_wheels
|
||||||
run: |
|
uses: actions/download-artifact@v4.1.4
|
||||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
with:
|
||||||
for requirement_file in ${requirement_files}; do
|
name: requirements_all_wheels
|
||||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
|
||||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
|
||||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
|
||||||
sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file}
|
|
||||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
|
||||||
|
|
||||||
# Some packages are not buildable on armhf anymore
|
|
||||||
if [ "${{ matrix.arch }}" = "armhf" ]; then
|
|
||||||
|
|
||||||
# Pandas has issues building on armhf, it is expected they
|
|
||||||
# will drop the platform in the near future (they consider it
|
|
||||||
# "flimsy" on 386). The following packages depend on pandas,
|
|
||||||
# so we comment them out.
|
|
||||||
sed -i "s|env-canada|# env-canada|g" ${requirement_file}
|
|
||||||
sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file}
|
|
||||||
sed -i "s|pyezviz|# pyezviz|g" ${requirement_file}
|
|
||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file}
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Split requirements all
|
- name: Split requirements all
|
||||||
run: |
|
run: |
|
||||||
|
@ -169,7 +179,7 @@ jobs:
|
||||||
# This is to prevent the build from running out of memory when
|
# This is to prevent the build from running out of memory when
|
||||||
# resolving packages on 32-bits systems (like armhf, armv7).
|
# resolving packages on 32-bits systems (like armhf, armv7).
|
||||||
|
|
||||||
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt
|
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
|
||||||
|
|
||||||
- name: Create requirements for cython<3
|
- name: Create requirements for cython<3
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -17,7 +17,10 @@ from typing import Any
|
||||||
from homeassistant.util.yaml.loader import load_yaml
|
from homeassistant.util.yaml.loader import load_yaml
|
||||||
from script.hassfest.model import Integration
|
from script.hassfest.model import Integration
|
||||||
|
|
||||||
COMMENT_REQUIREMENTS = (
|
# Requirements which can't be installed on all systems because they rely on additional
|
||||||
|
# system packages. Requirements listed in EXCLUDED_REQUIREMENTS_ALL will be commented-out
|
||||||
|
# in requirements_all.txt and requirements_test_all.txt.
|
||||||
|
EXCLUDED_REQUIREMENTS_ALL = {
|
||||||
"atenpdu", # depends on pysnmp which is not maintained at this time
|
"atenpdu", # depends on pysnmp which is not maintained at this time
|
||||||
"avea", # depends on bluepy
|
"avea", # depends on bluepy
|
||||||
"avion",
|
"avion",
|
||||||
|
@ -36,10 +39,39 @@ COMMENT_REQUIREMENTS = (
|
||||||
"pyuserinput",
|
"pyuserinput",
|
||||||
"tensorflow",
|
"tensorflow",
|
||||||
"tf-models-official",
|
"tf-models-official",
|
||||||
)
|
}
|
||||||
|
|
||||||
COMMENT_REQUIREMENTS_NORMALIZED = {
|
# Requirements excluded by EXCLUDED_REQUIREMENTS_ALL which should be included when
|
||||||
commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS
|
# building integration wheels for all architectures.
|
||||||
|
INCLUDED_REQUIREMENTS_WHEELS = {
|
||||||
|
"decora-wifi",
|
||||||
|
"evdev",
|
||||||
|
"pycups",
|
||||||
|
"python-gammu",
|
||||||
|
"pyuserinput",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Requirements to exclude or include when running github actions.
|
||||||
|
# Requirements listed in "exclude" will be commented-out in
|
||||||
|
# requirements_all_{action}.txt
|
||||||
|
# Requirements listed in "include" must be listed in EXCLUDED_REQUIREMENTS_CI, and
|
||||||
|
# will be included in requirements_all_{action}.txt
|
||||||
|
|
||||||
|
OVERRIDDEN_REQUIREMENTS_ACTIONS = {
|
||||||
|
"pytest": {"exclude": set(), "include": {"python-gammu"}},
|
||||||
|
"wheels_aarch64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
|
||||||
|
# Pandas has issues building on armhf, it is expected they
|
||||||
|
# will drop the platform in the near future (they consider it
|
||||||
|
# "flimsy" on 386). The following packages depend on pandas,
|
||||||
|
# so we comment them out.
|
||||||
|
"wheels_armhf": {
|
||||||
|
"exclude": {"env-canada", "noaa-coops", "pyezviz", "pykrakenapi"},
|
||||||
|
"include": INCLUDED_REQUIREMENTS_WHEELS,
|
||||||
|
},
|
||||||
|
"wheels_armv7": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
|
||||||
|
"wheels_amd64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
|
||||||
|
"wheels_i386": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
|
||||||
}
|
}
|
||||||
|
|
||||||
IGNORE_PIN = ("colorlog>2.1,<3", "urllib3")
|
IGNORE_PIN = ("colorlog>2.1,<3", "urllib3")
|
||||||
|
@ -254,6 +286,12 @@ def gather_recursive_requirements(
|
||||||
return reqs
|
return reqs
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_package_name(package_name: str) -> str:
|
||||||
|
"""Normalize a package name."""
|
||||||
|
# pipdeptree needs lowercase and dash instead of underscore or period as separator
|
||||||
|
return package_name.lower().replace("_", "-").replace(".", "-")
|
||||||
|
|
||||||
|
|
||||||
def normalize_package_name(requirement: str) -> str:
|
def normalize_package_name(requirement: str) -> str:
|
||||||
"""Return a normalized package name from a requirement string."""
|
"""Return a normalized package name from a requirement string."""
|
||||||
# This function is also used in hassfest.
|
# This function is also used in hassfest.
|
||||||
|
@ -262,12 +300,24 @@ def normalize_package_name(requirement: str) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# pipdeptree needs lowercase and dash instead of underscore or period as separator
|
# pipdeptree needs lowercase and dash instead of underscore or period as separator
|
||||||
return match.group(1).lower().replace("_", "-").replace(".", "-")
|
return _normalize_package_name(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
def comment_requirement(req: str) -> bool:
|
def comment_requirement(req: str) -> bool:
|
||||||
"""Comment out requirement. Some don't install on all systems."""
|
"""Comment out requirement. Some don't install on all systems."""
|
||||||
return normalize_package_name(req) in COMMENT_REQUIREMENTS_NORMALIZED
|
return normalize_package_name(req) in EXCLUDED_REQUIREMENTS_ALL
|
||||||
|
|
||||||
|
|
||||||
|
def process_action_requirement(req: str, action: str) -> str:
|
||||||
|
"""Process requirement for a specific github action."""
|
||||||
|
normalized_package_name = normalize_package_name(req)
|
||||||
|
if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["exclude"]:
|
||||||
|
return f"# {req}"
|
||||||
|
if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["include"]:
|
||||||
|
return req
|
||||||
|
if normalized_package_name in EXCLUDED_REQUIREMENTS_ALL:
|
||||||
|
return f"# {req}"
|
||||||
|
return req
|
||||||
|
|
||||||
|
|
||||||
def gather_modules() -> dict[str, list[str]] | None:
|
def gather_modules() -> dict[str, list[str]] | None:
|
||||||
|
@ -353,6 +403,16 @@ def generate_requirements_list(reqs: dict[str, list[str]]) -> str:
|
||||||
return "".join(output)
|
return "".join(output)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_action_requirements_list(reqs: dict[str, list[str]], action: str) -> str:
|
||||||
|
"""Generate a pip file based on requirements."""
|
||||||
|
output = []
|
||||||
|
for pkg, requirements in sorted(reqs.items(), key=itemgetter(0)):
|
||||||
|
output.extend(f"\n# {req}" for req in sorted(requirements))
|
||||||
|
processed_pkg = process_action_requirement(pkg, action)
|
||||||
|
output.append(f"\n{processed_pkg}\n")
|
||||||
|
return "".join(output)
|
||||||
|
|
||||||
|
|
||||||
def requirements_output() -> str:
|
def requirements_output() -> str:
|
||||||
"""Generate output for requirements."""
|
"""Generate output for requirements."""
|
||||||
output = [
|
output = [
|
||||||
|
@ -379,6 +439,18 @@ def requirements_all_output(reqs: dict[str, list[str]]) -> str:
|
||||||
return "".join(output)
|
return "".join(output)
|
||||||
|
|
||||||
|
|
||||||
|
def requirements_all_action_output(reqs: dict[str, list[str]], action: str) -> str:
|
||||||
|
"""Generate output for requirements_all_{action}."""
|
||||||
|
output = [
|
||||||
|
f"# Home Assistant Core, full dependency set for {action}\n",
|
||||||
|
GENERATED_MESSAGE,
|
||||||
|
"-r requirements.txt\n",
|
||||||
|
]
|
||||||
|
output.append(generate_action_requirements_list(reqs, action))
|
||||||
|
|
||||||
|
return "".join(output)
|
||||||
|
|
||||||
|
|
||||||
def requirements_test_all_output(reqs: dict[str, list[str]]) -> str:
|
def requirements_test_all_output(reqs: dict[str, list[str]]) -> str:
|
||||||
"""Generate output for test_requirements."""
|
"""Generate output for test_requirements."""
|
||||||
output = [
|
output = [
|
||||||
|
@ -459,7 +531,7 @@ def diff_file(filename: str, content: str) -> list[str]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(validate: bool) -> int:
|
def main(validate: bool, ci: bool) -> int:
|
||||||
"""Run the script."""
|
"""Run the script."""
|
||||||
if not os.path.isfile("requirements_all.txt"):
|
if not os.path.isfile("requirements_all.txt"):
|
||||||
print("Run this from HA root dir")
|
print("Run this from HA root dir")
|
||||||
|
@ -472,17 +544,28 @@ def main(validate: bool) -> int:
|
||||||
|
|
||||||
reqs_file = requirements_output()
|
reqs_file = requirements_output()
|
||||||
reqs_all_file = requirements_all_output(data)
|
reqs_all_file = requirements_all_output(data)
|
||||||
|
reqs_all_action_files = {
|
||||||
|
action: requirements_all_action_output(data, action)
|
||||||
|
for action in OVERRIDDEN_REQUIREMENTS_ACTIONS
|
||||||
|
}
|
||||||
reqs_test_all_file = requirements_test_all_output(data)
|
reqs_test_all_file = requirements_test_all_output(data)
|
||||||
|
# Always calling requirements_pre_commit_output is intentional to ensure
|
||||||
|
# the code is called by the pre-commit hooks.
|
||||||
reqs_pre_commit_file = requirements_pre_commit_output()
|
reqs_pre_commit_file = requirements_pre_commit_output()
|
||||||
constraints = gather_constraints()
|
constraints = gather_constraints()
|
||||||
|
|
||||||
files = (
|
files = [
|
||||||
("requirements.txt", reqs_file),
|
("requirements.txt", reqs_file),
|
||||||
("requirements_all.txt", reqs_all_file),
|
("requirements_all.txt", reqs_all_file),
|
||||||
("requirements_test_pre_commit.txt", reqs_pre_commit_file),
|
("requirements_test_pre_commit.txt", reqs_pre_commit_file),
|
||||||
("requirements_test_all.txt", reqs_test_all_file),
|
("requirements_test_all.txt", reqs_test_all_file),
|
||||||
("homeassistant/package_constraints.txt", constraints),
|
("homeassistant/package_constraints.txt", constraints),
|
||||||
)
|
]
|
||||||
|
if ci:
|
||||||
|
files.extend(
|
||||||
|
(f"requirements_all_{action}.txt", reqs_all_file)
|
||||||
|
for action, reqs_all_file in reqs_all_action_files.items()
|
||||||
|
)
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
errors = []
|
errors = []
|
||||||
|
@ -511,4 +594,5 @@ def main(validate: bool) -> int:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
_VAL = sys.argv[-1] == "validate"
|
_VAL = sys.argv[-1] == "validate"
|
||||||
sys.exit(main(_VAL))
|
_CI = sys.argv[-1] == "ci"
|
||||||
|
sys.exit(main(_VAL, _CI))
|
||||||
|
|
|
@ -15,13 +15,13 @@ from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
import homeassistant.util.package as pkg_util
|
import homeassistant.util.package as pkg_util
|
||||||
from script.gen_requirements_all import COMMENT_REQUIREMENTS, normalize_package_name
|
from script.gen_requirements_all import (
|
||||||
|
EXCLUDED_REQUIREMENTS_ALL,
|
||||||
|
normalize_package_name,
|
||||||
|
)
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
|
|
||||||
IGNORE_PACKAGES = {
|
|
||||||
commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS
|
|
||||||
}
|
|
||||||
PACKAGE_REGEX = re.compile(
|
PACKAGE_REGEX = re.compile(
|
||||||
r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$"
|
r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$"
|
||||||
)
|
)
|
||||||
|
@ -116,7 +116,7 @@ def validate_requirements(integration: Integration) -> None:
|
||||||
f"Failed to normalize package name from requirement {req}",
|
f"Failed to normalize package name from requirement {req}",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if package in IGNORE_PACKAGES:
|
if package in EXCLUDED_REQUIREMENTS_ALL:
|
||||||
continue
|
continue
|
||||||
integration_requirements.add(req)
|
integration_requirements.add(req)
|
||||||
integration_packages.add(package)
|
integration_packages.add(package)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for scripts."""
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""Tests for the gen_requirements_all script."""
|
||||||
|
|
||||||
|
from script import gen_requirements_all
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_normalized() -> None:
|
||||||
|
"""Test override lists are using normalized package names."""
|
||||||
|
for req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL:
|
||||||
|
assert req == gen_requirements_all._normalize_package_name(req)
|
||||||
|
for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS:
|
||||||
|
assert req == gen_requirements_all._normalize_package_name(req)
|
||||||
|
for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values():
|
||||||
|
for req in overrides["exclude"]:
|
||||||
|
assert req == gen_requirements_all._normalize_package_name(req)
|
||||||
|
for req in overrides["include"]:
|
||||||
|
assert req == gen_requirements_all._normalize_package_name(req)
|
||||||
|
|
||||||
|
|
||||||
|
def test_include_overrides_subsets() -> None:
|
||||||
|
"""Test packages in include override lists are present in the exclude list."""
|
||||||
|
for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS:
|
||||||
|
assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL
|
||||||
|
for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values():
|
||||||
|
for req in overrides["include"]:
|
||||||
|
assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL
|
Loading…
Reference in New Issue