feat: finish cmake generation
This commit is contained in:
@@ -9,6 +9,7 @@ authors = [
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jinja2==3.1.6",
|
"jinja2==3.1.6",
|
||||||
|
"semver>=3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -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 dataclasses import dataclass
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
|
from re import Pattern, compile
|
||||||
from . import common
|
from . import common
|
||||||
|
from semver import Version
|
||||||
|
|
||||||
|
|
||||||
|
_CMAKE_NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_.+-]+")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CMakeProperties:
|
class CMakeProperties:
|
||||||
name: str
|
"""Inputs required to render the CMake package-configuration files.
|
||||||
"""The identifier of project"""
|
|
||||||
|
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:
|
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
|
__render: common.BaseRender
|
||||||
__properties: CMakeProperties
|
__properties: CMakeProperties
|
||||||
|
|
||||||
def __init__(self, properties: CMakeProperties) -> None:
|
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.__render = common.BaseRender()
|
||||||
self.__properties = properties
|
self.__properties = properties
|
||||||
|
|
||||||
def render_config(self) -> None:
|
def render_config(self) -> str:
|
||||||
pass
|
"""Render the ``<pkg>Config.cmake`` file.
|
||||||
|
|
||||||
def render_config_version(self) -> None:
|
:returns: The rendered ``Config.cmake`` content as a string.
|
||||||
pass
|
"""
|
||||||
|
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
|
from typing import Any
|
||||||
import jinja2
|
import jinja2
|
||||||
from .. import utils
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
class BaseRender:
|
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
|
__loader: jinja2.BaseLoader
|
||||||
__environment: jinja2.Environment
|
__environment: jinja2.Environment
|
||||||
|
|
||||||
def __init__(self) -> None:
|
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.__loader = jinja2.FileSystemLoader(utils.get_templates_dir())
|
||||||
self.__environment = jinja2.Environment(loader=self.__loader)
|
self.__environment = jinja2.Environment(loader=self.__loader)
|
||||||
|
|
||||||
def render(
|
def render(self, template_filename: str, payload: dict[str, Any]) -> str:
|
||||||
self, template_filename: str, dest_filepath: Path, payload: dict[str, Any]
|
"""Render a named template with the given payload.
|
||||||
) -> None:
|
|
||||||
|
: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
|
# fetch template
|
||||||
template = self.__environment.get_template(template_filename)
|
template = self.__environment.get_template(template_filename)
|
||||||
# render template and save
|
# render template and return
|
||||||
with open(dest_filepath, "w", encoding="utf-8") as f:
|
return template.render(**payload)
|
||||||
f.write(template.render(**payload))
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
from . import common
|
from . import common
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -16,5 +16,6 @@ class PkgConfigRender:
|
|||||||
self.__render = common.BaseRender()
|
self.__render = common.BaseRender()
|
||||||
self.__properties = properties
|
self.__properties = properties
|
||||||
|
|
||||||
def render(self) -> None:
|
def render(self) -> str:
|
||||||
pass
|
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) -#}
|
{# Check whether this target is already exported -#}
|
||||||
get_filename_component(PACKAGE_CMAKE_CONFIG_PATH "${CMAKE_CURRENT_LIST_FILE}" PATH)
|
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 -#}
|
{# Setup library and header file paths -#}
|
||||||
set(MyRust_LIBRARY "${PACKAGE_PREFIX_DIR}/lib/ {{- artifact_filename -}} }")
|
set(MyRust_LIBRARY "${_IMPORT_PREFIX}/lib/{{ artifact_dll }}")
|
||||||
set(MyRust_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")
|
set(MyRust_INCLUDE_DIR "${_IMPORT_PREFIX}/include")
|
||||||
|
|
||||||
{# Create modern CMake target (IMPORTED target) -#}
|
{# Create modern CMake target (IMPORTED target) -#}
|
||||||
add_library(MyRust::MyRust SHARED IMPORTED)
|
add_library({{ namespace }}::{{ target }} SHARED IMPORTED)
|
||||||
set_target_properties(MyRust::MyRust PROPERTIES
|
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
|
||||||
IMPORTED_LOCATION "${MyRust_LIBRARY}"
|
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${MyRust_INCLUDE_DIR}"
|
|
||||||
)
|
)
|
||||||
|
{# Handle Windows and UNIX-like respectively -#}
|
||||||
{# Handle the Windows-specific case that .dll is seperated with .lib -#}
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set_target_properties(MyRust::MyRust PROPERTIES
|
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
|
||||||
IMPORTED_IMPLIB "${PACKAGE_PREFIX_DIR}/lib/myrust.lib"
|
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()
|
endif()
|
||||||
|
|
||||||
|
{# Cleanup temporary variables -#}
|
||||||
|
set(_IMPORT_PREFIX)
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
{# Setup package version -#}
|
# This is a basic version file for the Config-mode of find_package().
|
||||||
set(PACKAGE_VERSION "1.2.3") # TODO: replace with jinja argument
|
# 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 -#}
|
{# Setup package version -#}
|
||||||
if(PACKAGE_FIND_VERSION_EXACT)
|
set(PACKAGE_VERSION "{{- version -}}")
|
||||||
if(NOT "${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
|
||||||
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
|
{# Check package version (code are copied from CMake generation) -#}
|
||||||
endif()
|
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()
|
else()
|
||||||
{# Check whether user requested version is <= current version (compatible with new version) -#}
|
if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
|
||||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||||
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
|
else()
|
||||||
|
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||||
|
if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)
|
||||||
|
set(PACKAGE_VERSION_EXACT TRUE)
|
||||||
endif()
|
endif()
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
Generated
+14
-1
@@ -72,7 +72,20 @@ version = "0.1.0"
|
|||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
|
{ name = "semver" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "jinja2", specifier = "==3.1.6" }]
|
requires-dist = [
|
||||||
|
{ name = "jinja2", specifier = "==3.1.6" },
|
||||||
|
{ name = "semver", specifier = ">=3.0.4" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user