123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761 |
- /*
- * 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
|