jw.pkg: Fix "make check" static code check fallout
The previous commits have put rules for linting and formatting via ruff, yapf, mypy and pyright into place. They are checked with the make check target, and this commit adds the fixes for the target to succeed.
It does some refactoring where type checking dug up dirty bits, and also adds lots of churn in the Python code. To a good deal, that's owed to mere formatting changes. It would have been better to seperate those from syntax and refactoring fixes into multiple commits, so that the interesting changes don't drown in the formatting nose. However, that would have been a lot of additional work only to be thrown away by later commits, hence this commit has a big diff in one piece. The size of the diff is regrettable but hopefully a one-off: What it buys is automatic format checking for CI and predictble formats for smaller diffs in the future.
Rules that "make check" enforces are, in the following order
- Syntax checkers:
- ruff check . - mypy . - pyright- Format check:
- yapf --diff --recursive .The refactoring includes:
- Turn the Result class into a more elaborate object, capable of doing more heavy lifting around stderr and stdout decoding, summarizing outcome, and matching error strings.Aside from fixing broken type checks, this also removes lots of boilerplate calling code which is currently used for handling possible call outcome scenarios. Trying to access an inexistent, decoded string should raise a meaningful exception by itself now, which removes lots of code with case distinctions.- Fix Cmd type hierarchy:
- Add the AbstractCmd class above Cmd. This is necessary because the checker rightfully complains it can't instantiate a Cmd instance where constructor arguments were needed. They never were, but the type used at the instantiating code's location in jw.pkg.App so claims.- Lots of sub- and sub-subcommands are derived from the base class of the invoking command. That provides some properties shared across the ancestor hierarchy of a command, but is semantically unsound. Fix that by introducing jw.pkg.BaseCmd class as a place to provide basic helpers shared across all commands used in a jw.pkg.App's context, and derive all command classes from that afresh. The parent command is still reachable via a common parent property.Formatting changes are conforming to PEP-8, mostly, with minor tweaks. All in all they include the following changes.
- Remove # -*- coding: utf-8 -*-
The line was needed by Python 2 which is not supported anylonger. For Python 3, the default encoding is UTF-8, anyway.- Allow to run "make py-format" without having it produce any changes. It's basically "yapf --in-place --recursive ." with some code style settings, see conf/topdir/pyproject.toml. The settings may be debatable. I've had custom tweaks in place on that target, too, but then again, IDEs would have more hassle to integrate that.- Introduce a 88 character line length limit
- One import per line, reshuffle them semantically, see [tool.isort] in pyproject.toml.- Hide imports needed for type-checking only behind
if TYPE_CHECKING- Spaces around assignments accounts for much churn. Having having no spaces in inline parameter list assignments and default parameter values would arguably be more compact where it's useful. On the other hand, I have not found a code formatter which allows spaces around assignments in parameter lists broken into one per line and that's often better than a wall of text.- Add two spaces before # export, as this seems to be mandated by PEP-8- Use single quotes by default
Signed-off-by: Jan Lindemann <jan@janware.com>
|
|
@ -1,29 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This source code file is a merge of various build tools and a horrible mess.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterable
|
||||
import argparse
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import sys
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeAlias
|
||||
import os, sys, pwd, re
|
||||
|
||||
import os, sys, argparse, pwd, re
|
||||
from functools import lru_cache, cached_property
|
||||
from enum import Enum, auto
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .lib.App import App as Base
|
||||
from .lib.log import *
|
||||
from .lib.Distro import Distro
|
||||
from .lib.base import InputMode
|
||||
from .lib.log import DEBUG, ERR, log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import sys
|
||||
|
||||
from typing import TypeAlias
|
||||
|
||||
from .lib.ExecContext import ExecContext
|
||||
from .lib.PackageFilter import PackageFilter
|
||||
|
||||
# Meaning of pkg.requires.xxx variables
|
||||
# build: needs to be built and installed before this can be built
|
||||
# devel: needs to be installed before this-devel can be installed, i.e. before _other_ packages can be built against this
|
||||
# run: needs to be installed before this-run can be installed, i.e. before this and other packages can run with this
|
||||
# devel: needs to be installed before this-devel can be installed,
|
||||
# i.e. before _other_ packages can be built against this
|
||||
# run: needs to be installed before this-run can be installed,
|
||||
# i.e. before this and other packages can run with this
|
||||
|
||||
# --------------------------------------------------------------------- Helpers
|
||||
|
||||
|
|
@ -35,32 +46,34 @@ class ResultCache(object):
|
|||
def run(self, func, args):
|
||||
d = self.__cache
|
||||
depth = 0
|
||||
keys = [ func.__name__ ] + args
|
||||
l = len(keys)
|
||||
keys = [func.__name__] + args
|
||||
sz = len(keys)
|
||||
for k in keys:
|
||||
if k is None:
|
||||
k = 'None'
|
||||
else:
|
||||
k = str(k)
|
||||
depth += 1
|
||||
#log(DEBUG, 'depth = ', depth, 'key = ', k, 'd = ', str(d))
|
||||
# log(DEBUG, 'depth = ', depth, 'key = ', k, 'd = ', str(d))
|
||||
if k in d:
|
||||
if l == depth:
|
||||
if sz == depth:
|
||||
return d[k]
|
||||
d = d[k]
|
||||
continue
|
||||
if l == depth:
|
||||
if sz == depth:
|
||||
r = func(*args)
|
||||
d[k] = r
|
||||
return r
|
||||
d = d[k] = {}
|
||||
#d = d[k]
|
||||
raise Exception('cache algorithm failed for function', func.__name__, 'in depth', depth)
|
||||
# d = d[k]
|
||||
raise Exception(
|
||||
'cache algorithm failed for function', func.__name__, 'in depth', depth
|
||||
)
|
||||
|
||||
class Scope(Enum):
|
||||
Self = auto()
|
||||
One = auto()
|
||||
Subtree = auto()
|
||||
Self = auto()
|
||||
One = auto()
|
||||
Subtree = auto()
|
||||
|
||||
Graph: TypeAlias = dict[str, set[str]]
|
||||
|
||||
|
|
@ -68,27 +81,41 @@ Graph: TypeAlias = dict[str, set[str]]
|
|||
|
||||
class App(Base):
|
||||
|
||||
def __format_topdir(self, topdir: None|str, fmt: str) -> str:
|
||||
if topdir is None:
|
||||
def __format_topdir(self, path: None | str, fmt: str) -> str | None:
|
||||
if path is None:
|
||||
return None
|
||||
match fmt:
|
||||
case 'unaltered':
|
||||
return topdir
|
||||
return path
|
||||
case None | 'absolute':
|
||||
return os.path.abspath(self.__topdir)
|
||||
return os.path.abspath(path)
|
||||
case _:
|
||||
m = re.search(r'^make:(\S+)$', fmt)
|
||||
if m is None:
|
||||
raise Exception(f'Can\'t interpret "{fmt}" as valid topdir ' +
|
||||
'reference, expecting "unaltered", "absolute", or "make:<variable-name>"')
|
||||
raise Exception(
|
||||
f'Can\'t interpret "{fmt}" as valid topdir reference, '
|
||||
'expecting "unaltered", "absolute", or "make:<variable-name>"'
|
||||
)
|
||||
return '$(' + m.group(1) + ')'
|
||||
|
||||
def __proj_dir(self, name: str, pretty) -> str:
|
||||
@property
|
||||
def __topdir(self) -> str:
|
||||
if self.___topdir is None:
|
||||
raise Exception('Tried to access undefined top directory')
|
||||
return self.___topdir
|
||||
|
||||
@property
|
||||
def __pretty_topdir(self) -> str:
|
||||
if self.___pretty_topdir is None:
|
||||
raise Exception('Tried to access undefined pretty top directory')
|
||||
return self.___pretty_topdir
|
||||
|
||||
def __proj_dir(self, name: str, pretty: bool) -> str | None:
|
||||
if name == self.__top_name:
|
||||
if pretty:
|
||||
return self.__pretty_topdir
|
||||
return self.__topdir
|
||||
for d in [ self.__projs_root, '/opt' ]:
|
||||
for d in [self.__projs_root, '/opt']:
|
||||
ret = d + '/' + name
|
||||
if os.path.exists(ret):
|
||||
return ret
|
||||
|
|
@ -97,7 +124,14 @@ class App(Base):
|
|||
return None
|
||||
raise Exception('No project path found for module "{}"'.format(name))
|
||||
|
||||
def __find_dir(self, name: str, search_subdirs: list[str]=[], search_absdirs: list[str]=[], pretty: bool=True) -> str|None:
|
||||
def __find_dir(
|
||||
self,
|
||||
name: str,
|
||||
search_subdirs: list[str] = [],
|
||||
search_absdirs: list[str] = [],
|
||||
pretty: bool = True,
|
||||
) -> str | None:
|
||||
|
||||
def format_pd(name: str, pd: str, pretty: bool):
|
||||
if not pretty:
|
||||
return pd
|
||||
|
|
@ -107,7 +141,10 @@ class App(Base):
|
|||
return pd
|
||||
if name == self.__top_name:
|
||||
return self.__pretty_topdir
|
||||
raise NotImplementedError(f'Tried to pretty-format directory {pd}, not implemented')
|
||||
raise NotImplementedError(
|
||||
f'Tried to pretty-format directory {pd}, not implemented'
|
||||
)
|
||||
|
||||
pd = self.__proj_dir(name, False)
|
||||
if pd is None:
|
||||
return None
|
||||
|
|
@ -126,11 +163,25 @@ class App(Base):
|
|||
return ret
|
||||
return None
|
||||
|
||||
def __get_project_refs_cached(self, buf, visited, spec, section, key, add_self, scope, names_only):
|
||||
return self.__res_cache.run(self.__get_project_refs, [buf, visited, spec, section, key, add_self, scope, names_only])
|
||||
def __get_project_refs_cached(
|
||||
self, buf, visited, spec, section, key, add_self, scope, names_only
|
||||
):
|
||||
return self.__res_cache.run(
|
||||
self.__get_project_refs,
|
||||
[buf, visited, spec, section, key, add_self, scope, names_only],
|
||||
)
|
||||
|
||||
def __get_project_refs(self, buf: list[str], visited: set[str], spec: str,
|
||||
section: str, key: str, add_self: bool, scope: Scope, names_only: bool) -> None:
|
||||
def __get_project_refs(
|
||||
self,
|
||||
buf: list[str],
|
||||
visited: set[str],
|
||||
spec: str,
|
||||
section: str,
|
||||
key: str,
|
||||
add_self: bool,
|
||||
scope: Scope,
|
||||
names_only: bool,
|
||||
) -> None:
|
||||
name = self.strip_module_from_spec(spec)
|
||||
if names_only:
|
||||
spec = name
|
||||
|
|
@ -142,59 +193,95 @@ class App(Base):
|
|||
return
|
||||
visited.add(spec)
|
||||
deps = self.get_value(name, section, key)
|
||||
log(DEBUG, 'name = ', name, 'section = ', section, 'key = ', key, 'deps = ', deps, 'scope = ', scope.name, 'visited = ', visited)
|
||||
log(
|
||||
DEBUG,
|
||||
(
|
||||
f'name={name}, section={section}, key={key}, deps={deps}, '
|
||||
f'scope={scope.name}, visited={visited}'
|
||||
),
|
||||
)
|
||||
if deps and scope != Scope.Self:
|
||||
if scope == Scope.One:
|
||||
subscope = Scope.Self
|
||||
else:
|
||||
subscope = Scope.Subtree
|
||||
deps = deps.split(',')
|
||||
for dep in deps:
|
||||
for dep in deps.split(','):
|
||||
dep = dep.strip()
|
||||
if not(len(dep)):
|
||||
if not (len(dep)):
|
||||
continue
|
||||
self.__get_project_refs_cached(buf, visited, dep,
|
||||
section, key, add_self=True, scope=subscope,
|
||||
names_only=names_only)
|
||||
self.__get_project_refs_cached(
|
||||
buf,
|
||||
visited,
|
||||
dep,
|
||||
section,
|
||||
key,
|
||||
add_self = True,
|
||||
scope = subscope,
|
||||
names_only = names_only,
|
||||
)
|
||||
if add_self:
|
||||
buf.append(spec)
|
||||
|
||||
def __read_dep_graph(self, projects: list[str], section: str, graph: Graph) -> None:
|
||||
def __read_dep_graph(
|
||||
self,
|
||||
projects: list[str],
|
||||
sections: str | list[str],
|
||||
graph: Graph,
|
||||
) -> None:
|
||||
if isinstance(sections, str):
|
||||
sections = [sections]
|
||||
for project in projects:
|
||||
if project in graph:
|
||||
continue
|
||||
deps = self.get_project_refs([ project ], ['pkg.requires.jw'], section,
|
||||
scope = Scope.One, add_self=False, names_only=True)
|
||||
if not deps is None:
|
||||
for section in sections:
|
||||
deps = self.get_project_refs(
|
||||
[project],
|
||||
['pkg.requires.jw'],
|
||||
sections,
|
||||
scope = Scope.One,
|
||||
add_self = False,
|
||||
names_only = True,
|
||||
)
|
||||
if deps is None:
|
||||
continue
|
||||
graph[project] = set(deps)
|
||||
for dep in deps:
|
||||
self.__read_dep_graph([ dep ], section, graph)
|
||||
self.__read_dep_graph([dep], sections, graph)
|
||||
|
||||
def __flip_dep_graph(self, graph: Graph):
|
||||
ret: Graph = {}
|
||||
for project, deps in graph.items():
|
||||
for d in deps:
|
||||
if not d in ret:
|
||||
if d not in ret:
|
||||
ret[d] = set()
|
||||
ret[d].add(project)
|
||||
return ret
|
||||
|
||||
def __find_circular_deps_recursive(self, project: str, graph: Graph, unvisited: list[str],
|
||||
temp: set[str], path: str) -> str|None:
|
||||
def __find_circular_deps_recursive(
|
||||
self,
|
||||
project: str,
|
||||
graph: Graph,
|
||||
unvisited: list[str],
|
||||
temp: set[str],
|
||||
path: list[str],
|
||||
) -> str | None:
|
||||
if project in temp:
|
||||
log(DEBUG, 'found circular dependency at project', project)
|
||||
return project
|
||||
if not project in unvisited:
|
||||
if project not in unvisited:
|
||||
return None
|
||||
temp.add(project)
|
||||
if project in graph:
|
||||
for dep in graph[project]:
|
||||
last = self.__find_circular_deps_recursive(dep, graph, unvisited, temp, path)
|
||||
last = self.__find_circular_deps_recursive(
|
||||
dep, graph, unvisited, temp, path
|
||||
)
|
||||
if last is not None:
|
||||
path.insert(0, dep)
|
||||
return last
|
||||
unvisited.remove(project)
|
||||
temp.remove(project)
|
||||
return None
|
||||
|
||||
def __find_circular_deps(self, projects: list[str], flavours: list[str]) -> bool:
|
||||
graph: Graph = {}
|
||||
|
|
@ -205,25 +292,27 @@ class App(Base):
|
|||
while unvisited:
|
||||
project = unvisited[0]
|
||||
log(DEBUG, 'Checking circular dependency of', project)
|
||||
last = self.__find_circular_deps_recursive(project, self.__flip_dep_graph(graph), unvisited, temp, ret)
|
||||
last = self.__find_circular_deps_recursive(
|
||||
project, self.__flip_dep_graph(graph), unvisited, temp, ret
|
||||
)
|
||||
if last is not None:
|
||||
log(DEBUG, f'Found circular dependency below {project}, last is {last}')
|
||||
return True
|
||||
return False
|
||||
|
||||
def __init__(self, distro: Distro|None=None) -> None:
|
||||
def __init__(self, distro: Distro | None = None) -> None:
|
||||
|
||||
super().__init__('jw-pkg swiss army knife', modules=['jw.pkg.cmds'])
|
||||
super().__init__('jw-pkg swiss army knife', modules = ['jw.pkg.cmds'])
|
||||
|
||||
# -- Members without default values
|
||||
self.__opt_interactive: bool|None = None
|
||||
self.__opt_verbose: bool|None = None
|
||||
self.__top_name: str|None = None
|
||||
self.__opt_interactive: bool | None = None
|
||||
self.__opt_verbose: bool | None = None
|
||||
self.__top_name: str | None = None
|
||||
self.__distro = distro
|
||||
self.__res_cache = ResultCache()
|
||||
self.__topdir: str|None = None
|
||||
self.__pretty_topdir: str|None = None
|
||||
self.__exec_context: ExecContext|None = None
|
||||
self.___topdir: str | None = None
|
||||
self.___pretty_topdir: str | None = None
|
||||
self.__exec_context: ExecContext | None = None
|
||||
|
||||
# -- Members with default values
|
||||
self.__topdir_fmt = 'absolute'
|
||||
|
|
@ -235,48 +324,83 @@ class App(Base):
|
|||
pkg_filter_str = self.args.pkg_filter
|
||||
if pkg_filter_str is None:
|
||||
pkg_filter_str = os.getenv('JW_DEFAULT_PKG_FILTER')
|
||||
pkg_filter: PackageFilter|None = None
|
||||
pkg_filter: PackageFilter | None = None
|
||||
if pkg_filter_str is not None:
|
||||
from .lib.PackageFilter import PackageFilterString
|
||||
|
||||
pkg_filter = PackageFilterString(pkg_filter_str)
|
||||
self.__distro = await Distro.instantiate(
|
||||
ec = self.exec_context,
|
||||
id = self.args.distro_id,
|
||||
default_pkg_filter = pkg_filter,
|
||||
)
|
||||
ec = self.exec_context,
|
||||
id = self.args.distro_id,
|
||||
default_pkg_filter = pkg_filter,
|
||||
)
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
if self.__exec_context is not None:
|
||||
await self.__exec_context.close()
|
||||
self.__exec_context = None
|
||||
return super().__aexit__(exc_type, exc, tb)
|
||||
|
||||
def _add_arguments(self, parser) -> None:
|
||||
super()._add_arguments(parser)
|
||||
parser.add_argument('-t', '--topdir', default = None, help='Project Path')
|
||||
parser.add_argument('--topdir-format', default = 'absolute', help='Output references to topdir as '
|
||||
+ 'one of "make:<var-name>", "unaltered", "absolute". Absolute topdir by default')
|
||||
parser.add_argument('-p', '--prefix', default = None,
|
||||
help='Parent directory of project source directories')
|
||||
parser.add_argument('--distro-id', default=None, help='Distribution ID (default is taken from /etc/os-release)')
|
||||
parser.add_argument('--interactive', choices=['true', 'false', 'auto'], default='true', help='Wait for user input or try to proceed unattended')
|
||||
parser.add_argument('--verbose', action='store_true', default=False, help='Be verbose on stderr about what\'s being done on the distro level')
|
||||
parser.add_argument('--target', default='local', help='Run commands on this host')
|
||||
parser.add_argument('--pkg-filter', help='Default filter for all distribution package-related operations')
|
||||
parser.add_argument('-t', '--topdir', default = None, help = 'Project Path')
|
||||
parser.add_argument(
|
||||
'--topdir-format',
|
||||
default = 'absolute',
|
||||
help = (
|
||||
'Output references to topdir as one of "make:<var-name>", '
|
||||
'"unaltered", "absolute". Absolute topdir by default'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p',
|
||||
'--prefix',
|
||||
default = None,
|
||||
help = 'Parent directory of project source directories',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--distro-id',
|
||||
default = None,
|
||||
help = 'Distribution ID (default is taken from /etc/os-release)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--interactive',
|
||||
choices = ['true', 'false', 'auto'],
|
||||
default = 'true',
|
||||
help = 'Wait for user input or try to proceed unattended',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action = 'store_true',
|
||||
default = False,
|
||||
help = "Be verbose on stderr about what's being done on the distro level",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target', default = 'local', help = 'Run commands on this host'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pkg-filter',
|
||||
help = 'Default filter for all distribution package-related operations',
|
||||
)
|
||||
|
||||
async def _run(self, args: argparse.Namespace) -> None:
|
||||
self.__topdir = args.topdir
|
||||
self.__pretty_topdir = self.__format_topdir(self.__topdir, args.topdir_format)
|
||||
self.___topdir = args.topdir
|
||||
self.___pretty_topdir = self.__format_topdir(self.___topdir, args.topdir_format)
|
||||
self.__topdir_fmt = args.topdir_format
|
||||
if self.__topdir is not None:
|
||||
self.__top_name = self.read_value(self.__topdir + '/make/project.conf', 'build', 'name')
|
||||
if self.___topdir is not None:
|
||||
self.__top_name = self.read_value(
|
||||
self.___topdir + '/make/project.conf', 'build', 'name'
|
||||
)
|
||||
if not self.__top_name:
|
||||
self.__top_name = re.sub('-[0-9.-]*$', '', os.path.basename(os.path.realpath(self.__topdir)))
|
||||
self.__top_name = re.sub(
|
||||
'-[0-9.-]*$',
|
||||
'',
|
||||
os.path.basename(os.path.realpath(self.___topdir))
|
||||
)
|
||||
if args.prefix is not None:
|
||||
self.__projs_root = args.prefix
|
||||
self.__pretty_projs_root = args.prefix
|
||||
await self.__init_async()
|
||||
return await super()._run(args)
|
||||
await super()._run(args)
|
||||
|
||||
@property
|
||||
def interactive(self) -> bool:
|
||||
|
|
@ -288,20 +412,32 @@ class App(Base):
|
|||
self.__opt_interactive = False
|
||||
case 'auto':
|
||||
self.__opt_interactive = sys.stdin.isatty()
|
||||
case _:
|
||||
raise ValueError(
|
||||
f'Unknown --interactive value: {self.args.interactive}'
|
||||
)
|
||||
# Not logically possible to fail, but this keeps pyright happy
|
||||
assert self.__opt_interactive is not None
|
||||
return self.__opt_interactive
|
||||
|
||||
@property
|
||||
def verbose(self) -> bool:
|
||||
if self.__opt_verbose is None:
|
||||
self.__opt_verbose = self.args.verbose
|
||||
# Not logically possible to fail, but this keeps pyright happy
|
||||
assert self.__opt_verbose is not None
|
||||
return self.__opt_verbose
|
||||
|
||||
@property
|
||||
def exec_context(self) -> str:
|
||||
def exec_context(self) -> ExecContext:
|
||||
if self.__exec_context is None:
|
||||
from .lib.ExecContext import ExecContext
|
||||
self.__exec_context = ExecContext.create(self.args.target, interactive=self.interactive,
|
||||
verbose_default=self.verbose)
|
||||
|
||||
self.__exec_context = ExecContext.create(
|
||||
self.args.target,
|
||||
interactive = self.interactive,
|
||||
verbose_default = self.verbose,
|
||||
)
|
||||
return self.__exec_context
|
||||
|
||||
@property
|
||||
|
|
@ -318,29 +454,51 @@ class App(Base):
|
|||
raise Exception('No distro object')
|
||||
return self.__distro
|
||||
|
||||
def find_dir(self, name: str, search_subdirs: list[str]=[], search_absdirs: list[str]=[], pretty: bool=True):
|
||||
return self.__find_dir(name, search_subdirs, search_absdirs, pretty)
|
||||
def find_dir(
|
||||
self,
|
||||
name: str,
|
||||
search_subdirs: list[str] = [],
|
||||
search_absdirs: list[str] = [],
|
||||
pretty: bool = True,
|
||||
) -> str:
|
||||
ret = self.__find_dir(name, search_subdirs, search_absdirs, pretty)
|
||||
if ret is None:
|
||||
msg = f'Failed to find directory for "{name}"'
|
||||
log(ERR, msg)
|
||||
for search_name, search in [
|
||||
('subdirs', search_subdirs),
|
||||
('absdirs', search_absdirs),
|
||||
]:
|
||||
if search:
|
||||
log(ERR, f' Searched {search_name} in:')
|
||||
for d in search:
|
||||
log(ERR, f' - {d}')
|
||||
raise FileNotFoundError(msg)
|
||||
return ret
|
||||
|
||||
# TODO: add support for customizing this in project.conf
|
||||
def htdocs_dir(self, project: str) -> str:
|
||||
return self.find_dir(project, ['/src/html/htdocs', '/tools/html/htdocs', '/htdocs'],
|
||||
['/srv/www/proj/' + project])
|
||||
return self.find_dir(
|
||||
project,
|
||||
['/src/html/htdocs', '/tools/html/htdocs', '/htdocs'],
|
||||
['/srv/www/proj/' + project],
|
||||
)
|
||||
|
||||
# TODO: add support for customizing this in project.conf
|
||||
def tmpl_dir(self, name: str) -> str:
|
||||
return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl'])
|
||||
return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl'])
|
||||
|
||||
def strip_module_from_spec(self, mod):
|
||||
return re.sub(r'-dev$|-devel$|-run$', '', re.split('([=><]+)', mod)[0].strip())
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
@lru_cache(maxsize = None)
|
||||
def get_section(self, path: str, section: str) -> str:
|
||||
ret = ''
|
||||
pat = '[' + section + ']'
|
||||
in_section = False
|
||||
file = open(path)
|
||||
for line in file:
|
||||
if (line.rstrip() == pat):
|
||||
if line.rstrip() == pat:
|
||||
in_section = True
|
||||
continue
|
||||
if in_section:
|
||||
|
|
@ -350,10 +508,10 @@ class App(Base):
|
|||
file.close()
|
||||
return ret.rstrip()
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def read_value(self, path: str, section: str, key: str) -> str|None:
|
||||
@lru_cache(maxsize = None)
|
||||
def read_value(self, path: str, section: str, key: str) -> str | None:
|
||||
|
||||
def scan_section(f, key: str) -> str|None:
|
||||
def scan_section(f, key: str) -> str | None:
|
||||
if key is None:
|
||||
ret = ''
|
||||
for line in f:
|
||||
|
|
@ -374,43 +532,44 @@ class App(Base):
|
|||
cont_line = ''
|
||||
rx = re.compile(r'^\s*' + key + r'\s*=\s*(.*)\s*$')
|
||||
for line in lines:
|
||||
#log(DEBUG, ' looking for "%s" in line="%s"' % (key, line))
|
||||
# log(DEBUG, ' looking for "%s" in line="%s"' % (key, line))
|
||||
m = re.search(rx, line)
|
||||
if m is not None:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
def scan_section_debug(f, key: str) -> str|None:
|
||||
def scan_section_debug(f, key: str) -> str | None:
|
||||
ret = scan_section(f, key)
|
||||
#log(DEBUG, ' returning', rr)
|
||||
# log(DEBUG, ' returning', rr)
|
||||
return ret
|
||||
|
||||
try:
|
||||
#log(DEBUG, 'looking for {}::[{}].{}'.format(path, section, key))
|
||||
# log(DEBUG, 'looking for {}::[{}].{}'.format(path, section, key))
|
||||
# TODO: Parse
|
||||
with open(path, 'r') as f:
|
||||
if not len(section):
|
||||
rr = scan_section(f, key)
|
||||
return scan_section(f, key)
|
||||
pat = '[' + section + ']'
|
||||
for line in f:
|
||||
if line.rstrip() == pat:
|
||||
return scan_section(f, key)
|
||||
return None
|
||||
except:
|
||||
log(DEBUG, path, 'not found')
|
||||
except Exception:
|
||||
log(DEBUG, f'Not found: {path}')
|
||||
# TODO: handle this special case cleaner somewhere up the stack
|
||||
if section == 'build' and key == 'libname':
|
||||
return 'none'
|
||||
return None
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_value(self, project: str, section: str, key: str) -> str:
|
||||
if self.__top_name and project == self.__top_name:
|
||||
proj_root = self.__topdir
|
||||
else:
|
||||
proj_root = self.__projs_root + '/' + project
|
||||
@lru_cache(maxsize = None)
|
||||
def get_value(self, project: str, section: str, key: str) -> str | None:
|
||||
ret: str | None
|
||||
proj_dir = self.__proj_dir(project, pretty = False)
|
||||
if proj_dir is None:
|
||||
raise Exception(f"Can't get project directory for {project}")
|
||||
if section == 'version':
|
||||
proj_version_dirs = [ proj_root ]
|
||||
if proj_root != self.__topdir:
|
||||
proj_version_dirs = [proj_dir]
|
||||
if proj_dir != self.___topdir:
|
||||
proj_version_dirs.append('/usr/share/doc/packages/' + project)
|
||||
for d in proj_version_dirs:
|
||||
version_path = d + '/VERSION'
|
||||
|
|
@ -423,13 +582,24 @@ class App(Base):
|
|||
log(DEBUG, f'Ignoring unreadable file "{version_path}"')
|
||||
continue
|
||||
raise Exception(f'No version file found for project "{project}"')
|
||||
path = proj_root + '/make/project.conf'
|
||||
path = proj_dir + '/make/project.conf'
|
||||
ret = self.read_value(path, section, key)
|
||||
log(DEBUG, 'Lookup %s -> %s / [%s%s] -> "%s"' %
|
||||
(self.__top_name, project, section, '.' + key if key else '', ret))
|
||||
log(
|
||||
DEBUG,
|
||||
'Lookup %s -> %s / [%s%s] -> "%s"' %
|
||||
(self.__top_name, project, section, '.' + key if key else '', ret),
|
||||
)
|
||||
return ret
|
||||
|
||||
def get_values(self, projects: list[str], sections: list[str], keys: list[str]) -> list[str]:
|
||||
@lru_cache(maxsize = None)
|
||||
def get_version(self, project) -> str:
|
||||
ret = self.get_value(project, 'version', '')
|
||||
if ret is None:
|
||||
raise Exception(f"Can't get version of project {project}")
|
||||
return ret
|
||||
|
||||
def get_values(self, projects: list[str], sections: list[str],
|
||||
keys: list[str]) -> list[str]:
|
||||
"""
|
||||
Collect a list of values from a list of given projects, sections and
|
||||
keys, maintaining order
|
||||
|
|
@ -441,39 +611,60 @@ class App(Base):
|
|||
vals = self.get_value(p, section, key)
|
||||
if vals:
|
||||
ret += [val.strip() for val in vals.split(',')]
|
||||
return list(dict.fromkeys(ret)) # Remove duplicates, keep ordering
|
||||
return list(dict.fromkeys(ret)) # Remove duplicates, keep ordering
|
||||
|
||||
def get_project_refs(self, projects: list[str], sections: list[str],
|
||||
keys: str|list[str], add_self: bool, scope: Scope, names_only=True) -> list[str]:
|
||||
def get_project_refs(
|
||||
self,
|
||||
projects: list[str],
|
||||
sections: list[str],
|
||||
keys: str | list[str],
|
||||
add_self: bool,
|
||||
scope: Scope,
|
||||
names_only = True,
|
||||
) -> list[str]:
|
||||
if isinstance(keys, str):
|
||||
keys = [ keys ]
|
||||
keys = [keys]
|
||||
ret: list[str] = []
|
||||
for section in sections:
|
||||
for key in keys:
|
||||
visited = set()
|
||||
visited: set[str] = set()
|
||||
for name in projects:
|
||||
rr: list[str] = []
|
||||
self.__get_project_refs_cached(rr, visited, name, section, key, add_self, scope, names_only)
|
||||
self.__get_project_refs_cached(
|
||||
rr, visited, name, section, key, add_self, scope, names_only
|
||||
)
|
||||
# TODO: this looks like a performance hogger
|
||||
for m in rr:
|
||||
if not m in ret:
|
||||
ret.append(m)
|
||||
if m not in ret:
|
||||
ret.append(m)
|
||||
return ret
|
||||
|
||||
def get_libname(self, projects) -> str:
|
||||
vals = self.get_project_refs(projects, ['build'], 'libname',
|
||||
scope = Scope.One, add_self=False, names_only=True)
|
||||
vals = self.get_project_refs(
|
||||
projects,
|
||||
['build'],
|
||||
'libname',
|
||||
scope = Scope.One,
|
||||
add_self = False,
|
||||
names_only = True,
|
||||
)
|
||||
if not vals:
|
||||
return ' '.join(projects)
|
||||
if 'none' in vals:
|
||||
vals.remove('none')
|
||||
return ' '.join(reversed(vals))
|
||||
|
||||
def is_excluded_from_build(self, project: str) -> str|None:
|
||||
def is_excluded_from_build(self, project: str) -> str | None:
|
||||
log(DEBUG, 'checking if project ' + project + ' is excluded from build')
|
||||
exclude = self.get_project_refs([ project ], ['build'], 'exclude',
|
||||
scope = Scope.One, add_self=False, names_only=True)
|
||||
cascade = self.distro.os_cascade + [ 'all' ]
|
||||
exclude = self.get_project_refs(
|
||||
[project],
|
||||
['build'],
|
||||
'exclude',
|
||||
scope = Scope.One,
|
||||
add_self = False,
|
||||
names_only = True,
|
||||
)
|
||||
cascade = self.distro.os_cascade + ['all']
|
||||
for p1 in exclude:
|
||||
for p2 in cascade:
|
||||
if p1 == p2:
|
||||
|
|
|
|||