From 05489c804ae46e121307c30d6873119542838999 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 5 Mar 2024 09:54:06 -0300 Subject: [PATCH] fix(lifecycle): remove usrmerge-breaking symlinks (#498) This commit adds code in the post-prime callback to handle the specific combination of "ubuntu@24.04" base + "python" plugin. The handling detects and removes a primed "lib64" symlink to "lib", which can't stay because it breaks the "lib64" symlink to "usr/lib64" on the base layer, making the rock unusable. Read on for more context on *why* this started happening now: The Python plugin works by creating a virtual environment on the part's install dir (which eventually gets primed). By default, the creation of the virtual environment will setup a directory structure like this: ``` |- bin/ |- lib/ |- ... |- lib64 -> lib ``` That is, the creation itself places a "lib64" symlink pointing to "lib". If this gets primed as-is, this symlink will override the base layer's own symlink of "lib64" to "usr/lib64", which renders the rock unusable because all binaries expect the loader to exist in "lib64/ld-linux-x86-64.so.2", when the loader's "true" location is in "usr/lib64/ld-linux-x86-64.so.2". This wasn't an issue prior to 24.04 because of the way we extract stage packages: since the base layer doesn't include the Python interpreter, rockcraft projects need to provision Python themselves, typically via the "python3-venv" stage-package. This causes the inclusion of the package's dependencies, ultimately pulling in "libc6". On Ubuntu versions older than 24.04, the "lib64" package includes the "ld-linux-x86-64.so.2" loader as "/lib64/ld-linux-x86-64.so.2". This means that by the time the virtual environment creation starts the install dir already has a "lib64" directory, which I guess the venv just accepts and leaves alone. The end result is that "lib64" is not a symlink to "lib", and the usrmerge handling code that we already have when packing takes care of packing the files in "lib64" as inside "usr/lib64". The install dir looks like this: ``` |- bin/ |- lib/ |- ... |- lib64/ |- ld-linux-x86-64.so.2 ``` However, 24.04 changed the usrmerge handling and now the libc6 package includes that loader as "/usr/lib64/ld-linux-x86-64.so.2", which means that the "lib64" directory doesn't exist anymore when the venv is created. Thus, "lib64" is created as a symlink to "lib" and breaks the base layer: ``` |- bin/ |- lib/ |- ... |- lib64 -> lib |- usr |- lib64 |- ld-linux-x86-64.so.2 ``` Package listings: Jammy: https://packages.ubuntu.com/jammy/amd64/libc6/filelist Noble: https://packages.ubuntu.com/noble/amd64/libc6/filelist --- rockcraft/services/lifecycle.py | 28 +++++++++++++++ tests/unit/services/test_lifecycle.py | 50 ++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/rockcraft/services/lifecycle.py b/rockcraft/services/lifecycle.py index 10639f877..880115ac6 100644 --- a/rockcraft/services/lifecycle.py +++ b/rockcraft/services/lifecycle.py @@ -119,4 +119,32 @@ def _post_prime_callback(step_info: StepInfo) -> bool: files = step_info.state.files if step_info.state else set() layers.prune_prime_files(prime_dir, files, base_layer_dir) + + _python_usrmerge_fix(step_info) + return True + + +def _python_usrmerge_fix(step_info: StepInfo): + """Fix 'lib64' symlinks created by the Python plugin on ubuntu@24.04 projects.""" + if step_info.project_info.base != "ubuntu@24.04": + # The issue only affects rocks with 24.04 bases. + return + + state = step_info.state + if state is None: + # Can't inspect the files without a StepState. + return + + if state.part_properties["plugin"] != "python": + # Be conservative and don't try to fix the files if they didn't come + # from the Python plugin. + return + + if "lib64" not in state.files: + return + + prime_dir = step_info.prime_dir + lib64 = prime_dir / "lib64" + if lib64.is_symlink() and lib64.readlink() == Path("lib"): + lib64.unlink() diff --git a/tests/unit/services/test_lifecycle.py b/tests/unit/services/test_lifecycle.py index 5480f791f..c9b78490b 100644 --- a/tests/unit/services/test_lifecycle.py +++ b/tests/unit/services/test_lifecycle.py @@ -13,11 +13,22 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os from pathlib import Path from unittest import mock import pytest -from craft_parts import LifecycleManager, callbacks +from craft_parts import ( + LifecycleManager, + Part, + PartInfo, + ProjectDirs, + ProjectInfo, + Step, + StepInfo, + callbacks, +) +from craft_parts.state_manager.prime_state import PrimeState from rockcraft.services import lifecycle as lifecycle_module @@ -78,3 +89,40 @@ def test_lifecycle_package_repositories( mock_callback.assert_called_once_with( lifecycle_module._install_overlay_repositories ) + + +def test_python_usrmerge_fix(tmp_path): + # The test setup is rather involved because we need to recreate/mock an + # exact set of circumstances here: + + # 1) Create a project with 24.04 base; + dirs = ProjectDirs(work_dir=tmp_path) + project_info = ProjectInfo( + project_dirs=dirs, + application_name="test", + cache_dir=tmp_path, + strict_mode=False, + base="ubuntu@24.04", + ) + + # 2) Create a part using the Python plugin; + part = Part("p1", {"source": ".", "plugin": "python"}) + part_info = PartInfo(project_info=project_info, part=part) + + prime_dir = dirs.prime_dir + prime_dir.mkdir() + + # 3) Setup a 'prime' directory where "lib64" is a symlink to "lib"; + (prime_dir / "lib").mkdir() + (prime_dir / "lib64").symlink_to("lib") + + # 4) Create a StepInfo that contains all of this. + step_info = StepInfo(part_info=part_info, step=Step.PRIME) + step_info.state = PrimeState(part_properties=part.spec.marshal(), files={"lib64"}) + + assert sorted(os.listdir(prime_dir)) == ["lib", "lib64"] + + lifecycle_module._python_usrmerge_fix(step_info) + + # After running the fix the "lib64" symlink must be gone + assert sorted(os.listdir(prime_dir)) == ["lib"]