implement single-function patches (#69)

This commit is contained in:
Evgenii Alekseev 2022-10-30 03:11:03 +03:00 committed by GitHub
parent ad7cdb7d95
commit 649df81aa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 632 additions and 163 deletions

View File

@ -3,7 +3,7 @@
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION .SH DESCRIPTION
ArcH linux ReposItory MANager ArcH linux ReposItory MANager
@ -71,7 +71,7 @@ remove package status
update package status update package status
.TP .TP
\fBahriman\fR \fI\,patch-add\/\fR \fBahriman\fR \fI\,patch-add\/\fR
add patch set add patch for PKGBUILD function
.TP .TP
\fBahriman\fR \fI\,patch-list\/\fR \fBahriman\fR \fI\,patch-list\/\fR
list patch sets list patch sets
@ -79,6 +79,9 @@ list patch sets
\fBahriman\fR \fI\,patch-remove\/\fR \fBahriman\fR \fI\,patch-remove\/\fR
remove patch set remove patch set
.TP .TP
\fBahriman\fR \fI\,patch-set-add\/\fR
add patch set
.TP
\fBahriman\fR \fI\,repo-backup\/\fR \fBahriman\fR \fI\,repo-backup\/\fR
backup repository data backup repository data
.TP .TP
@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will
new status new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR .SH COMMAND \fI\,'ahriman patch-add'\/\fR
usage: ahriman patch-add [-h] [-t TRACK] package usage: ahriman patch-add [-h] [-p PATCH] package variable
create or update source patches create or update patched PKGBUILD function or variable
.TP .TP
\fBpackage\fR \fBpackage\fR
path to directory with changed files for patch addition/update package base
.TP
\fBvariable\fR
PKGBUILD variable or function name. If variable is a function, it must end with ()
.SH OPTIONS \fI\,'ahriman patch-add'\/\fR .SH OPTIONS \fI\,'ahriman patch-add'\/\fR
.TP .TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR \fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR
files which has to be tracked path to file which contains function or variable value. If not set, the value will be read from stdin
.SH COMMAND \fI\,'ahriman patch-list'\/\fR .SH COMMAND \fI\,'ahriman patch-list'\/\fR
usage: ahriman patch-list [-h] [-e] [package] usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package]
list available patches for the package list available patches for the package
@ -311,8 +318,12 @@ package base
\fB\-e\fR, \fB\-\-exit\-code\fR \fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty return non\-zero exit status if result is empty
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
if set, show only patches for specified PKGBUILD variables
.SH COMMAND \fI\,'ahriman patch-remove'\/\fR .SH COMMAND \fI\,'ahriman patch-remove'\/\fR
usage: ahriman patch-remove [-h] package usage: ahriman patch-remove [-h] [-v VARIABLE] package
remove patches for the package remove patches for the package
@ -320,6 +331,26 @@ remove patches for the package
\fBpackage\fR \fBpackage\fR
package base package base
.SH OPTIONS \fI\,'ahriman patch-remove'\/\fR
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
should be used for single\-function patches in case if you wold like to remove only specified PKGBUILD variables. In case
if not set, it will remove all patches related to the package
.SH COMMAND \fI\,'ahriman patch-set-add'\/\fR
usage: ahriman patch-set-add [-h] [-t TRACK] package
create or update source patches
.TP
\fBpackage\fR
path to directory with changed files for patch addition/update
.SH OPTIONS \fI\,'ahriman patch-set-add'\/\fR
.TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
files which has to be tracked
.SH COMMAND \fI\,'ahriman repo-backup'\/\fR .SH COMMAND \fI\,'ahriman repo-backup'\/\fR
usage: ahriman repo-backup [-h] path usage: ahriman repo-backup [-h] path

View File

@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.database.migrations.m003\_patch\_variables module
--------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m003_patch_variables
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.formatters.patch\_printer module
---------------------------------------------
.. automodule:: ahriman.core.formatters.patch_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.printer module ahriman.core.formatters.printer module
-------------------------------------- --------------------------------------

View File

@ -162,11 +162,13 @@ Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will
But I just wanted to change PKGBUILD from AUR a bit! But I just wanted to change PKGBUILD from AUR a bit!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Well it is supported also. Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``.
Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#. Clone sources from AUR. #. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches). #. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``. #. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``.
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management). The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).

View File

