aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc-André Lureau <marcandre.lureau@redhat.com>2025-09-02 12:54:25 +0400
committerMarc-André Lureau <marcandre.lureau@redhat.com>2025-09-03 16:18:12 +0400
commitbbcb92960abf67d01da056c95bfc88067fd61e26 (patch)
treed4f535a11ff4f9fcbf8562598dc6ea0f80f9b701
parentfce4fb9b8840b84f0f3d966e54fbef73ec602aea (diff)
downloadlibvirt-ci-bbcb92960abf67d01da056c95bfc88067fd61e26.zip
libvirt-ci-bbcb92960abf67d01da056c95bfc88067fd61e26.tar.gz
libvirt-ci-bbcb92960abf67d01da056c95bfc88067fd61e26.tar.bz2
formatters: add type annotations
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
-rw-r--r--lcitool/formatters.py234
-rw-r--r--pyproject.toml1
2 files changed, 164 insertions, 71 deletions
diff --git a/lcitool/formatters.py b/lcitool/formatters.py
index 33c12c6..35a27c7 100644
--- a/lcitool/formatters.py
+++ b/lcitool/formatters.py
@@ -11,6 +11,9 @@ import shlex
from lcitool import util, LcitoolError
from lcitool.packages import package_names_by_type
+from lcitool.projects import Projects
+from lcitool.targets import BuildTarget
+from typing import Any, Dict, List, Optional, Union
log = logging.getLogger(__name__)
@@ -28,17 +31,17 @@ class FormatterError(LcitoolError):
class DockerfileError(FormatterError):
- def __init__(self, message):
+ def __init__(self, message: str):
super().__init__(message, "Docker formatter")
class VariablesError(FormatterError):
- def __init__(self, message):
+ def __init__(self, message: str):
super().__init__(message, "Variables formatter")
class ShellBuildEnvError(FormatterError):
- def __init__(self, message):
+ def __init__(self, message: str):
super().__init__(message, "Shell build env formatter")
@@ -47,11 +50,11 @@ class Formatter(metaclass=abc.ABCMeta):
This an abstract base class that each formatter must subclass.
"""
- def __init__(self, projects):
+ def __init__(self, projects: Projects) -> None:
self._projects = projects
@abc.abstractmethod
- def format(self):
+ def format(self, target: BuildTarget, selected_projects: List[str]) -> str:
"""
Outputs a recipe using format implemented by a Foo(Formatter) subclass
@@ -63,12 +66,14 @@ class Formatter(metaclass=abc.ABCMeta):
"""
pass
- def _get_meson_cross(self, cross_abi):
+ def _get_meson_cross(self, cross_abi: str) -> str:
cross_path = util.package_resource(__package__, f"cross/{cross_abi}.meson")
with open(cross_path, "r") as c:
return c.read().rstrip()
- def _generator_build_varmap(self, target, selected_projects):
+ def _generator_build_varmap(
+ self, target: BuildTarget, selected_projects: List[str]
+ ) -> Dict[str, Union[str, List[str]]]:
projects = self._projects
# we need the 'base' internal project here, but packages for internal
@@ -107,7 +112,9 @@ class Formatter(metaclass=abc.ABCMeta):
cross_arch_deb = util.native_arch_to_deb_arch(target.cross_arch)
varmap["cross_arch_deb"] = cross_arch_deb
- if "rust" in varmap["mappings"]:
+ mappings = varmap["mappings"]
+ assert isinstance(mappings, list)
+ if "rust" in mappings:
varmap["cross_rust_target"] = util.native_arch_to_rust_target(
varmap["cross_arch"]
)
@@ -118,13 +125,19 @@ class Formatter(metaclass=abc.ABCMeta):
class BuildEnvFormatter(Formatter):
- def __init__(self, inventory, indent=0, pkgcleanup=False, nosync=False):
+ def __init__(
+ self,
+ inventory: Projects,
+ indent: int = 0,
+ pkgcleanup: bool = False,
+ nosync: bool = False,
+ ) -> None:
super().__init__(inventory)
self._indent = indent
self._pkgcleanup = pkgcleanup
self._nosync = nosync
- def _align(self, command, strings):
+ def _align(self, command: str, strings: List[str]) -> str:
if len(strings) == 1:
return strings[0]
@@ -132,7 +145,9 @@ class BuildEnvFormatter(Formatter):
strings = [shlex.quote(x) for x in strings]
return align[1:] + align.join(strings)
- def _generator_build_varmap(self, target, selected_projects):
+ def _generator_build_varmap(
+ self, target: BuildTarget, selected_projects: List[str]
+ ) -> Dict[str, Union[str, List[str]]]:
varmap = super()._generator_build_varmap(target, selected_projects)
varmap["nosync"] = ""
@@ -153,36 +168,53 @@ class BuildEnvFormatter(Formatter):
pass
nosync = varmap["nosync"]
+ assert isinstance(nosync, str)
+
+ pkgs = varmap["pkgs"]
+ assert isinstance(pkgs, list)
varmap["pkgs"] = self._align(
- nosync + target.facts["packaging"]["command"], varmap["pkgs"]
+ nosync + target.facts["packaging"]["command"], pkgs
)
- if varmap["cross_pkgs"]:
+ cross_pkgs = varmap["cross_pkgs"]
+ if cross_pkgs:
+ assert isinstance(cross_pkgs, list)
varmap["cross_pkgs"] = self._align(
- nosync + target.facts["packaging"]["command"], varmap["cross_pkgs"]
+ nosync + target.facts["packaging"]["command"], cross_pkgs
)
- if varmap["pypi_pkgs"]:
+
+ pypi_pkgs = varmap["pypi_pkgs"]
+ if pypi_pkgs:
+ assert isinstance(pypi_pkgs, list)
varmap["pypi_pkgs"] = self._align(
- nosync + target.facts["paths"]["pip3"], varmap["pypi_pkgs"]
+ nosync + target.facts["paths"]["pip3"], pypi_pkgs
)
- if varmap["cpan_pkgs"]:
- varmap["cpan_pkgs"] = self._align(nosync + "cpanm", varmap["cpan_pkgs"])
+
+ cpan_pkgs = varmap["cpan_pkgs"]
+ if cpan_pkgs:
+ assert isinstance(cpan_pkgs, list)
+ varmap["cpan_pkgs"] = self._align(nosync + "cpanm", cpan_pkgs)
return varmap
- def _format_commands_ccache(self, target, varmap):
+ def _format_commands_ccache(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> List[Union[Any, str]]:
commands = []
compilers = set()
- if "ccache" not in varmap["mappings"]:
+ mappings = varmap["mappings"]
+ assert isinstance(mappings, list)
+
+ if "ccache" not in mappings:
return []
for compiler in ["gcc", "clang"]:
- if compiler in varmap["mappings"]:
+ if compiler in mappings:
compilers.add(compiler)
compilers.add("cc")
for compiler in ["g++"]:
- if compiler in varmap["mappings"]:
+ if compiler in mappings:
compilers.add(compiler)
compilers.add("c++")
@@ -203,7 +235,7 @@ class BuildEnvFormatter(Formatter):
)
return commands
- def _format_commands_pkglist(self, target):
+ def _format_commands_pkglist(self, target: BuildTarget) -> List[str]:
facts = target.facts
commands = []
if facts["packaging"]["format"] == "apk":
@@ -218,12 +250,17 @@ class BuildEnvFormatter(Formatter):
commands.extend(["rpm -qa | sort > /packages.txt"])
return commands
- def _format_commands_native(self, target, varmap):
+ def _format_commands_native(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> List[List[str]]:
facts = target.facts
commands = []
osname = facts["os"]["name"]
osversion = facts["os"]["version"]
+ mappings = varmap["mappings"]
+ assert isinstance(mappings, list)
+
if facts["packaging"]["format"] == "apk":
# See earlier comment about adding this later
# "{packaging_command} add libeatmydata",
@@ -362,7 +399,7 @@ class BuildEnvFormatter(Formatter):
# If distro forces "pip" to use a venv by default,
# then undo that, because our CI env is expected to
# have a mix of distro & pip installed pieces
- if "python3" in varmap["mappings"]:
+ if "python3" in mappings:
commands.extend(["rm -f /usr/lib*/python3*/EXTERNALLY-MANAGED"])
if not target.cross_arch:
@@ -380,22 +417,35 @@ class BuildEnvFormatter(Formatter):
return groups
- def _format_env_native(self, varmap):
+ def _format_env_native(
+ self, varmap: Dict[str, Union[str, List[str]]]
+ ) -> Dict[str, str]:
env = {}
+ mappings = varmap["mappings"]
+ assert isinstance(mappings, list)
+
env["LANG"] = "en_US.UTF-8"
- if "make" in varmap["mappings"]:
- env["MAKE"] = varmap["paths_make"]
- if "meson" in varmap["mappings"]:
- env["NINJA"] = varmap["paths_ninja"]
- if "python3" in varmap["mappings"]:
- env["PYTHON"] = varmap["paths_python"]
- if "ccache" in varmap["mappings"]:
+ if "make" in mappings:
+ make_path = varmap["paths_make"]
+ assert isinstance(make_path, str)
+ env["MAKE"] = make_path
+ if "meson" in mappings:
+ ninja_path = varmap["paths_ninja"]
+ assert isinstance(ninja_path, str)
+ env["NINJA"] = ninja_path
+ if "python3" in mappings:
+ python_path = varmap["paths_python"]
+ assert isinstance(python_path, str)
+ env["PYTHON"] = python_path
+ if "ccache" in mappings:
env["CCACHE_WRAPPERSDIR"] = "/usr/libexec/ccache-wrappers"
return env
- def _format_commands_foreign(self, target, varmap):
+ def _format_commands_foreign(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> List[str]:
facts = target.facts
cross_commands = []
@@ -448,7 +498,7 @@ class BuildEnvFormatter(Formatter):
]
)
- if not target.cross_arch.startswith("mingw"):
+ if target.cross_arch is not None and not target.cross_arch.startswith("mingw"):
cross_commands.extend(
[
"mkdir -p /usr/local/share/meson/cross",
@@ -456,7 +506,9 @@ class BuildEnvFormatter(Formatter):
]
)
- cross_meson = self._get_meson_cross(varmap["cross_abi"])
+ cross_abi = varmap["cross_abi"]
+ assert isinstance(cross_abi, str)
+ cross_meson = self._get_meson_cross(cross_abi)
varmap["cross_meson"] = cross_meson.replace("\n", "\\n\\\n")
cross_commands.extend(self._format_commands_pkglist(target))
@@ -466,45 +518,57 @@ class BuildEnvFormatter(Formatter):
return cross_commands
- def _format_env_foreign(self, target, varmap):
+ def _format_env_foreign(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> Dict[str, str]:
env = {}
- env["ABI"] = varmap["cross_abi"]
- if "autoconf" in varmap["mappings"]:
- env["CONFIGURE_OPTS"] = "--host=" + varmap["cross_abi"]
+ mappings = varmap["mappings"]
+ assert isinstance(mappings, list)
+ cross_abi = varmap["cross_abi"]
+ assert isinstance(cross_abi, str)
- if "meson" in varmap["mappings"]:
- if target.cross_arch.startswith("mingw"):
+ env["ABI"] = cross_abi
+
+ if "autoconf" in mappings:
+ env["CONFIGURE_OPTS"] = "--host=" + cross_abi
+
+ if "meson" in mappings:
+ if target.cross_arch is not None and target.cross_arch.startswith("mingw"):
+ cross_arch = varmap["cross_arch"]
+ assert isinstance(cross_arch, str)
env["MESON_OPTS"] = (
- "--cross-file=/usr/share/mingw/toolchain-"
- + varmap["cross_arch"]
- + ".meson"
+ "--cross-file=/usr/share/mingw/toolchain-" + cross_arch + ".meson"
)
else:
- env["MESON_OPTS"] = "--cross-file=" + varmap["cross_abi"]
+ env["MESON_OPTS"] = "--cross-file=" + cross_abi
- if varmap["cross_rust_target"] is not None:
- env["RUST_TARGET"] = varmap["cross_rust_target"]
+ cross_rust_target = varmap["cross_rust_target"]
+ if cross_rust_target is not None:
+ assert isinstance(cross_rust_target, str)
+ env["RUST_TARGET"] = cross_rust_target
return env
class DockerfileFormatter(BuildEnvFormatter):
- def __init__(self, inventory, base=None, layers="all"):
+ def __init__(
+ self, inventory: Projects, base: Optional[str] = None, layers: str = "all"
+ ):
super().__init__(inventory, indent=len("RUN "), pkgcleanup=True, nosync=True)
self._base = base
self._layers = layers
@staticmethod
- def _format_env(env):
+ def _format_env(env: Dict[str, str]) -> str:
lines = []
for key in sorted(env.keys()):
val = env[key]
lines.append(f'\nENV {key} "{val}"')
return "".join(lines)
- def _format_section_base(self, target):
+ def _format_section_base(self, target: BuildTarget) -> List[str]:
strings = []
if self._base:
base = self._base
@@ -513,7 +577,9 @@ class DockerfileFormatter(BuildEnvFormatter):
strings.append(f"FROM {base}")
return strings
- def _format_section_native(self, target, varmap):
+ def _format_section_native(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> List[str]:
groups = self._format_commands_native(target, varmap)
strings = []
@@ -524,7 +590,9 @@ class DockerfileFormatter(BuildEnvFormatter):
strings.append(self._format_env(env))
return strings
- def _format_section_foreign(self, target, varmap):
+ def _format_section_foreign(
+ self, target: BuildTarget, varmap: Dict[str, Union[str, List[str]]]
+ ) -> List[str]:
commands = self._format_commands_foreign(target, varmap)
strings = ["\nRUN " + " && \\\n ".join(commands)]
@@ -533,7 +601,12 @@ class DockerfileFormatter(BuildEnvFormatter):
strings.append(self._format_env(env))
return strings
- def _format_dockerfile(self, target, project, varmap):
+ def _format_dockerfile(
+ self,
+ target: BuildTarget,
+ project: List[str],
+ varmap: Dict[str, Union[str, List[str]]],
+ ) -> List[str]:
strings = []
strings.extend(self._format_section_base(target))
if self._layers in ["all", "native"]:
@@ -542,7 +615,7 @@ class DockerfileFormatter(BuildEnvFormatter):
strings.extend(self._format_section_foreign(target, varmap))
return strings
- def format(self, target, selected_projects):
+ def format(self, target: BuildTarget, selected_projects: List[str]) -> str:
"""
Generates and formats a Dockerfile.
@@ -574,7 +647,9 @@ class DockerfileFormatter(BuildEnvFormatter):
class VariablesFormatter(Formatter):
@staticmethod
- def _normalize_variables(varmap):
+ def _normalize_variables(
+ varmap: Dict[str, Union[str, List[str]]],
+ ) -> Dict[str, Union[str, List[str]]]:
normalized_vars = {}
for key in varmap:
if varmap[key] is None:
@@ -588,16 +663,18 @@ class VariablesFormatter(Formatter):
name = key[len("paths_") :]
else:
name = key
- normalized_vars[name] = varmap[key]
+ value = varmap[key]
+ assert value is not None # We already checked for None above
+ normalized_vars[name] = value
return normalized_vars
@staticmethod
@abc.abstractmethod
- def _format_variables(varmap):
+ def _format_variables(varmap: Dict[str, Union[str, List[str]]]) -> str:
pass
- def format(self, target, selected_projects):
+ def format(self, target: BuildTarget, selected_projects: List[str]) -> str:
"""
Generates and formats environment variables as KEY=VAL pairs.
@@ -615,17 +692,17 @@ class VariablesFormatter(Formatter):
)
try:
- varmap = self._generator_build_varmap(target, selected_projects)
+ raw_varmap = self._generator_build_varmap(target, selected_projects)
except FormatterError as ex:
raise VariablesError(str(ex))
- varmap = self._normalize_variables(varmap)
+ varmap = self._normalize_variables(raw_varmap)
return self._format_variables(varmap)
class ShellVariablesFormatter(VariablesFormatter):
@staticmethod
- def _format_variables(varmap):
+ def _format_variables(varmap: Dict[str, Union[str, List[str]]]) -> str:
strings = []
for key in sorted(varmap.keys()):
@@ -640,47 +717,62 @@ class ShellVariablesFormatter(VariablesFormatter):
class JSONVariablesFormatter(VariablesFormatter):
@staticmethod
- def _format_variables(varmap):
+ def _format_variables(varmap: Dict[str, Union[str, List[str]]]) -> str:
return json.dumps(varmap, indent=" ", sort_keys=True)
class YamlVariablesFormatter(VariablesFormatter):
@staticmethod
- def _format_variables(varmap):
- packages = varmap.get("pkgs", []) + varmap.get("cross_pkgs", [])
+ def _format_variables(varmap: Dict[str, Union[str, List[str]]]) -> str:
+ pkgs = varmap.get("pkgs", [])
+ cross_pkgs = varmap.get("cross_pkgs", [])
+
+ # Ensure both are lists
+ if isinstance(pkgs, str):
+ pkgs = [pkgs]
+ if isinstance(cross_pkgs, str):
+ cross_pkgs = [cross_pkgs]
+
+ packages = pkgs + cross_pkgs
yaml_output = "packages:\n"
for pkg in packages:
yaml_output += f" - {pkg}\n"
return yaml_output
- def format(self, target, selected_projects):
+ def format(self, target: BuildTarget, selected_projects: List[str]) -> str:
log.debug(
f"Generating YAML package list for projects "
f"'{selected_projects} on target {target}"
)
try:
- varmap = self._generator_build_varmap(target, selected_projects)
+ raw_varmap = self._generator_build_varmap(target, selected_projects)
except FormatterError as ex:
raise VariablesError(str(ex))
+ varmap = self._normalize_variables(raw_varmap)
return self._format_variables(varmap)
class ShellBuildEnvFormatter(BuildEnvFormatter):
- def __init__(self, inventory, base=None, layers="all"):
+ def __init__(self, inventory: Projects, base: None = None, layers: str = "all"):
super().__init__(inventory, indent=len(" "), pkgcleanup=False, nosync=False)
@staticmethod
- def _format_env(env):
+ def _format_env(env: Dict[str, str]) -> str:
exp = []
for key in sorted(env.keys()):
val = env[key]
exp.append(f'export {key}="{val}"')
return "\n" + "\n".join(exp)
- def _format_buildenv(self, target, project, varmap):
+ def _format_buildenv(
+ self,
+ target: BuildTarget,
+ project: List[str],
+ varmap: Dict[str, Union[str, List[str]]],
+ ) -> List[str]:
strings = [
"function install_buildenv() {",
]
@@ -697,7 +789,7 @@ class ShellBuildEnvFormatter(BuildEnvFormatter):
strings.append(self._format_env(self._format_env_foreign(target, varmap)))
return strings
- def format(self, target, selected_projects):
+ def format(self, target: BuildTarget, selected_projects: List[str]) -> str:
"""
Generates and formats a Shell script for preparing a build env.
diff --git a/pyproject.toml b/pyproject.toml
index 8be152e..1e315d8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,6 +70,7 @@ files = [
"lcitool/config.py",
"lcitool/containers/*.py",
"lcitool/install/*.py",
+ "lcitool/formatters.py",
"lcitool/gitlab.py",
"lcitool/inventory.py",
"lcitool/libvirt_wrapper.py",