xref: /dpdk/dts/framework/utils.py (revision 6ef07151aac4b4d9601d547f94a996d1c71b3871)
1812c4071SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
2812c4071SJuraj Linkeš# Copyright(c) 2010-2014 Intel Corporation
3680d8a24SJuraj Linkeš# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4680d8a24SJuraj Linkeš# Copyright(c) 2022-2023 University of New Hampshire
5812c4071SJuraj Linkeš
6*6ef07151SJuraj Linkeš"""Various utility classes and functions.
7*6ef07151SJuraj Linkeš
8*6ef07151SJuraj LinkešThese are used in multiple modules across the framework. They're here because
9*6ef07151SJuraj Linkešthey provide some non-specific functionality, greatly simplify imports or just don't
10*6ef07151SJuraj Linkešfit elsewhere.
11*6ef07151SJuraj Linkeš
12*6ef07151SJuraj LinkešAttributes:
13*6ef07151SJuraj Linkeš    REGEX_FOR_PCI_ADDRESS: The regex representing a PCI address, e.g. ``0000:00:08.0``.
14*6ef07151SJuraj Linkeš"""
15*6ef07151SJuraj Linkeš
16c9d31a7eSJuraj Linkešimport atexit
17cecfe0aaSJuraj Linkešimport json
18c9d31a7eSJuraj Linkešimport os
19c9d31a7eSJuraj Linkešimport subprocess
20c9d31a7eSJuraj Linkešfrom enum import Enum
21c9d31a7eSJuraj Linkešfrom pathlib import Path
22c9d31a7eSJuraj Linkešfrom subprocess import SubprocessError
23c9d31a7eSJuraj Linkeš
24cecfe0aaSJuraj Linkešfrom scapy.packet import Packet  # type: ignore[import]
25cecfe0aaSJuraj Linkeš
26c9d31a7eSJuraj Linkešfrom .exception import ConfigurationError
27c9d31a7eSJuraj Linkeš
28840b1e01SJuraj LinkešREGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"
2957c58bf8SJuraj Linkeš
30812c4071SJuraj Linkeš
31c020b7ceSJuraj Linkešdef expand_range(range_str: str) -> list[int]:
32*6ef07151SJuraj Linkeš    """Process `range_str` into a list of integers.
33c020b7ceSJuraj Linkeš
34*6ef07151SJuraj Linkeš    There are two possible formats of `range_str`:
35*6ef07151SJuraj Linkeš
36*6ef07151SJuraj Linkeš        * ``n`` - a single integer,
37*6ef07151SJuraj Linkeš        * ``n-m`` - a range of integers.
38*6ef07151SJuraj Linkeš
39*6ef07151SJuraj Linkeš    The returned range includes both ``n`` and ``m``. Empty string returns an empty list.
40*6ef07151SJuraj Linkeš
41*6ef07151SJuraj Linkeš    Args:
42*6ef07151SJuraj Linkeš        range_str: The range to expand.
43*6ef07151SJuraj Linkeš
44*6ef07151SJuraj Linkeš    Returns:
45*6ef07151SJuraj Linkeš        All the numbers from the range.
46c020b7ceSJuraj Linkeš    """
47c020b7ceSJuraj Linkeš    expanded_range: list[int] = []
48c020b7ceSJuraj Linkeš    if range_str:
49c020b7ceSJuraj Linkeš        range_boundaries = range_str.split("-")
50c020b7ceSJuraj Linkeš        # will throw an exception when items in range_boundaries can't be converted,
51c020b7ceSJuraj Linkeš        # serving as type check
52517b4b26SJuraj Linkeš        expanded_range.extend(range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1))
53c020b7ceSJuraj Linkeš
54c020b7ceSJuraj Linkeš    return expanded_range
55c020b7ceSJuraj Linkeš
56c020b7ceSJuraj Linkeš
57840b1e01SJuraj Linkešdef get_packet_summaries(packets: list[Packet]) -> str:
58*6ef07151SJuraj Linkeš    """Format a string summary from `packets`.
59*6ef07151SJuraj Linkeš
60*6ef07151SJuraj Linkeš    Args:
61*6ef07151SJuraj Linkeš        packets: The packets to format.
62*6ef07151SJuraj Linkeš
63*6ef07151SJuraj Linkeš    Returns:
64*6ef07151SJuraj Linkeš        The summary of `packets`.
65*6ef07151SJuraj Linkeš    """
66cecfe0aaSJuraj Linkeš    if len(packets) == 1:
67cecfe0aaSJuraj Linkeš        packet_summaries = packets[0].summary()
68cecfe0aaSJuraj Linkeš    else:
69517b4b26SJuraj Linkeš        packet_summaries = json.dumps(list(map(lambda pkt: pkt.summary(), packets)), indent=4)
70cecfe0aaSJuraj Linkeš    return f"Packet contents: \n{packet_summaries}"
71cecfe0aaSJuraj Linkeš
72cecfe0aaSJuraj Linkeš
73840b1e01SJuraj Linkešclass StrEnum(Enum):
74*6ef07151SJuraj Linkeš    """Enum with members stored as strings."""
75*6ef07151SJuraj Linkeš
76840b1e01SJuraj Linkeš    @staticmethod
77840b1e01SJuraj Linkeš    def _generate_next_value_(name: str, start: int, count: int, last_values: object) -> str:
78840b1e01SJuraj Linkeš        return name
79840b1e01SJuraj Linkeš
80840b1e01SJuraj Linkeš    def __str__(self) -> str:
81*6ef07151SJuraj Linkeš        """The string representation is the name of the member."""
82840b1e01SJuraj Linkeš        return self.name
83680d8a24SJuraj Linkeš
84680d8a24SJuraj Linkeš
85680d8a24SJuraj Linkešclass MesonArgs(object):
86*6ef07151SJuraj Linkeš    """Aggregate the arguments needed to build DPDK."""
87680d8a24SJuraj Linkeš
88680d8a24SJuraj Linkeš    _default_library: str
89680d8a24SJuraj Linkeš
90680d8a24SJuraj Linkeš    def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
91*6ef07151SJuraj Linkeš        """Initialize the meson arguments.
92*6ef07151SJuraj Linkeš
93*6ef07151SJuraj Linkeš        Args:
94*6ef07151SJuraj Linkeš            default_library: The default library type, Meson supports ``shared``, ``static`` and
95*6ef07151SJuraj Linkeš                ``both``. Defaults to :data:`None`, in which case the argument won't be used.
96*6ef07151SJuraj Linkeš            dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory.
97*6ef07151SJuraj Linkeš                Do not use ``-D`` with them.
98*6ef07151SJuraj Linkeš
99*6ef07151SJuraj Linkeš        Example:
100*6ef07151SJuraj Linkeš            ::
101*6ef07151SJuraj Linkeš
102*6ef07151SJuraj Linkeš                meson_args = MesonArgs(enable_kmods=True).
103*6ef07151SJuraj Linkeš        """
104517b4b26SJuraj Linkeš        self._default_library = f"--default-library={default_library}" if default_library else ""
105680d8a24SJuraj Linkeš        self._dpdk_args = " ".join(
106680d8a24SJuraj Linkeš            (
107680d8a24SJuraj Linkeš                f"-D{dpdk_arg_name}={dpdk_arg_value}"
108680d8a24SJuraj Linkeš                for dpdk_arg_name, dpdk_arg_value in dpdk_args.items()
109680d8a24SJuraj Linkeš            )
110680d8a24SJuraj Linkeš        )
111680d8a24SJuraj Linkeš
112680d8a24SJuraj Linkeš    def __str__(self) -> str:
113*6ef07151SJuraj Linkeš        """The actual args."""
114680d8a24SJuraj Linkeš        return " ".join(f"{self._default_library} {self._dpdk_args}".split())
115c9d31a7eSJuraj Linkeš
116c9d31a7eSJuraj Linkeš
117c9d31a7eSJuraj Linkešclass _TarCompressionFormat(StrEnum):
118c9d31a7eSJuraj Linkeš    """Compression formats that tar can use.
119c9d31a7eSJuraj Linkeš
120c9d31a7eSJuraj Linkeš    Enum names are the shell compression commands
121c9d31a7eSJuraj Linkeš    and Enum values are the associated file extensions.
122c9d31a7eSJuraj Linkeš    """
123c9d31a7eSJuraj Linkeš
124c9d31a7eSJuraj Linkeš    gzip = "gz"
125c9d31a7eSJuraj Linkeš    compress = "Z"
126c9d31a7eSJuraj Linkeš    bzip2 = "bz2"
127c9d31a7eSJuraj Linkeš    lzip = "lz"
128c9d31a7eSJuraj Linkeš    lzma = "lzma"
129c9d31a7eSJuraj Linkeš    lzop = "lzo"
130c9d31a7eSJuraj Linkeš    xz = "xz"
131c9d31a7eSJuraj Linkeš    zstd = "zst"
132c9d31a7eSJuraj Linkeš
133c9d31a7eSJuraj Linkeš
134c9d31a7eSJuraj Linkešclass DPDKGitTarball(object):
135*6ef07151SJuraj Linkeš    """Compressed tarball of DPDK from the repository.
136c9d31a7eSJuraj Linkeš
137*6ef07151SJuraj Linkeš    The class supports the :class:`os.PathLike` protocol,
138c9d31a7eSJuraj Linkeš    which is used to get the Path of the tarball::
139c9d31a7eSJuraj Linkeš
140c9d31a7eSJuraj Linkeš        from pathlib import Path
141c9d31a7eSJuraj Linkeš        tarball = DPDKGitTarball("HEAD", "output")
142c9d31a7eSJuraj Linkeš        tarball_path = Path(tarball)
143c9d31a7eSJuraj Linkeš    """
144c9d31a7eSJuraj Linkeš
145c9d31a7eSJuraj Linkeš    _git_ref: str
146c9d31a7eSJuraj Linkeš    _tar_compression_format: _TarCompressionFormat
147c9d31a7eSJuraj Linkeš    _tarball_dir: Path
148c9d31a7eSJuraj Linkeš    _tarball_name: str
149c9d31a7eSJuraj Linkeš    _tarball_path: Path | None
150c9d31a7eSJuraj Linkeš
151c9d31a7eSJuraj Linkeš    def __init__(
152c9d31a7eSJuraj Linkeš        self,
153c9d31a7eSJuraj Linkeš        git_ref: str,
154c9d31a7eSJuraj Linkeš        output_dir: str,
155c9d31a7eSJuraj Linkeš        tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz,
156c9d31a7eSJuraj Linkeš    ):
157*6ef07151SJuraj Linkeš        """Create the tarball during initialization.
158*6ef07151SJuraj Linkeš
159*6ef07151SJuraj Linkeš        The DPDK version is specified with `git_ref`. The tarball will be compressed with
160*6ef07151SJuraj Linkeš        `tar_compression_format`, which must be supported by the DTS execution environment.
161*6ef07151SJuraj Linkeš        The resulting tarball will be put into `output_dir`.
162*6ef07151SJuraj Linkeš
163*6ef07151SJuraj Linkeš        Args:
164*6ef07151SJuraj Linkeš            git_ref: A git commit ID, tag ID or tree ID.
165*6ef07151SJuraj Linkeš            output_dir: The directory where to put the resulting tarball.
166*6ef07151SJuraj Linkeš            tar_compression_format: The compression format to use.
167*6ef07151SJuraj Linkeš        """
168c9d31a7eSJuraj Linkeš        self._git_ref = git_ref
169c9d31a7eSJuraj Linkeš        self._tar_compression_format = tar_compression_format
170c9d31a7eSJuraj Linkeš
171c9d31a7eSJuraj Linkeš        self._tarball_dir = Path(output_dir, "tarball")
172c9d31a7eSJuraj Linkeš
173c9d31a7eSJuraj Linkeš        self._get_commit_id()
174c9d31a7eSJuraj Linkeš        self._create_tarball_dir()
175c9d31a7eSJuraj Linkeš
176c9d31a7eSJuraj Linkeš        self._tarball_name = (
177c9d31a7eSJuraj Linkeš            f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}"
178c9d31a7eSJuraj Linkeš        )
179c9d31a7eSJuraj Linkeš        self._tarball_path = self._check_tarball_path()
180c9d31a7eSJuraj Linkeš        if not self._tarball_path:
181c9d31a7eSJuraj Linkeš            self._create_tarball()
182c9d31a7eSJuraj Linkeš
183c9d31a7eSJuraj Linkeš    def _get_commit_id(self) -> None:
184c9d31a7eSJuraj Linkeš        result = subprocess.run(
185c9d31a7eSJuraj Linkeš            ["git", "rev-parse", "--verify", self._git_ref],
186c9d31a7eSJuraj Linkeš            text=True,
187c9d31a7eSJuraj Linkeš            capture_output=True,
188c9d31a7eSJuraj Linkeš        )
189c9d31a7eSJuraj Linkeš        if result.returncode != 0:
190c9d31a7eSJuraj Linkeš            raise ConfigurationError(
191c9d31a7eSJuraj Linkeš                f"{self._git_ref} is neither a path to an existing DPDK "
192c9d31a7eSJuraj Linkeš                "archive nor a valid git reference.\n"
193c9d31a7eSJuraj Linkeš                f"Command: {result.args}\n"
194c9d31a7eSJuraj Linkeš                f"Stdout: {result.stdout}\n"
195c9d31a7eSJuraj Linkeš                f"Stderr: {result.stderr}"
196c9d31a7eSJuraj Linkeš            )
197c9d31a7eSJuraj Linkeš        self._git_ref = result.stdout.strip()
198c9d31a7eSJuraj Linkeš
199c9d31a7eSJuraj Linkeš    def _create_tarball_dir(self) -> None:
200c9d31a7eSJuraj Linkeš        os.makedirs(self._tarball_dir, exist_ok=True)
201c9d31a7eSJuraj Linkeš
202c9d31a7eSJuraj Linkeš    def _check_tarball_path(self) -> Path | None:
203c9d31a7eSJuraj Linkeš        if self._tarball_name in os.listdir(self._tarball_dir):
204c9d31a7eSJuraj Linkeš            return Path(self._tarball_dir, self._tarball_name)
205c9d31a7eSJuraj Linkeš        return None
206c9d31a7eSJuraj Linkeš
207c9d31a7eSJuraj Linkeš    def _create_tarball(self) -> None:
208c9d31a7eSJuraj Linkeš        self._tarball_path = Path(self._tarball_dir, self._tarball_name)
209c9d31a7eSJuraj Linkeš
210c9d31a7eSJuraj Linkeš        atexit.register(self._delete_tarball)
211c9d31a7eSJuraj Linkeš
212c9d31a7eSJuraj Linkeš        result = subprocess.run(
213c9d31a7eSJuraj Linkeš            'git -C "$(git rev-parse --show-toplevel)" archive '
214c9d31a7eSJuraj Linkeš            f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | '
215c9d31a7eSJuraj Linkeš            f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}",
216c9d31a7eSJuraj Linkeš            shell=True,
217c9d31a7eSJuraj Linkeš            text=True,
218c9d31a7eSJuraj Linkeš            capture_output=True,
219c9d31a7eSJuraj Linkeš        )
220c9d31a7eSJuraj Linkeš
221c9d31a7eSJuraj Linkeš        if result.returncode != 0:
222c9d31a7eSJuraj Linkeš            raise SubprocessError(
223c9d31a7eSJuraj Linkeš                f"Git archive creation failed with exit code {result.returncode}.\n"
224c9d31a7eSJuraj Linkeš                f"Command: {result.args}\n"
225c9d31a7eSJuraj Linkeš                f"Stdout: {result.stdout}\n"
226c9d31a7eSJuraj Linkeš                f"Stderr: {result.stderr}"
227c9d31a7eSJuraj Linkeš            )
228c9d31a7eSJuraj Linkeš
229c9d31a7eSJuraj Linkeš        atexit.unregister(self._delete_tarball)
230c9d31a7eSJuraj Linkeš
231c9d31a7eSJuraj Linkeš    def _delete_tarball(self) -> None:
232c9d31a7eSJuraj Linkeš        if self._tarball_path and os.path.exists(self._tarball_path):
233c9d31a7eSJuraj Linkeš            os.remove(self._tarball_path)
234c9d31a7eSJuraj Linkeš
235840b1e01SJuraj Linkeš    def __fspath__(self) -> str:
236*6ef07151SJuraj Linkeš        """The os.PathLike protocol implementation."""
237c9d31a7eSJuraj Linkeš        return str(self._tarball_path)
238