867 lines
36 KiB
Python
867 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
|
|
from ptlinux import __version__
|
|
from ptlinux.desktop_entry import escape_icon_path, quote_exec_argument
|
|
from ptlinux.launcher_wrapper import quote_shell_word
|
|
from scripts.repository_paths import PROJECT_ROOT, SOURCE_ROOT
|
|
from tools.sign_release_artifact import sign_release_artifact
|
|
|
|
|
|
class AioPackagingTests(unittest.TestCase):
|
|
def test_bootstrap_release_remains_reproducible_with_a_fixed_epoch(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
outputs = (root / "first", root / "second")
|
|
for output in outputs:
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_VERSION", None)
|
|
environment.update(
|
|
{
|
|
"PTLINUX_OUTPUT_DIRECTORY": str(output),
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
"SOURCE_DATE_EPOCH": "0",
|
|
}
|
|
)
|
|
built = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
|
|
first = {
|
|
path.name: path.read_bytes()
|
|
for path in outputs[0].iterdir()
|
|
if path.is_file()
|
|
}
|
|
second = {
|
|
path.name: path.read_bytes()
|
|
for path in outputs[1].iterdir()
|
|
if path.is_file()
|
|
}
|
|
self.assertEqual(first, second)
|
|
|
|
def test_packaged_installer_uses_the_compatible_top_level_source_layout(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
output = root / "dist"
|
|
build_environment = os.environ.copy()
|
|
build_environment.pop("PTLINUX_VERSION", None)
|
|
build_environment.update(
|
|
{
|
|
"PTLINUX_OUTPUT_DIRECTORY": str(output),
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
"SOURCE_DATE_EPOCH": "0",
|
|
}
|
|
)
|
|
built = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=build_environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
|
|
release_name = (
|
|
f"pristontale-eu-linux-installer-v{__version__.replace('.', '-')}"
|
|
)
|
|
archive = output / f"{release_name}.tar.gz"
|
|
extracted = root / "extracted"
|
|
with tarfile.open(archive, "r:gz") as bundle:
|
|
bundle.extractall(extracted, filter="data")
|
|
packaged_root = extracted / release_name
|
|
self.assertTrue((packaged_root / "ptlinux").is_dir())
|
|
self.assertFalse((packaged_root / "src").exists())
|
|
|
|
home = root / "home"
|
|
data_home = root / "desktop-data"
|
|
install_directory = root / "managed-install"
|
|
install_environment = os.environ.copy()
|
|
install_environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
install_environment.update(
|
|
{
|
|
"HOME": str(home),
|
|
"XDG_DATA_HOME": str(data_home),
|
|
"PATH": "/usr/bin:/bin",
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
}
|
|
)
|
|
installed = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(packaged_root / "install.sh"),
|
|
"--install-dir",
|
|
str(install_directory),
|
|
"--skip-deps",
|
|
"--skip-doctor",
|
|
"--skip-runtime-bootstrap",
|
|
],
|
|
cwd=packaged_root,
|
|
env=install_environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(installed.returncode, 0, installed.stderr)
|
|
current = install_directory / "app/current"
|
|
self.assertTrue((current / "ptlinux/cli.py").is_file())
|
|
self.assertTrue((current / "ptdiag/logs.py").is_file())
|
|
|
|
def test_installer_dry_run_reports_destinations_without_writing(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
home = root / "home"
|
|
data_home = root / "desktop-data"
|
|
install_directory = root / "managed-install"
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
environment.update(
|
|
{
|
|
"HOME": str(home),
|
|
"XDG_DATA_HOME": str(data_home),
|
|
"PATH": "/usr/bin:/bin",
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
}
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(PROJECT_ROOT / "install.sh"),
|
|
"--install-dir",
|
|
str(install_directory),
|
|
"--dry-run",
|
|
"--skip-deps",
|
|
"--skip-doctor",
|
|
"--skip-runtime-bootstrap",
|
|
],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn(f"Install data: {install_directory}", result.stdout)
|
|
self.assertIn("Skipping host dependency installation.", result.stdout)
|
|
self.assertFalse(install_directory.exists())
|
|
self.assertFalse(home.exists())
|
|
self.assertFalse(data_home.exists())
|
|
|
|
def test_installer_writes_path_independent_desktop_entry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
home = root / "home with spaces"
|
|
data_home = root / "data with spaces"
|
|
home.mkdir()
|
|
fake_bin = root / "bin"
|
|
fake_bin.mkdir()
|
|
fake_umu = fake_bin / "umu-run"
|
|
fake_umu.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
fake_umu.chmod(0o755)
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
environment.update(
|
|
{
|
|
"HOME": str(home),
|
|
"XDG_DATA_HOME": str(data_home),
|
|
"PATH": f"{fake_bin}:/usr/bin:/bin",
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
}
|
|
)
|
|
result = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(PROJECT_ROOT / "install.sh"),
|
|
"--skip-deps",
|
|
"--skip-doctor",
|
|
],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn(str(fake_umu.resolve()), result.stdout)
|
|
self.assertIn(
|
|
f"Note: {home / '.local/bin'} is not in PATH for this shell.",
|
|
result.stdout,
|
|
)
|
|
output_lines = result.stdout.splitlines()
|
|
start_line = output_lines[output_lines.index("Start PristonTale EU with:") + 1]
|
|
self.assertEqual(
|
|
shlex.split(start_line),
|
|
[str(home / ".local/bin/pristontale-eu"), "gui"],
|
|
)
|
|
self.assertIn("export PATH=", result.stdout)
|
|
default_install = home / "Games/PristontaleEU"
|
|
self.assertTrue((default_install / "app/current/ptlinux").is_dir())
|
|
self.assertTrue((default_install / "app/current/ptlinux/umu.py").is_file())
|
|
self.assertTrue(
|
|
(default_install / "app/current/ptlinux/ui/dashboard.py").is_file()
|
|
)
|
|
self.assertTrue(
|
|
(
|
|
default_install
|
|
/ "app/current/ptlinux/ui/viewmodels/downloader.py"
|
|
).is_file()
|
|
)
|
|
self.assertTrue(
|
|
(default_install / "app/current/ptlinux/native/pt-identity-probe.exe").is_file()
|
|
)
|
|
self.assertTrue(
|
|
(default_install / "app/current/ptlinux/native/ptr-bcrypt-proxy.dll").is_file()
|
|
)
|
|
gamescope = (
|
|
default_install
|
|
/ "app/current/ptlinux/native/gamescope-3.16.20-ubuntu26.04-amd64"
|
|
)
|
|
self.assertTrue(gamescope.is_file())
|
|
self.assertEqual(gamescope.stat().st_mode & 0o777, 0o755)
|
|
self.assertFalse(
|
|
(default_install / "app/current/ptlinux/desktop_entry.py").exists()
|
|
)
|
|
self.assertFalse(
|
|
(default_install / "app/current/ptlinux/launcher_wrapper.py").exists()
|
|
)
|
|
self.assertFalse(
|
|
(default_install / "app/current/ptlinux/world_fix.py").exists()
|
|
)
|
|
self.assertTrue((default_install / "app/current").is_symlink())
|
|
self.assertEqual(
|
|
(default_install / "app/current").resolve().stat().st_mode & 0o777,
|
|
0o755,
|
|
)
|
|
launcher = home / ".local/bin/pristontale-eu"
|
|
desktop = data_home / "applications/eu.pristontale.PTLinux.desktop"
|
|
icon = data_home / "pixmaps/eu.pristontale.PTLinux.png"
|
|
self.assertTrue(launcher.is_file())
|
|
self.assertEqual(
|
|
icon.read_bytes(),
|
|
(PROJECT_ROOT / "packaging/eu.pristontale.PTLinux.png").read_bytes(),
|
|
)
|
|
entry = desktop.read_text(encoding="utf-8")
|
|
exec_line = next(line for line in entry.splitlines() if line.startswith("Exec="))
|
|
icon_line = next(line for line in entry.splitlines() if line.startswith("Icon="))
|
|
self.assertEqual(
|
|
exec_line,
|
|
f"Exec={quote_exec_argument(str(launcher))} gui",
|
|
)
|
|
self.assertEqual(icon_line, f"Icon={escape_icon_path(str(icon))}")
|
|
self.assertIn("StartupWMClass=PristonTaleEU", entry)
|
|
self.assertNotIn("TryExec=", entry)
|
|
self.assertIn(
|
|
f"install_directory={quote_shell_word(str(default_install))}",
|
|
launcher.read_text(encoding="utf-8"),
|
|
)
|
|
self.assertIn(
|
|
'exec "$python_bin" -B -P -m ptlinux',
|
|
launcher.read_text(encoding="utf-8"),
|
|
)
|
|
|
|
arguments = shlex.split(exec_line.removeprefix("Exec="))
|
|
self.assertEqual(arguments, [str(launcher), "gui"])
|
|
arguments[1] = "--help"
|
|
launched = subprocess.run(
|
|
arguments,
|
|
cwd=root,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
self.assertEqual(launched.returncode, 0, launched.stderr)
|
|
self.assertIn("PristonTale EU all-in-one", launched.stdout)
|
|
|
|
stale_directory = root / "unpacked-old-installer"
|
|
stale_package = stale_directory / "ptlinux"
|
|
stale_package.mkdir(parents=True)
|
|
(stale_package / "__init__.py").write_text("", encoding="utf-8")
|
|
(stale_package / "__main__.py").write_text(
|
|
'print("STALE LAUNCHER WAS IMPORTED")\n',
|
|
encoding="utf-8",
|
|
)
|
|
isolated = subprocess.run(
|
|
[launcher, "--version"],
|
|
cwd=stale_directory,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
self.assertEqual(isolated.returncode, 0, isolated.stderr)
|
|
self.assertEqual(isolated.stdout.strip(), f"pristontale-eu {__version__}")
|
|
self.assertNotIn("STALE LAUNCHER", isolated.stdout)
|
|
|
|
def test_installer_persists_custom_install_directory(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
home = root / "home"
|
|
data_home = root / "desktop data"
|
|
install_directory = root / "External $(touch INJECTED)'s Game"
|
|
home.mkdir()
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
environment.update(
|
|
{
|
|
"HOME": str(home),
|
|
"XDG_DATA_HOME": str(data_home),
|
|
"PATH": "/usr/bin:/bin",
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
}
|
|
)
|
|
result = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(PROJECT_ROOT / "install.sh"),
|
|
"--install-dir",
|
|
str(install_directory),
|
|
"--skip-deps",
|
|
"--skip-doctor",
|
|
"--skip-runtime-bootstrap",
|
|
],
|
|
cwd=root,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertTrue((install_directory / "app/current/ptlinux").is_dir())
|
|
self.assertTrue((install_directory / "app/current").is_symlink())
|
|
launcher = home / ".local/bin/pristontale-eu"
|
|
wrapper = launcher.read_text(encoding="utf-8")
|
|
self.assertIn(
|
|
f"install_directory={quote_shell_word(str(install_directory))}",
|
|
wrapper,
|
|
)
|
|
|
|
runtime_environment = environment.copy()
|
|
runtime_environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
runtime_environment["XDG_DATA_HOME"] = str(root / "different data home")
|
|
launched = subprocess.run(
|
|
[str(launcher), "--help"],
|
|
cwd=root,
|
|
env=runtime_environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(launched.returncode, 0, launched.stderr)
|
|
self.assertIn("PristonTale EU all-in-one", launched.stdout)
|
|
self.assertFalse((root / "INJECTED").exists())
|
|
|
|
def test_installer_preserves_the_previous_default_on_upgrade(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
home = root / "home"
|
|
data_home = root / "data"
|
|
legacy_install = data_home / "pristontale-eu"
|
|
legacy_game = (
|
|
legacy_install
|
|
/ "launcher-prefix/drive_c/Program Files (x86)/Pristontale EU/Game.exe"
|
|
)
|
|
legacy_game.parent.mkdir(parents=True)
|
|
legacy_game.write_bytes(b"existing managed game")
|
|
legacy_package = legacy_install / "app/ptlinux"
|
|
legacy_package.mkdir(parents=True)
|
|
(legacy_package / "__init__.py").write_text(
|
|
'__version__ = "0.4.7"\n',
|
|
encoding="utf-8",
|
|
)
|
|
settings_file = home / ".config/pristontale-eu/settings.json"
|
|
settings_file.parent.mkdir(parents=True)
|
|
settings_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"borderless": False,
|
|
"runtime": "proton7",
|
|
"proton_sync": "esync",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_INSTALL_DIR", None)
|
|
environment.update(
|
|
{
|
|
"HOME": str(home),
|
|
"XDG_DATA_HOME": str(data_home),
|
|
"PATH": "/usr/bin:/bin",
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
}
|
|
)
|
|
result = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(PROJECT_ROOT / "install.sh"),
|
|
"--skip-deps",
|
|
"--skip-doctor",
|
|
"--skip-runtime-bootstrap",
|
|
],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("preserving its previous data location", result.stdout)
|
|
launcher = home / ".local/bin/pristontale-eu"
|
|
self.assertIn(
|
|
f"install_directory={quote_shell_word(str(legacy_install))}",
|
|
launcher.read_text(encoding="utf-8"),
|
|
)
|
|
self.assertFalse((home / "Games/PristontaleEU/app").exists())
|
|
self.assertTrue((legacy_install / "app/current").is_symlink())
|
|
settings_result = subprocess.run(
|
|
[launcher, "--json", "settings", "--init"],
|
|
cwd=root,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
self.assertEqual(settings_result.returncode, 0, settings_result.stderr)
|
|
migrated = json.loads(settings_result.stdout)["settings"]
|
|
self.assertFalse(migrated["borderless"])
|
|
self.assertEqual(migrated["runtime"], "ge-proton")
|
|
self.assertEqual(migrated["proton_sync"], "esync")
|
|
persisted = json.loads(settings_file.read_text(encoding="utf-8"))
|
|
self.assertEqual(persisted["runtime"], "ge-proton")
|
|
self.assertIs(persisted["ge_proton_default_migrated"], True)
|
|
|
|
def test_production_release_requires_a_signing_key(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
output = Path(temporary_directory) / "output"
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_SIGNING_KEY", None)
|
|
environment["PTLINUX_RELEASE_MODE"] = "production"
|
|
environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output)
|
|
result = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("PTLINUX_SIGNING_KEY is required", result.stderr)
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_production_release_rejects_a_key_path_inside_the_checkout(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
output = Path(temporary_directory) / "output"
|
|
environment = os.environ.copy()
|
|
environment["PTLINUX_RELEASE_MODE"] = "production"
|
|
environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output)
|
|
environment["PTLINUX_PYTHON"] = sys.executable
|
|
environment["PTLINUX_SIGNING_KEY"] = str(
|
|
PROJECT_ROOT / "release-keys/fixture-private.pem"
|
|
)
|
|
result = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("outside the repository", result.stderr)
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_release_artifact_signature_is_generated_and_verified(self) -> None:
|
|
openssl = shutil.which("openssl")
|
|
if openssl is None:
|
|
self.skipTest("OpenSSL is unavailable")
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
private_key = root / "private.pem"
|
|
public_key = root / "public.pem"
|
|
artifact = root / "installer.tar.gz"
|
|
artifact.write_bytes(b"release bytes")
|
|
commands = (
|
|
[
|
|
openssl,
|
|
"genpkey",
|
|
"-quiet",
|
|
"-algorithm",
|
|
"RSA",
|
|
"-pkeyopt",
|
|
"rsa_keygen_bits:2048",
|
|
"-out",
|
|
str(private_key),
|
|
],
|
|
[
|
|
openssl,
|
|
"pkey",
|
|
"-in",
|
|
str(private_key),
|
|
"-pubout",
|
|
"-out",
|
|
str(public_key),
|
|
],
|
|
)
|
|
for command in commands:
|
|
generated = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=20,
|
|
check=False,
|
|
)
|
|
self.assertEqual(generated.returncode, 0, generated.stderr)
|
|
private_key.chmod(0o600)
|
|
|
|
signature = sign_release_artifact(
|
|
artifact,
|
|
signing_key=private_key,
|
|
public_key=public_key,
|
|
)
|
|
|
|
self.assertTrue(signature.is_file())
|
|
self.assertEqual(signature.stat().st_mode & 0o777, 0o644)
|
|
artifact.write_bytes(b"tampered release bytes")
|
|
verification = subprocess.run(
|
|
[
|
|
openssl,
|
|
"dgst",
|
|
"-sha256",
|
|
"-verify",
|
|
str(public_key),
|
|
"-signature",
|
|
str(signature),
|
|
str(artifact),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=20,
|
|
check=False,
|
|
)
|
|
self.assertNotEqual(verification.returncode, 0)
|
|
|
|
def test_release_builder_packages_current_updater_sources(self) -> None:
|
|
self.assertIn(
|
|
f'version = "{__version__}"',
|
|
(PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"),
|
|
)
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
output = Path(temporary_directory)
|
|
environment = os.environ.copy()
|
|
environment.pop("PTLINUX_VERSION", None)
|
|
environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output)
|
|
environment["SOURCE_DATE_EPOCH"] = "0"
|
|
result = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
release_name = (
|
|
f"pristontale-eu-linux-installer-v{__version__.replace('.', '-')}"
|
|
)
|
|
archive = output / f"{release_name}.tar.gz"
|
|
checksum = archive.with_suffix(archive.suffix + ".sha256")
|
|
self.assertTrue(archive.is_file())
|
|
self.assertTrue(checksum.is_file())
|
|
self.assertFalse(Path(f"{archive}.sig").exists())
|
|
self.assertTrue((output / "launcher-update-public.pem").is_file())
|
|
zip_archive = output / f"{release_name}.zip"
|
|
self.assertTrue(zip_archive.is_file())
|
|
self.assertFalse(Path(f"{zip_archive}.sig").exists())
|
|
update_archive = output / f"pristontale-eu-linux-update-v{__version__}.tar.gz"
|
|
self.assertTrue(update_archive.is_file())
|
|
update_checksum = update_archive.with_suffix(update_archive.suffix + ".sha256")
|
|
self.assertTrue(update_checksum.is_file())
|
|
manifest = output / "stable.json"
|
|
self.assertTrue(manifest.is_file())
|
|
for artifact in (
|
|
archive,
|
|
checksum,
|
|
update_archive,
|
|
update_checksum,
|
|
manifest,
|
|
):
|
|
self.assertEqual(artifact.stat().st_mode & 0o777, 0o644)
|
|
expected_sha256 = checksum.read_text(encoding="utf-8").split()[0]
|
|
self.assertEqual(
|
|
hashlib.sha256(archive.read_bytes()).hexdigest(),
|
|
expected_sha256,
|
|
)
|
|
|
|
release_root = release_name
|
|
with tarfile.open(archive, "r:gz") as release:
|
|
members = release.getmembers()
|
|
names = {member.name for member in members}
|
|
self.assertTrue(
|
|
all(member.mode & 0o022 == 0 for member in members),
|
|
"release entries must not be group- or world-writable",
|
|
)
|
|
self.assertEqual(
|
|
next(member.mode for member in members if member.name == release_root),
|
|
0o755,
|
|
)
|
|
self.assertEqual(
|
|
{name for name in names if name.endswith(".md")},
|
|
{f"{release_root}/README.md"},
|
|
)
|
|
self.assertIn(f"{release_root}/ptlinux/desktop_entry.py", names)
|
|
self.assertIn(f"{release_root}/ptlinux/install_status.py", names)
|
|
self.assertIn(f"{release_root}/ptlinux/ui/dashboard.py", names)
|
|
self.assertIn(
|
|
f"{release_root}/ptlinux/ui/components/progress_panel.py",
|
|
names,
|
|
)
|
|
self.assertIn(f"{release_root}/ptlinux/launcher_wrapper.py", names)
|
|
self.assertIn(f"{release_root}/ptlinux/self_update.py", names)
|
|
self.assertIn(f"{release_root}/ptlinux/container_host.py", names)
|
|
self.assertIn(f"{release_root}/release/config.json", names)
|
|
self.assertIn(f"{release_root}/scripts/__init__.py", names)
|
|
self.assertIn(f"{release_root}/scripts/repository_paths.py", names)
|
|
self.assertIn(f"{release_root}/scripts/release_lib.py", names)
|
|
self.assertFalse(any(name.startswith(f"{release_root}/devtools/") for name in names))
|
|
self.assertIn(
|
|
f"{release_root}/ptlinux/launcher-update-public.pem",
|
|
names,
|
|
)
|
|
self.assertIn(
|
|
f"{release_root}/ptlinux/native/pt-identity-probe.exe",
|
|
names,
|
|
)
|
|
self.assertIn(
|
|
f"{release_root}/ptlinux/native/ptr-bcrypt-proxy.dll",
|
|
names,
|
|
)
|
|
gamescope_name = (
|
|
f"{release_root}/ptlinux/native/"
|
|
"gamescope-3.16.20-ubuntu26.04-amd64"
|
|
)
|
|
self.assertIn(gamescope_name, names)
|
|
self.assertEqual(
|
|
next(member.mode for member in members if member.name == gamescope_name),
|
|
0o755,
|
|
)
|
|
for name in (
|
|
"gamescope-compat.README",
|
|
"gamescope-compat.copyright",
|
|
"gamescope-lazy-output-images.patch",
|
|
"gamescope-wayland-live-resize.patch",
|
|
"gamescope-wayland-letterbox-backing.patch",
|
|
"gamescope-wayland-libdecor-input-scope.patch",
|
|
"gamescope-keep-nested-windows-visible.patch",
|
|
):
|
|
self.assertIn(f"{release_root}/ptlinux/native/{name}", names)
|
|
self.assertNotIn(
|
|
f"{release_root}/ptlinux/native/pt-world-fix.exe",
|
|
names,
|
|
)
|
|
self.assertNotIn(f"{release_root}/ptlinux/world_fix.py", names)
|
|
archived_readme = release.extractfile(f"{release_root}/README.md")
|
|
self.assertIsNotNone(archived_readme)
|
|
assert archived_readme is not None
|
|
readme = archived_readme.read().decode("utf-8")
|
|
self.assertIn(release_name, readme)
|
|
self.assertIn("./install.sh", readme)
|
|
self.assertIn("pristontale-eu update --repair", readme)
|
|
self.assertIn("pristontale-eu --help", readme)
|
|
for command in (
|
|
"gui",
|
|
"doctor",
|
|
"container-host",
|
|
"fetch",
|
|
"setup",
|
|
"update",
|
|
"sync",
|
|
"wine-update",
|
|
"import-game",
|
|
"cache-compatibility",
|
|
"prepare",
|
|
"play",
|
|
"settings",
|
|
"launcher-update",
|
|
):
|
|
self.assertIn(f"pristontale-eu {command}", readme)
|
|
archived_depot = release.extractfile(f"{release_root}/ptlinux/depot.py")
|
|
self.assertIsNotNone(archived_depot)
|
|
assert archived_depot is not None
|
|
self.assertEqual(
|
|
archived_depot.read(),
|
|
(SOURCE_ROOT / "ptlinux/depot.py").read_bytes(),
|
|
)
|
|
archived_login_trace = release.extractfile(
|
|
f"{release_root}/ptlinux/login_trace.py"
|
|
)
|
|
self.assertIsNotNone(archived_login_trace)
|
|
assert archived_login_trace is not None
|
|
login_trace_source = archived_login_trace.read().decode("utf-8")
|
|
self.assertIn("LOGIN_TRACE_ENABLED = False", login_trace_source)
|
|
self.assertNotIn("+relay", login_trace_source)
|
|
self.assertNotIn("RelayInclude", login_trace_source)
|
|
|
|
with tarfile.open(update_archive, "r:gz") as update:
|
|
update_root = f"pristontale-eu-linux-update-v{__version__}"
|
|
update_members = update.getmembers()
|
|
update_names = {member.name for member in update_members}
|
|
self.assertIn(f"{update_root}/release.json", update_names)
|
|
self.assertIn(f"{update_root}/ptlinux/self_update.py", update_names)
|
|
self.assertIn(f"{update_root}/ptlinux/container_host.py", update_names)
|
|
self.assertIn(f"{update_root}/ptlinux/import_state.py", update_names)
|
|
self.assertIn(f"{update_root}/ptlinux/ui/dashboard.py", update_names)
|
|
self.assertIn(
|
|
f"{update_root}/ptlinux/ui/dialogs/settings_dialog.py",
|
|
update_names,
|
|
)
|
|
self.assertNotIn(
|
|
f"{update_root}/ptlinux/desktop_entry.py",
|
|
update_names,
|
|
)
|
|
self.assertNotIn(
|
|
f"{update_root}/ptlinux/launcher_wrapper.py",
|
|
update_names,
|
|
)
|
|
self.assertIn(
|
|
f"{update_root}/ptlinux/launcher-update-public.pem",
|
|
update_names,
|
|
)
|
|
self.assertIn(
|
|
f"{update_root}/ptlinux/native/pt-identity-probe.exe",
|
|
update_names,
|
|
)
|
|
self.assertIn(
|
|
f"{update_root}/ptlinux/native/ptr-bcrypt-proxy.dll",
|
|
update_names,
|
|
)
|
|
gamescope_name = (
|
|
f"{update_root}/ptlinux/native/"
|
|
"gamescope-3.16.20-ubuntu26.04-amd64"
|
|
)
|
|
self.assertIn(gamescope_name, update_names)
|
|
self.assertEqual(
|
|
next(
|
|
member.mode
|
|
for member in update_members
|
|
if member.name == gamescope_name
|
|
),
|
|
0o755,
|
|
)
|
|
for name in (
|
|
"gamescope-compat.README",
|
|
"gamescope-compat.copyright",
|
|
"gamescope-lazy-output-images.patch",
|
|
"gamescope-wayland-live-resize.patch",
|
|
"gamescope-wayland-letterbox-backing.patch",
|
|
"gamescope-wayland-libdecor-input-scope.patch",
|
|
"gamescope-keep-nested-windows-visible.patch",
|
|
):
|
|
self.assertIn(
|
|
f"{update_root}/ptlinux/native/{name}",
|
|
update_names,
|
|
)
|
|
self.assertNotIn(
|
|
f"{update_root}/ptlinux/native/pt-world-fix.exe",
|
|
update_names,
|
|
)
|
|
self.assertNotIn(f"{update_root}/ptlinux/world_fix.py", update_names)
|
|
|
|
def test_release_builder_rejects_unsafe_version_text(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
output = Path(temporary_directory) / "output"
|
|
environment = os.environ.copy()
|
|
environment["PTLINUX_VERSION"] = "../unsafe"
|
|
environment["PTLINUX_OUTPUT_DIRECTORY"] = str(output)
|
|
result = subprocess.run(
|
|
["bash", str(PROJECT_ROOT / "tools/build-aio-release.sh")],
|
|
cwd=PROJECT_ROOT,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("expected X.Y.Z", result.stderr)
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_compact_release_can_import_overlay_in_clean_interpreter(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
release_root = Path(temporary_directory)
|
|
shutil.copytree(SOURCE_ROOT / "ptlinux", release_root / "ptlinux")
|
|
(release_root / "ptdiag").mkdir()
|
|
shutil.copy2(
|
|
SOURCE_ROOT / "ptdiag" / "__init__.py",
|
|
release_root / "ptdiag" / "__init__.py",
|
|
)
|
|
shutil.copy2(
|
|
SOURCE_ROOT / "ptdiag" / "assbin.py",
|
|
release_root / "ptdiag" / "assbin.py",
|
|
)
|
|
shutil.copy2(
|
|
SOURCE_ROOT / "ptdiag" / "logs.py",
|
|
release_root / "ptdiag" / "logs.py",
|
|
)
|
|
|
|
environment = os.environ.copy()
|
|
environment["PYTHONPATH"] = str(release_root)
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", "import ptlinux.overlay"],
|
|
cwd=release_root,
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|