From 6742ecf70f91a35a7da537c308d23bdea9276c2e Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:05:02 +0300 Subject: [PATCH 1/3] fix(relinker): use app0 guest module layout and clarify output diagnostics --- core/relinker/main.cpp | 12 ++++++++++++ .../relinker/src/guest/GuestModuleBuilder.cpp | 14 +++++--------- core/relinker/relinker/tests/test_empty_tls.py | 2 +- .../relinker/tests/test_linux_load_alignment.py | 12 +++++++++++- core/relinker/relinker/tests/test_optional_plt.py | 2 +- .../relinker/tests/test_tls_function_coverage.py | 2 +- core/relinker/relinker/tests/test_windows_gui.py | 6 +++--- 7 files changed, 34 insertions(+), 16 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f36f383c3..705c3f6b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -36,6 +36,9 @@ int main(const int argc, char* argv[]) { } try { + auto extension = std::filesystem::path(args.outputPath).extension().string(); + for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + if (!args.toWindows && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n"; Io::FileReader fileReader; Io::FileWriter fileWriter; @@ -111,6 +114,15 @@ int main(const int argc, char* argv[]) { } fileWriter.Write(absPath, executableBytes); std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; + std::cout << "Expected runtime layout (relative to the output executable):\n" + << std::filesystem::path(absPath).filename().string() << "\n" + << "libs/\n *.prx\napp0/\n \n"; + if (!guestArtifacts.empty()) { + std::cout << " " << guestArtifacts.front().Path.parent_path().filename().string() << "/\n"; + for (const auto& artifact : guestArtifacts) std::cout << " " << artifact.Path.filename().string() << '\n'; + } + std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; + if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 4c4c92fea..a8655e1e4 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -18,10 +18,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const bool hasSingular = std::filesystem::exists(singular); const bool hasPlural = std::filesystem::exists(plural); if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist beside the input executable"); - if (!hasSingular && !hasPlural) { - std::cout << "WARNING: sce_module not found beside the input executable; it may be unnecessary or missing.\n"; - return {}; - } + if (!hasSingular && !hasPlural) throw Domain::RelinkerException("sce_module/sce_modules was not found beside the input executable: " + root.string() + ". Use --skip-sce-module only if this game can run without these modules."); const auto directory = hasSingular ? singular : plural; if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); std::vector paths; @@ -119,18 +116,17 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - const auto relativeDirectory = directory.filename().generic_string(); + const auto relativeDirectory = "app0/" + directory.filename().generic_string(); for (const auto index : order) if (!windows) addNeeded("$ORIGIN/" + relativeDirectory + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); std::string guestRunPath = runPath; if (!windows) { - if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/.."; - else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../"); + if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/../.."; + else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../../"); else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); } std::vector artifacts; - const auto destination = std::filesystem::absolute(outputPath).parent_path() / directory.filename(); - if (std::filesystem::exists(destination) && std::filesystem::equivalent(destination, directory)) throw Domain::RelinkerException("Guest output directory must differ from the source module directory"); + const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; for (const auto index : order) { const auto& image = images[index]; const auto target = destination / image.OutputName; diff --git a/core/relinker/relinker/tests/test_empty_tls.py b/core/relinker/relinker/tests/test_empty_tls.py index aaf0c633e..c92deba32 100644 --- a/core/relinker/relinker/tests/test_empty_tls.py +++ b/core/relinker/relinker/tests/test_empty_tls.py @@ -29,7 +29,7 @@ def main(): if access: data[0x200:0x20a] = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") source.write_bytes(data) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=20) if error: assert result.returncode == 2 and error in result.stderr and not output.exists(), result diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index 2e2d8afaf..bcb3ebaec 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -41,7 +41,11 @@ def main(): source = Path(directory) / "input.elf" output = Path(directory) / "output.elf" source.write_bytes(fixture()) - result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=20) + missing = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=20) + assert missing.returncode != 0 and not output.exists(), missing + assert "sce_module/sce_modules was not found" in missing.stderr, missing.stderr + assert "--skip-sce-module only if this game" in missing.stderr, missing.stderr + result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) if result.returncode != 0: raise AssertionError((result.returncode, result.stdout, result.stderr)) segments = loads(output.read_bytes()) @@ -49,6 +53,12 @@ def main(): alignment = max(segment[7] for segment in segments) if alignment != 0x4000 or first[3] % alignment != 0 or first[7] != alignment: raise AssertionError(("first PT_LOAD is not aligned to the largest segment alignment", segments)) + assert "output.elf\nlibs/\n *.prx\napp0/" in result.stdout, result.stdout + named_exe = Path(directory) / "linux.EXE" + warning = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(named_exe)], capture_output=True, text=True, timeout=20) + assert warning.returncode == 0, (warning.stdout, warning.stderr) + assert "--windows was not specified" in warning.stderr, warning.stderr + assert named_exe.read_bytes().startswith(b"\x7fELF"), "The filename must not select the output format" print("Linux load alignment test passed") diff --git a/core/relinker/relinker/tests/test_optional_plt.py b/core/relinker/relinker/tests/test_optional_plt.py index 894b47a7b..548197caa 100644 --- a/core/relinker/relinker/tests/test_optional_plt.py +++ b/core/relinker/relinker/tests/test_optional_plt.py @@ -38,7 +38,7 @@ def main(): source = work / (name + ".elf") output = work / (name + ".exe") source.write_bytes(fixture(tags)) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=20) if error is not None: if result.returncode != 2 or error not in result.stderr or output.exists(): diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index c0bc8145b..afc35a9ab 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -78,7 +78,7 @@ def main(): source = work / (name + ".elf") output = source.with_suffix(".exe") source.write_bytes(image) - result = subprocess.run([str(relinker), "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], capture_output=True, text=True, timeout=30) if error is not None: assert result.returncode == 2 and error in result.stderr and not output.exists(), result diff --git a/core/relinker/relinker/tests/test_windows_gui.py b/core/relinker/relinker/tests/test_windows_gui.py index 12698e829..f7dbc2b83 100644 --- a/core/relinker/relinker/tests/test_windows_gui.py +++ b/core/relinker/relinker/tests/test_windows_gui.py @@ -22,19 +22,19 @@ def main(): source.write_bytes(fixture()) cui_output = work / "cui.exe" - result = subprocess.run([str(relinker), "--windows", str(source), str(cui_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(cui_output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) assert subsystem(cui_output.read_bytes()) == 3, "expected IMAGE_SUBSYSTEM_WINDOWS_CUI by default" gui_output = work / "gui.exe" - result = subprocess.run([str(relinker), "--windows", "--windows-gui", str(source), str(gui_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", "--windows-gui", str(source), str(gui_output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) assert subsystem(gui_output.read_bytes()) == 2, "expected IMAGE_SUBSYSTEM_WINDOWS_GUI with --windows-gui" bad_output = work / "bad.exe" - result = subprocess.run([str(relinker), "--windows-gui", str(source), str(bad_output)], + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows-gui", str(source), str(bad_output)], capture_output=True, text=True, timeout=20) assert result.returncode != 0 and not bad_output.exists(), result print("Windows GUI subsystem integration tests passed") From c8beb90a5e40259d53ad27dfa2566395f91d4a03 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:13:22 +0300 Subject: [PATCH 2/3] build(release): package libraries under libs subfolder --- tools/package_release.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 86209fb60..25296672b 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -25,12 +25,12 @@ def package(platform, build, output, version): if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive: + with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for file in files: - archive.write(file, arcname=file.name) - with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz") as archive: + archive.write(file, arcname=f"libs/{file.name}") + with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9) as archive: for file in files: - archive.add(file, arcname=file.name) + archive.add(file, arcname=f"libs/{file.name}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" shutil.copy2(binary, output / asset) From 7445c6599418aefe8e16f69045e78a38bcda3835 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 29 Sep 2026 02:17:56 +0300 Subject: [PATCH 3/3] chore(readme): return percent calculation info --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b658c471..9d2bdc641 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Includes a [relinker](core/relinker) that converts executable to the target syst [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) +* System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. + [List of verified games](docs/user/COMPATIBILITY.md) Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz.