#!/usr/bin/env python3 #script to launch Wine with the correct environment from __future__ import print_function import filecmp import json import os import shutil import errno import struct import subprocess import sys import tarfile from filelock import FileLock #To enable debug logging, copy "user_settings.sample.py" to "user_settings.py" #and edit it if needed. CURRENT_PREFIX_VERSION="3.16-1" PFX="Proton: " ld_path_var = "LD_LIBRARY_PATH" def nonzero(s): return len(s) > 0 and s != "0" def log(msg): sys.stderr.write(PFX + msg + os.linesep) sys.stderr.flush() def remove_tracked_files(prefix): if not os.path.exists(prefix + "/tracked_files"): log("Prefix has no tracked_files??") return with open(prefix + "/tracked_files", "r") as tracked_files: dirs = [] for f in tracked_files: path = prefix + "/pfx/" + f.strip() if os.path.exists(path): if os.path.isfile(path) or os.path.islink(path): os.remove(path) else: dirs.append(path) for d in dirs: try: os.rmdir(d) except OSError: #not empty pass os.remove(prefix + "/tracked_files") os.remove(prefix + "/version") def upgrade_pfx(old_ver): if old_ver == CURRENT_PREFIX_VERSION: return log("Upgrading prefix from " + str(old_ver) + " to " + CURRENT_PREFIX_VERSION + " (" + os.environ["STEAM_COMPAT_DATA_PATH"] + ")") if old_ver is None: return if not '-' in old_ver: #How can this happen?? log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.") #If it does, just let the Wine upgrade happen and hope it works... return try: old_proton_ver, old_prefix_ver = old_ver.split('-') old_proton_maj, old_proton_min = old_proton_ver.split('.') new_proton_ver, new_prefix_ver = CURRENT_PREFIX_VERSION.split('-') new_proton_maj, new_proton_min = new_proton_ver.split('.') if int(new_proton_maj) < int(old_proton_maj) or \ (int(new_proton_maj) == int(old_proton_maj) and \ int(new_proton_min) < int(old_proton_min)): log("Removing newer prefix") if old_proton_ver == "3.7" and not os.path.exists(os.environ["STEAM_COMPAT_DATA_PATH"] + "/tracked_files"): #proton 3.7 did not generate tracked_files, so copy it into place first try_copy(basedir + "/proton_3.7_tracked_files", os.environ["STEAM_COMPAT_DATA_PATH"] + "/tracked_files") remove_tracked_files(os.environ["STEAM_COMPAT_DATA_PATH"]) return if old_proton_ver == "3.7" and old_prefix_ver == "1": if not os.path.exists(prefix + "/drive_c/windows/syswow64/kernel32.dll"): #shipped a busted 64-bit-only installation on 20180822. detect and wipe clean log("Detected broken 64-bit-only installation, re-creating prefix.") shutil.rmtree(prefix) return except ValueError: log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.") #Just let the Wine upgrade happen and hope it works... return def run_wine(args): subprocess.call(args, env=env, stderr=lfile) def makedirs(path): try: os.makedirs(path) except OSError: #already exists pass def try_copy(src, dst): try: shutil.copy(src, dst) except PermissionError as e: if e.errno == errno.EPERM: #be forgiving about permissions errors; if it's a real problem, things will explode later anyway log('Error while copying to \"' + dst + '\": ' + e.strerror) else: raise def real_copy(src, dst): if os.path.islink(src): os.symlink(os.readlink(src), dst) else: try_copy(src, dst) def mergedirs(src, dst, tracked_files): for src_dir, dirs, files in os.walk(src): rel_dir = src_dir.replace(src, "", 1).lstrip('/') if len(rel_dir) > 0: rel_dir = rel_dir + "/" dst_dir = src_dir.replace(src, dst, 1) if not os.path.exists(dst_dir): os.makedirs(dst_dir) tracked_files.write(rel_dir + "\n") for dir_ in dirs: src_file = os.path.join(src_dir, dir_) dst_file = os.path.join(dst_dir, dir_) if os.path.islink(src_file) and not os.path.exists(dst_file): real_copy(src_file, dst_file) for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) if not os.path.exists(dst_file): real_copy(src_file, dst_file) tracked_files.write(rel_dir + file_ + "\n") if not "STEAM_COMPAT_DATA_PATH" in os.environ: log("No compat data path?") sys.exit(1) basedir = os.path.dirname(sys.argv[0]) bindir = basedir + "/dist/bin/" libdir = basedir + "/dist/lib" lib64dir = basedir + "/dist/lib64" fontsdir = basedir + "/dist/share/fonts" wine_path = bindir + "/wine" #extract if needed dist_lock = FileLock(basedir + "/dist.lock", timeout=-1) with dist_lock: if not os.path.exists(basedir + "/dist") or \ not os.path.exists(basedir + "/dist/version") or \ not filecmp.cmp(basedir + "/version", basedir + "/dist/version"): if os.path.exists(basedir + "/dist"): shutil.rmtree(basedir + "/dist") tar = tarfile.open(basedir + "/proton_dist.tar.gz", mode="r:gz") tar.extractall(path=basedir + "/dist") tar.close() try_copy(basedir + "/version", basedir + "/dist/") env = dict(os.environ) dlloverrides = {} if "HOST_LC_ALL" in env and len(env["HOST_LC_ALL"]) > 0: #steam sets LC_ALL=C to help some games, but Wine requires the real value #in order to do path conversion between win32 and host. steam sets #HOST_LC_ALL to allow us to use the real value. env["LC_ALL"] = env["HOST_LC_ALL"] else: env.pop("LC_ALL", "") #for performance, logging is disabled by default; override with user_settings.py env["DXVK_LOG_LEVEL"] = "none" env["WINEDEBUG"] = "-all" env.pop("WINEARCH", "") if ld_path_var in os.environ: env[ld_path_var] = lib64dir + ":" + libdir + ":" + os.environ[ld_path_var] else: env[ld_path_var] = lib64dir + ":" + libdir env["WINEDLLPATH"] = lib64dir + "/wine:" + libdir + "/wine" if "PATH" in os.environ: env["PATH"] = bindir + ":" + os.environ["PATH"] else: env["PATH"] = bindir with dist_lock: if not os.path.isdir(basedir + "/dist/share/default_pfx"): #make default prefix env["WINEPREFIX"] = basedir + "/dist/share/default_pfx" run_wine([wine_path, "wineboot"]) run_wine([bindir + "/wineserver", "-w"]) prefix = os.environ["STEAM_COMPAT_DATA_PATH"] + "/pfx/" env["WINEPREFIX"] = prefix if "PROTON_LOG" in env and nonzero(env["PROTON_LOG"]): env["WINEDEBUG"] = "+timestamp,+pid,+tid,+seh,+debugstr,+module" #load environment overrides if os.path.exists(basedir + "/user_settings.py"): try: import user_settings env.update(user_settings.user_settings) except: log("************************************************") log("THERE IS AN ERROR IN YOUR user_settings.py FILE:") log("%s" % sys.exc_info()[1]) log("************************************************") def check_environment(env_name, config_name): if not env_name in env: return False if nonzero(env[env_name]): config_opts.add(config_name) else: config_opts.discard(config_name) return True if "STEAM_COMPAT_CONFIG" in os.environ: config_opts = set(os.environ["STEAM_COMPAT_CONFIG"].split(",")) else: config_opts = set() if "wined3d11" in config_opts: config_opts.add("wined3d") if not check_environment("PROTON_USE_WINED3D", "wined3d"): check_environment("PROTON_USE_WINED3D11", "wined3d") check_environment("PROTON_NO_D3D11", "nod3d11") check_environment("PROTON_NO_D3D10", "nod3d10") check_environment("PROTON_NO_ESYNC", "noesync") check_environment("PROTON_FORCE_LARGE_ADDRESS_AWARE", "forcelgadd") check_environment("PROTON_OLD_GL_STRING", "oldglstr") if not "noesync" in config_opts: env["WINEESYNC"] = "1" if "oldglstr" in config_opts: #mesa override env["MESA_EXTENSION_MAX_YEAR"] = "2003" #nvidia override env["__GL_ExtensionStringVersion"] = "17700" if "forcelgadd" in config_opts: #forcelgadd should be used just for testing whether a game is helped by #setting LARGE_ADDRESS_AWARE. If it does, then add an AppDefault in the #registry, so that it doesn't impact every executable in the prefix. env["WINE_LARGE_ADDRESS_AWARE"] = "1" lfile = None if "SteamGameId" in env: if env["WINEDEBUG"] != "-all": lfile_path = os.environ["HOME"] + "/steam-" + os.environ["SteamGameId"] + ".log" if os.path.exists(lfile_path): os.remove(lfile_path) lfile = open(lfile_path, "w+") lfile.write("======================\n") with open(basedir + "/version", "r") as f: lfile.write("Proton: " + f.readline().strip() + "\n") lfile.write("SteamGameId: " + env["SteamGameId"] + "\n") lfile.write("Command: " + str(sys.argv[2:]) + "\n") lfile.write("======================\n") lfile.flush() else: env["WINEDEBUG"] = "-all" def create_fonts_symlinks(prefix_path): fontsmap = [ ( "LiberationSans-Regular.ttf", "arial.ttf" ), ( "LiberationSans-Bold.ttf", "arialbd.ttf" ), ( "LiberationSerif-Regular.ttf", "times.ttf" ), ( "LiberationMono-Regular.ttf", "cour.ttf" ), ] windowsfonts = prefix_path + "/drive_c/windows/Fonts" makedirs(windowsfonts) for p in fontsmap: lname = os.path.join(windowsfonts, p[1]) fname = os.path.join(fontsdir, p[0]) if os.path.lexists(lname): if os.path.islink(lname): os.remove(lname) os.symlink(fname, lname) else: os.symlink(fname, lname) prefix_lock = FileLock(os.environ["STEAM_COMPAT_DATA_PATH"] + "/pfx.lock", timeout=-1) with prefix_lock: version_file = os.environ["STEAM_COMPAT_DATA_PATH"] + "/version" if os.path.exists(version_file): with open(version_file, "r") as f: upgrade_pfx(f.readline().strip()) else: upgrade_pfx(None) if not os.path.exists(prefix + "/user.reg"): #copy default prefix into place with open(os.environ["STEAM_COMPAT_DATA_PATH"] + "/tracked_files", "w") as tfiles: mergedirs(basedir + "/dist/share/default_pfx", prefix, tfiles) with open(version_file, "w") as f: f.write(CURRENT_PREFIX_VERSION + "\n") #create font files symlinks create_fonts_symlinks(prefix) #copy steam files into place if "STEAM_COMPAT_CLIENT_INSTALL_PATH" in os.environ: #modern steam client sets this steamdir = os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] else: #linux-only fallback, really shouldn't get here steamdir = os.environ["HOME"] + ".steam/root/" dst = prefix + "/drive_c/Program Files (x86)/" makedirs(dst + "Steam") filestocopy = ["steamclient.dll", "steamclient64.dll", "Steam.dll"] for f in filestocopy: if os.path.isfile(steamdir + "/legacycompat/" + f): dstfile = dst + "Steam/" + f if os.path.isfile(dstfile): os.remove(dstfile) try_copy(steamdir + "/legacycompat/" + f, dstfile) #copy openvr files into place dst = prefix + "/drive_c/vrclient/bin/" makedirs(dst) try_copy(basedir + "/dist/lib/wine/fakedlls/vrclient.dll", dst) try_copy(basedir + "/dist/lib64/wine/fakedlls/vrclient_x64.dll", dst) try_copy(basedir + "/dist/lib/wine/dxvk/openvr_api_dxvk.dll", prefix + "/drive_c/windows/syswow64/") try_copy(basedir + "/dist/lib64/wine/dxvk/openvr_api_dxvk.dll", prefix + "/drive_c/windows/system32/") #parse linux openvr config and present it in win32 format to the app. #logic from openvr's CVRPathRegistry_Public::GetPaths #check environment for overrides vr_runtime = None if "VR_OVERRIDE" in env: vr_runtime = env["VR_OVERRIDE"] env.pop("VR_OVERRIDE") vr_config = None if "VR_CONFIG_PATH" in env: vr_config = env["VR_CONFIG_PATH"] env.pop("VR_CONFIG_PATH") vr_log = None if "VR_LOG_PATH" in env: vr_log = env["VR_LOG_PATH"] env.pop("VR_LOG_PATH") #load from json if needed if vr_runtime is None or \ vr_config is None or \ vr_log is None: try: path = os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config") path = path + "/openvr/openvrpaths.vrpath" with open(path, "r") as jfile: j = json.load(jfile) if vr_runtime is None: vr_runtime = j["runtime"][0] if vr_config is None: vr_config = j["config"][0] if vr_log is None: vr_log = j["log"][0] except (TypeError, ValueError, OSError): log("Missing or invalid openvrpaths.vrpath file! " + str(sys.exc_info()[1])) makedirs(prefix + "/drive_c/users/steamuser/Local Settings/Application Data/openvr") #remove existing file vrpaths_name = prefix + "/drive_c/users/steamuser/Local Settings/Application Data/openvr/openvrpaths.vrpath" if os.path.exists(vrpaths_name): os.remove(vrpaths_name) #dump new file if not vr_runtime is None: try: env["PROTON_VR_RUNTIME"] = vr_runtime j = { "runtime": [ "C:\\vrclient\\", "C:\\vrclient" ] } if not vr_config is None: win_vr_config = subprocess.check_output([wine_path, "winepath", "-w", vr_config], env=env, stderr=lfile).decode("utf-8") j["config"] = [ win_vr_config.strip() ] if not vr_log is None: win_vr_log = subprocess.check_output([wine_path, "winepath", "-w", vr_log], env=env, stderr=lfile).decode("utf-8") j["log"] = [ win_vr_log.strip() ] j["version"] = 1 j["jsonid"] = "vrpathreg" with open(vrpaths_name, "w") as vfile: json.dump(j, vfile, indent=2) except (ValueError, OSError): log("Unable to write VR config! " + str(sys.exc_info()[1])) dxvkfiles = ("d3d11", "d3d10", "d3d10core", "d3d10_1", "dxgi") def make_dxvk_links(dll_dir, link_dir): for f in dxvkfiles: dst = link_dir + "/" + f + ".dll" src = dll_dir + "/" + f + ".dll" if os.path.lexists(dst): os.remove(dst) os.symlink(src, dst) if "wined3d" in config_opts: #use gl-based wined3d for d3d11 and d3d10 make_dxvk_links(basedir + "/dist/lib64/wine/fakedlls/", prefix + "drive_c/windows/system32") make_dxvk_links(basedir + "/dist/lib/wine/fakedlls/", prefix + "drive_c/windows/syswow64") else: #use vulkan-based dxvk for d3d11 and d3d10 make_dxvk_links(basedir + "/dist/lib64/wine/dxvk/", prefix + "drive_c/windows/system32") make_dxvk_links(basedir + "/dist/lib/wine/dxvk/", prefix + "drive_c/windows/syswow64") for f in dxvkfiles: dlloverrides[f] = "n" if "nod3d11" in config_opts: dlloverrides["d3d11"] = "" if "dxgi" in dlloverrides: del dlloverrides["dxgi"] if "nod3d10" in config_opts: dlloverrides["d3d10_1"] = "" dlloverrides["d3d10"] = "" dlloverrides["dxgi"] = "" s = "" for dll in dlloverrides: setting = dlloverrides[dll] if len(s) > 0: s = s + ";" + dll + "=" + setting else: s = dll + "=" + setting if "WINEDLLOVERRIDES" in os.environ: env["WINEDLLOVERRIDES"] = os.environ["WINEDLLOVERRIDES"] + ";" + s else: env["WINEDLLOVERRIDES"] = s ARCH_UNKNOWN=0 ARCH_I386=1 ARCH_X86_64=2 def determine_architecture(path): #algorithm from file's msdos magic file try: with open(path, "rb") as f: magic = f.read(2) if magic != b"MZ": return ARCH_UNKNOWN f.seek(0x18) reloc = struct.unpack(' windows path path = subprocess.check_output([wine_path, "winepath", "-w", sys.argv[2]], env=env, stderr=lfile) binary_stdout.write(path) elif sys.argv[1] == "getnativepath": #windows -> linux path path = subprocess.check_output([wine_path, "winepath", sys.argv[2]], env=env, stderr=lfile) binary_stdout.write(path) else: log("Need a verb.") sys.exit(1) sys.exit(0) # vim: set syntax=python: