← 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
2024-03-01 19:14:20+00:00
Updated
2025-03-11 14:28:34+00:00
Closed

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: ![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

No evaluation history available.