Compare commits

..

12 Commits

Author SHA1 Message Date
528aba4b42 fix: explicitly process list of packages
Small workaround to remove debug packages from being processed
2024-08-08 18:06:41 +03:00
6b64089d59 fix: remove trailit slash when loading packages files from a database 2024-08-08 14:53:25 +03:00
e45383418b fix: skip debug packages as well 2024-08-08 13:53:55 +03:00
dd5baaa07e docs: update documentation for implicit dependencies resolution 2024-08-08 13:34:00 +03:00
d4a6f031c2 feat: remove excess dependencies leaves (#128)
This mr improves implicit dependencies processing by reducing tree leaves by using the following algorithm:

* remove paths which belong to any base package
* remove packages which are (opt)dependencies of one of the package which provides same path. It also tries to handle circular dependencies by excluding them from being "satisfied"
* remove packages which are already satisfied by any children path
2024-08-06 18:01:34 +03:00
4e9beca339 type: remove another unused mypy directive 2024-08-06 17:55:15 +03:00
9bbbd9da2e
feat: improve lock mechanisms
* improve lock mechanisms

* use /run/ahriman for sockett

* better water
2024-07-17 17:58:24 +03:00
985307a89e type: fix mypy warn for fresh unixsocket release 2024-07-17 17:08:00 +03:00
e2efe21a8b build: use requests-unixsocket2 fork
Since requests-2.32.0, the http+unix url scheme is brokek, check
https://github.com/msabramo/requests-unixsocket/issues/73 for more
details
2024-06-12 17:08:28 +03:00
5995b78572
feat: implement local reporter mode (#126)
* implement local reporter mode

* simplify watcher class

* review changes

* do not update unknown status

* allow empty key patches via api

* fix some pylint warnings in tests
2024-05-21 16:27:17 +03:00
ac19c407d3 feat: allow to use simplified keys for context
Initial implementation requires explicit context key name to be set.
Though it is still useful sometimes (e.g. if there should be two
variables with the same type), in the most used scenarios internally
only type is required. This commit extends set and get methods to allow
to construct ContextKey from type directly

Also it breaks old keys, since - in order to reduce amount of possible
mistakes - internal classes uses this generation method
2024-05-12 12:00:02 +03:00
c74cd68ad6 feat: add abillity to check broken dependencies (#122)
* implement elf dynamic linking check

* load local database too in pacman wrapper
2024-05-12 11:59:57 +03:00
8 changed files with 87 additions and 100 deletions

View File

@ -17,7 +17,6 @@
# 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/>.
# #
import itertools
import shutil import shutil
import tarfile import tarfile
@ -178,48 +177,39 @@ class Pacman(LazyLogging):
PacmanDatabase(database, self.configuration).sync(force=force) PacmanDatabase(database, self.configuration).sync(force=force)
transaction.release() transaction.release()
def files(self, packages: Iterable[str]) -> dict[str, set[str]]: def files(self, packages: Iterable[str] | None = None) -> dict[str, set[str]]:
""" """
extract list of known packages from the databases extract list of known packages from the databases
Args: Args:
packages(Iterable[str]): filter by package names packages(Iterable[str] | None, optional): filter by package names (Default value = None)
Returns: Returns:
dict[str, set[str]]: map of package name to its list of files dict[str, set[str]]: map of package name to its list of files
""" """
def extract(tar: tarfile.TarFile, package_names: dict[str, str]) -> Generator[tuple[str, set[str]], None, None]: packages = packages or []
for package_name, version in package_names.items():
path = Path(f"{package_name}-{version}") / "files" def extract(tar: tarfile.TarFile) -> Generator[tuple[str, set[str]], None, None]:
try: for descriptor in filter(lambda info: info.path.endswith("/files"), tar.getmembers()):
content = tar.extractfile(str(path)) package, *_ = str(Path(descriptor.path).parent).rsplit("-", 2)
except KeyError: if packages and package not in packages:
# in case if database and its files has been desync somehow, the extractfile will raise continue # skip unused packages
# KeyError because the entry doesn't exist content = tar.extractfile(descriptor)
content = None
if content is None: if content is None:
continue continue
# this is just array of files, however, the directories are with trailing slash, # this is just array of files, however, the directories are with trailing slash,
# which previously has been removed by the conversion to ``pathlib.Path`` # which previously has been removed by the conversion to ``pathlib.Path``
files = {filename.decode("utf8").rstrip().removesuffix("/") for filename in content.readlines()} files = {filename.decode("utf8").rstrip().removesuffix("/") for filename in content.readlines()}
yield package_name, files
# sort is required for the following group by operation yield package, files
descriptors = sorted(
(package for package_name in packages for package in self.package(package_name)),
key=lambda package: package.db.name
)
result: dict[str, set[str]] = {} result: dict[str, set[str]] = {}
for database_name, pacman_packages in itertools.groupby(descriptors, lambda package: package.db.name): for database in self.handle.get_syncdbs():
database_file = self.repository_paths.pacman / "sync" / f"{database_name}.files.tar.gz" database_file = self.repository_paths.pacman / "sync" / f"{database.name}.files.tar.gz"
if not database_file.is_file(): if not database_file.is_file():
continue # no database file found continue # no database file found
package_names = {package.name: package.version for package in pacman_packages}
with tarfile.open(database_file, "r:gz") as archive: with tarfile.open(database_file, "r:gz") as archive:
result.update(extract(archive, package_names)) result.update(extract(archive))
return result return result

View File

@ -22,3 +22,4 @@ from collections.abc import Awaitable, Callable
HandlerType = Callable[[Request], Awaitable[StreamResponse]] HandlerType = Callable[[Request], Awaitable[StreamResponse]]
MiddlewareType = Callable[[Request, HandlerType], Awaitable[StreamResponse]]

View File

@ -21,7 +21,6 @@ import aiohttp_security
import socket import socket
import types import types
from aiohttp.typedefs import Middleware
from aiohttp.web import Application, Request, StaticResource, StreamResponse, middleware from aiohttp.web import Application, Request, StaticResource, StreamResponse, middleware
from aiohttp_session import setup as setup_session from aiohttp_session import setup as setup_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage from aiohttp_session.cookie_storage import EncryptedCookieStorage
@ -31,7 +30,7 @@ from enum import Enum
from ahriman.core.auth import Auth from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.middlewares import HandlerType from ahriman.web.middlewares import HandlerType, MiddlewareType
__all__ = ["setup_auth"] __all__ = ["setup_auth"]
@ -85,7 +84,7 @@ class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
return await self.validator.verify_access(identity, permission, context) return await self.validator.verify_access(identity, permission, context)
def _auth_handler(allow_read_only: bool) -> Middleware: def _auth_handler(allow_read_only: bool) -> MiddlewareType:
""" """
authorization and authentication middleware authorization and authentication middleware
@ -93,7 +92,7 @@ def _auth_handler(allow_read_only: bool) -> Middleware:
allow_read_only: allow allow_read_only: allow
Returns: Returns:
Middleware: built middleware MiddlewareType: built middleware
""" """
@middleware @middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse: async def handle(request: Request, handler: HandlerType) -> StreamResponse:

View File

@ -20,11 +20,10 @@
import aiohttp_jinja2 import aiohttp_jinja2
import logging import logging
from aiohttp.typedefs import Middleware
from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \ from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
HTTPUnauthorized, Request, StreamResponse, json_response, middleware HTTPUnauthorized, Request, StreamResponse, json_response, middleware
from ahriman.web.middlewares import HandlerType from ahriman.web.middlewares import HandlerType, MiddlewareType
__all__ = ["exception_handler"] __all__ = ["exception_handler"]
@ -44,7 +43,7 @@ def _is_templated_unauthorized(request: Request) -> bool:
and "application/json" not in request.headers.getall("accept", []) and "application/json" not in request.headers.getall("accept", [])
def exception_handler(logger: logging.Logger) -> Middleware: def exception_handler(logger: logging.Logger) -> MiddlewareType:
""" """
exception handler middleware. Just log any exception (except for client ones) exception handler middleware. Just log any exception (except for client ones)
@ -52,7 +51,7 @@ def exception_handler(logger: logging.Logger) -> Middleware:
logger(logging.Logger): class logger logger(logging.Logger): class logger
Returns: Returns:
Middleware: built middleware MiddlewareType: built middleware
Raises: Raises:
HTTPNoContent: OPTIONS method response HTTPNoContent: OPTIONS method response

View File

@ -4,7 +4,7 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any, TypeVar from typing import Any, TypeVar
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
@ -476,41 +476,6 @@ def passwd() -> MagicMock:
return passwd return passwd
@pytest.fixture
def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
"""
mock object for pyalpm package
Args:
aur_package_ahriman(AURPackage): package fixture
Returns:
MagicMock: pyalpm package mock
"""
mock = MagicMock()
db = type(mock).db = MagicMock()
type(mock).base = PropertyMock(return_value=aur_package_ahriman.package_base)
type(mock).builddate = PropertyMock(
return_value=aur_package_ahriman.last_modified.replace(tzinfo=datetime.timezone.utc).timestamp())
type(mock).conflicts = PropertyMock(return_value=aur_package_ahriman.conflicts)
type(db).name = PropertyMock(return_value="aur")
type(mock).depends = PropertyMock(return_value=aur_package_ahriman.depends)
type(mock).desc = PropertyMock(return_value=aur_package_ahriman.description)
type(mock).licenses = PropertyMock(return_value=aur_package_ahriman.license)
type(mock).makedepends = PropertyMock(return_value=aur_package_ahriman.make_depends)
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
type(mock).packager = PropertyMock(return_value="packager")
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)
type(mock).groups = PropertyMock(return_value=aur_package_ahriman.groups)
return mock
@pytest.fixture @pytest.fixture
def remote_source() -> RemoteSource: def remote_source() -> RemoteSource:
""" """

View File

@ -1,4 +1,3 @@
import pyalpm
import pytest import pytest
import tarfile import tarfile
@ -176,12 +175,31 @@ def test_database_sync_forced(pacman: Pacman, mocker: MockerFixture) -> None:
sync_mock.assert_called_once_with(force=True) sync_mock.assert_called_once_with(force=True)
def test_files_package(pacman: Pacman, package_ahriman: Package, pyalpm_package_ahriman: pyalpm.Package, def test_files(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
mocker: MockerFixture, resource_path_root: Path) -> None: """
must load files from databases
"""
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock
tarball = resource_path_root / "core" / "arcanisrepo.files.tar.gz"
with tarfile.open(tarball, "r:gz") as fd:
mocker.patch("pathlib.Path.is_file", return_value=True)
open_mock = mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=fd)
files = pacman.files()
assert len(files) == 2
assert package_ahriman.base in files
assert "usr/bin/ahriman" in files[package_ahriman.base]
open_mock.assert_called_once_with(pytest.helpers.anyvar(int), "r:gz")
def test_files_package(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture,
resource_path_root: Path) -> None:
""" """
must load files only for the specified package must load files only for the specified package
""" """
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[pyalpm_package_ahriman])
handle_mock = MagicMock() handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()] handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock pacman.handle = handle_mock
@ -192,35 +210,34 @@ def test_files_package(pacman: Pacman, package_ahriman: Package, pyalpm_package_
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=fd) mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=fd)
files = pacman.files([package_ahriman.base]) files = pacman.files(package_ahriman.base)
assert len(files) == 1 assert len(files) == 1
assert package_ahriman.base in files assert package_ahriman.base in files
def test_files_skip(pacman: Pacman, pyalpm_package_ahriman: pyalpm.Package, mocker: MockerFixture) -> None: def test_files_skip(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must return empty list if no database found must return empty list if no database found
""" """
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[pyalpm_package_ahriman])
handle_mock = MagicMock() handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()] handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock pacman.handle = handle_mock
mocker.patch("pathlib.Path.is_file", return_value=False) mocker.patch("pathlib.Path.is_file", return_value=False)
assert not pacman.files([pyalpm_package_ahriman.name]) assert not pacman.files()
def test_files_no_content(pacman: Pacman, pyalpm_package_ahriman: pyalpm.Package, mocker: MockerFixture) -> None: def test_files_no_content(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must skip package if no content can be loaded must skip package if no content can be loaded
""" """
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[pyalpm_package_ahriman])
handle_mock = MagicMock() handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()] handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock pacman.handle = handle_mock
tar_mock = MagicMock() tar_mock = MagicMock()
tar_mock.getmembers.return_value = [MagicMock()]
tar_mock.extractfile.return_value = None tar_mock.extractfile.return_value = None
open_mock = MagicMock() open_mock = MagicMock()
@ -229,28 +246,7 @@ def test_files_no_content(pacman: Pacman, pyalpm_package_ahriman: pyalpm.Package
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=open_mock) mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=open_mock)
assert not pacman.files([pyalpm_package_ahriman.name]) assert not pacman.files()
def test_files_no_entry(pacman: Pacman, pyalpm_package_ahriman: pyalpm.Package, mocker: MockerFixture) -> None:
"""
must skip package if it wasn't found in the archive
"""
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[pyalpm_package_ahriman])
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock
tar_mock = MagicMock()
tar_mock.extractfile.side_effect = KeyError()
open_mock = MagicMock()
open_mock.__enter__.return_value = tar_mock
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=open_mock)
assert not pacman.files([pyalpm_package_ahriman.name])
def test_package(pacman: Pacman) -> None: def test_package(pacman: Pacman) -> None:

