Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions editor/export/editor_export_platform_pc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
#include "core/os/shared_object.h"
#include "scene/resources/image_texture.h" // IWYU pragma: keep. Misdetection of `logo`.
Expand Down Expand Up @@ -61,6 +62,7 @@ void EditorExportPlatformPC::get_export_options(List<ExportOption> *r_options) c
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, ext_filter), ""));

r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_wrapper", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_debug_symbols", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1));

r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "binary_format/embed_pck"), false));

Expand Down Expand Up @@ -152,6 +154,98 @@ Error EditorExportPlatformPC::export_project(const Ref<EditorExportPreset> &p_pr
return err;
}

Error EditorExportPlatformPC::_unzip_debugsymbols(Ref<DirAccess> &p_da, const String &p_path, const String &p_symbols_path, const String &p_zip_path) {
PackedByteArray extracted_data;
{
Ref<FileAccess> zip_access;
zlib_filefunc_def io = zipio_create_io(&zip_access);
unzFile uzf = unzOpen2(p_zip_path.utf8().get_data(), &io);
if (!uzf) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not find debug symbols to export: \"%s\"."), p_zip_path));
return ERR_FILE_NOT_FOUND;
}

int err = UNZ_OK;

// Locate and open the file.
err = godot_unzip_locate_file(uzf, p_symbols_path, true);
if (err != UNZ_OK) {
unzClose(uzf);
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not find debug symbols to export: \"%s\" \"%s\"."), p_symbols_path, p_zip_path));
return ERR_FILE_NOT_FOUND;
}

err = unzOpenCurrentFile(uzf);
if (err != UNZ_OK) {
unzClose(uzf);
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not open debug symbols to export: \"%s\" \"%s\"."), p_symbols_path, p_zip_path));
return ERR_FILE_CANT_OPEN;
}

// Read the file info.
unz_file_info info;
err = unzGetCurrentFileInfo(uzf, &info, nullptr, 0, nullptr, 0, nullptr, 0);
if (err != UNZ_OK) {
unzCloseCurrentFile(uzf);
unzClose(uzf);
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not open debug symbols to export: \"%s\" \"%s\"."), p_symbols_path, p_zip_path));
return ERR_FILE_CANT_OPEN;
}

// Read the file data.
extracted_data.resize(info.uncompressed_size);
uint8_t *buffer = extracted_data.ptrw();
int to_read = extracted_data.size();
while (to_read > 0) {
int bytes_read_current = unzReadCurrentFile(uzf, buffer, to_read);
if (bytes_read_current < 0 || (bytes_read_current == UNZ_EOF && to_read != 0)) {
unzCloseCurrentFile(uzf);
unzClose(uzf);
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not open debug symbols to export: \"%s\" \"%s\"."), p_symbols_path, p_zip_path));
return ERR_FILE_CANT_READ;
}
buffer += bytes_read_current;
to_read -= bytes_read_current;
}

// Verify the data and return.
err = unzCloseCurrentFile(uzf);
if (err != UNZ_OK) {
unzClose(uzf);
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Could not open debug symbols to export: \"%s\" \"%s\"."), p_symbols_path, p_zip_path));
return ERR_FILE_CANT_READ;
}
unzClose(uzf);
}

// Prepare target file.
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
if (f.is_null()) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Prepare Templates"), vformat(TTR("Failed to copy file \"%s\"."), p_path));
return ERR_CANT_CREATE;
}
f->store_buffer(extracted_data);
Comment on lines +158 to +227

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is largely taken from export_template_manager.cpp and can be improved, currently reads the entire file into memory and that's how things are done there too, could change it to read it piecewise to reduce memory overhead if desired


return OK;
}

bool EditorExportPlatformPC::_copy_debugsymbols(Ref<DirAccess> &p_da, const String &p_path, const String &p_symbols_path, Error &r_err) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be made into a virtual method but as there's no difference currently I've left it a local method, as it got a bit cluttered with the different options, some of these paths should probably be collapsed and cleaned up

r_err = OK;

