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>
diff --git a/poetry.lock b/poetry.lock
index cd2a41a..9a90704 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -44,13 +44,13 @@
[[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 @@
[[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 @@
[[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 @@
[[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 @@
[[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 @@
[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 @@
[[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 @@
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 @@
[[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 60402f1..decec59 100644
--- a/tools/tlc/poetry.lock
+++ b/tools/tlc/poetry.lock
@@ -1431,4 +1431,4 @@
[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 ea842e5..b606238 100644
--- a/tools/tlc/pyproject.toml
+++ b/tools/tlc/pyproject.toml
@@ -38,6 +38,7 @@
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 4fad349..a5ef30e 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 @@
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 1d4949d..3d60938 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 *
@@ -166,6 +167,34 @@
@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):
"""Validate the contents of an existing Transfer List."""
TransferList.fromfile(filename)
diff --git a/tools/tlc/tlc/templates/header.h.j2 b/tools/tlc/tlc/templates/header.h.j2
new file mode 100644
index 0000000..87707ce
--- /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 b8bb399..98d2205 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 @@
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 @@
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 @@
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 @@
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 @@
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())