Skip to content

Commit

Permalink
Merge pull request #2961 from koplo199/bottlesdevs-pr
Browse files Browse the repository at this point in the history
fix: Steam and Proton usage
  • Loading branch information
mirkobrombin authored Aug 5, 2023
2 parents 39b7cfb + 8667cbe commit d8e142d
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 42 deletions.
4 changes: 4 additions & 0 deletions bottles/backend/managers/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ def __get_steam_runtime():
return available_runtimes

lookup = {
"sniper": {
"name": "sniper",
"entry_point": os.path.join(steam_manager.steam_path, "steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point"),
},
"soldier": {
"name": "soldier",
"entry_point": os.path.join(steam_manager.steam_path, "steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point"),
Expand Down
27 changes: 16 additions & 11 deletions bottles/backend/managers/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def __get_library_folders(self) -> Union[list, None]:
return None

with open(library_folders_path, "r", errors="replace") as f:
_library_folders = SteamUtils.parse_acf(f.read())
_library_folders = SteamUtils.parse_vdf(f.read())

if _library_folders is None or not _library_folders.get("libraryfolders"):
logging.warning(f"Could not parse libraryfolders.vdf")
Expand Down Expand Up @@ -155,7 +155,7 @@ def __get_local_config(self) -> dict:
return {}

with open(self.localconfig_path, "r", errors="replace") as f:
data = SteamUtils.parse_acf(f.read())
data = SteamUtils.parse_vdf(f.read())

if data is None:
logging.warning(f"Could not parse localconfig.vdf")
Expand Down Expand Up @@ -191,14 +191,18 @@ def get_runner_path(pfx_path: str) -> Union[tuple, None]:
logging.error(f"{config_info} is not valid, cannot get Steam Proton path")
return None

proton_path = lines[2].strip()[:-5]
proton_name = os.path.basename(proton_path.rsplit("/", 1)[0])
proton_path = lines[1].strip().removesuffix("/share/fonts/")

if proton_path.endswith("/files"):
proton_path = proton_path.removesuffix("/files")
elif proton_path.endswith("/dist"):
proton_path = proton_path.removesuffix("/dist")

if not os.path.isdir(proton_path):
if not SteamUtils.is_proton(proton_path):
logging.error(f"{proton_path} is not a valid Steam Proton path")
return None

return proton_name, proton_path
return proton_path

def list_apps_ids(self) -> dict:
"""List all apps in Steam"""
Expand Down Expand Up @@ -272,7 +276,7 @@ def list_prefixes(self) -> Dict[str, BottleConfig]:
_launch_options = self.get_launch_options(appid, appdata)
_dir_name = os.path.basename(_path)
_acf = self.get_acf_data(_library_path, _dir_name)
_runner = self.get_runner_path(_path)
_runner_path = self.get_runner_path(_path)
_creation_date = datetime.fromtimestamp(os.path.getctime(_path)) \
.strftime("%Y-%m-%d %H:%M:%S.%f")

Expand All @@ -284,11 +288,12 @@ def list_prefixes(self) -> Dict[str, BottleConfig]:
logging.warning(f"A Steam prefix was found, but there is no ACF for it: {_dir_name}, skipping…")
continue

if "Proton" in _acf["AppState"]["name"]:
if SteamUtils.is_proton(os.path.join(_library_path, "steamapps/common", _acf["AppState"]["installdir"])):
# skip Proton default prefix
logging.warning(f"A Steam prefix was found, but it is a Proton one: {_dir_name}, skipping…")
continue

if _runner is None:
if _runner_path is None:
logging.warning(f"A Steam prefix was found, but there is no Proton for it: {_dir_name}, skipping…")
continue

Expand All @@ -297,8 +302,8 @@ def list_prefixes(self) -> Dict[str, BottleConfig]:
_conf.Environment = "Steam"
_conf.CompatData = _dir_name
_conf.Path = os.path.join(_path, "pfx")
_conf.Runner = _runner[0]
_conf.RunnerPath = _runner[1]
_conf.Runner = os.path.basename(_runner_path)
_conf.RunnerPath = _runner_path
_conf.WorkingDir = os.path.join(_conf.get("Path", ""), "drive_c")
_conf.Creation_Date = _creation_date
_conf.Update_Date = datetime.fromtimestamp(
Expand Down
45 changes: 43 additions & 2 deletions bottles/backend/utils/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import os
import os, subprocess
from typing import Union, TextIO
from typing import TextIO

from bottles.backend.logger import Logger
from bottles.backend.models.vdict import VDFDict
from bottles.backend.utils import vdf

logging = Logger()

class SteamUtils:

@staticmethod
def parse_acf(data: str) -> VDFDict:
"""
Parses aN ACF file. Just a wrapper for vdf.loads.
Parses an ACF file. Just a wrapper for vdf.loads.
"""
return vdf.loads(data)

Expand Down Expand Up @@ -63,3 +66,41 @@ def is_proton(path: str) -> bool:
.get("commandline", {})

return "proton" in compat_layer_name or "proton" in commandline

@staticmethod
def get_associated_runtime(path: str) -> str:
"""
Get the associated runtime of a Proton directory.
"""
toolmanifest = os.path.join(path, f"toolmanifest.vdf")
if not os.path.isfile(toolmanifest):
logging.error(f"toolmanifest.vdf not found in Proton directory: {path}")
return None

runtime = "scout"
f = open(toolmanifest, "r", errors="replace")
data = SteamUtils.parse_vdf(f.read())
tool_appid = data.get("manifest", {}) \
.get("require_tool_appid", {})

if "1628350" in tool_appid:
runtime = "sniper"
elif "1391110" in tool_appid:
runtime = "soldier"

return runtime

@staticmethod
def get_dist_directory(path: str) -> str:
"""
Get the sub-directory containing the wine libraries and binaries.
"""
dist_directory = path
if os.path.isdir(os.path.join(path, f"dist")):
dist_directory = os.path.join(path, f"dist")
elif os.path.isdir(os.path.join(path, f"files")):
dist_directory = os.path.join(path, f"files")
else:
logging.warning(f"No /dist or /files sub-directory was found under this Proton directory: {path}")

return dist_directory
60 changes: 32 additions & 28 deletions bottles/backend/wine/winecommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bottles.backend.utils.gpu import GPUUtils
from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.utils.terminal import TerminalUtils
from bottles.backend.utils.steam import SteamUtils

logging = Logger()

Expand Down Expand Up @@ -98,7 +99,7 @@ def __init__(
self.minimal = minimal
self.arguments = arguments
self.cwd = self._get_cwd(cwd)
self.runner = self._get_runner()
self.runner, self.runner_runtime = self._get_runner_info()
self.command = self.get_cmd(command, post_script)
self.terminal = terminal
self.env = self.get_env(environment)
Expand Down Expand Up @@ -151,10 +152,15 @@ def get_env(self, environment: Optional[dict] = None, return_steam_env: bool = F
if environment is None:
environment = {}

bottle = ManagerUtils.get_bottle_path(config)
runner_path = ManagerUtils.get_runner_path(config.Runner)

if config.Environment == "Steam":
bottle = config.Path
else:
bottle = ManagerUtils.get_bottle_path(config)
runner_path = config.RunnerPath

if SteamUtils.is_proton(runner_path):
runner_path = SteamUtils.get_dist_directory(runner_path)

# Clean some env variables which can cause trouble
# ref: <https://github.com/bottlesdevs/Bottles/issues/2127>
Expand Down Expand Up @@ -224,7 +230,6 @@ def get_env(self, environment: Optional[dict] = None, return_steam_env: bool = F
logging.warning("Bottles runtime was requested but not found")

# Get Runner libraries
runner_path = ManagerUtils.get_runner_path(config.Runner)
if arch == "win64":
runner_libs = [
"lib",
Expand Down Expand Up @@ -401,36 +406,26 @@ def get_env(self, environment: Optional[dict] = None, return_steam_env: bool = F

return env.get()["envs"]

def _get_runner(self) -> str:
def _get_runner_info(self) -> (str, str):
config = self.config
runner = config.Runner
runner = ManagerUtils.get_runner_path(config.Runner)
arch = config.Arch
runner_runtime = None

if config.Environment == "Steam":
runner = config.RunnerPath

if runner in [None, ""]:
return ""

if "Proton" in runner \
and "lutris" not in runner \
and config.Environment != "Steam":
if SteamUtils.is_proton(runner):
'''
If the runner is Proton, set the pat to /dist or /files
If the runner is Proton, set the path to /dist or /files
based on check if files exists.
Additionally, check for its corresponding runtime.
'''
_runner = f"{runner}/files"
if os.path.exists(f"{Paths.runners}/{runner}/dist"):
_runner = f"{runner}/dist"
runner = f"{Paths.runners}/{_runner}/bin/wine"

elif config.Environment == "Steam":
'''
If the environment is Steam, runner path is defined
in the bottle configuration and point to the right
main folder.
'''
runner = f"{runner}/bin/wine"
runner_runtime = SteamUtils.get_associated_runtime(runner)
runner = os.path.join(SteamUtils.get_dist_directory(runner), f"bin/wine")

elif runner.startswith("sys-"):
'''
Expand All @@ -440,14 +435,14 @@ def _get_runner(self) -> str:
runner = shutil.which("wine")

else:
runner = f"{Paths.runners}/{runner}/bin/wine"
runner = f"{runner}/bin/wine"

if arch == "win64":
runner = f"{runner}64"

runner = runner.replace(" ", "\\ ")

return runner
return runner, runner_runtime

def get_cmd(
self,
Expand Down Expand Up @@ -511,13 +506,22 @@ def get_cmd(
_picked = {}

if _rs:
if "soldier" in _rs.keys() and "proton" in self.runner.lower():
'''
Soldier doesn't works with Soda/Caffe and maybe other Wine runners, but it
works with Proton. So, if the runner is Proton, use the soldier runtime.
if "sniper" in _rs.keys() and "sniper" in self.runner_runtime:
'''
Sniper is the default runtime used by Proton version >= 8.0
'''
_picked = _rs["sniper"]
if "soldier" in _rs.keys() and "soldier" in self.runner_runtime:
'''
Sniper is the default runtime used by Proton version >= 5.13 and < 8.0
'''
_picked = _rs["soldier"]
elif "scout" in _rs.keys():
'''
For Wine runners, we cannot make assumption about which runtime would suits
them the best, as it would depend on their build environment.
Sniper/Soldier are not backward-compatible, defaulting to Scout should maximize compatibility.
'''
_picked = _rs["scout"]
else:
logging.warning("Steam runtime was requested but not found")
Expand Down
11 changes: 11 additions & 0 deletions bottles/backend/wine/wineserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from bottles.backend.logger import Logger
from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.utils.proc import ProcUtils
from bottles.backend.utils.steam import SteamUtils
from bottles.backend.wine.wineprogram import WineProgram

logging = Logger()
Expand Down Expand Up @@ -34,6 +35,9 @@ def is_alive(self):
bottle = config.Path
runner = config.RunnerPath

if SteamUtils.is_proton(runner):
runner = SteamUtils.get_dist_directory(runner)

env = os.environ.copy()
env["WINEPREFIX"] = bottle
env["PATH"] = f"{runner}/bin:{env['PATH']}"
Expand All @@ -56,6 +60,13 @@ def wait(self):
bottle = ManagerUtils.get_bottle_path(config)
runner = ManagerUtils.get_runner_path(config.Runner)

if config.Environment == "Steam":
bottle = config.Path
runner = config.RunnerPath

if SteamUtils.is_proton(runner):
runner = SteamUtils.get_dist_directory(runner)

env = os.environ.copy()
env["WINEPREFIX"] = bottle
env["PATH"] = f"{runner}/bin:{env['PATH']}"
Expand Down
11 changes: 10 additions & 1 deletion com.usebottles.bottles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ add-extensions:
version: "43"
no-autodownload: true

com.valvesoftware.Steam.CompatibilityTool:
subdirectories: true
directory: share/steam/compatibilitytools.d
version: stable
versions: stable;beta;test
no-autodownload: true
autodelete: false

com.valvesoftware.Steam.Utility:
subdirectories: true
directory: utils
Expand All @@ -49,7 +57,7 @@ add-extensions:
add-ld-path: lib
merge-dirs: bin
no-autodownload: true
autodelete: true
autodelete: false

x-compat-i386-opts: &compat_i386_opts
prepend-pkg-config-path: /app/lib32/pkgconfig:/usr/lib/i386-linux-gnu/pkgconfig
Expand Down Expand Up @@ -77,6 +85,7 @@ cleanup:

cleanup-commands:
- mkdir -p /app/utils
- mkdir -p /app/share/steam/compatibilitytools.d

modules:
# PYPI modules
Expand Down

0 comments on commit d8e142d

Please sign in to comment.