Files
2026-10-14 04:20:24 +02:00

103 lines
5.0 KiB
Python

# Copyright (C) 2026 half144 and PS5 React contributors
# SPDX-License-Identifier: GPL-3.0-or-later
# Additional attribution term: see LICENSE-ATTRIBUTION.
"""rar-extract (native/rar_worker): RarLab's UnRAR library and a small MIT-licensed driver, built as a
program of its own. UnRAR's license allows its use to extract RAR archives but is not GPL-compatible,
so it is never linked into the engine; the engine starts this program and reads its reports.
"""
import subprocess
import tarfile
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from common import DEPS, LOCK, ROOT, digest, fetch
# The library objects of UnRAR's makefile, without its command line front end and unrar.dll layer.
SOURCES = ("strlist strfn pathfn smallfn global file filefn filcreat archive arcread unicode system crypt "
"crc rawread encname resource match timefn rdwrfn consio options errhnd rarvm secpassword "
"rijndael getbits sha1 sha256 blake2s hash extinfo extract volume list find unpack headers "
"threadpool rs16 cmddata ui largepage filestr scantree qopen").split()
DEFINES = ["-DRARDLL", "-DSILENT", "-DRAR_SMP", "-DUNRAR_UTF8_NAMES", "-D_FILE_OFFSET_BITS=64", "-D_LARGEFILE_SOURCE"]
def unrar_source():
"""The pinned UnRAR source tree with the repository's patches applied."""
pinned = LOCK["unrarSource"]
root = DEPS / f"unrar-{pinned['version']}"
marker = root / ".patched"
patches = [ROOT / p for p in pinned["patches"]]
expected = "\n".join(digest(p) for p in patches) + "\n"
if not marker.exists() or marker.read_text() != expected:
archive = fetch(pinned["url"], DEPS / f"unrarsrc-{pinned['version']}.tar.gz", pinned["sha256"])
root.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive) as package:
for member in package.getmembers():
member.name = member.name.removeprefix("unrar/")
if member.isfile() and "/" not in member.name:
package.extract(member, root, filter="data")
for patch in patches:
subprocess.run(["patch", "-p1", "--forward", "--quiet", "-i", str(patch)], cwd=root, check=True)
marker.write_text(expected)
return root
def up_to_date(target, command, inputs):
"""Whether `target` was made by `command` (recorded beside it) and is newer than every input."""
recorded = target.with_name(target.name + ".cmd")
if not target.exists() or not recorded.exists() or recorded.read_text() != "\0".join(command):
return False
built = target.stat().st_mtime
return all(Path(p).exists() and Path(p).stat().st_mtime <= built for p in inputs)
def depfile_inputs(depfile):
"""The files listed in a compiler -MMD depfile, or None when it is missing."""
if not depfile.exists():
return None
text = depfile.read_text().replace("\\\n", " ")
return text.split(":", 1)[1].split() if ":" in text else None
def build(output, compiler, flags=(), libraries=(), env=None):
"""Compiles UnRAR and the driver with `compiler` (a command list) and links `output`, skipping objects
and the link when their command and inputs are unchanged since the last build."""
source = unrar_source()
objects = output.parent / (output.name + ".obj")
objects.mkdir(parents=True, exist_ok=True)
common = [*compiler, "-O2", "-fexceptions", *DEFINES, *flags]
def compile_one(item):
path, extra = item
obj = objects / (path.stem + ".o")
depfile = obj.with_suffix(".d")
# UnRAR's headers are a system include for the driver, which builds with the engine's warnings.
command = [*map(str, common), *extra, "-isystem", str(source), "-MMD", "-MF", str(depfile),
"-c", str(path), "-o", str(obj)]
inputs = depfile_inputs(depfile)
if inputs is not None and up_to_date(obj, command, inputs):
return obj
result = subprocess.run(command, capture_output=True, text=True, env=env)
if result.returncode:
raise RuntimeError(f"{path.name}: {result.stderr[-4000:]}")
obj.with_name(obj.name + ".cmd").write_text("\0".join(command))
return obj
library = ["-std=c++17", "-w"]
driver = ["-std=c++20", "-Wall", "-Wextra", "-Werror"]
items = [(source / f"{name}.cpp", library) for name in SOURCES] + [(ROOT / "native/rar_worker/rar_worker.cpp", driver)]
with ThreadPoolExecutor(8) as pool:
built = list(pool.map(compile_one, items))
command = [*map(str, compiler), *map(str, flags), *map(str, built), *map(str, libraries), "-o", str(output)]
if up_to_date(output, command, [*built, *libraries]):
return output
result = subprocess.run(command, capture_output=True, text=True, env=env)
if result.returncode:
raise RuntimeError(f"rar-extract link: {result.stderr[-4000:]}")
output.with_name(output.name + ".cmd").write_text("\0".join(command))
return output
def build_host(output):
"""rar-extract for the desktop preview and the archive tests."""
return build(output, ["clang++"], ["-pthread"])