add repo-structure subcommand

This commit also changes Tree class, replacing load method by resolve
This commit is contained in:
2022-12-27 02:06:10 +02:00
parent 8c04dc4c2a
commit e0126bb811
17 changed files with 364 additions and 37 deletions

View File

@ -109,6 +109,7 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers)
_set_repo_sync_parser(subparsers)
_set_repo_tree_parser(subparsers)
_set_repo_triggers_parser(subparsers)
_set_repo_update_parser(subparsers)
_set_shell_parser(subparsers)
@ -703,6 +704,23 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository tree subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-tree", help="dump repository tree",
description="dump repository tree based on packages dependencies",
formatter_class=_formatter)
parser.set_defaults(handler=handlers.Structure, lock=None, report=False, quiet=True)
return parser
def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository triggers subcommand

View File

@ -145,8 +145,8 @@ class ApplicationRepository(ApplicationProperties):
process_update(packages, build_result)
# process manual packages
tree = Tree.load(updates, self.repository.paths, self.database)
for num, level in enumerate(tree.levels()):
tree = Tree.resolve(updates, self.repository.paths, self.database)
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)
packages = self.repository.packages_built()
@ -181,8 +181,12 @@ class ApplicationRepository(ApplicationProperties):
local_versions = {package.base: package.version for package in self.repository.packages()}
updated_packages = [package for _, package in sorted(updates.items())]
for package in updated_packages:
UpdatePrinter(package, local_versions.get(package.base)).print(
verbose=True, log_fn=log_fn, separator=" -> ")
# reorder updates according to the dependency tree
tree = Tree.resolve(updated_packages, self.repository.paths, self.database)
for level in tree:
for package in level:
UpdatePrinter(package, local_versions.get(package.base)).print(
verbose=True, log_fn=log_fn, separator=" -> ")
return updated_packages

View File

@ -37,6 +37,7 @@ from ahriman.application.handlers.shell import Shell
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.structure import Structure
from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update

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/>.
#
import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import TreePrinter
from ahriman.core.tree import Tree
class Structure(Handler):
"""
dump repository structure handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, report=report, unsafe=unsafe)
packages = application.repository.packages()
tree = Tree.resolve(packages, application.repository.paths, application.database)
for num, level in enumerate(tree):
TreePrinter(num, level).print(verbose=True, separator=" ")

View File

@ -26,6 +26,7 @@ from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
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.tree_printer import TreePrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.core.formatters.version_printer import VersionPrinter

View File

@ -0,0 +1,53 @@
#
# 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 Iterable, List
from ahriman.core.formatters import StringPrinter
from ahriman.models.package import Package
from ahriman.models.property import Property
class TreePrinter(StringPrinter):
"""
print content of the package tree level
Attributes:
packages(Iterable[Package]): packages which belong to this level
"""
def __init__(self, level: int, packages: Iterable[Package]) -> None:
"""
default constructor
Args:
level(int): dependencies tree level
packages(Iterable[Package]): packages which belong to this level
"""
StringPrinter.__init__(self, f"level {level}")
self.packages = packages
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return [Property(package.base, package.version, is_required=True) for package in self.packages]

View File

@ -19,9 +19,11 @@
#
from __future__ import annotations
import itertools
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Iterable, List, Set, Type
from typing import Callable, Iterable, List, Set, Tuple, Type
from ahriman.core.build_tools.sources import Sources
from ahriman.core.database import SQLite
@ -77,6 +79,21 @@ class Leaf:
dependencies = Package.dependencies(clone_dir)
return cls(package, dependencies)
def is_dependency(self, packages: Iterable[Leaf]) -> bool:
"""
check if the package is dependency of any other package from list or not
Args:
packages(Iterable[Leaf]): list of known leaves
Returns:
bool: True in case if package is dependency of others and False otherwise
"""
for leaf in packages:
if leaf.dependencies.intersection(self.items):
return True
return False
def is_root(self, packages: Iterable[Leaf]) -> bool:
"""
check if package depends on any other package from list of not
@ -113,8 +130,8 @@ class Tree:
>>> repository = Repository.load("x86_64", configuration, database, report=True, unsafe=False)
>>> packages = repository.packages()
>>>
>>> tree = Tree.load(packages, configuration.repository_paths, database)
>>> for tree_level in tree.levels():
>>> tree = Tree.resolve(packages, configuration.repository_paths, database)
>>> for tree_level in tree:
>>> for package in tree_level:
>>> print(package.base)
>>> print()
@ -141,9 +158,10 @@ class Tree:
self.leaves = leaves
@classmethod
def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths, database: SQLite) -> Tree:
def resolve(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths,
database: SQLite) -> List[List[Package]]:
"""
load tree from packages
resolve dependency tree
Args:
packages(Iterable[Package]): packages list
@ -151,22 +169,45 @@ class Tree:
database(SQLite): database instance
Returns:
Tree: loaded class
List[List[Package]]: list of packages lists based on their dependencies
"""
return cls([Leaf.load(package, paths, database) for package in packages])
leaves = [Leaf.load(package, paths, database) for package in packages]
tree = cls(leaves)
return tree.levels()
def levels(self) -> List[List[Package]]:
"""
get build levels starting from the packages which do not require any other package to build
Returns:
List[List[Package]]: list of packages lists
List[List[Package]]: sorted list of packages lists based on their dependencies
"""
result: List[List[Package]] = []
# https://docs.python.org/dev/library/itertools.html#itertools-recipes
def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]:
first_iter, second_iter = itertools.tee(source)
filter_fn: Callable[[Leaf], bool] = lambda leaf: leaf.is_dependency(next_level)
# materialize first list and leave second as iterator
return list(filter(filter_fn, first_iter)), itertools.filterfalse(filter_fn, second_iter)
unsorted: List[List[Leaf]] = []
# build initial tree
unprocessed = self.leaves[:]
while unprocessed:
result.append([leaf.package for leaf in unprocessed if leaf.is_root(unprocessed)])
unsorted.append([leaf for leaf in unprocessed if leaf.is_root(unprocessed)])
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
return result
# move leaves to the end if they are not required at the next level
for current_num, current_level in enumerate(unsorted[:-1]):
next_num = current_num + 1
next_level = unsorted[next_num]
# change lists inside the collection
unsorted[current_num], to_be_moved = partition(current_level)
unsorted[next_num].extend(to_be_moved)
comparator: Callable[[Package], str] = lambda package: package.base
return [
sorted([leaf.package for leaf in level], key=comparator)
for level in unsorted if level
]