Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
202
tools/build-aio-release.sh
Executable file
202
tools/build-aio-release.sh
Executable 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
103
tools/build_launcher_update.py
Executable 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())
|
||||
139
tools/sign_release_artifact.py
Normal file
139
tools/sign_release_artifact.py
Normal 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())
|
||||
Reference in New Issue
Block a user