feat: basically finish packer
This commit is contained in:
@@ -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,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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user