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