← Back to issue list

Python plugin is not idempotent on iterative builds due to faulty `PATH` set up

View original Github issue

Metadata

Project
craft-parts
Number
#675
Type
issue
State
open
Author
NucciTheBoss
Labels
—
Created
Updated
Closed
—

Current evaluation

Python plugin iterative rebuilds can resolve python3 from the prior install venv, causing ensurepip to fail; the current legacy plugin still prepends install/bin to PATH and needs triage.

Suggested action: needs triage

Reason: The issue has no labels, assignee, or maintainer assessment; its only comment is an automated notice that internal ticket CRAFT-2923 was created. The exact two-pack reproducer and ensurepip trace identify the interpreter selection failure, while current BasePythonPlugin still prepends the install bin directory and does not clean an existing venv, so it remains a valid regression target.

Evaluated by: mr-cal-bot

Impact: 58 Quick Win: 27 Complexity: 47 Confidence: 92 Actionability: 88

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: ![image](https://github.com/canonical/craft-parts/assets/40342202/88d59805-8d4e-4ca0-801a-9478e85dc9be) 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

Date Model Scores Action Summary
stealth/space-bunny-alpha
Impact: 58
Quick Win: 27
Complexity: 47
Confidence: 92
Actionability: 88
needs triage Python plugin iterative rebuilds can resolve python3 from the prior install venv, causing ensurepip to fail; the current legacy plugin still prepends install/bin to PATH and needs triage.
qwen/qwen3.6-35b-a3b
Impact: 70
Quick Win: 34
Complexity: 40
Confidence: 85
Actionability: 80
keep open Python plugin fails on iterative builds because PATH prepends install dir, causing venv python to shadow system python and break venv recreation
qwen/qwen3.8-27b
Impact: 60
Quick Win: 27
Staleness: 75
Complexity: 55
Confidence: 70
Support Request: 5
needs triage Python plugin PATH setup causes non-idempotent iterative builds: on rebuild, the venv python in the install dir shadows the build-package python, breaking venv creation. Untriaged since filing 913 days ago; code refactored but core PATH logic unchanged.
qwen/qwen3.6-35b-a3b
Staleness: 85
Complexity: 45
Confidence: 75
Support Request: 15
needs triage Python plugin fails on iterative snap builds due to PATH manipulation causing incorrect interpreter selection. Currently unlabelled and awaiting maintainer triage after over a year of inactivity.
qwen3.6-35b-a3b-mtp-q6
Staleness: 95
Complexity: 40
Confidence: 75
Support Request: 5
close stale Python plugin fails on iterative builds due to PATH and virtualenv conflicts. Open for over two years with no maintainer response or labels.

Update history

No update history recorded yet.

Related work

  • Related To: canonical/craft-parts#1264 (confidence 90%)

    Introduced the python-v2 plugin, which avoids virtual environments, but this does not fix the reported legacy Python plugin.

Related issues

No related issues found above the similarity threshold.