feat: basically finish pkgconfig generation

This commit is contained in:
2026-08-03 14:22:56 +08:00
parent 02526784a2
commit 8c0775d784
11 changed files with 246 additions and 36 deletions
+17 -10
View File
@@ -1,13 +1,20 @@
from .archiver import pack
from .cargo import get_artifacts
from .cli import Cli, parse
from .cmake import render as render_cmake
from .pkgconfig import render as render_pkgconfig
import logging
from . import cli
from .archiver import Archiver
from .metadata import Metadata
from .renders.cmake import CMakeProperties, CMakeRender
def main() -> None:
cli = parse()
get_artifacts(cli.manifest_path)
render_cmake(cli.output_dir)
render_pkgconfig(cli.output_dir)
pack(cli.output_dir, cli.output_dir + ".zip")
# parse command line arguments
opts = cli.parse()
# setup logging
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
# build metadata
metadata = Metadata(opts.manifest)
# create distribution
with Archiver(opts.dist_dir, opts.dist_zip) as archiver:
pass
-22
View File
@@ -1,22 +0,0 @@
import json
import subprocess
def get_metadata(manifest_path: str) -> dict:
cmd = [
"cargo",
"metadata",
"--format-version=1",
f"--manifest-path={manifest_path}",
]
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, _ = proc.communicate()
return json.loads(stdout)
def get_artifacts(manifest_path: str) -> list[str]:
metadata = get_metadata(manifest_path)
target_directory = metadata["target_directory"]
return [target_directory]
@@ -0,0 +1,60 @@
import json
import subprocess
from typing import Any
from pathlib import Path
import tomli
# YYC MARK:
# We use `tomli` with at least 2.4.0 version by design.
# Considering that Cargo has approve the change allowing TOML 1.1 syntax in `Cargo.toml`,
# Python embedded `toml` package, which only support TOML 1.0 syntax until Python 3.15,
# is not suit for parsing `Cargo.toml` in future.
# So we use the upstream of official `toml` package, i.e. `tomli` as our solution.
# And according to the document if `tomli`, the version starting support TOML 1.1 syntax is 2.4.0.
_TOKEN_PACKAGES: str = "packages"
_TOKEN_PACKAGE_MANIFEST_PATH: str = "manifest_path"
_TOKEN_TARGET_DIRECTORY: str = "target_directory"
class Metadata:
__cargo_toml_path: Path
__cargo_metadata: dict[str, Any]
__cargo_toml: dict[str, Any]
def __init__(self, cargo_toml: Path) -> None:
self.__cargo_toml_path = cargo_toml
self.__cargo_toml = Metadata.__extract_cargo_toml(cargo_toml)
self.__cargo_metadata = Metadata.__extract_cargo_metadata(cargo_toml)
@staticmethod
def __extract_cargo_toml(cargo_toml: Path) -> dict[str, Any]:
with open(cargo_toml, "rb") as f:
return tomli.load(f)
@staticmethod
def __extract_cargo_metadata(cargo_toml: Path) -> dict[str, Any]:
cmd = [
"cargo",
"metadata",
"--no-deps",
"--format-version",
"1",
"--manifest-path",
str(cargo_toml),
]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
stdout, stderr = proc.communicate(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.communicate()
raise RuntimeError("fail to fetch cargo metadata: timed out")
if proc.returncode != 0:
raise RuntimeError(
"fail to fetch cargo metadata: "
+ stderr.decode("utf-8", errors="ignore")
)
else:
return json.loads(stdout)
@@ -65,9 +65,9 @@ class CMakeProperties:
"bad target name component in CMake. consider manually specifying it"
)
if self.artifact_dll == "":
raise ValueError("bad artifact dll file name")
raise ValueError("bad artifact dll file name in CMake")
if self.artifact_lib == "":
raise ValueError("bad artifact lib file name")
raise ValueError("bad artifact lib file name in CMake")
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"
@@ -1,21 +1,90 @@
"""Renderer for pkg-config package files.
Generates the ``<pkg>.pc`` file consumed by the ``pkg-config`` tool so that the
distributed Rust FFI library can be located and linked by downstream build
systems. The data fed to the template is carried by
:class:`PkgConfigProperties`, and :class:`PkgConfigRender` performs the
rendering on top of :class:`renders.common.BaseRender`.
"""
from dataclasses import dataclass
from typing import Any
from . import common
from semver import Version
@dataclass(frozen=True)
class PkgConfigProperties:
pass
"""Inputs required to render the pkg-config ``.pc`` file.
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 ``.pc`` template.
"""
name: str
"""Human-readable name of the library or package.
This is purely descriptive; the ``pkg-config`` tool itself resolves
packages by the file name of the ``.pc`` file, not by this field.
"""
description: str
"""Brief description of the package."""
artifact: str
"""Name of the build artifact.
``pkg-config`` uses this value to determine which dynamic library to link.
For an artifact named ``XXX``, it looks for ``libXXX.so`` on Linux and
``XXX.lib`` or ``libXXX.dll.a`` on Windows.
"""
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 name or description is blank, or the
version carries a prerelease or build component (unsupported in
pkg-config).
"""
if self.name == "":
raise ValueError("unexpected blank name of pkg-config")
if self.description == "":
raise ValueError("unexpected blank description of pkg-config")
if self.version.prerelease is not None or self.version.build is not None:
raise ValueError(
"unsupported semantic version components (prerelease or build) in pkg-config"
)
class PkgConfigRender:
"""Renderer that turns :class:`PkgConfigProperties` into a ``.pc`` file.
Wraps a :class:`renders.common.BaseRender` and exposes :meth:`render` to
emit the package configuration file.
"""
__render: common.BaseRender
__properties: PkgConfigProperties
def __init__(self, properties: PkgConfigProperties) -> None:
"""Create the renderer with its Jinja2 backend and bound properties.
:param properties: Validated inputs reused by :meth:`render`.
"""
self.__render = common.BaseRender()
self.__properties = properties
def render(self) -> str:
payload: dict[str, Any] = {}
"""Render the ``<pkg>.pc`` file.
:returns: The rendered ``.pc`` content as a string.
"""
payload: dict[str, Any] = {
"name": self.__properties.name,
"description": self.__properties.description,
"artifact": self.__properties.artifact,
}
return self.__render.render("XXX.pc.jinja", payload)
@@ -0,0 +1,14 @@
{#
Utilize ${pcfiledir} to fetch the directory where current .pc file is.
And back to parent twice to obtain the installation directory.
-#}
prefix=${pcfiledir}/../..
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: {{ name }}
Description: {{ description }}
Version: {{ version }}
Libs: -L${libdir} -l{{ artifact }}
Cflags: -I${includedir}
+3
View File
@@ -1,5 +1,8 @@
from pathlib import Path
from semver import Version
VERSION: Version = Version(1, 0, 0)
"""The current version of sarasacw-omrf-packer"""
def get_root_dir() -> Path:
return Path(__file__).resolve().parent