You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp

762 lines
30 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <PythonSystemComponent.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
#include <osdefs.h> // for DELIM
#include <AzCore/Component/EntityId.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/Module/Module.h>
#include <AzCore/Module/ModuleManagerBus.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzCore/Utils/Utils.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzFramework/Asset/AssetSystemComponent.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/CommandLine/CommandRegistrationBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
#include <AzToolsFramework/API/EditorPythonScriptNotificationsBus.h>
namespace Platform
{
// Implemented in each different platform's implentation files, as it differs per platform.
bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot);
AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot);
}
// this is called the first time a Python script contains "import azlmbr"
PYBIND11_EMBEDDED_MODULE(azlmbr, m)
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindings::EditorPythonBindingsNotificationBus::Events::OnImportModule, m.ptr());
}
namespace RedirectOutput
{
using RedirectOutputFunc = AZStd::function<void(const char*)>;
struct RedirectOutput
{
PyObject_HEAD
RedirectOutputFunc write;
};
PyObject* RedirectWrite(PyObject* self, PyObject* args)
{
std::size_t written(0);
RedirectOutput* selfimpl = reinterpret_cast<RedirectOutput*>(self);
if (selfimpl->write)
{
char* data;
if (!PyArg_ParseTuple(args, "s", &data))
{
return PyLong_FromSize_t(0);
}
selfimpl->write(data);
written = strlen(data);
}
return PyLong_FromSize_t(written);
}
PyObject* RedirectFlush([[maybe_unused]] PyObject* self, [[maybe_unused]] PyObject* args)
{
// no-op
return Py_BuildValue("");
}
PyMethodDef RedirectMethods[] =
{
{"write", RedirectWrite, METH_VARARGS, "sys.stdout.write"},
{"flush", RedirectFlush, METH_VARARGS, "sys.stdout.flush"},
{"write", RedirectWrite, METH_VARARGS, "sys.stderr.write"},
{"flush", RedirectFlush, METH_VARARGS, "sys.stderr.flush"},
{0, 0, 0, 0} // sentinel
};
PyTypeObject RedirectOutputType =
{
PyVarObject_HEAD_INIT(0, 0)
"azlmbr_redirect.RedirectOutputType", // tp_name
sizeof(RedirectOutput), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"azlmbr_redirect objects", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
RedirectMethods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
0 /* tp_new */
};
PyModuleDef RedirectOutputModule = { PyModuleDef_HEAD_INIT, "azlmbr_redirect", 0, -1, 0, };
// Internal state
PyObject* g_redirect_stdout = nullptr;
PyObject* g_redirect_stdout_saved = nullptr;
PyObject* g_redirect_stderr = nullptr;
PyObject* g_redirect_stderr_saved = nullptr;
PyMODINIT_FUNC PyInit_RedirectOutput(void)
{
g_redirect_stdout = nullptr;
g_redirect_stdout_saved = nullptr;
g_redirect_stderr = nullptr;
g_redirect_stderr_saved = nullptr;
RedirectOutputType.tp_new = PyType_GenericNew;
if (PyType_Ready(&RedirectOutputType) < 0)
{
return 0;
}
PyObject* m = PyModule_Create(&RedirectOutputModule);
if (m)
{
Py_INCREF(&RedirectOutputType);
PyModule_AddObject(m, "Redirect", reinterpret_cast<PyObject*>(&RedirectOutputType));
}
return m;
}
void SetRedirection(const char* funcname, PyObject*& saved, PyObject*& current, RedirectOutputFunc func)
{
if (PyType_Ready(&RedirectOutputType) < 0)
{
AZ_Warning("python", false, "RedirectOutputType not ready!");
return;
}
if (!current)
{
saved = PySys_GetObject(funcname); // borrowed
current = RedirectOutputType.tp_new(&RedirectOutputType, 0, 0);
}
RedirectOutput* redirectOutput = reinterpret_cast<RedirectOutput*>(current);
redirectOutput->write = func;
PySys_SetObject(funcname, current);
}
void ResetRedirection(const char* funcname, PyObject*& saved, PyObject*& current)
{
if (current)
{
PySys_SetObject(funcname, saved);
}
Py_XDECREF(current);
current = nullptr;
}
PyObject* s_RedirectModule = nullptr;
void Intialize(PyObject* module)
{
using namespace AzToolsFramework;
s_RedirectModule = module;
SetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout, [](const char* msg)
{
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnTraceMessage, msg);
});
SetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr, [](const char* msg)
{
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnErrorMessage, msg);
});
PySys_WriteStdout("RedirectOutput installed");
}
void Shutdown()
{
ResetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout);
ResetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr);
Py_XDECREF(s_RedirectModule);
s_RedirectModule = nullptr;
}
} // namespace RedirectOutput
namespace EditorPythonBindings
{
void PythonSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PythonSystemComponent, AZ::Component>()
->Version(1)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>{AZ_CRC_CE("AssetBuilder")})
;
if (AZ::EditContext* ec = serialize->GetEditContext())
{
ec->Class<PythonSystemComponent>("PythonSystemComponent", "The Python interpreter")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
;
}
}
}
void PythonSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(PythonEmbeddedService);
}
void PythonSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(PythonEmbeddedService);
}
void PythonSystemComponent::Activate()
{
AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Register(this);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusConnect();
}
void PythonSystemComponent::Deactivate()
{
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Unregister(this);
StopPython(true);
}
bool PythonSystemComponent::StartPython([[maybe_unused]] bool silenceWarnings)
{
struct ReleaseInitalizeWaiterScope final
{
using ReleaseFunction = AZStd::function<void(void)>;
ReleaseInitalizeWaiterScope(ReleaseFunction releaseFunction)
{
m_releaseFunction = AZStd::move(releaseFunction);
}
~ReleaseInitalizeWaiterScope()
{
m_releaseFunction();
}
ReleaseFunction m_releaseFunction;
};
ReleaseInitalizeWaiterScope scope([this]()
{
m_initalizeWaiter.release(m_initalizeWaiterCount);
m_initalizeWaiterCount = 0;
});
if (Py_IsInitialized())
{
AZ_Warning("python", silenceWarnings, "Python is already active!");
return false;
}
PythonPathStack pythonPathStack;
DiscoverPythonPaths(pythonPathStack);
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreInitialize);
if (StartPythonInterpreter(pythonPathStack))
{
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostInitialize);
// initialize internal base module and bootstrap scripts
ExecuteByString("import azlmbr", false);
ExecuteBootstrapScripts(pythonPathStack);
return true;
}
return false;
}
bool PythonSystemComponent::StopPython([[maybe_unused]] bool silenceWarnings)
{
if (!Py_IsInitialized())
{
AZ_Warning("python", silenceWarnings, "Python is not active!");
return false;
}
bool result = false;
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreFinalize);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
result = StopPythonInterpreter();
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostFinalize);
return result;
}
bool PythonSystemComponent::IsPythonActive()
{
return Py_IsInitialized() != 0;
}
void PythonSystemComponent::WaitForInitialization()
{
m_initalizeWaiterCount++;
m_initalizeWaiter.acquire();
}
void PythonSystemComponent::ExecuteWithLock(AZStd::function<void()> executionCallback)
{
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_release release;
pybind11::gil_scoped_acquire acquire;
executionCallback();
}
void PythonSystemComponent::DiscoverPythonPaths(PythonPathStack& pythonPathStack)
{
// the order of the Python paths is the order the Python bootstrap scripts will execute
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (!settingsRegistry)
{
return;
}
AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
if (projectPath.empty())
{
return;
}
auto resolveScriptPath = [&pythonPathStack](AZStd::string_view path)
{
auto editorScriptsPath = AZ::IO::Path(path) / "Editor" / "Scripts";
if (AZ::IO::SystemFile::Exists(editorScriptsPath.c_str()))
{
pythonPathStack.emplace_back(AZStd::move(editorScriptsPath.LexicallyNormal().Native()));
}
};
// The discovery order will be:
// 1 - engine-root/EngineAsets
// 2 - gems
// 3 - project
// 4 - user(dev)
// 1 - engine
AZ::IO::FixedMaxPath engineRoot;
if (settingsRegistry->Get(engineRoot.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder); !engineRoot.empty())
{
resolveScriptPath((engineRoot / "Assets").Native());
}
// 2 - gems
struct GetGemSourcePathsVisitor
: AZ::SettingsRegistryInterface::Visitor
{
GetGemSourcePathsVisitor(AZ::SettingsRegistryInterface& settingsRegistry)
: m_settingsRegistry(settingsRegistry)
{}
void Visit(AZStd::string_view path, AZStd::string_view, AZ::SettingsRegistryInterface::Type,
AZStd::string_view value) override
{
AZStd::string_view jsonSourcePathPointer{ path };
// Remove the array index from the path and check if the JSON path ends with "/SourcePaths"
AZ::StringFunc::TokenizeLast(jsonSourcePathPointer, "/");
if (jsonSourcePathPointer.ends_with("/SourcePaths"))
{
AZ::IO::Path newSourcePath = jsonSourcePathPointer;
// Resolve any file aliases first - Do not use ResolvePath() as that assumes
// any relative path is underneath the @assets@ alias
if (auto fileIoBase = AZ::IO::FileIOBase::GetInstance(); fileIoBase != nullptr)
{
AZ::IO::FixedMaxPath replacedAliasPath;
if (fileIoBase->ReplaceAlias(replacedAliasPath, value))
{
newSourcePath = AZ::IO::PathView(replacedAliasPath);
}
}
// The current assumption is that the gem source path is the relative to the engine root
AZ::IO::Path engineRootPath;
m_settingsRegistry.Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
newSourcePath = (engineRootPath / newSourcePath).LexicallyNormal();
if (auto gemSourcePathIter = AZStd::find(m_gemSourcePaths.begin(), m_gemSourcePaths.end(), newSourcePath);
gemSourcePathIter == m_gemSourcePaths.end())
{
m_gemSourcePaths.emplace_back(AZStd::move(newSourcePath));
}
}
}
AZStd::vector<AZ::IO::Path> m_gemSourcePaths;
private:
AZ::SettingsRegistryInterface& m_settingsRegistry;
};
GetGemSourcePathsVisitor visitor{ *settingsRegistry };
constexpr auto gemListKey = AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::OrganizationRootKey)
+ "/Gems";
settingsRegistry->Visit(visitor, gemListKey);
for (const AZ::IO::Path& gemSourcePath : visitor.m_gemSourcePaths)
{
resolveScriptPath(gemSourcePath.Native());
}
// 3 - project
resolveScriptPath(AZStd::string_view{ projectPath });
// 4 - user
AZStd::string assetsType;
AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, assetsType,
AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, AzFramework::AssetSystem::Assets);
if (!assetsType.empty())
{
AZ::IO::FixedMaxPath userCachePath;
if (settingsRegistry->Get(userCachePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
!userCachePath.empty())
{
userCachePath /= "user";
resolveScriptPath(userCachePath.Native());
}
}
}
void PythonSystemComponent::ExecuteBootstrapScripts(const PythonPathStack& pythonPathStack)
{
for(const auto& path : pythonPathStack)
{
AZStd::string bootstrapPath;
AzFramework::StringFunc::Path::Join(path.c_str(), "bootstrap.py", bootstrapPath);
if (AZ::IO::SystemFile::Exists(bootstrapPath.c_str()))
{
ExecuteByFilename(bootstrapPath);
}
}
}
bool PythonSystemComponent::StartPythonInterpreter(const PythonPathStack& pythonPathStack)
{
AZStd::unordered_set<AZStd::string> pyPackageSites(pythonPathStack.begin(), pythonPathStack.end());
const char* engineRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
// set PYTHON_HOME
AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, engineRoot);
if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
{
AZ_Warning("python", false, "Python home path must exist! path:%s", pyBasePath.c_str());
return false;
}
AZStd::wstring pyHomePath;
AZStd::to_wstring(pyHomePath, pyBasePath);
Py_SetPythonHome(pyHomePath.c_str());
// display basic Python information
AZ_TracePrintf("python", "Py_GetVersion=%s \n", Py_GetVersion());
AZ_TracePrintf("python", "Py_GetPath=%ls \n", Py_GetPath());
AZ_TracePrintf("python", "Py_GetExecPrefix=%ls \n", Py_GetExecPrefix());
AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
try
{
// ignore system location for sites site-packages
Py_IsolatedFlag = 1; // -I - Also sets Py_NoUserSiteDirectory. If removed PyNoUserSiteDirectory should be set.
Py_IgnoreEnvironmentFlag = 1; // -E
Py_InspectFlag = 1; // unhandled SystemExit will terminate the process unless Py_InspectFlag is set
const bool initializeSignalHandlers = true;
pybind11::initialize_interpreter(initializeSignalHandlers);
// Add custom site packages after initializing the interpreter above. Calling Py_SetPath before initialization
// alters the behavior of the initializer to not compute default search paths. See https://docs.python.org/3/c-api/init.html#c.Py_SetPath
if (pyPackageSites.size())
{
ExtendSysPath(pyPackageSites);
}
RedirectOutput::Intialize(PyImport_ImportModule("azlmbr_redirect"));
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// print Python version using AZ logging
const int verRet = PyRun_SimpleStringFlags("import sys \nprint (sys.version) \n", nullptr);
AZ_Error("python", verRet == 0, "Error trying to fetch the version number in Python!");
return verRet == 0 && !PyErr_Occurred();
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("python", false, "Py_Initialize() failed with %s!", e.what());
return false;
}
}
bool PythonSystemComponent::ExtendSysPath(const AZStd::unordered_set<AZStd::string>& extendPaths)
{
AZStd::unordered_set<AZStd::string> oldPathSet;
auto SplitPath = [&oldPathSet](AZStd::string_view pathPart)
{
oldPathSet.emplace(pathPart);
};
AZ::StringFunc::TokenizeVisitor(Py_EncodeLocale(Py_GetPath(), nullptr), SplitPath, DELIM);
bool appended{ false };
AZStd::string pathAppend{ "import sys\n" };
for (const auto& thisStr : extendPaths)
{
if (!oldPathSet.contains(thisStr))
{
pathAppend.append(AZStd::string::format("sys.path.append(r'%s')\n", thisStr.c_str()));
appended = true;
}
}
if (appended)
{
ExecuteByString(pathAppend.c_str(), false);
return true;
}
return false;
}
bool PythonSystemComponent::StopPythonInterpreter()
{
if (Py_IsInitialized())
{
RedirectOutput::Shutdown();
pybind11::finalize_interpreter();
}
else
{
AZ_Warning("python", false, "Did not finalize since Py_IsInitialized() was false.");
}
return !PyErr_Occurred();
}
void PythonSystemComponent::ExecuteByString(AZStd::string_view script, bool printResult)
{
if (!Py_IsInitialized())
{
AZ_Error("python", false, "Can not ExecuteByString() since the embeded Python VM is not ready.");
return;
}
if (!script.empty())
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByString, script);
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// Acquire scope for __main__ for executing our script
pybind11::object scope = pybind11::module::import("__main__").attr("__dict__");
bool shouldPrintValue = false;
if (printResult)
{
// Attempt to compile our code to determine if it's an expression
// i.e. a Python code object with only an rvalue
// If it is, it can be evaled to produce a PyObject
// If it's not, we can't evaluate it into a result and should fall back to exec
shouldPrintValue = true;
using namespace pybind11::literals;
// codeop.compile_command is a thin wrapper around the Python compile builtin
// We attempt to compile using symbol="eval" to see if the string is valid for eval
// This is similar to what the Python REPL does internally
pybind11::object codeop = pybind11::module::import("codeop");
pybind11::object compileCommand = codeop.attr("compile_command");
try
{
compileCommand(script.data(), "symbol"_a="eval");
}
catch (const pybind11::error_already_set&)
{
shouldPrintValue = false;
}
}
try
{
if (shouldPrintValue)
{
// We're an expression, run and print the result
pybind11::object result = pybind11::eval(script.data(), scope);
pybind11::print(result);
}
else
{
// Just exec the code block
pybind11::exec(script.data(), scope);
}
}
catch (pybind11::error_already_set& pythonError)
{
// Release the exception stack and let Python print it to stderr
pythonError.restore();
PyErr_Print();
}
}
}
void PythonSystemComponent::ExecuteByFilename(AZStd::string_view filename)
{
AZStd::vector<AZStd::string_view> args;
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilename, filename);
ExecuteByFilenameWithArgs(filename, args);
}
bool PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector<AZStd::string_view>& args)
{
AZ_TracePrintf("python", "Running automated test: %.*s (testcase %.*s)", AZ_STRING_ARG(filename), AZ_STRING_ARG(testCase))
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameAsTest, filename, testCase, args);
const Result evalResult = EvaluateFile(filename, args);
return evalResult == Result::Okay;
}
void PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameWithArgs, filename, args);
EvaluateFile(filename, args);
}
PythonSystemComponent::Result PythonSystemComponent::EvaluateFile(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
{
if (!Py_IsInitialized())
{
AZ_Error("python", false, "Can not evaluate file since the embedded Python VM is not ready.");
return Result::Error_IsNotInitialized;
}
if (filename.empty())
{
AZ_Error("python", false, "Invalid empty filename detected.");
return Result::Error_InvalidFilename;
}
// support the alias version of a script such as @devroot@/Editor/Scripts/select_story_anim_objects.py
AZStd::string theFilename(filename);
{
char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(theFilename.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
theFilename = resolvedPath;
}
if (!AZ::IO::FileIOBase::GetInstance()->Exists(theFilename.c_str()))
{
AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
return Result::Error_MissingFile;
}
FILE* file = _Py_fopen(theFilename.data(), "rb");
if (!file)
{
AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
return Result::Error_FileOpenValidation;
}
Result pythonScriptResult = Result::Okay;
try
{
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// Create standard "argc" / "argv" command-line parameters to pass in to the Python script via sys.argv.
// argc = number of parameters. This will always be at least 1, since the first parameter is the script name.
// argv = the list of parameters, in wchar format.
// Our expectation is that the args passed into this function does *not* already contain the script name.
int argc = aznumeric_cast<int>(args.size()) + 1;
// Note: This allocates from PyMem to ensure that Python has access to the memory.
wchar_t** argv = static_cast<wchar_t**>(PyMem_Malloc(argc * sizeof(wchar_t*)));
// Python 3.x is expecting wchar* strings for the command-line args.
argv[0] = Py_DecodeLocale(theFilename.c_str(), nullptr);
for (int arg = 0; arg < args.size(); arg++)
{
AZStd::string argString(args[arg]);
argv[arg + 1] = Py_DecodeLocale(argString.c_str(), nullptr);
}
// Tell Python the command-line args.
// Note that this has a side effect of adding the script's path to the set of directories checked for "import" commands.
const int updatePath = 1;
PySys_SetArgvEx(argc, argv, updatePath);
PyCompilerFlags flags;
flags.cf_flags = 0;
const int bAutoCloseFile = true;
const int returnCode = PyRun_SimpleFileExFlags(file, theFilename.c_str(), bAutoCloseFile, &flags);
if (returnCode != 0)
{
AZStd::string message = AZStd::string::format("Detected script failure in Python script(%s); return code %d!", theFilename.c_str(), returnCode);
AZ_Warning("python", false, message.c_str());
using namespace AzToolsFramework;
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnExceptionMessage, message.c_str());
pythonScriptResult = Result::Error_PythonException;
}
// Free any memory allocated for the command-line args.
for (int arg = 0; arg < argc; arg++)
{
PyMem_RawFree(argv[arg]);
}
PyMem_Free(argv);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("python", false, "Detected an internal exception %s while running script (%s)!", e.what(), theFilename.c_str());
return Result::Error_InternalException;
}
return pythonScriptResult;
}
} // namespace EditorPythonBindings