feat: finish basic packer

- finish basic packer
- add basic jinja2 template
This commit is contained in:
2026-08-02 20:54:14 +08:00
parent 8098216a83
commit 3261a4cd4b
11 changed files with 203 additions and 32 deletions
+87 -6
View File
@@ -1,10 +1,91 @@
import zipfile import zipfile
import shutil
from pathlib import Path from pathlib import Path
from types import TracebackType
from typing import Optional
def pack(dist_dir: str, output_zip: str) -> None: class Archiver:
dist = Path(dist_dir) """Dual sink that mirrors the same distribution tree into a directory and a zip archive.
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zf:
for path in dist.rglob("*"): On construction the archiver (optionally) prepares a directory on disk and
if path.is_file(): (optionally) opens a :class:`zipfile.ZipFile` for writing. Every subsequent
zf.write(path, path.relative_to(dist)) :meth:`push_file`/:meth:`push_text` call then writes the same logical entry
-- identified by its ``arcname``, a path relative to the distribution root
-- into both destinations, skipping whichever sink was not requested.
The archiver is usable as a context manager so that the underlying archive
is closed deterministically::
with Archiver(dist_dir, dist_zip) as arc:
arc.push_file(...)
"""
__dist_dir: Optional[Path]
__dist_zip: Optional[zipfile.ZipFile]
def __init__(self, dist_dir: Optional[Path], dist_zip: Optional[Path]) -> None:
"""Configure the sinks and acquire their backing resources.
:param dist_dir: Directory to mirror the tree into. Created (with
parents) when given; ``None`` disables the directory sink.
:param dist_zip: Path of the zip archive to create, truncated on open.
``None`` disables the zip sink.
"""
# make sure the distribution directory is existing
self.__dist_dir = dist_dir
if self.__dist_dir is not None:
self.__dist_dir.mkdir(parents=True, exist_ok=True)
# create zip instance
if dist_zip is not None:
self.__dist_zip = zipfile.ZipFile(dist_zip, "w", zipfile.ZIP_DEFLATED)
else:
self.__dist_zip = None
def push_file(self, filepath: Path, arcname: Path) -> None:
"""Mirror an existing file into both sinks.
:param filepath: Source file on disk to copy / embed.
:param arcname: Destination path relative to the distribution root.
Missing parent directories are created for the directory sink.
"""
if self.__dist_dir is not None:
target_filepath = self.__dist_dir / arcname
target_filepath.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(filepath, target_filepath)
if self.__dist_zip is not None:
self.__dist_zip.write(filepath, arcname)
def push_text(self, text: str, arcname: Path) -> None:
"""Mirror in-memory text into both sinks.
:param text: UTF-8 text content to write.
:param arcname: Destination path relative to the distribution root.
Missing parent directories are created for the directory sink.
"""
if self.__dist_dir is not None:
target_filepath = self.__dist_dir / arcname
target_filepath.parent.mkdir(parents=True, exist_ok=True)
with open(target_filepath, "w", encoding="utf-8") as f:
f.write(text)
if self.__dist_zip is not None:
self.__dist_zip.writestr(str(arcname), text)
def __enter__(self) -> "Archiver":
"""Enter the context and return this archiver as the bound target."""
return self
def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
"""Release the zip sink by closing its backing archive, if any.
Exceptions raised inside the ``with`` body are never suppressed.
"""
if self.__dist_zip is not None:
self.__dist_zip.close()
-13
View File
@@ -1,13 +0,0 @@
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
def render(output_dir: str) -> None:
templates_dir = Path(__file__).parent / "templates"
env = Environment(
loader=FileSystemLoader(templates_dir),
autoescape=select_autoescape(),
)
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
@@ -1,13 +0,0 @@
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
def render(output_dir: str) -> None:
templates_dir = Path(__file__).parent / "templates"
env = Environment(
loader=FileSystemLoader(templates_dir),
autoescape=select_autoescape(),
)
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,24 @@
from dataclasses import dataclass
from pathlib import Path
from . import common
@dataclass(frozen=True)
class CMakeProperties:
name: str
"""The identifier of project"""
class CMakeRender:
__render: common.BaseRender
__properties: CMakeProperties
def __init__(self, properties: CMakeProperties) -> None:
self.__render = common.BaseRender()
self.__properties = properties
def render_config(self) -> None:
pass
def render_config_version(self) -> None:
pass
@@ -0,0 +1,22 @@
from pathlib import Path
from typing import Any
import jinja2
from .. import utils
class BaseRender:
__loader: jinja2.BaseLoader
__environment: jinja2.Environment
def __init__(self) -> None:
self.__loader = jinja2.FileSystemLoader(utils.get_templates_dir())
self.__environment = jinja2.Environment(loader=self.__loader)
def render(
self, template_filename: str, dest_filepath: Path, payload: dict[str, Any]
) -> None:
# fetch template
template = self.__environment.get_template(template_filename)
# render template and save
with open(dest_filepath, "w", encoding="utf-8") as f:
f.write(template.render(**payload))
@@ -0,0 +1,20 @@
from dataclasses import dataclass
from pathlib import Path
from . import common
@dataclass(frozen=True)
class PkgConfigProperties:
pass
class PkgConfigRender:
__render: common.BaseRender
__properties: PkgConfigProperties
def __init__(self, properties: PkgConfigProperties) -> None:
self.__render = common.BaseRender()
self.__properties = properties
def render(self) -> None:
pass
@@ -0,0 +1,23 @@
{# Get the path to directory where current XXXConfig.cmake file is (i.e. /path/to/installation/lib/cmake/XXX) -#}
get_filename_component(PACKAGE_CMAKE_CONFIG_PATH "${CMAKE_CURRENT_LIST_FILE}" PATH)
{# Compute installation root directory (back to parent 3 times, i.e. /path/to/installation) -#}
get_filename_component(PACKAGE_PREFIX_DIR "${PACKAGE_CMAKE_CONFIG_PATH}/../../../" ABSOLUTE)
{# Setup library and header file paths -#}
set(MyRust_LIBRARY "${PACKAGE_PREFIX_DIR}/lib/ {{- artifact_filename -}} }")
set(MyRust_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")
{# Create modern CMake target (IMPORTED target) -#}
add_library(MyRust::MyRust SHARED IMPORTED)
set_target_properties(MyRust::MyRust PROPERTIES
IMPORTED_LOCATION "${MyRust_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MyRust_INCLUDE_DIR}"
)
{# Handle the Windows-specific case that .dll is seperated with .lib -#}
if(WIN32)
set_target_properties(MyRust::MyRust PROPERTIES
IMPORTED_IMPLIB "${PACKAGE_PREFIX_DIR}/lib/myrust.lib"
)
endif()
@@ -0,0 +1,14 @@
{# Setup package version -#}
set(PACKAGE_VERSION "1.2.3") # TODO: replace with jinja argument
{# Check whether user request EXACT match -#}
if(PACKAGE_FIND_VERSION_EXACT)
if(NOT "${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
endif()
else()
{# Check whether user requested version is <= current version (compatible with new version) -#}
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_UNSUPPORTED TRUE)
endif()
endif()
+13
View File
@@ -0,0 +1,13 @@
from pathlib import Path
def get_root_dir() -> Path:
return Path(__file__).resolve().parent
def get_templates_dir() -> Path:
return get_root_dir() / 'templates'