From 8eed2dc7f055159ce6426cc55630fb89d9bee984 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 5 Aug 2026 10:45:51 +0800 Subject: [PATCH] feat: basically finish packer --- packer/README.md | 3 + packer/pyproject.toml | 2 +- packer/src/sarasacw_omrf_packer/__init__.py | 75 +++++---- packer/src/sarasacw_omrf_packer/artifact.py | 148 +++++++++++++----- packer/src/sarasacw_omrf_packer/metadata.py | 2 + .../sarasacw_omrf_packer/renders/pkgconfig.py | 2 +- 6 files changed, 154 insertions(+), 78 deletions(-) diff --git a/packer/README.md b/packer/README.md index a9b590e..e9e6215 100644 --- a/packer/README.md +++ b/packer/README.md @@ -41,6 +41,9 @@ namespace_name = "foobar" target_name = "foobar" [package.metadata.omrf.pkgconfig] +# The unique identifier of the package. +# This property is optional; it defaults to the name of the target Rust project. +id = "foobar" # Human-readable name of the package. # This property is optional; it defaults to the name of the target Rust project. name = "Foo Bar" diff --git a/packer/pyproject.toml b/packer/pyproject.toml index 3facf5e..67f468a 100644 --- a/packer/pyproject.toml +++ b/packer/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sarasacw-omrf-packer" -version = "0.1.0" +version = "1.0.0" description = "Add your description here" readme = "README.md" authors = [ diff --git a/packer/src/sarasacw_omrf_packer/__init__.py b/packer/src/sarasacw_omrf_packer/__init__.py index b612f77..7b6264a 100644 --- a/packer/src/sarasacw_omrf_packer/__init__.py +++ b/packer/src/sarasacw_omrf_packer/__init__.py @@ -1,36 +1,44 @@ import logging import sys from pathlib import Path -from semver import Version +from .utils import VERSION from .cli import Cli, parse as parse_cli -from .artifact import resolve_include_copy, resolve_lib_copy, FileCopyInfo +from .artifact import ( + ArtifactContext, + resolve_include_copy, + resolve_lib_copy, + resolve_cmake_copy, + resolve_pkgconfig_copy, +) from .archiver import Archiver from .metadata import MetadataExtractor -from .renders.cmake import CMakeProperties, CMakeRender -from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender class App: __opts: Cli __extractor: MetadataExtractor + __ctx: ArtifactContext def __init__(self, opts: Cli) -> None: # assign cli options self.__opts = opts # initialize packer try: + # build essential instances self.__extractor = MetadataExtractor(self.__opts.manifest) - _ = self.__extractor.get_metadata + self.__ctx = ArtifactContext(self.__extractor) + # check version + metadata = self.__ctx.metadata + if metadata.min_version is not None: + if metadata.min_version > VERSION: + raise RuntimeError( + f"requested minimum version is not fulfilled. {metadata.min_version} required got {VERSION}" + ) except Exception as e: logging.error(f"fail to initialize packer: {e}") sys.exit(1) def run(self) -> None: - - # create renders and their properties from metadata - cmake_render = self.__build_cmake_render() - pkgconfig_render = self.__build_pkgconfig_render() - # create distribution with Archiver(self.__opts.dist_dir, self.__opts.dist_zip) as archiver: # create basic directory @@ -38,36 +46,23 @@ class App: archiver.push_dir(Path("include")) archiver.push_dir(Path("lib")) # copy header files - include_dir = Path("include") - for header_copy in self.__build_header_copy(): - archiver.push_file(header_copy.from_path, include_dir / header_copy.to_path) - # create package infos - 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") - ) - - def __build_header_copy(self) -> tuple[IncludeCopyInfo, ...]: - return resolve_header_copy(self.__extractor) - - def __build_cmake_render(self) -> CMakeRender: - properties = CMakeProperties( - "wfassoc", "wfassoc", "wfassoc.dll", "wfassoc.lib", Version(1, 0, 0) - ) - return CMakeRender(properties) - - def __build_pkgconfig_render(self) -> PkgConfigRender: - properties = PkgConfigProperties( - "wfassoc", "wfassoc C/C++ FFI", "wfassoc", Version(1, 0, 0) - ) - return PkgConfigRender(properties) + logging.info("Copying header files...") + for info in resolve_include_copy(self.__ctx): + logging.info("Copying {0} -> {1}", info.from_path, info.to_path) + archiver.push_file(info.from_path, info.to_path) + # copy lib files + logging.info("Copying library files...") + for info in resolve_lib_copy(self.__ctx): + logging.info("Copying {0} -> {1}", info.from_path, info.to_path) + archiver.push_file(info.from_path, info.to_path) + # copy package distribution files + logging.info("Creating package manager files...") + for info in resolve_cmake_copy(self.__ctx): + logging.info("Creating {0}", info.to_path) + archiver.push_text(info.text, info.to_path) + for info in resolve_pkgconfig_copy(self.__ctx): + logging.info("Creating {0}", info.to_path) + archiver.push_text(info.text, info.to_path) def main() -> None: diff --git a/packer/src/sarasacw_omrf_packer/artifact.py b/packer/src/sarasacw_omrf_packer/artifact.py index 66d3aee..e7bf9d8 100644 --- a/packer/src/sarasacw_omrf_packer/artifact.py +++ b/packer/src/sarasacw_omrf_packer/artifact.py @@ -8,7 +8,8 @@ from .metadata import MetadataExtractor, Metadata from .renders.cmake import CMakeProperties, CMakeRender from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender -class ArtifactMetadata: + +class ArtifactContext: """A wrapper of metadata with proper fallback""" __extractor: MetadataExtractor @@ -18,20 +19,30 @@ class ArtifactMetadata: self.__extractor = extractor self.__metadata = self.__extractor.get_metadata() - - + @property + def extractor(self) -> MetadataExtractor: + return self.__extractor + + @property + def metadata(self) -> Metadata: + return self.__metadata +_GLOB_MAGIC_PATTERN: Pattern = compile(r"[*?[]") def _is_glob_pattern(pattern: str) -> bool: """Check whether given string is a valid UNIX-style glob (pathname) pattern - + :returns: True if it is, otherwise false. """ - pass - -def _extract_common_prefix(pattern: str) -> (str, str): + # TODO: only magic-character presence is checked; bracket expressions such + # as ``[abc]`` are not validated for balance. Acceptable today since users + # mostly rely on ``*``/``**``, but ``[]`` patterns may break this later. + return _GLOB_MAGIC_PATTERN.search(pattern) is not None + + +def _extract_common_prefix(pattern: str) -> tuple[str, str]: """Extract the immutable common prefix of given UNIX-style glob (pathname) pattern. For example, give ``some/path/**/*.h``, this function return ``some/path`` @@ -39,24 +50,31 @@ def _extract_common_prefix(pattern: str) -> (str, str): First item is the immutable common prefix of given UNIX-style glob (pathname) pattern, and the last item is the residue. """ - pass + # TODO: per-component wildcard detection reuses ``_is_glob_pattern``, which + # does not validate bracket expressions (see the TODO there). Same caveat. + parts = Path(pattern).parts + i = 0 + while i < len(parts) and not _is_glob_pattern(parts[i]): + i += 1 + return "/".join(parts[:i]), "/".join(parts[i:]) + @dataclass(frozen=True) class FileCopyInfo: - from_path: Path """The absolute path pointing to source file without any wildcard""" to_path: Path """The path of destination file relative to the install directory""" -def resolve_include_copy(extractor: MetadataExtractor) -> tuple[FileCopyInfo, ...]: - metadata = extractor.get_metadata +def resolve_include_copy(ctx: ArtifactContext) -> tuple[FileCopyInfo, ...]: + extractor = ctx.extractor + metadata = ctx.metadata project_root = Path(extractor.get_project_dir()) rv: list[FileCopyInfo] = [] for header in metadata.headers: - to_path = Path(header.to_path) + to_path = Path("include") / Path(header.to_path) if _is_glob_pattern(header.from_path): (common_prefix, glob_residue) = _extract_common_prefix(header.from_path) new_project_root = project_root / Path(common_prefix) @@ -64,12 +82,15 @@ def resolve_include_copy(extractor: MetadataExtractor) -> tuple[FileCopyInfo, .. for subpath in new_project_root.glob(glob_residue): rv.append(FileCopyInfo(new_project_root / subpath, to_path / subpath)) else: - rv.append(FileCopyInfo(project_root / header.from_path, to_path / header.from_path)) + rv.append( + FileCopyInfo( + project_root / header.from_path, to_path / header.from_path + ) + ) return tuple(rv) - def _generate_dll_artifact_name(name: str) -> str: match sys.platform: case "win32" | "cygwin": @@ -87,68 +108,123 @@ def _generate_lib_artifact_name(name: str) -> str: return f"{name}.dll.lib" -def resolve_lib_copy(extractor: MetadataExtractor) -> tuple[FileCopyInfo, ...]: +def resolve_lib_copy(ctx: ArtifactContext) -> tuple[FileCopyInfo, ...]: + extractor = ctx.extractor target_directory = extractor.get_target_directory() / "release" target_name = extractor.get_target_name() rv: list[FileCopyInfo] = [] # copy artifact for redist dll_artifact_filename = _generate_dll_artifact_name(target_name) - rv.append(FileCopyInfo(target_directory / dll_artifact_filename, Path("bin" if sys.platform == "win32" else "lib") / dll_artifact_filename )) + rv.append( + FileCopyInfo( + target_directory / dll_artifact_filename, + Path("bin" if sys.platform == "win32" else "lib") / dll_artifact_filename, + ) + ) # copy artifact for linking only on windows if sys.platform == "win32": lib_artifact_filename = _generate_lib_artifact_name(target_name) - rv.append(FileCopyInfo(target_directory / lib_artifact_filename, Path("lib") / lib_artifact_filename)) + rv.append( + FileCopyInfo( + target_directory / lib_artifact_filename, + Path("lib") / lib_artifact_filename, + ) + ) return tuple(rv) + @dataclass(frozen=True) class TextCopyInfo: - text: str """The content of source file""" to_path: Path """The path of destination file relative to the install directory""" -_BAD_NAME_PATTERN: Pattern = compile(r'[^a-zA-Z0-9_+-]') + +_BAD_NAME_PATTERN: Pattern = compile(r"[^a-zA-Z0-9_+-]") + def _sanitize_name(name: str) -> str: - new_name = _BAD_NAME_PATTERN.sub('', name) - if new_name == '': - raise ValueError('name is blank after sanitizing') + new_name = _BAD_NAME_PATTERN.sub("", name) + if new_name == "": + raise ValueError("name is blank after sanitizing") else: return new_name -def resolve_cmake_copy(extractor: MetadataExtractor) -> tuple[TextCopyInfo, ...]: +def resolve_cmake_copy(ctx: ArtifactContext) -> tuple[TextCopyInfo, ...]: + extractor = ctx.extractor + metadata = ctx.metadata target_name = extractor.get_target_name() - name = _sanitize_name(target_name) + # compute fallback name + cmake_fallback_name = _sanitize_name(target_name) + # get namespace and target name with fallback + cmake_namespace_name = cmake_fallback_name + cmake_target_name = cmake_fallback_name + if metadata.cmake is not None: + if metadata.cmake.namespace_name is not None: + cmake_namespace_name = metadata.cmake.namespace_name + if metadata.cmake.target_name is not None: + cmake_target_name = metadata.cmake.target_name + + # build cmake properties and render properties = CMakeProperties( - name, name, _generate_dll_artifact_name(target_name), _generate_lib_artifact_name(target_name), extractor.get_version() + cmake_namespace_name, + cmake_target_name, + _generate_dll_artifact_name(target_name), + _generate_lib_artifact_name(target_name), + extractor.get_version(), ) render = CMakeRender(properties) + # return infos return ( - TextCopyInfo(render.render_config(), - Path("lib", "cmake", name, f"{name}Config.cmake")), + TextCopyInfo( + render.render_config(), + Path( + "lib", + "cmake", + cmake_target_name, + f"{cmake_target_name}Config.cmake", + ), + ), TextCopyInfo( render.render_config_version(), - Path("lib", "cmake", name, f"{name}ConfigVersion.cmake") - ) + Path( + "lib", + "cmake", + cmake_target_name, + f"{cmake_target_name}ConfigVersion.cmake", + ), + ), ) -def resolve_pkgconfig_copy(extractor: MetadataExtractor) -> tuple[TextCopyInfo, ...]: +def resolve_pkgconfig_copy(ctx: ArtifactContext) -> tuple[TextCopyInfo, ...]: + extractor = ctx.extractor + metadata = ctx.metadata target_name = extractor.get_target_name() - name = _sanitize_name(target_name) + + # compute fallback name and description + pkgconfig_id = _sanitize_name(target_name) + pkgconfig_name = _sanitize_name(target_name) + pkgconfig_description = extractor.get_description() + # get name and description with fallback + if metadata.pkgconfig is not None: + if metadata.pkgconfig.name is not None: + pkgconfig_name = metadata.pkgconfig.name + if metadata.pkgconfig.description is not None: + pkgconfig_description = metadata.pkgconfig.description + # correct None description + if pkgconfig_description is None: + pkgconfig_description = "" properties = PkgConfigProperties( - name, "wfassoc C/C++ FFI", target_name, extractor.get_version() + pkgconfig_name, pkgconfig_description, target_name, extractor.get_version() ) render = PkgConfigRender(properties) return ( - TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{name}.pc")), + TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{pkgconfig_id}.pc")), ) - - - diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index dc16a62..2a066cc 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -42,12 +42,14 @@ class MetadataCMake: @dataclass(frozen=True) class MetadataPkgConfig: + id: str | None name: str | None description: str | None @staticmethod def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": return MetadataPkgConfig( + id=dict_typed_get(d, "id", str), name=dict_typed_get(d, "name", str), description=dict_typed_get(d, "description", str), ) diff --git a/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py b/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py index f172f95..e0e7eab 100644 --- a/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py +++ b/packer/src/sarasacw_omrf_packer/renders/pkgconfig.py @@ -25,7 +25,7 @@ def _is_good_sentence(s: str) -> bool: because EOL chars are not allowed in these properties. :returns: True if given string don't contain any EOL chars, otherwise false. """ - return _EOL_PATTERN.match(s) is None + return _EOL_PATTERN.search(s) is None @dataclass(frozen=True)