Source code for aws_lbd_art_builder_core.source.builder

# -*- coding: utf-8 -*-

"""
Build-step functions for Lambda source deployment artifacts.

Each function installs the current Python package into a caller-supplied
target directory (``dir_lambda_source_build``) using a different tool:

- :func:`build_source_dir_using_pip` — ``pip install --no-dependencies --target``
- :func:`build_source_dir_using_uv`  — ``uv pip install --no-deps --target``
- :func:`create_source_zip`          — zip the build dir and return its SHA256

All three functions are intentionally thin: they accept plain :class:`~pathlib.Path`
arguments rather than a :class:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout`
object so that the build logic stays decoupled from any particular path convention.
Callers that use ``SourcePathLayout`` pass ``path_layout.dir_build`` /
``path_layout.path_source_zip`` as arguments.

Key assumptions
---------------
- The Lambda entry point lives **inside** the installed package, not as a
  separate file outside it.
- Only ``pyproject.toml``-based projects are supported (``setup.py`` is not).
- The build backend configured in ``[build-system]`` (typically setuptools)
  controls which packages and data files are included; ``packages.find`` /
  ``package-data`` settings in ``pyproject.toml`` are fully respected.
- ``zip`` (the system binary) must be available on ``PATH`` for
  :func:`create_source_zip`.
"""

import glob
import subprocess
from pathlib import Path

from ..vendor.better_pathlib import temp_cwd
from ..vendor.hashes import hashes
from ..typehint import T_PRINTER
from ..utils import clean_build_directory


[docs] def build_source_dir_using_pip( path_bin_pip: Path, path_pyproject_toml: Path, dir_lambda_source_build: Path, skip_prompt: bool = False, verbose: bool = True, printer: T_PRINTER = print, ): """ Install the current Python package into a target directory using pip. **Why pip install instead of copying files?** pip resolves package metadata, entry points, and import paths exactly as the Lambda runtime expects. Plain file copying can silently break relative imports or miss ``*.dist-info`` records that some libraries rely on at runtime. ``temp_cwd`` is intentionally **not** used here: ``path_pyproject_toml`` is always an absolute path so pip does not need a specific working directory to locate the project. The build directory is cleaned before installation to guarantee a reproducible, artefact-free starting state. :param path_bin_pip: pip executable, e.g. ``/path/to/.venv/bin/pip`` :param path_pyproject_toml: ``pyproject.toml`` of the project to install. Its parent directory is passed to ``pip install`` as the source. :param dir_lambda_source_build: Target directory for the installed files, i.e. :attr:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout.dir_build`. :param skip_prompt: When ``True``, wipe the build directory without asking for confirmation. :param verbose: When ``True``, print the pip command and its output. :param printer: Callable used for log output (default: :func:`print`). """ if verbose: # pragma: no cover printer("--- Building Lambda source dir using pip ...") printer(f"{path_bin_pip = !s}") printer(f"{path_pyproject_toml = !s}") printer(f"{dir_lambda_source_build = !s}") clean_build_directory( dir_build=dir_lambda_source_build, folder_alias="lambda source build folder", skip_prompt=skip_prompt, ) dir_workspace = path_pyproject_toml.parent args = [ f"{path_bin_pip}", "install", f"{dir_workspace}", "--no-dependencies", f"--target={dir_lambda_source_build}", ] if verbose is False: # pragma: no cover args.append("--disable-pip-version-check") args.append("--quiet") subprocess.run(args, check=True)
[docs] def build_source_dir_using_uv( path_bin_uv: Path, path_pyproject_toml: Path, dir_lambda_source_build: Path, skip_prompt: bool = False, verbose: bool = True, printer: T_PRINTER = print, ): """ Install the current Python package into a target directory using uv. ``uv pip install`` has its own wheel installer and does **not** require pip to be present in the virtual environment. It is otherwise equivalent to ``pip install --no-deps --target`` but noticeably faster. ``temp_cwd`` is intentionally **not** used: the project directory is passed as an explicit absolute path argument to uv, so the working directory is irrelevant. ``setup.py``-only projects are **not** supported; the project must have a ``pyproject.toml`` with a ``[build-system]`` table. The build directory is cleaned before installation to guarantee a reproducible, artefact-free starting state. :param path_bin_uv: uv executable, e.g. ``/path/to/.venv/bin/uv`` or the system-wide uv resolved via :func:`shutil.which`. :param path_pyproject_toml: ``pyproject.toml`` of the project to install. Its parent directory is passed to ``uv pip install`` as the source. :param dir_lambda_source_build: Target directory for the installed files, i.e. :attr:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout.dir_build`. :param skip_prompt: When ``True``, wipe the build directory without asking for confirmation. :param verbose: When ``True``, print the uv command and its output. :param printer: Callable used for log output (default: :func:`print`). """ if verbose: # pragma: no cover printer("--- Building Lambda source dir using uv ...") printer(f"{path_bin_uv = !s}") printer(f"{path_pyproject_toml = !s}") printer(f"{dir_lambda_source_build = !s}") clean_build_directory( dir_build=dir_lambda_source_build, folder_alias="lambda source build folder", skip_prompt=skip_prompt, ) args = [ f"{path_bin_uv}", "pip", "install", f"{path_pyproject_toml.parent}", "--no-deps", f"--target={dir_lambda_source_build}", ] if verbose is False: # pragma: no cover args.append("--quiet") subprocess.run(args, check=True)
[docs] def create_source_zip( dir_lambda_source_build: Path, path_source_zip: Path, verbose: bool = True, printer: T_PRINTER = print, ) -> str: """ Zip the Lambda source build directory and return its SHA256 hash. The zip is created with maximum compression (``-9``) and its root entries are the direct children of *dir_lambda_source_build*, so that the Lambda runtime can import them without any extra path prefix. ``path_source_zip`` **must not** be inside *dir_lambda_source_build*. Place it at :attr:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout.path_source_zip` (a sibling of the build directory) to avoid the zip accidentally archiving itself mid-creation. ``temp_cwd`` **is** required here: :func:`glob.glob` is CWD-relative and the ``zip`` command must run from inside *dir_lambda_source_build* so that archive entries carry relative (not absolute) paths. The SHA256 is computed over the *build directory* (not the zip file) so that the hash is stable regardless of zip metadata or timestamps. :param dir_lambda_source_build: Directory containing the installed package files, i.e. :attr:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout.dir_build`. :param path_source_zip: Output path for the zip archive, i.e. :attr:`~aws_lbd_art_builder_core.source.foundation.SourcePathLayout.path_source_zip`. :param verbose: When ``True``, print progress and the computed hash. :param printer: Callable used for log output (default: :func:`print`). :return: SHA256 hex digest of the source build directory. """ if verbose: # pragma: no cover printer("--- Creating Lambda source zip file ...") printer(f"{dir_lambda_source_build = !s}") printer(f"{path_source_zip = !s}") args = [ "zip", f"{path_source_zip}", "-r", "-9", ] if verbose is False: # pragma: no cover args.append("-q") # temp_cwd is required: glob("*") is CWD-relative and zip entries must be # relative paths so the Lambda runtime can find them at the root of the archive. with temp_cwd(dir_lambda_source_build): args.extend(glob.glob("*")) subprocess.run(args, check=True) source_sha256 = hashes.of_paths([dir_lambda_source_build]) if verbose: # pragma: no cover printer(f"{source_sha256 = }") return source_sha256