feat: improve metadata
This commit is contained in:
@@ -1,8 +1,25 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from semver import Version
|
||||
from . import cli
|
||||
from .archiver import Archiver
|
||||
from .metadata import MetadataExtractor
|
||||
from .renders.cmake import CMakeProperties, CMakeRender
|
||||
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||
|
||||
|
||||
def build_cmake_render(extractor: MetadataExtractor) -> CMakeRender:
|
||||
properties = CMakeProperties(
|
||||
"wfassoc", "wfassoc", "wfassoc.dll", "wfassoc.lib", Version(1, 0, 0)
|
||||
)
|
||||
return CMakeRender(properties)
|
||||
|
||||
|
||||
def build_pkgconfig_render(extractor: MetadataExtractor) -> PkgConfigRender:
|
||||
properties = PkgConfigProperties(
|
||||
"wfassoc", "wfassoc C/C++ FFI", "wfassoc", Version(1, 0, 0)
|
||||
)
|
||||
return PkgConfigRender(properties)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -11,10 +28,22 @@ def main() -> None:
|
||||
# setup logging
|
||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||
|
||||
# build metadata
|
||||
metadata = MetadataExtractor(opts.manifest)
|
||||
# build metadata extractor
|
||||
extractor = MetadataExtractor(opts.manifest)
|
||||
# extract omrf metadata
|
||||
metadata = extractor.get_metadata()
|
||||
|
||||
|
||||
|
||||
# create renders and their properties from metadata
|
||||
cmake_render = extractor
|
||||
|
||||
|
||||
# create distribution
|
||||
with Archiver(opts.dist_dir, opts.dist_zip) as archiver:
|
||||
pass
|
||||
archiver.push_dir(Path("bin"))
|
||||
archiver.push_dir(Path("include"))
|
||||
archiver.push_dir(Path("lib"))
|
||||
archiver.push_text(cmake_render.render_config(), Path("lib", "cmake", "wfassoc", "wfassocConfig.cmake"))
|
||||
archiver.push_text(cmake_render.render_config_version(), Path("lib", "cmake", "wfassoc", "wfassocConfigVersion.cmake"))
|
||||
archiver.push_text(pkgconfig_render.render(), Path("lib", "pkgconfig", "wfassoc.pc"))
|
||||
|
||||
@@ -2,19 +2,12 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import tomli
|
||||
from typing import Any, Callable
|
||||
from semver import Version
|
||||
|
||||
|
||||
# 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.
|
||||
from . import utils
|
||||
from .utils import dict_chain_get, dict_typed_get, dict_typed_get_required
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -24,19 +17,14 @@ class MetadataHeader:
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.from_path == "":
|
||||
raise ValueError("header asset 'from' must not be empty")
|
||||
raise ValueError("header '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)
|
||||
return MetadataHeader(
|
||||
from_path=dict_typed_get_required(d, "from", str),
|
||||
to_path=dict_typed_get(d, "to", str, ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -46,13 +34,10 @@ class MetadataCMake:
|
||||
|
||||
@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)
|
||||
return MetadataCMake(
|
||||
namespace_name=dict_typed_get(d, "namespace_name", str),
|
||||
target_name=dict_typed_get(d, "target_name", str),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -62,13 +47,10 @@ class MetadataPkgConfig:
|
||||
|
||||
@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)
|
||||
return MetadataPkgConfig(
|
||||
name=dict_typed_get(d, "name", str),
|
||||
description=dict_typed_get(d, "description", str),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -80,24 +62,13 @@ class Metadata:
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> "Metadata":
|
||||
raw_min_version = d.get("min_version")
|
||||
raw_min_version = dict_typed_get(d, "min_version", str)
|
||||
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
|
||||
min_version = utils.parse_version(raw_min_version)
|
||||
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")
|
||||
raw_headers = dict_typed_get_required(d, "headers", list)
|
||||
headers_list: list[MetadataHeader] = []
|
||||
for i, item in enumerate(raw_headers):
|
||||
if not isinstance(item, dict):
|
||||
@@ -105,21 +76,15 @@ class Metadata:
|
||||
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_cmake = dict_typed_get(d, "cmake", dict)
|
||||
cmake = MetadataCMake.from_dict(raw_cmake) if raw_cmake is not None else 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
|
||||
raw_pkgconfig = dict_typed_get(d, "pkgconfig", dict)
|
||||
pkgconfig = (
|
||||
MetadataPkgConfig.from_dict(raw_pkgconfig)
|
||||
if raw_pkgconfig is not None
|
||||
else None
|
||||
)
|
||||
|
||||
return Metadata(
|
||||
min_version=min_version,
|
||||
@@ -129,29 +94,36 @@ class Metadata:
|
||||
)
|
||||
|
||||
|
||||
def wrap_metadata_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(func)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"error occurs when fetching metadata: {e}") from e
|
||||
return wrapper
|
||||
|
||||
|
||||
class MetadataExtractor:
|
||||
__cargo_toml_path: Path
|
||||
__cargo_metadata: dict[str, Any]
|
||||
__cargo_toml: dict[str, Any]
|
||||
__metadata: dict[str, Any]
|
||||
"""The direct output of ``cargo metadata``"""
|
||||
__metadata_package: dict[str, Any]
|
||||
"""The package item in cargo metadata's packages list pointing to user request package"""
|
||||
__metadata_target: dict[str, Any]
|
||||
"""The target item in package item's targets list pointing to the main target"""
|
||||
|
||||
def __init__(self, cargo_toml: Path) -> None:
|
||||
self.__cargo_toml_path = 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)
|
||||
def __init__(self, cargo_toml_path: Path) -> None:
|
||||
self.__metadata = MetadataExtractor.__extract_metadata(cargo_toml_path)
|
||||
self.__metadata_package = MetadataExtractor.__extract_metadata_package(
|
||||
self.__metadata, cargo_toml_path
|
||||
)
|
||||
self.__metadata_target = MetadataExtractor.__extract_metadata_target(
|
||||
self.__metadata_package, cargo_toml_path
|
||||
)
|
||||
|
||||
@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]:
|
||||
@wrap_metadata_errors
|
||||
def __extract_metadata(cargo_toml_path: Path) -> dict[str, Any]:
|
||||
cargo_bin = os.getenv("OMRF_PACKER_CARGO", "cargo")
|
||||
cmd = [
|
||||
cargo_bin,
|
||||
@@ -160,7 +132,7 @@ class MetadataExtractor:
|
||||
"--format-version",
|
||||
"1",
|
||||
"--manifest-path",
|
||||
str(cargo_toml),
|
||||
str(cargo_toml_path),
|
||||
]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
@@ -176,3 +148,66 @@ class MetadataExtractor:
|
||||
)
|
||||
else:
|
||||
return json.loads(stdout)
|
||||
|
||||
@staticmethod
|
||||
@wrap_metadata_errors
|
||||
def __extract_metadata_package(
|
||||
cargo_metadata: dict[str, Any], cargo_toml_path: Path
|
||||
) -> dict[str, Any]:
|
||||
packages: list[Any] = dict_typed_get_required(cargo_metadata, "packages", list)
|
||||
for i, package in enumerate(packages):
|
||||
if not isinstance(package, dict):
|
||||
raise TypeError(f"packages[{i}] must be a table")
|
||||
|
||||
raw_manifest_path = dict_typed_get_required(package, "manifest_path", str)
|
||||
manifest_path = Path(raw_manifest_path)
|
||||
if manifest_path == cargo_toml_path:
|
||||
return package
|
||||
raise RuntimeError("can not find user given package in metadata")
|
||||
|
||||
@staticmethod
|
||||
@wrap_metadata_errors
|
||||
def __extract_metadata_target(
|
||||
cargo_package: dict[str, Any], cargo_toml_path: Path
|
||||
) -> dict[str, Any]:
|
||||
# build the path to lib.rs for comparing
|
||||
librs = cargo_toml_path.parent / "src" / "lib.rs"
|
||||
# start checking
|
||||
targets: list[Any] = dict_typed_get_required(cargo_package, "targets", list)
|
||||
for i, target in enumerate(targets):
|
||||
if not isinstance(target, dict):
|
||||
raise TypeError(f"targets[{i}] must be a table")
|
||||
|
||||
raw_src_path = dict_typed_get_required(target, "src_path", str)
|
||||
src_path = Path(raw_src_path)
|
||||
if src_path == librs:
|
||||
return target
|
||||
raise RuntimeError(
|
||||
"can not find the main target of user given package in metadata"
|
||||
)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_target_directory(self) -> str:
|
||||
return dict_typed_get_required(self.__metadata, "target_directory", str)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_name(self) -> str:
|
||||
return dict_typed_get_required(self.__metadata_package, "name", str)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_description(self) -> str | None:
|
||||
return dict_typed_get(self.__metadata_package, "description", str)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_version(self) -> Version:
|
||||
raw_version = dict_typed_get_required(self.__metadata_package, "version", str)
|
||||
return utils.parse_version(raw_version)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_metadata(self) -> Metadata:
|
||||
omrf = dict_chain_get(self.__metadata_package, "metadata", "omrf")
|
||||
return Metadata.from_dict(omrf)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_target_name(self) -> str:
|
||||
return dict_typed_get_required(self.__metadata_target, "name", str)
|
||||
|
||||
@@ -1,16 +1,50 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, cast, overload
|
||||
from semver import Version
|
||||
|
||||
VERSION: Version = Version(1, 0, 0)
|
||||
"""The current version of sarasacw-omrf-packer"""
|
||||
|
||||
|
||||
def parse_version(vs: str) -> Version:
|
||||
"""This package specific version parser which explicit only support x.x.x style version."""
|
||||
v = Version.parse(vs)
|
||||
if v.prerelease is not None or v.build is not None:
|
||||
raise ValueError("prerelease and build component of version is not supported")
|
||||
else:
|
||||
return v
|
||||
|
||||
|
||||
def get_root_dir() -> Path:
|
||||
return Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def get_templates_dir() -> Path:
|
||||
return get_root_dir() / 'templates'
|
||||
return get_root_dir() / "templates"
|
||||
|
||||
|
||||
@overload
|
||||
def dict_typed_get[T](d: dict[str, Any], key: str, ty: type[T]) -> T | None: ...
|
||||
@overload
|
||||
def dict_typed_get[T, D](d: dict[str, Any], key: str, ty: type[T], default: D) -> T | D: ...
|
||||
def dict_typed_get[T](d: dict[str, Any], key: str, ty: type[T], default: Any = None) -> Any:
|
||||
tmp = d.get(key, None)
|
||||
if tmp is None:
|
||||
return default
|
||||
if not isinstance(tmp, ty):
|
||||
raise TypeError(f'the value of key "{key}" is not a {ty.__name__}')
|
||||
|
||||
return tmp
|
||||
|
||||
|
||||
def dict_typed_get_required[T](d: dict[str, Any], key: str, ty: type[T]) -> T:
|
||||
result = dict_typed_get(d, key, ty)
|
||||
if result is None:
|
||||
raise ValueError(f'can not find key "{key}" in given dictionary')
|
||||
return result
|
||||
|
||||
|
||||
def dict_chain_get(d: dict[str, Any], *args: str) -> dict[str, Any]:
|
||||
for arg in args:
|
||||
d = dict_typed_get_required(d, arg, dict)
|
||||
return d
|
||||
|
||||
Reference in New Issue
Block a user