patch architecture list in runtime (#66)

This commit is contained in:
Evgenii Alekseev 2022-08-09 15:18:20 +03:00 committed by GitHub
parent 9d016f51b5
commit 8befee58fe
7 changed files with 244 additions and 6 deletions

View File

@ -20,7 +20,7 @@ archbuild_flags =
build_command = extra-x86_64-build build_command = extra-x86_64-build
ignore_packages = ignore_packages =
makechrootpkg_flags = makechrootpkg_flags =
makepkg_flags = --nocolor makepkg_flags = --nocolor --ignorearch
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
[repository] [repository]

View File

@ -25,6 +25,7 @@ from typing import List, Optional
from ahriman.core.lazy_logging import LazyLogging from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import check_output, walk from ahriman.core.util import check_output, walk
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -42,6 +43,24 @@ class Sources(LazyLogging):
_check_output = check_output _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 @staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: 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) shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote) instance.fetch(sources_dir, package.remote)
if patch is None: if patch is not None:
instance.logger.info("no patches found")
return
instance.patch_apply(sources_dir, patch) instance.patch_apply(sources_dir, patch)
instance.extend_architectures(sources_dir, paths.architecture)
@staticmethod @staticmethod
def patch_create(sources_dir: Path, *pattern: str) -> str: def patch_create(sources_dir: Path, *pattern: str) -> str:

View File

@ -267,6 +267,26 @@ class Package(LazyLogging):
packages = set(srcinfo["packages"].keys()) packages = set(srcinfo["packages"].keys())
return (depends | makedepends) - packages 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: def actual_version(self, paths: RepositoryPaths) -> str:
""" """
additional method to handle VCS package versions additional method to handle VCS package versions

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -10,6 +10,30 @@ from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths 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: def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None:
""" """
must do nothing in case if no branches available 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) mocker.patch("pathlib.Path.is_dir", return_value=False)
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") 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) Sources.load(Path("local"), package_ahriman, "patch", repository_paths)
fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote) fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote)
patch_mock.assert_called_once_with(Path("local"), "patch") 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: def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:

View File

@ -154,7 +154,7 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage
def test_dependencies_failed(mocker: MockerFixture) -> None: 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.Package._check_output", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) 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"} 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: def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
""" """
must return same actual_version as version is must return same actual_version as version is

View File

@ -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")])