if (FileAccess::exists(p_symbols_path)) {
r_err = p_da->copy(p_symbols_path, p_path + ".debugsymbols");
} else if (FileAccess::exists(p_symbols_path + ".zip")) {
r_err = _unzip_debugsymbols(p_da, p_path + ".debugsymbols", p_symbols_path.get_file(), p_symbols_path + ".zip");
} else {
return false;
}
if (r_err == OK) {
r_err = fixup_debug_symbol_link(p_path, p_path.get_file() + ".debugsymbols");
}

return r_err == OK;
}

Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
if (!DirAccess::exists(p_path.get_base_dir())) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("The given export path doesn't exist."));
Expand Down Expand Up @@ -182,9 +276,13 @@ Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_
"console.exe",
nullptr,
};

int con_wrapper_mode = p_preset->get("debug/export_console_wrapper");
bool copy_wrapper = (con_wrapper_mode == 1 && p_debug) || (con_wrapper_mode == 2);

int debug_symbols_mode = p_preset->get("debug/export_debug_symbols");
bool copy_debug_symbols = (debug_symbols_mode == 1 && p_debug) || (debug_symbols_mode == 2);

Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
da->make_dir_recursive(p_path.get_base_dir());
Error err = da->copy(template_path, p_path, get_chmod_flags());
Expand All @@ -197,6 +295,16 @@ Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_
}
}
}
if (err == OK && copy_debug_symbols) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section along with the wrapper is getting a bit cluttered but I think it'd be something to clean up in a follow-up PR, properly moving the console wrapper code into the windows platform code to make it easier to maintain

_copy_debugsymbols(da, p_path, template_path + ".debugsymbols", err);
if (err == OK && copy_wrapper) {
for (int i = 0; wrapper_extensions[i]; ++i) {
if (_copy_debugsymbols(da, p_path.get_basename() + ".console.exe", template_path.get_basename() + wrapper_extensions[i] + ".debugsymbols", err)) {
break;
}
}
}
}
Comment on lines +298 to +307

@Calinou Calinou Jul 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows export templates compiled with MSVC use a .pdb extension, so I would also check for that (after .debugsymbols).

This is not needed for official export templates as they use MinGW, but it can be needed for custom export templates.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that fetched automatically like the .debugsymbols file? Is it an alternative to the debug symbols or a replacement?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this rename check for the .debugsymbols file to ensure renames work, not sure if that works out of the box with the .pdb file, I haven't built with msvc lately so can't test currently but can set up and test

if (FileAccess::exists(exec_path + ".debugsymbols")) {
exec_path = exec_path + ".debugsymbols";
}

Would this need a different check for that case?

@Calinou Calinou Jul 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an alternative to the debug symbols or a replacement?

It's an alternative; you can't have both .debugsymbols and .pdb for a single build.

Note that MSVC debug symbols are always separate as .pdb, while MinGW debug symbols can be embedded into the .exe.

Would this need a different check for that case?

Yes, as this check is only intended to find separate MinGW debug symbols.

There is another issue we'll probably need to tackle with MSVC debug symbols: the executable finds the PDB location with a path defined in the .exe, which is written as absolute in Godot. This means that if you export the project to any location that isn't the same path as the location the project was built, the debug symbols won't be recognized automatically.

@bruvzg Any ideas?

