Skip to content

Commit

Permalink
Use bwrap with newer Steam Runtime installations
Browse files Browse the repository at this point in the history
Use the provided `run` script to launch Proton executables using Steam
Runtime containers. A different proxy script is used to ensure that
containers are not launched recursively and to ensure LD_LIBRARY_PATH
can be updated inside the container.

This should match the actual Proton behavior better, as well as allow
compatibility with Linux distros that ship older versions of glibc.

Fixes #72
  • Loading branch information
Matoking committed Mar 2, 2021
1 parent e997181 commit 33820a1
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 115 deletions.
31 changes: 19 additions & 12 deletions src/protontricks/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
#
# Script licensed under the GPLv3!

import sys
import argparse
import os
import logging
import os
import sys

from . import __version__
from .steam import (find_proton_app, find_steam_path, find_steam_runtime_path,
get_steam_apps, get_steam_lib_paths)
from .winetricks import get_winetricks_path
from .gui import select_steam_app_with_gui
from .steam import (find_legacy_steam_runtime_path, find_proton_app,
find_steam_path, get_steam_apps, get_steam_lib_paths)
from .util import run_command
from .winetricks import get_winetricks_path

logger = logging.getLogger("protontricks")

Expand Down Expand Up @@ -132,16 +132,20 @@ def main(args=None):
)
sys.exit(-1)

# 2. Find Steam Runtime if enabled
steam_runtime_path = None
# 2. Find the pre-installed legacy Steam Runtime if enabled
legacy_steam_runtime_path = None
use_steam_runtime = True

if os.environ.get("STEAM_RUNTIME", "") != "0" and not args.no_runtime:
steam_runtime_path = find_steam_runtime_path(steam_root=steam_root)
legacy_steam_runtime_path = find_legacy_steam_runtime_path(
steam_root=steam_root
)

if not steam_runtime_path:
if not legacy_steam_runtime_path:
print("Steam Runtime was enabled but couldn't be found!")
sys.exit(-1)
else:
use_steam_runtime = False
logger.info("Steam Runtime disabled.")

# 3. Find Winetricks
Expand Down Expand Up @@ -201,7 +205,8 @@ def main(args=None):
winetricks_path=winetricks_path,
proton_app=proton_app,
steam_app=steam_app,
steam_runtime_path=steam_runtime_path,
use_steam_runtime=use_steam_runtime,
legacy_steam_runtime_path=legacy_steam_runtime_path,
command=[winetricks_path, "--gui"]
)

