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
+19 -2
View File
@@ -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"))
@@ -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,
)
@@ -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)
@@ -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)
@@ -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}
@@ -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)