Skip to content

Commit

Permalink
fix: fix Python venv in core24 classic snaps (#4946)
Browse files Browse the repository at this point in the history
  • Loading branch information
tigarmo authored Aug 1, 2024
1 parent 6425f1c commit 952a8e0
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 7 deletions.
35 changes: 34 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 StepInfo, errors
from craft_parts.plugins import python_plugin
from overrides import override

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

@classmethod
def post_prime(cls, step_info: StepInfo) -> None:
"""Perform Python-specific actions right before packing."""
base = step_info.project_base

if base in ("core20", "core22"):
# 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,
)

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

if old_contents != contents:
logger.debug("Updating pyvenv.cfg to:\n%s", contents)
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ execute: |
cat "$file"
for exp in \
"^SNAPCRAFT_PROJECT_GRADE=devel$" \
"^SNAPCRAFT_PROJECT_NAME=None$" \
"^SNAPCRAFT_PROJECT_NAME=variables$" \
"^SNAPCRAFT_PROJECT_VERSION=1$" \
"^SNAPCRAFT_PARALLEL_BUILD_COUNT=[0-9]\+$" \
"^SNAPCRAFT_PROJECT_DIR=${root}$" \
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, Step, StepInfo, errors

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 952a8e0

Please sign in to comment.