mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
setup failures reported in #137, #138 and #143 happened only after the long fetch/build or at Riot startup. scripts/preflight.py now runs from scripts/build.sh (before configure.py, so an old interpreter gets this message and not a traceback) and from scripts/install-runtime.sh, and checks the host once: - python 3.10+, cmake 3.20+ (the bundled indium CMakeLists requires it), git-lfs 2.13+, kernel 5.14+ and LLVM 16+ exit nonzero with one actionable line each. RCLOL_SKIP_PREFLIGHT=1 bypasses the gate. - llvm-objdump and llvm-nm are required, and unresolved names are explained. - Vulkan 1.3 and timeline semaphores are required when vulkaninfo is installed; they only warn when it is not, because a missing tool does not mean a missing driver. - the X display is probed with xdpyinfo in the environment Darling inherits. It only warns: a build does not need a display. - a prepared build/darling-src is scanned for small .dylib/.a/.so/.o files that still contain Git LFS pointer text, which links but crashes the guest. - --json prints the same report for the diagnostic tools. The installers no longer call hashlib.file_digest (Python 3.11+), so Ubuntu 22.04's Python 3.10 works instead of failing after the overlays are built (#137). A streaming tools/file_digest.py replaces it. LLVM command line tools are resolved by tools/llvm_tools.py: an unversioned name on PATH first, then the highest llvm-<tool>-<N>, then llvm-config --bindir. Debian/Ubuntu install llvm-objdump and llvm-nm only under /usr/lib/llvm-<N>/bin, which broke the runtime validation (#138). Six tools now use it. The LLVM 16 floor also stops the LLVM 15 runtime abort from #139 before anything is built. Two test helpers used APIs added after Python 3.10; they are shimmed so the same suite runs on 3.10, 3.11 and 3.14. Adds tests/test_preflight.py (24 tests, fake host tools only).
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Atomically install a built Darling server; never stop or start any process."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import stat
|
|
import tempfile
|
|
|
|
from file_digest import sha256
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def digest(path):
|
|
return sha256(path)
|
|
|
|
|
|
def live_binaries(destination):
|
|
result = {}
|
|
for process in Path("/proc").iterdir():
|
|
if not process.name.isdigit():
|
|
continue
|
|
executable = process / "exe"
|
|
try:
|
|
if str(executable.readlink()).removesuffix(" (deleted)") == str(destination):
|
|
result[process.name] = digest(executable)
|
|
except (OSError, ProcessLookupError):
|
|
continue
|
|
return result
|
|
|
|
|
|
def install(source, destination, backups):
|
|
source = source.resolve()
|
|
destination = destination.absolute()
|
|
if destination.is_symlink():
|
|
raise ValueError("The server destination must be a regular file, not a symlink")
|
|
installed = destination.stat()
|
|
if not stat.S_ISREG(installed.st_mode):
|
|
raise ValueError("The server destination is not a regular file")
|
|
with source.open("rb") as input:
|
|
if input.read(4) != b"\x7fELF":
|
|
raise ValueError("The source is not an ELF binary")
|
|
new_hash, old_hash = digest(source), digest(destination)
|
|
before = live_binaries(destination)
|
|
if new_hash == old_hash:
|
|
return {"installed": True, "already_current": True, "sha256": new_hash, "restart_performed": False}
|
|
backups.mkdir(parents=True, exist_ok=True)
|
|
backup = backups / ("darlingserver-" + old_hash)
|
|
if backup.exists():
|
|
if digest(backup) != old_hash:
|
|
raise ValueError("The existing backup has unexpected contents")
|
|
else:
|
|
shutil.copy2(destination, backup)
|
|
fd, temporary = tempfile.mkstemp(prefix=".darlingserver-staged-", dir=destination.parent)
|
|
try:
|
|
with os.fdopen(fd, "wb") as output, source.open("rb") as input:
|
|
shutil.copyfileobj(input, output)
|
|
output.flush()
|
|
os.fchmod(output.fileno(), installed.st_mode & 0o777)
|
|
os.fchown(output.fileno(), installed.st_uid, installed.st_gid)
|
|
os.fsync(output.fileno())
|
|
if digest(Path(temporary)) != new_hash:
|
|
raise ValueError("The staged binary does not match the build")
|
|
os.replace(temporary, destination)
|
|
directory = os.open(destination.parent, os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(directory)
|
|
finally:
|
|
os.close(directory)
|
|
finally:
|
|
if os.path.exists(temporary):
|
|
os.unlink(temporary)
|
|
if digest(destination) != new_hash:
|
|
raise ValueError("The installed binary does not match the build")
|
|
after = live_binaries(destination)
|
|
if any(after[pid] != old for pid, old in before.items() if pid in after):
|
|
raise ValueError("An active process no longer maps its original binary")
|
|
return {
|
|
"installed": True, "sha256": new_hash, "backup": str(backup),
|
|
"active_servers_preserved": len(before.keys() & after.keys()), "restart_performed": False,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--binary", type=Path, default=ROOT / "build/darling-src/build/src/external/darlingserver/darlingserver")
|
|
parser.add_argument("--destination", type=Path, default=Path("/usr/local/bin/darlingserver"))
|
|
parser.add_argument("--backups", type=Path, default=ROOT / "build/backups/darlingserver")
|
|
args = parser.parse_args()
|
|
try:
|
|
result = install(args.binary, args.destination, args.backups)
|
|
except PermissionError:
|
|
parser.exit(1, "Installation requires permission to write the server directory; run this installer with sudo in your terminal.\n")
|
|
print(json.dumps(result))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|