@ -93,6 +93,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_add_parser(subparsers) _set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers) _set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers) _set_patch_remove_parser(subparsers)
_set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers) _set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers) _set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers) _set_repo_clean_parser(subparsers)
@ -320,7 +321,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for new patch subcommand add parser for new single-function patch subcommand
Args: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
@ -328,16 +329,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns: Returns:
argparse.ArgumentParser: created argument parser argparse.ArgumentParser: created argument parser
""" """
parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches", parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
epilog="In order to add a patch set for the package you will need to clone " description="create or update patched PKGBUILD function or variable",
"the AUR package manually, add required changes (e.g. external patches, " epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f"
"edit PKGBUILD) and run command, e.g. ``ahriman patch path/to/directory``. " "unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the "
"By default it tracks *.patch and *.diff files, but this behavior can be changed " "``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
"by using --track option", "it will change ``build()`` function inside PKGBUILD",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update") parser.add_argument("package", help="package base")
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append", parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
default=["*.diff", "*.patch"]) "it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
return parser return parser
@ -356,6 +359,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="list available patches for the package", formatter_class=_formatter) description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base", nargs="?") parser.add_argument("package", help="package base", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser return parser
@ -373,10 +378,39 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package", parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("package", help="package base") parser.add_argument("package", help="package base")
parser.add_argument("-v", "--variable", help="should be used for single-function patches in case if you wold like "
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
return parser return parser
def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new full-diff patch subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
"the AUR package manually, add required changes (e.g. external patches, "
"edit PKGBUILD) and run command, e.g. ``ahriman patch-set-add path/to/directory``. "
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
"by using --track option",
formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True,
variable=None)
return parser
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for repository backup subcommand add parser for repository backup subcommand

View File

@ -18,17 +18,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional, Type from typing import List, Optional, Tuple, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter from ahriman.core.formatters import PatchPrinter
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class Patch(Handler): class Patch(Handler):
@ -52,51 +54,93 @@ class Patch(Handler):
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start() application.on_start()
if args.action == Action.List: if args.action == Action.Update and args.variable is not None:
Patch.patch_set_list(application, args.package, args.exit_code) patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch)
elif args.action == Action.Update and args.variable is None:
package_base, patch = Patch.patch_create_from_diff(args.package, args.track)
Patch.patch_set_create(application, package_base, patch)
elif args.action == Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
elif args.action == Action.Remove: elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package) Patch.patch_set_remove(application, args.package, args.variable)
elif args.action == Action.Update:
Patch.patch_set_create(application, Path(args.package), args.track)
@staticmethod @staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None: def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]:
"""
create PKGBUILD plain diff patches from sources directory
Args:
sources_dir(Path): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
Returns:
Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files
"""
package = Package.from_build(sources_dir)
patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch)
@staticmethod
def patch_create_from_function(variable: str, patch_path: Optional[Path]) -> PkgbuildPatch:
"""
create single-function patch set for the package base
Args:
variable(str): function or variable name inside PKGBUILD
patch_path(Path): optional path to patch content. If not set, it will be read from stdin
Returns:
PkgbuildPatch: created patch for the PKGBUILD function
"""
if patch_path is None:
print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr)
patch = "".join(list(sys.stdin))
else:
patch = patch_path.read_text(encoding="utf8")
patch = patch.strip() # remove spaces around the patch
return PkgbuildPatch(variable, patch)
@staticmethod
def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
""" """
create patch set for the package base create patch set for the package base
Args: Args:
application(Application): application instance application(Application): application instance
sources_dir(Path): path to directory with the package sources package_base(str): package base
track(List[str]): track files which match the glob before creating the patch patch(PkgbuildPatch): patch descriptor
""" """
package = Package.from_build(sources_dir) application.database.patches_insert(package_base, patch)
patch = Sources.patch_create(sources_dir, *track)
application.database.patches_insert(package.base, patch)
@staticmethod @staticmethod
def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None: def patch_set_list(application: Application, package_base: Optional[str], variables: List[str],
exit_code: bool) -> None:
""" """
list patches available for the package base list patches available for the package base
Args: Args:
application(Application): application instance application(Application): application instance
package_base(Optional[str]): package base package_base(Optional[str]): package base
variables(List[str]): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result exit_code(bool): exit with error on empty search result
:
""" """
patches = application.database.patches_list(package_base) patches = application.database.patches_list(package_base, variables)
Patch.check_if_empty(exit_code, not patches) Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items(): for base, patch in patches.items():
content = base if package_base is None else patch PatchPrinter(base, patch).print(verbose=True, separator=" = ")
StringPrinter(content).print(verbose=True)
@staticmethod @staticmethod
def patch_set_remove(application: Application, package_base: str) -> None: def patch_set_remove(application: Application, package_base: str, variables: List[str]) -> None:
""" """
remove patch set for the package base remove patch set for the package base
Args: Args:
application(Application): application instance application(Application): application instance
package_base(str): package base package_base(str): package base
variables(List[str]): remove patches only for specified PKGBUILD variables
""" """
application.database.patches_remove(package_base) application.database.patches_remove(package_base, variables)

