"""Helpers for preparing Docker build contexts. These functions exist to make Docker builds robust when running from: - a source checkout (may contain runtime artifacts like `knoe-db/data/`) - a packaged distribution (resources extracted to a temp dir) The Docker build context should only contain source-controlled inputs needed for the image build. Runtime data directories can contain broken symlinks, concurrently-mutating files, or large volumes that should never be copied. """ from __future__ import annotations import shutil from pathlib import Path from typing import Iterable DEFAULT_IGNORED_NAMES: tuple[str, ...] = ( # Runtime/stateful artifacts (can be huge and may contain dangling paths) "data", # Common VCS/cache noise ".git", "__pycache__", ".pytest_cache", ) def copy_build_context_dir( source_dir: Path, build_dir: Path, *, ignored_names: Iterable[str] = DEFAULT_IGNORED_NAMES, ) -> None: """Copy a directory tree into `build_dir` for use as a Docker build context. - Removes any existing `build_dir` first. - Preserves symlinks (does not follow them). - Ignores selected directory/file names anywhere in the tree. """ source_dir = Path(source_dir) build_dir = Path(build_dir) if source_dir.exists() is False: return if source_dir.resolve() == build_dir.resolve(): # Nothing to do; copying onto itself is both pointless and dangerous. return ignored_set = set(ignored_names) def _ignore(_dir: str, names: list[str]) -> set[str]: return {name for name in names if name in ignored_set} if build_dir.exists(): shutil.rmtree(build_dir) shutil.copytree( source_dir, build_dir, symlinks=True, ignore=_ignore, ignore_dangling_symlinks=True, )