Expand Down Expand Up @@ -269,15 +274,17 @@ def main(args=None):
winetricks_path=winetricks_path,
proton_app=proton_app,
steam_app=steam_app,
steam_runtime_path=steam_runtime_path,
use_steam_runtime=use_steam_runtime,
legacy_steam_runtime_path=legacy_steam_runtime_path,
command=[winetricks_path] + args.winetricks_command)
elif args.command:
run_command(
winetricks_path=winetricks_path,
proton_app=proton_app,
steam_app=steam_app,
command=args.command,
steam_runtime_path=steam_runtime_path,
use_steam_runtime=use_steam_runtime,
legacy_steam_runtime_path=legacy_steam_runtime_path,
# Pass the command directly into the shell *without*
# escaping it
cwd=steam_app.install_path,
Expand Down
10 changes: 5 additions & 5 deletions src/protontricks/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

__all__ = (
"COMMON_STEAM_DIRS", "SteamApp", "find_steam_path",
"find_steam_proton_app", "find_proton_app", "find_steam_runtime_path",
"find_appid_proton_prefix", "get_steam_lib_paths", "get_steam_apps",
"get_custom_proton_installations"
"find_steam_proton_app", "find_proton_app",
"find_legacy_steam_runtime_path", "find_appid_proton_prefix",
"get_steam_lib_paths", "get_steam_apps", "get_custom_proton_installations"
)

COMMON_STEAM_DIRS = [
Expand Down Expand Up @@ -254,9 +254,9 @@ def has_runtime_dir(path):
return None, None


def find_steam_runtime_path(steam_root):
def find_legacy_steam_runtime_path(steam_root):
"""
Find the Steam Runtime either using the STEAM_RUNTIME env or
Find the legacy Steam Runtime either using the STEAM_RUNTIME env or
steam_root
"""
env_steam_runtime = os.environ.get("STEAM_RUNTIME", "")
Expand Down
149 changes: 62 additions & 87 deletions src/protontricks/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,76 +25,12 @@ def lower_dict(d):
return {k.lower(): v for k, v in d.items()}


def get_host_library_paths():
def get_legacy_runtime_library_paths(legacy_steam_runtime_path, proton_app):
"""
Get host library paths to use when creating the LD_LIBRARY_PATH environment
variable for use with newer Steam Runtime installations
Get LD_LIBRARY_PATH value to use when running a command using Steam Runtime
"""
# The traditional Steam Runtime does the following when running the
# `run.sh --print-steam-runtime-library-paths` command.
# Since that command is unavailable with newer Steam Runtime releases,
# do it ourselves here.
result = run(
["/sbin/ldconfig", "-XNv"],
check=True, stdout=PIPE, stderr=PIPE
)
lines = result.stdout.decode("utf-8").split("\n")
paths = [
line.split(":")[0] for line in lines
if line.startswith("/") and ":" in line
]

return ":".join(paths)


RUNTIME_ROOT_GLOB_PATTERNS = (
"var/*/files/",
"*/files/"
)


def get_runtime_library_paths(steam_runtime_path, proton_app):
"""
Get LD_LIBRARY_PATH value to run a command using Steam Runtime
"""
def find_runtime_app_root(runtime_app):
"""
Find the runtime root (the directory containing the root fileystem
used for the container) for separately installed Steam Runtime app
"""
for pattern in RUNTIME_ROOT_GLOB_PATTERNS:
try:
return next(
runtime_app.install_path.glob(pattern)
)
except StopIteration:
pass

raise RuntimeError(
"Could not find Steam Runtime runtime root for {}".format(
runtime_app.name
)
)

if proton_app.required_tool_appid:
# bwrap based Steam Runtime is used for Proton installations that
# use separate Steam runtimes
# TODO: Try to run the Wine binaries inside an user namespace somehow.
# Newer Steam Runtime environments may rely on a newer glibc than what
# is available on the host system, which may cause potential problems
# otherwise.
runtime_root = find_runtime_app_root(proton_app.required_tool_app)
return "".join([
str(proton_app.install_path / "dist" / "lib"), os.pathsep,
str(proton_app.install_path / "dist" / "lib64"), os.pathsep,
get_host_library_paths(), os.pathsep,
str(runtime_root / "lib" / "i386-linux-gnu"), os.pathsep,
str(runtime_root / "lib" / "x86_64-linux-gnu")
])

# Traditional LD_LIBRARY_PATH based Steam Runtime is used otherwise
steam_runtime_paths = check_output([
str(steam_runtime_path / "run.sh"),
str(legacy_steam_runtime_path / "run.sh"),
"--print-steam-runtime-library-paths"
])
steam_runtime_paths = str(steam_runtime_paths, "utf-8")
Expand All @@ -107,20 +43,54 @@ def find_runtime_app_root(runtime_app):
])


WINE_SCRIPT_TEMPLATE = (
def get_runtime_library_paths(proton_app):
"""
Get LD_LIBRARY_PATH value to use when running a command using Steam Runtime
"""
return "".join([
str(proton_app.install_path / "dist" / "lib"), os.pathsep,
str(proton_app.install_path / "dist" / "lib64"), os.pathsep
])


WINE_SCRIPT_RUNTIME_V1_TEMPLATE = (
"#!/bin/bash\n"
"# Helper script created by Protontricks to run Wine binaries using Steam Runtime\n"
"export LD_LIBRARY_PATH=\"$PROTON_LD_LIBRARY_PATH\"\n"
"exec \"$PROTON_PATH\"/dist/bin/{name} \"$@\""
)

WINE_SCRIPT_RUNTIME_V2_TEMPLATE = """#!/bin/bash
# Helper script created by Protontricks to run Wine binaries using Steam Runtime
PROTONTRICKS_PROXY_SCRIPT_PATH="{script_path}"
if [[ -n "$PROTONTRICKS_INSIDE_STEAM_RUNTIME" ]]; then
# Command is being executed inside Steam Runtime
# LD_LIBRARY_PATH can now be set.
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH":"$PROTON_LD_LIBRARY_PATH"
"$PROTON_PATH"/dist/bin/{name} "$@"
else
exec "$STEAM_RUNTIME_PATH"/run --share-pid --batch --filesystem=/mnt \
--filesystem=/tmp --filesystem=/run/media --filesystem=/etc --filesystem=/opt \
--filesystem=/var --filesystem=/home --filesystem=/usr -- \
env PROTONTRICKS_INSIDE_STEAM_RUNTIME=1 \
"$PROTONTRICKS_PROXY_SCRIPT_PATH" "$@"
fi
"""


def create_wine_bin_dir(proton_app):
"""
Create a directory with "proxy" executables that load shared libraries
using Steam Runtime and Proton's own libraries instead of the system
libraries
"""
# If the Proton installation uses a newer version of Steam Runtime,
# use a different template for the scripts
bin_template = (
WINE_SCRIPT_RUNTIME_V2_TEMPLATE if proton_app.required_tool_app
else WINE_SCRIPT_RUNTIME_V1_TEMPLATE
)

binaries = list((proton_app.install_path / "dist" / "bin").iterdir())

# Create the base directory containing files for every Proton installation
Expand All @@ -139,28 +109,20 @@ def create_wine_bin_dir(proton_app):
"Created Steam Runtime Wine binary directory at %s", str(bin_path)
)

