#!/usr/bin/env python3 #script to launch Wine with the correct environment import fcntl import array import filecmp import fnmatch import json import os import os.path import shutil import errno import stat import subprocess import sys import tarfile #To enable debug logging, copy "user_settings.sample.py" to "user_settings.py" #and edit it if needed. 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 makedirs(path): try: os.makedirs(path) except OSError: #already exists pass def try_copy(src, dst, add_write_perm=True): try: if os.path.isdir(dst): dstfile = dst + "/" + os.path.basename(src) if os.path.lexists(dstfile): os.remove(dstfile) else: dstfile = dst if os.path.lexists(dst): os.remove(dst) shutil.copy(src, dst) if add_write_perm: new_mode = os.lstat(dstfile).st_mode | stat.S_IWUSR | stat.S_IWGRP os.chmod(dstfile, new_mode) 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 try_copyfile(src, dst): try: if os.path.isdir(dst): dstfile = dst + "/" + os.path.basename(src) if os.path.lexists(dstfile): os.remove(dstfile) elif os.path.lexists(dst): os.remove(dst) shutil.copyfile(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 getmtimestr(*path_fragments): path = os.path.join(*path_fragments) try: return str(os.path.getmtime(path)) except IOError: return "0" class Proton: def __init__(self, base_dir): self.base_dir = os.environ["PW_COMPAT_DATA_PATH"] self.dist_dir = os.environ["WINEDIR"] self.bin_dir = self.dist_dir + "/bin/" self.lib_dir = self.dist_dir + "/lib/" self.lib64_dir = self.dist_dir + "/lib64/" self.fonts_dir = self.dist_dir + "/share/fonts/" self.wine_bin = self.bin_dir + "/wine" self.wineserver_bin = self.bin_dir + "/wineserver" self.gamemoderun = "gamemoderun" def path(self, d): return self.base_dir + d class CompatData: def __init__(self, compatdata): self.base_dir = os.environ["PW_COMPAT_DATA_PATH"] self.prefix_dir = self.path("pfx/") def path(self, d): return self.base_dir + d def create_fonts_symlinks(self): fontsmap = [ ( "LiberationSans-Regular.ttf", "arial.ttf" ), ( "LiberationSans-Bold.ttf", "arialbd.ttf" ), ( "LiberationSerif-Regular.ttf", "times.ttf" ), ( "LiberationMono-Regular.ttf", "cour.ttf" ), ( "SourceHanSansSCRegular.otf", "msyh.ttf" ), ] windowsfonts = self.prefix_dir + "/drive_c/windows/Fonts" makedirs(windowsfonts) for p in fontsmap: lname = os.path.join(windowsfonts, p[1]) fname = os.path.join(g_proton.fonts_dir, p[0]) if os.path.lexists(lname): if os.path.islink(lname): os.remove(lname) os.symlink(fname, lname) else: os.symlink(fname, lname) def setup_prefix(self): if not os.path.exists(self.prefix_dir): makedirs(self.prefix_dir + "/drive_c") set_dir_casefold_bit(self.prefix_dir + "/drive_c") use_wined3d = "wined3d" in g_session.compat_config builtin_dll_copy = os.environ.get("PROTON_DLL_COPY", # #dxsetup redist # "d3dcompiler_*.dll," + # "d3dcsx*.dll," + # "d3dx*.dll," + # "x3daudio*.dll," + # "xactengine*.dll," + # "xapofx*.dll," + # "xaudio*.dll," + # "xinput*.dll," + # #vcruntime redist "atl1*.dll," + # "concrt1*.dll," + # "msvcp1*.dll," + # "msvcr1*.dll," + # "vcamp1*.dll," + # "vcomp1*.dll," + # "vccorlib1*.dll," + # "vcruntime1*.dll," + # "api-ms-win-crt-conio-l1-1-0.dll," + # "api-ms-win-crt-heap-l1-1-0.dll," + # "api-ms-win-crt-locale-l1-1-0.dll," + # "api-ms-win-crt-math-l1-1-0.dll," + # "api-ms-win-crt-runtime-l1-1-0.dll," + # "api-ms-win-crt-stdio-l1-1-0.dll," + "ucrtbase.dll," + #some games balk at ntdll symlink(?) "ntdll.dll," + #some games require official vulkan loader "vulkan-1.dll" ) #create font files symlinks self.create_fonts_symlinks() #copy openvr files into place if os.path.isfile(g_proton.lib_dir + "wine/fakedlls/vrclient.dll"): dst = self.prefix_dir + "/drive_c/vrclient/bin/" makedirs(dst) try_copy(g_proton.lib_dir + "wine/fakedlls/vrclient.dll", dst) try_copy(g_proton.lib64_dir + "wine/fakedlls/vrclient_x64.dll", dst) if os.path.isfile(g_proton.lib_dir + "wine/dxvk/openvr_api_dxvk.dll"): try_copy(g_proton.lib_dir + "wine/dxvk/openvr_api_dxvk.dll", self.prefix_dir + "/drive_c/windows/syswow64/") try_copy(g_proton.lib64_dir + "wine/dxvk/openvr_api_dxvk.dll", self.prefix_dir + "/drive_c/windows/system32/") if use_wined3d: dxvkfiles = [] wined3dfiles = ["d3d11", "d3d10", "d3d10core", "d3d10_1", "d3d9"] if os.path.isfile(g_proton.lib64_dir + "wine/dxvk/dxvk_config.dll"): dxvkfiles.append("dxvk_config") else: dxvkfiles = ["dxvk_config", "d3d11", "d3d10", "d3d10core", "d3d10_1", "d3d9"] wined3dfiles = [] #if the user asked for dxvk's dxgi (dxgi=n), then copy it into place if "PW_DXGI_FROM_DXVK" in os.environ and nonzero(os.environ["PW_DXGI_FROM_DXVK"]): dxvkfiles.append("dxgi") else: wined3dfiles.append("dxgi") for f in wined3dfiles: try_copy(g_proton.lib64_dir + "wine/" + f + ".dll", self.prefix_dir + "drive_c/windows/system32/" + f + ".dll") try_copy(g_proton.lib_dir + "wine/" + f + ".dll", self.prefix_dir + "drive_c/windows/syswow64/" + f + ".dll") for f in dxvkfiles: if os.path.isfile(g_proton.lib64_dir + "wine/dxvk/" + f + ".dll"): try_copy(g_proton.lib64_dir + "wine/dxvk/" + f + ".dll", self.prefix_dir + "drive_c/windows/system32/" + f + ".dll") try_copy(g_proton.lib_dir + "wine/dxvk/" + f + ".dll", self.prefix_dir + "drive_c/windows/syswow64/" + f + ".dll") g_session.dlloverrides[f] = "n" if os.path.isfile(g_proton.lib64_dir + "wine/vkd3d-proton/d3d12.dll"): try_copy(g_proton.lib64_dir + "wine/vkd3d-proton/d3d12.dll", self.prefix_dir + "drive_c/windows/system32/d3d12.dll") if os.path.isfile(g_proton.lib_dir + "wine/vkd3d-proton/d3d12.dll"): try_copy(g_proton.lib_dir + "wine/vkd3d-proton/d3d12.dll", self.prefix_dir + "drive_c/windows/syswow64/d3d12.dll") def comma_escaped(s): escaped = False idx = -1 while s[idx] == '\\': escaped = not escaped idx = idx - 1 return escaped class Session: def __init__(self): self.env = dict(os.environ) self.dlloverrides = { "winemenubuilder.exe": "", "dotnetfx35.exe": "b", #replace the broken installer, as does Windows "mfplay": "n", #disable built-in mfplay "steam_api": "n", #disable built-in steam dll "steam_api64": "n", #disable built-in steam dll "steamclient": "n", #disable built-in steam dll "steamclient64": "n", #disable built-in steam dll "steamworks.net": "n" #disable built-in steam dll } self.compat_config = set() self.cmdlineappend = [] def init_wine(self): self.env.pop("WINEARCH", "") if 'ORIG_'+ld_path_var not in os.environ: # Allow wine to restore this when calling an external app. self.env['ORIG_'+ld_path_var] = os.environ.get(ld_path_var, '') if ld_path_var in os.environ: self.env[ld_path_var] = g_proton.lib64_dir + ":" + g_proton.lib_dir + ":" + os.environ[ld_path_var] else: self.env[ld_path_var] = g_proton.lib64_dir + ":" + g_proton.lib_dir self.env["WINEDLLPATH"] = g_proton.lib64_dir + "/wine:" + g_proton.lib_dir + "/wine" self.env["GST_PLUGIN_SYSTEM_PATH_1_0"] = g_proton.lib64_dir + "gstreamer-1.0" + ":" + g_proton.lib_dir + "gstreamer-1.0" self.env["WINE_GST_REGISTRY_DIR"] = g_compatdata.path("/tmp/gstreamer-1.0/") if "PW_COMPAT_MEDIA_PATH" in os.environ: self.env["MEDIACONV_AUDIO_DUMP_FILE"] = os.environ["PW_COMPAT_MEDIA_PATH"] + "/audio.foz" self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] = os.environ["PW_COMPAT_MEDIA_PATH"] + "/transcoded_audio.foz" self.env["MEDIACONV_VIDEO_DUMP_FILE"] = os.environ["PW_COMPAT_MEDIA_PATH"] + "/video.foz" self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] = os.environ["PW_COMPAT_MEDIA_PATH"] + "/transcoded_video.foz" if "PATH" in os.environ: self.env["PATH"] = g_proton.bin_dir + ":" + os.environ["PATH"] else: self.env["PATH"] = g_proton.bin_dir def check_environment(self, env_name, config_name): if not env_name in self.env: return False if nonzero(self.env[env_name]): self.compat_config.add(config_name) else: self.compat_config.discard(config_name) return True def init_session(self): self.env["WINEPREFIX"] = g_compatdata.prefix_dir #load environment overrides if "PW_LOG" in os.environ and nonzero(os.environ["PW_LOG"]): self.env.setdefault("WINEDEBUG", "+timestamp,+pid,+tid,+seh,+debugstr,+loaddll,+mscoree") self.env.setdefault("DXVK_LOG_LEVEL", "info") self.env.setdefault("VKD3D_DEBUG", "warn") self.env.setdefault("WINE_MONO_TRACE", "E:System.NotImplementedException") else: self.env.setdefault("WINEDEBUG", "-all") self.env.setdefault("DXVK_LOG_LEVEL", "none") self.env.setdefault("VKD3D_DEBUG", "none") self.env.setdefault("DXVK_LOG_PATH","none") #default wine-mono override for FNA games self.env.setdefault("WINE_MONO_OVERRIDES", "Microsoft.Xna.Framework.*,Gac=n") if "wined3d11" in self.compat_config: self.compat_config.add("wined3d") if not self.check_environment("PW_USE_WINED3D", "wined3d"): self.check_environment("PW_USE_WINED3D11", "wined3d") self.check_environment("PW_NO_ESYNC", "noesync") self.check_environment("PW_NO_FSYNC", "nofsync") self.check_environment("PW_FORCE_LARGE_ADDRESS_AWARE", "forcelgadd") self.check_environment("PW_OLD_GL_STRING", "oldglstr") self.check_environment("PW_NO_WINEMFPLAY", "nomfplay") self.check_environment("PW_NO_WRITE_WATCH", "nowritewatch") self.check_environment("PW_DXVK_ASYNC", "dxvkasync") self.check_environment("PW_NVAPI_DISABLE", "nonvapi") self.check_environment("PW_WINEDBG_DISABLE", "nowinedbg") self.check_environment("PW_HIDE_NVIDIA_GPU", "hidenvgpu") self.check_environment("PW_VKD3D_FEATURE_LEVEL", "vkd3dfl12") self.check_environment("PW_DX12_DISABLED", "nod3d12") if "noesync" in self.compat_config: self.env.pop("WINEESYNC", "") else: self.env["WINEESYNC"] = "1" if "nofsync" in self.compat_config: self.env.pop("WINEFSYNC", "") else: self.env["WINEFSYNC"] = "1" if "nowritewatch" in self.compat_config: self.env["WINE_DISABLE_WRITE_WATCH"] = "1" if "oldglstr" in self.compat_config: #mesa override self.env["MESA_EXTENSION_MAX_YEAR"] = "2003" #nvidia override self.env["__GL_ExtensionStringVersion"] = "17700" if "forcelgadd" in self.compat_config: self.env["WINE_LARGE_ADDRESS_AWARE"] = "1" if "vkd3dfl12" in self.compat_config: if not "VKD3D_FEATURE_LEVEL" in self.env: self.env["VKD3D_FEATURE_LEVEL"] = "12_0" if "hidenvgpu" in self.compat_config: self.env["WINE_HIDE_NVIDIA_GPU"] = "1" g_compatdata.setup_prefix() if "nowritewatch" in self.compat_config: self.env["WINE_DISABLE_WRITE_WATCH"] = "1" if "nonvapi" in self.compat_config: self.dlloverrides["nvapi"] = "d" self.dlloverrides["nvapi64"] = "d" if "nowinedbg" in self.compat_config: self.dlloverrides["winedbg.exe"] = "d" if "nod3d12" in self.compat_config: self.dlloverrides["d3d12"] = "d" s = "" for dll in self.dlloverrides: setting = self.dlloverrides[dll] if len(s) > 0: s = s + ";" + dll + "=" + setting else: s = dll + "=" + setting if "WINEDLLOVERRIDES" in self.env: self.env["WINEDLLOVERRIDES"] = self.env["WINEDLLOVERRIDES"] + ";" + s else: self.env["WINEDLLOVERRIDES"] = s def run_proc(self, args, local_env=None): if local_env is None: local_env = self.env subprocess.call(args, env=local_env) def run(self): if "PW_GAMEMODERUN" in os.environ and nonzero(os.environ["PW_GAMEMODERUN"]): self.run_proc([g_proton.gamemoderun] + [g_proton.wine_bin] + sys.argv[2:] + sys.argv[3:] + sys.argv[4:]) else: self.run_proc([g_proton.wine_bin] + sys.argv[2:] + sys.argv[3:] + sys.argv[4:]) def init_run(self): self.run_proc([g_proton.wine_bin] + " wineboot") if __name__ == "__main__": if not "PW_COMPAT_DATA_PATH" in os.environ: log("No compat data path?") sys.exit(1) g_proton = Proton(os.path.dirname(sys.argv[0])) g_compatdata = CompatData(os.environ["PW_COMPAT_DATA_PATH"]) g_session = Session() g_session.init_wine() g_session.init_session() #determine mode if sys.argv[1] == "run": #start target app g_session.run() elif sys.argv[1] == "init_run": #first start g_session.init_run() elif sys.argv[1] == "waitforexitandrun": #wait for wineserver to shut down g_session.run_proc([g_proton.wineserver_bin, "-w"]) #then run g_session.run() elif sys.argv[1] == "getcompatpath": #linux -> windows path path = subprocess.check_output([g_proton.wine_bin, "winepath", "-w", sys.argv[2]], env=g_session.env) sys.stdout.buffer.write(path) elif sys.argv[1] == "getnativepath": #windows -> linux path path = subprocess.check_output([g_proton.wine_bin, "winepath", sys.argv[2]], env=g_session.env) sys.stdout.buffer.write(path) else: log("Need a verb.") sys.exit(1) sys.exit(0) #pylint --disable=C0301,C0326,C0330,C0111,C0103,R0902,C1801,R0914,R0912,R0915 # vim: set syntax=python: