feat: support public/interface dependencies in packer

This commit is contained in:
2026-08-06 12:48:41 +08:00
parent 7527d3763f
commit ccf19ac0ab
7 changed files with 148 additions and 7 deletions
+22
View File
@@ -39,6 +39,20 @@ namespace_name = "foobar"
# Target-name part of the generated CMake target. # Target-name part of the generated CMake target.
# This property is optional; it defaults to the name of the target Rust project. # This property is optional; it defaults to the name of the target Rust project.
target_name = "foobar" 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] [package.metadata.omrf.pkgconfig]
# The unique identifier of the package. # The unique identifier of the package.
@@ -50,6 +64,14 @@ name = "Foo Bar"
# Brief description of the package. # Brief description of the package.
# This property is optional; it defaults to the description of the target Rust project. # This property is optional; it defaults to the description of the target Rust project.
description = "a brown fox jumps over a lazy dog." 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"]
``` ```
+19 -2
View File
@@ -13,7 +13,7 @@ from re import Pattern, compile
from .cli import Cli from .cli import Cli
from .utils import Triple from .utils import Triple
from .metadata import MetadataExtractor, Metadata from .metadata import MetadataExtractor, Metadata
from .renders.cmake import CMakeProperties, CMakeRender from .renders.cmake import CMakeDependency, CMakeProperties, CMakeRender
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender 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: if metadata.cmake.target_name is not None:
cmake_target_name = metadata.cmake.target_name 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 # build cmake properties and render
properties = CMakeProperties( properties = CMakeProperties(
cmake_namespace_name, cmake_namespace_name,
@@ -248,6 +256,7 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
_generate_dll_artifact_name(target_name, target_triple), _generate_dll_artifact_name(target_name, target_triple),
_generate_lib_artifact_name(target_name, target_triple), _generate_lib_artifact_name(target_name, target_triple),
extractor.get_version(), extractor.get_version(),
cmake_dependencies,
) )
render = CMakeRender(properties) render = CMakeRender(properties)
# return infos # return infos
@@ -291,8 +300,16 @@ def resolve_pkgconfig_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
if pkgconfig_description is None: if pkgconfig_description is None:
pkgconfig_description = "" 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( 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) render = PkgConfigRender(properties)
yield TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{pkgconfig_id}.pc")) yield TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{pkgconfig_id}.pc"))
@@ -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) @dataclass(frozen=True)
class MetadataCMake: class MetadataCMake:
"""Optional ``[package.metadata.omrf.cmake]`` section.""" """Optional ``[package.metadata.omrf.cmake]`` section."""
@@ -56,6 +85,8 @@ class MetadataCMake:
"""CMake target namespace, or ``None`` to fall back to the project name.""" """CMake target namespace, or ``None`` to fall back to the project name."""
target_name: str | None target_name: str | None
"""CMake target name, or ``None`` to fall back to the project name.""" """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: def __post_init__(self) -> None:
"""Validate the namespace and target names when specified. """Validate the namespace and target names when specified.
@@ -72,9 +103,21 @@ class MetadataCMake:
@staticmethod @staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataCMake": def from_dict(d: dict[str, Any]) -> "MetadataCMake":
"""Build a :class:`MetadataCMake` from its raw TOML table.""" """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( return MetadataCMake(
namespace_name=dict_typed_get(d, "namespace_name", str), namespace_name=dict_typed_get(d, "namespace_name", str),
target_name=dict_typed_get(d, "target_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.""" """Human-readable package name, or ``None`` to fall back to the project name."""
description: str | None description: str | None
"""Brief package description, or ``None`` to fall back to the project description.""" """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: def __post_init__(self) -> None:
"""Validate the id, name and description when specified. """Validate the id, name and description when specified.
@@ -103,14 +148,30 @@ class MetadataPkgConfig:
self.description self.description
): ):
raise ValueError("bad pkg-config description (has EOL chars)") 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 @staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig":
"""Build a :class:`MetadataPkgConfig` from its raw TOML table.""" """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( return MetadataPkgConfig(
id=dict_typed_get(d, "id", str), id=dict_typed_get(d, "id", str),
name=dict_typed_get(d, "name", str), name=dict_typed_get(d, "name", str),
description=dict_typed_get(d, "description", str), description=dict_typed_get(d, "description", str),
requires=requires,
) )
@@ -14,6 +14,27 @@ from . import common
from .. import utils 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) @dataclass(frozen=True)
class CMakeProperties: class CMakeProperties:
"""Inputs required to render the CMake package-configuration files. """Inputs required to render the CMake package-configuration files.
@@ -44,13 +65,16 @@ class CMakeProperties:
version: Version version: Version
"""Version of the library. Must not carry a prerelease or build component.""" """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: def __post_init__(self) -> None:
"""Validate the fields after construction. """Validate the fields after construction.
:raises ValueError: if the namespace or target name contains characters :raises ValueError: if the namespace or target name contains characters
outside the CMake name pattern, an artifact file name is empty, or outside the CMake name pattern, an artifact file name is empty, or the
the version carries a prerelease or build component (unsupported by version carries a prerelease or build component (unsupported by
CMake config-version files). CMake config-version files).
""" """
if not utils.is_good_name(self.namespace_name): if not utils.is_good_name(self.namespace_name):
@@ -99,6 +123,7 @@ class CMakeRender:
"target": self.__properties.target_name, "target": self.__properties.target_name,
"artifact_dll": self.__properties.artifact_dll, "artifact_dll": self.__properties.artifact_dll,
"artifact_lib": self.__properties.artifact_lib, "artifact_lib": self.__properties.artifact_lib,
"dependencies": self.__properties.dependencies,
} }
return self.__render.render("XXXConfig.cmake.jinja", payload) return self.__render.render("XXXConfig.cmake.jinja", payload)
@@ -42,13 +42,14 @@ class PkgConfigProperties:
version: Version version: Version
"""Version of the library. Must not carry a prerelease or build component.""" """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: def __post_init__(self) -> None:
"""Validate the fields after construction. """Validate the fields after construction.
:raises ValueError: if the name or description is blank, or the :raises ValueError: if the name or description is blank, or the version
version carries a prerelease or build component (unsupported in carries a prerelease or build component (unsupported in pkg-config).
pkg-config).
""" """
if self.name == "" or not utils.is_good_sentence(self.name): if self.name == "" or not utils.is_good_sentence(self.name):
raise ValueError("bad name (blank or has EOL chars) of pkg-config") raise ValueError("bad name (blank or has EOL chars) of pkg-config")
@@ -89,5 +90,6 @@ class PkgConfigRender:
"name": self.__properties.name, "name": self.__properties.name,
"description": self.__properties.description, "description": self.__properties.description,
"artifact": self.__properties.artifact, "artifact": self.__properties.artifact,
"requires": self.__properties.requires,
} }
return self.__render.render("XXX.pc.jinja", payload) return self.__render.render("XXX.pc.jinja", payload)
@@ -10,5 +10,8 @@ includedir=${prefix}/include
Name: {{ name }} Name: {{ name }}
Description: {{ description }} Description: {{ description }}
Version: {{ version }} Version: {{ version }}
{% if len(requires) != 0 -%}
Requires: {{ requires | join(", ") }}
{%- endif %}
Libs: -L${libdir} -l{{ artifact }} Libs: -L${libdir} -l{{ artifact }}
Cflags: -I${includedir} Cflags: -I${includedir}
@@ -32,5 +32,16 @@ else()
) )
endif() 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 -#} {# Cleanup temporary variables -#}
set(_IMPORT_PREFIX) set(_IMPORT_PREFIX)