Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: Check if Steam configuration files exist before marking as available #356

Merged
merged 6 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions pupgui2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,32 @@
TEMP_DIR = os.path.join(os.getenv('XDG_CACHE_HOME'), 'tmp', 'pupgui2.a70200/') if os.path.exists(os.getenv('XDG_CACHE_HOME', '')) else '/tmp/pupgui2.a70200/'
HOME_DIR = os.path.expanduser('~')

# support different Steam root directories
# valid install dir should have config.vdf and libraryfolders.vdf, to ensure it is not an unused folder with correct directory structure
_POSSIBLE_STEAM_ROOTS = ['~/.local/share/Steam', '~/.steam/root', '~/.steam/steam', '~/.steam/debian-installation']
_STEAM_ROOT = _POSSIBLE_STEAM_ROOTS[0]
for steam_root in _POSSIBLE_STEAM_ROOTS:
ct_dir = os.path.join(os.path.expanduser(steam_root), 'config')
config_vdf = os.path.join(ct_dir, 'config.vdf')
libraryfolders_vdf = os.path.join(ct_dir, 'libraryfolders.vdf')
if os.path.exists(config_vdf) and os.path.exists(libraryfolders_vdf):
_STEAM_ROOT = steam_root
break
# support different Steam root directories, building paths relative to HOME_DIR (i.e. /home/gaben/.local/share/Steam)
# Use os.path.realpath to expand all _STEAM_ROOT paths
_POSSIBLE_STEAM_ROOTS = [
os.path.realpath(os.path.join(HOME_DIR, _STEAM_ROOT)) for _STEAM_ROOT in ['.local/share/Steam', '.steam/root', '.steam/steam', '.steam/debian-installation']
]

# Remove duplicate paths while preserving order, as os.path.realpath may expand some symlinks to the real Steam root
_POSSIBLE_STEAM_ROOTS = list(dict.fromkeys(_POSSIBLE_STEAM_ROOTS))

# Steam can be installled in any of the locations at '_POSSIBLE_STEAM_ROOTS' - usually only one, and the others (if they exist) are typically symlinks,
# i.e. '~/.steam/root' is usually a symlink to '~/.local/share/Steam'
# These paths may still not be valid installations however, as they could be leftother paths from an old Steam installation without the data files we need ('config.vdf' and 'libraryfolders.vdf')
# We catch this later on in util#is_valid_launcher_installation though
POSSIBLE_INSTALL_LOCATIONS = [
{'install_dir': f'{_STEAM_ROOT}/compatibilitytools.d/', 'display_name': 'Steam', 'launcher': 'steam', 'type': 'native', 'icon': 'steam', 'vdf_dir': f'{_STEAM_ROOT}/config'},
{
'install_dir': f'{_STEAM_ROOT}/compatibilitytools.d/',
'display_name': 'Steam',
'launcher': 'steam',
'type': 'native',
'icon': 'steam',
'vdf_dir': f'{_STEAM_ROOT}/config'
} for _STEAM_ROOT in _POSSIBLE_STEAM_ROOTS if os.path.exists(_STEAM_ROOT)
]

