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