@AThousandShips AThousandShips Jul 20, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I think we can leave that as a follow-up including code to integrate that into the system (official builds are made with gcc so will only be relevant for custom builds with msvc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Official templates are not using MSVC, so I think it's OK to skip it for now.

I think path can be set to relative using /PDBALTPATH:, but it still will need change when exporting.
See WindowsUtils::copy_and_rename_pdb which is doing the PDB renaming for GDExtension dlls, something similar probably can be done for the export.

if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("Failed to copy export template."));
return err;
Expand Down
6 changes: 6 additions & 0 deletions editor/export/editor_export_platform_pc.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class EditorExportPlatformPC : public EditorExportPlatform {

int chmod_flags = -1;

Error _unzip_debugsymbols(Ref<DirAccess> &p_da, const String &p_path, const String &p_symbols_path, const String &p_zip_path);
bool _copy_debugsymbols(Ref<DirAccess> &p_da, const String &p_path, const String &p_symbols_path, Error &r_err);

public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
virtual void get_export_options(List<ExportOption> *r_options) const override;
Expand Down Expand Up @@ -78,4 +81,7 @@ class EditorExportPlatformPC : public EditorExportPlatform {
virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
return Error::OK;
}
virtual Error fixup_debug_symbol_link(const String &p_path, const String &p_symbol_path) {
return Error::OK;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<member name="debug/export_console_wrapper" type="int" setter="" getter="">
If [code]true[/code], a console wrapper is exported alongside the main executable, which allows running the project with enabled console output.
</member>
<member name="debug/export_debug_symbols" type="int" setter="" getter="">
If [code]true[/code], a debug symbol file ([code].debugsymbols[/code]) is exported alongside the main executable.
[b]Note:[/b] This requires a separate debug symbols file. This file must have the same name as the export template binary and an additional [code].debugsymbols[/code] extension, and must be present in the same directory as the export template binary, or in a ZIP archive with a [code].debugsymbols.zip[/code] extension.
[b]Note:[/b] If debug symbols are embedded in the export template binary, they will remain embedded in the exported project binary regardless of this option, so it should be disabled in that case.
</member>
<member name="shader_baker/enabled" type="bool" setter="" getter="">
If [code]true[/code], shaders will be compiled and embedded in the application. This option is only supported when using the Forward+ or Mobile renderers.
[b]Note:[/b] When exporting as a dedicated server, the shader baker is always disabled since no rendering is performed.
Expand Down
171 changes: 171 additions & 0 deletions platform/linuxbsd/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,177 @@ String EditorExportPlatformLinuxBSD::_get_exe_arch(const String &p_path) const {
}
}

static inline size_t _padding(size_t p_s, size_t p_a) {
return (p_s % p_a == 0) ? 0 : (p_a - p_s % p_a);
}

Error EditorExportPlatformLinuxBSD::fixup_debug_symbol_link(const String &p_path, const String &p_symbol_file) {
// Patch the ".gnu_debuglink" section in the ELF file so that it corresponds to external debug symbols file.
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE);
if (f.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Symbols Link"), vformat(TTR("Failed to open executable file \"%s\"."), p_path));
return ERR_CANT_OPEN;
}

// New debug symbols link data.
CharString cs_link = p_symbol_file.utf8();
uint32_t new_link_size = cs_link.size() + _padding(cs_link.size(), 4) + 4;

// Read and check ELF magic number.
{
uint32_t magic = f->get_32();
if (magic != 0x464c457f) { // 0x7F + "ELF"
add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Symbols Link"), TTR("Executable file header corrupted."));
return ERR_FILE_CORRUPT;
}
}

// Read program architecture bits from class field.

int bits = f->get_8() * 32;

// Get info about the section header table.

int64_t section_table_pos;
int64_t section_header_size;
if (bits == 32) {
section_header_size = 40;
f->seek(0x20);
section_table_pos = f->get_32();
f->seek(0x30);
} else { // 64
section_header_size = 64;
f->seek(0x28);
section_table_pos = f->get_64();
f->seek(0x3c);
}
int num_sections = f->get_16();
int string_section_idx = f->get_16();

// Load the strings table.
uint8_t *strings;
{
// Jump to the strings section header.
f->seek(section_table_pos + string_section_idx * section_header_size);

// Read strings data size and offset.
int64_t string_data_pos;
int64_t string_data_size;
if (bits == 32) {
f->seek(f->get_position() + 0x10);
string_data_pos = f->get_32();
string_data_size = f->get_32();
} else { // 64
f->seek(f->get_position() + 0x18);
string_data_pos = f->get_64();
string_data_size = f->get_64();
}

// Read strings data.
f->seek(string_data_pos);
strings = (uint8_t *)memalloc(string_data_size);
if (!strings) {
return ERR_OUT_OF_MEMORY;
}
f->get_buffer(strings, string_data_size);
}

// Search for the ".gnu_debuglink" section.

