#include "python_stdlib.h" #include #include #include #include #include #include #ifdef MTGODOT_HAVE_PYTHON #include "platform/ScriptLib/PythonHost.h" #endif using namespace godot; namespace mtgodot { namespace { // Both ship in the PCK; the build copies them next to project.godot (extension/CMakeLists.txt). constexpr const char *ZIP_RES = "res://python27.zip"; constexpr const char *SHA_RES = "res://python27.zip.sha256"; constexpr const char *ZIP_USER = "user://python27.zip"; constexpr const char *ZIP_USER_TMP = "user://python27.zip.part"; // std::string, not String: this lives at file scope and a godot::String's constructor would run at // dlopen time, before the extension's bindings exist. std::string g_last_error; String fail(String *error, const String &text) { g_last_error = text.utf8().get_data(); if (error) { *error = text; } return String(); } String expected_sha() { if (!FileAccess::file_exists(SHA_RES)) { return String(); } return FileAccess::get_file_as_string(SHA_RES).strip_edges(); } } // namespace String stage_python_stdlib(String *error, bool force_stage) { g_last_error.clear(); if (error) { *error = String(); } if (!FileAccess::file_exists(ZIP_RES)) { return fail(error, String("no ") + ZIP_RES + " (build with -DMTGODOT_EMBED_PYTHON=ON, which copies it into project/)"); } ProjectSettings *settings = ProjectSettings::get_singleton(); // Desktop and the editor: res:// is a directory on disk, so CPython can open the file in place and // nothing is copied. In an exported game globalize_path() returns a path next to the executable // that does not exist, which is exactly the case the staging below is for. const String in_place = settings->globalize_path(ZIP_RES); if (!force_stage && !in_place.begins_with("res://") && FileAccess::file_exists(in_place)) { return in_place; } const String want = expected_sha(); if (want.is_empty()) { return fail(error, String("no ") + SHA_RES + " beside the stdlib zip"); } // An already-staged copy is reused only when it is the zip this build ships: a new version of the // app brings a new digest, and a first start that was killed mid-copy leaves a short file. if (FileAccess::file_exists(ZIP_USER) && FileAccess::get_sha256(ZIP_USER) == want) { return settings->globalize_path(ZIP_USER); } const PackedByteArray data = FileAccess::get_file_as_bytes(ZIP_RES); if (data.is_empty()) { return fail(error, String("cannot read ") + ZIP_RES); } { Ref out = FileAccess::open(ZIP_USER_TMP, FileAccess::WRITE); if (out.is_null()) { return fail(error, String("cannot write ") + ZIP_USER_TMP + ": " + itos((int) FileAccess::get_open_error())); } out->store_buffer(data); out->close(); } // Hash the file as written, not the buffer: a full disk reports itself here and not later as a // zipimport failure three imports into system.py. if (FileAccess::get_sha256(ZIP_USER_TMP) != want) { DirAccess::remove_absolute(ZIP_USER_TMP); return fail(error, String("staged ") + ZIP_USER + " does not match " + want); } if (FileAccess::file_exists(ZIP_USER)) { DirAccess::remove_absolute(ZIP_USER); } if (DirAccess::rename_absolute(ZIP_USER_TMP, ZIP_USER) != OK) { return fail(error, String("cannot rename ") + ZIP_USER_TMP + " to " + ZIP_USER); } UtilityFunctions::print("[python27] staged ", ZIP_USER, " (", data.size(), " bytes, sha256 ", want, ")"); return settings->globalize_path(ZIP_USER); } String Metin2Python::stdlib_path(bool force_stage) { const String path = stage_python_stdlib(nullptr, force_stage); #ifdef MTGODOT_HAVE_PYTHON if (!path.is_empty()) { PythonHost::SetDefaultStdLibPath(path.utf8().get_data()); } #endif return path; } String Metin2Python::last_error() { return String::utf8(g_last_error.c_str()); } void Metin2Python::_bind_methods() { ClassDB::bind_static_method("Metin2Python", D_METHOD("stdlib_path", "force_stage"), &Metin2Python::stdlib_path, DEFVAL(false)); ClassDB::bind_static_method("Metin2Python", D_METHOD("last_error"), &Metin2Python::last_error); } } // namespace mtgodot