# Check if the correct binaries exist
files_already_exist = (
{binary.name for binary in binaries}
== {binary.name for binary in bin_path.iterdir()}
)

if files_already_exist:
# The correct files exist and nothing needs to be rewritten
return bin_path

# Delete the directory and rewrite the scripts. Some binaries may no
# longer exist in the Proton installation, so we'll also get rid of
# scripts that point to non-existing files
shutil.rmtree(str(bin_path))
bin_path.mkdir(parents=True)

for binary in binaries:
content = WINE_SCRIPT_TEMPLATE.format(
name=shlex.quote(binary.name)
proxy_script_path = bin_path / binary.name

content = bin_template.format(
name=shlex.quote(binary.name),
script_path=str(proxy_script_path)
).encode("utf-8")

proxy_script_path = bin_path / binary.name
proxy_script_path.write_bytes(content)

script_stat = proxy_script_path.stat()
Expand All @@ -172,21 +134,23 @@ def create_wine_bin_dir(proton_app):

def run_command(
winetricks_path, proton_app, steam_app, command,
steam_runtime_path=None,
use_steam_runtime=False, legacy_steam_runtime_path=None,
**kwargs):
"""Run an arbitrary command with the correct environment variables
for the given Proton app
The environment variables are set for the duration of the call
and restored afterwards
If 'steam_runtime_path' is provided, run the command using Steam Runtime
If 'use_steam_runtime' is True, run the command using Steam Runtime
using either 'legacy_steam_runtime_path' or the Proton app's specific
Steam Runtime installation, depending on which one is required
"""
# Check for incomplete Steam Runtime installation
runtime_install_incomplete = \
proton_app.required_tool_appid and not proton_app.required_tool_app

if steam_runtime_path and runtime_install_incomplete:
if use_steam_runtime and runtime_install_incomplete:
raise RuntimeError(
"{} is missing the required Steam Runtime. You may need to launch "
"a Steam app using this Proton version to finish the "
Expand Down Expand Up @@ -234,8 +198,13 @@ def run_command(
os.environ.pop("WINEARCH", "")

wine_bin_dir = None
if steam_runtime_path:
if use_steam_runtime:
if proton_app.required_tool_app:
os.environ["STEAM_RUNTIME_PATH"] = \
str(proton_app.required_tool_app.install_path)
os.environ["PROTON_LD_LIBRARY_PATH"] = \
get_runtime_library_paths(proton_app)

runtime_name = proton_app.required_tool_app.name
logger.info(
"Using separately installed Steam Runtime: %s",
Expand All @@ -246,13 +215,19 @@ def run_command(
logger.warning(
"Current Steam Runtime not recognized by Protontricks."
)
else:
# Legacy Steam Runtime requires a different LD_LIBRARY_PATH
os.environ["PROTON_LD_LIBRARY_PATH"] = \
get_legacy_runtime_library_paths(
legacy_steam_runtime_path, proton_app
)

# When Steam Runtime is enabled, create a set of helper scripts
# that load the underlying Proton Wine executables with Steam Runtime
# and Proton libraries instead of system libraries
wine_bin_dir = create_wine_bin_dir(proton_app=proton_app)
os.environ["PROTON_LD_LIBRARY_PATH"] = \
get_runtime_library_paths(steam_runtime_path, proton_app)
os.environ["LEGACY_STEAM_RUNTIME_PATH"] = \
str(legacy_steam_runtime_path)
os.environ["PATH"] = "".join([
str(wine_bin_dir), os.pathsep, os.environ["PATH"]
])
Expand Down
Loading

0 comments on commit 33820a1

Please sign in to comment.