Python plugin is not idempotent on iterative builds due to faulty `PATH` set up
Metadata
Current evaluation
No evaluation has been recorded for this issue yet.
Issue body
### Bug Description
The Python plugin is not idempotent. This causes challenges when building new snaps, where a snap developer needs to build their package iteratively due to having multiple parts in their _snapcraft.yaml_ file.
For example, I am building a snap that requires me to bundle several different services together to create one unified computing package. I'm using Python-based hooks since I need more advanced functionality (and are easier to maintain) than POSIX shell-based hooks. However, after I build my snap successfully on the first iteration, I cannot make any changes to the snap and then repack. `snapcraft` fails with the following error:

So why is it that the snap succeeds on the first pack, but not subsequent packs if the developer has made changes to a certain part? Turns out the issue is in the _part/<part-name>/run/build.sh_ script. The problem is right here in the source code for _python_plugin.py_:
https://github.com/canonical/craft-parts/blob/c11570597d4a397aba1744fa08adcd344682246e/craft_parts/plugins/python_plugin.py#L71-L91
On the first pack of a snap, `${PARTS_PYTHON_INTERPRETER}` is set to `python3` and path is modified to become `${CRAFT_PART_INSTALL}/bin:${PATH}`. This is fine on the first execution because the `venv` that is created by the _build.sh_ script uses the python3 brought in as a build package. IIRC this interpreter path is _usr/bin/python3_.
However, this prepend modification to the `${PATH}` environment is not good on subsequent packs of the snap. Since the virtualenv now exists in `${CRAFT_PART_INSTALL}` - it didn't on the first pack of the snap - the `${PARTS_PYTHON_INTERPRETER}` now becomes `${CRAFT_PART_INSTALL}/bin/python3`, not the python3 provided by the build package. I assume that this python3 interpreter provided by the virtualenv is missing some dependencies from the build package (I believe it to be setuptools), so the part using the Python plugin will crash and burn on the subsequent pack of the snap package.
__Potential fix:__ Looks like the script _build.sh_ needs to be prepended with a conditional that cleans up the virtual environment if it already exists in `${CRAFT_PART_INSTALL}` so that the correct python3 interpreter is used when creating the virtualenv.
### To Reproduce
1. _Assume that you already have `snapcraft` and `lxd` installed on your host..._
2. Clone my snap: `git clone git@github.com:charmed-hpc/ondemand-snap.git`
3. Pack the snap: `snapcraft -v pack`
4. Change something in _snapcraft.yaml_ or make a modification to one of the overlay files.
5. Pack the snap again: `snapcraft -v pack`
6. See error.
### part yaml
```shell
# This is part is from a classic snap.
snap-hooks:
source: .
plugin: python
build-attributes: [enable-patchelf]
python-requirements: [requirements.txt]
build-packages:
- python3-setuptools
stage-packages:
- libpython3.10-stdlib
- libpython3.10-minimal
- python3-venv
- python3.10-minimal
override-build: |
craftctl default
snap-helpers write-hooks
```
### Relevant log output
```shell
# snapcraft log
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.448 Updating build for snap-hooks ('PULL' step changed)
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.448 execute action snap-hooks:Action(part_name='snap-hooks', step=Step.BUILD, action_type=ActionType.UPDATE, reason="'PULL' step changed", project_vars=None, properties=ActionProperties(changed_files=[], changed_dirs=['ondemandhelpers']))
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.449 ignore patterns: []
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.502 updated files: set()
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.502 updated directories: {'ondemandhelpers'}
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.503 remove directory /root/parts/snap-hooks/install
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.546 load state file: /root/parts/snap-hooks/state/pull
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.648 fix artifacts: unpack_dir='/root/parts/snap-hooks/install'
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:27.827 :: + craftctl default
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:28.428 Executing PosixPath('/root/parts/snap-hooks/run/build.sh')
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:28.429 :: + python3 -m venv /root/parts/snap-hooks/install
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:29.244 :: Error: Command '['/root/parts/snap-hooks/install/bin/python3', '-m', 'ensurepip', '--upgrade', '--default-pip']' returned non-zero exit status 1.
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:29.249 :: error: Failed to run the build script for part 'snap-hooks'.
2024-03-01 09:52:41.846 :: 2024-03-01 09:52:29.450 'override-build' in part 'snap-hooks' failed with code 1.
2024-03-01 09:52:41.846 :: Review the scriptlet and make sure it's correct.
# Error output from failed command in _build.sh_
# Raised when ${CRAFT_PART_INSTALL}/bin/python3 -m venv ${CRAFT_PART_INSTALL} is executed.
Traceback (most recent call last):
File "<string>", line 6, in <module>
File "/root/stage/usr/lib/python3.10/runpy.py", line 224, in run_module
return _run_module_code(code, init_globals, run_name, mod_spec)
File "/root/stage/usr/lib/python3.10/runpy.py", line 96, in _run_module_code
_run_code(code, mod_globals, init_globals,
File "/root/stage/usr/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/__main__.py", line 29, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/cli/main.py", line 9, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/cli/autocompletion.py", line 10, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/cli/main_parser.py", line 8, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/cli/cmdoptions.py", line 23, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/cli/parser.py", line 12, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/configuration.py", line 26, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/utils/logging.py", line 27, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/utils/misc.py", line 39, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/locations/__init__.py", line 14, in <module>
File "/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl/pip/_internal/locations/_distutils.py", line 9, in <module>
ModuleNotFoundError: No module named 'distutils.cmd'
Traceback (most recent call last):
File "/root/stage/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/root/stage/usr/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/root/stage/usr/lib/python3.10/ensurepip/__main__.py", line 5, in <module>
sys.exit(ensurepip._main())
File "/root/stage/usr/lib/python3.10/ensurepip/__init__.py", line 320, in _main
return _bootstrap(
File "/root/stage/usr/lib/python3.10/ensurepip/__init__.py", line 236, in _bootstrap
return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)
File "/root/stage/usr/lib/python3.10/ensurepip/__init__.py", line 132, in _run_pip
return subprocess.run(cmd, check=True).returncode
File "/root/stage/usr/lib/python3.10/subprocess.py", line 526, in run
raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['/root/parts/snap-hooks/install/bin/python3', '-W', 'ignore::DeprecationWarning', '-c', '\nimport runpy\nimport sys\nsys.path = [\'/tmp/tmp929m8vwi/pip-22.0.2-py3-none-any.whl\', \'/tmp/tmp929m8vwi/setuptools-59.6.0-py3-none-any.whl\'] + sys.path\nsys.argv[1:] = [\'install\', \'--no-cache-dir\', \'--no-index\', \'--find-links\', \'/tmp/tmp929m8vwi\', \'--upgrade\', \'setuptools\', \'pip\']\nrunpy.run_module("pip", run_name="__main__", alter_sys=True)\n']' returned non-zero exit status 1.
```
Evaluation history
No evaluation history available.