feat: finish cmake generation
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user