automatically bump pkgrel on version duplicates

The new --(no-)increment flag has been added to add, update and rebuild
subcommands. In case if it is true and package version is the same as in
repository, it will automatically bump pkgrel appending (increasing)
minor part of it (e.g. 1.0.0-1 -> 1.0.0-1.1).

Inn order to implement this, the shadow (e.g. it will not store it in
database) patch for pkgrel will be created
This commit is contained in:
2023-08-07 00:04:08 +03:00
parent 368db86dde
commit 3b3ef43863
27 changed files with 288 additions and 51 deletions

View File

@ -256,6 +256,8 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
@ -577,6 +579,8 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
@ -751,6 +755,8 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", 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("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--local", help="enable or disable checking of local packages for updates",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates",

View File

@ -123,7 +123,8 @@ class ApplicationRepository(ApplicationProperties):
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
bump_pkgrel: bool = False) -> Result:
"""
run package updates
@ -131,6 +132,7 @@ class ApplicationRepository(ApplicationProperties):
updates(Iterable[Package]): list of packages to update
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
Returns:
Result: update result
@ -150,7 +152,7 @@ class ApplicationRepository(ApplicationProperties):
tree = Tree.resolve(updates)
for num, level in enumerate(tree):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level, packagers)
build_result = self.repository.process_build(level, packagers, bump_pkgrel=bump_pkgrel)
packages = self.repository.packages_built()
process_update(packages, build_result)

View File

@ -52,5 +52,5 @@ class Add(Handler):
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -53,7 +53,7 @@ class Rebuild(Handler):
application.print_updates(updates, log_fn=print)
return
result = application.update(updates, args.username)
result = application.update(updates, args.username, bump_pkgrel=args.increment)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -54,7 +54,7 @@ class Update(Handler):
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Update.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -26,6 +26,7 @@ from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_paths import RepositoryPaths
@ -34,6 +35,11 @@ class Task(LazyLogging):
base package build task
Attributes:
archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture
build_command(str): build command
makechroootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
uid(int): uid of the repository owner user
@ -41,18 +47,21 @@ class Task(LazyLogging):
_check_output = check_output
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
def __init__(self, package: Package, configuration: Configuration, architecture: str,
paths: RepositoryPaths) -> None:
"""
default constructor
Args:
package(Package): package definitions
configuration(Configuration): configuration instance
architecture(str): repository architecture
paths(RepositoryPaths): repository paths instance
"""
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
self.architecture = architecture
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command")
@ -98,12 +107,23 @@ class Task(LazyLogging):
).splitlines()
return [Path(package) for package in packages]
def init(self, sources_dir: Path, database: SQLite) -> None:
def init(self, sources_dir: Path, database: SQLite, local_version: str | None) -> None:
"""
fetch package from git
Args:
sources_dir(Path): local path to fetch
database(SQLite): database instance
local_version(str | None): local version of the package. If set and equal to current version, it will
automatically bump pkgrel
"""
Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)
if local_version is None:
return # there is no local package or pkgrel increment is disabled
# load fresh package
loaded_package = Package.from_build(sources_dir, self.architecture, None)
if (pkgrel := loaded_package.next_pkgrel(local_version)) is not None:
self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
patch = PkgbuildPatch("pkgrel", pkgrel)
patch.write(sources_dir / "PKGBUILD")

View File

@ -64,7 +64,8 @@ class Executor(Cleaner):
"""
raise NotImplementedError
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
bump_pkgrel: bool = False) -> Result:
"""
build packages
@ -72,20 +73,23 @@ class Executor(Cleaner):
updates(Iterable[Package]): list of packages properties to build
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
Returns:
Result: build result
"""
def build_single(package: Package, local_path: Path, packager_id: str | None) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths)
task.init(local_path, self.database)
task = Task(package, self.configuration, self.architecture, self.paths)
local_version = local_versions.get(package.base) if bump_pkgrel else None
task.init(local_path, self.database, local_version)
built = task.build(local_path, packager_id)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
packagers = packagers or Packagers()
local_versions = {package.base: package.version for package in self.packages()}
result = Result()
for single in updates:

