Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix Python venv in core24 classic snaps #4946

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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$" \
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
"^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
Loading