feat: partially write artifact module
This commit is contained in:
@@ -1,49 +1,80 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from semver import Version
|
||||
from . import cli
|
||||
from .cli import Cli, parse as parse_cli
|
||||
from .artifact import resolve_include_copy, resolve_lib_copy, FileCopyInfo
|
||||
from .archiver import Archiver
|
||||
from .metadata import MetadataExtractor
|
||||
from .renders.cmake import CMakeProperties, CMakeRender
|
||||
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||
|
||||
|
||||
def build_cmake_render(extractor: MetadataExtractor) -> CMakeRender:
|
||||
properties = CMakeProperties(
|
||||
"wfassoc", "wfassoc", "wfassoc.dll", "wfassoc.lib", Version(1, 0, 0)
|
||||
)
|
||||
return CMakeRender(properties)
|
||||
class App:
|
||||
__opts: Cli
|
||||
__extractor: MetadataExtractor
|
||||
|
||||
def __init__(self, opts: Cli) -> None:
|
||||
# assign cli options
|
||||
self.__opts = opts
|
||||
# initialize packer
|
||||
try:
|
||||
self.__extractor = MetadataExtractor(self.__opts.manifest)
|
||||
_ = self.__extractor.get_metadata
|
||||
except Exception as e:
|
||||
logging.error(f"fail to initialize packer: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def build_pkgconfig_render(extractor: MetadataExtractor) -> PkgConfigRender:
|
||||
properties = PkgConfigProperties(
|
||||
"wfassoc", "wfassoc C/C++ FFI", "wfassoc", Version(1, 0, 0)
|
||||
)
|
||||
return PkgConfigRender(properties)
|
||||
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
|
||||
archiver.push_dir(Path("bin"))
|
||||
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)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# parse command line arguments
|
||||
opts = cli.parse()
|
||||
opts = parse_cli()
|
||||
# setup logging
|
||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO)
|
||||
|
||||
# build metadata extractor
|
||||
extractor = MetadataExtractor(opts.manifest)
|
||||
# extract omrf metadata
|
||||
metadata = extractor.get_metadata()
|
||||
|
||||
|
||||
|
||||
# create renders and their properties from metadata
|
||||
cmake_render = extractor
|
||||
|
||||
|
||||
# create distribution
|
||||
with Archiver(opts.dist_dir, opts.dist_zip) as archiver:
|
||||
archiver.push_dir(Path("bin"))
|
||||
archiver.push_dir(Path("include"))
|
||||
archiver.push_dir(Path("lib"))
|
||||
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"))
|
||||
app = App(opts)
|
||||
app.run()
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import fnmatch
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from re import Pattern, compile
|
||||
from .metadata import MetadataExtractor, Metadata
|
||||
from .renders.cmake import CMakeProperties, CMakeRender
|
||||
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||
|
||||
class ArtifactMetadata:
|
||||
"""A wrapper of metadata with proper fallback"""
|
||||
|
||||
__extractor: MetadataExtractor
|
||||
__metadata: Metadata
|
||||
|
||||
def __init__(self, extractor: MetadataExtractor) -> None:
|
||||
self.__extractor = extractor
|
||||
self.__metadata = self.__extractor.get_metadata()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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):
|
||||
"""Extract the immutable common prefix of given UNIX-style glob (pathname) pattern.
|
||||
|
||||
For example, give ``some/path/**/*.h``, this function return ``some/path``
|
||||
:returns: A tuple with 2 items.
|
||||
First item is the immutable common prefix of given UNIX-style glob (pathname) pattern,
|
||||
and the last item is the residue.
|
||||
"""
|
||||
pass
|
||||
|
||||
@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
|
||||
project_root = Path(extractor.get_project_dir())
|
||||
rv: list[FileCopyInfo] = []
|
||||
|
||||
for header in metadata.headers:
|
||||
to_path = 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)
|
||||
|
||||
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))
|
||||
|
||||
return tuple(rv)
|
||||
|
||||
|
||||
|
||||
def _generate_dll_artifact_name(name: str) -> str:
|
||||
match sys.platform:
|
||||
case "win32" | "cygwin":
|
||||
return f"{name}.dll"
|
||||
case "linux" | "android" | "freebsd":
|
||||
return f"lib{name}.so"
|
||||
case "darwin" | "ios":
|
||||
return f"lib{name}.dylib"
|
||||
case _:
|
||||
raise RuntimeError("not supported system")
|
||||
|
||||
|
||||
def _generate_lib_artifact_name(name: str) -> str:
|
||||
# only Windows has this feature, so we simply return it
|
||||
return f"{name}.dll.lib"
|
||||
|
||||
|
||||
def resolve_lib_copy(extractor: MetadataExtractor) -> tuple[FileCopyInfo, ...]:
|
||||
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 ))
|
||||
# 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))
|
||||
|
||||
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_+-]')
|
||||
|
||||
def _sanitize_name(name: str) -> str:
|
||||
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, ...]:
|
||||
target_name = extractor.get_target_name()
|
||||
name = _sanitize_name(target_name)
|
||||
|
||||
properties = CMakeProperties(
|
||||
name, name, _generate_dll_artifact_name(target_name), _generate_lib_artifact_name(target_name), extractor.get_version()
|
||||
)
|
||||
render = CMakeRender(properties)
|
||||
return (
|
||||
TextCopyInfo(render.render_config(),
|
||||
Path("lib", "cmake", name, f"{name}Config.cmake")),
|
||||
TextCopyInfo(
|
||||
render.render_config_version(),
|
||||
Path("lib", "cmake", name, f"{name}ConfigVersion.cmake")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def resolve_pkgconfig_copy(extractor: MetadataExtractor) -> tuple[TextCopyInfo, ...]:
|
||||
target_name = extractor.get_target_name()
|
||||
name = _sanitize_name(target_name)
|
||||
|
||||
properties = PkgConfigProperties(
|
||||
name, "wfassoc C/C++ FFI", target_name, extractor.get_version()
|
||||
)
|
||||
render = PkgConfigRender(properties)
|
||||
return (
|
||||
TextCopyInfo(render.render(), Path("lib", "pkgconfig", f"{name}.pc")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ def wrap_metadata_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"error occurs when fetching metadata: {e}") from e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -187,8 +188,10 @@ class MetadataExtractor:
|
||||
)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_target_directory(self) -> str:
|
||||
return dict_typed_get_required(self.__metadata, "target_directory", str)
|
||||
def get_target_directory(self) -> Path:
|
||||
"""Get the absolute path to directory where Rust target directory is"""
|
||||
raw_target_directory = dict_typed_get_required(self.__metadata, "target_directory", str)
|
||||
return Path(raw_target_directory)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_name(self) -> str:
|
||||
@@ -203,6 +206,14 @@ class MetadataExtractor:
|
||||
raw_version = dict_typed_get_required(self.__metadata_package, "version", str)
|
||||
return utils.parse_version(raw_version)
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_project_dir(self) -> Path:
|
||||
"""Get the absolute path to directory where exported Cargo.toml is"""
|
||||
raw_manifest_path = dict_typed_get_required(
|
||||
self.__metadata_package, "manifest_path", str
|
||||
)
|
||||
return Path(raw_manifest_path).parent
|
||||
|
||||
@wrap_metadata_errors
|
||||
def get_metadata(self) -> Metadata:
|
||||
omrf = dict_chain_get(self.__metadata_package, "metadata", "omrf")
|
||||
|
||||
@@ -9,12 +9,9 @@ top of :class:`renders.common.BaseRender`.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from re import Pattern, compile
|
||||
from . import common
|
||||
from semver import Version
|
||||
|
||||
|
||||
_CMAKE_NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_.+-]+")
|
||||
from . import common
|
||||
from .. import utils
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -56,11 +53,11 @@ class CMakeProperties:
|
||||
the version carries a prerelease or build component (unsupported by
|
||||
CMake config-version files).
|
||||
"""
|
||||
if _CMAKE_NAME_PATTERN.fullmatch(self.namespace_name) is None:
|
||||
if not utils.is_good_name(self.namespace_name):
|
||||
raise ValueError(
|
||||
"bad namespace component in CMake. consider manually specifying it"
|
||||
)
|
||||
if _CMAKE_NAME_PATTERN.fullmatch(self.target_name) is None:
|
||||
if not utils.is_good_name(self.target_name) is None:
|
||||
raise ValueError(
|
||||
"bad target name component in CMake. consider manually specifying it"
|
||||
)
|
||||
@@ -68,7 +65,7 @@ class CMakeProperties:
|
||||
raise ValueError("bad artifact dll file name in CMake")
|
||||
if self.artifact_lib == "":
|
||||
raise ValueError("bad artifact lib file name in CMake")
|
||||
if self.version.prerelease is not None or self.version.build is not None:
|
||||
if not utils.is_good_version(self.version):
|
||||
raise ValueError(
|
||||
"unsupported semantic version components (prerelease or build) in CMake"
|
||||
)
|
||||
|
||||
@@ -9,10 +9,25 @@ rendering on top of :class:`renders.common.BaseRender`.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from re import Pattern, compile
|
||||
from . import common
|
||||
from .. import utils
|
||||
from semver import Version
|
||||
|
||||
|
||||
_EOL_PATTERN: Pattern = compile(r"[\r\n]")
|
||||
|
||||
|
||||
def _is_good_sentence(s: str) -> bool:
|
||||
"""Check whether given string has EOL chars.
|
||||
|
||||
This function is used for checking human-readable name and description,
|
||||
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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PkgConfigProperties:
|
||||
"""Inputs required to render the pkg-config ``.pc`` file.
|
||||
@@ -49,11 +64,13 @@ class PkgConfigProperties:
|
||||
version carries a prerelease or build component (unsupported in
|
||||
pkg-config).
|
||||
"""
|
||||
if self.name == "":
|
||||
raise ValueError("unexpected blank name of pkg-config")
|
||||
if self.description == "":
|
||||
raise ValueError("unexpected blank description of pkg-config")
|
||||
if self.version.prerelease is not None or self.version.build is not None:
|
||||
if self.name == "" or not _is_good_sentence(self.name):
|
||||
raise ValueError("bad name (blank or has EOL chars) of pkg-config")
|
||||
if self.description == "" or not _is_good_sentence(self.description):
|
||||
raise ValueError("bad description (blank or has EOL chars) of pkg-config")
|
||||
if not utils.is_good_name(self.artifact):
|
||||
raise ValueError("bad artifact name in pkg-config")
|
||||
if not utils.is_good_version(self.version):
|
||||
raise ValueError(
|
||||
"unsupported semantic version components (prerelease or build) in pkg-config"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, cast, overload
|
||||
from re import Pattern, compile
|
||||
from typing import Any, overload
|
||||
from semver import Version
|
||||
|
||||
VERSION: Version = Version(1, 0, 0)
|
||||
@@ -14,6 +15,12 @@ def parse_version(vs: str) -> Version:
|
||||
else:
|
||||
return v
|
||||
|
||||
def is_good_version(v: Version) -> bool:
|
||||
"""Check whether given version has ``prerelease`` or ``build`` components which is not allowed in this package.
|
||||
|
||||
:returns: True if given string do not have these components, otherwise false.
|
||||
"""
|
||||
return v.prerelease is None and v.build is None
|
||||
|
||||
def get_root_dir() -> Path:
|
||||
return Path(__file__).resolve().parent
|
||||
@@ -48,3 +55,16 @@ def dict_chain_get(d: dict[str, Any], *args: str) -> dict[str, Any]:
|
||||
for arg in args:
|
||||
d = dict_typed_get_required(d, arg, dict)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
_NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_+-]+")
|
||||
|
||||
def is_good_name(name: str) -> bool:
|
||||
"""This package specific name checker
|
||||
|
||||
These allowed chars are picked as the subset of allowed chars in all generated contents,
|
||||
including file name restrictions, CMake name restrictions and etc.
|
||||
:returns: True if given name is legal, otherwise false.
|
||||
"""
|
||||
return _NAME_PATTERN.fullmatch(name) is not None
|
||||
|
||||
Reference in New Issue
Block a user