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."""importshlexfromtextwrapimportdedentfromtypingimportLiteralfromtyping_extensionsimportoverridefromcraft_parts.pluginsimportPlugin,PluginProperties
[docs]classPythonPluginProperties(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 pluginsource:str# pyright: ignore[reportGeneralTypeIssues]
[docs]classPythonPlugin(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]@overridedefget_build_snaps(self)->set[str]:"""Return a set of required snaps to install in the build environment."""returnset()
[docs]@overridedefget_build_packages(self)->set[str]:"""Return a set of required packages to install in the build environment."""returnset()
[docs]@overridedefget_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]@overridedefget_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 requirementsrequirements=" ".join(f"-r {req}"forreqinself._options.python_requirements)pip_lines.append(f'REQUIREMENTS="{requirements}"')# Then any extra packagespackages=" ".join(shlex.quote(pkg)forpkginself._options.python_packages)pip_lines.append(f'PACKAGES="{packages}"')# Then finally the project in the source itself, if it existsproject=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 heresitecustomize=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]