From ccf19ac0ab0fe93dee9f801ec5aa3bf31d945f89 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 6 Aug 2026 12:48:41 +0800 Subject: [PATCH] feat: support public/interface dependencies in packer --- packer/README.md | 22 +++++++ packer/src/sarasacw_omrf_packer/artifact.py | 21 ++++++- packer/src/sarasacw_omrf_packer/metadata.py | 61 +++++++++++++++++++ .../src/sarasacw_omrf_packer/renders/cmake.py | 29 ++++++++- .../sarasacw_omrf_packer/renders/pkgconfig.py | 8 ++- .../templates/XXX.pc.jinja | 3 + .../templates/XXXConfig.cmake.jinja | 11 ++++ 7 files changed, 148 insertions(+), 7 deletions(-) diff --git a/packer/README.md b/packer/README.md index 74dd48c..ed9c1f0 100644 --- a/packer/README.md +++ b/packer/README.md @@ -39,6 +39,20 @@ namespace_name = "foobar" # Target-name part of the generated CMake target. # This property is optional; it defaults to the name of the target Rust project. target_name = "foobar" +# Declared CMake dependencies. +# Each entry carries a `package` (passed to find_dependency argument) and a `target` +# (linked into the generated imported target via INTERFACE_LINK_LIBRARIES). +# This property is optional; it defaults to no dependencies. +# Because the packer only ever ships dynamic libraries, there are no private or +# static dependencies, and no separate field for them. +# Declaring dependencies here is rarely necessary and is NOT recommended: it +# behaves like a CMake interface/public dependency, and exposing other +# libraries' raw types across the FFI boundary raises ABI-compatibility +# concerns across runtimes. Use it only when this crate is explicitly a wrapper +# around another library. +dependencies = [ + { package = "ZLIB 1.3.2 REQUIRED", target = "ZLIB::ZLIB" }, +] [package.metadata.omrf.pkgconfig] # The unique identifier of the package. @@ -50,6 +64,14 @@ name = "Foo Bar" # Brief description of the package. # This property is optional; it defaults to the description of the target Rust project. description = "a brown fox jumps over a lazy dog." +# Declared public pkg-config dependencies. +# This property is optional; it defaults to no dependencies. +# Because the packer only ever ships dynamic libraries, there are no private +# dependencies and Requires.private is not used. +# Declaring dependencies here is rarely necessary and is NOT recommended, for +# the same ABI-compatibility reasons as the CMake dependencies above. Use it +# only when this crate is explicitly a wrapper around another library. +requires = ["libfoo >= 1.0", "libbar"] ``` diff --git a/packer/src/sarasacw_omrf_packer/artifact.py b/packer/src/sarasacw_omrf_packer/artifact.py index 1c89a6a..42c34dd 100644 --- a/packer/src/sarasacw_omrf_packer/artifact.py +++ b/packer/src/sarasacw_omrf_packer/artifact.py @@ -13,7 +13,7 @@ from re import Pattern, compile from .cli import Cli from .utils import Triple from .metadata import MetadataExtractor, Metadata -from .renders.cmake import CMakeProperties, CMakeRender +from .renders.cmake import CMakeDependency, CMakeProperties, CMakeRender from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender @@ -241,6 +241,14 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]: if metadata.cmake.target_name is not None: cmake_target_name = metadata.cmake.target_name + # gather declared dependencies (with metadata -> render conversion) + cmake_dependencies: tuple[CMakeDependency, ...] = tuple() + if metadata.cmake is not None and metadata.cmake.dependencies is not None: + cmake_dependencies = tuple( + CMakeDependency(dep.package, dep.target) + for dep in metadata.cmake.dependencies + ) + # build cmake properties and render properties = CMakeProperties( cmake_namespace_name, @@ -248,6 +256,7 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]: _generate_dll_artifact_name(target_name, target_triple), _generate_lib_artifact_name(target_name, target_triple), extractor.get_version(), + cmake_dependencies, ) render = CMakeRender(properties) # return infos @@ -291,8 +300,16 @@ def resolve_pkgconfig_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]: if pkgconfig_description is None: pkgconfig_description = "" + pkgconfig_requires: tuple[str, ...] = tuple() + if metadata.pkgconfig is not None and metadata.pkgconfig.requires is not None: + pkgconfig_requires = metadata.pkgconfig.requires + properties = PkgConfigProperties( - pkgconfig_name, pkgconfig_description, target_name, extractor.get_version() + pkgconfig_name, + pkgconfig_description, + target_name, + extractor.get_version(), + pkgconfig_requires, ) render = PkgConfigRender(properties) yield TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{pkgconfig_id}.pc")) diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index bbea75f..7a5bed6 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -48,6 +48,35 @@ class MetadataHeader: ) +@dataclass(frozen=True) +class MetadataCMakeDependency: + """One CMake dependency declared in the OMRF metadata. + + Carries the ``find_dependency`` package name and the link target to wire + into the imported target's ``INTERFACE_LINK_LIBRARIES``. + """ + + package: str + """Package name passed to ``find_dependency`` (e.g. ``ZLIB``).""" + target: str + """Link target wired into ``INTERFACE_LINK_LIBRARIES`` (e.g. ``ZLIB::ZLIB``).""" + + def __post_init__(self) -> None: + """Validate that ``package`` and ``target`` are non-empty strings.""" + if self.package == "": + raise ValueError("bad cmake dependency package") + if self.target == "": + raise ValueError("bad cmake dependency target") + + @staticmethod + def from_dict(d: dict[str, Any]) -> "MetadataCMakeDependency": + """Build a :class:`CMakeDependency` from its raw TOML table.""" + return MetadataCMakeDependency( + package=dict_typed_get_required(d, "package", str), + target=dict_typed_get_required(d, "target", str), + ) + + @dataclass(frozen=True) class MetadataCMake: """Optional ``[package.metadata.omrf.cmake]`` section.""" @@ -56,6 +85,8 @@ class MetadataCMake: """CMake target namespace, or ``None`` to fall back to the project name.""" target_name: str | None """CMake target name, or ``None`` to fall back to the project name.""" + dependencies: tuple[MetadataCMakeDependency, ...] | None + """Declared CMake dependencies, or ``None`` to no dependencies specified.""" def __post_init__(self) -> None: """Validate the namespace and target names when specified. @@ -72,9 +103,21 @@ class MetadataCMake: @staticmethod def from_dict(d: dict[str, Any]) -> "MetadataCMake": """Build a :class:`MetadataCMake` from its raw TOML table.""" + raw_dependencies = dict_typed_get(d, "dependencies", list) + if raw_dependencies is not None: + dependencies_list: list[MetadataCMakeDependency] = [] + for i, item in enumerate(raw_dependencies): + if not isinstance(item, dict): + raise TypeError(f"dependencies[{i}] must be a table") + dependencies_list.append(MetadataCMakeDependency.from_dict(item)) + dependencies = tuple(dependencies_list) + else: + dependencies = None + return MetadataCMake( namespace_name=dict_typed_get(d, "namespace_name", str), target_name=dict_typed_get(d, "target_name", str), + dependencies=dependencies, ) @@ -88,6 +131,8 @@ class MetadataPkgConfig: """Human-readable package name, or ``None`` to fall back to the project name.""" description: str | None """Brief package description, or ``None`` to fall back to the project description.""" + requires: tuple[str, ...] | None + """Declared public pkg-config dependencies, or ``None`` to no dependencies specified.""" def __post_init__(self) -> None: """Validate the id, name and description when specified. @@ -103,14 +148,30 @@ class MetadataPkgConfig: self.description ): raise ValueError("bad pkg-config description (has EOL chars)") + if self.requires is not None: + for req in self.requires: + if req == "": + raise ValueError("bad pkg-config require spec") @staticmethod def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": """Build a :class:`MetadataPkgConfig` from its raw TOML table.""" + raw_requires = dict_typed_get(d, "requires", list) + if raw_requires is not None: + requires_list: list[str] = [] + for i, item in enumerate(raw_requires): + if not isinstance(item, str): + raise TypeError(f"requires[{i}] must be a string") + requires_list.append(item) + requires = tuple(requires_list) + else: + requires = None + return MetadataPkgConfig( id=dict_typed_get(d, "id", str), name=dict_typed_get(d, "name", str), description=dict_typed_get(d, "description", str), + requires=requires, ) diff --git a/packer/src/sarasacw_omrf_packer/renders/cmake.py b/packer/src/sarasacw_omrf_packer/renders/cmake.py index f38b0f4..1ce2c6e 100644 --- a/packer/src/sarasacw_omrf_packer/renders/cmake.py +++ b/packer/src/sarasacw_omrf_packer/renders/cmake.py @@ -14,6 +14,27 @@ from . import common from .. import utils +@dataclass(frozen=True) +class CMakeDependency: + """One CMake dependency consumed by the renderer. + + Carries the ``find_dependency`` package name and the link target wired into + the imported target's ``INTERFACE_LINK_LIBRARIES``. + """ + + package: str + """Package name passed to ``find_dependency`` (e.g. ``ZLIB``).""" + target: str + """Link target wired into ``INTERFACE_LINK_LIBRARIES`` (e.g. ``ZLIB::ZLIB``).""" + + def __post_init__(self) -> None: + """Validate that ``package`` and ``target`` are non-empty strings.""" + if self.package == "": + raise ValueError("bad cmake dependency package") + if self.target == "": + raise ValueError("bad cmake dependency target") + + @dataclass(frozen=True) class CMakeProperties: """Inputs required to render the CMake package-configuration files. @@ -44,13 +65,16 @@ class CMakeProperties: version: Version """Version of the library. Must not carry a prerelease or build component.""" + dependencies: tuple[CMakeDependency, ...] + """CMake dependencies to ``find_dependency`` and link into the target + (empty when none are declared).""" def __post_init__(self) -> None: """Validate the fields after construction. :raises ValueError: if the namespace or target name contains characters - outside the CMake name pattern, an artifact file name is empty, or - the version carries a prerelease or build component (unsupported by + outside the CMake name pattern, an artifact file name is empty, or the + version carries a prerelease or build component (unsupported by CMake config-version files). """ if not utils.is_good_name(self.namespace_name): @@ -99,6 +123,7 @@ class CMakeRender: "target": self.__properties.target_name, "artifact_dll": self.__properties.artifact_dll, "artifact_lib": self.__properties.artifact_lib, + "dependencies": self.__properties.dependencies, } return self.__render.render("XXXConfig.cmake.jinja", payload) diff --git a/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py b/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py index b82925c..35a96a7 100644 --- a/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py +++ b/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py @@ -42,13 +42,14 @@ class PkgConfigProperties: version: Version """Version of the library. Must not carry a prerelease or build component.""" + requires: tuple[str, ...] + """Public pkg-config dependencies (empty when none are declared).""" def __post_init__(self) -> None: """Validate the fields after construction. - :raises ValueError: if the name or description is blank, or the - version carries a prerelease or build component (unsupported in - pkg-config). + :raises ValueError: if the name or description is blank, or the version + carries a prerelease or build component (unsupported in pkg-config). """ if self.name == "" or not utils.is_good_sentence(self.name): raise ValueError("bad name (blank or has EOL chars) of pkg-config") @@ -89,5 +90,6 @@ class PkgConfigRender: "name": self.__properties.name, "description": self.__properties.description, "artifact": self.__properties.artifact, + "requires": self.__properties.requires, } return self.__render.render("XXX.pc.jinja", payload) diff --git a/packer/src/sarasacw_omrf_packer/templates/XXX.pc.jinja b/packer/src/sarasacw_omrf_packer/templates/XXX.pc.jinja index 22d0c16..d3d8696 100644 --- a/packer/src/sarasacw_omrf_packer/templates/XXX.pc.jinja +++ b/packer/src/sarasacw_omrf_packer/templates/XXX.pc.jinja @@ -10,5 +10,8 @@ includedir=${prefix}/include Name: {{ name }} Description: {{ description }} Version: {{ version }} +{% if len(requires) != 0 -%} +Requires: {{ requires | join(", ") }} +{%- endif %} Libs: -L${libdir} -l{{ artifact }} Cflags: -I${includedir} diff --git a/packer/src/sarasacw_omrf_packer/templates/XXXConfig.cmake.jinja b/packer/src/sarasacw_omrf_packer/templates/XXXConfig.cmake.jinja index f01da55..4184539 100644 --- a/packer/src/sarasacw_omrf_packer/templates/XXXConfig.cmake.jinja +++ b/packer/src/sarasacw_omrf_packer/templates/XXXConfig.cmake.jinja @@ -32,5 +32,16 @@ else() ) endif() +{% if len(dependencies) != 0 -%} +{# Pull in declared dependencies and wire them into the imported target -#} +include(CMakeFindDependencyMacro) +{% for dep in dependencies -%} +find_dependency({{ dep.package }}) +{% endfor -%} +set_target_properties({{ namespace }}::{{ target }} PROPERTIES + INTERFACE_LINK_LIBRARIES "{{ dependencies | map(attribute='target') | join(';') }}" +) +{%- endif %} + {# Cleanup temporary variables -#} set(_IMPORT_PREFIX)