Skip to content

Commit

Permalink
Merge pull request #75 from R1kaB3rN/immutable-distros
Browse files Browse the repository at this point in the history
Add support for immutable distributions
  • Loading branch information
R1kaB3rN authored Mar 29, 2024
2 parents d749cfe + 9678521 commit 218fed9
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 29 deletions.
7 changes: 7 additions & 0 deletions umu/umu_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ class Color(Enum):
"getcompatpath",
"getnativepath",
}


class MODE(Enum):
"""Represent the permission to apply to a file."""

USER_RW = 0o0644
USER_RWX = 0o0755
101 changes: 101 additions & 0 deletions umu/umu_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pathlib import Path
from shutil import rmtree, copytree, copy
from pwd import getpwuid
from umu_consts import MODE


class TestGameLauncher(unittest.TestCase):
Expand Down Expand Up @@ -163,6 +164,106 @@ def tearDown(self):
if self.test_local_share.exists():
rmtree(self.test_local_share.as_posix())

def test_copy(self):
"""Test _copy when copying a subset of core files from a system path."""
# Make files read-only
self.test_user_share.joinpath("reaper").chmod(0o0444)
self.test_user_share.joinpath("umu_run.py").chmod(0o0444)
self.test_user_share.joinpath("umu_consts.py").chmod(0o0444)
# Make umu-launcher files read-only
Path(self.test_user_share, "umu-launcher", "compatibilitytool.vdf").chmod(
0o0444
)
Path(self.test_user_share, "umu-launcher", "toolmanifest.vdf").chmod(0o0444)
# Verify read-only before copying to ~/.local/share/umu
self.assertTrue(
self.test_user_share.joinpath("reaper").stat().st_mode == 33060,
"Expected reaper to be read only",
)
self.assertTrue(
self.test_user_share.joinpath("umu_run.py").stat().st_mode == 33060,
"Expected umu_run to be read only",
)
self.assertTrue(
self.test_user_share.joinpath("umu_consts.py").stat().st_mode == 33060,
"Expected umu_consts to be read only",
)
self.assertTrue(
self.test_user_share.joinpath("umu-launcher", "compatibilitytool.vdf")
.stat()
.st_mode
== 33060,
"Expected compat vdf to be read only",
)
self.assertTrue(
self.test_user_share.joinpath("umu-launcher", "toolmanifest.vdf")
.stat()
.st_mode
== 33060,
"Expected manifest vdf to be read only",
)
# Copy from source dir
umu_util._copy(
self.test_user_share.joinpath("reaper"),
self.test_local_share.joinpath("reaper"),
MODE.USER_RWX,
)
umu_util._copy(
self.test_user_share.joinpath("umu_run.py"),
self.test_local_share.joinpath("umu_run.py"),
MODE.USER_RWX,
)
umu_util._copy(
self.test_user_share.joinpath("umu_consts.py"),
self.test_local_share.joinpath("umu_consts.py"),
)
umu_util._copytree(
self.test_user_share.joinpath("umu-launcher"),
self.test_local_share.joinpath("umu-launcher"),
)
# Test reaper for user rwx
# In particular, it's important umu_run and reaper are executable
# otherwise, the launcher will not work
self.assertTrue(
os.access(self.test_local_share.joinpath("reaper"), os.X_OK),
"Expected execute perm for reaper",
)
self.assertTrue(
os.access(self.test_local_share.joinpath("reaper"), os.W_OK),
"Expected write perm for reaper",
)
# Test umu_run.py for user rwx
self.assertTrue(
os.access(self.test_local_share.joinpath("umu_run.py"), os.X_OK),
"Expected execute perm for umu_run",
)
self.assertTrue(
os.access(self.test_local_share.joinpath("umu_run.py"), os.W_OK),
"Expected write perm for umu_run",
)
# Test launcher files for user rw
# Just test one of them since the default is rw and if one of them
# fails then that signals the rest will too
self.assertTrue(
os.access(self.test_local_share.joinpath("umu_consts.py"), os.W_OK),
"Expected write perm for launcher file",
)
# Test umu-launcher
self.assertTrue(
os.access(
self.test_local_share.joinpath("umu-launcher", "compatibilitytool.vdf"),
os.W_OK,
),
"Expected write perm for launcher file",
)
self.assertTrue(
os.access(
self.test_local_share.joinpath("umu-launcher", "toolmanifest.vdf"),
os.W_OK,
),
"Expected write perm for launcher file",
)

def test_ge_proton(self):
"""Test check_env when the code name GE-Proton is set for PROTONPATH.
Expand Down
87 changes: 58 additions & 29 deletions umu/umu_util.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from tarfile import open as tar_open, TarInfo
from os import environ
from umu_consts import CONFIG, STEAM_COMPAT, UMU_LOCAL
from umu_consts import CONFIG, STEAM_COMPAT, UMU_LOCAL, MODE
from typing import Any, Dict, List, Callable
from json import load, dump
from umu_log import log
from pathlib import Path
from shutil import rmtree, move, copy, copytree
from shutil import rmtree, move, copy
from umu_plugins import enable_zenity
from urllib.request import urlopen
from ssl import create_default_context
Expand Down Expand Up @@ -154,8 +154,7 @@ def _install_umu(
~/.local/share/umu, ~/.local/share/Steam/compatibilitytools.d
The tools that will be copied are:
SteamRT, Pressure Vessel, umu-launcher, umu Launcher files, Reaper
and umu_version.json
umu-launcher, umu Launcher files, reaper and umu_version.json
"""
log.debug("New install detected")
log.console("Setting up Unified Launcher for Windows Games on Linux ...")
Expand All @@ -164,17 +163,18 @@ def _install_umu(

# Config
log.console(f"Copied {CONFIG} -> {local}")
copy(root.joinpath(CONFIG), local.joinpath(CONFIG))
_copy(root.joinpath(CONFIG), local.joinpath(CONFIG))

# Reaper
log.console(f"Copied reaper -> {local}")
copy(root.joinpath("reaper"), local.joinpath("reaper"))
_copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX)

