PythonBindings.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /*
  2. * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. * its licensors.
  4. *
  5. * For complete copyright and license terms please see the LICENSE at the root of this
  6. * distribution (the "License"). All use of this software is governed by the License,
  7. * or, if provided, by the license below or the license accompanying this file. Do not
  8. * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  9. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. *
  11. */
  12. #include <PythonBindings.h>
  13. // Qt defines slots, which interferes with the use here.
  14. #pragma push_macro("slots")
  15. #undef slots
  16. #include <pybind11/functional.h>
  17. #include <pybind11/embed.h>
  18. #include <pybind11/eval.h>
  19. #pragma pop_macro("slots")
  20. #include <AzCore/IO/FileIO.h>
  21. #include <AzCore/IO/SystemFile.h>
  22. #include <AzCore/std/string/conversions.h>
  23. #include <AzCore/StringFunc/StringFunc.h>
  24. namespace Platform
  25. {
  26. bool InsertPythonLibraryPath(
  27. AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot, const char* subPath)
  28. {
  29. // append lib path to Python paths
  30. AZ::IO::FixedMaxPath libPath = engineRoot;
  31. libPath /= AZ::IO::FixedMaxPathString::format(subPath, pythonPackage);
  32. libPath = libPath.LexicallyNormal();
  33. if (AZ::IO::SystemFile::Exists(libPath.c_str()))
  34. {
  35. paths.insert(libPath.c_str());
  36. return true;
  37. }
  38. AZ_Warning("python", false, "Python library path should exist. path:%s", libPath.c_str());
  39. return false;
  40. }
  41. // Implemented in each different platform's PAL implentation files, as it differs per platform.
  42. AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot);
  43. } // namespace Platform
  44. #define Py_To_String(obj) obj.cast<std::string>().c_str()
  45. #define Py_To_String_Optional(dict, key, default_string) dict.contains(key) ? Py_To_String(dict[key]) : default_string
  46. namespace O3DE::ProjectManager
  47. {
  48. PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
  49. : m_enginePath(enginePath)
  50. {
  51. StartPython();
  52. }
  53. PythonBindings::~PythonBindings()
  54. {
  55. StopPython();
  56. }
  57. bool PythonBindings::StartPython()
  58. {
  59. if (Py_IsInitialized())
  60. {
  61. AZ_Warning("python", false, "Python is already active");
  62. return false;
  63. }
  64. // set PYTHON_HOME
  65. AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, m_enginePath.c_str());
  66. if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
  67. {
  68. AZ_Warning("python", false, "Python home path must exist. path:%s", pyBasePath.c_str());
  69. return false;
  70. }
  71. AZStd::wstring pyHomePath;
  72. AZStd::to_wstring(pyHomePath, pyBasePath);
  73. Py_SetPythonHome(pyHomePath.c_str());
  74. // display basic Python information
  75. AZ_TracePrintf("python", "Py_GetVersion=%s \n", Py_GetVersion());
  76. AZ_TracePrintf("python", "Py_GetPath=%ls \n", Py_GetPath());
  77. AZ_TracePrintf("python", "Py_GetExecPrefix=%ls \n", Py_GetExecPrefix());
  78. AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
  79. try
  80. {
  81. // ignore system location for sites site-packages
  82. Py_IsolatedFlag = 1; // -I - Also sets Py_NoUserSiteDirectory. If removed PyNoUserSiteDirectory should be set.
  83. Py_IgnoreEnvironmentFlag = 1; // -E
  84. const bool initializeSignalHandlers = true;
  85. pybind11::initialize_interpreter(initializeSignalHandlers);
  86. // Acquire GIL before calling Python code
  87. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  88. pybind11::gil_scoped_acquire acquire;
  89. // Setup sys.path
  90. int result = PyRun_SimpleString("import sys");
  91. AZ_Warning("ProjectManagerWindow", result != -1, "Import sys failed");
  92. result = PyRun_SimpleString(AZStd::string::format("sys.path.append('%s')", m_enginePath.c_str()).c_str());
  93. AZ_Warning("ProjectManagerWindow", result != -1, "Append to sys path failed");
  94. // import required modules
  95. m_registration = pybind11::module::import("cmake.Tools.registration");
  96. return result == 0 && !PyErr_Occurred();
  97. } catch ([[maybe_unused]] const std::exception& e)
  98. {
  99. AZ_Warning("ProjectManagerWindow", false, "Py_Initialize() failed with %s", e.what());
  100. return false;
  101. }
  102. }
  103. bool PythonBindings::StopPython()
  104. {
  105. if (Py_IsInitialized())
  106. {
  107. pybind11::finalize_interpreter();
  108. }
  109. else
  110. {
  111. AZ_Warning("ProjectManagerWindow", false, "Did not finalize since Py_IsInitialized() was false");
  112. }
  113. return !PyErr_Occurred();
  114. }
  115. bool PythonBindings::ExecuteWithLock(AZStd::function<void()> executionCallback)
  116. {
  117. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  118. pybind11::gil_scoped_release release;
  119. pybind11::gil_scoped_acquire acquire;
  120. try
  121. {
  122. executionCallback();
  123. return true;
  124. }
  125. catch ([[maybe_unused]] const std::exception& e)
  126. {
  127. AZ_Warning("PythonBindings", false, "Python exception %s", e.what());
  128. return false;
  129. }
  130. }
  131. AZ::Outcome<EngineInfo> PythonBindings::GetEngineInfo()
  132. {
  133. return AZ::Failure();
  134. }
  135. bool PythonBindings::SetEngineInfo([[maybe_unused]] const EngineInfo& engineInfo)
  136. {
  137. return false;
  138. }
  139. AZ::Outcome<GemInfo> PythonBindings::GetGem(const QString& path)
  140. {
  141. GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()));
  142. if (gemInfo.IsValid())
  143. {
  144. return AZ::Success(AZStd::move(gemInfo));
  145. }
  146. else
  147. {
  148. return AZ::Failure();
  149. }
  150. }
  151. AZ::Outcome<QVector<GemInfo>> PythonBindings::GetGems()
  152. {
  153. QVector<GemInfo> gems;
  154. bool result = ExecuteWithLock([&] {
  155. // external gems
  156. for (auto path : m_registration.attr("get_gems")())
  157. {
  158. gems.push_back(GemInfoFromPath(path));
  159. }
  160. // gems from the engine
  161. for (auto path : m_registration.attr("get_engine_gems")())
  162. {
  163. gems.push_back(GemInfoFromPath(path));
  164. }
  165. });
  166. if (!result)
  167. {
  168. return AZ::Failure();
  169. }
  170. else
  171. {
  172. return AZ::Success(AZStd::move(gems));
  173. }
  174. }
  175. AZ::Outcome<ProjectInfo> PythonBindings::CreateProject([[maybe_unused]] const ProjectTemplateInfo& projectTemplate,[[maybe_unused]] const ProjectInfo& projectInfo)
  176. {
  177. return AZ::Failure();
  178. }
  179. AZ::Outcome<ProjectInfo> PythonBindings::GetProject(const QString& path)
  180. {
  181. ProjectInfo projectInfo = ProjectInfoFromPath(pybind11::str(path.toStdString()));
  182. if (projectInfo.IsValid())
  183. {
  184. return AZ::Success(AZStd::move(projectInfo));
  185. }
  186. else
  187. {
  188. return AZ::Failure();
  189. }
  190. }
  191. GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path)
  192. {
  193. GemInfo gemInfo;
  194. gemInfo.m_path = Py_To_String(path);
  195. auto data = m_registration.attr("get_gem_data")(pybind11::none(), path);
  196. if (pybind11::isinstance<pybind11::dict>(data))
  197. {
  198. try
  199. {
  200. // required
  201. gemInfo.m_name = Py_To_String(data["Name"]);
  202. gemInfo.m_uuid = AZ::Uuid(Py_To_String(data["Uuid"]));
  203. // optional
  204. gemInfo.m_displayName = Py_To_String_Optional(data, "DisplayName", gemInfo.m_name);
  205. gemInfo.m_summary = Py_To_String_Optional(data, "Summary", "");
  206. gemInfo.m_version = Py_To_String_Optional(data, "Version", "");
  207. if (data.contains("Dependencies"))
  208. {
  209. for (auto dependency : data["Dependencies"])
  210. {
  211. gemInfo.m_dependingGemUuids.push_back(AZ::Uuid(Py_To_String(dependency["Uuid"])));
  212. }
  213. }
  214. if (data.contains("Tags"))
  215. {
  216. for (auto tag : data["Tags"])
  217. {
  218. gemInfo.m_features.push_back(Py_To_String(tag));
  219. }
  220. }
  221. }
  222. catch ([[maybe_unused]] const std::exception& e)
  223. {
  224. AZ_Warning("PythonBindings", false, "Failed to get GemInfo for gem %s", Py_To_String(path));
  225. }
  226. }
  227. return gemInfo;
  228. }
  229. ProjectInfo PythonBindings::ProjectInfoFromPath(pybind11::handle path)
  230. {
  231. ProjectInfo projectInfo;
  232. projectInfo.m_path = Py_To_String(path);
  233. auto projectData = m_registration.attr("get_project_data")(pybind11::none(), path);
  234. if (pybind11::isinstance<pybind11::dict>(projectData))
  235. {
  236. try
  237. {
  238. // required fields
  239. projectInfo.m_productName = Py_To_String(projectData["product_name"]);
  240. projectInfo.m_projectName = Py_To_String(projectData["project_name"]);
  241. projectInfo.m_projectId = AZ::Uuid(Py_To_String(projectData["project_id"]));
  242. }
  243. catch ([[maybe_unused]] const std::exception& e)
  244. {
  245. AZ_Warning("PythonBindings", false, "Failed to get ProjectInfo for project %s", Py_To_String(path));
  246. }
  247. }
  248. return projectInfo;
  249. }
  250. AZ::Outcome<QVector<ProjectInfo>> PythonBindings::GetProjects()
  251. {
  252. QVector<ProjectInfo> projects;
  253. bool result = ExecuteWithLock([&] {
  254. // external projects
  255. for (auto path : m_registration.attr("get_projects")())
  256. {
  257. projects.push_back(ProjectInfoFromPath(path));
  258. }
  259. // projects from the engine
  260. for (auto path : m_registration.attr("get_engine_projects")())
  261. {
  262. projects.push_back(ProjectInfoFromPath(path));
  263. }
  264. });
  265. if (!result)
  266. {
  267. return AZ::Failure();
  268. }
  269. else
  270. {
  271. return AZ::Success(AZStd::move(projects));
  272. }
  273. }
  274. bool PythonBindings::UpdateProject([[maybe_unused]] const ProjectInfo& projectInfo)
  275. {
  276. return false;
  277. }
  278. ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path)
  279. {
  280. ProjectTemplateInfo templateInfo;
  281. templateInfo.m_path = Py_To_String(path);
  282. auto data = m_registration.attr("get_template_data")(pybind11::none(), path);
  283. if (pybind11::isinstance<pybind11::dict>(data))
  284. {
  285. try
  286. {
  287. // required
  288. templateInfo.m_displayName = Py_To_String(data["display_name"]);
  289. templateInfo.m_name = Py_To_String(data["template_name"]);
  290. templateInfo.m_summary = Py_To_String(data["summary"]);
  291. // optional
  292. if (data.contains("canonical_tags"))
  293. {
  294. for (auto tag : data["canonical_tags"])
  295. {
  296. templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
  297. }
  298. }
  299. if (data.contains("user_tags"))
  300. {
  301. for (auto tag : data["user_tags"])
  302. {
  303. templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
  304. }
  305. }
  306. }
  307. catch ([[maybe_unused]] const std::exception& e)
  308. {
  309. AZ_Warning("PythonBindings", false, "Failed to get ProjectTemplateInfo for %s", Py_To_String(path));
  310. }
  311. }
  312. return templateInfo;
  313. }
  314. AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates()
  315. {
  316. QVector<ProjectTemplateInfo> templates;
  317. bool result = ExecuteWithLock([&] {
  318. for (auto path : m_registration.attr("get_project_templates")())
  319. {
  320. templates.push_back(ProjectTemplateInfoFromPath(path));
  321. }
  322. });
  323. if (!result)
  324. {
  325. return AZ::Failure();
  326. }
  327. else
  328. {
  329. return AZ::Success(AZStd::move(templates));
  330. }
  331. }
  332. }