From 6f1c5312e0f24af29981a56ed4591edf7f79e6cd Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 3 Aug 2026 16:05:08 +0800 Subject: [PATCH] feat: add metadata resolve in module --- packer/README.md | 58 +++++---- packer/src/sarasacw_omrf_packer/__init__.py | 4 +- packer/src/sarasacw_omrf_packer/metadata.py | 132 ++++++++++++++++++-- 3 files changed, 161 insertions(+), 33 deletions(-) diff --git a/packer/README.md b/packer/README.md index 1f22a3d..a9b590e 100644 --- a/packer/README.md +++ b/packer/README.md @@ -1,45 +1,55 @@ # sarasacw-omrf-packer +## Metadata + All packer used properties are stored in target Rust manifest file as metadata style. There is an example about how to define them in `Cargo.toml`. ```toml [package.metadata.omrf] -# Configures the minimum required sarasacw-omrf-packer version. -# Trying to run with an older version causes an error. -# This property is optional. If there is no specification, no version constraint is applied. +# Minimum required version of sarasacw-omrf-packer. +# Running with an older version fails with an error. +# This property is optional; when omitted, no version constraint is enforced. min_version = "1.0.0" -[package.metadata.omrf.headers] -assets = [ - # `from` is the relative path to directory where this Cargo.toml is. - # `to` is the relative path to `include` directory which is the subdirectory of installation directory. - # Blank `to` means that put it directly in `include` directory. +# Header files to distribute. Each entry installs one file (or, with glob +# expansion, a set of files) into the `include` directory of the installation. +# Each entry carries a `from` source path and a `to` destination path. +# This property is required. If you really want to distribute nothing header files, +# leave empty list here. +headers = [ + # `from` is a path relative to the directory that contains this `Cargo.toml`. + # `to` is a path relative to the `include` directory (a subdirectory of the install directory). + # An empty `to` places the file directly under the `include` directory. { from = "cbindgen/my_crate.h", to = "" }, { from = "cbindgen/our_crate.h", to = "SomePrefix" }, - # `from` support UNIX style pathname pattern expansion. - # It will find the toppest immutable component of given pattern. - # In this exmaple, this toppest immutable component is `pattern/with`. - # And use that as the root directory and put found files to `to` directory - # with preserving directory hierarchy. - { from = "pattern/with/**/*", to = "" } - { from = "pattern/with/**/*", to = "AllInOne" } + # `from` also supports UNIX-style glob (pathname) expansion. + # The matcher uses the leading literal portion of the pattern (`pattern/with` + # here) as the root directory and copies every matched file into the `to` + # directory, preserving the directory hierarchy beneath that root. + { from = "pattern/with/**/*", to = "" }, + # The same pattern, but nested under an `AllInOne` prefix. + { from = "pattern/with/**/*", to = "AllInOne" }, ] [package.metadata.omrf.cmake] -# Configures the namespace part of CMake target. -# This property is optional. The default value is extracted from the name of target Rust project. +# Namespace part of the generated CMake target. +# This property is optional; it defaults to the name of the target Rust project. namespace_name = "foobar" -# Configures the target part of CMake target. -# This property is optional. The default value is extracted from the name of target Rust project. +# Target-name part of the generated CMake target. +# This property is optional; it defaults to the name of the target Rust project. target_name = "foobar" [package.metadata.omrf.pkgconfig] -# Configures the human-readable name of this package. -# This property is optional. The default value is extracted from the name of target Rust project. +# Human-readable name of the package. +# This property is optional; it defaults to the name of the target Rust project. name = "Foo Bar" -# Configures the brief description of this package. -# This property is optional. The default value is extracted from the description of target Rust project. -name = "a brown fox jumps over a lazy dog." +# Brief description of the package. +# This property is optional; it defaults to the description of the target Rust project. +description = "a brown fox jumps over a lazy dog." ``` + +## Environment Variables + +- `OMRF_PACKER_CARGO`: Path to the `cargo` executable used to collect project metadata. When unset, the packer invokes `cargo` as resolved from `PATH`. diff --git a/packer/src/sarasacw_omrf_packer/__init__.py b/packer/src/sarasacw_omrf_packer/__init__.py index c40fb01..f1ce012 100644 --- a/packer/src/sarasacw_omrf_packer/__init__.py +++ b/packer/src/sarasacw_omrf_packer/__init__.py @@ -1,7 +1,7 @@ import logging from . import cli from .archiver import Archiver -from .metadata import Metadata +from .metadata import MetadataExtractor from .renders.cmake import CMakeProperties, CMakeRender @@ -12,7 +12,7 @@ def main() -> None: logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) # build metadata - metadata = Metadata(opts.manifest) + metadata = MetadataExtractor(opts.manifest) # create distribution diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index 86cb10e..fdd1f32 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -1,8 +1,11 @@ import json +import os import subprocess -from typing import Any +from dataclasses import dataclass from pathlib import Path +from typing import Any import tomli +from semver import Version # YYC MARK: @@ -13,20 +16,134 @@ import tomli # 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" + +@dataclass(frozen=True) +class MetadataHeader: + from_path: str + to_path: str + + def __post_init__(self) -> None: + if self.from_path == "": + raise ValueError("header asset 'from' must not be empty") + + @staticmethod + def from_dict(d: dict[str, Any]) -> "MetadataHeader": + from_path = d.get("from") + if from_path is None: + raise ValueError("header asset is missing required key 'from'") + if not isinstance(from_path, str): + raise TypeError("header asset 'from' must be a string") + to_path = d.get("to", "") + if not isinstance(to_path, str): + raise TypeError("header asset 'to' must be a string") + return MetadataHeader(from_path=from_path, to_path=to_path) +@dataclass(frozen=True) +class MetadataCMake: + namespace_name: str | None + target_name: str | None + + @staticmethod + def from_dict(d: dict[str, Any]) -> "MetadataCMake": + namespace_name = d.get("namespace_name") + if namespace_name is not None and not isinstance(namespace_name, str): + raise TypeError("cmake 'namespace_name' must be a string or None") + target_name = d.get("target_name") + if target_name is not None and not isinstance(target_name, str): + raise TypeError("cmake 'target_name' must be a string or None") + return MetadataCMake(namespace_name=namespace_name, target_name=target_name) + + +@dataclass(frozen=True) +class MetadataPkgConfig: + name: str | None + description: str | None + + @staticmethod + def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": + name = d.get("name") + if name is not None and not isinstance(name, str): + raise TypeError("pkgconfig 'name' must be a string or None") + description = d.get("description") + if description is not None and not isinstance(description, str): + raise TypeError("pkgconfig 'description' must be a string or None") + return MetadataPkgConfig(name=name, description=description) + + +@dataclass(frozen=True) class Metadata: + min_version: Version | None + headers: tuple[MetadataHeader, ...] + cmake: MetadataCMake | None + pkgconfig: MetadataPkgConfig | None + + @staticmethod + def from_dict(d: dict[str, Any]) -> "Metadata": + raw_min_version = d.get("min_version") + if raw_min_version is not None: + if not isinstance(raw_min_version, str): + raise TypeError("min_version must be a string") + try: + min_version = Version.parse(raw_min_version) + except ValueError as e: + raise ValueError(f"invalid min_version {raw_min_version!r}: {e}") from e + else: + min_version = None + + raw_headers = d.get("headers") + if raw_headers is None: + raise ValueError( + "headers is required (use an empty list to distribute no headers)" + ) + if not isinstance(raw_headers, list): + raise TypeError("headers must be a list") + headers_list: list[MetadataHeader] = [] + for i, item in enumerate(raw_headers): + if not isinstance(item, dict): + raise TypeError(f"headers[{i}] must be a table") + headers_list.append(MetadataHeader.from_dict(item)) + headers = tuple(headers_list) + + raw_cmake = d.get("cmake") + if raw_cmake is not None: + if not isinstance(raw_cmake, dict): + raise TypeError("cmake must be a table") + cmake = MetadataCMake.from_dict(raw_cmake) + else: + cmake = None + + raw_pkgconfig = d.get("pkgconfig") + if raw_pkgconfig is not None: + if not isinstance(raw_pkgconfig, dict): + raise TypeError("pkgconfig must be a table") + pkgconfig = MetadataPkgConfig.from_dict(raw_pkgconfig) + else: + pkgconfig = None + + return Metadata( + min_version=min_version, + headers=headers, + cmake=cmake, + pkgconfig=pkgconfig, + ) + + +class MetadataExtractor: __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) + self.__cargo_toml = MetadataExtractor.__extract_cargo_toml(cargo_toml) + self.__cargo_metadata = MetadataExtractor.__extract_cargo_metadata(cargo_toml) + + def extract_omrf_metadata(self) -> Metadata: + omrf = self.__cargo_toml.get("package", {}).get("metadata", {}).get("omrf") + if not isinstance(omrf, dict): + raise ValueError("[package.metadata.omrf] is required") + return Metadata.from_dict(omrf) @staticmethod def __extract_cargo_toml(cargo_toml: Path) -> dict[str, Any]: @@ -35,8 +152,9 @@ class Metadata: @staticmethod def __extract_cargo_metadata(cargo_toml: Path) -> dict[str, Any]: + cargo_bin = os.getenv("OMRF_PACKER_CARGO", "cargo") cmd = [ - "cargo", + cargo_bin, "metadata", "--no-deps", "--format-version",