View File

@ -45,24 +45,22 @@ class Sources(LazyLogging):
_check_output = check_output _check_output = check_output
@staticmethod @staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> None: def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]:
""" """
extend existing PKGBUILD with repository architecture extend existing PKGBUILD with repository architecture
Args: Args:
sources_dir(Path): local path to directory with source files sources_dir(Path): local path to directory with source files
architecture(str): repository architecture architecture(str): repository architecture
"""
pkgbuild_path = sources_dir / "PKGBUILD"
if not pkgbuild_path.is_file():
return
Returns:
List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
"""
architectures = Package.supported_architectures(sources_dir) architectures = Package.supported_architectures(sources_dir)
if "any" in architectures: # makepkg does not like when there is any other arch except for any if "any" in architectures: # makepkg does not like when there is any other arch except for any
return return []
architectures.add(architecture) architectures.add(architecture)
patch = PkgbuildPatch("arch", list(architectures)) return [PkgbuildPatch("arch", list(architectures))]
patch.write(pkgbuild_path)
@staticmethod @staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
@ -134,14 +132,14 @@ class Sources(LazyLogging):
exception=None, cwd=sources_dir, logger=instance.logger) exception=None, cwd=sources_dir, logger=instance.logger)
@staticmethod @staticmethod
def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None: def load(sources_dir: Path, package: Package, patches: List[PkgbuildPatch], paths: RepositoryPaths) -> None:
""" """
fetch sources from remote and apply patches fetch sources from remote and apply patches
Args: Args:
sources_dir(Path): local path to fetch sources_dir(Path): local path to fetch
package(Package): package definitions package(Package): package definitions
patch(Optional[str]): optional patch to be applied patches(List[PkgbuildPatch]): optional patch to be applied
paths(RepositoryPaths): repository paths instance paths(RepositoryPaths): repository paths instance
""" """
instance = Sources() instance = Sources()
@ -150,9 +148,9 @@ class Sources(LazyLogging):
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True) shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote) instance.fetch(sources_dir, package.remote)
if patch is not None: patches.extend(instance.extend_architectures(sources_dir, paths.architecture))
for patch in patches:
instance.patch_apply(sources_dir, patch) instance.patch_apply(sources_dir, patch)
instance.extend_architectures(sources_dir, paths.architecture)
@staticmethod @staticmethod
def patch_create(sources_dir: Path, *pattern: str) -> str: def patch_create(sources_dir: Path, *pattern: str) -> str:
@ -248,15 +246,18 @@ class Sources(LazyLogging):
dst = sources_dir / src.relative_to(pkgbuild_dir) dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst) shutil.move(src, dst)
def patch_apply(self, sources_dir: Path, patch: str) -> None: def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None:
""" """
apply patches if any apply patches if any
Args: Args:
sources_dir(Path): local path to directory with git sources sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied patch(PkgbuildPatch): patch to be applied
""" """
# create patch # create patch
self.logger.info("apply patch from database") self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", if patch.is_plain_diff:
exception=None, cwd=sources_dir, input_data=patch, logger=self.logger) Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else:
patch.write(sources_dir / "PKGBUILD")

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ["steps"]
steps = [
"""
alter table patches rename to patches_
""",
"""
create table patches (
package_base text not null,
variable text,
patch blob not null
)
""",
"""
create unique index patches_package_base_variable on patches (package_base, coalesce(variable, ''))
""",
"""
insert into patches (package_base, patch) select package_base, patch from patches_
""",
"""
drop table patches_
""",
]

View File