View File

@ -25,6 +25,7 @@ import logging
import os
import re
import requests
import selectors
import subprocess
from collections.abc import Callable, Generator, Iterable
@ -48,6 +49,7 @@ __all__ = [
"filter_json",
"full_version",
"package_like",
"parse_version",
"partition",
"pretty_datetime",
"pretty_size",
@ -107,15 +109,24 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
channel: IO[str] | None = getattr(proc, channel_name, None)
return channel if channel is not None else io.StringIO()
def log(single: str) -> None:
if logger is not None:
logger.debug(single)
# wrapper around selectors polling
def poll(sel: selectors.BaseSelector) -> Generator[str, None, None]:
for key, _ in sel.select(): # we don't need to check mask here because we have only subscribed on reading
line = key.fileobj.readline() # type: ignore[union-attr]
if not line: # in case of empty line we remove selector as there is no data here anymore
sel.unregister(key.fileobj)
continue
line = line.rstrip()
if logger is not None:
logger.debug(line)
if key.data == "stdout":
yield line # yield only stdout data
environment = environment or {}
if user is not None:
environment["HOME"] = getpwuid(user).pw_dir
# FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, env=environment, text=True, encoding="utf8", bufsize=1) as process:
if input_data is not None:
@ -123,16 +134,13 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
input_channel.write(input_data)
input_channel.close()
# read stdout and append to output result
result: list[str] = []
for line in iter(get_io(process, "stdout").readline, ""):
line = line.strip()
result.append(line)
log(line)
selector = selectors.DefaultSelector()
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
# read stderr and write info to logs
for line in iter(get_io(process, "stderr").readline, ""):
log(line.strip())
result: list[str] = []
while selector.get_map(): # while there are unread selectors, keep reading
result.extend(poll(selector))
process.terminate() # make sure that process is terminated
status_code = process.wait()
@ -275,6 +283,25 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig")
def parse_version(version: str) -> tuple[str | None, str, str]:
"""
parse version and returns its components
Args:
version(str): full version string
Returns:
tuple[str | None, str, str]: epoch if any, pkgver and pkgrel variables
"""
if ":" in version:
epoch, version = version.split(":", maxsplit=1)
else:
epoch = None
pkgver, pkgrel = version.rsplit("-", maxsplit=1)
return epoch, pkgver, pkgrel
def partition(source: list[T], predicate: Callable[[T], bool]) -> tuple[list[T], list[T]]:
"""
partition list into two based on predicate, based on # https://docs.python.org/dev/library/itertools.html#itertools-recipes

View File

@ -34,7 +34,7 @@ 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.log import LazyLogging
from ahriman.core.util import check_output, dataclass_view, full_version, srcinfo_property_list, utcnow
from ahriman.core.util import check_output, 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.remote_source import RemoteSource
@ -507,6 +507,35 @@ class Package(LazyLogging):
result: int = vercmp(self.version, remote_version)
return result < 0
def next_pkgrel(self, local_version: str) -> str | None:
"""
generate next pkgrel variable. The package release will be incremented if ``local_version`` is more or equal to
the ``Package.version``; in this case the function will return new pkgrel value, otherwise ``None`` will be
returned
Args:
local_version(str): locally stored package version
Returns:
str | None: new generated package release version if any. In case if the release contains dot (e.g. 1.2),
the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor version
equals to 1 will be appended
"""
epoch, pkgver, _ = parse_version(self.version)
local_epoch, local_pkgver, local_pkgrel = parse_version(local_version)
if epoch != local_epoch or pkgver != local_pkgver:
return None # epoch or pkgver are different, keep upstream pkgrel
if vercmp(self.version, local_version) > 0:
return None # upstream version is newer than local one, keep upstream pkgrel
if "." in local_pkgrel:
major, minor = local_pkgrel.rsplit(".", maxsplit=1)
else:
major, minor = local_pkgrel, "0"
return f"{major}.{int(minor) + 1}"
def pretty_print(self) -> str:
"""
generate pretty string representation

View File

@ -48,6 +48,7 @@ def _info() -> dict[str, Any]:
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support.
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options.