Merge pull request #716 from merwie/feat/relinker-needed-modules-outside-module-dirs

feat(relinker): relink needed modules outside the module directories
This commit is contained in:
Adrià 2026-10-07 17:51:47 +02:00 committed by GitHub
commit f71bf7fd40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 9 deletions

View File

@ -218,6 +218,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $<TARGET_FILE:relinker>)
add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $<TARGET_FILE:relinker>)
add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $<TARGET_FILE:relinker>)
add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $<TARGET_FILE:relinker>)
add_test(NAME guest_symbol_names COMMAND ${Python3_EXECUTABLE}

View File

@ -135,7 +135,7 @@ int main(const int argc, char* argv[]) {
<< std::filesystem::path(absPath).filename().string() << "\n"
<< "libs/\n *.prx\napp0/\n <game resources>\n";
for (const auto& artifact : guestArtifacts)
std::cout << " " << artifact.Path.parent_path().filename().string() << "/" << artifact.Path.filename().string() << '\n';
std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_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';

View File

@ -27,6 +27,14 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (hasPrx) directories.push_back(prx);
std::vector<std::filesystem::path> paths;
std::set<std::string> unmatchedExclusions = excludedModules;
const auto isElf = [](const std::filesystem::path& path) {
std::ifstream stream(path, std::ios::binary);
if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + path.string());
char magic[4]{};
stream.read(magic, 4);
if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + path.string());
return stream.gcount() == 4 && static_cast<unsigned char>(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F';
};
for (const auto& directory : directories) {
if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string());
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
@ -36,14 +44,35 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
continue;
}
if (!entry.is_regular_file()) continue;
std::ifstream stream(entry.path(), std::ios::binary);
if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string());
char magic[4]{};
stream.read(magic, 4);
if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string());
if (stream.gcount() == 4 && static_cast<unsigned char>(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path());
if (isElf(entry.path())) paths.push_back(entry.path());
}
}
if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table");
std::set<std::string> missingNeeded;
for (std::size_t offset = 0; offset < dynamic.DynamicSegmentData.size(); offset += 16) {
if (Io::ReadU64(dynamic.DynamicSegmentData, offset) != 1) continue;
const auto nameOffset = Io::ReadU64(dynamic.DynamicSegmentData, offset + 8);
if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid dependency string offset");
const auto start = dynamic.DynStrData.begin() + static_cast<std::ptrdiff_t>(nameOffset);
const auto end = std::find(start, dynamic.DynStrData.end(), 0);
if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated dependency string");
const std::string name(start, end);
if (excludedModules.contains(name)) continue;
if (std::none_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) missingNeeded.insert(name);
}
if (!missingNeeded.empty()) {
std::map<std::string, std::filesystem::path> found;
for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) {
if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) {
it.disable_recursion_pending();
continue;
}
const auto name = it->path().filename().string();
if (!it->is_regular_file() || !missingNeeded.contains(name) || !isElf(it->path())) continue;
if (!found.emplace(name, it->path()).second) throw Domain::RelinkerException("Ambiguous needed module: " + found.at(name).string() + " and " + it->path().string());
}
for (const auto& [name, path] : found) paths.push_back(path);
}
if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin());
std::sort(paths.begin(), paths.end());
if (paths.empty()) return {};
@ -193,7 +222,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size());
Io::AppendString(dynamic.DynStrData, name);
};
for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().filename().generic_string() + "/" + images[index].OutputName);
for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName);
for (const auto& name : hostLibraries) addNeeded(name);
std::string guestRunPath = runPath;
if (!windows) {
@ -204,7 +233,7 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
std::vector<GuestArtifact> artifacts;
for (const auto index : order) {
const auto& image = images[index];
const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().filename().generic_string();
const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string();
const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory;
const auto target = destination / image.OutputName;
if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output");

View File

@ -0,0 +1,77 @@
from pathlib import Path
import os
import struct
import subprocess
import sys
import tempfile
from test_guest_intel_trampolines import main_fixture
from test_guest_module_directories import module_with_symbol, needed_libraries
NEEDED = b"needed.prx"
def executable_with_needed():
image = main_fixture()
strings = b"\0" + NEEDED + b"\0"
image[0x4800:0x4800 + len(strings)] = strings
tags = []
for position in range(0x4600, 0x4600 + 0x200, 16):
tag, value = struct.unpack_from("<qQ", image, position)
if tag == 0:
break
tags.append((tag, value))
tags = [(5, 0x4800) if tag == 5 else (10, len(strings)) if tag == 10 else (tag, value) for tag, value in tags]
tags += [(1, 1), (0, 0)]
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, 0x4600 + index * 16, *tag)
struct.pack_into("<QQ", image, 120 + 32, len(tags) * 16, len(tags) * 16)
return image
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-needed-modules-") as directory:
work = Path(directory)
def convert(case, windows):
(case / "sce_module").mkdir(parents=True, exist_ok=True)
source = case / "input.elf"
source.write_bytes(executable_with_needed())
output = case / ("output.exe" if windows else "output.elf")
result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)],
capture_output=True, text=True, timeout=30)
return result, output
for windows in (False, True):
case = work / f"{windows}-found"
modules = case / "Media" / "Modules"
modules.mkdir(parents=True)
(modules / "needed.prx").write_bytes(module_with_symbol(True))
(case / "Media" / "needed.prx").write_text("not ELF")
result, output = convert(case, windows)
assert result.returncode == 0, (result.stdout, result.stderr)
artifact = case / "app0" / "Media" / "Modules" / "needed.prx.guest.prx"
assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact
assert list((case / "app0").rglob("*.guest.prx")) == [artifact]
assert " Media/Modules/needed.prx.guest.prx\n" in result.stdout, result.stdout
if windows and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42, (run.returncode, run.stdout, run.stderr)
if not windows:
needed = needed_libraries(output.read_bytes())
assert needed == ["$ORIGIN/app0/Media/Modules/needed.prx.guest.prx"], needed
case = work / f"{windows}-ambiguous"
for name in ("first", "second"):
(case / name).mkdir(parents=True)
(case / name / "needed.prx").write_bytes(module_with_symbol(True))
result, output = convert(case, windows)
assert result.returncode == 2 and "Ambiguous needed module" in result.stderr, result.stderr
assert not output.exists(), output
print("Guest needed module tests passed")
if __name__ == "__main__":
main()