uint32_t link_crc32 = 0;
bool found = false;
for (int i = 0; i < num_sections; ++i) {
int64_t section_header_pos = section_table_pos + i * section_header_size;
f->seek(section_header_pos);

uint32_t name_offset = f->get_32();
if (strcmp((char *)strings + name_offset, ".gnu_debuglink") == 0) {
// ".gnu_debuglink" section found.

int64_t link_start = 0;
int64_t link_size = 0;
if (bits == 32) {
f->seek(section_header_pos + 0x10);
link_start = f->get_32();
link_size = f->get_32();
} else { // 64
f->seek(section_header_pos + 0x18);
link_start = f->get_64();
link_size = f->get_64();
}

f->seek(link_start);

// Read CRC.
uint32_t link_off = 0;
while (link_off < link_size) {
uint8_t c = f->get_8();
link_off++;
if (c == 0x00) {
uint32_t pad = _padding(link_off, 4);
f->seek(f->get_position() + pad);
link_crc32 = f->get_32();
break;
}
}

if (link_size >= new_link_size) {
// Update existing section in place.
f->seek(link_start);
f->store_buffer((const uint8_t *)cs_link.get_data(), cs_link.size());
for (uint32_t j = 0; j < _padding(cs_link.size(), 4); j++) {
f->store_8(0x00);
}
f->store_32(link_crc32);

// Update section data.
if (bits == 32) {
f->seek(section_header_pos + 0x14);
f->store_32(new_link_size);
} else { // 64
f->seek(section_header_pos + 0x20);
f->store_64(new_link_size);
}
} else {
// Zero old section data.
f->seek(link_start);
for (uint32_t j = 0; j < link_size; j++) {
f->store_8(0x00);
}
// Append new data.
f->seek_end();
uint64_t link_pos = f->get_position();
f->store_buffer((const uint8_t *)cs_link.get_data(), cs_link.size());
for (uint32_t j = 0; j < _padding(cs_link.size(), 4); j++) {
f->store_8(0x00);
}
f->store_32(link_crc32);
// Update section data.
if (bits == 32) {
f->seek(section_header_pos + 0x10);
f->store_32(link_pos);
f->store_32(new_link_size);
} else { // 64
f->seek(section_header_pos + 0x18);
f->store_64(link_pos);
f->store_64(new_link_size);
}
}

found = true;
break;
}
}

memfree(strings);

if (!found) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Debug Symbols Link"), TTR("Executable \".gnu_debuglink\" section not found."));
return ERR_FILE_CORRUPT;
}
return OK;
}

Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
// Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data.

Expand Down
1 change: 1 addition & 0 deletions platform/linuxbsd/export/export_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC {
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0, bool p_notify = true) override;
virtual String get_template_file_name(const String &p_target, const String &p_arch) const override;
virtual Error fixup_debug_symbol_link(const String &p_path, const String &p_symbol_file) override;
virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override;
virtual bool is_executable(const String &p_path) const override;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
<member name="debug/export_console_wrapper" type="int" setter="" getter="">
If [code]true[/code], a console wrapper executable is exported alongside the main executable, which allows running the project with enabled console output.
</member>
<member name="debug/export_debug_symbols" type="int" setter="" getter="">
If [code]true[/code], a debug symbol file ([code].debugsymbols[/code]) is exported alongside the main executable. This also applies to the console wrapper if enabled.
[b]Note:[/b] This requires a separate debug symbols file. This file must have the same name as the export template binary and an additional [code].debugsymbols[/code] extension, and must be present in the same directory as the export template binary, or in a ZIP archive with a [code].debugsymbols.zip[/code] extension. A separate file is required for the console wrapper.
[b]Note:[/b] If debug symbols are embedded in the export template binary, they will remain embedded in the exported project binary regardless of this option, so it should be disabled in that case.
</member>
<member name="shader_baker/enabled" type="bool" setter="" getter="">
If [code]true[/code], shaders will be compiled and embedded in the application. This option is only supported when using the Forward+ and Mobile renderers.
[b]Note:[/b] When exporting as a dedicated server, the shader baker is always disabled since no rendering is performed.
Expand Down
Loading