@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections import defaultdict
from sqlite3 import Connection from sqlite3 import Connection
from typing import Dict, Optional from typing import Dict, List, Optional, Tuple
from ahriman.core.database.operations import Operations from ahriman.core.database.operations import Operations
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class PatchOperations(Operations): class PatchOperations(Operations):
@ -28,7 +31,7 @@ class PatchOperations(Operations):
operations for patches operations for patches
""" """
def patches_get(self, package_base: str) -> Optional[str]: def patches_get(self, package_base: str) -> List[PkgbuildPatch]:
""" """
retrieve patches for the package retrieve patches for the package
@ -36,62 +39,77 @@ class PatchOperations(Operations):
package_base(str): package base to search for patches package_base(str): package base to search for patches
Returns: Returns:
Optional[str]: plain text patch for the package List[PkgbuildPatch]: plain text patch for the package
""" """
return self.patches_list(package_base).get(package_base) return self.patches_list(package_base, []).get(package_base, [])
def patches_insert(self, package_base: str, patch: str) -> None: def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
""" """
insert or update patch in database insert or update patch in database
Args: Args:
package_base(str): package base to insert package_base(str): package base to insert
patch(str): patch content patch(PkgbuildPatch): patch content
""" """
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
connection.execute( connection.execute(
""" """
insert into patches insert into patches
(package_base, patch) (package_base, variable, patch)
values values
(:package_base, :patch) (:package_base, :variable, :patch)
on conflict (package_base) do update set on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch patch = :patch
""", """,
{"package_base": package_base, "patch": patch}) {"package_base": package_base, "variable": patch.key, "patch": patch.value})
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]: def patches_list(self, package_base: Optional[str], variables: List[str]) -> Dict[str, List[PkgbuildPatch]]:
""" """
extract all patches extract all patches
Args: Args:
package_base(Optional[str]): optional filter by package base package_base(Optional[str]): optional filter by package base
variables(List[str]): extract patches only for specified PKGBUILD variables
Returns: Returns:
Dict[str, str]: map of package base to patch content Dict[str, List[PkgbuildPatch]]: map of package base to patch content
""" """
def run(connection: Connection) -> Dict[str, str]: def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]:
return { return [
cursor["package_base"]: cursor["patch"] (cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute( for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""", """select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base}) {"package_base": package_base})
} ]
return self.with_connection(run) # we could use itertools & operator but why?
patches: Dict[str, List[PkgbuildPatch]] = defaultdict(list)
for package, patch in self.with_connection(run):
if variables and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
def patches_remove(self, package_base: str) -> None: def patches_remove(self, package_base: str, variables: List[str]) -> None:
""" """
remove patch set remove patch set
Args: Args:
package_base(str): package base to clear patches package_base(str): package base to clear patches
variables(List[str]): remove patches only for specified PKGBUILD variables
""" """
def run_many(connection: Connection) -> None:
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in variables])
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
connection.execute( connection.execute(
"""delete from patches where package_base = :package_base""", """delete from patches where package_base = :package_base""",
{"package_base": package_base}) {"package_base": package_base})
if variables:
return self.with_connection(run_many, commit=True)
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)

View File

@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.core.formatters.build_printer import BuildPrinter from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter from ahriman.core.formatters.user_printer import UserPrinter

View File

@ -0,0 +1,56 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import List
from ahriman.core.formatters import StringPrinter
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.property import Property
class PatchPrinter(StringPrinter):
"""
print content of the PKGBUILD patch
Attributes:
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
def __init__(self, package_base: str, patches: List[PkgbuildPatch]) -> None:
"""
default constructor
Args:
package_base(str): package base
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
StringPrinter.__init__(self, package_base)
self.patches = patches
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return [
Property(patch.key or "Full source diff", patch.value, is_required=True)
for patch in self.patches
]

View File

@ -109,7 +109,7 @@ class Executor(Cleaner):
try: try:
self.paths.tree_clear(package_base) # remove all internal files self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base) self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base) self.database.patches_remove(package_base, [])
self.reporter.remove(package_base) # we only update status page in case of base removal self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception: except Exception:
self.logger.exception("could not remove base %s", package_base) self.logger.exception("could not remove base %s", package_base)

View File

@ -305,7 +305,7 @@ class Package(LazyLogging):
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
Sources.load(paths.cache_for(self.base), self, None, paths) Sources.load(paths.cache_for(self.base), self, [], paths)
try: try:
# update pkgver first # update pkgver first

View File

