diff --git a/ulwgl_run.py b/ulwgl_run.py index c0925f7ce..fc3c6f544 100755 --- a/ulwgl_run.py +++ b/ulwgl_run.py @@ -10,6 +10,7 @@ import ulwgl_plugins from re import match import subprocess +from ulwgl_util import UnixUser def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103 @@ -43,14 +44,45 @@ def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103 def setup_pfx(path: str) -> None: - """Create a symlink to the WINE prefix and tracked_files file.""" + """Create a symlink to the WINE prefix and tracked_files file. + + Also, create a symlink of steamuser to the Unix username + """ + steam: Path = Path(path + "/drive_c/users/steamuser").expanduser() + uid: int = os.getuid() + user: UnixUser = UnixUser() + wineuser: Path = Path(path + "/drive_c/users/" + user.get_user()).expanduser() + if not (Path(path + "/pfx")).expanduser().is_symlink(): # When creating the symlink, we want it to be in expanded form when passed unexpanded paths - # Example: pfx -> /home/foo/.wine - # NOTE: When parsing a config file, an error can be raised if the prefix doesn't already exist + # Good: pfx -> /home/foo/.wine + # Bad: pfx -> ~/.wine Path(path + "/pfx").expanduser().symlink_to(Path(path).expanduser()) Path(path + "/tracked_files").expanduser().touch() + # Create a symlink of the current user to the steamuser dir for Steam functionality + # Only create a symlink for the current user + if not wineuser.is_dir() and not steam.is_dir() and user.is_user(uid): + steam.mkdir(parents=True) + wineuser.symlink_to(steam) + elif wineuser.is_dir() and not steam.is_dir() and user.is_user(uid): + wineuser.rename(steam) + wineuser.symlink_to(steam) + elif ( + not (wineuser.exists() or wineuser.is_symlink()) + and steam.is_dir() + and user.is_user(uid) + ): + wineuser.symlink_to(steam) + elif not user.is_user(uid): + print( + f"Unable to identify the current user.\nSymbolic link will not be created to path: {steam}" + ) + else: + print( + f"Paths exist: {steam} and {wineuser}\nPlease consider merging {wineuser} to {steam}." + ) + def check_env( env: Dict[str, str], toml: Dict[str, Any] = None diff --git a/ulwgl_test.py b/ulwgl_test.py index 496934405..ecba0dbcb 100644 --- a/ulwgl_test.py +++ b/ulwgl_test.py @@ -866,8 +866,111 @@ def test_set_env(self): "Expected STEAM_COMPAT_MOUNTS to be set", ) + def test_setup_pfx_symlinks_steam_mv(self): + """Test setup_pfx for symbolic link to steamuser. + + Tests the case when the steamuser dir does not exist and user dir exists + In this case, rename the existing user dir to steamuser and create a new symlink to it + """ + result = None + pattern = r"^/home/[a-zA-Z]+" + unexpanded_path = re.sub( + pattern, + "~", + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), + ) + + # Create only the user dir + Path( + unexpanded_path + "/drive_c/users/" + Path().home().name + ).expanduser().mkdir(parents=True, exist_ok=True) + + result = ulwgl_run.setup_pfx(unexpanded_path) + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + self.assertTrue( + Path(self.test_file + "/tracked_files").is_file(), + "Expected tracked_files to be a file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + # Check if the symlink is in its unexpanded form + self.assertEqual( + Path(self.test_file + "/pfx").readlink().as_posix(), + Path(unexpanded_path).expanduser().as_posix(), + ) + self.assertTrue( + Path(self.test_file + "/drive_c/users/steamuser").is_dir(), + "Expected steamuser to be created", + ) + self.assertTrue( + Path(unexpanded_path + "/drive_c/users/" + Path().home().name) + .expanduser() + .is_symlink(), + "Expected symlink of username -> steamuser", + ) + + def test_setup_pfx_symlinks_steam(self): + """Test setup_pfx for symbolic link to steamuser. + + Tests the case when the steamuser exists and a user does not exists + """ + result = None + pattern = r"^/home/[a-zA-Z]+" + unexpanded_path = re.sub( + pattern, + "~", + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), + ) + + # Create only the user dir + Path(unexpanded_path + "/drive_c/users/steamuser").expanduser().mkdir( + parents=True, exist_ok=True + ) + result = ulwgl_run.setup_pfx(unexpanded_path) + + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + self.assertTrue( + Path(self.test_file + "/tracked_files").is_file(), + "Expected tracked_files to be a file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + # Check if the symlink is in its unexpanded form + self.assertEqual( + Path(self.test_file + "/pfx").readlink().as_posix(), + Path(unexpanded_path).expanduser().as_posix(), + ) + self.assertTrue( + Path(self.test_file + "/drive_c/users/steamuser").is_dir(), + "Expected steamuser to be created", + ) + self.assertTrue( + Path(unexpanded_path + "/drive_c/users/" + Path().home().name) + .expanduser() + .is_symlink(), + "Expected symlink of username -> steamuser", + ) + def test_setup_pfx_symlinks(self): - """Test _setup_pfx for valid symlinks. + """Test setup_pfx for valid symlinks. Ensure that symbolic links to the WINE prefix (pfx) are always in expanded form when passed an unexpanded path. For example: @@ -879,6 +982,9 @@ def test_setup_pfx_symlinks(self): """ result = None pattern = r"^/home/[a-zA-Z]+" + + # Replaces the expanded path to unexpanded + # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file unexpanded_path = re.sub( pattern, "~", @@ -888,8 +994,6 @@ def test_setup_pfx_symlinks(self): ) result = ulwgl_run.setup_pfx(unexpanded_path) - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file self.assertIsNone( result, "Expected None when creating symbolic link to WINE prefix and tracked_files file", @@ -918,6 +1022,9 @@ def test_setup_pfx_paths(self): """ result = None pattern = r"^/home/[a-zA-Z]+" + + # Replaces the expanded path to unexpanded + # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file unexpanded_path = re.sub( pattern, "~", @@ -925,8 +1032,6 @@ def test_setup_pfx_paths(self): ) result = ulwgl_run.setup_pfx(unexpanded_path) - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file self.assertIsNone( result, "Expected None when creating symbolic link to WINE prefix and tracked_files file", @@ -954,6 +1059,17 @@ def test_setup_pfx(self): Path(self.test_file + "/tracked_files").is_file(), "Expected tracked_files to be a file", ) + # For new prefixes, steamuser should exist and a user symlink + self.assertTrue( + Path(self.test_file + "/drive_c/users/steamuser").is_dir(), + "Expected steamuser to be created", + ) + self.assertTrue( + Path(self.test_file + "/drive_c/users/" + Path().home().name) + .expanduser() + .is_symlink(), + "Expected symlink of username -> steamuser", + ) def test_parse_args(self): """Test parse_args with no options. diff --git a/ulwgl_util.py b/ulwgl_util.py new file mode 100644 index 000000000..48bb2492b --- /dev/null +++ b/ulwgl_util.py @@ -0,0 +1,32 @@ +import os +import pwd +from pathlib import Path +import typing +from pwd import struct_passwd + +class UnixUser: + """Represents the User of the system as determined by the password database rather than environment variables or file system paths.""" + + def __init__(self): + entry: struct_passwd = pwd.getpwuid(os.getuid()) + # Immutable properties, hence no setters + self.name: str = entry.pw_name + self.puid: str = entry.pw_uid # Should be equivalent to the value from getuid + self.dir: str = entry.pw_dir + + def get_home_dir(self) -> Path: + """The User home directory as determined by the password database that's derived from the current process's real user id.""" + return Path(self.dir).as_posix() + + def get_user(self) -> str: + """The User (login name) as determined by the password database that's derived from the current process's real user id.""" + return self.name + + def get_puid(self) -> int: + """The numerical user ID as determined by the password database that's derived from the current process's real user id.""" + return self.puid + + def is_user(self, uid: int) -> bool: + """Compare the UID passed in to this instance's""" + return uid == self.puid +