From cf3c48ffebf7ab68abb31b2ad962ce8d362eec11 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Tue, 9 Aug 2022 15:18:20 +0300 Subject: [PATCH] patch architecture list in runtime (#66) --- package/share/ahriman/settings/ahriman.ini | 2 +- src/ahriman/core/build_tools/sources.py | 26 +++++- src/ahriman/models/package.py | 20 ++++ src/ahriman/models/pkgbuild_patch.py | 92 +++++++++++++++++++ .../ahriman/core/build_tools/test_sources.py | 26 ++++++ tests/ahriman/models/test_package.py | 23 ++++- tests/ahriman/models/test_pkgbuild_patch.py | 61 ++++++++++++ 7 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 src/ahriman/models/pkgbuild_patch.py create mode 100644 tests/ahriman/models/test_pkgbuild_patch.py diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index ab3e3472..fb043370 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -20,7 +20,7 @@ archbuild_flags = build_command = extra-x86_64-build ignore_packages = makechrootpkg_flags = -makepkg_flags = --nocolor +makepkg_flags = --nocolor --ignorearch triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger [repository] diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 8f549d5d..5672c077 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -25,6 +25,7 @@ from typing import List, Optional from ahriman.core.lazy_logging import LazyLogging from ahriman.core.util import check_output, walk from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths @@ -42,6 +43,24 @@ class Sources(LazyLogging): _check_output = check_output + @staticmethod + def extend_architectures(sources_dir: Path, architecture: str) -> None: + """ + extend existing PKGBUILD with repository architecture + + Args: + sources_dir(Path): local path to directory with source files + architecture(str): repository architecture + """ + pkgbuild_path = sources_dir / "PKGBUILD" + if not pkgbuild_path.is_file(): + return + + architectures = Package.supported_architectures(sources_dir) + architectures.add(architecture) + patch = PkgbuildPatch("arch", list(architectures)) + patch.write(pkgbuild_path) + @staticmethod def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: """ @@ -128,10 +147,9 @@ class Sources(LazyLogging): shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True) instance.fetch(sources_dir, package.remote) - if patch is None: - instance.logger.info("no patches found") - return - instance.patch_apply(sources_dir, patch) + if patch is not None: + instance.patch_apply(sources_dir, patch) + instance.extend_architectures(sources_dir, paths.architecture) @staticmethod def patch_create(sources_dir: Path, *pattern: str) -> str: diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index e59cb494..96183717 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -267,6 +267,26 @@ class Package(LazyLogging): packages = set(srcinfo["packages"].keys()) return (depends | makedepends) - packages + @staticmethod + def supported_architectures(path: Path) -> Set[str]: + """ + load supported architectures from package sources + + Args: + path(Path): path to package sources directory + + Returns: + Set[str]: list of package supported architectures + + Raises: + InvalidPackageInfo: if there are parsing errors + """ + srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", exception=None, cwd=path) + srcinfo, errors = parse_srcinfo(srcinfo_source) + if errors: + raise InvalidPackageInfo(errors) + return set(srcinfo.get("arch", [])) + def actual_version(self, paths: RepositoryPaths) -> str: """ additional method to handle VCS package versions diff --git a/src/ahriman/models/pkgbuild_patch.py b/src/ahriman/models/pkgbuild_patch.py new file mode 100644 index 00000000..64c7a5dc --- /dev/null +++ b/src/ahriman/models/pkgbuild_patch.py @@ -0,0 +1,92 @@ +# +# Copyright (c) 2021-2022 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import shlex + +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Union + + +@dataclass(frozen=True) +class PkgbuildPatch: + """ + wrapper for patching PKBGUILDs + + Attributes: + key(str): name of the property in PKGBUILD, e.g. version, url etc + value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string + values + unsafe(bool): if set, value will be not quoted, might break PKGBUILD + """ + + key: str + value: Union[str, List[str]] + unsafe: bool = field(default=False, kw_only=True) + + @property + def is_function(self) -> bool: + """ + parse key and define whether it function or not + + Returns: + bool: True in case if key ends with parentheses and False otherwise + """ + return self.key.endswith("()") + + def quote(self, value: str) -> str: + """ + quote value according to the unsafe flag + + Args: + value(str): value to be quoted + + Returns: + str: quoted string in case if unsafe is False and as is otherwise + """ + return value if self.unsafe else shlex.quote(value) + + def serialize(self) -> str: + """ + serialize key-value pair into PKBGBUILD string. List values will be put inside parentheses. All string + values (including the ones inside list values) will be put inside quotes, no shell variables expanding supported + at the moment + + Returns: + str: serialized key-value pair, print-friendly + """ + if isinstance(self.value, list): # list like + value = " ".join(map(self.quote, self.value)) + return f"""{self.key}=({value})""" + # we suppose that function values are only supported in string-like values + if self.is_function: + return f"{self.key} {self.value}" # no quoting enabled here + return f"""{self.key}={self.quote(self.value)}""" + + def write(self, pkgbuild_path: Path) -> None: + """ + write serialized value into PKGBUILD by specified path + + Args: + pkgbuild_path(Path): path to PKGBUILD file + """ + with pkgbuild_path.open("a") as pkgbuild: + pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end + pkgbuild.write(self.serialize()) + pkgbuild.write("\n") # append new line after the values diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 4348d041..c71b0dfa 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -10,6 +10,30 @@ from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths +def test_extend_architectures(mocker: MockerFixture) -> None: + """ + must update available architecture list + """ + mocker.patch("pathlib.Path.is_file", return_value=True) + archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"}) + write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") + + Sources.extend_architectures(Path("local"), "i686") + archs_mock.assert_called_once_with(Path("local")) + write_mock.assert_called_once_with(Path("local") / "PKGBUILD") + + +def test_extend_architectures_skip(mocker: MockerFixture) -> None: + """ + must skip extending list of the architectures in case if no PKGBUILD file found + """ + mocker.patch("pathlib.Path.is_file", return_value=False) + write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") + + Sources.extend_architectures(Path("local"), "i686") + write_mock.assert_not_called() + + def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must do nothing in case if no branches available @@ -134,10 +158,12 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke mocker.patch("pathlib.Path.is_dir", return_value=False) fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") + architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures") Sources.load(Path("local"), package_ahriman, "patch", repository_paths) fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote) patch_mock.assert_called_once_with(Path("local"), "patch") + architectures_mock.assert_called_once_with(Path("local"), repository_paths.architecture) def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index e1f42f8e..d1b176e4 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -154,7 +154,7 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage def test_dependencies_failed(mocker: MockerFixture) -> None: """ - must raise exception if there are errors during srcinfo load + must raise exception if there are errors during srcinfo load for dependencies """ mocker.patch("ahriman.models.package.Package._check_output", return_value="") mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) @@ -183,6 +183,27 @@ def test_dependencies_with_version_and_overlap(mocker: MockerFixture, resource_p assert Package.dependencies(Path("path")) == {"glibc", "doxygen", "binutils", "git", "libmpc", "python", "zstd"} +def test_supported_architectures(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must generate list of available architectures + """ + srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() + mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) + assert Package.supported_architectures(Path("path")) == \ + {"i686", "pentium4", "x86_64", "arm", "armv7h", "armv6h", "aarch64"} + + +def test_supported_architectures_failed(mocker: MockerFixture) -> None: + """ + must raise exception if there are errors during srcinfo load for architectures + """ + mocker.patch("ahriman.models.package.Package._check_output", return_value="") + mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) + + with pytest.raises(InvalidPackageInfo): + Package.supported_architectures(Path("path")) + + def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: """ must return same actual_version as version is diff --git a/tests/ahriman/models/test_pkgbuild_patch.py b/tests/ahriman/models/test_pkgbuild_patch.py new file mode 100644 index 00000000..9063ee06 --- /dev/null +++ b/tests/ahriman/models/test_pkgbuild_patch.py @@ -0,0 +1,61 @@ +from pathlib import Path +from pytest_mock import MockerFixture +from unittest.mock import MagicMock, call + +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_is_function() -> None: + """ + must correctly define key as function + """ + assert not PkgbuildPatch("key", "value").is_function + assert PkgbuildPatch("key()", "value").is_function + + +def test_quote() -> None: + """ + must quote strings if unsafe flag is not set + """ + assert PkgbuildPatch("key", "value").quote("value") == """value""" + assert PkgbuildPatch("key", "va'lue").quote("va'lue") == """'va'"'"'lue'""" + assert PkgbuildPatch("key", "va'lue", unsafe=True).quote("va'lue") == """va'lue""" + + +def test_serialize() -> None: + """ + must correctly serialize string values + """ + assert PkgbuildPatch("key", "value").serialize() == "key=value" + assert PkgbuildPatch("key", "42").serialize() == "key=42" + assert PkgbuildPatch("key", "4'2").serialize() == """key='4'"'"'2'""" + assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2" + + +def test_serialize_function() -> None: + """ + must correctly serialize function values + """ + assert PkgbuildPatch("key()", "{ value }", unsafe=True).serialize() == "key() { value }" + + +def test_serialize_list() -> None: + """ + must correctly serialize list values + """ + assert PkgbuildPatch("arch", ["i686", "x86_64"]).serialize() == """arch=(i686 x86_64)""" + assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')""" + assert PkgbuildPatch("key", ["val'ue", "val\"ue2"], unsafe=True).serialize() == """key=(val'ue val"ue2)""" + + +def test_write(mocker: MockerFixture) -> None: + """ + must write serialized value to the file + """ + file_mock = MagicMock() + open_mock = mocker.patch("pathlib.Path.open") + open_mock.return_value.__enter__.return_value = file_mock + + PkgbuildPatch("key", "value").write(Path("PKGBUILD")) + open_mock.assert_called_once_with("a") + file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")])