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"
[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"
+1 -1
View File
@@ -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 = [
+35 -40
View File
@@ -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:
+110 -34
View File
@@ -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,10 +19,16 @@ 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:
@@ -29,9 +36,13 @@ def _is_glob_pattern(pattern: str) -> bool:
: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
# mostly rely on ``*``/``**``, but ``[]`` patterns may break this later.
return _GLOB_MAGIC_PATTERN.search(pattern) is not None
def _extract_common_prefix(pattern: str) -> (str, str):
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")),
)
@@ -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),
)
@@ -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)