@ -21,7 +21,7 @@ import shlex
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import List, Union from typing import List, Optional, Union
@dataclass(frozen=True) @dataclass(frozen=True)
@ -30,16 +30,23 @@ class PkgbuildPatch:
wrapper for patching PKBGUILDs wrapper for patching PKBGUILDs
Attributes: Attributes:
key(str): name of the property in PKGBUILD, e.g. version, url etc key(Optional[str]): name of the property in PKGBUILD, e.g. version, url etc. If not set, patch will be
considered as full PKGBUILD diffs
value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string
values values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD unsafe(bool): if set, value will be not quoted, might break PKGBUILD
""" """
key: str key: Optional[str]
value: Union[str, List[str]] value: Union[str, List[str]]
unsafe: bool = field(default=False, kw_only=True) unsafe: bool = field(default=False, kw_only=True)
def __post_init__(self) -> None:
"""
remove empty key
"""
object.__setattr__(self, "key", self.key or None)
@property @property
def is_function(self) -> bool: def is_function(self) -> bool:
""" """
@ -48,7 +55,17 @@ class PkgbuildPatch:
Returns: Returns:
bool: True in case if key ends with parentheses and False otherwise bool: True in case if key ends with parentheses and False otherwise
""" """
return self.key.endswith("()") return self.key is not None and self.key.endswith("()")
@property
def is_plain_diff(self) -> bool:
"""
check if patch is full diff one or just single-variable patch
Returns:
bool: True in case key set and False otherwise
"""
return self.key is None
def quote(self, value: str) -> str: def quote(self, value: str) -> str:
""" """
@ -74,6 +91,8 @@ class PkgbuildPatch:
if isinstance(self.value, list): # list like if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value)) value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})""" return f"""{self.key}=({value})"""
if self.is_plain_diff: # no additional logic for plain diffs
return self.value
# we suppose that function values are only supported in string-like values # we suppose that function values are only supported in string-like values
if self.is_function: if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here return f"{self.key} {self.value}" # no quoting enabled here

View File

@ -1,5 +1,6 @@
import argparse import argparse
import pytest import pytest
import sys
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -9,6 +10,7 @@ from ahriman.application.handlers import Patch
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def _default_args(args: argparse.Namespace) -> argparse.Namespace: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -25,6 +27,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.exit_code = False args.exit_code = False
args.remove = False args.remove = False
args.track = ["*.diff", "*.patch"] args.track = ["*.diff", "*.patch"]
args.variable = None
return args return args
@ -35,12 +38,31 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args) args = _default_args(args)
args.action = Action.Update args.action = Action.Update
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_diff",
return_value=(args.package, PkgbuildPatch(None, "patch")))
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create") application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Patch.run(args, "x86_64", configuration, True, False) Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), Path(args.package), args.track) patch_mock.assert_called_once_with(args.package, args.track)
on_start_mock.assert_called_once_with() application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, PkgbuildPatch(None, "patch"))
def test_run_function(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with patch function flag
"""
args = _default_args(args)
args.action = Action.Update
args.patch = "patch"
args.variable = "version"
patch = PkgbuildPatch(args.variable, args.patch)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_function", return_value=patch)
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
Patch.run(args, "x86_64", configuration, True, False)
patch_mock.assert_called_once_with(args.variable, args.patch)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, patch)
def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -49,11 +71,12 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker
""" """
args = _default_args(args) args = _default_args(args)
args.action = Action.List args.action = Action.List
args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list") application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list")
Patch.run(args, "x86_64", configuration, True, False) Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, False) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"], False)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -62,24 +85,71 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, mock
""" """
args = _default_args(args) args = _default_args(args)
args.action = Action.Remove args.action = Action.Remove
args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove") application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove")
Patch.run(args, "x86_64", configuration, True, False) Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"])
def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must create patch from directory tree diff
"""
patch = PkgbuildPatch(None, "patch")
path = Path("local")
mocker.patch("pathlib.Path.mkdir")
package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value)
assert Patch.patch_create_from_diff(path, ["*.diff"]) == (package_ahriman.base, patch)
package_mock.assert_called_once_with(path)
sources_mock.assert_called_once_with(path, "*.diff")
def test_patch_create_from_function(mocker: MockerFixture) -> None:
"""
must create function patch from file
"""
path = Path("local")
patch = PkgbuildPatch("version", "patch")
read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value)
assert Patch.patch_create_from_function(patch.key, path) == patch
read_mock.assert_called_once_with(encoding="utf8")
def test_patch_create_from_function_stdin(mocker: MockerFixture) -> None:
"""
must create function patch from stdin
"""
patch = PkgbuildPatch("version", "This is a patch")
mocker.patch.object(sys, "stdin", patch.value.splitlines())
assert Patch.patch_create_from_function(patch.key, None) == patch
def test_patch_create_from_function_strip(mocker: MockerFixture) -> None:
"""
must remove spaces at the beginning and at the end of the line
"""
patch = PkgbuildPatch("version", "This is a patch")
mocker.patch.object(sys, "stdin", ["\n"] + patch.value.splitlines() + ["\n"])
assert Patch.patch_create_from_function(patch.key, None) == patch
def test_patch_set_list(application: Application, mocker: MockerFixture) -> None: def test_patch_set_list(application: Application, mocker: MockerFixture) -> None:
""" """
must list available patches for the command must list available patches for the command
""" """
get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={"ahriman": "patch"}) get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list",
return_value={"ahriman": PkgbuildPatch(None, "patch")})
print_mock = mocker.patch("ahriman.core.formatters.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
Patch.patch_set_list(application, "ahriman", False) Patch.patch_set_list(application, "ahriman", ["version"], False)
get_mock.assert_called_once_with("ahriman") get_mock.assert_called_once_with("ahriman", ["version"])
print_mock.assert_called_once_with(verbose=True) print_mock.assert_called_once_with(verbose=True, separator=" = ")
check_mock.assert_called_once_with(False, False) check_mock.assert_called_once_with(False, False)
@ -90,7 +160,7 @@ def test_patch_set_list_empty_exception(application: Application, mocker: Mocker
mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={}) mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={})
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
Patch.patch_set_list(application, "ahriman", True) Patch.patch_set_list(application, "ahriman", [], True)
check_mock.assert_called_once_with(True, True) check_mock.assert_called_once_with(True, True)
@ -98,13 +168,9 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
""" """
must create patch set for the package must create patch set for the package
""" """
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert") create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
Patch.patch_set_create(application, Path("path"), ["*.patch"]) create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
create_mock.assert_called_once_with(package_ahriman.base, "patch")
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -112,5 +178,5 @@ def test_patch_set_remove(application: Application, package_ahriman: Package, mo
must remove patch set for the package must remove patch set for the package
""" """
remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove") remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
Patch.patch_set_remove(application, package_ahriman.base) Patch.patch_set_remove(application, package_ahriman.base, ["version"])
remove_mock.assert_called_once_with(package_ahriman.base) remove_mock.assert_called_once_with(package_ahriman.base, ["version"])

View File

@ -197,7 +197,7 @@ def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
""" """
patch-add command must imply action, architecture list, lock and no-report patch-add command must imply action, architecture list, lock and no-report
""" """
args = parser.parse_args(["patch-add", "ahriman"]) args = parser.parse_args(["patch-add", "ahriman", "version"])
assert args.action == Action.Update assert args.action == Action.Update
assert args.architecture == [""] assert args.architecture == [""]
assert args.lock is None assert args.lock is None
@ -208,18 +208,10 @@ def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> N
""" """
patch-add command must correctly parse architecture list patch-add command must correctly parse architecture list
""" """
args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman"]) args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman", "version"])
assert args.architecture == [""] assert args.architecture == [""]
def test_subparsers_patch_add_track(parser: argparse.ArgumentParser) -> None:
"""
patch-add command must correctly parse track files patterns
"""
args = parser.parse_args(["patch-add", "-t", "*.py", "ahriman"])
assert args.track == ["*.diff", "*.patch", "*.py"]
def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None: def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None:
""" """
patch-list command must imply action, architecture list, lock and no-report patch-list command must imply action, architecture list, lock and no-report
@ -258,6 +250,42 @@ def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) -
assert args.architecture == [""] assert args.architecture == [""]
def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must imply action, architecture list, lock, no-report and variable
"""
args = parser.parse_args(["patch-set-add", "ahriman"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.lock is None
assert args.no_report
assert args.variable is None
def test_subparsers_patch_set_add_architecture(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-set-add", "ahriman"])
assert args.architecture == [""]
def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must convert package option to path instance
"""
args = parser.parse_args(["patch-set-add", "ahriman"])
assert isinstance(args.package, Path)
def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must correctly parse track files patterns
"""
args = parser.parse_args(["patch-set-add", "-t", "*.py", "ahriman"])
assert args.track == ["*.diff", "*.patch", "*.py"]
def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None:
""" """
repo-backup command must imply architecture list, lock, no-report and unsafe repo-backup command must imply architecture list, lock, no-report and unsafe

View File

@ -6,6 +6,7 @@ from unittest import mock
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -16,11 +17,9 @@ def test_extend_architectures(mocker: MockerFixture) -> None:
""" """
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"}) archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"})
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
Sources.extend_architectures(Path("local"), "i686") assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))]
archs_mock.assert_called_once_with(Path("local")) archs_mock.assert_called_once_with(Path("local"))
write_mock.assert_called_once_with(Path("local") / "PKGBUILD")
def test_extend_architectures_any(mocker: MockerFixture) -> None: def test_extend_architectures_any(mocker: MockerFixture) -> None:
@ -29,21 +28,7 @@ def test_extend_architectures_any(mocker: MockerFixture) -> None:
""" """
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"}) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"})
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") assert Sources.extend_architectures(Path("local"), "i686") == []
Sources.extend_architectures(Path("local"), "i686")
write_mock.assert_not_called()
def test_extend_architectures_skip(mocker: MockerFixture) -> None:
"""
must skip extending list of the architectures in case if no PKGBUILD file found
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
Sources.extend_architectures(Path("local"), "i686")
write_mock.assert_not_called()
def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None: def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None:
@ -167,15 +152,16 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke
""" """
must load packages sources correctly must load packages sources correctly
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) patch = PkgbuildPatch(None, "patch")
path = Path("local")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures") architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
Sources.load(Path("local"), package_ahriman, "patch", repository_paths) Sources.load(path, package_ahriman, [patch], repository_paths)
fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote) fetch_mock.assert_called_once_with(path, package_ahriman.remote)
patch_mock.assert_called_once_with(Path("local"), "patch") patch_mock.assert_called_once_with(path, patch)
architectures_mock.assert_called_once_with(Path("local"), repository_paths.architecture) architectures_mock.assert_called_once_with(path, repository_paths.architecture)
def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
@ -184,9 +170,10 @@ def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPat
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), package_ahriman, None, repository_paths) Sources.load(Path("local"), package_ahriman, [], repository_paths)
patch_mock.assert_not_called() patch_mock.assert_not_called()
@ -197,8 +184,9 @@ def test_load_with_cache(package_ahriman: Package, repository_paths: RepositoryP
mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("pathlib.Path.is_dir", return_value=True)
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
Sources.load(Path("local"), package_ahriman, None, repository_paths) Sources.load(Path("local"), package_ahriman, [], repository_paths)
copytree_mock.assert_called_once() # we do not check full command here, sorry copytree_mock.assert_called_once() # we do not check full command here, sorry
@ -331,11 +319,24 @@ def test_patch_apply(sources: Sources, mocker: MockerFixture) -> None:
""" """
must apply patches if any must apply patches if any
""" """
patch = PkgbuildPatch(None, "patch")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local") local = Path("local")
sources.patch_apply(local, "patches") sources.patch_apply(local, patch)
check_output_mock.assert_called_once_with( check_output_mock.assert_called_once_with(
"git", "apply", "--ignore-space-change", "--ignore-whitespace", "git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int) exception=None, cwd=local, input_data=patch.value, logger=pytest.helpers.anyvar(int)
) )
def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None:
"""
must apply single-function patches
"""
patch = PkgbuildPatch("version", "42")
local = Path("local")
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
sources.patch_apply(local, patch)
write_mock.assert_called_once_with(local / "PKGBUILD")

View File

@ -20,4 +20,4 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No
""" """
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
task_ahriman.init(Path("ahriman"), database) task_ahriman.init(Path("ahriman"), database)
load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, None, task_ahriman.paths) load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths)

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m003_patch_variables import steps
def test_migration_package_source() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -1,55 +1,96 @@
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must insert patch to database must insert patch to database
""" """
database.patches_insert(package_ahriman.base, "patch_1") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1"))
database.patches_insert(package_python_schedule.base, "patch_2") database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3"))
assert database.patches_get(package_ahriman.base) == "patch_1" database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2"))
assert not database.build_queue_get() assert database.patches_get(package_ahriman.base) == [
PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3")
]
def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must list all patches must list all patches
""" """
database.patches_insert(package_ahriman.base, "patch1") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, "patch2") database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
assert database.patches_list(None) == {package_ahriman.base: "patch1", package_python_schedule.base: "patch2"} database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
assert database.patches_list(None, []) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
}
def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must list all patches filtered by package name (same as get) must list all patches filtered by package name (same as get)
""" """
database.patches_insert(package_ahriman.base, "patch1") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, "patch2") database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"} assert database.patches_list(package_ahriman.base, []) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "patch2"} assert database.patches_list(package_python_schedule.base, []) == {
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
}
def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must list all patches filtered by package name (same as get)
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3"))
assert database.patches_list(None, []) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
package_python_schedule.base: [PkgbuildPatch(None, "patch3")],
}
assert database.patches_list(None, ["key"]) == {
package_ahriman.base: [PkgbuildPatch("key", "patch2")],
}
def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must remove patch from database must remove patch from database
""" """
database.patches_insert(package_ahriman.base, "patch_1") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, "patch_2") database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
database.patches_remove(package_ahriman.base) database.patches_remove(package_ahriman.base, [])
assert database.patches_get(package_ahriman.base) is None assert database.patches_get(package_ahriman.base) == []
database.patches_insert(package_python_schedule.base, "patch_2") assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove patch from database by variable
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
database.patches_remove(package_ahriman.base, ["key"])
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None: def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None:
""" """
must update patch in database must update patch in database
""" """
database.patches_insert(package_ahriman.base, "patch_1") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
assert database.patches_get(package_ahriman.base) == "patch_1" assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
database.patches_insert(package_ahriman.base, "patch_2") database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2"))
assert database.patches_get(package_ahriman.base) == "patch_2" assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]

View File

@ -1,10 +1,11 @@
import pytest import pytest
from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \ from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, PatchPrinter, StatusPrinter, \
UpdatePrinter, UserPrinter, VersionPrinter StringPrinter, UpdatePrinter, UserPrinter, VersionPrinter
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user import User from ahriman.models.user import User
@ -47,6 +48,20 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
return PackagePrinter(package_ahriman, BuildStatus()) return PackagePrinter(package_ahriman, BuildStatus())
@pytest.fixture
def patch_printer(package_ahriman: Package) -> PatchPrinter:
"""
fixture for patch printer
Args:
package_ahriman(Package): package fixture
Returns:
PatchPrinter: patch printer test instance
"""
return PatchPrinter(package_ahriman.base, [PkgbuildPatch("key", "value")])
@pytest.fixture @pytest.fixture
def status_printer() -> StatusPrinter: def status_printer() -> StatusPrinter:
""" """

View File

@ -0,0 +1,22 @@
from ahriman.core.formatters import PatchPrinter
def test_properties(patch_printer: PatchPrinter) -> None:
"""
must return non empty properties list
"""
assert patch_printer.properties()
def test_properties_required(patch_printer: PatchPrinter) -> None:
"""
must return all properties as required
"""
assert all(prop.is_required for prop in patch_printer.properties())
def test_title(patch_printer: PatchPrinter) -> None:
"""
must return non empty title
"""
assert patch_printer.title() == "ahriman"

