def test_get_steam_apps_proton_precedence(self, custom_proton_factory, home_dir, steam_root, steam_dir, monkeypatch): """ Create two Proton apps with the same name but located in different paths. Only one will be returned due to precedence in the directory paths """ custom_compat_dir = home_dir / "CompatTools" monkeypatch.setenv("STEAM_EXTRA_COMPAT_TOOLS_PATHS", str(custom_compat_dir)) proton_app_a = custom_proton_factory(name="Fake Proton", compat_tool_dir=custom_compat_dir) steam_apps = get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) assert len(steam_apps) == 1 assert str(steam_apps[0].install_path) == \ str(proton_app_a.install_path) # Create a Proton app with the same name in the default directory; # this will override the former Proton app we created proton_app_b = custom_proton_factory(name="Fake Proton") steam_apps = get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) assert len(steam_apps) == 1 assert str(steam_apps[0].install_path) == \ str(proton_app_b.install_path)
def test_get_steam_apps_steamapps_case_warning(self, steam_app_factory, steam_library_factory, steam_root, steam_dir, caplog): """ Ensure a warning is logged if both 'steamapps' and 'SteamApps' directories exist at one of the Steam library directories """ get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) # No log was created yet assert len([ record for record in caplog.records if record.levelname == "WARNING" ]) == 0 (steam_dir / "SteamApps").mkdir() get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) # Warning was logged due to two Steam app directories log = next(record for record in caplog.records if record.levelname == "WARNING") assert ("directories were found at {}".format(str(steam_dir)) in log.getMessage())
def test_get_steam_apps_missing_library_folder(self, steam_library_factory, steam_dir, steam_root, caplog): """ Create multiple Steam library folders, delete one of them and ensure a warning is printed. This can happen if Protontricks is executed inside a Flatpak sandbox without the necessary filesystem permissions. """ library_dir_a = steam_library_factory(name="LibraryA") library_dir_b = steam_library_factory(name="LibraryB") # Delete library B shutil.rmtree(str(library_dir_b)) get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[library_dir_a, library_dir_b]) warnings = [ record for record in caplog.records if record.levelname == "WARNING" ] assert len(warnings) == 1 warning = warnings[0] assert "{} not found.".format(str(library_dir_b)) in warning.message
def test_get_steam_apps_in_library_folder(self, default_proton, steam_library_factory, steam_app_factory, steam_dir, steam_root): """ Create two games, one installed in the Steam installation directory and another in a Steam library folder """ library_dir = steam_library_factory(name="GameDrive") steam_app_factory(name="Fake game 1", appid=10) steam_app_factory(name="Fake game 2", appid=20, library_dir=library_dir) steam_apps = get_steam_apps( steam_root=str(steam_root), steam_path=str(steam_dir), steam_lib_paths=[str(steam_dir), str(library_dir)]) # Two games and the default Proton installation should be found assert len(steam_apps) == 3 steam_app_a = next(app for app in steam_apps if app.appid == 10) steam_app_b = next(app for app in steam_apps if app.appid == 20) assert steam_app_a.install_path == \ str(steam_dir / "steamapps" / "common" / "Fake game 1") assert steam_app_b.install_path == \ str(library_dir / "steamapps" / "common" / "Fake game 2")
def test_get_steam_apps_custom_proton_corrupted_compatibilitytool( self, custom_proton_factory, steam_dir, steam_root, caplog): """ Create a custom Proton installation with a corrupted compatibilitytool.vdf and ensure a warning is printed and the app is ignored """ custom_proton = custom_proton_factory(name="Custom Proton") (custom_proton.install_path / "compatibilitytool.vdf").write_text("corrupted") steam_apps = get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) # Custom Proton is skipped due to empty tool manifest assert not any(app for app in steam_apps if app.name == "Custom Proton") assert len([ record for record in caplog.records if record.levelname == "WARNING" ]) == 1 record = next(record for record in caplog.records if record.levelname == "WARNING") assert record.getMessage().startswith( "Compatibility tool declaration at")
def test_get_steam_apps_steamapps_case_insensitive_fs( self, monkeypatch, steam_root, steam_dir, caplog): """ Ensure that the "'steamapps' and 'SteamApps' both exist" warning is not printed if a case-insensitive file system is in use Regression test for https://github.com/Matoking/protontricks/issues/112 """ def _mock_is_dir(self): return self.name in ("steamapps", "SteamApps", "steam") # Mock the "existence" of both 'steamapps' and 'SteamApps' by # monkeypatching pathlib monkeypatch.setattr("pathlib.Path.is_dir", _mock_is_dir) get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir]) # No warning is printed assert len([ record for record in caplog.records if record.levelname == "WARNING" ]) == 0
def test_get_steam_apps_custom_proton(self, default_proton, custom_proton_factory, steam_dir, steam_root): """ Create a custom Proton installation and ensure 'get_steam_apps' can find it """ custom_proton = custom_proton_factory(name="Custom Proton") steam_apps = get_steam_apps(steam_root=str(steam_root), steam_path=str(steam_dir), steam_lib_paths=[str(steam_dir)]) assert len(steam_apps) == 2 found_custom_proton = next(app for app in steam_apps if app.name == "Custom Proton") assert found_custom_proton.install_path == \ str(custom_proton.install_path)
def test_get_steam_apps_escape_chars(self, steam_app_factory, steam_library_factory, steam_root, steam_dir): """ Create a Steam library directory with a name containing the character '[' and ensure it is found correctly. Regression test for https://github.com/Matoking/protontricks/issues/47 """ library_dir = steam_library_factory(name="[HDD-1] SteamLibrary") steam_app_factory(name="Test game", appid=10, library_dir=library_dir) steam_apps = get_steam_apps(steam_root=steam_root, steam_path=steam_dir, steam_lib_paths=[steam_dir, library_dir]) assert len(steam_apps) == 1 assert steam_apps[0].name == "Test game" assert str(steam_apps[0].install_path).startswith(str(library_dir))
def main(): """ 'protontricks' script entrypoint """ parser = argparse.ArgumentParser( description=( "Wrapper for running Winetricks commands for " "Steam Play/Proton games.\n" "\n" "Usage:\n" "\n" "Run winetricks for game with APPID\n" "$ protontricks APPID COMMAND\n" "\n" "Search installed games to find the APPID\n" "$ protontricks -s GAME_NAME\n" "\n" "Launch the Protontricks GUI\n" "$ protontricks --gui\n" "\n" "Environment variables:\n" "\n" "PROTON_VERSION: name of the preferred Proton installation\n" "STEAM_DIR: path to custom Steam installation\n" "WINETRICKS: path to a custom 'winetricks' executable\n" "WINE: path to a custom 'wine' executable\n" "WINESERVER: path to a custom 'wineserver' executable"), formatter_class=argparse.RawTextHelpFormatter) parser.add_argument("--verbose", "-v", action="store_true", help="Print debug information") parser.add_argument("-s", "--search", type=str, dest="search", nargs="+", required=False, help="Search for game(s) with the given name") parser.add_argument( "-c", "--command", type=str, dest="command", required=False, help="Run a command in the game's installation directory with " "Wine-related environment variables set. " "The command is passed to the shell as-is without being escaped.") parser.add_argument("--gui", action="store_true", help="Launch the Protontricks GUI.") parser.add_argument("appid", type=int, nargs="?", default=None) parser.add_argument("winetricks_command", nargs=argparse.REMAINDER) parser.add_argument("-V", "--version", action="version", version="%(prog)s ({})".format(__version__)) args = parser.parse_args() do_command = bool(args.command) do_search = bool(args.search) do_gui = bool(args.gui) do_winetricks = bool(args.appid and args.winetricks_command) if not do_command and not do_search and not do_gui and not do_winetricks: parser.print_help() return # Don't allow more than one action if sum([do_search, do_gui, do_winetricks, do_command]) != 1: print("Only one action can be performed at a time.") parser.print_help() return enable_logging(args.verbose) # 1. Find Steam path steam_path = find_steam_path() if not steam_path: print("Steam installation directory could not be found.") sys.exit(-1) # 2. Find Winetricks winetricks_path = get_winetricks_path() if not winetricks_path: print("Winetricks isn't installed, please install " "winetricks in order to use this script!") sys.exit(-1) # 3. Find any Steam library folders steam_lib_paths = get_steam_lib_paths(steam_path) # 4. Find any Steam apps steam_apps = get_steam_apps(steam_path, steam_lib_paths) # 5. Find active Proton version proton_app = find_proton_app(steam_path=steam_path, steam_apps=steam_apps, appid=args.appid) if not proton_app: print("Proton installation could not be found!") sys.exit(-1) # Run the GUI if args.gui: steam_app = select_steam_app_with_gui(steam_apps=steam_apps) run_command(steam_path=steam_path, winetricks_path=winetricks_path, proton_app=proton_app, steam_app=steam_app, command=[winetricks_path, "--gui"]) return # Perform a search elif args.search: # Search for games search_query = " ".join(args.search) matching_apps = [ app for app in steam_apps if app.prefix_path_exists and app.name_contains(search_query) ] if matching_apps: matching_games = "\n".join([ "{} ({})".format(app.name, app.appid) for app in matching_apps ]) print("Found the following games:" "\n{}\n".format(matching_games)) print("To run protontricks for the chosen game, run:\n" "$ protontricks APPID COMMAND") else: print("Found no games.") print( "\n" "NOTE: A game must be launched at least once before protontricks " "can find the game.") return # If neither search or GUI are set, do a normal Winetricks command # Find game by appid steam_appid = int(args.appid) try: steam_app = next(app for app in steam_apps if not app.is_proton and app.appid == steam_appid and app.prefix_path_exists) except StopIteration: print("Steam app with the given app ID could not be found. " "Is it installed, Proton compatible and have you launched it at " "least once? You can search for the app ID using the following " "command:\n" "$ protontricks -s <GAME NAME>") sys.exit(-1) if args.winetricks_command: run_command(steam_path=steam_path, winetricks_path=winetricks_path, proton_app=proton_app, steam_app=steam_app, command=[winetricks_path] + args.winetricks_command) elif args.command: run_command( steam_path=steam_path, winetricks_path=winetricks_path, proton_app=proton_app, steam_app=steam_app, command=args.command, # Pass the command directly into the shell *without* # escaping it cwd=steam_app.install_path, shell=True)