Source code for craft_parts.plugins.python_v2.python_plugin

# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2025 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Version 2 of the Python plugin."""

import shlex
from textwrap import dedent
from typing import Literal

from typing_extensions import override

from craft_parts.plugins import Plugin, PluginProperties


[docs] class PythonPluginProperties(PluginProperties, frozen=True): """The part properties used by the python plugin.""" plugin: Literal["python"] = "python" python_requirements: list[str] = [] python_packages: list[str] = [] # part properties required by the plugin source: str # pyright: ignore[reportGeneralTypeIssues]
[docs] class PythonPlugin(Plugin): """Python plugin, version 2. Unlike the original Python plugin, this one does *not* require the creation of virtual environments to work. Instead, it uses pip's support for both a) installing directly into the user's base dir and b) using a Python interpreter that is not the one running pip initially. This allows lighter payloads as the -venv package is not required, and interacts well with usrmerged directories. The downside is that the payload generated by the plugin assumes that the primed contents will include the Python interpreter; if this is not the case, the runtime will need to add the payload to Python's path (e.g. via PYTHONPATH). """ properties_class = PythonPluginProperties _options: PythonPluginProperties
[docs] @override def get_build_snaps(self) -> set[str]: """Return a set of required snaps to install in the build environment.""" return set()
[docs] @override def get_build_packages(self) -> set[str]: """Return a set of required packages to install in the build environment.""" return set()
[docs] @override def get_build_environment(self) -> dict[str, str]: """Return a dictionary with the environment to use in the build step.""" return { "PIP_USER": "1", "PYTHONUSERBASE": str(self._part_info.part_install_dir), "PIP_BREAK_SYSTEM_PACKAGES": "1", "PIP_PYTHON": "$(which python3)", }
[docs] @override def get_build_commands(self) -> list[str]: """Return a list of commands to run during the build step.""" # Disallow using the system's Python interpreter for now. The issue is that # using the system interpreter will make pip skip dependencies that are already # installed in the system, even though they won't be part of the part's files. python_check = dedent("""\ if [[ ${PIP_PYTHON} == /usr/bin/* ]]; then echo "Using the system Python interpreter is not supported." 1>&2 exit 1 fi """) pip_lines: list[str] = [] # First the requirements requirements = " ".join( f"-r {req}" for req in self._options.python_requirements ) pip_lines.append(f'REQUIREMENTS="{requirements}"') # Then any extra packages packages = " ".join(shlex.quote(pkg) for pkg in self._options.python_packages) pip_lines.append(f'PACKAGES="{packages}"') # Then finally the project in the source itself, if it exists project = dedent("""\ if [ -f setup.py -o -f pyproject.toml ]; then PACKAGES="${PACKAGES} ." fi """) pip_lines.append(project) pip_lines.append("pip install ${REQUIREMENTS} ${PACKAGES}") # Add a sitecustomize so that the bundled Python interpreter (if any) will # pick up the packages installed by pip here sitecustomize = dedent("""\ # Add a sitecustomize python_bin=${PIP_PYTHON} python_dir=python$($python_bin -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") mkdir -p ${CRAFT_PART_INSTALL}/lib/${python_dir} cat > ${CRAFT_PART_INSTALL}/lib/${python_dir}/sitecustomize.py << EOF import site import sys from pathlib import Path # Add the directory that contains the pip-installed packages. site_dir = Path(__file__).parent / "site-packages" site.addsitedir(str(site_dir)) EOF """) return [python_check, *pip_lines, sitecustomize]