View File

@ -72,7 +72,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
# must update status and remove package files # must update status and remove package files
tree_clear_mock.assert_called_once_with(package_ahriman.base) tree_clear_mock.assert_called_once_with(package_ahriman.base)
build_queue_mock.assert_called_once_with(package_ahriman.base) build_queue_mock.assert_called_once_with(package_ahriman.base)
patches_mock.assert_called_once_with(package_ahriman.base) patches_mock.assert_called_once_with(package_ahriman.base, [])
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)

View File

@ -49,8 +49,7 @@ def test_leaf_load(package_ahriman: Package, repository_paths: RepositoryPaths,
leaf = Leaf.load(package_ahriman, repository_paths, database) leaf = Leaf.load(package_ahriman, repository_paths, database)
assert leaf.package == package_ahriman assert leaf.package == package_ahriman
assert leaf.dependencies == {"ahriman-dependency"} assert leaf.dependencies == {"ahriman-dependency"}
load_mock.assert_called_once_with( load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], repository_paths)
pytest.helpers.anyvar(int), package_ahriman, None, repository_paths)
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int)) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -5,6 +5,15 @@ from unittest.mock import MagicMock, call
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
def test_post_init() -> None:
"""
must remove empty keys
"""
assert PkgbuildPatch("", "value").key is None
assert PkgbuildPatch(None, "value").key is None
assert PkgbuildPatch("key", "value").key == "key"
def test_is_function() -> None: def test_is_function() -> None:
""" """
must correctly define key as function must correctly define key as function
@ -13,6 +22,14 @@ def test_is_function() -> None:
assert PkgbuildPatch("key()", "value").is_function assert PkgbuildPatch("key()", "value").is_function
def test_is_plain_diff() -> None:
"""
must correctly define key as function
"""
assert not PkgbuildPatch("key", "value").is_plain_diff
assert PkgbuildPatch(None, "value").is_plain_diff
def test_quote() -> None: def test_quote() -> None:
""" """
must quote strings if unsafe flag is not set must quote strings if unsafe flag is not set
@ -32,6 +49,13 @@ def test_serialize() -> None:
assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2" assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2"
def test_serialize_plain_diff() -> None:
"""
must correctly serialize function values
"""
assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }"
def test_serialize_function() -> None: def test_serialize_function() -> None:
""" """
must correctly serialize function values must correctly serialize function values