Skip to content

Commit

Permalink
fix(lifecycle): remove usrmerge-breaking symlinks (#498)
Browse files Browse the repository at this point in the history
This commit adds code in the post-prime callback to handle the specific
combination of "[email protected]" 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:

```
<install-dir>
     |- 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:

```
<install-dir>
     |- 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:

```
<install-dir>
     |- 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
  • Loading branch information
tigarmo authored Mar 5, 2024
1 parent 924b608 commit 05489c8
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 1 deletion.
28 changes: 28 additions & 0 deletions rockcraft/services/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] projects."""
if step_info.project_info.base != "[email protected]":
# 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()
50 changes: 49 additions & 1 deletion tests/unit/services/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

Expand Down Expand Up @@ -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="[email protected]",
)

# 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"]

0 comments on commit 05489c8

Please sign in to comment.