# Launcher files
for file in root.glob("*.py"):
if not file.name.startswith("umu_test"):
if not file.name.startswith(("umu_test", "umu_run")):
log.console(f"Copied {file} -> {local}")
copy(file, local.joinpath(file.name))
_copy(file, local.joinpath(file.name))
_copy(root.joinpath("umu_run.py"), local.joinpath("umu_run.py"), MODE.USER_RWX)

local.joinpath("umu-run").symlink_to("umu_run.py")

Expand All @@ -184,11 +184,9 @@ def _install_umu(
# Remove existing files if they exist -- this is a clean install.
if steam_compat.joinpath("umu-launcher").is_dir():
rmtree(steam_compat.joinpath("umu-launcher").as_posix())
copytree(
_copytree(
root.joinpath("umu-launcher"),
steam_compat.joinpath("umu-launcher"),
dirs_exist_ok=True,
symlinks=True,
)
steam_compat.joinpath("umu-launcher", "umu-run").symlink_to(
"../../../umu/umu_run.py"
Expand Down Expand Up @@ -232,15 +230,15 @@ def _update_umu(
# Directory is absent
if not local.joinpath("reaper").is_file():
log.warning("Reaper not found")
copy(root.joinpath("reaper"), local.joinpath("reaper"))
_copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX)
log.console(f"Restored {key} to {val}")

# Update
if val != reaper:
log.console(f"Updating {key} to {val}")

local.joinpath("reaper").unlink(missing_ok=True)
copy(root.joinpath("reaper"), local.joinpath("reaper"))
_copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX)

json_local["umu"]["versions"]["reaper"] = val
elif key == "runtime_platform":
Expand Down Expand Up @@ -286,9 +284,14 @@ def _update_umu(

# Python files
for file in root.glob("*.py"):
if not file.name.startswith("umu_test"):
if not file.name.startswith(("umu_test", "umu_run")):
local.joinpath(file.name).unlink(missing_ok=True)
copy(file, local.joinpath(file.name))
_copy(file, local.joinpath(file.name))
_copy(
root.joinpath("umu_run.py"),
local.joinpath("umu_run.py"),
MODE.USER_RWX,
)

# Symlink
local.joinpath("umu-run").unlink(missing_ok=True)
Expand All @@ -298,14 +301,23 @@ def _update_umu(
continue

# Check for missing files
for file in root.glob("*.py"):
if (
not file.name.startswith("umu_test")
and not local.joinpath(file.name).is_file()
):
is_missing = True
log.warning("Missing %s", file.name)
copy(file, local.joinpath(file.name))
for file in [
file
for file in root.glob("*.py")
if not file.name.startswith(("umu_test", "umu_run"))
and not local.joinpath(file.name).is_file()
]:
is_missing = True
log.warning("Missing %s", file.name)
_copy(file, local.joinpath(file.name))

if not local.joinpath("umu_run.py"):
log.warning("Missing %s", file.name)
_copy(
root.joinpath("umu_run.py"),
local.joinpath("umu_run.py"),
MODE.USER_RWX,
)

if is_missing:
log.console(f"Restored {key} to {val}")
Expand All @@ -319,11 +331,9 @@ def _update_umu(
if not steam_compat.joinpath("umu-launcher").is_dir():
log.warning("umu-launcher not found")

copytree(
_copytree(
root.joinpath("umu-launcher"),
steam_compat.joinpath("umu-launcher"),
dirs_exist_ok=True,
symlinks=True,
)

steam_compat.joinpath("umu-launcher", "umu-run").symlink_to(
Expand All @@ -335,11 +345,9 @@ def _update_umu(
log.console(f"Updating {key} to {val}")

rmtree(steam_compat.joinpath("umu-launcher").as_posix())
copytree(
_copytree(
root.joinpath("umu-launcher"),
steam_compat.joinpath("umu-launcher"),
dirs_exist_ok=True,
symlinks=True,
)

steam_compat.joinpath("umu-launcher", "umu-run").symlink_to(
Expand Down Expand Up @@ -384,3 +392,24 @@ def _get_json(path: Path, config: str) -> Dict[str, Any]:
raise ValueError(err)

return json


def _copy(src: Path, dst: Path, mode: MODE = MODE.USER_RW) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)

if src.is_symlink():
dst.symlink_to(src.readlink())
return

copy(src, dst)
dst.chmod(mode.value)


def _copytree(src: Path, dest: Path, mode: MODE = MODE.USER_RW) -> None:
for file in src.iterdir():
if file.is_dir():
dest_subdir = dest / file.name
dest_subdir.mkdir(parents=True, exist_ok=True)
_copytree(file, dest_subdir)
else:
_copy(file, dest / file.name, mode)

0 comments on commit 218fed9

Please sign in to comment.