fix(relinker): use app0 guest module layout and clarify output diagnostics

This commit is contained in:
boykopovar 2026-09-29 02:05:02 +03:00
parent cc41e847c8
commit 6742ecf70f
7 changed files with 34 additions and 16 deletions

View File

@ -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<char>(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 <game resources>\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);

View File

@ -18,10 +18,7 @@ std::vector<GuestArtifact> 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<std::filesystem::path> paths;
@ -119,18 +116,17 @@ std::vector<GuestArtifact> 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<GuestArtifact> 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;

View File

@ -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

View File

@ -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")

View File

@ -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():

View File

@ -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

View File

@ -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")