diff --git a/.github/workflows/setup.sh b/.github/workflows/setup.sh index 5ed7afcb..b52bd430 100755 --- a/.github/workflows/setup.sh +++ b/.github/workflows/setup.sh @@ -10,13 +10,11 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never # refresh the image pacman -Syu --noconfirm # main dependencies -pacman -Sy --noconfirm devtools git pyalpm python-inflection python-passlib python-pyelftools python-requests python-srcinfo python-systemd sudo +pacman -Sy --noconfirm devtools git pyalpm python-inflection python-passlib python-pyelftools python-requests python-systemd sudo # make dependencies pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel # optional dependencies if [[ -z $MINIMAL_INSTALL ]]; then - # VCS support - pacman -Sy --noconfirm breezy darcs mercurial subversion # web server pacman -Sy --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja # additional features diff --git a/Dockerfile b/Dockerfile index 5a887293..d00f0c63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,6 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \ echo "build ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/build" COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package" ## install package dependencies -## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size RUN pacman -Sy --noconfirm --asdeps \ devtools \ git \ @@ -40,7 +39,6 @@ RUN pacman -Sy --noconfirm --asdeps \ python-passlib \ python-pyelftools \ python-requests \ - python-srcinfo \ && \ pacman -Sy --noconfirm --asdeps \ base-devel \ @@ -50,9 +48,7 @@ RUN pacman -Sy --noconfirm --asdeps \ python-wheel \ && \ pacman -Sy --noconfirm --asdeps \ - breezy \ git \ - mercurial \ python-aiohttp \ python-boto3 \ python-cerberus \ @@ -61,7 +57,6 @@ RUN pacman -Sy --noconfirm --asdeps \ python-matplotlib \ python-systemd \ rsync \ - subversion \ && \ runuser -u build -- install-aur-package \ python-aioauth-client \ diff --git a/docs/ahriman.core.alpm.rst b/docs/ahriman.core.alpm.rst index 1b218d86..ec47db9a 100644 --- a/docs/ahriman.core.alpm.rst +++ b/docs/ahriman.core.alpm.rst @@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module :no-undoc-members: :show-inheritance: +ahriman.core.alpm.pkgbuild\_parser module +----------------------------------------- + +.. automodule:: ahriman.core.alpm.pkgbuild_parser + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.alpm.repo module ----------------------------- diff --git a/docs/ahriman.models.rst b/docs/ahriman.models.rst index a1af1af1..e671b36f 100644 --- a/docs/ahriman.models.rst +++ b/docs/ahriman.models.rst @@ -172,6 +172,14 @@ ahriman.models.pacman\_synchronization module :no-undoc-members: :show-inheritance: +ahriman.models.pkgbuild module +------------------------------ + +.. automodule:: ahriman.models.pkgbuild + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.pkgbuild\_patch module ------------------------------------- diff --git a/docs/faq/general.rst b/docs/faq/general.rst index 8ac04879..c7f00682 100644 --- a/docs/faq/general.rst +++ b/docs/faq/general.rst @@ -265,11 +265,7 @@ TL;DR How to update VCS packages ^^^^^^^^^^^^^^^^^^^^^^^^^^ -Normally the service handles VCS packages correctly, however it requires additional dependencies: - -.. code-block:: shell - - pacman -S breezy darcs mercurial subversion +Normally the service handles VCS packages correctly. The version is updated in clean chroot, no additional actions are required. How to review changes before build ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 59b4b36f..f73feb53 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -7,12 +7,9 @@ pkgdesc="ArcH linux ReposItory MANager" arch=('any') url="https://github.com/arcan1s/ahriman" license=('GPL3') -depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests' 'python-srcinfo') +depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests') makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel') -optdepends=('breezy: -bzr packages support' - 'darcs: -darcs packages support' - 'mercurial: -hg packages support' - 'python-aioauth-client: web server with OAuth2 authorization' +optdepends=('python-aioauth-client: web server with OAuth2 authorization' 'python-aiohttp: web server' 'python-aiohttp-apispec>=3.0.0: web server' 'python-aiohttp-cors: web server' @@ -26,8 +23,7 @@ optdepends=('breezy: -bzr packages support' 'python-requests-unixsocket2: client report to web server by unix socket' 'python-jinja: html report generation' 'python-systemd: journal support' - 'rsync: sync by using rsync' - 'subversion: -svn packages support') + 'rsync: sync by using rsync') source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver.tar.gz" 'ahriman.sysusers' 'ahriman.tmpfiles') diff --git a/pyproject.toml b/pyproject.toml index e85e1865..c72d137c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ "passlib", "pyelftools", "requests", - "srcinfo", ] dynamic = ["version"] diff --git a/src/ahriman/application/handlers/service_updates.py b/src/ahriman/application/handlers/service_updates.py index 1d608462..ddd159ab 100644 --- a/src/ahriman/application/handlers/service_updates.py +++ b/src/ahriman/application/handlers/service_updates.py @@ -47,7 +47,7 @@ class ServiceUpdates(Handler): report(bool): force enable or disable reporting """ remote = Package.from_aur("ahriman", None) - _, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it + _, release = remote.version.rsplit("-", maxsplit=1) # we don't store pkgrel locally, so we just append it local_version = f"{__version__}-{release}" # technically we would like to compare versions, but it is fine to raise an exception in case if locally diff --git a/src/ahriman/core/alpm/pkgbuild_parser.py b/src/ahriman/core/alpm/pkgbuild_parser.py new file mode 100644 index 00000000..a5e5c43b --- /dev/null +++ b/src/ahriman/core/alpm/pkgbuild_parser.py @@ -0,0 +1,316 @@ +# +# Copyright (c) 2021-2024 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 itertools +import re +import shlex + +from collections.abc import Generator +from enum import StrEnum +from typing import IO + +from ahriman.core.exceptions import PkgbuildParserError +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +class PkgbuildToken(StrEnum): + """ + well-known tokens dictionary + + Attributes: + ArrayEnds(PkgbuildToken): (class attribute) array ends token + ArrayStarts(PkgbuildToken): (class attribute) array starts token + Comma(PkgbuildToken): (class attribute) comma token + Comment(PkgbuildToken): (class attribute) comment token + FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token + FunctionEnds(PkgbuildToken): (class attribute) function ends token + FunctionStarts(PkgbuildToken): (class attribute) function starts token + """ + + ArrayStarts = "(" + ArrayEnds = ")" + + Comma = "," + + Comment = "#" + + FunctionDeclaration = "()" + FunctionStarts = "{" + FunctionEnds = "}" + + +class PkgbuildParser(shlex.shlex): + """ + simple pkgbuild reader implementation in pure python, because others suck. + + What is it: + + #. Simple PKGBUILD parser written in python. + #. No shell execution, so it is free from random shell attacks. + #. Able to parse simple constructions (assignments, comments, functions, arrays). + + What it is not: + + #. Fully functional shell parser. + #. Shell executor. + #. No parameter expansion. + + For more details what does it support, please, consult with the test cases. + + Examples: + This class is heavily based on :mod:`shlex` parser, but instead of strings operates with the + :class:`ahriman.models.pkgbuild_patch.PkgbuildPatch` objects. The main way to use it is to call :func:`parse()` + function and collect parsed objects, e.g.:: + + >>> parser = PkgbuildParser(StringIO("input string")) + >>> for patch in parser.parse(): + >>> print(f"{patch.key} = {patch.value}") + + It doesn't store the state of the fields (but operates with the :mod:`shlex` parser state), so no shell + post-processing is performed (e.g. variable substitution). + """ + + _ARRAY_ASSIGNMENT = re.compile(r"^(?P\w+)=$") + # in addition to usual assignment, functions can have dash + _FUNCTION_DECLARATION = re.compile(r"^(?P[\w-]+)$") + _STRING_ASSIGNMENT = re.compile(r"^(?P\w+)=(?P.+)$") + + def __init__(self, stream: IO[str]) -> None: + """ + Args: + stream(IO[str]): input stream containing PKGBUILD content + """ + shlex.shlex.__init__(self, stream, posix=True, punctuation_chars=True) + self._io = stream # direct access without type casting + + # ignore substitution and extend bash symbols + self.wordchars += "${}#:+-@" + # in case of default behaviour, it will ignore, for example, segment part of url outside of quotes + self.commenters = "" + + @staticmethod + def _expand_array(array: list[str]) -> list[str]: + """ + bash array expansion simulator. It takes raw array and tries to expand constructions like + ``(first prefix-{mid1,mid2}-suffix last)`` into ``(first, prefix-mid1-suffix prefix-mid2-suffix last)`` + + Args: + array(list[str]): input array + + Returns: + list[str]: either source array or expanded array if possible + + Raises: + PkgbuildParserError: if there are errors in parser + """ + # we are using comma as marker for expansion (if any) + if PkgbuildToken.Comma not in array: + return array + # again sanity check, for expansion there are at least 3 elements (first, last and comma) + if len(array) < 3: + return array + + result = [] + buffer, prefix = [], None + + for index, (first, second) in enumerate(itertools.pairwise(array)): + match (first, second): + # in this case we check if expansion should be started + # this condition matches "prefix{first", "," + case (_, PkgbuildToken.Comma) if PkgbuildToken.FunctionStarts in first: + prefix, part = first.rsplit(PkgbuildToken.FunctionStarts, maxsplit=1) + buffer.append(f"{prefix}{part}") + + # the last element case, it matches either ",", "last}" or ",", "last}suffix" + # in case if there is suffix, it must be appended to all list elements + case (PkgbuildToken.Comma, _) if prefix is not None and PkgbuildToken.FunctionEnds in second: + part, suffix = second.rsplit(PkgbuildToken.FunctionEnds, maxsplit=1) + buffer.append(f"{prefix}{part}") + result.extend([f"{part}{suffix}" for part in buffer]) + # reset state + buffer, prefix = [], None + + # we have already prefix string, so we are in progress of expansion + # we always operate the last element, so this matches ",", "next" + case (PkgbuildToken.Comma, _) if prefix is not None: + buffer.append(f"{prefix}{second}") + + # exactly first element of the list + case (_, _) if prefix is None and index == 0: + result.append(first) + + # any next normal element + case (_, _) if prefix is None: + result.append(second) + + # small sanity check + if prefix is not None: + raise PkgbuildParserError("error in array expansion", array) + + return result + + def _is_quoted(self) -> bool: + """ + check if the last element was quoted. ``shlex.shlex`` parser doesn't provide information about was the token + quoted or not, thus there is no difference between "'#'" (diez in quotes) and "#" (diez without quotes). This + method simply rolls back to the last non-space character and check if it is a quotation mark + + Returns: + bool: ``True`` if the previous element of the stream is a quote and ``False`` otherwise + """ + current_position = self._io.tell() + + last_char = None + for index in range(current_position - 1, -1, -1): + self._io.seek(index) + last_char = self._io.read(1) + if not last_char.isspace(): + break + + self._io.seek(current_position) # reset position of the stream + return last_char is not None and last_char in self.quotes + + def _parse_array(self) -> list[str]: + """ + parse array from the PKGBUILD. This method will extract tokens from parser until it matches closing array, + modifying source parser state + + Returns: + list[str]: extracted arrays elements + + Raises: + PkgbuildParserError: if array is not closed + """ + def extract() -> Generator[str, None, None]: + while token := self.get_token(): + match token: + case _ if self._is_quoted(): + pass + case PkgbuildToken.ArrayEnds: + break + case PkgbuildToken.Comment: + self.instream.readline() + continue + yield token + + if token != PkgbuildToken.ArrayEnds: + raise PkgbuildParserError("no closing array bracket found") + + return self._expand_array(list(extract())) + + def _parse_function(self) -> str: + """ + parse function from the PKGBUILD. This method will extract tokens from parser until it matches closing function, + modifying source parser state. Instead of trying to combine tokens together, it uses positions of the file + and reads content again in this range + + Returns: + str: function body + + Raises: + PkgbuildParserError: if function body wasn't found or parser input stream doesn't support position reading + """ + # find start and end positions + start_position = end_position = -1 + counter = 0 # simple processing of the inner "{" and "}" + while token := self.get_token(): + match token: + case _ if self._is_quoted(): + continue + case PkgbuildToken.FunctionStarts: + if counter == 0: + start_position = self._io.tell() - 1 + counter += 1 + case PkgbuildToken.FunctionEnds: + end_position = self._io.tell() + counter -= 1 + if counter == 0: + break + + if not 0 < start_position < end_position: + raise PkgbuildParserError("function body wasn't found") + + # read the specified interval from source stream + self._io.seek(start_position - 1) # start from the previous symbol + content = self._io.read(end_position - start_position) + + # special case of the end of file + if self.state == self.eof: # type: ignore[attr-defined] + content += self._io.read(1) + + # reset position (because the last position was before the next token starts) + self._io.seek(end_position) + + return content + + def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]: + """ + parse single token to the PKGBUILD field + + Args: + token(str): current token + + Yields: + PkgbuildPatch: extracted a PKGBUILD node + """ + # simple assignment rule + if (match := self._STRING_ASSIGNMENT.match(token)) is not None: + key = match.group("key") + value = match.group("value") + yield PkgbuildPatch(key, value) + return + + if token == PkgbuildToken.Comment: + self.instream.readline() + return + + match self.get_token(): + # array processing. Arrays will be sent as "key=", "(", values, ")" + case PkgbuildToken.ArrayStarts if (match := self._ARRAY_ASSIGNMENT.match(token)) is not None: + key = match.group("key") + value = self._parse_array() + yield PkgbuildPatch(key, value) + + # functions processing. Function will be sent as "name", "()", "{", body, "}" + case PkgbuildToken.FunctionDeclaration if self._FUNCTION_DECLARATION.match(token): + key = f"{token}{PkgbuildToken.FunctionDeclaration}" + value = self._parse_function() + yield PkgbuildPatch(key, value) # this is not mistake, assign to token without () + + # special function case, where "(" and ")" are separated tokens, e.g. "pkgver ( )" + case PkgbuildToken.ArrayStarts if self._FUNCTION_DECLARATION.match(token): + next_token = self.get_token() + if next_token == PkgbuildToken.ArrayEnds: # replace closing bracket with "()" + next_token = PkgbuildToken.FunctionDeclaration + self.push_token(next_token) # type: ignore[arg-type] + yield from self._parse_token(token) + + # some random token received without continuation, lets guess it is empty assignment (i.e. key=) + case other if other is not None: + yield from self._parse_token(other) + + def parse(self) -> Generator[PkgbuildPatch, None, None]: + """ + parse source stream and yield parsed entries + + Yields: + PkgbuildPatch: extracted a PKGBUILD node + """ + for token in self: + yield from self._parse_token(token) diff --git a/src/ahriman/core/build_tools/package_archive.py b/src/ahriman/core/build_tools/package_archive.py index 177b216b..38598a98 100644 --- a/src/ahriman/core/build_tools/package_archive.py +++ b/src/ahriman/core/build_tools/package_archive.py @@ -115,7 +115,7 @@ class PackageArchive: Returns: FilesystemPackage: generated pacman package model with empty paths """ - package_name, *_ = path.parent.name.rsplit("-", 2) + package_name, *_ = path.parent.name.rsplit("-", maxsplit=2) try: pacman_package = OfficialSyncdb.info(package_name, pacman=self.pacman) return FilesystemPackage( diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py index a53b11ab..ca4b3480 100644 --- a/src/ahriman/core/build_tools/task.py +++ b/src/ahriman/core/build_tools/task.py @@ -17,13 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from collections.abc import Generator from pathlib import Path from ahriman.core.build_tools.sources import Sources from ahriman.core.configuration import Configuration from ahriman.core.exceptions import BuildError from ahriman.core.log import LazyLogging -from ahriman.core.utils import check_output +from ahriman.core.utils import check_output, package_like from ahriman.models.package import Package from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.repository_paths import RepositoryPaths @@ -65,12 +66,43 @@ class Task(LazyLogging): self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) - def build(self, sources_dir: Path, **kwargs: str | None) -> list[Path]: + def _package_archives(self, sources_dir: Path, source_files: list[Path]) -> list[Path]: + """ + extract package archives from the directory + + Args: + sources_dir(Path): path to where sources are + source_files(list[Path]): list of files which were initially in the directory + + Returns: + list[Path]: list of file paths which looks like freshly generated archives + """ + def files() -> Generator[Path, None, None]: + for filepath in sources_dir.iterdir(): + if filepath in source_files: + continue # skip files which were already there + if filepath.suffix == ".log": + continue # skip log files + if not package_like(filepath): + continue # path doesn't look like a package + yield filepath + + # debug packages are always formed as package.base-debug + # see /usr/share/makepkg/util/pkgbuild.sh for more details + debug_package_prefix = f"{self.package.base}-debug-" + return [ + package + for package in files() + if self.include_debug_packages or not package.name.startswith(debug_package_prefix) + ] + + def build(self, sources_dir: Path, *, dry_run: bool = False, **kwargs: str | None) -> list[Path]: """ run package build Args: sources_dir(Path): path to where sources are + dry_run(bool, optional): do not perform build itself (Default value = False) **kwargs(str | None): environment variables to be passed to build processes Returns: @@ -80,6 +112,8 @@ class Task(LazyLogging): command.extend(self.archbuild_flags) command.extend(["--"] + self.makechrootpkg_flags) command.extend(["--"] + self.makepkg_flags) + if dry_run: + command.extend(["--nobuild"]) self.logger.info("using %s for %s", command, self.package.base) environment: dict[str, str] = { @@ -89,6 +123,7 @@ class Task(LazyLogging): } self.logger.info("using environment variables %s", environment) + source_files = list(sources_dir.iterdir()) check_output( *command, exception=BuildError.from_process(self.package.base), @@ -98,20 +133,7 @@ class Task(LazyLogging): environment=environment, ) - package_list_command = ["makepkg", "--packagelist"] - if not self.include_debug_packages: - package_list_command.append("OPTIONS=(!debug)") # disable debug flag manually - packages = check_output( - *package_list_command, - exception=BuildError.from_process(self.package.base), - cwd=sources_dir, - logger=self.logger, - environment=environment, - ).splitlines() - # some dirty magic here - # the filter is applied in order to make sure that result will only contain packages which were actually built - # e.g. in some cases packagelist command produces debug packages which were not actually built - return list(filter(lambda path: path.is_file(), map(Path, packages))) + return self._package_archives(sources_dir, source_files) def init(self, sources_dir: Path, patches: list[PkgbuildPatch], local_version: str | None) -> str | None: """ diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py index 06d8c705..57602d23 100644 --- a/src/ahriman/core/exceptions.py +++ b/src/ahriman/core/exceptions.py @@ -212,6 +212,23 @@ class PacmanError(RuntimeError): RuntimeError.__init__(self, f"Could not perform operation with pacman: `{details}`") +class PkgbuildParserError(ValueError): + """ + exception raises in case of PKGBUILD parser errors + """ + + def __init__(self, reason: str, source: Any = None) -> None: + """ + Args: + reason(str): parser error reason + source(Any, optional): source line if available (Default value = None) + """ + message = f"Could not parse PKGBUILD: {reason}" + if source is not None: + message += f", source: `{source}`" + ValueError.__init__(self, message) + + class PathError(ValueError): """ exception which will be raised on path which is not belong to root directory diff --git a/src/ahriman/core/repository/package_info.py b/src/ahriman/core/repository/package_info.py index 1ab0b3f0..2382d53c 100644 --- a/src/ahriman/core/repository/package_info.py +++ b/src/ahriman/core/repository/package_info.py @@ -58,7 +58,7 @@ class PackageInfo(RepositoryProperties): # force version to max of them self.logger.warning("version of %s differs, found %s and %s", current.base, current.version, local.version) - if current.is_outdated(local, self.paths, calculate_version=False): + if current.is_outdated(local, self.configuration, calculate_version=False): current.version = local.version current.packages.update(local.packages) except Exception: diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index 696f6f60..663278c1 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -51,7 +51,6 @@ class RepositoryProperties(EventLogger, LazyLogging): scan_paths(ScanPaths): scan paths for the implicit dependencies sign(GPG): GPG wrapper instance triggers(TriggerLoader): triggers holder - vcs_allowed_age(int): maximal age of the VCS packages before they will be checked """ def __init__(self, repository_id: RepositoryId, configuration: Configuration, database: SQLite, *, report: bool, @@ -68,8 +67,6 @@ class RepositoryProperties(EventLogger, LazyLogging): self.configuration = configuration self.database = database - self.vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) - self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[]) diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index 0c6ee228..20a93511 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -67,10 +67,7 @@ class UpdateHandler(PackageInfo, Cleaner): try: remote = load_remote(local) - if local.is_outdated( - remote, self.paths, - vcs_allowed_age=self.vcs_allowed_age, - calculate_version=vcs): + if local.is_outdated(remote, self.configuration, calculate_version=vcs): self.reporter.set_pending(local.base) self.event(local.base, EventType.PackageOutdated, "Remote version is newer than local") result.append(remote) @@ -154,9 +151,7 @@ class UpdateHandler(PackageInfo, Cleaner): if local is None: continue # we don't add packages automatically - if local.is_outdated(remote, self.paths, - vcs_allowed_age=self.vcs_allowed_age, - calculate_version=vcs): + if local.is_outdated(remote, self.configuration, calculate_version=vcs): self.reporter.set_pending(local.base) self.event(local.base, EventType.PackageOutdated, "Locally pulled sources are outdated") result.append(remote) diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index bcece0c4..3f583470 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -197,7 +197,7 @@ class Watcher(LazyLogging): proxy methods for reporter client Args: - item(str): property name: + item(str): property name Returns: Any: attribute by its name diff --git a/src/ahriman/core/utils.py b/src/ahriman/core/utils.py index ab1651ef..bf50f9e5 100644 --- a/src/ahriman/core/utils.py +++ b/src/ahriman/core/utils.py @@ -27,7 +27,7 @@ import re import selectors import subprocess -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator, Iterable, Mapping from dataclasses import asdict from enum import Enum from pathlib import Path @@ -407,7 +407,7 @@ def safe_filename(source: str) -> str: return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source) -def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *, +def srcinfo_property(key: str, srcinfo: Mapping[str, Any], package_srcinfo: Mapping[str, Any], *, default: Any = None) -> Any: """ extract property from SRCINFO. This method extracts property from package if this property is presented in @@ -416,8 +416,8 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st Args: key(str): key to extract - srcinfo(dict[str, Any]): root structure of SRCINFO - package_srcinfo(dict[str, Any]): package specific SRCINFO + srcinfo(Mapping[str, Any]): root structure of SRCINFO + package_srcinfo(Mapping[str, Any]): package specific SRCINFO default(Any, optional): the default value for the specified key (Default value = None) Returns: @@ -426,7 +426,7 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st return package_srcinfo.get(key) or srcinfo.get(key) or default -def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *, +def srcinfo_property_list(key: str, srcinfo: Mapping[str, Any], package_srcinfo: Mapping[str, Any], *, architecture: str | None = None) -> list[Any]: """ extract list property from SRCINFO. Unlike :func:`srcinfo_property()` it supposes that default return value is @@ -435,8 +435,8 @@ def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: di Args: key(str): key to extract - srcinfo(dict[str, Any]): root structure of SRCINFO - package_srcinfo(dict[str, Any]): package specific SRCINFO + srcinfo(Mapping[str, Any]): root structure of SRCINFO + package_srcinfo(Mapping[str, Any]): package specific SRCINFO architecture(str | None, optional): package architecture if set (Default value = None) Returns: diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 9f988aba..a4b9f98d 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -26,19 +26,18 @@ from collections.abc import Callable, Generator, Iterable from dataclasses import dataclass from pathlib import Path from pyalpm import vercmp # type: ignore[import-not-found] -from srcinfo.parse import parse_srcinfo # type: ignore[import-untyped] from typing import Any, Self from urllib.parse import urlparse from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb -from ahriman.core.exceptions import PackageInfoError +from ahriman.core.configuration import Configuration from ahriman.core.log import LazyLogging -from ahriman.core.utils import check_output, dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow +from ahriman.core.utils import dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.remote_source import RemoteSource -from ahriman.models.repository_paths import RepositoryPaths @dataclass(kw_only=True) @@ -255,25 +254,19 @@ class Package(LazyLogging): Returns: Self: package properties - - Raises: - PackageInfoError: if there are parsing errors """ - srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path) - srcinfo, errors = parse_srcinfo(srcinfo_source) - if errors: - raise PackageInfoError(errors) + pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") packages = { package: PackageDescription( - depends=srcinfo_property_list("depends", srcinfo, properties, architecture=architecture), - make_depends=srcinfo_property_list("makedepends", srcinfo, properties, architecture=architecture), - opt_depends=srcinfo_property_list("optdepends", srcinfo, properties, architecture=architecture), - check_depends=srcinfo_property_list("checkdepends", srcinfo, properties, architecture=architecture), + depends=srcinfo_property_list("depends", pkgbuild, properties, architecture=architecture), + make_depends=srcinfo_property_list("makedepends", pkgbuild, properties, architecture=architecture), + opt_depends=srcinfo_property_list("optdepends", pkgbuild, properties, architecture=architecture), + check_depends=srcinfo_property_list("checkdepends", pkgbuild, properties, architecture=architecture), ) - for package, properties in srcinfo["packages"].items() + for package, properties in pkgbuild.packages().items() } - version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) + version = full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) remote = RemoteSource( source=PackageSource.Local, @@ -284,7 +277,7 @@ class Package(LazyLogging): ) return cls( - base=srcinfo["pkgbase"], + base=pkgbuild["pkgbase"], version=version, remote=remote, packages=packages, @@ -363,18 +356,14 @@ class Package(LazyLogging): Raises: PackageInfoError: if there are parsing errors """ - srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path) - srcinfo, errors = parse_srcinfo(srcinfo_source) - if errors: - raise PackageInfoError(errors) - + pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") # we could use arch property, but for consistency it is better to call special method architectures = Package.supported_architectures(path) for architecture in architectures: - for source in srcinfo_property_list("source", srcinfo, {}, architecture=architecture): + for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture): if "::" in source: - _, source = source.split("::", 1) # in case if filename is specified, remove it + _, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it if urlparse(source).scheme: # basically file schema should use absolute path which is impossible if we are distributing @@ -383,7 +372,7 @@ class Package(LazyLogging): yield Path(source) - if (install := srcinfo.get("install", None)) is not None: + if (install := pkgbuild.get("install")) is not None: yield Path(install) @staticmethod @@ -396,15 +385,9 @@ class Package(LazyLogging): Returns: set[str]: list of package supported architectures - - Raises: - PackageInfoError: if there are parsing errors """ - srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path) - srcinfo, errors = parse_srcinfo(srcinfo_source) - if errors: - raise PackageInfoError(errors) - return set(srcinfo.get("arch", [])) + pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") + return set(pkgbuild.get("arch", [])) def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]: """ @@ -426,39 +409,39 @@ class Package(LazyLogging): return sorted(set(generator())) - def actual_version(self, paths: RepositoryPaths) -> str: + def actual_version(self, configuration: Configuration) -> str: """ additional method to handle VCS package versions Args: - paths(RepositoryPaths): repository paths instance + configuration(Configuration): configuration instance Returns: str: package version if package is not VCS and current version according to VCS otherwise - - Raises: - PackageInfoError: if there are parsing errors """ if not self.is_vcs: return self.version - from ahriman.core.build_tools.sources import Sources + from ahriman.core.build_tools.task import Task - Sources.load(paths.cache_for(self.base), self, [], paths) + _, repository_id = configuration.check_loaded() + paths = configuration.repository_paths + task = Task(self, configuration, repository_id.architecture, paths) try: - # update pkgver first - check_output("makepkg", "--nodeps", "--nobuild", cwd=paths.cache_for(self.base), logger=self.logger) - # generate new .SRCINFO and put it to parser - srcinfo_source = check_output("makepkg", "--printsrcinfo", - cwd=paths.cache_for(self.base), logger=self.logger) - srcinfo, errors = parse_srcinfo(srcinfo_source) - if errors: - raise PackageInfoError(errors) + # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD + task.init(paths.cache_for(self.base), [], None) + task.build(paths.cache_for(self.base), dry_run=True) - return full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) + pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD") + + return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) except Exception: - self.logger.exception("cannot determine version of VCS package, make sure that VCS tools are installed") + self.logger.exception("cannot determine version of VCS package") + finally: + # clear log files generated by devtools + for log_file in paths.cache_for(self.base).glob("*.log"): + log_file.unlink() return self.version @@ -513,26 +496,25 @@ class Package(LazyLogging): if package.build_date is not None ) - def is_outdated(self, remote: Package, paths: RepositoryPaths, *, - vcs_allowed_age: float | int = 0, + def is_outdated(self, remote: Package, configuration: Configuration, *, calculate_version: bool = True) -> bool: """ check if package is out-of-dated Args: remote(Package): package properties from remote source - paths(RepositoryPaths): repository paths instance. Required for VCS packages cache - vcs_allowed_age(float | int, optional): max age of the built packages before they will be - forced to calculate actual version (Default value = 0) + configuration(Configuration): configuration instance calculate_version(bool, optional): expand version to actual value (by calculating git versions) (Default value = True) Returns: bool: ``True`` if the package is out-of-dated and ``False`` otherwise """ + vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age + if calculate_version and not self.is_newer_than(min_vcs_build_date): - remote_version = remote.actual_version(paths) + remote_version = remote.actual_version(configuration) else: remote_version = remote.version diff --git a/src/ahriman/models/pkgbuild.py b/src/ahriman/models/pkgbuild.py new file mode 100644 index 00000000..173b8107 --- /dev/null +++ b/src/ahriman/models/pkgbuild.py @@ -0,0 +1,149 @@ +# +# Copyright (c) 2021-2024 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 . +# +from collections.abc import Iterator, Mapping +from dataclasses import dataclass +from io import StringIO +from pathlib import Path +from typing import Any, IO, Self + +from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +@dataclass(frozen=True) +class Pkgbuild(Mapping[str, Any]): + """ + model and proxy for PKGBUILD properties + + Attributes: + fields(dict[str, PkgbuildPatch]): PKGBUILD fields + """ + + fields: dict[str, PkgbuildPatch] + + @property + def variables(self) -> dict[str, str]: + """ + list of variables defined and (maybe) used in this PKGBUILD + + Returns: + dict[str, str]: map of variable name to its value. The value will be included here in case if it presented + in the internal dictionary, it is not a function and the value has string type + """ + return { + key: value.value + for key, value in self.fields.items() + if not value.is_function and isinstance(value.value, str) + } + + @classmethod + def from_file(cls, path: Path) -> Self: + """ + parse PKGBUILD from the file + + Args: + path(Path): path to the PKGBUILD file + + Returns: + Self: constructed instance of self + """ + with path.open() as input_file: + return cls.from_io(input_file) + + @classmethod + def from_io(cls, stream: IO[str]) -> Self: + """ + parse PKGBUILD from input stream + + Args: + stream(IO[str]): input stream containing PKGBUILD content + + Returns: + Self: constructed instance of self + """ + parser = PkgbuildParser(stream) + fields = {patch.key: patch for patch in parser.parse()} + + # pkgbase is optional field, the pkgname must be used instead if not set + # however, pkgname is not presented is "package()" functions which we are parsing here too, + # thus, in our terms, it is optional too + if "pkgbase" not in fields and "pkgname" in fields: + fields["pkgbase"] = fields["pkgname"] + + return cls({key: value for key, value in fields.items() if key}) + + def packages(self) -> dict[str, Self]: + """ + extract properties from internal package functions + + Returns: + dict[str, Self]: map of package name to its inner properties if defined + """ + packages = [self["pkgname"]] if isinstance(self["pkgname"], str) else self["pkgname"] + + def io(package_name: str) -> IO[str]: + # try to read package specific function and fallback to default otherwise + content = self.get(f"package_{package_name}") or self["package"] + return StringIO(content) + + return {package: self.from_io(io(package)) for package in packages} + + def __getitem__(self, item: str) -> Any: + """ + get the field of the PKGBUILD. This method tries to get exact key value if possible; if none found, it tries to + fetch function with the same name + + Args: + item(str): key name + + Returns: + Any: substituted value by the key + + Raises: + KeyError: if key doesn't exist + """ + value = self.fields.get(item) + # if the key wasn't found and user didn't ask for function explicitly, we can try to get by function name + if value is None and not item.endswith(PkgbuildToken.FunctionDeclaration): + value = self.fields.get(f"{item}{PkgbuildToken.FunctionDeclaration}") + + # if we still didn't find anything, we can just raise the exception + if value is None: + raise KeyError(item) + + return value.substitute(self.variables) + + def __iter__(self) -> Iterator[str]: + """ + iterate over the fields + + Returns: + Iterator[str]: keys iterator + """ + return iter(self.fields) + + def __len__(self) -> int: + """ + get length of the mapping + + Returns: + int: amount of the fields in this PKGBUILD + """ + return len(self.fields) diff --git a/src/ahriman/models/pkgbuild_patch.py b/src/ahriman/models/pkgbuild_patch.py index 808ac8cb..b4efe3a2 100644 --- a/src/ahriman/models/pkgbuild_patch.py +++ b/src/ahriman/models/pkgbuild_patch.py @@ -21,6 +21,7 @@ import shlex from dataclasses import dataclass, fields from pathlib import Path +from string import Template from typing import Any, Generator, Self from ahriman.core.utils import dataclass_view, filter_json @@ -167,6 +168,21 @@ class PkgbuildPatch: return f"{self.key} {self.value}" # no quoting enabled here return f"""{self.key}={PkgbuildPatch.quote(self.value)}""" + def substitute(self, variables: dict[str, str]) -> str | list[str]: + """ + substitute variables into the value + + Args: + variables(dict[str, str]): map of variables available for usage + + Returns: + str | list[str]: substituted value. All unknown variables will remain as links to their values. + This function doesn't support recursive substitution + """ + if isinstance(self.value, str): + return Template(self.value).safe_substitute(variables) + return [Template(value).safe_substitute(variables) for value in self.value] + def view(self) -> dict[str, Any]: """ generate json patch view diff --git a/tests/ahriman/application/handlers/test_handler_versions.py b/tests/ahriman/application/handlers/test_handler_versions.py index 73602a42..be64b9c2 100644 --- a/tests/ahriman/application/handlers/test_handler_versions.py +++ b/tests/ahriman/application/handlers/test_handler_versions.py @@ -28,9 +28,9 @@ def test_package_dependencies() -> None: """ must extract package dependencies """ - packages = dict(Versions.package_dependencies("srcinfo")) + packages = dict(Versions.package_dependencies("requests")) assert packages - assert packages.get("parse") is not None + assert packages.get("urllib3") is not None def test_package_dependencies_missing() -> None: diff --git a/tests/ahriman/core/alpm/test_pkgbuild_parser.py b/tests/ahriman/core/alpm/test_pkgbuild_parser.py new file mode 100644 index 00000000..5ac1b2e0 --- /dev/null +++ b/tests/ahriman/core/alpm/test_pkgbuild_parser.py @@ -0,0 +1,256 @@ +import pytest + +from io import StringIO +from pathlib import Path + +from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser +from ahriman.core.exceptions import PkgbuildParserError +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_expand_array() -> None: + """ + must correctly expand array + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs", ",", "-fortran}"]) == [ + "${pkgbase}", "${pkgbase}-libs", "${pkgbase}-fortran" + ] + assert PkgbuildParser._expand_array(["first", "prefix{1", ",", "2", ",", "3}suffix", "last"]) == [ + "first", "prefix1suffix", "prefix2suffix", "prefix3suffix", "last" + ] + + +def test_expand_array_no_comma() -> None: + """ + must skip array extraction if there is no comma + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", "-libs", "-fortran}"]) == ["${pkgbase}{", "-libs", "-fortran}"] + + +def test_expand_array_short() -> None: + """ + must skip array extraction if it is short + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", ","]) == ["${pkgbase}{", ","] + + +def test_expand_array_exception() -> None: + """ + must raise exception if there is unclosed element + """ + with pytest.raises(PkgbuildParserError): + assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"]) + + +def test_parse_array() -> None: + """ + must parse array + """ + parser = PkgbuildParser(StringIO("var=(first second)")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", "second"])] + + +def test_parse_array_comment() -> None: + """ + must parse array with comments inside + """ + parser = PkgbuildParser(StringIO("""validpgpkeys=( + 'F3691687D867B81B51CE07D9BBE43771487328A9' # bpiotrowski@archlinux.org + '86CFFCA918CF3AF47147588051E8B148A9999C34' # evangelos@foutrelis.com + '13975A70E63C361C73AE69EF6EEB81F8981C74C7' # richard.guenther@gmail.com + 'D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62' # Jakub Jelinek +)""")) + assert list(parser.parse()) == [PkgbuildPatch("validpgpkeys", [ + "F3691687D867B81B51CE07D9BBE43771487328A9", + "86CFFCA918CF3AF47147588051E8B148A9999C34", + "13975A70E63C361C73AE69EF6EEB81F8981C74C7", + "D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62", + ])] + + +def test_parse_array_quotes() -> None: + """ + must correctly process quoted brackets + """ + parser = PkgbuildParser(StringIO("""var=(first "(" second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", "(", "second"])] + + parser = PkgbuildParser(StringIO("""var=(first ")" second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", ")", "second"])] + + parser = PkgbuildParser(StringIO("""var=(first ')' second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", ")", "second"])] + + +def test_parse_array_exception() -> None: + """ + must raise exception if there is no closing bracket + """ + parser = PkgbuildParser(StringIO("var=(first second")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + +def test_parse_function() -> None: + """ + must parse function + """ + parser = PkgbuildParser(StringIO("var() { echo hello world } ")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_eof() -> None: + """ + must parse function with "}" at the end of the file + """ + parser = PkgbuildParser(StringIO("var() { echo hello world }")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_spaces() -> None: + """ + must parse function with spaces in declaration + """ + parser = PkgbuildParser(StringIO("var ( ) { echo hello world } ")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_inner_shell() -> None: + """ + must parse function with inner shell + """ + parser = PkgbuildParser(StringIO("var ( ) { { echo hello world } } ")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ { echo hello world } }")] + + +def test_parse_function_quotes() -> None: + """ + must parse function with bracket in quotes + """ + parser = PkgbuildParser(StringIO("""var ( ) { echo "hello world {" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo "hello world {" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world "{" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world "{" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo "hello world }" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo "hello world }" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world "}" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world "}" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world '}' } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world '}' }""")] + + +def test_parse_function_exception() -> None: + """ + must raise exception if no bracket found + """ + parser = PkgbuildParser(StringIO("var() echo hello world } ")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + parser = PkgbuildParser(StringIO("var() { echo hello world")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + +def test_parse_token_assignment() -> None: + """ + must parse simple assignment + """ + parser = PkgbuildParser(StringIO()) + assert next(parser._parse_token("var=value")) == PkgbuildPatch("var", "value") + assert next(parser._parse_token("var=$value")) == PkgbuildPatch("var", "$value") + assert next(parser._parse_token("var=${value}")) == PkgbuildPatch("var", "${value}") + assert next(parser._parse_token("var=${value/-/_}")) == PkgbuildPatch("var", "${value/-/_}") + + +def test_parse_token_comment() -> None: + """ + must correctly parse comment + """ + parser = PkgbuildParser(StringIO("""first=1 # comment + # comment line + second=2 + #third=3 + """)) + assert list(parser.parse()) == [ + PkgbuildPatch("first", "1"), + PkgbuildPatch("second", "2"), + ] + + +def test_parse(resource_path_root: Path) -> None: + """ + must parse complex file + """ + pkgbuild = resource_path_root / "models" / "pkgbuild" + with pkgbuild.open() as content: + parser = PkgbuildParser(content) + assert list(parser.parse()) == [ + PkgbuildPatch("var", "value"), + PkgbuildPatch("var", "value"), + PkgbuildPatch("var", "value with space"), + PkgbuildPatch("var", "value"), + PkgbuildPatch("var", "$ref"), + PkgbuildPatch("var", "${ref}"), + PkgbuildPatch("var", "$ref value"), + PkgbuildPatch("var", "${ref}value"), + PkgbuildPatch("var", "${ref/-/_}"), + PkgbuildPatch("var", "${ref##.*}"), + PkgbuildPatch("var", "${ref%%.*}"), + PkgbuildPatch("array", ["first", "second", "third", "with space"]), + PkgbuildPatch("array", ["single"]), + PkgbuildPatch("array", ["$ref"]), + PkgbuildPatch("array", ["first", "second", "third"]), + PkgbuildPatch("array", ["first", "second", "third"]), + PkgbuildPatch("array", ["first", "last"]), + PkgbuildPatch("array", ["first", "1suffix", "2suffix", "last"]), + PkgbuildPatch("array", ["first", "prefix1", "prefix2", "last"]), + PkgbuildPatch("array", ["first", "prefix1suffix", "prefix2suffix", "last"]), + PkgbuildPatch("array", ["first", "(", "second"]), + PkgbuildPatch("array", ["first", ")", "second"]), + PkgbuildPatch("array", ["first", "(", "second"]), + PkgbuildPatch("array", ["first", ")", "second"]), + PkgbuildPatch("function()", """{ single line }"""), + PkgbuildPatch("function()", """{ + multi + line +}"""), + PkgbuildPatch("function()", """{ + c + multi + line +}"""), + PkgbuildPatch("function()", """{ + # comment + multi + line +}"""), + PkgbuildPatch("function()", """{ + body +}"""), + PkgbuildPatch("function()", """{ + body +}"""), + PkgbuildPatch("function_with-package-name()", """{ body }"""), + PkgbuildPatch("function()", """{ + first + { inner shell } + last +}"""), + PkgbuildPatch("function()", """{ + body "{" argument +}"""), + PkgbuildPatch("function()", """{ + body "}" argument +}"""), + PkgbuildPatch("function()", """{ + body '{' argument +}"""), + PkgbuildPatch("function()", """{ + body '}' argument +}"""), + ] diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py index 399baeac..2433733f 100644 --- a/tests/ahriman/core/build_tools/test_task.py +++ b/tests/ahriman/core/build_tools/test_task.py @@ -2,37 +2,65 @@ import pytest from pathlib import Path from pytest_mock import MockerFixture -from unittest.mock import call as MockCall from ahriman.core.build_tools.task import Task from ahriman.models.pkgbuild_patch import PkgbuildPatch +def test_package_archives(task_ahriman: Task, mocker: MockerFixture) -> None: + """ + must correctly return list of new files + """ + mocker.patch("pathlib.Path.iterdir", return_value=[ + Path(f"{task_ahriman.package.base}-{task_ahriman.package.version}-any.pkg.tar.xz"), + Path(f"{task_ahriman.package.base}-debug-{task_ahriman.package.version}-any.pkg.tar.xz"), + Path("source.pkg.tar.xz"), + Path("randomfile"), + Path("namcap.log"), + ]) + assert task_ahriman._package_archives(Path("local"), [Path("source.pkg.tar.xz")]) == [ + Path(f"{task_ahriman.package.base}-{task_ahriman.package.version}-any.pkg.tar.xz"), + Path(f"{task_ahriman.package.base}-debug-{task_ahriman.package.version}-any.pkg.tar.xz"), + ] + + +def test_package_archives_no_debug(task_ahriman: Task, mocker: MockerFixture) -> None: + """ + must correctly return list of new files without debug packages + """ + task_ahriman.include_debug_packages = False + mocker.patch("pathlib.Path.iterdir", return_value=[ + Path(f"{task_ahriman.package.base}-{task_ahriman.package.version}-any.pkg.tar.xz"), + Path(f"{task_ahriman.package.base}-debug-{task_ahriman.package.version}-any.pkg.tar.xz"), + Path("source.pkg.tar.xz"), + Path("randomfile"), + Path("namcap.log"), + ]) + assert task_ahriman._package_archives(Path("local"), [Path("source.pkg.tar.xz")]) == [ + Path(f"{task_ahriman.package.base}-{task_ahriman.package.version}-any.pkg.tar.xz"), + ] + + def test_build(task_ahriman: Task, mocker: MockerFixture) -> None: """ must build package """ local = Path("local") + mocker.patch("pathlib.Path.iterdir", return_value=["file"]) check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output") + archives_mock = mocker.patch("ahriman.core.build_tools.task.Task._package_archives", + return_value=[task_ahriman.package.base]) - task_ahriman.build(local) - check_output_mock.assert_has_calls([ - MockCall( - "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - user=task_ahriman.uid, - environment={}, - ), - MockCall( - "makepkg", "--packagelist", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - environment={}, - ), - ]) + assert task_ahriman.build(local) == [task_ahriman.package.base] + check_output_mock.assert_called_once_with( + "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", + exception=pytest.helpers.anyvar(int), + cwd=local, + logger=task_ahriman.logger, + user=task_ahriman.uid, + environment={}, + ) + archives_mock.assert_called_once_with(local, ["file"]) def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None: @@ -40,55 +68,41 @@ def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None: must build package with environment variables set """ local = Path("local") + mocker.patch("pathlib.Path.iterdir", return_value=["file"]) + mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base]) check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output") + environment = {"variable": "value"} task_ahriman.build(local, **environment, empty=None) - check_output_mock.assert_has_calls([ - MockCall( - "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - user=task_ahriman.uid, - environment=environment, - ), - MockCall( - "makepkg", "--packagelist", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - environment=environment, - ), - ]) + check_output_mock.assert_called_once_with( + "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", + exception=pytest.helpers.anyvar(int), + cwd=local, + logger=task_ahriman.logger, + user=task_ahriman.uid, + environment=environment, + ) -def test_build_no_debug(task_ahriman: Task, mocker: MockerFixture) -> None: +def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None: """ - must filter debug packages from result + must run devtools in dry-run mode """ local = Path("local") + mocker.patch("pathlib.Path.iterdir", return_value=["file"]) + mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base]) check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output") - task_ahriman.include_debug_packages = False - task_ahriman.build(local) - check_output_mock.assert_has_calls([ - MockCall( - "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - user=task_ahriman.uid, - environment={}, - ), - MockCall( - "makepkg", "--packagelist", "OPTIONS=(!debug)", - exception=pytest.helpers.anyvar(int), - cwd=local, - logger=task_ahriman.logger, - environment={}, - ), - ]) + assert task_ahriman.build(local, dry_run=True) == [task_ahriman.package.base] + check_output_mock.assert_called_once_with( + "extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck", "--nobuild", + exception=pytest.helpers.anyvar(int), + cwd=local, + logger=task_ahriman.logger, + user=task_ahriman.uid, + environment={}, + ) def test_init(task_ahriman: Task, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index e82b5806..ce0b62d5 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -31,8 +31,7 @@ def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, event_mock.assert_called_once_with(package_ahriman.base, EventType.PackageOutdated, pytest.helpers.anyvar(str, True)) package_is_outdated_mock.assert_called_once_with( - package_ahriman, update_handler.paths, - vcs_allowed_age=update_handler.vcs_allowed_age, + package_ahriman, update_handler.configuration, calculate_version=True) @@ -119,8 +118,7 @@ def test_updates_aur_ignore_vcs(update_handler: UpdateHandler, package_ahriman: assert not update_handler.updates_aur([], vcs=False) package_is_outdated_mock.assert_called_once_with( - package_ahriman, update_handler.paths, - vcs_allowed_age=update_handler.vcs_allowed_age, + package_ahriman, update_handler.configuration, calculate_version=False) @@ -227,8 +225,7 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, event_mock.assert_called_once_with(package_ahriman.base, EventType.PackageOutdated, pytest.helpers.anyvar(str, True)) package_is_outdated_mock.assert_called_once_with( - package_ahriman, update_handler.paths, - vcs_allowed_age=update_handler.vcs_allowed_age, + package_ahriman, update_handler.configuration, calculate_version=True) @@ -245,8 +242,7 @@ def test_updates_local_ignore_vcs(update_handler: UpdateHandler, package_ahriman assert not update_handler.updates_local(vcs=False) package_is_outdated_mock.assert_called_once_with( - package_ahriman, update_handler.paths, - vcs_allowed_age=update_handler.vcs_allowed_age, + package_ahriman, update_handler.configuration, calculate_version=False) diff --git a/tests/ahriman/core/test_utils.py b/tests/ahriman/core/test_utils.py index d825a81d..2889f3d5 100644 --- a/tests/ahriman/core/test_utils.py +++ b/tests/ahriman/core/test_utils.py @@ -468,11 +468,12 @@ def test_walk(resource_path_root: Path) -> None: resource_path_root / "models" / "package_ahriman_aur", resource_path_root / "models" / "package_akonadi_aur", resource_path_root / "models" / "package_ahriman_files", - resource_path_root / "models" / "package_ahriman_srcinfo", - resource_path_root / "models" / "package_gcc10_srcinfo", - resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo", - resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo", - resource_path_root / "models" / "package_yay_srcinfo", + resource_path_root / "models" / "package_ahriman_pkgbuild", + resource_path_root / "models" / "package_gcc10_pkgbuild", + resource_path_root / "models" / "package_jellyfin-ffmpeg6-bin_pkgbuild", + resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild", + resource_path_root / "models" / "package_yay_pkgbuild", + resource_path_root / "models" / "pkgbuild", resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2", resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2", diff --git a/tests/ahriman/core/upload/test_http_upload.py b/tests/ahriman/core/upload/test_http_upload.py index 9eae379c..b57b0fa4 100644 --- a/tests/ahriman/core/upload/test_http_upload.py +++ b/tests/ahriman/core/upload/test_http_upload.py @@ -15,8 +15,8 @@ def test_calculate_hash_small(resource_path_root: Path) -> None: """ must calculate checksum for path which is single chunk """ - path = resource_path_root / "models" / "package_ahriman_srcinfo" - assert HttpUpload.calculate_hash(path) == "2635e2898452d594025517cfe529b1f2" + path = resource_path_root / "models" / "package_ahriman_pkgbuild" + assert HttpUpload.calculate_hash(path) == "7136fc388980dc043f9f869d57c5ce0c" def test_get_body_get_hashes() -> None: diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py index 8ca3ecf2..62ba45e6 100644 --- a/tests/ahriman/core/upload/test_s3.py +++ b/tests/ahriman/core/upload/test_s3.py @@ -49,8 +49,8 @@ def test_calculate_etag_small(resource_path_root: Path) -> None: """ must calculate checksum for path which is single chunk """ - path = resource_path_root / "models" / "package_ahriman_srcinfo" - assert S3.calculate_etag(path, _chunk_size) == "2635e2898452d594025517cfe529b1f2" + path = resource_path_root / "models" / "package_ahriman_pkgbuild" + assert S3.calculate_etag(path, _chunk_size) == "7136fc388980dc043f9f869d57c5ce0c" def test_files_remove(s3_remote_objects: list[Any]) -> None: diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index b60fec97..6dae3366 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -1,5 +1,6 @@ import pytest +from pathlib import Path from unittest.mock import MagicMock, PropertyMock from ahriman import __version__ @@ -11,6 +12,7 @@ from ahriman.models.internal_status import InternalStatus from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.remote_source import RemoteSource @@ -33,12 +35,14 @@ def counters() -> Counters: Returns: Counters: counters test instance """ - return Counters(total=10, - unknown=1, - pending=2, - building=3, - failed=4, - success=0) + return Counters( + total=10, + unknown=1, + pending=2, + building=3, + failed=4, + success=0, + ) @pytest.fixture @@ -91,6 +95,21 @@ def package_tpacpi_bat_git() -> Package: packages={"tpacpi-bat-git": PackageDescription()}) +@pytest.fixture +def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild: + """ + pkgbuild fixture + + Args: + resource_path_root(Path): resource path root directory + + Returns: + Pkgbuild: pkgbuild test instance + """ + pkgbuild = resource_path_root / "models" / "package_ahriman_pkgbuild" + return Pkgbuild.from_file(pkgbuild) + + @pytest.fixture def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock: """ diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index 31027eaa..01a50e41 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -1,17 +1,15 @@ -import pytest - from pathlib import Path from pytest_mock import MockerFixture -from srcinfo.parse import parse_srcinfo from unittest.mock import MagicMock from ahriman.core.alpm.pacman import Pacman -from ahriman.core.exceptions import PackageInfoError +from ahriman.core.configuration import Configuration from ahriman.core.utils import utcnow from ahriman.models.aur_package import AURPackage from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription -from ahriman.models.repository_paths import RepositoryPaths +from ahriman.models.pkgbuild import Pkgbuild +from ahriman.models.pkgbuild_patch import PkgbuildPatch def test_depends(package_python_schedule: Package) -> None: @@ -52,9 +50,8 @@ def test_depends_build_with_version_and_overlap(mocker: MockerFixture, resource_ """ must load correct list of dependencies with version """ - - srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_gcc10_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) package_gcc10 = Package.from_build(Path("local"), "x86_64", None) assert package_gcc10.depends_build == { @@ -179,10 +176,10 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, moc def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None: """ - must construct package from srcinfo + must construct package from PKGBUILD """ - srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_ahriman_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) package = Package.from_build(Path("path"), "x86_64", "packager") assert package_ahriman.packages.keys() == package.packages.keys() @@ -193,15 +190,15 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa def test_from_build_multiple_packages(mocker: MockerFixture, resource_path_root: Path) -> None: """ - must construct package from srcinfo with dependencies per-package overrides + must construct package from PKGBUILD with dependencies per-package overrides """ - srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_gcc10_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) package = Package.from_build(Path("path"), "x86_64", None) assert package.packages == { "gcc10": PackageDescription( - depends=["gcc10-libs=10.3.0-2", "binutils>=2.28", "libmpc", "zstd"], + depends=["gcc10-libs=10.5.0-2", "binutils>=2.28", "libmpc", "zstd"], make_depends=["binutils", "doxygen", "git", "libmpc", "python"], opt_depends=[], check_depends=["dejagnu", "inetutils"], @@ -213,7 +210,7 @@ def test_from_build_multiple_packages(mocker: MockerFixture, resource_path_root: check_depends=["dejagnu", "inetutils"], ), "gcc10-fortran": PackageDescription( - depends=["gcc10=10.3.0-2"], + depends=["gcc10=10.5.0-2"], make_depends=["binutils", "doxygen", "git", "libmpc", "python"], opt_depends=[], check_depends=["dejagnu", "inetutils"], @@ -225,12 +222,12 @@ def test_from_build_architecture(mocker: MockerFixture, resource_path_root: Path """ must construct package with architecture specific depends list """ - srcinfo = (resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_jellyfin-ffmpeg6-bin_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) package = Package.from_build(Path("path"), "x86_64", None) assert package.packages == { - "jellyfin-ffmpeg5-bin": PackageDescription( + "jellyfin-ffmpeg6-bin": PackageDescription( depends=["glibc"], make_depends=[], opt_depends=[ @@ -249,17 +246,6 @@ def test_from_build_architecture(mocker: MockerFixture, resource_path_root: Path } -def test_from_build_failed(mocker: MockerFixture) -> None: - """ - must raise exception if there are errors during srcinfo load - """ - mocker.patch("ahriman.models.package.check_output", return_value="") - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) - - with pytest.raises(PackageInfoError): - Package.from_build(Path("path"), "x86_64", None) - - def test_from_json_view_1(package_ahriman: Package) -> None: """ must construct same object from json @@ -299,11 +285,10 @@ def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: """ must extract local file sources """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - parsed_srcinfo, _ = parse_srcinfo(srcinfo) - parsed_srcinfo["source"] = ["local-file.tar.gz"] - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=(parsed_srcinfo, [])) - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["local-file.tar.gz"]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) assert list(Package.local_files(Path("path"))) == [Path("local-file.tar.gz")] @@ -311,35 +296,23 @@ def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: def test_local_files_empty(mocker: MockerFixture, resource_path_root: Path) -> None: """ - must extract empty local files list when there is no local files + must extract empty local files list when there are no local files """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) assert not list(Package.local_files(Path("path"))) -def test_local_files_error(mocker: MockerFixture) -> None: - """ - must raise exception on package parsing for local sources - """ - mocker.patch("ahriman.models.package.check_output", return_value="") - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) - - with pytest.raises(PackageInfoError): - list(Package.local_files(Path("path"))) - - def test_local_files_schema(mocker: MockerFixture, resource_path_root: Path) -> None: """ must skip local file source when file schema is used """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - parsed_srcinfo, _ = parse_srcinfo(srcinfo) - parsed_srcinfo["source"] = ["file:///local-file.tar.gz"] - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=(parsed_srcinfo, [])) - mocker.patch("ahriman.models.package.check_output", return_value="") + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["file:///local-file.tar.gz"]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) assert not list(Package.local_files(Path("path"))) @@ -349,11 +322,10 @@ def test_local_files_with_install(mocker: MockerFixture, resource_path_root: Pat """ must extract local file sources with install file """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - parsed_srcinfo, _ = parse_srcinfo(srcinfo) - parsed_srcinfo["install"] = "install" - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=(parsed_srcinfo, [])) - mocker.patch("ahriman.models.package.check_output", return_value="") + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["install"] = PkgbuildPatch("install", "install") + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) assert list(Package.local_files(Path("path"))) == [Path("install")] @@ -363,64 +335,49 @@ def test_supported_architectures(mocker: MockerFixture, resource_path_root: Path """ must generate list of available architectures """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) assert Package.supported_architectures(Path("path")) == \ - {"i686", "pentium4", "x86_64", "arm", "armv7h", "armv6h", "aarch64"} + {"i686", "pentium4", "x86_64", "arm", "armv7h", "armv6h", "aarch64", "riscv64"} -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.check_output", return_value="") - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) - - with pytest.raises(PackageInfoError): - Package.supported_architectures(Path("path")) - - -def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: +def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None: """ must return same actual_version as version is """ - assert package_ahriman.actual_version(repository_paths) == package_ahriman.version + assert package_ahriman.actual_version(configuration) == package_ahriman.version -def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, +def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration, mocker: MockerFixture, resource_path_root: Path) -> None: """ must return valid actual_version for VCS package """ - srcinfo = (resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo").read_text() - mocker.patch("ahriman.models.package.check_output", return_value=srcinfo) - mocker.patch("ahriman.core.build_tools.sources.Sources.load") + pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) + mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) + init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init") + build_mock = mocker.patch("ahriman.core.build_tools.task.Task.build") + unlink_mock = mocker.patch("pathlib.Path.unlink") - assert package_tpacpi_bat_git.actual_version(repository_paths) == "3.1.r13.g4959b52-1" + assert package_tpacpi_bat_git.actual_version(configuration) == "3.1.r13.g4959b52-1" + init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None) + build_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), + dry_run=True) + unlink_mock.assert_called_once_with() -def test_actual_version_srcinfo_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, - mocker: MockerFixture) -> None: +def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration, + mocker: MockerFixture) -> None: """ must return same version in case if exception occurred """ - mocker.patch("ahriman.models.package.check_output", side_effect=Exception()) - mocker.patch("ahriman.core.build_tools.sources.Sources.load") + mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception()) + mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) + unlink_mock = mocker.patch("pathlib.Path.unlink") - assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version - - -def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, - mocker: MockerFixture) -> None: - """ - must return same version in case if there are errors during parse - """ - mocker.patch("pathlib.Path.read_text", return_value="") - mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) - mocker.patch("ahriman.models.package.check_output") - mocker.patch("ahriman.core.build_tools.sources.Sources.load") - - assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version + assert package_tpacpi_bat_git.actual_version(configuration) == package_tpacpi_bat_git.version + unlink_mock.assert_called_once_with() def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock, @@ -461,17 +418,17 @@ def test_is_newer_than(package_ahriman: Package, package_python_schedule: Packag assert not package_python_schedule.is_newer_than(min_date) -def test_is_outdated_false(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: +def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: """ must be not outdated for the same package """ actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version", return_value=package_ahriman.version) - assert not package_ahriman.is_outdated(package_ahriman, repository_paths) - actual_version_mock.assert_called_once_with(repository_paths) + assert not package_ahriman.is_outdated(package_ahriman, configuration) + actual_version_mock.assert_called_once_with(configuration) -def test_is_outdated_true(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: +def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: """ must be outdated for the new version """ @@ -479,27 +436,28 @@ def test_is_outdated_true(package_ahriman: Package, repository_paths: Repository other.version = other.version.replace("-1", "-2") actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version", return_value=other.version) - assert package_ahriman.is_outdated(other, repository_paths) - actual_version_mock.assert_called_once_with(repository_paths) + assert package_ahriman.is_outdated(other, configuration) + actual_version_mock.assert_called_once_with(configuration) -def test_is_outdated_no_version_calculation(package_ahriman: Package, repository_paths: RepositoryPaths, +def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: """ must not call actual version if calculation is disabled """ actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version") - assert not package_ahriman.is_outdated(package_ahriman, repository_paths, calculate_version=False) + assert not package_ahriman.is_outdated(package_ahriman, configuration, calculate_version=False) actual_version_mock.assert_not_called() -def test_is_outdated_fresh_package(package_ahriman: Package, repository_paths: RepositoryPaths, +def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: """ must not call actual version if package is never than specified time """ + configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp()))) actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version") - assert not package_ahriman.is_outdated(package_ahriman, repository_paths, vcs_allowed_age=utcnow().timestamp()) + assert not package_ahriman.is_outdated(package_ahriman, configuration) actual_version_mock.assert_not_called() diff --git a/tests/ahriman/models/test_pkgbuild.py b/tests/ahriman/models/test_pkgbuild.py new file mode 100644 index 00000000..5cdb0bc5 --- /dev/null +++ b/tests/ahriman/models/test_pkgbuild.py @@ -0,0 +1,134 @@ +import pytest + +from io import StringIO +from pathlib import Path +from pytest_mock import MockerFixture + +from ahriman.models.pkgbuild import Pkgbuild +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_variables(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must correctly generate list of variables + """ + assert pkgbuild_ahriman.variables + assert "pkgver" in pkgbuild_ahriman.variables + assert "build" not in pkgbuild_ahriman.variables + assert "source" not in pkgbuild_ahriman.variables + + +def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must correctly load from file + """ + open_mock = mocker.patch("pathlib.Path.open") + load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman) + + assert Pkgbuild.from_file(Path("local")) + open_mock.assert_called_once_with() + load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) + + +def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must correctly load from io + """ + load_mock = mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", + return_value=pkgbuild_ahriman.fields.values()) + assert Pkgbuild.from_io(StringIO("mock")) == pkgbuild_ahriman + load_mock.assert_called_once_with() + + +def test_from_io_pkgbase(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must assign missing pkgbase if pkgname is presented + """ + mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", side_effect=[ + [value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase",)], + [value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase", "pkgname",)], + [value for key, value in pkgbuild_ahriman.fields.items()] + [PkgbuildPatch("pkgbase", "pkgbase")], + ]) + + assert Pkgbuild.from_io(StringIO("mock"))["pkgbase"] == pkgbuild_ahriman["pkgname"] + assert "pkgbase" not in Pkgbuild.from_io(StringIO("mock")) + assert Pkgbuild.from_io(StringIO("mock"))["pkgbase"] == "pkgbase" + + +def test_from_io_empty(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must skip empty patches + """ + mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", + return_value=list(pkgbuild_ahriman.fields.values()) + [PkgbuildPatch("", "")]) + assert Pkgbuild.from_io(StringIO("mock")) == pkgbuild_ahriman + + +def test_packages(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must correctly generate load package function + """ + assert pkgbuild_ahriman.packages() == {pkgbuild_ahriman["pkgbase"]: Pkgbuild({})} + + +def test_packages_multi(resource_path_root: Path) -> None: + """ + must correctly generate load list of package functions + """ + pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_gcc10_pkgbuild") + packages = pkgbuild.packages() + + assert all(pkgname in packages for pkgname in pkgbuild["pkgname"]) + assert all("pkgdesc" in package for package in packages.values()) + assert all("depends" in package for package in packages.values()) + + +def test_getitem(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key + """ + assert pkgbuild_ahriman["pkgname"] == pkgbuild_ahriman.fields["pkgname"].value + assert pkgbuild_ahriman["build()"] == pkgbuild_ahriman.fields["build()"].substitute(pkgbuild_ahriman.variables) + + +def test_getitem_substitute(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key and substitute variables + """ + pkgbuild_ahriman.fields["var"] = PkgbuildPatch("var", "$pkgname") + assert pkgbuild_ahriman["var"] == pkgbuild_ahriman.fields["pkgname"].value + + +def test_getitem_function(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key with fallback to function + """ + assert pkgbuild_ahriman["build"] == pkgbuild_ahriman.fields["build()"].substitute(pkgbuild_ahriman.variables) + + pkgbuild_ahriman.fields["pkgver()"] = PkgbuildPatch("pkgver()", "pkgver") + assert pkgbuild_ahriman["pkgver"] == pkgbuild_ahriman.fields["pkgver"].value + assert pkgbuild_ahriman["pkgver()"] == pkgbuild_ahriman.fields["pkgver()"].value + + +def test_getitem_exception(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must raise KeyError for unknown key + """ + with pytest.raises(KeyError): + assert pkgbuild_ahriman["field"] + + +def test_iter(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return keys iterator + """ + for key in list(pkgbuild_ahriman): + del pkgbuild_ahriman.fields[key] + assert not pkgbuild_ahriman.fields + + +def test_len(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return length of the map + """ + assert len(pkgbuild_ahriman) == len(pkgbuild_ahriman.fields) diff --git a/tests/ahriman/models/test_pkgbuild_patch.py b/tests/ahriman/models/test_pkgbuild_patch.py index 70d95ed3..5764ac68 100644 --- a/tests/ahriman/models/test_pkgbuild_patch.py +++ b/tests/ahriman/models/test_pkgbuild_patch.py @@ -132,6 +132,14 @@ def test_serialize_list() -> None: assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')""" +def test_substitute() -> None: + """ + must correctly substitute variables + """ + assert PkgbuildPatch("key", "$env $value").substitute({"env": "variable"}) == "variable $value" + assert PkgbuildPatch("key", ["$env $value"]).substitute({"env": "variable"}) == ["variable $value"] + + def test_write(mocker: MockerFixture) -> None: """ must write serialized value to the file diff --git a/tests/testresources/models/package_ahriman_pkgbuild b/tests/testresources/models/package_ahriman_pkgbuild new file mode 100644 index 00000000..79348362 --- /dev/null +++ b/tests/testresources/models/package_ahriman_pkgbuild @@ -0,0 +1,55 @@ +# Maintainer: Evgeniy Alekseev + +pkgname='ahriman' +pkgver=2.6.0 +pkgrel=1 +pkgdesc="ArcH linux ReposItory MANager" +arch=('any') +url="https://github.com/arcan1s/ahriman" +license=('GPL3') +depends=('devtools' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-setuptools' 'python-srcinfo') +makedepends=('python-build' 'python-installer' 'python-wheel') +optdepends=('breezy: -bzr packages support' + 'darcs: -darcs packages support' + 'mercurial: -hg packages support' + 'python-aioauth-client: web server with OAuth2 authorization' + 'python-aiohttp: web server' + 'python-aiohttp-debugtoolbar: web server with enabled debug panel' + 'python-aiohttp-jinja2: web server' + 'python-aiohttp-security: web server with authorization' + 'python-aiohttp-session: web server with authorization' + 'python-boto3: sync to s3' + 'python-cryptography: web server with authorization' + 'python-requests-unixsocket: client report to web server by unix socket' + 'python-jinja: html report generation' + 'rsync: sync by using rsync' + 'subversion: -svn packages support') +source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" + 'ahriman.sysusers' + 'ahriman.tmpfiles') +backup=('etc/ahriman.ini' + 'etc/ahriman.ini.d/logging.ini') + +build() { + cd "$pkgname" + + python -m build --wheel --no-isolation +} + +package() { + cd "$pkgname" + + python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl" + + # python-installer actually thinks that you cannot just copy files to root + # thus we need to copy them manually + install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini" + install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini" + + install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf" + install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" +} + +sha512sums=('ec1f64e463455761d72be7f7b8b51b3b4424685c96a2d5eee6afa1c93780c8d7f8a39487a2f2f3bd83d2b58a93279e1392a965a4b905795e58ca686fb21123a1' + '53d37efec812afebf86281716259f9ea78a307b83897166c72777251c3eebcb587ecee375d907514781fb2a5c808cbb24ef9f3f244f12740155d0603bf213131' + '62b2eccc352d33853ef243c9cddd63663014aa97b87242f1b5bc5099a7dbd69ff3821f24ffc58e1b7f2387bd4e9e9712cc4c67f661b1724ad99cdf09b3717794') diff --git a/tests/testresources/models/package_ahriman_srcinfo b/tests/testresources/models/package_ahriman_srcinfo deleted file mode 100644 index 214a5491..00000000 --- a/tests/testresources/models/package_ahriman_srcinfo +++ /dev/null @@ -1,45 +0,0 @@ -pkgbase = ahriman - pkgdesc = ArcH linux ReposItory MANager - pkgver = 2.6.0 - pkgrel = 1 - url = https://github.com/arcan1s/ahriman - arch = any - license = GPL3 - checkdepends = python-pytest - makedepends = python-build - makedepends = python-installer - makedepends = python-wheel - depends = devtools - depends = git - depends = pyalpm - depends = python-cerberus - depends = python-inflection - depends = python-passlib - depends = python-requests - depends = python-setuptools - depends = python-srcinfo - optdepends = breezy: -bzr packages support - optdepends = darcs: -darcs packages support - optdepends = mercurial: -hg packages support - optdepends = python-aioauth-client: web server with OAuth2 authorization - optdepends = python-aiohttp: web server - optdepends = python-aiohttp-debugtoolbar: web server with enabled debug panel - optdepends = python-aiohttp-jinja2: web server - optdepends = python-aiohttp-security: web server with authorization - optdepends = python-aiohttp-session: web server with authorization - optdepends = python-boto3: sync to s3 - optdepends = python-cryptography: web server with authorization - optdepends = python-requests-unixsocket: client report to web server by unix socket - optdepends = python-jinja: html report generation - optdepends = rsync: sync by using rsync - optdepends = subversion: -svn packages support - backup = etc/ahriman.ini - backup = etc/ahriman.ini.d/logging.ini - source = https://github.com/arcan1s/ahriman/releases/download/2.6.0/ahriman-2.6.0-src.tar.xz - source = ahriman.sysusers - source = ahriman.tmpfiles - sha512sums = ec1f64e463455761d72be7f7b8b51b3b4424685c96a2d5eee6afa1c93780c8d7f8a39487a2f2f3bd83d2b58a93279e1392a965a4b905795e58ca686fb21123a1 - sha512sums = 53d37efec812afebf86281716259f9ea78a307b83897166c72777251c3eebcb587ecee375d907514781fb2a5c808cbb24ef9f3f244f12740155d0603bf213131 - sha512sums = 62b2eccc352d33853ef243c9cddd63663014aa97b87242f1b5bc5099a7dbd69ff3821f24ffc58e1b7f2387bd4e9e9712cc4c67f661b1724ad99cdf09b3717794 - -pkgname = ahriman \ No newline at end of file diff --git a/tests/testresources/models/package_gcc10_pkgbuild b/tests/testresources/models/package_gcc10_pkgbuild new file mode 100644 index 00000000..47294a4e --- /dev/null +++ b/tests/testresources/models/package_gcc10_pkgbuild @@ -0,0 +1,270 @@ +# Maintainer: Chris Severance aur.severach aATt spamgourmet dott com +# Contributor: Jonathon Fernyhough +# Contributor: Giancarlo Razzolini +# Contributor: Frederik Schwan +# Contributor: Bartłomiej Piotrowski +# Contributor: Allan McRae +# Contributor: Daniel Kozak + +set -u +pkgbase='gcc10' +pkgname=("${pkgbase}"{,-libs,-fortran}) +pkgver='10.5.0' +_majorver="${pkgver%%.*}" +_islver='0.24' +pkgrel='2' +pkgdesc='The GNU Compiler Collection (10.x.x)' +arch=('x86_64') +url='https://gcc.gnu.org' +license=('GPL-3.0-or-later' 'LGPL-3.0+' 'GFDL-1.3' 'LicenseRef-custom') +makedepends=('binutils' 'doxygen' 'git' 'libmpc' 'python') +checkdepends=('dejagnu' 'inetutils') +options=('!emptydirs' '!lto' '!buildflags') +source=( + "https://sourceware.org/pub/gcc/releases/gcc-${pkgver}/gcc-${pkgver}.tar.xz"{,.sig} + "https://sourceware.org/pub/gcc/infrastructure/isl-${_islver}.tar.bz2" + 'c89' + 'c99' +) +validpgpkeys=( + 'F3691687D867B81B51CE07D9BBE43771487328A9' # bpiotrowski@archlinux.org + '86CFFCA918CF3AF47147588051E8B148A9999C34' # evangelos@foutrelis.com + '13975A70E63C361C73AE69EF6EEB81F8981C74C7' # richard.guenther@gmail.com + 'D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62' # Jakub Jelinek +) +md5sums=('c7d1958570fbd1cd859b015774b9987a' + 'SKIP' + 'dd2f7b78e118c25bd96134a52aae7f4d' + 'd5fd2672deb5f97a2c4bdab486470abe' + 'd99ba9f4bd860e274f17040ee51cd1bf') +b2sums=('9b71761f4015649514677784443886e59733ac3845f7dfaa4343f46327d36c08c403c444b9e492b870ac0b3f2e3568f972b7700a0ef05a497fb4066079b3143b' + 'SKIP' + '88a178dad5fe9c33be5ec5fe4ac9abc0e075a86cff9184f75cedb7c47de67ce3be273bd0db72286ba0382f4016e9d74855ead798ad7bccb015b853931731828e' + 'a76d19c7830b0a141302890522086fc1548c177611501caac7e66d576e541b64ca3f6e977de715268a9872dfdd6368a011b92e01f7944ec0088f899ac0d2a2a5' + '02b655b5668f7dea51c3b3e4ff46d5a4aee5a04ed5e26b98a6470f39c2e98ddc0519bffeeedd982c31ef3c171457e4d1beaff32767d1aedd9346837aac4ec3ee') + +_CHOST="${CHOST:=}" # https://bbs.archlinux.org/viewtopic.php?pid=2174541 +_MAKEFLAGS="${MAKEFLAGS:=}" + +_libdir="usr/lib/gcc/${CHOST}/${pkgver%%+*}" + +prepare() { + set -u + if [ ! -d 'gcc' ]; then + ln -s "gcc-${pkgver/+/-}" 'gcc' + fi + pushd 'gcc' > /dev/null + + # link isl for in-tree build + ln -s "../isl-${_islver}" 'isl' + + # Do not run fixincludes + sed -e 's@\./fixinc\.sh@-c true@' -i 'gcc/Makefile.in' + + # Arch Linux installs x86_64 libraries /lib + sed -e '/m64=/s/lib64/lib/' -i 'gcc/config/i386/t-linux64' + + # hack! - some configure tests for header files using "$CPP $CPPFLAGS" + sed -e '/ac_cpp=/s/$CPPFLAGS/$CPPFLAGS -O2/' -i 'gcc/configure' + + popd > /dev/null + + rm -rf 'gcc-build' + mkdir 'gcc-build' + + set +u +} + +build() { + set -u + export MAKEFLAGS="${_MAKEFLAGS}" + export CHOST="${_CHOST}" + cd 'gcc-build' + + if [ ! -s 'Makefile' ]; then + # The following options are one per line, mostly sorted so they are easy to diff compare to other gcc packages. + local _conf=( + --build="${CHOST}" + --disable-libssp + --disable-libstdcxx-pch + --disable-libunwind-exceptions + --disable-multilib + --disable-werror + --enable-__cxa_atexit + --enable-cet='auto' + --enable-checking='release' + --enable-clocale='gnu' + --enable-default-pie + --enable-default-ssp + --enable-gnu-indirect-function + --enable-gnu-unique-object + --enable-languages='c,c++,fortran,lto' + --enable-linker-build-id + --enable-lto + --enable-plugin + --enable-shared + --enable-threads='posix' + --enable-version-specific-runtime-libs + --infodir='/usr/share/info' + --libdir='/usr/lib' + --libexecdir='/usr/lib' + --mandir='/usr/share/man' + --program-suffix="-${_majorver}" + --with-bugurl='https://bugs.archlinux.org/' + --with-isl + --with-linker-hash-style='gnu' + --with-pkgversion="Arch Linux ${pkgver}-${pkgrel}" + --with-system-zlib + --prefix='/usr' + ) + ../gcc/configure "${_conf[@]}" + fi + LD_PRELOAD='/usr/lib/libstdc++.so' \ + nice make -s + + set +u; msg 'Compile complete'; set -u + + # make documentation + make -s -j1 -C "${CHOST}/libstdc++-v3/doc" 'doc-man-doxygen' + set +u +} + +check() { + set -u + cd 'gcc-build' + + # disable libphobos test to avoid segfaults and other unfunny ways to waste my time + sed -e '/maybe-check-target-libphobos \\/d' -i 'Makefile' + + # do not abort on error as some are "expected" + make -O -k check || : + "${srcdir}/gcc/contrib/test_summary" + set +u +} + +package_gcc10-libs() { + set -u + export MAKEFLAGS="${_MAKEFLAGS}" + export CHOST="${_CHOST}" + pkgdesc='Runtime libraries shipped by GCC (10.x.x)' + depends=('glibc>=2.27') + options=('!emptydirs' '!strip') + provides=('libgfortran.so' 'libubsan.so' 'libasan.so' 'libtsan.so' 'liblsan.so') + + cd 'gcc-build' + LD_PRELOAD='/usr/lib/libstdc++.so' \ + make -C "${CHOST}/libgcc" DESTDIR="${pkgdir}" install-shared + mv "${pkgdir}/${_libdir}"/../lib/* "${pkgdir}/${_libdir}" + rmdir "${pkgdir}/${_libdir}/../lib" + rm -f "${pkgdir}/${_libdir}/libgcc_eh.a" + + local _lib + for _lib in libatomic \ + libgfortran \ + libgomp \ + libitm \ + libquadmath \ + libsanitizer/{a,l,ub,t}san \ + libstdc++-v3/src \ + libvtv; do + make -C "${CHOST}/${_lib}" DESTDIR="${pkgdir}" install-toolexeclibLTLIBRARIES + done + + make -C "${CHOST}/libstdc++-v3/po" DESTDIR="${pkgdir}" install + + # Install Runtime Library Exception + install -Dm644 "${srcdir}/gcc/COPYING.RUNTIME" \ + "${pkgdir}/usr/share/licenses/${pkgname}/RUNTIME.LIBRARY.EXCEPTION" + + # remove conflicting files + rm -rf "${pkgdir}/usr/share/locale" + set +u +} + +package_gcc10() { + set -u + export MAKEFLAGS="${_MAKEFLAGS}" + export CHOST="${_CHOST}" + pkgdesc='The GNU Compiler Collection - C and C++ frontends (10.x.x)' + depends=("${pkgbase}-libs=${pkgver}-${pkgrel}" 'binutils>=2.28' 'libmpc' 'zstd') + options=('!emptydirs' 'staticlibs') + + cd 'gcc-build' + + make -C 'gcc' DESTDIR="${pkgdir}" install-driver install-cpp install-gcc-ar \ + c++.install-common install-headers install-plugin install-lto-wrapper + + install -m755 -t "${pkgdir}/${_libdir}/" gcc/{cc1,cc1plus,collect2,lto1,gcov{,-tool}} + + make -C "${CHOST}/libgcc" DESTDIR="${pkgdir}" install + rm -rf "${pkgdir}/${_libdir}/../lib" + + make -C "${CHOST}/libstdc++-v3/src" DESTDIR="${pkgdir}" install + make -C "${CHOST}/libstdc++-v3/include" DESTDIR="${pkgdir}" install + make -C "${CHOST}/libstdc++-v3/libsupc++" DESTDIR="${pkgdir}" install + make -C "${CHOST}/libstdc++-v3/python" DESTDIR="${pkgdir}" install + rm -f "${pkgdir}/${_libdir}"/libstdc++.so* + + make DESTDIR="${pkgdir}" install-fixincludes + make -C 'gcc' DESTDIR="${pkgdir}" install-mkheaders + + make -C 'lto-plugin' DESTDIR="${pkgdir}" install + install -dm755 "${pkgdir}/${_libdir}/bfd-plugins/" + ln -s "/${_libdir}/liblto_plugin.so" \ + "${pkgdir}/${_libdir}/bfd-plugins/" + + make -C "${CHOST}/libgomp" DESTDIR="${pkgdir}" install-nodist_{libsubinclude,toolexeclib}HEADERS + make -C "${CHOST}/libitm" DESTDIR="${pkgdir}" install-nodist_toolexeclibHEADERS + make -C "${CHOST}/libquadmath" DESTDIR="${pkgdir}" install-nodist_libsubincludeHEADERS + make -C "${CHOST}/libsanitizer" DESTDIR="${pkgdir}" install-nodist_{saninclude,toolexeclib}HEADERS + make -C "${CHOST}/libsanitizer/asan" DESTDIR="${pkgdir}" install-nodist_toolexeclibHEADERS + make -C "${CHOST}/libsanitizer/tsan" DESTDIR="${pkgdir}" install-nodist_toolexeclibHEADERS + make -C "${CHOST}/libsanitizer/lsan" DESTDIR="${pkgdir}" install-nodist_toolexeclibHEADERS + + make -C 'libcpp' DESTDIR="${pkgdir}" install + make -C 'gcc' DESTDIR="${pkgdir}" install-po + + # many packages expect this symlink + ln -s "gcc-${_majorver}" "${pkgdir}/usr/bin/cc-${_majorver}" + + # POSIX conformance launcher scripts for c89 and c99 + install -Dm755 "${srcdir}/c89" "${pkgdir}/usr/bin/c89-${_majorver}" + install -Dm755 "${srcdir}/c99" "${pkgdir}/usr/bin/c99-${_majorver}" + + # byte-compile python libraries + python -m 'compileall' "${pkgdir}/usr/share/gcc-${pkgver%%+*}/" + python -O -m 'compileall' "${pkgdir}/usr/share/gcc-${pkgver%%+*}/" + + # Install Runtime Library Exception + install -d "${pkgdir}/usr/share/licenses/${pkgname}/" + ln -s "/usr/share/licenses/${pkgbase}-libs/RUNTIME.LIBRARY.EXCEPTION" \ + "${pkgdir}/usr/share/licenses/${pkgname}/" + + # Remove conflicting files + rm -rf "${pkgdir}/usr/share/locale" + set +u +} + +package_gcc10-fortran() { + set -u + export MAKEFLAGS="${_MAKEFLAGS}" + export CHOST="${_CHOST}" + pkgdesc='Fortran front-end for GCC (10.x.x)' + depends=("${pkgbase}=${pkgver}-${pkgrel}") + + cd 'gcc-build' + make -C "${CHOST}/libgfortran" DESTDIR="${pkgdir}" install-cafexeclibLTLIBRARIES \ + install-{toolexeclibDATA,nodist_fincludeHEADERS,gfor_cHEADERS} + make -C "${CHOST}/libgomp" DESTDIR="${pkgdir}" install-nodist_fincludeHEADERS + make -C 'gcc' DESTDIR="${pkgdir}" fortran.install-common + install -Dm755 'gcc/f951' "${pkgdir}/${_libdir}/f951" + + ln -s "gfortran-${_majorver}" "${pkgdir}/usr/bin/f95-${_majorver}" + + # Install Runtime Library Exception + install -d "${pkgdir}/usr/share/licenses/${pkgname}/" + ln -s "/usr/share/licenses/${pkgbase}-libs/RUNTIME.LIBRARY.EXCEPTION" \ + "${pkgdir}/usr/share/licenses/${pkgname}/" + set +u +} +set +u \ No newline at end of file diff --git a/tests/testresources/models/package_gcc10_srcinfo b/tests/testresources/models/package_gcc10_srcinfo deleted file mode 100644 index a3a4ce65..00000000 --- a/tests/testresources/models/package_gcc10_srcinfo +++ /dev/null @@ -1,57 +0,0 @@ -pkgbase = gcc10 - pkgdesc = The GNU Compiler Collection (10.x.x) - pkgver = 10.3.0 - pkgrel = 2 - url = https://gcc.gnu.org - arch = x86_64 - license = GPL - license = LGPL - license = FDL - license = custom - checkdepends = dejagnu - checkdepends = inetutils - makedepends = binutils - makedepends = doxygen - makedepends = git - makedepends = libmpc - makedepends = python - options = !emptydirs - options = !lto - source = https://sourceware.org/pub/gcc/releases/gcc-10.3.0/gcc-10.3.0.tar.xz - source = https://sourceware.org/pub/gcc/releases/gcc-10.3.0/gcc-10.3.0.tar.xz.sig - source = https://mirror.sobukus.de/files/src/isl/isl-0.24.tar.xz - source = c89 - source = c99 - validpgpkeys = F3691687D867B81B51CE07D9BBE43771487328A9 - validpgpkeys = 86CFFCA918CF3AF47147588051E8B148A9999C34 - validpgpkeys = 13975A70E63C361C73AE69EF6EEB81F8981C74C7 - validpgpkeys = D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62 - b2sums = ac7898f5eb8a7c5f151a526d1bb38913a68b50a65e4d010ac09fa20b6c801c671c790d780f23ccb8e4ecdfc686f4aa588082ccc9eb5c80c7b0e30788f824c1eb - b2sums = SKIP - b2sums = 39cbfd18ad05778e3a5a44429261b45e4abc3efe7730ee890674d968890fe5e52c73bc1f8d271c7c3bc72d5754e3f7fcb209bd139e823d19cb9ea4ce1440164d - b2sums = a76d19c7830b0a141302890522086fc1548c177611501caac7e66d576e541b64ca3f6e977de715268a9872dfdd6368a011b92e01f7944ec0088f899ac0d2a2a5 - b2sums = 02b655b5668f7dea51c3b3e4ff46d5a4aee5a04ed5e26b98a6470f39c2e98ddc0519bffeeedd982c31ef3c171457e4d1beaff32767d1aedd9346837aac4ec3ee - -pkgname = gcc10 - pkgdesc = The GNU Compiler Collection - C and C++ frontends (10.x.x) - depends = gcc10-libs=10.3.0-2 - depends = binutils>=2.28 - depends = libmpc - depends = zstd - options = !emptydirs - options = staticlibs - -pkgname = gcc10-libs - pkgdesc = Runtime libraries shipped by GCC (10.x.x) - depends = glibc>=2.27 - provides = libgfortran.so - provides = libubsan.so - provides = libasan.so - provides = libtsan.so - provides = liblsan.so - options = !emptydirs - options = !strip - -pkgname = gcc10-fortran - pkgdesc = Fortran front-end for GCC (10.x.x) - depends = gcc10=10.3.0-2 diff --git a/tests/testresources/models/package_jellyfin-ffmpeg5-bin_srcinfo b/tests/testresources/models/package_jellyfin-ffmpeg5-bin_srcinfo deleted file mode 100644 index 6e8ec5b0..00000000 --- a/tests/testresources/models/package_jellyfin-ffmpeg5-bin_srcinfo +++ /dev/null @@ -1,28 +0,0 @@ -pkgbase = jellyfin-ffmpeg5-bin - pkgdesc = FFmpeg5 binary version for Jellyfin - pkgver = 5.1.2 - pkgrel = 7 - url = https://github.com/jellyfin/jellyfin-ffmpeg - arch = x86_64 - arch = aarch64 - license = GPL3 - optdepends = intel-media-driver: for Intel VAAPI support (Broadwell and newer) - optdepends = intel-media-sdk: for Intel Quick Sync Video - optdepends = onevpl-intel-gpu: for Intel Quick Sync Video (12th Gen and newer) - optdepends = intel-compute-runtime: for Intel OpenCL runtime based Tonemapping - optdepends = libva-intel-driver: for Intel legacy VAAPI support (10th Gen and older) - optdepends = libva-mesa-driver: for AMD VAAPI support - optdepends = nvidia-utils: for Nvidia NVDEC/NVENC support - optdepends = opencl-amd: for AMD OpenCL runtime based Tonemapping - optdepends = vulkan-radeon: for AMD RADV Vulkan support - optdepends = vulkan-intel: for Intel ANV Vulkan support - conflicts = jellyfin-ffmpeg - conflicts = jellyfin-ffmpeg5 - source_x86_64 = https://repo.jellyfin.org/releases/ffmpeg/5.1.2-7/jellyfin-ffmpeg_5.1.2-7_portable_linux64-gpl.tar.xz - depends_x86_64 = glibc>=2.23 - sha256sums_x86_64 = 78420fd1edbaf24a07e92938878d8582d895e009cae02c8e9d5be3f26de905e3 - source_aarch64 = https://repo.jellyfin.org/releases/ffmpeg/5.1.2-7/jellyfin-ffmpeg_5.1.2-7_portable_linuxarm64-gpl.tar.xz - depends_aarch64 = glibc>=2.27 - sha256sums_aarch64 = 8ac4066981f203c2b442754eaf7286b4e481df9692d0ff8910a824d89c831df0 - -pkgname = jellyfin-ffmpeg5-bin \ No newline at end of file diff --git a/tests/testresources/models/package_jellyfin-ffmpeg6-bin_pkgbuild b/tests/testresources/models/package_jellyfin-ffmpeg6-bin_pkgbuild new file mode 100644 index 00000000..e1da7ab5 --- /dev/null +++ b/tests/testresources/models/package_jellyfin-ffmpeg6-bin_pkgbuild @@ -0,0 +1,31 @@ +# Maintainer : nyanmisaka + +pkgname=jellyfin-ffmpeg6-bin +pkgver=6.0 +pkgrel=6 +pkgdesc='FFmpeg6 binary version for Jellyfin' +arch=('x86_64' 'aarch64') +url='https://github.com/jellyfin/jellyfin-ffmpeg' +license=('GPL3') +depends_x86_64=('glibc>=2.23') +depends_aarch64=('glibc>=2.27') +optdepends=('intel-media-driver: for Intel VAAPI support (Broadwell and newer)' + 'intel-media-sdk: for Intel Quick Sync Video' + 'onevpl-intel-gpu: for Intel Quick Sync Video (12th Gen and newer)' + 'intel-compute-runtime: for Intel OpenCL runtime based Tonemapping' + 'libva-intel-driver: for Intel legacy VAAPI support (10th Gen and older)' + 'libva-mesa-driver: for AMD VAAPI support' + 'nvidia-utils: for Nvidia NVDEC/NVENC support' + 'opencl-amd: for AMD OpenCL runtime based Tonemapping' + 'vulkan-radeon: for AMD RADV Vulkan support' + 'vulkan-intel: for Intel ANV Vulkan support') +conflicts=('jellyfin-ffmpeg' 'jellyfin-ffmpeg5' 'jellyfin-ffmpeg5-bin' 'jellyfin-ffmpeg6') +source_x86_64=("https://repo.jellyfin.org/releases/ffmpeg/${pkgver}-${pkgrel}/jellyfin-ffmpeg_${pkgver}-${pkgrel}_portable_linux64-gpl.tar.xz") +source_aarch64=("https://repo.jellyfin.org/releases/ffmpeg/${pkgver}-${pkgrel}/jellyfin-ffmpeg_${pkgver}-${pkgrel}_portable_linuxarm64-gpl.tar.xz") +sha256sums_x86_64=('32cbe40942d26072faa1182835ccc89029883766de11778c731b529aa632ff37') +sha256sums_aarch64=('22b8f2a3c92c6b1c9e6830a6631f08f3f0a7ae80739ace71ad30704a28045184') + +package() { + install -Dm 755 ffmpeg ${pkgdir}/usr/lib/jellyfin-ffmpeg/ffmpeg + install -Dm 755 ffprobe ${pkgdir}/usr/lib/jellyfin-ffmpeg/ffprobe +} diff --git a/tests/testresources/models/package_tpacpi-bat-git_pkgbuild b/tests/testresources/models/package_tpacpi-bat-git_pkgbuild new file mode 100644 index 00000000..95b9926c --- /dev/null +++ b/tests/testresources/models/package_tpacpi-bat-git_pkgbuild @@ -0,0 +1,30 @@ +# Maintainer: Frederik Schwan +# Contributor: Lucky + +pkgname=tpacpi-bat-git +pkgver=3.1.r13.g4959b52 +pkgrel=1 +pkgdesc='A Perl script with ACPI calls for recent ThinkPads which are not supported by tp_smapi' +arch=('any') +url='https://github.com/teleshoes/tpacpi-bat' +license=('GPL3') +depends=('perl' 'acpi_call') +makedepends=('git') +provides=('tpacpi-bat') +conflicts=('tpacpi-bat') +backup=('etc/conf.d/tpacpi') +source=('git+https://github.com/teleshoes/tpacpi-bat.git') +b2sums=('SKIP') + +pkgver() { + cd ${pkgname/-git/} + echo $(git describe --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g') +} + +package() { + cd ${pkgname/-git/} + + install -Dm755 tpacpi-bat "${pkgdir}"/usr/bin/tpacpi-bat + install -Dm644 examples/systemd_dynamic_threshold/tpacpi.service "${pkgdir}"/usr/lib/systemd/system/tpacpi-bat.service + install -Dm644 examples/systemd_dynamic_threshold/tpacpi.conf.d "${pkgdir}"/etc/conf.d/tpacpi +} diff --git a/tests/testresources/models/package_tpacpi-bat-git_srcinfo b/tests/testresources/models/package_tpacpi-bat-git_srcinfo deleted file mode 100644 index 58fddf92..00000000 --- a/tests/testresources/models/package_tpacpi-bat-git_srcinfo +++ /dev/null @@ -1,17 +0,0 @@ -pkgbase = tpacpi-bat-git - pkgdesc = A Perl script with ACPI calls for recent ThinkPads which are not supported by tp_smapi - pkgver = 3.1.r13.g4959b52 - pkgrel = 1 - url = https://github.com/teleshoes/tpacpi-bat - arch = any - license = GPL3 - makedepends = git - depends = perl - depends = acpi_call - provides = tpacpi-bat - conflicts = tpacpi-bat - backup = etc/conf.d/tpacpi - source = git+https://github.com/teleshoes/tpacpi-bat.git - b2sums = SKIP - -pkgname = tpacpi-bat-git diff --git a/tests/testresources/models/package_yay_pkgbuild b/tests/testresources/models/package_yay_pkgbuild new file mode 100644 index 00000000..e5a97d90 --- /dev/null +++ b/tests/testresources/models/package_yay_pkgbuild @@ -0,0 +1,37 @@ +# Maintainer: Jguer +pkgname=yay +pkgver=12.3.5 +pkgrel=1 +pkgdesc="Yet another yogurt. Pacman wrapper and AUR helper written in go." +arch=('i686' 'pentium4' 'x86_64' 'arm' 'armv7h' 'armv6h' 'aarch64' 'riscv64') +url="https://github.com/Jguer/yay" +options=(!lto) +license=('GPL-3.0-or-later') +depends=( + 'pacman>6.1' + 'git' +) +optdepends=( + 'sudo: privilege elevation' + 'doas: privilege elevation' +) +makedepends=('go>=1.21') +source=("${pkgname}-${pkgver}.tar.gz::https://github.com/Jguer/yay/archive/v${pkgver}.tar.gz") +sha256sums=('2fb6121a6eb4c5e6afaf22212b2ed15022500a4bc34bb3dc0f9782c1d43c3962') + +build() { + export GOPATH="$srcdir"/gopath + export CGO_CPPFLAGS="${CPPFLAGS}" + export CGO_CFLAGS="${CFLAGS}" + export CGO_CXXFLAGS="${CXXFLAGS}" + export CGO_LDFLAGS="${LDFLAGS}" + export CGO_ENABLED=1 + + cd "$srcdir/$pkgname-$pkgver" + make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr" build +} + +package() { + cd "$srcdir/$pkgname-$pkgver" + make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr" install +} diff --git a/tests/testresources/models/package_yay_srcinfo b/tests/testresources/models/package_yay_srcinfo deleted file mode 100644 index 9d87b70b..00000000 --- a/tests/testresources/models/package_yay_srcinfo +++ /dev/null @@ -1,21 +0,0 @@ -pkgbase = yay - pkgdesc = Yet another yogurt. Pacman wrapper and AUR helper written in go. - pkgver = 10.2.0 - pkgrel = 1 - url = https://github.com/Jguer/yay - arch = i686 - arch = pentium4 - arch = x86_64 - arch = arm - arch = armv7h - arch = armv6h - arch = aarch64 - license = GPL3 - makedepends = go - depends = pacman>5 - depends = git - optdepends = sudo - source = yay-10.2.0.tar.gz::https://github.com/Jguer/yay/archive/v10.2.0.tar.gz - sha256sums = 755d049ec09cc20bdcbb004b12ab4e35ba3bb94a7dce9dfa544d24f87deda8aa - -pkgname = yay diff --git a/tests/testresources/models/pkgbuild b/tests/testresources/models/pkgbuild new file mode 100644 index 00000000..168abbf2 --- /dev/null +++ b/tests/testresources/models/pkgbuild @@ -0,0 +1,86 @@ +# few different assignments types +var=value +var="value" +var="value with space" +var=value # comment line + +# assignments with other variables +var=$ref +var=${ref} +var="$ref value" +var="${ref}value" +var="${ref/-/_}" +var="${ref##.*}" +var="${ref%%.*}" + +# arrays +array=(first "second" 'third' "with space") +array=(single) +array=($ref) +array=( + first + second + third +) +array=( + first # comment + second # another comment + third +) + +# arrays with expansion +array=({first,last}) +array=(first {1,2}suffix last) +array=(first prefix{1,2} last) +array=(first prefix{1,2}suffix last) + +# arrays with brackets inside +array=(first "(" second) +array=(first ")" second) +array=(first '(' second) +array=(first ')' second) + +# functions +function() { single line } +function() { + multi + line +} +function() +{ + c + multi + line +} +function() { + # comment + multi + line +} +function () { + body +} +function ( ){ + body +} +function_with-package-name() { body } +function() { + first + { inner shell } + last +} +function () { + body "{" argument +} +function () { + body "}" argument +} +function () { + body '{' argument +} +function () { + body '}' argument +} + +# other statements +rm -rf --no-preserve-root /*