Skip to content

Commit

Permalink
fix: depend on ldconfig when finding shared library path prefixes (#180)
Browse files Browse the repository at this point in the history
* umu_run: delete set_steamrt_paths

* fix: use get_library_paths to find library paths

- The previous implementation of finding the shared library paths assumed every path containing the libc.so.6 was a valid path prefix. However, it's not that simple as it would be wrong for container environments such as Flatpak where an LD_LIBRARY_PATH is not set and shared library paths appear in multiple places, resulting in the incorrect environment variable for STEAM_RUNTIME_LIBRARY_PATH:

 STEAM_RUNTIME_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/app/lib/i386-linux-gnu:/home/foo/Games/Flowers - Le Volume Sur Automne

Where it really should be *all* the paths that appear in the dynamic linker's search path:

 STEAM_RUNTIME_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/app/lib/i386-linux-gnu:/app/lib32:/app/lib/i386-linux-gnu/GL/default/lib:/lib64:/app/lib:/usr/lib/x86_64-linux-gnu/GL/default/lib:/usr/lib/x86_64-linux-gnu/openh264/extra:/usr/lib/i386-linux-gnu:/home/foo/Games/Flowers - Le Volume Sur Automne

- Note, this problem hasn't been reported yet and it's not clear how it would effect games by having some paths missing yet. Nonetheless, the value was incorrect when matching against Steam's (Flatpak) STEAM_RUNTIME_LIBRARY_PATH value. As a result, minus the appended Scout runtime paths, the value for that environment variable will now be logically equivalent to Steam's as we are using the paths prefixes returned from ldconfig then resolving those real paths.

* umu_run: remove missing libc check

- libc.so is fundamental and users are going to run into problems if it couldn't be found by Python from the host or container environment. Those users will need to configure their environment accordingly within the container framework's expectations. In the next lines, umu-launcher will attempt to find the paths using ldconfig instead of exiting early if libc.so couldn't be found

* umu_test: remove set_steamrt_paths test

* umu_test: update tests

* umu_util: resolve the path
  • Loading branch information
R1kaB3rN authored Sep 10, 2024
1 parent 2d3c948 commit 3af25f4
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 125 deletions.
39 changes: 3 additions & 36 deletions umu/umu_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from umu.umu_runtime import setup_umu
from umu.umu_util import (
get_libc,
get_library_paths,
get_osrelease_id,
is_installed_verb,
is_winetricks_verb,
Expand Down Expand Up @@ -305,18 +306,6 @@ def enable_steam_game_drive(env: dict[str, str]) -> dict[str, str]:
"""Enable Steam Game Drive functionality."""
paths: set[str] = set()
root: Path = Path("/")
libc: str = get_libc()

# All library paths that are currently supported by the container framework
# See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/distro-assumptions.md#filesystem-layout
# Non-FHS filesystems should run in a FHS chroot to comply
steamrt_path_candidates: tuple[str, ...] = (
"/usr/lib64",
"/usr/lib32",
"/usr/lib",
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/i386-linux-gnu",
)

# Check for mount points going up toward the root
# NOTE: Subvolumes can be mount points
Expand All @@ -336,36 +325,14 @@ def enable_steam_game_drive(env: dict[str, str]) -> dict[str, str]:
if env["STEAM_COMPAT_INSTALL_PATH"]:
paths.add(env["STEAM_COMPAT_INSTALL_PATH"])

# When libc.so could not be found, depend on LD_LIBRARY_PATH
# In some cases, using ldconfig to determine library paths can fail in non-
# FHS compliant filesystems (e.g., NixOS).
# See https://github.com/Open-Wine-Components/umu-launcher/issues/106
if not libc:
log.warning("libc.so could not be found")
env["STEAM_RUNTIME_LIBRARY_PATH"] = ":".join(paths)
return env

# Set the shared library paths of the system after finding libc.so
set_steamrt_paths(steamrt_path_candidates, paths, libc)
# Set the shared library paths of the system
paths |= get_library_paths()

env["STEAM_RUNTIME_LIBRARY_PATH"] = ":".join(paths)

return env


def set_steamrt_paths(
steamrt_path_candidiates: tuple[str, ...],
steamrt_paths: set[str],
libc: str,
) -> set[str]:
"""Set the shared library paths for the Steam Runtime."""
for rtpath in steamrt_path_candidiates:
if (libc_path := Path(rtpath, libc).resolve()).is_file():
steamrt_paths.add(str(libc_path.parent))

return steamrt_paths


def build_command(
env: dict[str, str],
local: Path,
Expand Down
94 changes: 5 additions & 89 deletions umu/umu_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,68 +209,6 @@ def test_rearrange_gamescope_baselayer_order(self):
f"Expected {expected}, received {result}",
)

def test_set_steamrt_paths(self):
"""Test set_steamrt_paths to ensure resolved filesystem paths.
set_steamrt_path will find path containing the libc.so file from the
system, resolving any symbolic links in its path.
Expects a set to contain strings representing the user's shared
library paths and for the paths to not contain symbolic links.
"""
lib64_link = f"{self.test_usr}/lib64"
lib64 = f"{self.test_usr}/lib"
libc = "libc.so.6"
steamrt_path_candidates = (
lib64_link,
lib64,
f"{self.test_usr}/lib32",
f"{self.test_usr}/lib/x86_64-linux-gnu",
f"{self.test_usr}/lib/i386-linux-gnu",
)
steamrt_paths = set()

# Mock shared library paths and libc.so.6
for path in steamrt_path_candidates:
if path == lib64_link:
Path(lib64_link).symlink_to("lib")
continue
Path(path).mkdir()

Path(lib64, libc).touch()

# Find shared lib paths containing libc
for path in steamrt_path_candidates:
if Path(path, libc).is_file():
steamrt_paths.add(path)

# Assert mocked runtime paths
self.assertEqual(
len(steamrt_paths),
2,
f"Expected 2 elements for '{steamrt_paths}'",
)
self.assertTrue(
lib64 in steamrt_paths and lib64_link in steamrt_paths,
f"Expected '{steamrt_paths}' to contain linked and resolved path",
)
self.assertEqual(
Path(lib64_link).resolve(),
Path(lib64).absolute(),
"Expected linked shared library path to resolve to real path",
)

result = umu_run.set_steamrt_paths(
steamrt_path_candidates, set(), libc
)

# Ensure the resolved shared library paths is not the unresolved paths
self.assertNotEqual(
steamrt_paths,
result,
"Expected linked shared library paths to not be resolved paths",
)

def test_run_command(self):
"""Test run_command."""
mock_exe = "foo"
Expand Down Expand Up @@ -1190,13 +1128,6 @@ def test_game_drive_libpath_empty(self):
"Expected two elements in STEAM_RUNTIME_LIBRARY_PATHS",
)

# Expect LD_LIBRARY_PATH was added ontop of /usr/lib and /usr/lib64
self.assertEqual(
len(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":")),
2,
"Expected two values in STEAM_RUNTIME_LIBRARY_PATH",
)

# An error should be raised if /usr/lib or /usr/lib64 is found twice
lib_paths = set()
for path in self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":"):
Expand Down Expand Up @@ -1325,13 +1256,6 @@ def test_game_drive_empty(self):
args = None
result_gamedrive = None
# Expected library paths for the container runtime framework
libpaths = {
"/usr/lib64",
"/usr/lib32",
"/usr/lib",
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/i386-linux-gnu",
}
Path(self.test_file + "/proton").touch()

# Replicate main's execution and test up until enable_steam_game_drive
Expand Down Expand Up @@ -1375,26 +1299,18 @@ def test_game_drive_empty(self):
"Expected two elements in STEAM_RUNTIME_LIBRARY_PATHS",
)

# We just expect /usr/lib and /usr/lib32 since LD_LIBRARY_PATH is unset
self.assertEqual(
len(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":")),
2,
"Expected two values in STEAM_RUNTIME_LIBRARY_PATH",
)

# Check that there are no trailing colons, unexpected characters
# and is officially supported
str1, str2 = self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":")
self.assertTrue(str1 in libpaths, f"Expected a path in: {libpaths}")
self.assertTrue(str2 in libpaths, f"Expected a path in: {libpaths}")

# Ensure that umu sets the resolved shared library paths. The only time
# this variable will contain links is from the LD_LIBRARY_PATH set in
# the user's environment or client
for path in self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":"):
if Path(path).is_symlink():
err = f"Symbolic link found: {path}"
raise AssertionError(err)
if path.endswith(
(":", "/", ".")
): # There should be no trailing colons, slashes or periods
err = f"Trailing character in path: {path[-1]}"
raise AssertionError(err)

# Both of these values should be empty still after calling
# enable_steam_game_drive
Expand Down
37 changes: 37 additions & 0 deletions umu/umu_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,43 @@ def get_libc() -> str:
return find_library("c") or ""


@lru_cache
def get_library_paths() -> set[str]:
"""Find the shared library paths from the user's system."""
library_paths: set[str] = set()
ldconfig: str = which("ldconfig") or ""

if not ldconfig:
log.warning("ldconfig not found in $PATH, cannot find library paths")
return library_paths

# Find all shared library path prefixes within the assumptions of the
# Steam Runtime container framework. The framework already works hard by
# attempting to work with various distibutions' quirks. Unless it's Flatpak
# related, let's continue to make it their job.
try:
# Here, opt to using the ld.so cache similar to the stdlib
# implementation of _findSoname_ldconfig.
with Popen(
(ldconfig, "-p"),
text=True,
encoding="utf-8",
stdout=PIPE,
stderr=PIPE,
env={"LC_ALL": "C", "LANG": "C"},
) as proc:
stdout, _ = proc.communicate()
library_paths |= {
os.path.realpath(line[: line.rfind("/")])
for line in stdout.split()
if line.startswith("/")
}
except OSError as e:
log.exception(e)

return library_paths


def run_zenity(command: str, opts: list[str], msg: str) -> int:
"""Execute the command and pipe the output to zenity.
Expand Down

0 comments on commit 3af25f4

Please sign in to comment.