From 9b05c3739c44418f47c2b50980fe24651a1eed1f Mon Sep 17 00:00:00 2001 From: Harrison Mutai <harrison.mutai@arm.com> Date: Wed, 21 Aug 2024 13:06:46 +0000 Subject: [PATCH] feat(tlc): add command gen-header Introduce the gen-header command to the tool, enabling developers to create language bindings. Currently, it supports generating C headers from a transfer list. Change-Id: Ibec75639c38577802d5abe55c7bc718740aad2b8 Signed-off-by: Harrison Mutai <harrison.mutai@arm.com> --- poetry.lock | 67 +++++++++++++++-------------- tools/tlc/poetry.lock | 2 +- tools/tlc/pyproject.toml | 1 + tools/tlc/tests/test_cli.py | 64 +++++++++++++++++++++++++++ tools/tlc/tlc/cli.py | 29 +++++++++++++ tools/tlc/tlc/templates/header.h.j2 | 16 +++++++ tools/tlc/tlc/tl.py | 30 ++++++++----- 7 files changed, 164 insertions(+), 45 deletions(-) create mode 100644 tools/tlc/tlc/templates/header.h.j2 diff --git a/poetry.lock b/poetry.lock index cd2a41aa9d..9a907044a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,13 +44,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "build" -version = "1.2.1" +version = "1.2.2" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.8" files = [ - {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, - {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, + {file = "build-1.2.2-py3-none-any.whl", hash = "sha256:277ccc71619d98afdd841a0e96ac9fe1593b823af481d3b0cea748e8894e0613"}, + {file = "build-1.2.2.tar.gz", hash = "sha256:119b2fb462adef986483438377a13b2f42064a2a3a4161f24a0cca698a07ac8c"}, ] [package.dependencies] @@ -80,13 +80,13 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -282,19 +282,19 @@ files = [ [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "idna" @@ -610,29 +610,29 @@ testing = ["flit-core (>=2,<4)", "poetry-core (>=1.0.0)", "pytest (>=7.2.0)", "p [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "plotly" -version = "5.23.0" +version = "5.24.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.23.0-py3-none-any.whl", hash = "sha256:76cbe78f75eddc10c56f5a4ee3e7ccaade7c0a57465546f02098c0caed6c2d1a"}, - {file = "plotly-5.23.0.tar.gz", hash = "sha256:89e57d003a116303a34de6700862391367dd564222ab71f8531df70279fc0193"}, + {file = "plotly-5.24.0-py3-none-any.whl", hash = "sha256:0e54efe52c8cef899f7daa41be9ed97dfb6be622613a2a8f56a86a0634b2b67e"}, + {file = "plotly-5.24.0.tar.gz", hash = "sha256:eae9f4f54448682442c92c1e97148e3ad0c52f0cf86306e1b76daba24add554a"}, ] [package.dependencies] @@ -869,13 +869,13 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "setuptools" -version = "74.0.0" +version = "74.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-74.0.0-py3-none-any.whl", hash = "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f"}, - {file = "setuptools-74.0.0.tar.gz", hash = "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e"}, + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, ] [package.extras] @@ -1147,6 +1147,7 @@ develop = true [package.dependencies] click = "^8.1.7" +jinja2 = "^3.1.4" pyyaml = "^6.0.1" rich = "^10.14.0" tox = "^4.18.0" @@ -1169,17 +1170,17 @@ files = [ [[package]] name = "tox" -version = "4.18.0" +version = "4.18.1" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.18.0-py3-none-any.whl", hash = "sha256:0a457400cf70615dc0627eb70d293e80cd95d8ce174bb40ac011011f0c03a249"}, - {file = "tox-4.18.0.tar.gz", hash = "sha256:5dfa1cab9f146becd6e351333a82f9e0ade374451630ba65ee54584624c27b58"}, + {file = "tox-4.18.1-py3-none-any.whl", hash = "sha256:35d472032ee1f73fe20c3e0e73d7073a4e85075c86ff02c576f9fc7c6a15a578"}, + {file = "tox-4.18.1.tar.gz", hash = "sha256:3c0c96bc3a568a5c7e66387a4cfcf8c875b52e09f4d47c9f7a277ec82f1a0b11"}, ] [package.dependencies] -cachetools = ">=5.4" +cachetools = ">=5.5" chardet = ">=5.2" colorama = ">=0.4.6" filelock = ">=3.15.4" @@ -1191,8 +1192,8 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} virtualenv = ">=20.26.3" [package.extras] -docs = ["furo (>=2024.7.18)", "sphinx (>=7.4.7)", "sphinx-argparse-cli (>=1.16)", "sphinx-autodoc-typehints (>=2.2.3)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] -testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.3)", "time-machine (>=2.14.2)", "wheel (>=0.43)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-argparse-cli (>=1.17)", "sphinx-autodoc-typehints (>=2.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=24.8)"] +testing = ["build[virtualenv] (>=1.2.2)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=74.1.2)", "time-machine (>=2.15)", "wheel (>=0.44)"] [[package]] name = "typer" @@ -1246,13 +1247,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, ] [package.dependencies] diff --git a/tools/tlc/poetry.lock b/tools/tlc/poetry.lock index 60402f155a..decec59878 100644 --- a/tools/tlc/poetry.lock +++ b/tools/tlc/poetry.lock @@ -1431,4 +1431,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "9ad739a282f180a9c47cf8cb8f1f53a167e1739b1c5bf572932384b45749df26" +content-hash = "aac9123f3fa544b8c3e9b085f41f5a1c6c4ed2d59ce3236dcda6ea2aef5a694c" diff --git a/tools/tlc/pyproject.toml b/tools/tlc/pyproject.toml index ea842e53bc..b606238411 100644 --- a/tools/tlc/pyproject.toml +++ b/tools/tlc/pyproject.toml @@ -38,6 +38,7 @@ typer = {extras = ["all"], version = "^0.4.0"} rich = "^10.14.0" click = "^8.1.7" pyyaml = "^6.0.1" +tox = "^4.18.0" jinja2 = "^3.1.4" [tool.poetry.group.dev] diff --git a/tools/tlc/tests/test_cli.py b/tools/tlc/tests/test_cli.py index 4fad349eb6..a5ef30eed6 100644 --- a/tools/tlc/tests/test_cli.py +++ b/tools/tlc/tests/test_cli.py @@ -11,6 +11,7 @@ from math import ceil, log2 from pathlib import Path +from re import findall, search from unittest import mock import pytest @@ -383,6 +384,69 @@ def test_create_from_yaml_check_exact_data( assert actual == expected +@pytest.mark.parametrize("option", ["-O", "--output"]) +def test_gen_tl_header_with_output_name(tlcrunner, tmptlstr, option, filename="test.h"): + with tlcrunner.isolated_filesystem(): + result = tlcrunner.invoke( + cli, + [ + "gen-header", + option, + filename, + tmptlstr, + ], + ) + + assert result.exit_code == 0 + assert Path(filename).exists() + + +def test_gen_tl_with_fdt_header(tmptlstr, tmpfdt): + tlcrunner = CliRunner() + + with tlcrunner.isolated_filesystem(): + tlcrunner.invoke(cli, ["create", "--size", 1000, "--fdt", tmpfdt, tmptlstr]) + + result = tlcrunner.invoke( + cli, + [ + "gen-header", + tmptlstr, + ], + ) + + assert result.exit_code == 0 + assert Path("header.h").exists() + + with open("header.h", "r") as f: + dtb_match = search(r"DTB_OFFSET\s+(\d+)", "".join(f.readlines())) + assert dtb_match and dtb_match[1].isnumeric() + + +def test_gen_empty_tl_c_header(tlcrunner, tmptlstr): + with tlcrunner.isolated_filesystem(): + result = tlcrunner.invoke( + cli, + [ + "gen-header", + tmptlstr, + ], + ) + + assert result.exit_code == 0 + assert Path("header.h").exists() + + with open("header.h", "r") as f: + lines = "".join(f.readlines()) + + assert TransferList.hdr_size == int( + findall(r"SIZE\s+(0x[0-9a-fA-F]+|\d+)", lines)[0], 16 + ) + assert TransferList.version == int( + findall(r"VERSION.+(0x[0-9a-fA-F]+|\d+)", lines)[0] + ) + + def bytes_to_hex(data: bytes) -> str: """Convert bytes to a hex string in the same format as the debugger in ArmDS diff --git a/tools/tlc/tlc/cli.py b/tools/tlc/tlc/cli.py index 1d4949d679..3d609380d8 100644 --- a/tools/tlc/tlc/cli.py +++ b/tools/tlc/tlc/cli.py @@ -12,6 +12,7 @@ from pathlib import Path import click +import jinja2 import yaml from tlc.tl import * @@ -164,6 +165,34 @@ def unpack(filename, c): f.write(te.data) +@cli.command() +@click.argument("filename", type=click.Path(exists=True, dir_okay=False)) +@click.option( + "--output", + "-O", + type=click.Path(exists=False), + help="Output filename for the header", + default=Path("header.h"), +) +def gen_header(filename, output): + """Generate a header with common definitions.""" + tl = TransferList.fromfile(filename) + tmp_keys = tl.__dict__ + tmp_keys["header_guard"] = Path(output).name.replace(".", "_").upper() + + dtb_te = tl.get_entry(1) + + if dtb_te: + tmp_keys["dtb_offset"] = dtb_te.offset + dtb_te.hdr_size + + env = jinja2.Environment( + loader=jinja2.PackageLoader("tlc", "templates"), + ) + template = env.get_template("header.h.j2") + with open(output, "w") as f: + f.write(template.render(tmp_keys)) + + @cli.command() @click.argument("filename", type=click.Path(exists=True, dir_okay=False)) def validate(filename): diff --git a/tools/tlc/tlc/templates/header.h.j2 b/tools/tlc/tlc/templates/header.h.j2 new file mode 100644 index 0000000000..87707cec28 --- /dev/null +++ b/tools/tlc/tlc/templates/header.h.j2 @@ -0,0 +1,16 @@ +/* + * Auto-generated by TLC, this file includes declarations and macros + * derived from a Transfer List input. + */ + +#ifndef {{ header_guard }} +#define {{ header_guard }} + +{% if dtb_offset -%} +#define TRANSFER_LIST_DTB_OFFSET {{ "0x%x" % dtb_offset }} +{%- endif %} +#define TRANSFER_LIST_CONVENTION_VERSION {{ version }} +#define TRANSFER_LIST_HEADER_SIZE {{ "0x%x" % hdr_size }} +#define TRANSFER_LIST_SIZE {{ "0x%x" % size }} + +#endif /* {{ header_guard }} */ diff --git a/tools/tlc/tlc/tl.py b/tools/tlc/tlc/tl.py index b8bb399200..98d2205bf4 100644 --- a/tools/tlc/tlc/tl.py +++ b/tools/tlc/tlc/tl.py @@ -8,7 +8,7 @@ """Module containing definitions pertaining to the 'Transfer List' (TL) type.""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import math import struct @@ -93,7 +93,7 @@ class TransferList: self.size = self.hdr_size self.total_size = max_size self.flags = flags - self.entries: List["TransferEntry"] = [] + self.entries: List[TransferEntry] = [] self.update_checksum() def __str__(self) -> str: @@ -197,15 +197,23 @@ class TransferList: sum(self.header_to_bytes()) + sum(te.sum_of_bytes for te in self.entries) ) % 256 - def get_entry_data_offset(self, tag_id: int) -> int: - """Returns offset of data of a TE from the base of the TL.""" + def get_entry(self, tag_id: int) -> Optional[TransferEntry]: for te in self.entries: if te.id == tag_id: - return te.offset + te.hdr_size + return te + + return None + + def get_entry_data_offset(self, tag_id: int) -> int: + """Returns offset of data of a TE from the base of the TL.""" + te = self.get_entry(tag_id) + + if not te: + raise ValueError(f"Tag {tag_id} not found in TL!") - raise ValueError(f"Tag {tag_id} not found in TL!") + return te.offset + te.hdr_size - def add_transfer_entry(self, tag_id: int, data: bytes) -> "TransferEntry": + def add_transfer_entry(self, tag_id: int, data: bytes) -> TransferEntry: """Appends a TransferEntry into the internal list of TE's.""" if not (self.total_size >= self.size + TransferEntry.hdr_size + len(data)): raise MemoryError( @@ -220,14 +228,14 @@ class TransferList: def add_transfer_entry_from_struct_format( self, tag_id: int, struct_format: str, *args: Any - ) -> "TransferEntry": + ) -> TransferEntry: struct_format = "<" + struct_format data = struct.pack(struct_format, *args) return self.add_transfer_entry(tag_id, data) def add_entry_point_info_transfer_entry( self, entry: Dict[str, Any] - ) -> "TransferEntry": + ) -> TransferEntry: """Add entry_point_info transfer entry :param entry: Dictionary of the transfer entry, in the same format as @@ -285,7 +293,7 @@ class TransferList: def add_transfer_entry_from_dict( self, entry: Dict[str, Any], - ) -> "TransferEntry": + ) -> TransferEntry: """Add a transfer entry from data in a dictionary The dictionary should have the same format as the entries in the yaml @@ -320,7 +328,7 @@ class TransferList: else: raise ValueError(f"Invalid transfer entry {entry}.") - def add_transfer_entry_from_file(self, tag_id: int, path: Path) -> "TransferEntry": + def add_transfer_entry_from_file(self, tag_id: int, path: Path) -> TransferEntry: with open(path, "rb") as f: return self.add_transfer_entry(tag_id, f.read()) -- GitLab