b815c203da
Engine improvements/fixes Fixed behavior that made the editor automated test to be sometimes stuck if lost the focus is lost. Added support for specifying multiple tests to in batch to the editor, this is achieved by passing --runpythontest with the tests separated by ';' Added new cmdline argument --project-user-path for overriding the user path. This allows to have multiple editors running writing logs and crash logs in different locations. Moved responsability of exiting after a test finishes/passes out of ExecuteByFilenameAsTest, callers will use the bool return to know if the test passed. Editor test batch and parallelization implementation: Now the external python portion of the editor tests will be specified via test specs which will generate the test. Requiring no code. This is almost a data-driven approach. Tests can be specified as single tests, parallel, batchable or batchable+parallel Command line arguments for pytest to override the maximum number of editors, disable parallelization or batching. Automated tests for testing this new editor testing utility Signed-off-by: Garcia Ruiz <aljanru@amazon.co.uk> Co-authored-by: Garcia Ruiz <aljanru@amazon.co.uk>
762 lines
30 KiB
C++
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
|