feat: finish cmake generation

This commit is contained in:
2026-08-03 11:10:44 +08:00
parent 3261a4cd4b
commit 02526784a2
8 changed files with 203 additions and 44 deletions
@@ -1,24 +1,114 @@
"""Renderer for CMake package-configuration files.
Generates the ``<pkg>Config.cmake`` and ``<pkg>ConfigVersion.cmake`` files that
allow a downstream CMake project to ``find_package`` the distributed Rust FFI
library. The data fed to the templates is carried by
:class:`CMakeProperties`, and :class:`CMakeRender` performs the rendering on
top of :class:`renders.common.BaseRender`.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from re import Pattern, compile
from . import common
from semver import Version
_CMAKE_NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_.+-]+")
@dataclass(frozen=True)
class CMakeProperties:
name: str
"""The identifier of project"""
"""Inputs required to render the CMake package-configuration files.
A frozen dataclass validated in :meth:`__post_init__`; once constructed, an
instance is guaranteed to hold only values that are safe to substitute into
the CMake templates.
"""
namespace_name: str
"""Namespace component used when generating the CMake scripts."""
target_name: str
"""Target-name component used when generating the CMake scripts."""
artifact_dll: str
"""File name of the dynamic-loadable artifact, including its platform-dependent suffix.
Typically ``example.dll`` on Windows, ``example.so`` on Linux, and
``example.dylib`` on macOS.
"""
artifact_lib: str
"""File name of the import-library artifact, including its platform-dependent suffix.
Typically the dynamic-loadable file name with its suffix replaced by
``.lib``. This field is unused on UNIX-like systems, but it must still be
provided or rendering will fail.
"""
version: Version
"""Version of the library. Must not carry a prerelease or build component."""
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
CMake config-version files).
"""
if _CMAKE_NAME_PATTERN.fullmatch(self.namespace_name) is None:
raise ValueError(
"bad namespace component in CMake. consider manually specifying it"
)
if _CMAKE_NAME_PATTERN.fullmatch(self.target_name) is None:
raise ValueError(
"bad target name component in CMake. consider manually specifying it"
)
if self.artifact_dll == "":
raise ValueError("bad artifact dll file name")
if self.artifact_lib == "":
raise ValueError("bad artifact lib file name")
if self.version.prerelease is not None or self.version.build is not None:
raise ValueError(
"unsupported semantic version components (prerelease or build) in CMake"
)
class CMakeRender:
"""Renderer that turns :class:`CMakeProperties` into CMake config files.
Wraps a :class:`renders.common.BaseRender` and exposes one method per
generated file (:meth:`render_config`, :meth:`render_config_version`).
"""
__render: common.BaseRender
__properties: CMakeProperties
def __init__(self, properties: CMakeProperties) -> None:
"""Create the renderer with its Jinja2 backend and bound properties.
:param properties: Validated inputs reused by every render method.
"""
self.__render = common.BaseRender()
self.__properties = properties
def render_config(self) -> None:
pass
def render_config(self) -> str:
"""Render the ``<pkg>Config.cmake`` file.
def render_config_version(self) -> None:
pass
:returns: The rendered ``Config.cmake`` content as a string.
"""
payload: dict[str, Any] = {
"namespace": self.__properties.namespace_name,
"target": self.__properties.target_name,
"artifact_dll": self.__properties.artifact_dll,
"artifact_lib": self.__properties.artifact_lib,
}
return self.__render.render("XXXConfig.cmake.jinja", payload)
def render_config_version(self) -> str:
"""Render the ``<pkg>ConfigVersion.cmake`` file.
:returns: The rendered ``ConfigVersion.cmake`` content as a string.
"""
payload: dict[str, Any] = {"version": str(self.__properties.version)}
return self.__render.render("XXXConfigVersion.cmake.jinja", payload)
@@ -1,22 +1,47 @@
from pathlib import Path
"""Shared infrastructure for the template-based renderers.
Concrete renderers such as :class:`renders.cmake.CMakeRender` only decide which
template to render and with which payload; the actual Jinja2 plumbing -- the
loader, the environment and the template lookup -- is centralized here in
:class:`BaseRender`.
"""
from typing import Any
import jinja2
from .. import utils
class BaseRender:
"""Jinja2-backed renderer shared by every concrete renderer.
A single Jinja2 :class:`~jinja2.Environment` is created at construction
time, backed by a :class:`~jinja2.FileSystemLoader` rooted at the
package's template directory. The environment is reused across all
:meth:`render` calls.
"""
__loader: jinja2.BaseLoader
__environment: jinja2.Environment
def __init__(self) -> None:
"""Create the Jinja2 loader and environment.
The environment is configured to load templates from the directory
returned by :func:`utils.get_templates_dir`.
"""
self.__loader = jinja2.FileSystemLoader(utils.get_templates_dir())
self.__environment = jinja2.Environment(loader=self.__loader)
def render(
self, template_filename: str, dest_filepath: Path, payload: dict[str, Any]
) -> None:
def render(self, template_filename: str, payload: dict[str, Any]) -> str:
"""Render a named template with the given payload.
:param template_filename: Name of the template file, relative to the
package template directory.
:param payload: Mapping of variable names to their values, forwarded
to the template as its context.
:returns: The rendered template as a string.
"""
# fetch template
template = self.__environment.get_template(template_filename)
# render template and save
with open(dest_filepath, "w", encoding="utf-8") as f:
f.write(template.render(**payload))
# render template and return
return template.render(**payload)
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from . import common
@dataclass(frozen=True)
@@ -16,5 +16,6 @@ class PkgConfigRender:
self.__render = common.BaseRender()
self.__properties = properties
def render(self) -> None:
pass
def render(self) -> str:
payload: dict[str, Any] = {}
return self.__render.render("XXX.pc.jinja", payload)
@@ -1,23 +1,36 @@
{# Get the path to directory where current XXXConfig.cmake file is (i.e. /path/to/installation/lib/cmake/XXX) -#}
get_filename_component(PACKAGE_CMAKE_CONFIG_PATH "${CMAKE_CURRENT_LIST_FILE}" PATH)
{# Check whether this target is already exported -#}
if(TARGET {{ namespace }}::{{ target }})
message(STATUS "Target {{ namespace }}::{{ target }} is already defined.")
message(STATUS "We will reuse it. Please check their version for compatibility.")
return()
endif()
{# Compute installation root directory (back to parent 3 times, i.e. /path/to/installation) -#}
get_filename_component(PACKAGE_PREFIX_DIR "${PACKAGE_CMAKE_CONFIG_PATH}/../../../" ABSOLUTE)
{#
Get the path to directory where current XXXConfig.cmake file is (i.e. /path/to/installation/lib/cmake/XXX)
And compute installation root directory (back to parent 3 times, i.e. /path/to/installation)
-#}
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}/../../../" ABSOLUTE)
{# Setup library and header file paths -#}
set(MyRust_LIBRARY "${PACKAGE_PREFIX_DIR}/lib/ {{- artifact_filename -}} }")
set(MyRust_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")
set(MyRust_LIBRARY "${_IMPORT_PREFIX}/lib/{{ artifact_dll }}")
set(MyRust_INCLUDE_DIR "${_IMPORT_PREFIX}/include")
{# Create modern CMake target (IMPORTED target) -#}
add_library(MyRust::MyRust SHARED IMPORTED)
set_target_properties(MyRust::MyRust PROPERTIES
IMPORTED_LOCATION "${MyRust_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MyRust_INCLUDE_DIR}"
add_library({{ namespace }}::{{ target }} SHARED IMPORTED)
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
{# Handle the Windows-specific case that .dll is seperated with .lib -#}
{# Handle Windows and UNIX-like respectively -#}
if(WIN32)
set_target_properties(MyRust::MyRust PROPERTIES
IMPORTED_IMPLIB "${PACKAGE_PREFIX_DIR}/lib/myrust.lib"
)
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
IMPORTED_LOCATION "${_IMPORT_PREFIX}/bin/{{ artifact_dll }}"
IMPORTED_IMPLIB "${_IMPORT_PREFIX}/lib/{{ artifact_lib }}"
)
else()
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/{{ artifact_dll }}"
)
endif()
{# Cleanup temporary variables -#}
set(_IMPORT_PREFIX)
@@ -1,14 +1,30 @@
{# Setup package version -#}
set(PACKAGE_VERSION "1.2.3") # TODO: replace with jinja argument
# This is a basic version file for the Config-mode of find_package().
# It is created by sarasacw-omrf-packer and should not be changed manually.
#
# This file sets PACKAGE_VERSION_EXACT if the current version string and
# the requested version string are exactly the same and it sets
# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version.
{# Check whether user request EXACT match -#}
if(PACKAGE_FIND_VERSION_EXACT)
if(NOT "${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
endif()
{# Setup package version -#}
set(PACKAGE_VERSION "{{- version -}}")
{# Check package version (code are copied from CMake generation) -#}
if (PACKAGE_FIND_VERSION_RANGE)
# Package version must be in the requested version range
if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN)
OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX)
OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX)))
set(PACKAGE_VERSION_COMPATIBLE FALSE)
else()
set(PACKAGE_VERSION_COMPATIBLE TRUE)
endif()
else()
{# Check whether user requested version is <= current version (compatible with new version) -#}
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
set(PACKAGE_VERSION_COMPATIBLE FALSE)
else()
set(PACKAGE_VERSION_COMPATIBLE TRUE)
if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)
set(PACKAGE_VERSION_EXACT TRUE)
endif()
endif()
endif()
endif()