View File

@ -1,3 +1,4 @@
import datetime
import pytest import pytest
from typing import Any from typing import Any
@ -7,6 +8,7 @@ from pytest_mock import MockerFixture
from ahriman import __version__ from ahriman import __version__
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.filesystem_package import FilesystemPackage from ahriman.models.filesystem_package import FilesystemPackage
@ -132,6 +134,41 @@ def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
return mock return mock
@pytest.fixture
def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
"""
mock object for pyalpm package
Args:
aur_package_ahriman(AURPackage): package fixture
Returns:
MagicMock: pyalpm package mock
"""
mock = MagicMock()
db = type(mock).db = MagicMock()
type(mock).base = PropertyMock(return_value=aur_package_ahriman.package_base)
type(mock).builddate = PropertyMock(
return_value=aur_package_ahriman.last_modified.replace(tzinfo=datetime.timezone.utc).timestamp())
type(mock).conflicts = PropertyMock(return_value=aur_package_ahriman.conflicts)
type(db).name = PropertyMock(return_value="aur")
type(mock).depends = PropertyMock(return_value=aur_package_ahriman.depends)
type(mock).desc = PropertyMock(return_value=aur_package_ahriman.description)
type(mock).licenses = PropertyMock(return_value=aur_package_ahriman.license)
type(mock).makedepends = PropertyMock(return_value=aur_package_ahriman.make_depends)
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
type(mock).packager = PropertyMock(return_value="packager")
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)
type(mock).groups = PropertyMock(return_value=aur_package_ahriman.groups)
return mock
@pytest.fixture @pytest.fixture
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock: def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
""" """