Release 1.1.3 with game window and Gamescope lifecycle fixes

This commit is contained in:
2026-10-25 20:47:19 +01:00
parent b6de80e758
commit 1f2aad0ee2
42 changed files with 9291 additions and 153 deletions

202
tools/build-aio-release.sh Executable file
View File

@@ -0,0 +1,202 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
PACKAGE_SOURCE_ROOT="${PROJECT_ROOT}/src"
GAMESCOPE_PROVENANCE_ROOT="${PROJECT_ROOT}/native/gamescope"
SOURCE_VERSION="$(sed -n 's/^__version__ = "\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)"$/\1/p' "${PACKAGE_SOURCE_ROOT}/ptlinux/__init__.py")"
VERSION="${PTLINUX_VERSION:-$SOURCE_VERSION}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
printf 'Invalid release version: %s (expected X.Y.Z)\n' "$VERSION" >&2
exit 2
fi
if [[ "$VERSION" != "$SOURCE_VERSION" ]]
then
printf 'Release version %s does not match source version %s.\n' "$VERSION" "$SOURCE_VERSION" >&2
exit 2
fi
RELEASE_MODE="${PTLINUX_RELEASE_MODE:-development}"
if [[ "$RELEASE_MODE" != "development" && "$RELEASE_MODE" != "production" ]]
then
printf 'Invalid release mode: %s (expected development or production)\n' "$RELEASE_MODE" >&2
exit 2
fi
VERSION_SLUG="${VERSION//./-}"
ARCHIVE_NAME="pristontale-eu-linux-installer-v${VERSION_SLUG}"
OUTPUT_DIRECTORY="${PTLINUX_OUTPUT_DIRECTORY:-${PROJECT_ROOT}/.artifacts/releases/aio}"
PYTHON_BIN="${PTLINUX_PYTHON:-python3}"
if [[ "$RELEASE_MODE" == "production" && -z "${PTLINUX_SIGNING_KEY:-}" ]]
then
printf '%s\n' "PTLINUX_SIGNING_KEY is required for a production release." >&2
exit 2
fi
if [[ -n "${PTLINUX_SIGNING_KEY:-}" ]]
then
PTLINUX_SIGNING_KEY="$(
PYTHONPATH="${PROJECT_ROOT}/src:${PROJECT_ROOT}${PYTHONPATH:+:${PYTHONPATH}}" \
"$PYTHON_BIN" -c \
'import sys
from pathlib import Path
from scripts.release_lib import ReleaseToolError, _safe_signing_key_path
try:
print(_safe_signing_key_path(Path(sys.argv[1]), output_directory=Path(sys.argv[2])))
except ReleaseToolError as exc:
print(f"Invalid release signing key: {exc}", file=sys.stderr)
raise SystemExit(2)' \
"$PTLINUX_SIGNING_KEY" "$OUTPUT_DIRECTORY"
)"
export PTLINUX_SIGNING_KEY
fi
STAGING_DIRECTORY="$(mktemp -d /tmp/ptlinux-release.XXXXXX)"
RELEASE_ROOT="${STAGING_DIRECTORY}/${ARCHIVE_NAME}"
cleanup()
{
rm -r -- "$STAGING_DIRECTORY"
}
trap cleanup EXIT
install -d -m 0755 "$RELEASE_ROOT"
install -d -m 0755 \
"$RELEASE_ROOT/ptlinux" \
"$RELEASE_ROOT/ptlinux/native" \
"$RELEASE_ROOT/ptdiag" \
"$RELEASE_ROOT/packaging" \
"$RELEASE_ROOT/release" \
"$RELEASE_ROOT/scripts"
shopt -s globstar nullglob
for SOURCE_MODULE in "${PACKAGE_SOURCE_ROOT}/ptlinux/"**/*.py
do
RELATIVE_MODULE="${SOURCE_MODULE#"${PACKAGE_SOURCE_ROOT}/ptlinux/"}"
install -D -m 0644 \
"$SOURCE_MODULE" \
"${RELEASE_ROOT}/ptlinux/${RELATIVE_MODULE}"
done
shopt -u globstar nullglob
install -m 0644 \
"${PACKAGE_SOURCE_ROOT}/ptlinux/launcher-update-public.pem" \
"$RELEASE_ROOT/ptlinux/launcher-update-public.pem"
install -m 0644 \
"${PACKAGE_SOURCE_ROOT}/ptlinux/native/pt-identity-probe.exe" \
"$RELEASE_ROOT/ptlinux/native/pt-identity-probe.exe"
install -m 0644 \
"${PACKAGE_SOURCE_ROOT}/ptlinux/native/ptr-bcrypt-proxy.dll" \
"$RELEASE_ROOT/ptlinux/native/ptr-bcrypt-proxy.dll"
install -m 0755 \
"${PACKAGE_SOURCE_ROOT}/ptlinux/native/gamescope-3.16.20-ubuntu26.04-amd64" \
"$RELEASE_ROOT/ptlinux/native/gamescope-3.16.20-ubuntu26.04-amd64"
install -m 0644 \
"${GAMESCOPE_PROVENANCE_ROOT}/gamescope-compat.README" \
"${GAMESCOPE_PROVENANCE_ROOT}/gamescope-compat.copyright" \
"${GAMESCOPE_PROVENANCE_ROOT}/patches/gamescope-lazy-output-images.patch" \
"${GAMESCOPE_PROVENANCE_ROOT}/patches/gamescope-wayland-live-resize.patch" \
"${GAMESCOPE_PROVENANCE_ROOT}/patches/gamescope-wayland-letterbox-backing.patch" \
"${GAMESCOPE_PROVENANCE_ROOT}/patches/gamescope-wayland-libdecor-input-scope.patch" \
"${GAMESCOPE_PROVENANCE_ROOT}/patches/gamescope-keep-nested-windows-visible.patch" \
"$RELEASE_ROOT/ptlinux/native/"
install -m 0644 "${PACKAGE_SOURCE_ROOT}/ptdiag/__init__.py" "$RELEASE_ROOT/ptdiag/__init__.py"
install -m 0644 "${PACKAGE_SOURCE_ROOT}/ptdiag/assbin.py" "$RELEASE_ROOT/ptdiag/assbin.py"
install -m 0644 "${PACKAGE_SOURCE_ROOT}/ptdiag/logs.py" "$RELEASE_ROOT/ptdiag/logs.py"
install -m 0644 \
"${PROJECT_ROOT}/packaging/eu.pristontale.PTLinux.desktop" \
"${PROJECT_ROOT}/packaging/eu.pristontale.PTLinux.png" \
"${PROJECT_ROOT}/packaging/install-dependencies.sh" \
"$RELEASE_ROOT/packaging/"
install -m 0755 "${PROJECT_ROOT}/packaging/pristontale-eu" "$RELEASE_ROOT/packaging/pristontale-eu"
install -m 0644 \
"${PROJECT_ROOT}/release/config.json" \
"$RELEASE_ROOT/release/config.json"
install -m 0644 \
"${PROJECT_ROOT}/scripts/__init__.py" \
"${PROJECT_ROOT}/scripts/repository_paths.py" \
"${PROJECT_ROOT}/scripts/release_lib.py" \
"$RELEASE_ROOT/scripts/"
install -m 0755 "${PROJECT_ROOT}/install.sh" "$RELEASE_ROOT/install.sh"
install -m 0644 "${PROJECT_ROOT}/RELEASE_README.md" "$RELEASE_ROOT/README.md"
install -d "$OUTPUT_DIRECTORY"
tar \
--sort=name \
--owner=0 \
--group=0 \
--numeric-owner \
--mtime="@${SOURCE_DATE_EPOCH:-0}" \
-C "$STAGING_DIRECTORY" \
-czf "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.tar.gz" \
"$ARCHIVE_NAME"
chmod 0644 "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.tar.gz"
(
cd "$OUTPUT_DIRECTORY"
sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
)
chmod 0644 "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.tar.gz.sha256"
if ! command -v zip >/dev/null 2>&1
then
printf 'Missing release dependency: zip\n' >&2
exit 1
fi
ZIP_TIMESTAMP_EPOCH="${SOURCE_DATE_EPOCH:-315532800}"
if (( ZIP_TIMESTAMP_EPOCH < 315532800 ))
then
# ZIP timestamps cannot represent dates before 1980-01-01.
ZIP_TIMESTAMP_EPOCH=315532800
fi
find "$RELEASE_ROOT" -exec touch -h -d "@${ZIP_TIMESTAMP_EPOCH}" {} +
(
cd "$STAGING_DIRECTORY"
LC_ALL=C find "$ARCHIVE_NAME" -print \
| LC_ALL=C sort \
| zip -X -q "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.zip" -@
)
chmod 0644 "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.zip"
(
cd "$OUTPUT_DIRECTORY"
sha256sum "${ARCHIVE_NAME}.zip" > "${ARCHIVE_NAME}.zip.sha256"
)
chmod 0644 "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.zip.sha256"
PUBLIC_KEY_ARTIFACT="${OUTPUT_DIRECTORY}/launcher-update-public.pem"
install -m 0644 \
"${PACKAGE_SOURCE_ROOT}/ptlinux/launcher-update-public.pem" \
"$PUBLIC_KEY_ARTIFACT"
for installer_archive in \
"${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.tar.gz" \
"${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.zip"
do
if [[ -n "${PTLINUX_SIGNING_KEY:-}" ]]
then
"$PYTHON_BIN" "${PROJECT_ROOT}/tools/sign_release_artifact.py" \
--signing-key "$PTLINUX_SIGNING_KEY" \
--public-key "${PACKAGE_SOURCE_ROOT}/ptlinux/launcher-update-public.pem" \
"$installer_archive"
else
rm -f -- "${installer_archive}.sig"
fi
done
UPDATE_COMMAND=(
"$PYTHON_BIN"
"${PROJECT_ROOT}/tools/build_launcher_update.py"
--version "$VERSION"
--output-directory "$OUTPUT_DIRECTORY"
--repository-url "${PTLINUX_REPOSITORY_URL:-https://git.m4ko.net/m4ko/pristontale-eu-linux}"
)
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]
then
UPDATE_COMMAND+=(--source-date-epoch "$SOURCE_DATE_EPOCH")
fi
if [[ -n "${PTLINUX_SIGNING_KEY:-}" ]]
then
UPDATE_COMMAND+=(--signing-key "$PTLINUX_SIGNING_KEY")
fi
if [[ "$RELEASE_MODE" == "production" ]]
then
UPDATE_COMMAND+=(--require-signature)
fi
"${UPDATE_COMMAND[@]}"
printf '%s\n' "${OUTPUT_DIRECTORY}/${ARCHIVE_NAME}.tar.gz"

103
tools/build_launcher_update.py Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Compatibility entry point for the deterministic native-launcher update."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import sys
import time
from typing import Sequence
# This file remains at its historical path for release scripts and imports. The
# implementation belongs to the canonical release library under scripts/.
_BOOTSTRAP_ROOT = Path(__file__).resolve().parents[1]
if str(_BOOTSTRAP_ROOT) not in sys.path:
sys.path.insert(0, str(_BOOTSTRAP_ROOT))
from scripts.release_lib import ( # noqa: E402
ReleaseToolError,
build_legacy_launcher_update,
)
from scripts.repository_paths import PROJECT_ROOT # noqa: E402
DEFAULT_REPOSITORY_URL = "https://git.m4ko.net/m4ko/pristontale-eu-linux"
DEFAULT_MINIMUM_UPDATER_VERSION = "0.5.0"
ReleaseBuildError = ReleaseToolError
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument("--output-directory", type=Path, required=True)
parser.add_argument(
"--repository-url",
default=DEFAULT_REPOSITORY_URL,
help="public Gitea repository base URL",
)
parser.add_argument(
"--minimum-updater-version",
default=DEFAULT_MINIMUM_UPDATER_VERSION,
)
parser.add_argument("--signing-key", type=Path)
parser.add_argument(
"--require-signature",
action="store_true",
help="fail unless a signing key is provided and matches the bundled public key",
)
parser.add_argument(
"--source-date-epoch",
type=int,
default=int(os.environ.get("SOURCE_DATE_EPOCH", str(int(time.time())))),
)
return parser
def build_launcher_update(
*,
version: str,
output_directory: Path,
repository_url: str,
minimum_updater_version: str,
signing_key: Path | None,
require_signature: bool = False,
source_date_epoch: int,
) -> tuple[Path, ...]:
"""Forward the historical Python API to the canonical release library."""
return build_legacy_launcher_update(
version=version,
output_directory=output_directory,
repository_url=repository_url,
minimum_updater_version=minimum_updater_version,
signing_key=signing_key,
require_signature=require_signature,
source_date_epoch=source_date_epoch,
)
def main(argv: Sequence[str] | None = None) -> int:
arguments = build_parser().parse_args(argv)
try:
artifacts = build_launcher_update(
version=arguments.version,
output_directory=arguments.output_directory,
repository_url=arguments.repository_url,
minimum_updater_version=arguments.minimum_updater_version,
signing_key=arguments.signing_key,
require_signature=arguments.require_signature,
source_date_epoch=arguments.source_date_epoch,
)
except (OSError, ReleaseBuildError) as exc:
print(f"Could not build launcher update: {exc}", file=sys.stderr)
return 1
for artifact in artifacts:
print(artifact)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Create and verify an atomic OpenSSL signature for one release artifact."""
from __future__ import annotations
import argparse
from pathlib import Path
import shutil
import subprocess
import sys
from typing import Sequence
from uuid import uuid4
_BOOTSTRAP_ROOT = Path(__file__).resolve().parents[1]
if str(_BOOTSTRAP_ROOT) not in sys.path:
sys.path.insert(0, str(_BOOTSTRAP_ROOT))
from scripts.release_lib import ( # noqa: E402
ReleaseToolError,
_safe_signing_key_path,
)
class ReleaseSigningError(RuntimeError):
"""Raised when a release artifact cannot be signed safely."""
def sign_release_artifact(
artifact: Path,
*,
signing_key: Path,
public_key: Path,
) -> Path:
"""Sign an artifact and verify the result with the published public key."""
source = _regular_file(artifact, "release artifact")
try:
private = _safe_signing_key_path(signing_key)
except ReleaseToolError as exc:
raise ReleaseSigningError(str(exc)) from exc
public = _regular_file(public_key, "public key")
openssl = shutil.which("openssl")
if openssl is None:
raise ReleaseSigningError("OpenSSL is required to sign release artifacts")
signature = Path(f"{source}.sig")
if signature.is_dir() and not signature.is_symlink():
raise ReleaseSigningError(f"signature path is a directory: {signature}")
signature.unlink(missing_ok=True)
temporary = signature.with_name(f".{signature.name}.{uuid4().hex}.tmp")
try:
_sign(openssl, source, temporary, private)
_verify(openssl, source, temporary, public)
temporary.chmod(0o644)
temporary.replace(signature)
except BaseException:
temporary.unlink(missing_ok=True)
raise
return signature
def _regular_file(path: Path, label: str) -> Path:
value = path.expanduser()
if value.is_symlink() or not value.is_file():
raise ReleaseSigningError(f"{label} is missing or not a regular file: {value}")
return value.resolve()
def _sign(openssl: str, artifact: Path, signature: Path, private_key: Path) -> None:
_run_openssl(
[
openssl,
"dgst",
"-sha256",
"-sign",
str(private_key),
"-out",
str(signature),
str(artifact),
],
"sign the release artifact",
)
def _verify(openssl: str, artifact: Path, signature: Path, public_key: Path) -> None:
_run_openssl(
[
openssl,
"dgst",
"-sha256",
"-verify",
str(public_key),
"-signature",
str(signature),
str(artifact),
],
"verify the release artifact signature",
)
def _run_openssl(command: list[str], action: str) -> None:
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=120,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise ReleaseSigningError(f"OpenSSL could not {action}: {exc}") from exc
if result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip() or "unknown error"
raise ReleaseSigningError(f"OpenSSL could not {action}: {detail}")
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("artifact", type=Path)
parser.add_argument("--signing-key", type=Path, required=True)
parser.add_argument("--public-key", type=Path, required=True)
arguments = parser.parse_args(argv)
try:
signature = sign_release_artifact(
arguments.artifact,
signing_key=arguments.signing_key,
public_key=arguments.public_key,
)
except (OSError, ReleaseSigningError) as exc:
print(f"Could not sign release artifact: {exc}", file=sys.stderr)
return 1
print(signature)
return 0
if __name__ == "__main__":
raise SystemExit(main())