feat: partially write artifact module
This commit is contained in:
@@ -1,49 +1,80 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from semver import Version
|
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 .archiver import Archiver
|
||||||
from .metadata import MetadataExtractor
|
from .metadata import MetadataExtractor
|
||||||
from .renders.cmake import CMakeProperties, CMakeRender
|
from .renders.cmake import CMakeProperties, CMakeRender
|
||||||
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||||
|
|
||||||
|
|
||||||
def build_cmake_render(extractor: MetadataExtractor) -> CMakeRender:
|
class App:
|
||||||
properties = CMakeProperties(
|
__opts: Cli
|
||||||
"wfassoc", "wfassoc", "wfassoc.dll", "wfassoc.lib", Version(1, 0, 0)
|
__extractor: MetadataExtractor
|
||||||
)
|
|
||||||
return CMakeRender(properties)
|
|
||||||
|
|
||||||
|
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:
|
def run(self) -> None:
|
||||||
properties = PkgConfigProperties(
|
|
||||||
"wfassoc", "wfassoc C/C++ FFI", "wfassoc", Version(1, 0, 0)
|
# create renders and their properties from metadata
|
||||||
)
|
cmake_render = self.__build_cmake_render()
|
||||||
return PkgConfigRender(properties)
|
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:
|
def main() -> None:
|
||||||
# parse command line arguments
|
# parse command line arguments
|
||||||
opts = cli.parse()
|
opts = parse_cli()
|
||||||
# setup logging
|
# 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
|
app = App(opts)
|
||||||
extractor = MetadataExtractor(opts.manifest)
|
app.run()
|
||||||
# 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"))
|
|
||||||
|
|||||||
@@ -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)
|
return func(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"error occurs when fetching metadata: {e}") from e
|
raise RuntimeError(f"error occurs when fetching metadata: {e}") from e
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@@ -187,8 +188,10 @@ class MetadataExtractor:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@wrap_metadata_errors
|
@wrap_metadata_errors
|
||||||
def get_target_directory(self) -> str:
|
def get_target_directory(self) -> Path:
|
||||||
return dict_typed_get_required(self.__metadata, "target_directory", str)
|
"""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
|
@wrap_metadata_errors
|
||||||
def get_name(self) -> str:
|
def get_name(self) -> str:
|
||||||
@@ -203,6 +206,14 @@ class MetadataExtractor:
|
|||||||
raw_version = dict_typed_get_required(self.__metadata_package, "version", str)
|
raw_version = dict_typed_get_required(self.__metadata_package, "version", str)
|
||||||
return utils.parse_version(raw_version)
|
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
|
@wrap_metadata_errors
|
||||||
def get_metadata(self) -> Metadata:
|
def get_metadata(self) -> Metadata:
|
||||||
omrf = dict_chain_get(self.__metadata_package, "metadata", "omrf")
|
omrf = dict_chain_get(self.__metadata_package, "metadata", "omrf")
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ top of :class:`renders.common.BaseRender`.
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from re import Pattern, compile
|
|
||||||
from . import common
|
|
||||||
from semver import Version
|
from semver import Version
|
||||||
|
from . import common
|
||||||
|
from .. import utils
|
||||||
_CMAKE_NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_.+-]+")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -56,11 +53,11 @@ class CMakeProperties:
|
|||||||
the version carries a prerelease or build component (unsupported by
|
the version carries a prerelease or build component (unsupported by
|
||||||
CMake config-version files).
|
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(
|
raise ValueError(
|
||||||
"bad namespace component in CMake. consider manually specifying it"
|
"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(
|
raise ValueError(
|
||||||
"bad target name component in CMake. consider manually specifying it"
|
"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")
|
raise ValueError("bad artifact dll file name in CMake")
|
||||||
if self.artifact_lib == "":
|
if self.artifact_lib == "":
|
||||||
raise ValueError("bad artifact lib file name in CMake")
|
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(
|
raise ValueError(
|
||||||
"unsupported semantic version components (prerelease or build) in CMake"
|
"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 dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from re import Pattern, compile
|
||||||
from . import common
|
from . import common
|
||||||
|
from .. import utils
|
||||||
from semver import Version
|
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)
|
@dataclass(frozen=True)
|
||||||
class PkgConfigProperties:
|
class PkgConfigProperties:
|
||||||
"""Inputs required to render the pkg-config ``.pc`` file.
|
"""Inputs required to render the pkg-config ``.pc`` file.
|
||||||
@@ -49,11 +64,13 @@ class PkgConfigProperties:
|
|||||||
version carries a prerelease or build component (unsupported in
|
version carries a prerelease or build component (unsupported in
|
||||||
pkg-config).
|
pkg-config).
|
||||||
"""
|
"""
|
||||||
if self.name == "":
|
if self.name == "" or not _is_good_sentence(self.name):
|
||||||
raise ValueError("unexpected blank name of pkg-config")
|
raise ValueError("bad name (blank or has EOL chars) of pkg-config")
|
||||||
if self.description == "":
|
if self.description == "" or not _is_good_sentence(self.description):
|
||||||
raise ValueError("unexpected blank description of pkg-config")
|
raise ValueError("bad description (blank or has EOL chars) of pkg-config")
|
||||||
if self.version.prerelease is not None or self.version.build is not None:
|
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(
|
raise ValueError(
|
||||||
"unsupported semantic version components (prerelease or build) in pkg-config"
|
"unsupported semantic version components (prerelease or build) in pkg-config"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast, overload
|
from re import Pattern, compile
|
||||||
|
from typing import Any, overload
|
||||||
from semver import Version
|
from semver import Version
|
||||||
|
|
||||||
VERSION: Version = Version(1, 0, 0)
|
VERSION: Version = Version(1, 0, 0)
|
||||||
@@ -14,6 +15,12 @@ def parse_version(vs: str) -> Version:
|
|||||||
else:
|
else:
|
||||||
return v
|
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:
|
def get_root_dir() -> Path:
|
||||||
return Path(__file__).resolve().parent
|
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:
|
for arg in args:
|
||||||
d = dict_typed_get_required(d, arg, dict)
|
d = dict_typed_get_required(d, arg, dict)
|
||||||
return d
|
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