#!/usr/bin/env python3 #script to launch Wine with the correct environment from __future__ import print_function import fcntl import array 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="4.11-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 file_is_wine_fake_dll(path): if not os.path.exists(path): return False try: sfile = open(path, "rb") sfile.seek(0x40) tag = sfile.read(20) return tag == b"Wine placeholder DLL" except IOError: return False 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 #replace broken .NET installations with wine-mono support if os.path.exists(prefix + "/drive_c/windows/Microsoft.NET/NETFXRepair.exe") and \ file_is_wine_fake_dll(prefix + "/drive_c/windows/system32/mscoree.dll"): log("Broken .NET installation detected, switching to wine-mono.") #deleting this directory allows wine-mono to work shutil.rmtree(prefix + "/drive_c/windows/Microsoft.NET") 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, stdout=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 = {"steam.exe": "b"} #always use our special built-in steam.exe 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,+loaddll,+mscoree" env["DXVK_LOG_LEVEL"] = "info" env["WINE_MONO_TRACE"] = "E:System.NotImplementedException" #default wine-mono override for FNA games env["WINE_MONO_OVERRIDES"] = "Microsoft.Xna.Framework.*,Gac=n" #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_USE_D9VK", "d9vk") check_environment("PROTON_NO_D3D11", "nod3d11") check_environment("PROTON_NO_D3D10", "nod3d10") check_environment("PROTON_NO_ESYNC", "noesync") check_environment("PROTON_NO_FSYNC", "nofsync") 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 not "nofsync" in config_opts: env["WINEFSYNC"] = "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("Options: " + str(config_opts) + "\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) EXT2_IOC_GETFLAGS = 0x80086601 EXT2_IOC_SETFLAGS = 0x40086602 EXT4_CASEFOLD_FL = 0x40000000 def set_dir_casefold_bit(dir_path): dr = os.open(dir_path, 0o644) if dr < 0: return try: dat = array.array('I', [0]) if fcntl.ioctl(dr, EXT2_IOC_GETFLAGS, dat, True) >= 0: dat[0] = dat[0] | EXT4_CASEFOLD_FL fcntl.ioctl(dr, EXT2_IOC_SETFLAGS, dat, False) except OSError: #no problem pass os.close(dr) 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): makedirs(prefix + "/drive_c") set_dir_casefold_bit(prefix + "/drive_c") 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])) if "wined3d" in config_opts: dxvkfiles = [] wined3dfiles = ["d3d11", "d3d10", "d3d10core", "d3d10_1", "dxgi", "d3d9"] else: dxvkfiles = ["d3d11", "d3d10", "d3d10core", "d3d10_1", "dxgi"] wined3dfiles = [] if "d9vk" in config_opts: dxvkfiles.append("d3d9") else: wined3dfiles.append("d3d9") for f in wined3dfiles: try_copy(basedir + "/dist/share/default_pfx/drive_c/windows/system32/" + f + ".dll", prefix + "drive_c/windows/system32/" + f + ".dll") try_copy(basedir + "/dist/share/default_pfx/drive_c/windows/syswow64/" + f + ".dll", prefix + "drive_c/windows/syswow64/" + f + ".dll") for f in dxvkfiles: try_copy(basedir + "/dist/lib64/wine/dxvk/" + f + ".dll", prefix + "drive_c/windows/system32/" + f + ".dll") try_copy(basedir + "/dist/lib/wine/dxvk/" + f + ".dll", prefix + "drive_c/windows/syswow64/" + f + ".dll") 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 def dump_dbg_env(f): f.write("PATH=\"" + env["PATH"] + "\" \\\n") f.write("\tTERM=\"xterm\" \\\n") #XXX f.write("\tWINEDEBUG=\"-all\" \\\n") f.write("\tWINEDLLPATH=\"" + env["WINEDLLPATH"] + "\" \\\n") f.write("\t" + ld_path_var + "=\"" + env[ld_path_var] + "\" \\\n") f.write("\tWINEPREFIX=\"" + env["WINEPREFIX"] + "\" \\\n") if "WINEESYNC" in env: f.write("\tWINEESYNC=\"" + env["WINEESYNC"] + "\" \\\n") if "SteamGameId" in env: f.write("\tSteamGameId=\"" + env["SteamGameId"] + "\" \\\n") if "SteamAppId" in env: f.write("\tSteamAppId=\"" + env["SteamAppId"] + "\" \\\n") if "PROTON_VR_RUNTIME" in env: f.write("\tPROTON_VR_RUNTIME=\"" + env["PROTON_VR_RUNTIME"] + "\" \\\n") if "WINEDLLOVERRIDES" in env: f.write("\tWINEDLLOVERRIDES=\"" + env["WINEDLLOVERRIDES"] + "\" \\\n") if "STEAM_COMPAT_CLIENT_INSTALL_PATH" in env: f.write("\tSTEAM_COMPAT_CLIENT_INSTALL_PATH=\"" + env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] + "\" \\\n") if "WINE_LARGE_ADDRESS_AWARE" in env: f.write("\tWINE_LARGE_ADDRESS_AWARE=\"" + env["WINE_LARGE_ADDRESS_AWARE"] + "\" \\\n") def dump_dbg_scripts(): exe_name = os.path.basename(sys.argv[2]) tmpdir = env.get("PROTON_DEBUG_DIR", "/tmp") + "/proton_" + os.environ["USER"] + "/" makedirs(tmpdir) with open(tmpdir + "winedbg", "w") as f: f.write("#!/bin/bash\n") f.write("#Run winedbg with args\n\n") f.write("cd \"" + os.getcwd() + "\"\n") dump_dbg_env(f) f.write("\t\"" + wine_path + "\" winedbg \"$@\"\n") os.chmod(tmpdir + "winedbg", 0o755) with open(tmpdir + "winedbg_run", "w") as f: f.write("#!/bin/bash\n") f.write("#Run winedbg and prepare to run game or given program\n\n") f.write("cd \"" + os.getcwd() + "\"\n") f.write("DEF_CMD=(") first = True for arg in sys.argv[2:]: if first: f.write("\"" + arg + "\"") first = False else: f.write(" \"" + arg + "\"") f.write(")\n") dump_dbg_env(f) f.write("\t\"" + wine_path + "\" winedbg \"${@:-${DEF_CMD[@]}}\"\n") os.chmod(tmpdir + "winedbg_run", 0o755) with open(tmpdir + "gdb_attach", "w") as f: f.write("#!/bin/bash\n") f.write("#Run winedbg in gdb mode and auto-attach to already-running program\n\n") f.write("cd \"" + os.getcwd() + "\"\n") f.write("EXE_NAME=${1:-\"" + exe_name + "\"}\n") f.write("WPID_HEX=$(\"" + tmpdir + "winedbg\" --command 'info process' | grep -i \"$EXE_NAME\" | cut -f2 -d' ' | tr -d '0')\n") f.write("if [ -z \"$WPID_HEX\" ]; then \n") f.write(" echo \"Program does not appear to be running: \\\"$EXE_NAME\\\"\"\n") f.write(" exit 1\n") f.write("fi\n") f.write("WPID_DEC=$(printf %d 0x$WPID_HEX)\n") dump_dbg_env(f) f.write("\t\"" + wine_path + "\" winedbg --gdb $WPID_DEC\n") os.chmod(tmpdir + "gdb_attach", 0o755) with open(tmpdir + "gdb_run", "w") as f: f.write("#!/bin/bash\n") f.write("#Run winedbg in gdb mode and prepare to run game or given program\n\n") f.write("cd \"" + os.getcwd() + "\"\n") f.write("DEF_CMD=(") first = True for arg in sys.argv[2:]: if first: f.write("\"" + arg + "\"") first = False else: f.write(" \"" + arg + "\"") f.write(")\n") dump_dbg_env(f) f.write("\t\"" + wine_path + "\" winedbg --gdb \"${@:-${DEF_CMD[@]}}\"\n") os.chmod(tmpdir + "gdb_run", 0o755) with open(tmpdir + "run", "w") as f: f.write("#!/bin/bash\n") f.write("#Run game or given command in environment\n\n") f.write("cd \"" + os.getcwd() + "\"\n") f.write("DEF_CMD=(") first = True for arg in sys.argv[2:]: if first: f.write("\"" + arg + "\"") first = False else: f.write(" \"" + arg + "\"") f.write(")\n") dump_dbg_env(f) f.write("\t\"" + wine_path + "\" steam.exe \"${@:-${DEF_CMD[@]}}\"\n") os.chmod(tmpdir + "run", 0o755) def run(): if "PROTON_DUMP_DEBUG_COMMANDS" in env and nonzero(env["PROTON_DUMP_DEBUG_COMMANDS"]): try: dump_dbg_scripts() except OSError: log("Unable to write debug scripts! " + str(sys.exc_info()[1])) run_wine([wine_path, "steam"] + sys.argv[2:]) if sys.version_info[0] == 2: binary_stdout = sys.stdout elif sys.version_info[0] == 3: binary_stdout = sys.stdout.buffer else: raise Exception("Unsupported python version") #determine mode if sys.argv[1] == "run": #start target app run() elif sys.argv[1] == "waitforexitandrun": #wait for wineserver to shut down run_wine([bindir + "/wineserver", "-w"]) #then run run() elif sys.argv[1] == "getcompatpath": #linux -> 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: