From 61aa7d34d607be7df78995d0774f2f26e7ae2c3d Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Tue, 28 Apr 2026 15:19:32 +0200 Subject: [PATCH] Embed verbose debug log inside ZipPacker output ZipPacker now captures the full BAT_DEBUG log stream into an in-memory buffer for the lifetime of the pack and writes it as bat-debug.log inside the produced zip, so archives are self-describing for post-mortem debugging (which assets were found, where they were rewritten to, which files were missing, etc.). The capture forces the blender_asset_tracer logger to DEBUG only on a dedicated handler, so terminal verbosity is unchanged. The log is flushed into the closed zip via append mode in close(), which also covers post-transfer messages and KeyboardInterrupt / FileTransferError paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- blender_asset_tracer/pack/zipped.py | 113 +++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/blender_asset_tracer/pack/zipped.py b/blender_asset_tracer/pack/zipped.py index bb685b7..90aaf5e 100644 --- a/blender_asset_tracer/pack/zipped.py +++ b/blender_asset_tracer/pack/zipped.py @@ -22,8 +22,14 @@ Note: There is no official file name encoding for ZIP files. Expect trouble when you want to use the ZIP cross-platform and you have non-ASCII names. """ +import datetime +import io import logging import pathlib +import platform +import sys +import typing +import zipfile from . import Packer, transfer @@ -32,14 +38,119 @@ log = logging.getLogger(__name__) # Suffixes to store uncompressed in the zip. STORE_ONLY = {".jpg", ".jpeg", ".exr"} +# Filename used for the embedded debug log inside produced zips. +LOG_FILENAME_IN_ZIP = "bat-debug.log" + class ZipPacker(Packer): - """Creates a zipped BAT Pack instead of a directory.""" + """Creates a zipped BAT Pack instead of a directory. + + Captures the most verbose BAT log output in memory while the pack runs + and embeds it as ``bat-debug.log`` inside the produced zip, so the + archive is self-describing for post-mortem debugging. + """ + + def __init__( + self, + bfile: typing.Union[pathlib.Path, typing.List[pathlib.Path]], + project: pathlib.Path, + target: str, + *, + noop: bool = False, + compress: bool = False, + relative_only: bool = False, + keep_hierarchy: bool = False, + ) -> None: + super().__init__( + bfile, + project, + target, + noop=noop, + compress=compress, + relative_only=relative_only, + keep_hierarchy=keep_hierarchy, + ) + + self._log_buffer: io.StringIO = io.StringIO() + self._log_handler: typing.Optional[logging.Handler] = logging.StreamHandler( + self._log_buffer + ) + self._log_handler.setLevel(logging.DEBUG) + self._log_handler.setFormatter( + logging.Formatter( + fmt="%(asctime)s.%(msecs)03d %(levelname)-8s %(name)-45s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + + self._bat_logger: logging.Logger = logging.getLogger("blender_asset_tracer") + self._original_logger_level: int = self._bat_logger.level + + try: + self._bat_logger.setLevel(logging.DEBUG) + self._bat_logger.addHandler(self._log_handler) + except Exception: + self._bat_logger.removeHandler(self._log_handler) + self._bat_logger.setLevel(self._original_logger_level) + self._log_handler = None + raise + + self._log_pack_banner() + + def _log_pack_banner(self) -> None: + """Emit a header summarising the environment for the captured log.""" + try: + from blender_asset_tracer import __version__ as bat_version + except Exception: + bat_version = "unknown" + + log.info("=" * 78) + log.info("BAT Pack starting") + log.info("BAT version : %s", bat_version) + log.info( + "UTC timestamp : %s", + datetime.datetime.now(datetime.timezone.utc).isoformat(), + ) + log.info("Blend files : %s", self.blendfiles) + log.info("Project root : %s", self.project) + log.info("Target : %s", self.target) + log.info("noop : %s", self.noop) + log.info("relative_only : %s", self.relative_only) + log.info("keep_hierarchy : %s", self.keep_hierarchy) + log.info("sys.argv : %s", sys.argv) + log.info("Python : %s", sys.version.replace("\n", " ")) + log.info("Platform : %s", platform.platform()) + log.info("=" * 78) def _create_file_transferer(self) -> transfer.FileTransferer: target_path = pathlib.Path(self._target_path) return ZipTransferrer(target_path.absolute()) + def close(self) -> None: + """Flush the captured debug log into the zip, then run base cleanup.""" + if self._log_handler is None: + super().close() + return + + log.info("BAT Pack closing; flushing debug log to %s", LOG_FILENAME_IN_ZIP) + + self._bat_logger.removeHandler(self._log_handler) + self._bat_logger.setLevel(self._original_logger_level) + handler = self._log_handler + self._log_handler = None + + try: + handler.flush() + log_content = self._log_buffer.getvalue() + zippath = pathlib.Path(self.target) + if not self.noop and zippath.exists(): + with zipfile.ZipFile(str(zippath), "a") as outzip: + outzip.writestr(LOG_FILENAME_IN_ZIP, log_content) + except Exception: + log.exception("Failed to embed debug log into %s", self.target) + + super().close() + class ZipTransferrer(transfer.FileTransferer): """Creates a ZIP file instead of writing to a directory.