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

Add support for immutable distributions #75

Merged
merged 2 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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)