xref: /dpdk/dts/framework/utils.py (revision c9d31a7e1c94e82e56d5d7c28edd32874eef95e1)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2010-2014 Intel Corporation
3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4# Copyright(c) 2022-2023 University of New Hampshire
5
6import atexit
7import os
8import subprocess
9import sys
10from enum import Enum
11from pathlib import Path
12from subprocess import SubprocessError
13
14from .exception import ConfigurationError
15
16
17class StrEnum(Enum):
18    @staticmethod
19    def _generate_next_value_(
20        name: str, start: int, count: int, last_values: object
21    ) -> str:
22        return name
23
24    def __str__(self) -> str:
25        return self.name
26
27
28def check_dts_python_version() -> None:
29    if sys.version_info.major < 3 or (
30        sys.version_info.major == 3 and sys.version_info.minor < 10
31    ):
32        print(
33            RED(
34                (
35                    "WARNING: DTS execution node's python version is lower than"
36                    "python 3.10, is deprecated and will not work in future releases."
37                )
38            ),
39            file=sys.stderr,
40        )
41        print(RED("Please use Python >= 3.10 instead"), file=sys.stderr)
42
43
44def expand_range(range_str: str) -> list[int]:
45    """
46    Process range string into a list of integers. There are two possible formats:
47    n - a single integer
48    n-m - a range of integers
49
50    The returned range includes both n and m. Empty string returns an empty list.
51    """
52    expanded_range: list[int] = []
53    if range_str:
54        range_boundaries = range_str.split("-")
55        # will throw an exception when items in range_boundaries can't be converted,
56        # serving as type check
57        expanded_range.extend(
58            range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)
59        )
60
61    return expanded_range
62
63
64def GREEN(text: str) -> str:
65    return f"\u001B[32;1m{str(text)}\u001B[0m"
66
67
68def RED(text: str) -> str:
69    return f"\u001B[31;1m{str(text)}\u001B[0m"
70
71
72class EnvVarsDict(dict):
73    def __str__(self) -> str:
74        return " ".join(["=".join(item) for item in self.items()])
75
76
77class MesonArgs(object):
78    """
79    Aggregate the arguments needed to build DPDK:
80    default_library: Default library type, Meson allows "shared", "static" and "both".
81               Defaults to None, in which case the argument won't be used.
82    Keyword arguments: The arguments found in meson_options.txt in root DPDK directory.
83               Do not use -D with them, for example:
84               meson_args = MesonArgs(enable_kmods=True).
85    """
86
87    _default_library: str
88
89    def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
90        self._default_library = (
91            f"--default-library={default_library}" if default_library else ""
92        )
93        self._dpdk_args = " ".join(
94            (
95                f"-D{dpdk_arg_name}={dpdk_arg_value}"
96                for dpdk_arg_name, dpdk_arg_value in dpdk_args.items()
97            )
98        )
99
100    def __str__(self) -> str:
101        return " ".join(f"{self._default_library} {self._dpdk_args}".split())
102
103
104class _TarCompressionFormat(StrEnum):
105    """Compression formats that tar can use.
106
107    Enum names are the shell compression commands
108    and Enum values are the associated file extensions.
109    """
110
111    gzip = "gz"
112    compress = "Z"
113    bzip2 = "bz2"
114    lzip = "lz"
115    lzma = "lzma"
116    lzop = "lzo"
117    xz = "xz"
118    zstd = "zst"
119
120
121class DPDKGitTarball(object):
122    """Create a compressed tarball of DPDK from the repository.
123
124    The DPDK version is specified with git object git_ref.
125    The tarball will be compressed with _TarCompressionFormat,
126    which must be supported by the DTS execution environment.
127    The resulting tarball will be put into output_dir.
128
129    The class supports the os.PathLike protocol,
130    which is used to get the Path of the tarball::
131
132        from pathlib import Path
133        tarball = DPDKGitTarball("HEAD", "output")
134        tarball_path = Path(tarball)
135
136    Arguments:
137        git_ref: A git commit ID, tag ID or tree ID.
138        output_dir: The directory where to put the resulting tarball.
139        tar_compression_format: The compression format to use.
140    """
141
142    _git_ref: str
143    _tar_compression_format: _TarCompressionFormat
144    _tarball_dir: Path
145    _tarball_name: str
146    _tarball_path: Path | None
147
148    def __init__(
149        self,
150        git_ref: str,
151        output_dir: str,
152        tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz,
153    ):
154        self._git_ref = git_ref
155        self._tar_compression_format = tar_compression_format
156
157        self._tarball_dir = Path(output_dir, "tarball")
158
159        self._get_commit_id()
160        self._create_tarball_dir()
161
162        self._tarball_name = (
163            f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}"
164        )
165        self._tarball_path = self._check_tarball_path()
166        if not self._tarball_path:
167            self._create_tarball()
168
169    def _get_commit_id(self) -> None:
170        result = subprocess.run(
171            ["git", "rev-parse", "--verify", self._git_ref],
172            text=True,
173            capture_output=True,
174        )
175        if result.returncode != 0:
176            raise ConfigurationError(
177                f"{self._git_ref} is neither a path to an existing DPDK "
178                "archive nor a valid git reference.\n"
179                f"Command: {result.args}\n"
180                f"Stdout: {result.stdout}\n"
181                f"Stderr: {result.stderr}"
182            )
183        self._git_ref = result.stdout.strip()
184
185    def _create_tarball_dir(self) -> None:
186        os.makedirs(self._tarball_dir, exist_ok=True)
187
188    def _check_tarball_path(self) -> Path | None:
189        if self._tarball_name in os.listdir(self._tarball_dir):
190            return Path(self._tarball_dir, self._tarball_name)
191        return None
192
193    def _create_tarball(self) -> None:
194        self._tarball_path = Path(self._tarball_dir, self._tarball_name)
195
196        atexit.register(self._delete_tarball)
197
198        result = subprocess.run(
199            'git -C "$(git rev-parse --show-toplevel)" archive '
200            f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | '
201            f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}",
202            shell=True,
203            text=True,
204            capture_output=True,
205        )
206
207        if result.returncode != 0:
208            raise SubprocessError(
209                f"Git archive creation failed with exit code {result.returncode}.\n"
210                f"Command: {result.args}\n"
211                f"Stdout: {result.stdout}\n"
212                f"Stderr: {result.stderr}"
213            )
214
215        atexit.unregister(self._delete_tarball)
216
217    def _delete_tarball(self) -> None:
218        if self._tarball_path and os.path.exists(self._tarball_path):
219            os.remove(self._tarball_path)
220
221    def __fspath__(self):
222        return str(self._tarball_path)
223