PythonSystemComponent.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <PythonSystemComponent.h>
  9. #include <EditorPythonBindings/EditorPythonBindingsBus.h>
  10. #include <Source/PythonCommon.h>
  11. #include <pybind11/pybind11.h>
  12. #include <pybind11/embed.h>
  13. #include <pybind11/eval.h>
  14. #include <osdefs.h> // for DELIM
  15. #include <AzCore/Component/EntityId.h>
  16. #include <AzCore/IO/SystemFile.h>
  17. #include <AzCore/Module/DynamicModuleHandle.h>
  18. #include <AzCore/Module/Module.h>
  19. #include <AzCore/Module/ModuleManagerBus.h>
  20. #include <AzCore/PlatformDef.h>
  21. #include <AzCore/Serialization/EditContext.h>
  22. #include <AzCore/Serialization/SerializeContext.h>
  23. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  24. #include <AzCore/std/string/conversions.h>
  25. #include <AzCore/StringFunc/StringFunc.h>
  26. #include <AzCore/Utils/Utils.h>
  27. #include <AzFramework/API/ApplicationAPI.h>
  28. #include <AzFramework/Asset/AssetSystemComponent.h>
  29. #include <AzFramework/IO/LocalFileIO.h>
  30. #include <AzFramework/CommandLine/CommandRegistrationBus.h>
  31. #include <AzFramework/StringFunc/StringFunc.h>
  32. #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
  33. #include <AzToolsFramework/API/EditorPythonScriptNotificationsBus.h>
  34. namespace Platform
  35. {
  36. // Implemented in each different platform's implentation files, as it differs per platform.
  37. bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot);
  38. AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot);
  39. }
  40. // this is called the first time a Python script contains "import azlmbr"
  41. PYBIND11_EMBEDDED_MODULE(azlmbr, m)
  42. {
  43. EditorPythonBindings::EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindings::EditorPythonBindingsNotificationBus::Events::OnImportModule, m.ptr());
  44. }
  45. namespace RedirectOutput
  46. {
  47. using RedirectOutputFunc = AZStd::function<void(const char*)>;
  48. struct RedirectOutput
  49. {
  50. PyObject_HEAD
  51. RedirectOutputFunc write;
  52. };
  53. PyObject* RedirectWrite(PyObject* self, PyObject* args)
  54. {
  55. std::size_t written(0);
  56. RedirectOutput* selfimpl = reinterpret_cast<RedirectOutput*>(self);
  57. if (selfimpl->write)
  58. {
  59. char* data;
  60. if (!PyArg_ParseTuple(args, "s", &data))
  61. {
  62. return PyLong_FromSize_t(0);
  63. }
  64. selfimpl->write(data);
  65. written = strlen(data);
  66. }
  67. return PyLong_FromSize_t(written);
  68. }
  69. PyObject* RedirectFlush([[maybe_unused]] PyObject* self, [[maybe_unused]] PyObject* args)
  70. {
  71. // no-op
  72. return Py_BuildValue("");
  73. }
  74. PyMethodDef RedirectMethods[] =
  75. {
  76. {"write", RedirectWrite, METH_VARARGS, "sys.stdout.write"},
  77. {"flush", RedirectFlush, METH_VARARGS, "sys.stdout.flush"},
  78. {"write", RedirectWrite, METH_VARARGS, "sys.stderr.write"},
  79. {"flush", RedirectFlush, METH_VARARGS, "sys.stderr.flush"},
  80. {0, 0, 0, 0} // sentinel
  81. };
  82. PyTypeObject RedirectOutputType =
  83. {
  84. PyVarObject_HEAD_INIT(0, 0)
  85. "azlmbr_redirect.RedirectOutputType", // tp_name
  86. sizeof(RedirectOutput), /* tp_basicsize */
  87. 0, /* tp_itemsize */
  88. 0, /* tp_dealloc */
  89. 0, /* tp_print */
  90. 0, /* tp_getattr */
  91. 0, /* tp_setattr */
  92. 0, /* tp_reserved */
  93. 0, /* tp_repr */
  94. 0, /* tp_as_number */
  95. 0, /* tp_as_sequence */
  96. 0, /* tp_as_mapping */
  97. 0, /* tp_hash */
  98. 0, /* tp_call */
  99. 0, /* tp_str */
  100. 0, /* tp_getattro */
  101. 0, /* tp_setattro */
  102. 0, /* tp_as_buffer */
  103. Py_TPFLAGS_DEFAULT, /* tp_flags */
  104. "azlmbr_redirect objects", /* tp_doc */
  105. 0, /* tp_traverse */
  106. 0, /* tp_clear */
  107. 0, /* tp_richcompare */
  108. 0, /* tp_weaklistoffset */
  109. 0, /* tp_iter */
  110. 0, /* tp_iternext */
  111. RedirectMethods, /* tp_methods */
  112. 0, /* tp_members */
  113. 0, /* tp_getset */
  114. 0, /* tp_base */
  115. 0, /* tp_dict */
  116. 0, /* tp_descr_get */
  117. 0, /* tp_descr_set */
  118. 0, /* tp_dictoffset */
  119. 0, /* tp_init */
  120. 0, /* tp_alloc */
  121. 0 /* tp_new */
  122. };
  123. PyModuleDef RedirectOutputModule = { PyModuleDef_HEAD_INIT, "azlmbr_redirect", 0, -1, 0, };
  124. // Internal state
  125. PyObject* g_redirect_stdout = nullptr;
  126. PyObject* g_redirect_stdout_saved = nullptr;
  127. PyObject* g_redirect_stderr = nullptr;
  128. PyObject* g_redirect_stderr_saved = nullptr;
  129. PyMODINIT_FUNC PyInit_RedirectOutput(void)
  130. {
  131. g_redirect_stdout = nullptr;
  132. g_redirect_stdout_saved = nullptr;
  133. g_redirect_stderr = nullptr;
  134. g_redirect_stderr_saved = nullptr;
  135. RedirectOutputType.tp_new = PyType_GenericNew;
  136. if (PyType_Ready(&RedirectOutputType) < 0)
  137. {
  138. return 0;
  139. }
  140. PyObject* m = PyModule_Create(&RedirectOutputModule);
  141. if (m)
  142. {
  143. Py_INCREF(&RedirectOutputType);
  144. PyModule_AddObject(m, "Redirect", reinterpret_cast<PyObject*>(&RedirectOutputType));
  145. }
  146. return m;
  147. }
  148. void SetRedirection(const char* funcname, PyObject*& saved, PyObject*& current, RedirectOutputFunc func)
  149. {
  150. if (PyType_Ready(&RedirectOutputType) < 0)
  151. {
  152. AZ_Warning("python", false, "RedirectOutputType not ready!");
  153. return;
  154. }
  155. if (!current)
  156. {
  157. saved = PySys_GetObject(funcname); // borrowed
  158. current = RedirectOutputType.tp_new(&RedirectOutputType, 0, 0);
  159. }
  160. RedirectOutput* redirectOutput = reinterpret_cast<RedirectOutput*>(current);
  161. redirectOutput->write = func;
  162. PySys_SetObject(funcname, current);
  163. }
  164. void ResetRedirection(const char* funcname, PyObject*& saved, PyObject*& current)
  165. {
  166. if (current)
  167. {
  168. PySys_SetObject(funcname, saved);
  169. }
  170. Py_XDECREF(current);
  171. current = nullptr;
  172. }
  173. PyObject* s_RedirectModule = nullptr;
  174. void Intialize(PyObject* module)
  175. {
  176. using namespace AzToolsFramework;
  177. s_RedirectModule = module;
  178. SetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout, [](const char* msg)
  179. {
  180. EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnTraceMessage, msg);
  181. });
  182. SetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr, [](const char* msg)
  183. {
  184. EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnErrorMessage, msg);
  185. });
  186. PySys_WriteStdout("RedirectOutput installed");
  187. }
  188. void Shutdown()
  189. {
  190. ResetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout);
  191. ResetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr);
  192. Py_XDECREF(s_RedirectModule);
  193. s_RedirectModule = nullptr;
  194. }
  195. } // namespace RedirectOutput
  196. namespace EditorPythonBindings
  197. {
  198. void PythonSystemComponent::Reflect(AZ::ReflectContext* context)
  199. {
  200. if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
  201. {
  202. serialize->Class<PythonSystemComponent, AZ::Component>()
  203. ->Version(1)
  204. ->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>{AZ_CRC_CE("AssetBuilder")})
  205. ;
  206. if (AZ::EditContext* ec = serialize->GetEditContext())
  207. {
  208. ec->Class<PythonSystemComponent>("PythonSystemComponent", "The Python interpreter")
  209. ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
  210. ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
  211. ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
  212. ;
  213. }
  214. }
  215. }
  216. void PythonSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
  217. {
  218. provided.push_back(PythonEmbeddedService);
  219. }
  220. void PythonSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
  221. {
  222. incompatible.push_back(PythonEmbeddedService);
  223. }
  224. void PythonSystemComponent::Activate()
  225. {
  226. AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Register(this);
  227. AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusConnect();
  228. }
  229. void PythonSystemComponent::Deactivate()
  230. {
  231. AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
  232. AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Unregister(this);
  233. StopPython(true);
  234. }
  235. bool PythonSystemComponent::StartPython([[maybe_unused]] bool silenceWarnings)
  236. {
  237. struct ReleaseInitalizeWaiterScope final
  238. {
  239. using ReleaseFunction = AZStd::function<void(void)>;
  240. ReleaseInitalizeWaiterScope(ReleaseFunction releaseFunction)
  241. {
  242. m_releaseFunction = AZStd::move(releaseFunction);
  243. }
  244. ~ReleaseInitalizeWaiterScope()
  245. {
  246. m_releaseFunction();
  247. }
  248. ReleaseFunction m_releaseFunction;
  249. };
  250. ReleaseInitalizeWaiterScope scope([this]()
  251. {
  252. m_initalizeWaiter.release(m_initalizeWaiterCount);
  253. m_initalizeWaiterCount = 0;
  254. });
  255. if (Py_IsInitialized())
  256. {
  257. AZ_Warning("python", silenceWarnings, "Python is already active!");
  258. return false;
  259. }
  260. PythonPathStack pythonPathStack;
  261. DiscoverPythonPaths(pythonPathStack);
  262. EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreInitialize);
  263. if (StartPythonInterpreter(pythonPathStack))
  264. {
  265. EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostInitialize);
  266. // initialize internal base module and bootstrap scripts
  267. ExecuteByString("import azlmbr", false);
  268. ExecuteBootstrapScripts(pythonPathStack);
  269. return true;
  270. }
  271. return false;
  272. }
  273. bool PythonSystemComponent::StopPython([[maybe_unused]] bool silenceWarnings)
  274. {
  275. if (!Py_IsInitialized())
  276. {
  277. AZ_Warning("python", silenceWarnings, "Python is not active!");
  278. return false;
  279. }
  280. bool result = false;
  281. EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreFinalize);
  282. AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
  283. result = StopPythonInterpreter();
  284. EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostFinalize);
  285. return result;
  286. }
  287. bool PythonSystemComponent::IsPythonActive()
  288. {
  289. return Py_IsInitialized() != 0;
  290. }
  291. void PythonSystemComponent::WaitForInitialization()
  292. {
  293. m_initalizeWaiterCount++;
  294. m_initalizeWaiter.acquire();
  295. }
  296. void PythonSystemComponent::ExecuteWithLock(AZStd::function<void()> executionCallback)
  297. {
  298. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  299. pybind11::gil_scoped_release release;
  300. pybind11::gil_scoped_acquire acquire;
  301. executionCallback();
  302. }
  303. void PythonSystemComponent::DiscoverPythonPaths(PythonPathStack& pythonPathStack)
  304. {
  305. // the order of the Python paths is the order the Python bootstrap scripts will execute
  306. auto settingsRegistry = AZ::SettingsRegistry::Get();
  307. if (!settingsRegistry)
  308. {
  309. return;
  310. }
  311. AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
  312. if (projectPath.empty())
  313. {
  314. return;
  315. }
  316. auto resolveScriptPath = [&pythonPathStack](AZStd::string_view path)
  317. {
  318. auto editorScriptsPath = AZ::IO::Path(path) / "Editor" / "Scripts";
  319. if (AZ::IO::SystemFile::Exists(editorScriptsPath.c_str()))
  320. {
  321. pythonPathStack.emplace_back(AZStd::move(editorScriptsPath.LexicallyNormal().Native()));
  322. }
  323. };
  324. // The discovery order will be:
  325. // 1 - engine-root/EngineAsets
  326. // 2 - gems
  327. // 3 - project
  328. // 4 - user(dev)
  329. // 1 - engine
  330. AZ::IO::FixedMaxPath engineRoot;
  331. if (settingsRegistry->Get(engineRoot.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder); !engineRoot.empty())
  332. {
  333. resolveScriptPath((engineRoot / "Assets").Native());
  334. }
  335. // 2 - gems
  336. struct GetGemSourcePathsVisitor
  337. : AZ::SettingsRegistryInterface::Visitor
  338. {
  339. GetGemSourcePathsVisitor(AZ::SettingsRegistryInterface& settingsRegistry)
  340. : m_settingsRegistry(settingsRegistry)
  341. {}
  342. void Visit(AZStd::string_view path, AZStd::string_view, AZ::SettingsRegistryInterface::Type,
  343. AZStd::string_view value) override
  344. {
  345. AZStd::string_view jsonSourcePathPointer{ path };
  346. // Remove the array index from the path and check if the JSON path ends with "/SourcePaths"
  347. AZ::StringFunc::TokenizeLast(jsonSourcePathPointer, "/");
  348. if (jsonSourcePathPointer.ends_with("/SourcePaths"))
  349. {
  350. AZ::IO::Path newSourcePath = jsonSourcePathPointer;
  351. // Resolve any file aliases first - Do not use ResolvePath() as that assumes
  352. // any relative path is underneath the @assets@ alias
  353. if (auto fileIoBase = AZ::IO::FileIOBase::GetInstance(); fileIoBase != nullptr)
  354. {
  355. AZ::IO::FixedMaxPath replacedAliasPath;
  356. if (fileIoBase->ReplaceAlias(replacedAliasPath, value))
  357. {
  358. newSourcePath = AZ::IO::PathView(replacedAliasPath);
  359. }
  360. }
  361. // The current assumption is that the gem source path is the relative to the engine root
  362. AZ::IO::Path engineRootPath;
  363. m_settingsRegistry.Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
  364. newSourcePath = (engineRootPath / newSourcePath).LexicallyNormal();
  365. if (auto gemSourcePathIter = AZStd::find(m_gemSourcePaths.begin(), m_gemSourcePaths.end(), newSourcePath);
  366. gemSourcePathIter == m_gemSourcePaths.end())
  367. {
  368. m_gemSourcePaths.emplace_back(AZStd::move(newSourcePath));
  369. }
  370. }
  371. }
  372. AZStd::vector<AZ::IO::Path> m_gemSourcePaths;
  373. private:
  374. AZ::SettingsRegistryInterface& m_settingsRegistry;
  375. };
  376. GetGemSourcePathsVisitor visitor{ *settingsRegistry };
  377. constexpr auto gemListKey = AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::OrganizationRootKey)
  378. + "/Gems";
  379. settingsRegistry->Visit(visitor, gemListKey);
  380. for (const AZ::IO::Path& gemSourcePath : visitor.m_gemSourcePaths)
  381. {
  382. resolveScriptPath(gemSourcePath.Native());
  383. }
  384. // 3 - project
  385. resolveScriptPath(AZStd::string_view{ projectPath });
  386. // 4 - user
  387. AZStd::string assetsType;
  388. AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, assetsType,
  389. AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, AzFramework::AssetSystem::Assets);
  390. if (!assetsType.empty())
  391. {
  392. AZ::IO::FixedMaxPath userCachePath;
  393. if (settingsRegistry->Get(userCachePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
  394. !userCachePath.empty())
  395. {
  396. userCachePath /= "user";
  397. resolveScriptPath(userCachePath.Native());
  398. }
  399. }
  400. }
  401. void PythonSystemComponent::ExecuteBootstrapScripts(const PythonPathStack& pythonPathStack)
  402. {
  403. for(const auto& path : pythonPathStack)
  404. {
  405. AZStd::string bootstrapPath;
  406. AzFramework::StringFunc::Path::Join(path.c_str(), "bootstrap.py", bootstrapPath);
  407. if (AZ::IO::SystemFile::Exists(bootstrapPath.c_str()))
  408. {
  409. ExecuteByFilename(bootstrapPath);
  410. }
  411. }
  412. }
  413. bool PythonSystemComponent::StartPythonInterpreter(const PythonPathStack& pythonPathStack)
  414. {
  415. AZStd::unordered_set<AZStd::string> pyPackageSites(pythonPathStack.begin(), pythonPathStack.end());
  416. const char* engineRoot = nullptr;
  417. AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
  418. // set PYTHON_HOME
  419. AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, engineRoot);
  420. if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
  421. {
  422. AZ_Warning("python", false, "Python home path must exist! path:%s", pyBasePath.c_str());
  423. return false;
  424. }
  425. AZStd::wstring pyHomePath;
  426. AZStd::to_wstring(pyHomePath, pyBasePath);
  427. Py_SetPythonHome(pyHomePath.c_str());
  428. // display basic Python information
  429. AZ_TracePrintf("python", "Py_GetVersion=%s \n", Py_GetVersion());
  430. AZ_TracePrintf("python", "Py_GetPath=%ls \n", Py_GetPath());
  431. AZ_TracePrintf("python", "Py_GetExecPrefix=%ls \n", Py_GetExecPrefix());
  432. AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
  433. PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
  434. try
  435. {
  436. // ignore system location for sites site-packages
  437. Py_IsolatedFlag = 1; // -I - Also sets Py_NoUserSiteDirectory. If removed PyNoUserSiteDirectory should be set.
  438. Py_IgnoreEnvironmentFlag = 1; // -E
  439. Py_InspectFlag = 1; // unhandled SystemExit will terminate the process unless Py_InspectFlag is set
  440. const bool initializeSignalHandlers = true;
  441. pybind11::initialize_interpreter(initializeSignalHandlers);
  442. // Add custom site packages after initializing the interpreter above. Calling Py_SetPath before initialization
  443. // 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
  444. if (pyPackageSites.size())
  445. {
  446. ExtendSysPath(pyPackageSites);
  447. }
  448. RedirectOutput::Intialize(PyImport_ImportModule("azlmbr_redirect"));
  449. // Acquire GIL before calling Python code
  450. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  451. pybind11::gil_scoped_acquire acquire;
  452. // print Python version using AZ logging
  453. const int verRet = PyRun_SimpleStringFlags("import sys \nprint (sys.version) \n", nullptr);
  454. AZ_Error("python", verRet == 0, "Error trying to fetch the version number in Python!");
  455. return verRet == 0 && !PyErr_Occurred();
  456. }
  457. catch ([[maybe_unused]] const std::exception& e)
  458. {
  459. AZ_Warning("python", false, "Py_Initialize() failed with %s!", e.what());
  460. return false;
  461. }
  462. }
  463. bool PythonSystemComponent::ExtendSysPath(const AZStd::unordered_set<AZStd::string>& extendPaths)
  464. {
  465. AZStd::unordered_set<AZStd::string> oldPathSet;
  466. auto SplitPath = [&oldPathSet](AZStd::string_view pathPart)
  467. {
  468. oldPathSet.emplace(pathPart);
  469. };
  470. AZ::StringFunc::TokenizeVisitor(Py_EncodeLocale(Py_GetPath(), nullptr), SplitPath, DELIM);
  471. bool appended{ false };
  472. AZStd::string pathAppend{ "import sys\n" };
  473. for (const auto& thisStr : extendPaths)
  474. {
  475. if (!oldPathSet.contains(thisStr))
  476. {
  477. pathAppend.append(AZStd::string::format("sys.path.append(r'%s')\n", thisStr.c_str()));
  478. appended = true;
  479. }
  480. }
  481. if (appended)
  482. {
  483. ExecuteByString(pathAppend.c_str(), false);
  484. return true;
  485. }
  486. return false;
  487. }
  488. bool PythonSystemComponent::StopPythonInterpreter()
  489. {
  490. if (Py_IsInitialized())
  491. {
  492. RedirectOutput::Shutdown();
  493. pybind11::finalize_interpreter();
  494. }
  495. else
  496. {
  497. AZ_Warning("python", false, "Did not finalize since Py_IsInitialized() was false.");
  498. }
  499. return !PyErr_Occurred();
  500. }
  501. void PythonSystemComponent::ExecuteByString(AZStd::string_view script, bool printResult)
  502. {
  503. if (!Py_IsInitialized())
  504. {
  505. AZ_Error("python", false, "Can not ExecuteByString() since the embeded Python VM is not ready.");
  506. return;
  507. }
  508. if (!script.empty())
  509. {
  510. AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
  511. &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByString, script);
  512. // Acquire GIL before calling Python code
  513. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  514. pybind11::gil_scoped_acquire acquire;
  515. // Acquire scope for __main__ for executing our script
  516. pybind11::object scope = pybind11::module::import("__main__").attr("__dict__");
  517. bool shouldPrintValue = false;
  518. if (printResult)
  519. {
  520. // Attempt to compile our code to determine if it's an expression
  521. // i.e. a Python code object with only an rvalue
  522. // If it is, it can be evaled to produce a PyObject
  523. // If it's not, we can't evaluate it into a result and should fall back to exec
  524. shouldPrintValue = true;
  525. using namespace pybind11::literals;
  526. // codeop.compile_command is a thin wrapper around the Python compile builtin
  527. // We attempt to compile using symbol="eval" to see if the string is valid for eval
  528. // This is similar to what the Python REPL does internally
  529. pybind11::object codeop = pybind11::module::import("codeop");
  530. pybind11::object compileCommand = codeop.attr("compile_command");
  531. try
  532. {
  533. compileCommand(script.data(), "symbol"_a="eval");
  534. }
  535. catch (const pybind11::error_already_set&)
  536. {
  537. shouldPrintValue = false;
  538. }
  539. }
  540. try
  541. {
  542. if (shouldPrintValue)
  543. {
  544. // We're an expression, run and print the result
  545. pybind11::object result = pybind11::eval(script.data(), scope);
  546. pybind11::print(result);
  547. }
  548. else
  549. {
  550. // Just exec the code block
  551. pybind11::exec(script.data(), scope);
  552. }
  553. }
  554. catch (pybind11::error_already_set& pythonError)
  555. {
  556. // Release the exception stack and let Python print it to stderr
  557. pythonError.restore();
  558. PyErr_Print();
  559. }
  560. }
  561. }
  562. void PythonSystemComponent::ExecuteByFilename(AZStd::string_view filename)
  563. {
  564. AZStd::vector<AZStd::string_view> args;
  565. AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
  566. &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilename, filename);
  567. ExecuteByFilenameWithArgs(filename, args);
  568. }
  569. bool PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector<AZStd::string_view>& args)
  570. {
  571. AZ_TracePrintf("python", "Running automated test: %.*s (testcase %.*s)", AZ_STRING_ARG(filename), AZ_STRING_ARG(testCase))
  572. AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
  573. &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameAsTest, filename, testCase, args);
  574. const Result evalResult = EvaluateFile(filename, args);
  575. return evalResult == Result::Okay;
  576. }
  577. void PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
  578. {
  579. AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
  580. &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameWithArgs, filename, args);
  581. EvaluateFile(filename, args);
  582. }
  583. PythonSystemComponent::Result PythonSystemComponent::EvaluateFile(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
  584. {
  585. if (!Py_IsInitialized())
  586. {
  587. AZ_Error("python", false, "Can not evaluate file since the embedded Python VM is not ready.");
  588. return Result::Error_IsNotInitialized;
  589. }
  590. if (filename.empty())
  591. {
  592. AZ_Error("python", false, "Invalid empty filename detected.");
  593. return Result::Error_InvalidFilename;
  594. }
  595. // support the alias version of a script such as @devroot@/Editor/Scripts/select_story_anim_objects.py
  596. AZStd::string theFilename(filename);
  597. {
  598. char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
  599. AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(theFilename.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  600. theFilename = resolvedPath;
  601. }
  602. if (!AZ::IO::FileIOBase::GetInstance()->Exists(theFilename.c_str()))
  603. {
  604. AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
  605. return Result::Error_MissingFile;
  606. }
  607. FILE* file = _Py_fopen(theFilename.data(), "rb");
  608. if (!file)
  609. {
  610. AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
  611. return Result::Error_FileOpenValidation;
  612. }
  613. Result pythonScriptResult = Result::Okay;
  614. try
  615. {
  616. // Acquire GIL before calling Python code
  617. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  618. pybind11::gil_scoped_acquire acquire;
  619. // Create standard "argc" / "argv" command-line parameters to pass in to the Python script via sys.argv.
  620. // argc = number of parameters. This will always be at least 1, since the first parameter is the script name.
  621. // argv = the list of parameters, in wchar format.
  622. // Our expectation is that the args passed into this function does *not* already contain the script name.
  623. int argc = aznumeric_cast<int>(args.size()) + 1;
  624. // Note: This allocates from PyMem to ensure that Python has access to the memory.
  625. wchar_t** argv = static_cast<wchar_t**>(PyMem_Malloc(argc * sizeof(wchar_t*)));
  626. // Python 3.x is expecting wchar* strings for the command-line args.
  627. argv[0] = Py_DecodeLocale(theFilename.c_str(), nullptr);
  628. for (int arg = 0; arg < args.size(); arg++)
  629. {
  630. AZStd::string argString(args[arg]);
  631. argv[arg + 1] = Py_DecodeLocale(argString.c_str(), nullptr);
  632. }
  633. // Tell Python the command-line args.
  634. // Note that this has a side effect of adding the script's path to the set of directories checked for "import" commands.
  635. const int updatePath = 1;
  636. PySys_SetArgvEx(argc, argv, updatePath);
  637. PyCompilerFlags flags;
  638. flags.cf_flags = 0;
  639. const int bAutoCloseFile = true;
  640. const int returnCode = PyRun_SimpleFileExFlags(file, theFilename.c_str(), bAutoCloseFile, &flags);
  641. if (returnCode != 0)
  642. {
  643. AZStd::string message = AZStd::string::format("Detected script failure in Python script(%s); return code %d!", theFilename.c_str(), returnCode);
  644. AZ_Warning("python", false, message.c_str());
  645. using namespace AzToolsFramework;
  646. EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnExceptionMessage, message.c_str());
  647. pythonScriptResult = Result::Error_PythonException;
  648. }
  649. // Free any memory allocated for the command-line args.
  650. for (int arg = 0; arg < argc; arg++)
  651. {
  652. PyMem_RawFree(argv[arg]);
  653. }
  654. PyMem_Free(argv);
  655. }
  656. catch ([[maybe_unused]] const std::exception& e)
  657. {
  658. AZ_Error("python", false, "Detected an internal exception %s while running script (%s)!", e.what(), theFilename.c_str());
  659. return Result::Error_InternalException;
  660. }
  661. return pythonScriptResult;
  662. }
  663. } // namespace EditorPythonBindings