feat: basically finish packer

This commit is contained in:
2026-08-05 10:45:51 +08:00
parent 26ce893920
commit 8eed2dc7f0
6 changed files with 154 additions and 78 deletions
+3
View File
@@ -41,6 +41,9 @@ namespace_name = "foobar"
target_name = "foobar" target_name = "foobar"
[package.metadata.omrf.pkgconfig] [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. # Human-readable name of the package.
# This property is optional; it defaults to the name of the target Rust project. # This property is optional; it defaults to the name of the target Rust project.
name = "Foo Bar" name = "Foo Bar"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "sarasacw-omrf-packer" name = "sarasacw-omrf-packer"
version = "0.1.0" version = "1.0.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [ authors = [
+35 -40
View File
@@ -1,36 +1,44 @@
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
from semver import Version from .utils import VERSION
from .cli import Cli, parse as parse_cli 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 .archiver import Archiver
from .metadata import MetadataExtractor from .metadata import MetadataExtractor
from .renders.cmake import CMakeProperties, CMakeRender
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
class App: class App:
__opts: Cli __opts: Cli
__extractor: MetadataExtractor __extractor: MetadataExtractor
__ctx: ArtifactContext
def __init__(self, opts: Cli) -> None: def __init__(self, opts: Cli) -> None:
# assign cli options # assign cli options
self.__opts = opts self.__opts = opts
# initialize packer # initialize packer
try: try:
# build essential instances
self.__extractor = MetadataExtractor(self.__opts.manifest) 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: except Exception as e:
logging.error(f"fail to initialize packer: {e}") logging.error(f"fail to initialize packer: {e}")
sys.exit(1) sys.exit(1)
def run(self) -> None: 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 # create distribution
with Archiver(self.__opts.dist_dir, self.__opts.dist_zip) as archiver: with Archiver(self.__opts.dist_dir, self.__opts.dist_zip) as archiver:
# create basic directory # create basic directory
@@ -38,36 +46,23 @@ class App:
archiver.push_dir(Path("include")) archiver.push_dir(Path("include"))
archiver.push_dir(Path("lib")) archiver.push_dir(Path("lib"))
# copy header files # copy header files
include_dir = Path("include") logging.info("Copying header files...")
for header_copy in self.__build_header_copy(): for info in resolve_include_copy(self.__ctx):
archiver.push_file(header_copy.from_path, include_dir / header_copy.to_path) logging.info("Copying {0} -> {1}", info.from_path, info.to_path)
# create package infos archiver.push_file(info.from_path, info.to_path)
archiver.push_text( # copy lib files
cmake_render.render_config(), logging.info("Copying library files...")
Path("lib", "cmake", "wfassoc", "wfassocConfig.cmake"), for info in resolve_lib_copy(self.__ctx):
) logging.info("Copying {0} -> {1}", info.from_path, info.to_path)
archiver.push_text( archiver.push_file(info.from_path, info.to_path)
cmake_render.render_config_version(), # copy package distribution files
Path("lib", "cmake", "wfassoc", "wfassocConfigVersion.cmake"), logging.info("Creating package manager files...")
) for info in resolve_cmake_copy(self.__ctx):
archiver.push_text( logging.info("Creating {0}", info.to_path)
pkgconfig_render.render(), Path("lib", "pkgconfig", "wfassoc.pc") archiver.push_text(info.text, info.to_path)
) for info in resolve_pkgconfig_copy(self.__ctx):
logging.info("Creating {0}", info.to_path)
def __build_header_copy(self) -> tuple[IncludeCopyInfo, ...]: archiver.push_text(info.text, info.to_path)
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)
def main() -> None: def main() -> None:
+112 -36
View File
@@ -8,7 +8,8 @@ from .metadata import MetadataExtractor, Metadata
from .renders.cmake import CMakeProperties, CMakeRender from .renders.cmake import CMakeProperties, CMakeRender
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
class ArtifactMetadata:
class ArtifactContext:
"""A wrapper of metadata with proper fallback""" """A wrapper of metadata with proper fallback"""
__extractor: MetadataExtractor __extractor: MetadataExtractor
@@ -18,20 +19,30 @@ class ArtifactMetadata:
self.__extractor = extractor self.__extractor = extractor
self.__metadata = self.__extractor.get_metadata() 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: def _is_glob_pattern(pattern: str) -> bool:
"""Check whether given string is a valid UNIX-style glob (pathname) pattern """Check whether given string is a valid UNIX-style glob (pathname) pattern
:returns: True if it is, otherwise false. :returns: True if it is, otherwise false.
""" """
pass # TODO: only magic-character presence is checked; bracket expressions such
# as ``[abc]`` are not validated for balance. Acceptable today since users
def _extract_common_prefix(pattern: str) -> (str, str): # 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. """Extract the immutable common prefix of given UNIX-style glob (pathname) pattern.
For example, give ``some/path/**/*.h``, this function return ``some/path`` 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, First item is the immutable common prefix of given UNIX-style glob (pathname) pattern,
and the last item is the residue. 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) @dataclass(frozen=True)
class FileCopyInfo: class FileCopyInfo:
from_path: Path from_path: Path
"""The absolute path pointing to source file without any wildcard""" """The absolute path pointing to source file without any wildcard"""
to_path: Path to_path: Path
"""The path of destination file relative to the install directory""" """The path of destination file relative to the install directory"""
def resolve_include_copy(extractor: MetadataExtractor) -> tuple[FileCopyInfo, ...]: def resolve_include_copy(ctx: ArtifactContext) -> tuple[FileCopyInfo, ...]:
metadata = extractor.get_metadata extractor = ctx.extractor
metadata = ctx.metadata
project_root = Path(extractor.get_project_dir()) project_root = Path(extractor.get_project_dir())
rv: list[FileCopyInfo] = [] rv: list[FileCopyInfo] = []
for header in metadata.headers: 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): if _is_glob_pattern(header.from_path):
(common_prefix, glob_residue) = _extract_common_prefix(header.from_path) (common_prefix, glob_residue) = _extract_common_prefix(header.from_path)
new_project_root = project_root / Path(common_prefix) 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): for subpath in new_project_root.glob(glob_residue):
rv.append(FileCopyInfo(new_project_root / subpath, to_path / subpath)) rv.append(FileCopyInfo(new_project_root / subpath, to_path / subpath))
else: 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) return tuple(rv)
def _generate_dll_artifact_name(name: str) -> str: def _generate_dll_artifact_name(name: str) -> str:
match sys.platform: match sys.platform:
case "win32" | "cygwin": case "win32" | "cygwin":
@@ -87,68 +108,123 @@ def _generate_lib_artifact_name(name: str) -> str:
return f"{name}.dll.lib" 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_directory = extractor.get_target_directory() / "release"
target_name = extractor.get_target_name() target_name = extractor.get_target_name()
rv: list[FileCopyInfo] = [] rv: list[FileCopyInfo] = []
# copy artifact for redist # copy artifact for redist
dll_artifact_filename = _generate_dll_artifact_name(target_name) 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 # copy artifact for linking only on windows
if sys.platform == "win32": if sys.platform == "win32":
lib_artifact_filename = _generate_lib_artifact_name(target_name) 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) return tuple(rv)
@dataclass(frozen=True) @dataclass(frozen=True)
class TextCopyInfo: class TextCopyInfo:
text: str text: str
"""The content of source file""" """The content of source file"""
to_path: Path to_path: Path
"""The path of destination file relative to the install directory""" """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: def _sanitize_name(name: str) -> str:
new_name = _BAD_NAME_PATTERN.sub('', name) new_name = _BAD_NAME_PATTERN.sub("", name)
if new_name == '': if new_name == "":
raise ValueError('name is blank after sanitizing') raise ValueError("name is blank after sanitizing")
else: else:
return new_name 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() 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( 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) render = CMakeRender(properties)
# return infos
return ( return (
TextCopyInfo(render.render_config(), TextCopyInfo(
Path("lib", "cmake", name, f"{name}Config.cmake")), render.render_config(),
Path(
"lib",
"cmake",
cmake_target_name,
f"{cmake_target_name}Config.cmake",
),
),
TextCopyInfo( TextCopyInfo(
render.render_config_version(), 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() 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( properties = PkgConfigProperties(
name, "wfassoc C/C++ FFI", target_name, extractor.get_version() pkgconfig_name, pkgconfig_description, target_name, extractor.get_version()
) )
render = PkgConfigRender(properties) render = PkgConfigRender(properties)
return ( return (
TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{name}.pc")), TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{pkgconfig_id}.pc")),
) )
@@ -42,12 +42,14 @@ class MetadataCMake:
@dataclass(frozen=True) @dataclass(frozen=True)
class MetadataPkgConfig: class MetadataPkgConfig:
id: str | None
name: str | None name: str | None
description: str | None description: str | None
@staticmethod @staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig":
return MetadataPkgConfig( return MetadataPkgConfig(
id=dict_typed_get(d, "id", str),
name=dict_typed_get(d, "name", str), name=dict_typed_get(d, "name", str),
description=dict_typed_get(d, "description", str), description=dict_typed_get(d, "description", str),
) )
@@ -25,7 +25,7 @@ def _is_good_sentence(s: str) -> bool:
because EOL chars are not allowed in these properties. because EOL chars are not allowed in these properties.
:returns: True if given string don't contain any EOL chars, otherwise false. :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) @dataclass(frozen=True)