Skip to content

Commit

Permalink
fix: fix Python venv in core24 classic snaps
Browse files Browse the repository at this point in the history
Changes to virtual environment handling made our default approach - creating a
venv in the build system and then just moving it into the snap - not work for
Python 3.12.

My understanding of the issue is that the "home" key in the "pyvenv.cfg" file
points to a path in the build system, which obviously does not exist at
snap-run-time. This apparently works fine *somehow* in core22 - likely there's
some fallback logic that makes the interpreter correctly find the bundled
Python libraries, but not in core24. So to address this we update this "home"
key to point to its final destination in the snap.

Fixes #4942
  • Loading branch information
tigarmo committed Jul 31, 2024
1 parent 1d17810 commit a2df2cd
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 6 deletions.
32 changes: 31 additions & 1 deletion snapcraft/parts/plugins/python_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
"""The Snapcraft Python plugin."""

import logging
from pathlib import Path
from typing import Optional

from craft_parts import errors
from craft_parts import errors, StepInfo
from craft_parts.plugins import python_plugin
from overrides import override

Expand Down Expand Up @@ -64,3 +65,32 @@ def _get_system_python_interpreter(self) -> Optional[str]:
confinement,
)
return interpreter

@classmethod
def post_prime(cls, step_info: StepInfo) -> None:
base = step_info.project_base

if base != "core24":
# Only fix pyvenv.cfg on core24 snaps
return

root_path: Path = step_info.prime_dir

pyvenv = root_path / "pyvenv.cfg"
if not pyvenv.is_file():
return

snap_path = Path(f"/snap/{step_info.project_name}/current")
new_home = f"home = {snap_path}"

candidates = (
step_info.part_install_dir,
step_info.stage_dir,
)

contents = pyvenv.read_text()
for candidate in candidates:
old_home = f"home = {candidate}"
contents = contents.replace(old_home, new_home)

pyvenv.write_text(contents)
12 changes: 12 additions & 0 deletions snapcraft/services/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def setup(self) -> None:
extra_build_snaps=project.get_extra_build_snaps(),
confinement=project.confinement,
project_base=project.base or "",
project_name=project.name,
)
callbacks.register_prologue(parts.set_global_environment)
callbacks.register_pre_step(parts.set_step_environment)
Expand All @@ -85,8 +86,19 @@ def setup(self) -> None:
@overrides
def post_prime(self, step_info: StepInfo) -> bool:
"""Run post-prime parts steps for Snapcraft."""
from snapcraft.parts import plugins

project = cast(models.Project, self._project)

part_name = step_info.part_name
plugin_name = project.parts[part_name]["plugin"]

# Handle plugin-specific prime fixes
if plugin_name == "python":
plugins.PythonPlugin.post_prime(step_info)

# Handle patch-elf

# do not use system libraries in classic confinement
use_system_libs = not bool(project.confinement == "classic")

Expand Down
3 changes: 3 additions & 0 deletions tests/spread/core24/python-hello/classic/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ parts:
hello:
plugin: python
source: src
python-packages:
- black
build-attributes:
- enable-patchelf
stage-packages:
- libpython3.12-minimal
- libpython3.12-stdlib
- python3.12-minimal
- python3.12-venv
- python3-minimal # (for the "python3" symlink)
5 changes: 4 additions & 1 deletion tests/spread/core24/python-hello/src/hello/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
import black


def main():
print("hello world")
print(f"hello world! black version: {black.__version__}")
2 changes: 2 additions & 0 deletions tests/spread/core24/python-hello/strict/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ parts:
hello:
plugin: python
source: src
python-packages:
- black
7 changes: 6 additions & 1 deletion tests/spread/core24/python-hello/task.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
summary: Build and run Python-based snaps in core24

systems:
# Must *not* run this on 24.04, which can give false-positives due to the
# presence of the system Python 3.12.
- ubuntu-22.04*

environment:
PARAM/strict: ""
PARAM/classic: "--classic"
Expand All @@ -17,4 +22,4 @@ execute: |
# shellcheck disable=SC2086
snap install python-hello-"${SPREAD_VARIANT}"_1.0_*.snap --dangerous ${PARAM}
python-hello-"${SPREAD_VARIANT}" | MATCH "hello world"
python-hello-"${SPREAD_VARIANT}" | MATCH "hello world! black version"
40 changes: 39 additions & 1 deletion tests/unit/parts/plugins/test_python_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from textwrap import dedent

import pytest
from craft_parts import Part, PartInfo, ProjectInfo, errors
from craft_parts import Part, PartInfo, ProjectInfo, errors, StepInfo, Step

from snapcraft.parts.plugins import PythonPlugin

Expand Down Expand Up @@ -190,3 +190,41 @@ def test_get_system_python_interpreter_unknown_base(confinement, new_dir):
expected_error = "Don't know which interpreter to use for base core10"
with pytest.raises(errors.PartsError, match=expected_error):
plugin._get_system_python_interpreter()


@pytest.mark.parametrize("home_attr", ["part_install_dir", "stage_dir"])
def test_fix_pyvenv(new_dir, home_attr):
part_info = PartInfo(
project_info=ProjectInfo(
application_name="test",
project_name="test-snap",
base="core24",
confinement="classic",
project_base="core24",
cache_dir=new_dir,
),
part=Part("my-part", {"plugin": "python"}),
)

prime_dir = part_info.prime_dir
prime_dir.mkdir()

pyvenv = prime_dir / "pyvenv.cfg"
pyvenv.write_text(
dedent(
f"""\
home = {getattr(part_info, home_attr)}/usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /root/parts/my-part/install/usr/bin/python3.12
command = /root/parts/my-part/install/usr/bin/python3 -m venv /root/parts/my-part/install
"""
)
)

step_info = StepInfo(part_info, Step.PRIME)

PythonPlugin.post_prime(step_info)

new_contents = pyvenv.read_text()
assert "home = /snap/test-snap/current/usr/bin" in new_contents
10 changes: 8 additions & 2 deletions tests/unit/services/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ def test_lifecycle_installs_base(lifecycle_service, mocker):
)


def test_post_prime_no_patchelf(fp, tmp_path, lifecycle_service):
def test_post_prime_no_patchelf(fp, tmp_path, lifecycle_service, default_project):
new_attrs = {"parts": {"my-part": {"plugin": "nil"}}}
default_project.__dict__.update(**new_attrs)

mock_step_info = mock.Mock()
mock_step_info.configure_mock(
**{
"base": "core24",
"build_attributes": [],
"state.files": ["usr/bin/ls"],
"prime_dir": tmp_path / "prime",
"part_name": "my-part",
}
)

Expand Down Expand Up @@ -82,7 +86,7 @@ def test_post_prime_patchelf(
use_system_libs,
):
patchelf_spy = mocker.spy(snapcraft.parts, "patch_elf")
new_attrs = {"confinement": confinement}
new_attrs = {"confinement": confinement, "parts": {"my-part": {"plugin": "nil"}}}
default_project.__dict__.update(**new_attrs)

mock_step_info = mock.Mock()
Expand All @@ -92,6 +96,7 @@ def test_post_prime_patchelf(
"build_attributes": ["enable-patchelf"],
"state.files": ["usr/bin/ls"],
"prime_dir": tmp_path / "prime",
"part_name": "my-part",
}
)

Expand Down Expand Up @@ -207,6 +212,7 @@ def test_lifecycle_custom_arguments(

assert info.project_base == expected_base
assert info.confinement == expected_confinement
assert info.project_name == default_project.name == "default"


@pytest.mark.usefixtures("default_project")
Expand Down

0 comments on commit a2df2cd

Please sign in to comment.