# Possible install locations for all other launchers, ensuring Steam paths are at the top of the list
POSSIBLE_INSTALL_LOCATIONS += [
{'install_dir': '~/.var/app/com.valvesoftware.Steam/data/Steam/compatibilitytools.d/', 'display_name': 'Steam Flatpak', 'launcher': 'steam', 'type': 'flatpak', 'icon': 'steam', 'vdf_dir': '~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config'},
{'install_dir': '~/snap/steam/common/.steam/root/compatibilitytools.d/', 'display_name': 'Steam Snap', 'launcher': 'steam', 'type': 'snap', 'icon': 'steam', 'vdf_dir': '~/snap/steam/common/.steam/root/config'},
{'install_dir': '~/.local/share/lutris/runners/wine/', 'display_name': 'Lutris', 'launcher': 'lutris', 'type': 'native', 'icon': 'lutris', 'config_dir': '~/.config/lutris'},
Expand Down
19 changes: 18 additions & 1 deletion pupgui2/steamutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,4 +791,21 @@ def determine_most_recent_steam_user(steam_users: List[SteamUser]) -> SteamUser:
return steam_users[0]

print('Warning: No Steam users found. Returning None')
return None
return None


def is_valid_steam_install(steam_path) -> bool:

"""
Return whether required Steam data files actually exist to determine if 'steam_path' is a valid Steam installation.
Return Type: bool
"""

ct_dir = os.path.join(os.path.expanduser(steam_path), 'config')

config_vdf = os.path.join(ct_dir, 'config.vdf')
libraryfolders_vdf = os.path.join(ct_dir, 'libraryfolders.vdf')

is_valid_steam_install = os.path.exists(config_vdf) and os.path.exists(libraryfolders_vdf)

return is_valid_steam_install
27 changes: 24 additions & 3 deletions pupgui2/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pupgui2.constants import AWACY_GAME_LIST_URL, LOCAL_AWACY_GAME_LIST
from pupgui2.constants import GITHUB_API, GITLAB_API, GITLAB_API_RATELIMIT_TEXT
from pupgui2.datastructures import BasicCompatTool, CTType, Launcher, SteamApp, LutrisGame, HeroicGame
from pupgui2.steamutil import remove_steamtinkerlaunch
from pupgui2.steamutil import remove_steamtinkerlaunch, is_valid_steam_install


def create_msgbox(
Expand Down Expand Up @@ -196,6 +196,25 @@ def create_compatibilitytools_folder() -> None:
print(f'Error trying to create compatibility tools folder {str(install_dir)}: {str(e)}')


def is_valid_launcher_installation(loc) -> bool:

"""
Check whether a launcher installation is actually valid based on per-launcher criteria
Return Type: bool
"""

install_dir = os.path.expanduser(loc['install_dir'])

# Right now we only check to make sure regular Steam (not Flatpak or Snap) has config.vdf and libraryfolders.vdf
# because Steam can leave behind its directory structure when uninstalled, but not these files.
#
# In future we could expand this to other Steam flavours and other launchers.
if loc['display_name'] == 'Steam': # This seems to get called many times, why?
return is_valid_steam_install(os.path.realpath(os.path.join(install_dir, '..')))

return os.path.exists(install_dir) # Default to path check for all other launchers


def available_install_directories() -> List[str]:
"""
List available install directories
Expand All @@ -204,10 +223,12 @@ def available_install_directories() -> List[str]:
available_dirs = []
for loc in POSSIBLE_INSTALL_LOCATIONS:
install_dir = os.path.expanduser(loc['install_dir'])
if os.path.exists(install_dir):
# POSSIBLE_INSTALL_LOCATIONS may contain duplicate paths, so only add unique paths to available_dirs
# This avoids adding symlinked Steam paths
if is_valid_launcher_installation(loc) and not install_dir in available_dirs:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering: With this check not install_dir in available_dirs in place, do we still need list(dict.fromkeys(_POSSIBLE_STEAM_ROOTS)) in constants.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In short, no, but it should help catch any case where we might end up with duplicate paths in POSSIBLE_INSTALL_LOCATIONS.

Most probably not for functionality at least for Steam, but for consistency, list(dict.fromkeys(_POSSIBLE_STEAM_ROOTS)) (and the earlier checks to remove non-existent paths from _POSSIBLE_STEAM_ROOTS) should ensure that POSSIBLE_INSTALL_LOCATIONS will only contains Steam install paths that exist on the filesystem. Whether or not they're valid is another question, and that's what our function handles here.

The check might also be good in case other install paths in future could have duplicates. This probably won't ever happen, but it just makes sure at this level that we don't list the same path twice. If we really wanted it to be stricter, we could make sure all paths in available_dirs and install_dir itself are always expanded to their realpath, and also we could filter out duplicates by returning list(dict.fromkeys(available_dirs)) in this function, but I think that's overkill.

For the changes in this PR, the not install_dir in available_dirs condition is not strictly needed, it's just an extra guard.

Also, I guess the comment here is wrong, we don't list duplicate paths.

available_dirs.append(install_dir)
install_dir = config_custom_install_location().get('install_dir')
if install_dir and os.path.exists(install_dir):
if install_dir and os.path.exists(install_dir) and not install_dir in available_dirs:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could use is_valid_launcher_installation here too.
But let's leave it as is, maybe the user has some very strange Steam setup and wants to add it like this.

available_dirs.append(install_dir)
return available_dirs

Expand Down