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
[](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.
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")
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)