From 16fe8fe74fb35f05a29167901813854fe9da2c79 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Wed, 21 Aug 2024 15:07:43 -0400 Subject: [PATCH] experimental: uv support for Python plugins --- craft_parts/plugins/base.py | 43 ++++++++++++++- craft_parts/plugins/poetry_plugin.py | 1 + craft_parts/plugins/python_plugin.py | 1 + pyproject.toml | 10 ++++ tests/integration/plugins/test_poetry.py | 53 +++++++++++++----- tests/integration/plugins/test_python.py | 36 ++++++++----- tests/unit/plugins/test_base.py | 68 ++++++++++++++++++----- tests/unit/plugins/test_poetry_plugin.py | 10 ++-- tests/unit/plugins/test_python_plugin.py | 69 ++++++++++++++---------- 9 files changed, 216 insertions(+), 75 deletions(-) diff --git a/craft_parts/plugins/base.py b/craft_parts/plugins/base.py index 9cf8d1bbc..440dbcbca 100644 --- a/craft_parts/plugins/base.py +++ b/craft_parts/plugins/base.py @@ -136,18 +136,45 @@ class BasePythonPlugin(Plugin): Provides common methods for dealing with Python items. """ + def __init__( + self, *, properties: PluginProperties, part_info: infos.PartInfo + ) -> None: + super().__init__(properties=properties, part_info=part_info) + use_uv_attr_name = f"{properties.plugin}_use_uv" + if not hasattr(properties, use_uv_attr_name): + raise AttributeError( + f"Plugin properties requires a {use_uv_attr_name!r} property" + ) + + @property + def _use_uv(self) -> bool: + """Whether the plugin should use uv rather than venv and pip.""" + return getattr(self._options, f"{self._options.plugin}_use_uv", False) + @override def get_build_snaps(self) -> set[str]: """Return a set of required snaps to install in the build environment.""" + if self._use_uv: + return {"astral-uv"} return set() + @override + def get_pull_commands(self) -> list[str]: + commands = super().get_pull_commands() + if self._use_uv: + commands.append("snap alias astral-uv.uv uv || true") + return commands + @override def get_build_packages(self) -> set[str]: """Return a set of required packages to install in the build environment. Child classes that need to override this should extend the returned set. """ - return {"findutils", "python3-dev", "python3-venv"} + packages = {"findutils", "python3-dev"} + if not self._use_uv: + packages.add("python3-venv") + return packages @override def get_build_environment(self) -> dict[str, str]: @@ -178,8 +205,18 @@ def _get_venv_directory(self) -> pathlib.Path: def _get_create_venv_commands(self) -> list[str]: """Get the commands for setting up the virtual environment.""" venv_dir = self._part_info.part_install_dir + venv_commands = ( + [ + f'uv venv ${{PARTS_PYTHON_VENV_ARGS}} "{venv_dir}"', + f'export UV_PYTHON="{venv_dir}/bin/${{PARTS_PYTHON_INTERPRETER}}"', + ] + if self._use_uv + else [ + f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{venv_dir}"' + ] + ) return [ - f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{venv_dir}"', + *venv_commands, f'PARTS_PYTHON_VENV_INTERP_PATH="{venv_dir}/bin/${{PARTS_PYTHON_INTERPRETER}}"', ] @@ -286,6 +323,8 @@ def _get_script_interpreter(self) -> str: def _get_pip(self) -> str: """Get the pip command to use.""" + if self._use_uv: + return "uv pip" return f"{self._part_info.part_install_dir}/bin/pip" @abc.abstractmethod diff --git a/craft_parts/plugins/poetry_plugin.py b/craft_parts/plugins/poetry_plugin.py index acf0ee4bd..83522e156 100644 --- a/craft_parts/plugins/poetry_plugin.py +++ b/craft_parts/plugins/poetry_plugin.py @@ -41,6 +41,7 @@ class PoetryPluginProperties(PluginProperties, frozen=True): title="Optional dependency groups", description="optional dependency groups to include when installing.", ) + poetry_use_uv: bool = False # part properties required by the plugin source: str # pyright: ignore[reportGeneralTypeIssues] diff --git a/craft_parts/plugins/python_plugin.py b/craft_parts/plugins/python_plugin.py index b7b170397..5c0d5bda5 100644 --- a/craft_parts/plugins/python_plugin.py +++ b/craft_parts/plugins/python_plugin.py @@ -31,6 +31,7 @@ class PythonPluginProperties(PluginProperties, frozen=True): python_requirements: list[str] = [] python_constraints: list[str] = [] python_packages: list[str] = ["pip", "setuptools", "wheel"] + python_use_uv: bool = False # part properties required by the plugin source: str # pyright: ignore[reportGeneralTypeIssues] diff --git a/pyproject.toml b/pyproject.toml index 62669ad88..eae58c0a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,16 @@ lint.ignore = [ [tool.ruff.lint.flake8-annotations] allow-star-arg-any = true +[tool.ruff.lint.pydocstyle] +ignore-decorators = [ # Functions with these decorators don't have to have docstrings. + "typing.overload", # Default configuration + # The next four are all variations on override, so child classes don't have to repeat parent classes' docstrings. + "overrides.override", + "overrides.overrides", + "typing.override", + "typing_extensions.override", +] + [tool.ruff.lint.pylint] max-args = 8 max-branches = 16 diff --git a/tests/integration/plugins/test_poetry.py b/tests/integration/plugins/test_poetry.py index 42ca29775..ac926e2af 100644 --- a/tests/integration/plugins/test_poetry.py +++ b/tests/integration/plugins/test_poetry.py @@ -36,14 +36,23 @@ def teardown_module(): plugins.unregister_all() +@pytest.fixture(params=[False, True]) +def use_uv(request): + return request.param + + @pytest.fixture(params=["test_poetry"]) def source_directory(request): return pathlib.Path(__file__).parent / request.param @pytest.fixture -def poetry_part(source_directory): - return {"source": str(source_directory), "plugin": "poetry"} +def poetry_part(source_directory, use_uv): + return { + "source": str(source_directory), + "plugin": "poetry", + "poetry-use-uv": use_uv, + } @pytest.fixture @@ -201,13 +210,19 @@ def _get_script_interpreter(self) -> str: with lf.action_executor() as ctx: ctx.execute(actions) - primed_script = pathlib.Path(lf.project_info.prime_dir, "bin/pip") + primed_script = pathlib.Path(lf.project_info.prime_dir, "bin/mytest") assert primed_script.open().readline().rstrip() == "#!/my/script/interpreter" -def test_find_payload_python_bad_version(new_dir, partitions, parts_dict, poetry_part): +def test_find_payload_python_bad_version( + new_dir, partitions, parts_dict, poetry_part, use_uv +): """Test that the build fails if a payload interpreter is needed but it's the wrong Python version.""" + if use_uv: + poetry_part["build-environment"] = [ + {"PARTS_PYTHON_VENV_ARGS": "--allow-existing"} + ] poetry_part["override-build"] = textwrap.dedent( f"""\ # Put a binary called "python3.3" in the payload @@ -225,8 +240,7 @@ def _get_system_python_interpreter(self) -> str | None: plugins.register({"poetry": MyPoetryPlugin}) - real_python = pathlib.Path(sys.executable).resolve() - real_basename = real_python.name + pathlib.Path(sys.executable).resolve() lf = LifecycleManager( parts_dict, @@ -243,8 +257,7 @@ def _get_system_python_interpreter(self) -> str | None: output = out.read_text() expected_text = textwrap.dedent( - f"""\ - Looking for a Python interpreter called "{real_basename}" in the payload... + """\ Python interpreter not found in payload. No suitable Python interpreter found, giving up. """ @@ -252,9 +265,25 @@ def _get_system_python_interpreter(self) -> str | None: assert expected_text in output -def test_find_payload_python_good_version(new_dir, partitions, parts_dict, poetry_part): +def test_find_payload_python_good_version( + new_dir, partitions, parts_dict, poetry_part, use_uv +): """Test that the build succeeds if a payload interpreter is needed, and it's the right Python version.""" + if use_uv: + # uv can get multiple Python versions - ensure we're using the same version. + python_version_str = ".".join(str(i) for i in sys.version_info[:3]) + poetry_part["build-environment"] = [ + { + "PARTS_PYTHON_VENV_ARGS": " ".join( + [ + "--allow-existing", + f"--python={python_version_str}", + "--python-preference=only-system", + ] + ) + } + ] real_python = pathlib.Path(sys.executable).resolve() real_basename = real_python.name @@ -284,9 +313,5 @@ def test_find_payload_python_good_version(new_dir, partitions, parts_dict, poetr output = out.read_text() payload_python = (install_dir / f"usr/bin/{real_basename}").resolve() - expected_text = textwrap.dedent( - f"""\ - Found interpreter in payload: "{payload_python}" - """ - ) + expected_text = f'Found interpreter in payload: "{payload_python}"' assert expected_text in output diff --git a/tests/integration/plugins/test_python.py b/tests/integration/plugins/test_python.py index 7723e52e8..49c1654fc 100644 --- a/tests/integration/plugins/test_python.py +++ b/tests/integration/plugins/test_python.py @@ -34,7 +34,12 @@ def teardown_module(): plugins.unregister_all() -def test_python_plugin(new_dir, partitions): +@pytest.fixture(params=[False, True]) +def use_uv(request): + return request.param + + +def test_python_plugin(new_dir, partitions, use_uv): """Prime a simple python source.""" source_location = Path(__file__).parent / "test_python" @@ -44,6 +49,7 @@ def test_python_plugin(new_dir, partitions): foo: plugin: python source: {source_location} + python-use-uv: {use_uv} """ ) parts = yaml.safe_load(parts_yaml) @@ -61,7 +67,7 @@ def test_python_plugin(new_dir, partitions): assert primed_script.open().readline().rstrip() == "#!/usr/bin/env python3" -def test_python_plugin_with_pyproject_toml(new_dir, partitions): +def test_python_plugin_with_pyproject_toml(new_dir, partitions, use_uv): """Prime a simple python source.""" source_location = Path(__file__).parent / "test_python_pyproject_toml" @@ -71,6 +77,7 @@ def test_python_plugin_with_pyproject_toml(new_dir, partitions): foo: plugin: python source: {source_location} + python-use-uv: {use_uv} """ ) parts = yaml.safe_load(parts_yaml) @@ -91,14 +98,15 @@ def test_python_plugin_with_pyproject_toml(new_dir, partitions): assert primed_script.open().readline().rstrip() == "#!/usr/bin/env python3" -def test_python_plugin_symlink(new_dir, partitions): +def test_python_plugin_symlink(new_dir, partitions, use_uv): """Run in the standard scenario with no overrides.""" parts_yaml = textwrap.dedent( - """\ + f"""\ parts: foo: plugin: python source: . + python-use-uv: {use_uv} """ ) parts = yaml.safe_load(parts_yaml) @@ -120,7 +128,7 @@ def test_python_plugin_symlink(new_dir, partitions): assert os.path.basename(python_link).startswith("python3") -def test_python_plugin_override_get_system_interpreter(new_dir, partitions): +def test_python_plugin_override_get_system_interpreter(new_dir, partitions, use_uv): """Override the system interpreter, link should use it.""" class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): @@ -131,11 +139,12 @@ def _get_system_python_interpreter(self) -> str | None: plugins.register({"python": MyPythonPlugin}) parts_yaml = textwrap.dedent( - """\ + f"""\ parts: foo: plugin: python source: . + python-use-uv: {use_uv} """ ) parts = yaml.safe_load(parts_yaml) @@ -155,7 +164,7 @@ def _get_system_python_interpreter(self) -> str | None: @pytest.mark.parametrize("remove_symlinks", [(True), (False)]) def test_python_plugin_no_system_interpreter( - new_dir, partitions, remove_symlinks: bool # noqa: FBT001 + new_dir, partitions, use_uv, remove_symlinks: bool # noqa: FBT001 ): """Check that the build fails if a payload interpreter is needed but not found.""" @@ -173,11 +182,12 @@ def _should_remove_symlinks(self) -> bool: plugins.register({"python": MyPythonPlugin}) parts_yaml = textwrap.dedent( - """\ + f"""\ parts: foo: plugin: python source: . + python-use-uv: {use_uv} """ ) parts = yaml.safe_load(parts_yaml) @@ -191,7 +201,7 @@ def _should_remove_symlinks(self) -> bool: ctx.execute(actions) -def test_python_plugin_remove_symlinks(new_dir, partitions): +def test_python_plugin_remove_symlinks(new_dir, partitions, use_uv): """Override symlink removal.""" class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): @@ -223,7 +233,7 @@ def _should_remove_symlinks(self) -> bool: assert python_link.exists() is False -def test_python_plugin_fix_shebangs(new_dir, partitions): +def test_python_plugin_fix_shebangs(new_dir, partitions, use_uv): """Check if shebangs are properly fixed in scripts.""" parts_yaml = textwrap.dedent( """\ @@ -247,7 +257,7 @@ def test_python_plugin_fix_shebangs(new_dir, partitions): assert primed_script.open().readline().rstrip() == "#!/usr/bin/env python3" -def test_python_plugin_override_shebangs(new_dir, partitions): +def test_python_plugin_override_shebangs(new_dir, partitions, use_uv): """Override what we want in script shebang lines.""" class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): @@ -294,7 +304,7 @@ def _get_script_interpreter(self) -> str: """ -def test_find_payload_python_bad_version(new_dir, partitions): +def test_find_payload_python_bad_version(new_dir, partitions, use_uv): """Test that the build fails if a payload interpreter is needed but it's the wrong Python version.""" @@ -337,7 +347,7 @@ def _get_system_python_interpreter(self) -> str | None: assert expected_text in output -def test_find_payload_python_good_version(new_dir, partitions): +def test_find_payload_python_good_version(new_dir, partitions, use_uv): """Test that the build succeeds if a payload interpreter is needed, and it's the right Python version.""" diff --git a/tests/unit/plugins/test_base.py b/tests/unit/plugins/test_base.py index 7f33fbf3e..ea16330ff 100644 --- a/tests/unit/plugins/test_base.py +++ b/tests/unit/plugins/test_base.py @@ -87,10 +87,14 @@ class FaultyPlugin(Plugin): FaultyPlugin(properties=None, part_info=part_info) # type: ignore[reportGeneralTypeIssues] +class FooPythonPluginProperties(FooPluginProperties, frozen=True): + foo_use_uv: bool = False + + class FooPythonPlugin(BasePythonPlugin): """A plugin for testing the base Python plugin.""" - properties_class = FooPluginProperties + properties_class = FooPythonPluginProperties def _get_package_install_commands(self) -> list[str]: return ["echo 'This is where I put my install commands... if I had any!'"] @@ -107,12 +111,19 @@ def python_plugin(new_dir): return FooPythonPlugin(properties=properties, part_info=part_info) -def test_python_get_build_packages(python_plugin): - assert python_plugin.get_build_packages() == { - "findutils", - "python3-venv", - "python3-dev", - } +@pytest.mark.parametrize( + ("use_uv", "expected_packages"), + [ + (False, {"findutils", "python3-venv", "python3-dev"}), + (True, {"findutils", "python3-dev"}), + ], +) +def test_python_get_build_packages(python_plugin, use_uv, expected_packages): + python_plugin._options = python_plugin._options.model_copy( + update={"foo_use_uv": use_uv} + ) + + assert python_plugin.get_build_packages() == expected_packages def test_python_get_build_environment(new_dir, python_plugin): @@ -123,11 +134,44 @@ def test_python_get_build_environment(new_dir, python_plugin): } -def test_python_get_create_venv_commands(new_dir, python_plugin: FooPythonPlugin): - assert python_plugin._get_create_venv_commands() == [ - f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', - f'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', - ] +@pytest.mark.parametrize("use_uv", [False, True]) +def test_python_use_uv(python_plugin: FooPythonPlugin, use_uv): + python_plugin._options = python_plugin._options.model_copy( + update={"foo_use_uv": use_uv} + ) + + assert python_plugin._use_uv == use_uv + + +@pytest.mark.parametrize( + ("use_uv", "expected_template"), + [ + ( + False, + [ + '"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', + 'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + ], + ), + ( + True, + [ + 'uv venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', + 'export UV_PYTHON="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + 'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + ], + ), + ], +) +def test_python_get_create_venv_commands( + new_dir, python_plugin: FooPythonPlugin, use_uv, expected_template +): + expected = [i.format(new_dir=new_dir) for i in expected_template] + python_plugin._options = python_plugin._options.model_copy( + update={"foo_use_uv": use_uv} + ) + + assert python_plugin._get_create_venv_commands() == expected def test_python_get_find_python_interpreter_commands( diff --git a/tests/unit/plugins/test_poetry_plugin.py b/tests/unit/plugins/test_poetry_plugin.py index d740f017d..408ecab51 100644 --- a/tests/unit/plugins/test_poetry_plugin.py +++ b/tests/unit/plugins/test_poetry_plugin.py @@ -18,7 +18,6 @@ from textwrap import dedent import pytest -import pytest_check # type: ignore[import-untyped] from craft_parts import Part, PartInfo, ProjectInfo from craft_parts.plugins.poetry_plugin import PoetryPlugin from pydantic import ValidationError @@ -191,10 +190,7 @@ def test_call_should_remove_symlinks(plugin, new_dir, mocker): build_commands = plugin.get_build_commands() - pytest_check.is_in( + assert build_commands[-2:] == [ f"echo Removing python symlinks in {plugin._part_info.part_install_dir}/bin", - build_commands, - ) - pytest_check.is_in( - f'rm "{plugin._part_info.part_install_dir}"/bin/python*', build_commands - ) + f'rm "{plugin._part_info.part_install_dir}"/bin/python*', + ] diff --git a/tests/unit/plugins/test_python_plugin.py b/tests/unit/plugins/test_python_plugin.py index 68b9faf2d..b3d130624 100644 --- a/tests/unit/plugins/test_python_plugin.py +++ b/tests/unit/plugins/test_python_plugin.py @@ -23,19 +23,22 @@ from pydantic import ValidationError +@pytest.fixture(params=[False, True]) +def use_uv(request): + return request.param + + @pytest.fixture -def plugin(new_dir): - properties = PythonPlugin.properties_class.unmarshal({"source": "."}) +def plugin(new_dir, use_uv): + properties = PythonPlugin.properties_class.unmarshal( + {"source": ".", "python-use-uv": use_uv} + ) info = ProjectInfo(application_name="test", cache_dir=new_dir) part_info = PartInfo(project_info=info, part=Part("p1", {})) return PythonPlugin(properties=properties, part_info=part_info) -def test_get_build_packages(plugin): - assert plugin.get_build_packages() == {"findutils", "python3-venv", "python3-dev"} - - def test_get_build_environment(plugin, new_dir): assert plugin.get_build_environment() == { "PATH": f"{new_dir}/parts/p1/install/bin:${{PATH}}", @@ -108,17 +111,35 @@ def get_build_commands( ] -def test_get_build_commands(plugin, new_dir): - assert plugin.get_build_commands() == [ - f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', - f'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', - f"{new_dir}/parts/p1/install/bin/pip install -U pip setuptools wheel", - f"[ -f setup.py ] || [ -f pyproject.toml ] && {new_dir}/parts/p1/install/bin/pip install -U .", - *get_build_commands(new_dir), - ] - +@pytest.mark.parametrize( + ("uv", "expected_template"), + [ + ( + False, + [ + '"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', + 'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + "{new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U pip 'some-pkg; sys_platform != '\"'\"'win32'\"'\"''", + "{new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U -r 'requirements.txt'", + "[ -f setup.py ] || [ -f pyproject.toml ] && {new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U .", + ], + ), + ( + True, + [ + 'uv venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', + 'export UV_PYTHON="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + 'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', + "uv pip install -c 'constraints.txt' -U pip 'some-pkg; sys_platform != '\"'\"'win32'\"'\"''", + "uv pip install -c 'constraints.txt' -U -r 'requirements.txt'", + "[ -f setup.py ] || [ -f pyproject.toml ] && uv pip install -c 'constraints.txt' -U .", + ], + ), + ], +) +def test_get_build_commands_with_all_properties(new_dir, uv, expected_template): + expected = [s.format(new_dir=new_dir) for s in expected_template] -def test_get_build_commands_with_all_properties(new_dir): info = ProjectInfo(application_name="test", cache_dir=new_dir) part_info = PartInfo(project_info=info, part=Part("p1", {})) properties = PythonPlugin.properties_class.unmarshal( @@ -127,17 +148,14 @@ def test_get_build_commands_with_all_properties(new_dir): "python-constraints": ["constraints.txt"], "python-requirements": ["requirements.txt"], "python-packages": ["pip", "some-pkg; sys_platform != 'win32'"], + "python-use-uv": uv, } ) python_plugin = PythonPlugin(part_info=part_info, properties=properties) assert python_plugin.get_build_commands() == [ - f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', - f'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', - f"{new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U pip 'some-pkg; sys_platform != '\"'\"'win32'\"'\"''", - f"{new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U -r 'requirements.txt'", - f"[ -f setup.py ] || [ -f pyproject.toml ] && {new_dir}/parts/p1/install/bin/pip install -c 'constraints.txt' -U .", + *expected, *get_build_commands(new_dir), ] @@ -186,10 +204,7 @@ def test_call_should_remove_symlinks(plugin, new_dir, mocker): return_value=True, ) - assert plugin.get_build_commands() == [ - f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"', - f'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"', - f"{new_dir}/parts/p1/install/bin/pip install -U pip setuptools wheel", - f"[ -f setup.py ] || [ -f pyproject.toml ] && {new_dir}/parts/p1/install/bin/pip install -U .", - *get_build_commands(new_dir, should_remove_symlinks=True), + assert plugin.get_build_commands()[-2:] == [ + f"echo Removing python symlinks in {new_dir}/parts/p1/install/bin", + f'rm "{new_dir}/parts/p1/install"/bin/python*', ]