PythonBindings.cpp 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345
  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 <PythonBindings.h>
  9. #include <ProjectManagerDefs.h>
  10. // Qt defines slots, which interferes with the use here.
  11. #pragma push_macro("slots")
  12. #undef slots
  13. #include <pybind11/functional.h>
  14. #include <pybind11/embed.h>
  15. #include <pybind11/eval.h>
  16. #include <pybind11/stl.h>
  17. #pragma pop_macro("slots")
  18. #include <AzCore/IO/FileIO.h>
  19. #include <AzCore/IO/SystemFile.h>
  20. #include <AzCore/std/containers/unordered_set.h>
  21. #include <AzCore/std/string/conversions.h>
  22. #include <AzCore/std/numeric.h>
  23. #include <AzCore/StringFunc/StringFunc.h>
  24. #include <QDir>
  25. namespace Platform
  26. {
  27. bool InsertPythonLibraryPath(
  28. AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot, const char* subPath)
  29. {
  30. // append lib path to Python paths
  31. AZ::IO::FixedMaxPath libPath = engineRoot;
  32. libPath /= AZ::IO::FixedMaxPathString::format(subPath, pythonPackage);
  33. libPath = libPath.LexicallyNormal();
  34. if (AZ::IO::SystemFile::Exists(libPath.c_str()))
  35. {
  36. paths.insert(libPath.c_str());
  37. return true;
  38. }
  39. AZ_Warning("python", false, "Python library path should exist. path:%s", libPath.c_str());
  40. return false;
  41. }
  42. // Implemented in each different platform's PAL implementation files, as it differs per platform.
  43. AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot);
  44. } // namespace Platform
  45. #define Py_To_String(obj) pybind11::str(obj).cast<std::string>().c_str()
  46. #define Py_To_String_Optional(dict, key, default_string) dict.contains(key) ? Py_To_String(dict[key]) : default_string
  47. #define Py_To_Int(obj) obj.cast<int>()
  48. #define Py_To_Int_Optional(dict, key, default_int) dict.contains(key) ? Py_To_Int(dict[key]) : default_int
  49. #define QString_To_Py_String(value) pybind11::str(value.toStdString())
  50. #define QString_To_Py_Path(value) m_pathlib.attr("Path")(value.toStdString())
  51. namespace RedirectOutput
  52. {
  53. using RedirectOutputFunc = AZStd::function<void(const char*)>;
  54. struct RedirectOutput
  55. {
  56. PyObject_HEAD RedirectOutputFunc write;
  57. };
  58. PyObject* RedirectWrite(PyObject* self, PyObject* args)
  59. {
  60. std::size_t written(0);
  61. RedirectOutput* selfimpl = reinterpret_cast<RedirectOutput*>(self);
  62. if (selfimpl->write)
  63. {
  64. char* data;
  65. if (!PyArg_ParseTuple(args, "s", &data))
  66. {
  67. return PyLong_FromSize_t(0);
  68. }
  69. selfimpl->write(data);
  70. written = strlen(data);
  71. }
  72. return PyLong_FromSize_t(written);
  73. }
  74. PyObject* RedirectFlush([[maybe_unused]] PyObject* self,[[maybe_unused]] PyObject* args)
  75. {
  76. // no-op
  77. return Py_BuildValue("");
  78. }
  79. PyMethodDef RedirectMethods[] = {
  80. {"write", RedirectWrite, METH_VARARGS, "sys.stdout.write"},
  81. {"flush", RedirectFlush, METH_VARARGS, "sys.stdout.flush"},
  82. {"write", RedirectWrite, METH_VARARGS, "sys.stderr.write"},
  83. {"flush", RedirectFlush, METH_VARARGS, "sys.stderr.flush"},
  84. {0, 0, 0, 0} // sentinel
  85. };
  86. PyTypeObject RedirectOutputType = {
  87. PyVarObject_HEAD_INIT(0, 0) "azlmbr_redirect.RedirectOutputType", // tp_name
  88. sizeof(RedirectOutput), /* tp_basicsize */
  89. 0, /* tp_itemsize */
  90. 0, /* tp_dealloc */
  91. 0, /* tp_print */
  92. 0, /* tp_getattr */
  93. 0, /* tp_setattr */
  94. 0, /* tp_reserved */
  95. 0, /* tp_repr */
  96. 0, /* tp_as_number */
  97. 0, /* tp_as_sequence */
  98. 0, /* tp_as_mapping */
  99. 0, /* tp_hash */
  100. 0, /* tp_call */
  101. 0, /* tp_str */
  102. 0, /* tp_getattro */
  103. 0, /* tp_setattro */
  104. 0, /* tp_as_buffer */
  105. Py_TPFLAGS_DEFAULT, /* tp_flags */
  106. "azlmbr_redirect objects", /* tp_doc */
  107. 0, /* tp_traverse */
  108. 0, /* tp_clear */
  109. 0, /* tp_richcompare */
  110. 0, /* tp_weaklistoffset */
  111. 0, /* tp_iter */
  112. 0, /* tp_iternext */
  113. RedirectMethods, /* tp_methods */
  114. 0, /* tp_members */
  115. 0, /* tp_getset */
  116. 0, /* tp_base */
  117. 0, /* tp_dict */
  118. 0, /* tp_descr_get */
  119. 0, /* tp_descr_set */
  120. 0, /* tp_dictoffset */
  121. 0, /* tp_init */
  122. 0, /* tp_alloc */
  123. 0 /* tp_new */
  124. };
  125. PyModuleDef RedirectOutputModule = {
  126. PyModuleDef_HEAD_INIT, "azlmbr_redirect", 0, -1, 0,
  127. };
  128. // Internal state
  129. PyObject* g_redirect_stdout = nullptr;
  130. PyObject* g_redirect_stdout_saved = nullptr;
  131. PyObject* g_redirect_stderr = nullptr;
  132. PyObject* g_redirect_stderr_saved = nullptr;
  133. PyMODINIT_FUNC PyInit_RedirectOutput(void)
  134. {
  135. g_redirect_stdout = nullptr;
  136. g_redirect_stdout_saved = nullptr;
  137. g_redirect_stderr = nullptr;
  138. g_redirect_stderr_saved = nullptr;
  139. RedirectOutputType.tp_new = PyType_GenericNew;
  140. if (PyType_Ready(&RedirectOutputType) < 0)
  141. {
  142. return 0;
  143. }
  144. PyObject* redirectModule = PyModule_Create(&RedirectOutputModule);
  145. if (redirectModule)
  146. {
  147. Py_INCREF(&RedirectOutputType);
  148. PyModule_AddObject(redirectModule, "Redirect", reinterpret_cast<PyObject*>(&RedirectOutputType));
  149. }
  150. return redirectModule;
  151. }
  152. void SetRedirection(const char* funcname, PyObject*& saved, PyObject*& current, RedirectOutputFunc func)
  153. {
  154. if (PyType_Ready(&RedirectOutputType) < 0)
  155. {
  156. AZ_Warning("python", false, "RedirectOutputType not ready!");
  157. return;
  158. }
  159. if (!current)
  160. {
  161. saved = PySys_GetObject(funcname); // borrowed
  162. current = RedirectOutputType.tp_new(&RedirectOutputType, 0, 0);
  163. }
  164. RedirectOutput* redirectOutput = reinterpret_cast<RedirectOutput*>(current);
  165. redirectOutput->write = func;
  166. PySys_SetObject(funcname, current);
  167. }
  168. void ResetRedirection(const char* funcname, PyObject*& saved, PyObject*& current)
  169. {
  170. if (current)
  171. {
  172. PySys_SetObject(funcname, saved);
  173. }
  174. Py_XDECREF(current);
  175. current = nullptr;
  176. }
  177. PyObject* s_RedirectModule = nullptr;
  178. void Intialize(PyObject* module)
  179. {
  180. s_RedirectModule = module;
  181. SetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout, []([[maybe_unused]] const char* msg) {
  182. AZ_TracePrintf("Python", msg);
  183. });
  184. SetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr, []([[maybe_unused]] const char* msg) {
  185. AZStd::string lastPythonError = msg;
  186. constexpr const char* pythonErrorPrefix = "ERROR:root:";
  187. constexpr size_t lengthOfErrorPrefix = AZStd::char_traits<char>::length(pythonErrorPrefix);
  188. auto errorPrefix = lastPythonError.find(pythonErrorPrefix);
  189. if (errorPrefix != AZStd::string::npos)
  190. {
  191. lastPythonError.erase(errorPrefix, lengthOfErrorPrefix);
  192. }
  193. O3DE::ProjectManager::PythonBindingsInterface::Get()->AddErrorString(lastPythonError);
  194. AZ_TracePrintf("Python", msg);
  195. });
  196. PySys_WriteStdout("RedirectOutput installed");
  197. }
  198. void Shutdown()
  199. {
  200. ResetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout);
  201. ResetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr);
  202. Py_XDECREF(s_RedirectModule);
  203. s_RedirectModule = nullptr;
  204. }
  205. } // namespace RedirectOutput
  206. namespace O3DE::ProjectManager
  207. {
  208. PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
  209. : m_enginePath(enginePath)
  210. {
  211. m_pythonStarted = StartPython();
  212. }
  213. PythonBindings::~PythonBindings()
  214. {
  215. StopPython();
  216. }
  217. bool PythonBindings::PythonStarted()
  218. {
  219. return m_pythonStarted && Py_IsInitialized();
  220. }
  221. bool PythonBindings::StartPython()
  222. {
  223. if (Py_IsInitialized())
  224. {
  225. AZ_Warning("python", false, "Python is already active");
  226. return m_pythonStarted;
  227. }
  228. m_pythonStarted = false;
  229. // set PYTHON_HOME
  230. AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, m_enginePath.c_str());
  231. if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
  232. {
  233. AZ_Error("python", false, "Python home path does not exist: %s", pyBasePath.c_str());
  234. return false;
  235. }
  236. AZStd::wstring pyHomePath;
  237. AZStd::to_wstring(pyHomePath, pyBasePath);
  238. Py_SetPythonHome(pyHomePath.c_str());
  239. // display basic Python information
  240. AZ_TracePrintf("python", "Py_GetVersion=%s \n", Py_GetVersion());
  241. AZ_TracePrintf("python", "Py_GetPath=%ls \n", Py_GetPath());
  242. AZ_TracePrintf("python", "Py_GetExecPrefix=%ls \n", Py_GetExecPrefix());
  243. AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
  244. PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
  245. try
  246. {
  247. // ignore system location for sites site-packages
  248. Py_IsolatedFlag = 1; // -I - Also sets Py_NoUserSiteDirectory. If removed PyNoUserSiteDirectory should be set.
  249. Py_IgnoreEnvironmentFlag = 1; // -E
  250. const bool initializeSignalHandlers = true;
  251. pybind11::initialize_interpreter(initializeSignalHandlers);
  252. RedirectOutput::Intialize(PyImport_ImportModule("azlmbr_redirect"));
  253. // Acquire GIL before calling Python code
  254. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  255. pybind11::gil_scoped_acquire acquire;
  256. // sanity import check
  257. if (PyRun_SimpleString("import sys") != 0)
  258. {
  259. AZ_Assert(false, "Import sys failed");
  260. return false;
  261. }
  262. // import required modules
  263. m_cmake = pybind11::module::import("o3de.cmake");
  264. m_register = pybind11::module::import("o3de.register");
  265. m_manifest = pybind11::module::import("o3de.manifest");
  266. m_engineTemplate = pybind11::module::import("o3de.engine_template");
  267. m_enableGemProject = pybind11::module::import("o3de.enable_gem");
  268. m_disableGemProject = pybind11::module::import("o3de.disable_gem");
  269. m_editProjectProperties = pybind11::module::import("o3de.project_properties");
  270. m_download = pybind11::module::import("o3de.download");
  271. m_repo = pybind11::module::import("o3de.repo");
  272. m_pathlib = pybind11::module::import("pathlib");
  273. // make sure the engine is registered
  274. RegisterThisEngine();
  275. m_pythonStarted = !PyErr_Occurred();
  276. return m_pythonStarted;
  277. }
  278. catch ([[maybe_unused]] const std::exception& e)
  279. {
  280. AZ_Assert(false, "Py_Initialize() failed with %s", e.what());
  281. return false;
  282. }
  283. }
  284. bool PythonBindings::StopPython()
  285. {
  286. if (Py_IsInitialized())
  287. {
  288. RedirectOutput::Shutdown();
  289. pybind11::finalize_interpreter();
  290. }
  291. else
  292. {
  293. AZ_Warning("ProjectManagerWindow", false, "Did not finalize since Py_IsInitialized() was false");
  294. }
  295. return !PyErr_Occurred();
  296. }
  297. bool PythonBindings::RegisterThisEngine()
  298. {
  299. bool registrationResult = true; // already registered is considered successful
  300. bool pythonResult = ExecuteWithLock(
  301. [&]
  302. {
  303. // check current engine path against all other registered engines
  304. // to see if we are already registered
  305. auto allEngines = m_manifest.attr("get_engines")();
  306. if (pybind11::isinstance<pybind11::list>(allEngines))
  307. {
  308. for (auto engine : allEngines)
  309. {
  310. AZ::IO::FixedMaxPath enginePath(Py_To_String(engine));
  311. if (enginePath.Compare(m_enginePath) == 0)
  312. {
  313. return;
  314. }
  315. }
  316. }
  317. auto result = m_register.attr("register")(QString_To_Py_Path(QString(m_enginePath.c_str())));
  318. registrationResult = (result.cast<int>() == 0);
  319. });
  320. bool finalResult = (registrationResult && pythonResult);
  321. AZ_Assert(finalResult, "Registration of this engine failed!");
  322. return finalResult;
  323. }
  324. AZ::Outcome<void, AZStd::string> PythonBindings::ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback)
  325. {
  326. if (!Py_IsInitialized())
  327. {
  328. return AZ::Failure<AZStd::string>("Python is not initialized");
  329. }
  330. AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
  331. pybind11::gil_scoped_release release;
  332. pybind11::gil_scoped_acquire acquire;
  333. ClearErrorStrings();
  334. try
  335. {
  336. executionCallback();
  337. }
  338. catch ([[maybe_unused]] const std::exception& e)
  339. {
  340. AZ_Warning("PythonBindings", false, "Python exception %s", e.what());
  341. return AZ::Failure<AZStd::string>(e.what());
  342. }
  343. return AZ::Success();
  344. }
  345. bool PythonBindings::ExecuteWithLock(AZStd::function<void()> executionCallback)
  346. {
  347. return ExecuteWithLockErrorHandling(executionCallback).IsSuccess();
  348. }
  349. AZ::Outcome<EngineInfo> PythonBindings::GetEngineInfo()
  350. {
  351. EngineInfo engineInfo;
  352. bool result = ExecuteWithLock([&] {
  353. auto enginePath = m_manifest.attr("get_this_engine_path")();
  354. auto o3deData = m_manifest.attr("load_o3de_manifest")();
  355. if (pybind11::isinstance<pybind11::dict>(o3deData))
  356. {
  357. engineInfo.m_path = Py_To_String(enginePath);
  358. auto defaultGemsFolder = m_manifest.attr("get_o3de_gems_folder")();
  359. engineInfo.m_defaultGemsFolder = Py_To_String_Optional(o3deData, "default_gems_folder", Py_To_String(defaultGemsFolder));
  360. auto defaultProjectsFolder = m_manifest.attr("get_o3de_projects_folder")();
  361. engineInfo.m_defaultProjectsFolder = Py_To_String_Optional(o3deData, "default_projects_folder", Py_To_String(defaultProjectsFolder));
  362. auto defaultRestrictedFolder = m_manifest.attr("get_o3de_restricted_folder")();
  363. engineInfo.m_defaultRestrictedFolder = Py_To_String_Optional(o3deData, "default_restricted_folder", Py_To_String(defaultRestrictedFolder));
  364. auto defaultTemplatesFolder = m_manifest.attr("get_o3de_templates_folder")();
  365. engineInfo.m_defaultTemplatesFolder = Py_To_String_Optional(o3deData, "default_templates_folder", Py_To_String(defaultTemplatesFolder));
  366. auto defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")();
  367. engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData, "default_third_party_folder", Py_To_String(defaultThirdPartyFolder));
  368. }
  369. auto engineData = m_manifest.attr("get_engine_json_data")(pybind11::none(), enginePath);
  370. if (pybind11::isinstance<pybind11::dict>(engineData))
  371. {
  372. try
  373. {
  374. engineInfo.m_version = Py_To_String_Optional(engineData, "O3DEVersion", "0.0.0.0");
  375. engineInfo.m_name = Py_To_String_Optional(engineData, "engine_name", "O3DE");
  376. }
  377. catch ([[maybe_unused]] const std::exception& e)
  378. {
  379. AZ_Warning("PythonBindings", false, "Failed to get EngineInfo from %s", Py_To_String(enginePath));
  380. }
  381. }
  382. });
  383. if (!result || !engineInfo.IsValid())
  384. {
  385. return AZ::Failure();
  386. }
  387. else
  388. {
  389. return AZ::Success(AZStd::move(engineInfo));
  390. }
  391. }
  392. bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo)
  393. {
  394. bool result = ExecuteWithLock([&] {
  395. auto registrationResult = m_register.attr("register")(
  396. QString_To_Py_Path(engineInfo.m_path),
  397. pybind11::none(), // project_path
  398. pybind11::none(), // gem_path
  399. pybind11::none(), // external_subdir_path
  400. pybind11::none(), // template_path
  401. pybind11::none(), // restricted_path
  402. pybind11::none(), // repo_uri
  403. pybind11::none(), // default_engines_folder
  404. QString_To_Py_Path(engineInfo.m_defaultProjectsFolder),
  405. QString_To_Py_Path(engineInfo.m_defaultGemsFolder),
  406. QString_To_Py_Path(engineInfo.m_defaultTemplatesFolder),
  407. pybind11::none(), // default_restricted_folder
  408. QString_To_Py_Path(engineInfo.m_thirdPartyPath)
  409. );
  410. if (registrationResult.cast<int>() != 0)
  411. {
  412. result = false;
  413. }
  414. });
  415. return result;
  416. }
  417. AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path, const QString& projectPath)
  418. {
  419. GemInfo gemInfo = GemInfoFromPath(QString_To_Py_String(path), QString_To_Py_Path(projectPath));
  420. if (gemInfo.IsValid())
  421. {
  422. return AZ::Success(AZStd::move(gemInfo));
  423. }
  424. else
  425. {
  426. return AZ::Failure();
  427. }
  428. }
  429. AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetEngineGemInfos()
  430. {
  431. QVector<GemInfo> gems;
  432. auto result = ExecuteWithLockErrorHandling([&]
  433. {
  434. for (auto path : m_manifest.attr("get_engine_gems")())
  435. {
  436. gems.push_back(GemInfoFromPath(path, pybind11::none()));
  437. }
  438. });
  439. if (!result.IsSuccess())
  440. {
  441. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  442. }
  443. std::sort(gems.begin(), gems.end());
  444. return AZ::Success(AZStd::move(gems));
  445. }
  446. AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetAllGemInfos(const QString& projectPath)
  447. {
  448. QVector<GemInfo> gems;
  449. auto result = ExecuteWithLockErrorHandling([&]
  450. {
  451. auto pyProjectPath = QString_To_Py_Path(projectPath);
  452. for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
  453. {
  454. GemInfo gemInfo = GemInfoFromPath(path, pyProjectPath);
  455. // Mark as downloaded because this gem was registered with an existing directory
  456. gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded;
  457. gems.push_back(AZStd::move(gemInfo));
  458. }
  459. });
  460. if (!result.IsSuccess())
  461. {
  462. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  463. }
  464. std::sort(gems.begin(), gems.end());
  465. return AZ::Success(AZStd::move(gems));
  466. }
  467. AZ::Outcome<QVector<AZStd::string>, AZStd::string> PythonBindings::GetEnabledGemNames(const QString& projectPath)
  468. {
  469. // Retrieve the path to the cmake file that lists the enabled gems.
  470. pybind11::str enabledGemsFilename;
  471. auto result = ExecuteWithLockErrorHandling([&]
  472. {
  473. enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")(
  474. pybind11::none(), // project_name
  475. QString_To_Py_Path(projectPath)); // project_path
  476. });
  477. if (!result.IsSuccess())
  478. {
  479. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  480. }
  481. // Retrieve the actual list of names from the cmake file.
  482. QVector<AZStd::string> gemNames;
  483. result = ExecuteWithLockErrorHandling([&]
  484. {
  485. const auto pyGemNames = m_cmake.attr("get_enabled_gems")(enabledGemsFilename);
  486. for (auto gemName : pyGemNames)
  487. {
  488. gemNames.push_back(Py_To_String(gemName));
  489. }
  490. });
  491. if (!result.IsSuccess())
  492. {
  493. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  494. }
  495. return AZ::Success(AZStd::move(gemNames));
  496. }
  497. AZ::Outcome<void, AZStd::string> PythonBindings::GemRegistration(const QString& gemPath, const QString& projectPath, bool remove)
  498. {
  499. bool registrationResult = false;
  500. auto result = ExecuteWithLockErrorHandling(
  501. [&]
  502. {
  503. auto externalProjectPath = projectPath.isEmpty() ? pybind11::none() : QString_To_Py_Path(projectPath);
  504. auto pythonRegistrationResult = m_register.attr("register")(
  505. pybind11::none(), // engine_path
  506. pybind11::none(), // project_path
  507. QString_To_Py_Path(gemPath), // gem folder
  508. pybind11::none(), // external subdirectory
  509. pybind11::none(), // template_path
  510. pybind11::none(), // restricted folder
  511. pybind11::none(), // repo uri
  512. pybind11::none(), // default_engines_folder
  513. pybind11::none(), // default_projects_folder
  514. pybind11::none(), // default_gems_folder
  515. pybind11::none(), // default_templates_folder
  516. pybind11::none(), // default_restricted_folder
  517. pybind11::none(), // default_third_party_folder
  518. pybind11::none(), // external_subdir_engine_path
  519. externalProjectPath, // external_subdir_project_path
  520. remove // remove
  521. );
  522. // Returns an exit code so boolify it then invert result
  523. registrationResult = !pythonRegistrationResult.cast<bool>();
  524. });
  525. if (!result.IsSuccess())
  526. {
  527. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  528. }
  529. else if (!registrationResult)
  530. {
  531. return AZ::Failure<AZStd::string>(AZStd::string::format(
  532. "Failed to %s gem path %s", remove ? "unregister" : "register", gemPath.toUtf8().constData()));
  533. }
  534. return AZ::Success();
  535. }
  536. AZ::Outcome<void, AZStd::string> PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath)
  537. {
  538. return GemRegistration(gemPath, projectPath);
  539. }
  540. AZ::Outcome<void, AZStd::string> PythonBindings::UnregisterGem(const QString& gemPath, const QString& projectPath)
  541. {
  542. return GemRegistration(gemPath, projectPath, /*remove*/true);
  543. }
  544. bool PythonBindings::AddProject(const QString& path)
  545. {
  546. bool registrationResult = false;
  547. bool result = ExecuteWithLock(
  548. [&]
  549. {
  550. auto projectPath = QString_To_Py_Path(path);
  551. auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath);
  552. // Returns an exit code so boolify it then invert result
  553. registrationResult = !pythonRegistrationResult.cast<bool>();
  554. });
  555. return result && registrationResult;
  556. }
  557. bool PythonBindings::RemoveProject(const QString& path)
  558. {
  559. bool registrationResult = false;
  560. bool result = ExecuteWithLock(
  561. [&]
  562. {
  563. auto pythonRegistrationResult = m_register.attr("register")(
  564. pybind11::none(), // engine_path
  565. QString_To_Py_Path(path), // project_path
  566. pybind11::none(), // gem_path
  567. pybind11::none(), // external_subdir_path
  568. pybind11::none(), // template_path
  569. pybind11::none(), // restricted_path
  570. pybind11::none(), // repo_uri
  571. pybind11::none(), // default_engines_folder
  572. pybind11::none(), // default_projects_folder
  573. pybind11::none(), // default_gems_folder
  574. pybind11::none(), // default_templates_folder
  575. pybind11::none(), // default_restricted_folder
  576. pybind11::none(), // default_third_party_folder
  577. pybind11::none(), // external_subdir_engine_path
  578. pybind11::none(), // external_subdir_project_path
  579. true, // remove
  580. false // force
  581. );
  582. // Returns an exit code so boolify it then invert result
  583. registrationResult = !pythonRegistrationResult.cast<bool>();
  584. });
  585. return result && registrationResult;
  586. }
  587. AZ::Outcome<ProjectInfo> PythonBindings::CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo)
  588. {
  589. ProjectInfo createdProjectInfo;
  590. bool result = ExecuteWithLock([&] {
  591. auto projectPath = QString_To_Py_Path(projectInfo.m_path);
  592. auto createProjectResult = m_engineTemplate.attr("create_project")(
  593. projectPath,
  594. QString_To_Py_String(projectInfo.m_projectName), // project_path
  595. QString_To_Py_Path(projectTemplatePath) // template_path
  596. );
  597. if (createProjectResult.cast<int>() == 0)
  598. {
  599. createdProjectInfo = ProjectInfoFromPath(projectPath);
  600. }
  601. });
  602. if (!result || !createdProjectInfo.IsValid())
  603. {
  604. return AZ::Failure();
  605. }
  606. else
  607. {
  608. return AZ::Success(AZStd::move(createdProjectInfo));
  609. }
  610. }
  611. AZ::Outcome<ProjectInfo> PythonBindings::GetProject(const QString& path)
  612. {
  613. ProjectInfo projectInfo = ProjectInfoFromPath(QString_To_Py_Path(path));
  614. if (projectInfo.IsValid())
  615. {
  616. return AZ::Success(AZStd::move(projectInfo));
  617. }
  618. else
  619. {
  620. return AZ::Failure();
  621. }
  622. }
  623. GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
  624. {
  625. GemInfo gemInfo;
  626. gemInfo.m_path = Py_To_String(path);
  627. gemInfo.m_directoryLink = gemInfo.m_path;
  628. auto data = m_manifest.attr("get_gem_json_data")(pybind11::none(), path, pyProjectPath);
  629. if (pybind11::isinstance<pybind11::dict>(data))
  630. {
  631. try
  632. {
  633. // required
  634. gemInfo.m_name = Py_To_String(data["gem_name"]);
  635. // optional
  636. gemInfo.m_displayName = Py_To_String_Optional(data, "display_name", gemInfo.m_name);
  637. gemInfo.m_summary = Py_To_String_Optional(data, "summary", "");
  638. gemInfo.m_version = Py_To_String_Optional(data, "version", gemInfo.m_version);
  639. gemInfo.m_lastUpdatedDate = Py_To_String_Optional(data, "last_updated", gemInfo.m_lastUpdatedDate);
  640. gemInfo.m_binarySizeInKB = Py_To_Int_Optional(data, "binary_size", gemInfo.m_binarySizeInKB);
  641. gemInfo.m_requirement = Py_To_String_Optional(data, "requirements", "");
  642. gemInfo.m_creator = Py_To_String_Optional(data, "origin", "");
  643. gemInfo.m_documentationLink = Py_To_String_Optional(data, "documentation_url", "");
  644. gemInfo.m_licenseText = Py_To_String_Optional(data, "license", "Unspecified License");
  645. gemInfo.m_licenseLink = Py_To_String_Optional(data, "license_url", "");
  646. gemInfo.m_repoUri = Py_To_String_Optional(data, "repo_uri", "");
  647. if (gemInfo.m_creator.contains("Open 3D Engine"))
  648. {
  649. gemInfo.m_gemOrigin = GemInfo::GemOrigin::Open3DEngine;
  650. }
  651. else if (gemInfo.m_creator.contains("Amazon Web Services"))
  652. {
  653. gemInfo.m_gemOrigin = GemInfo::GemOrigin::Local;
  654. }
  655. else if (data.contains("origin"))
  656. {
  657. gemInfo.m_gemOrigin = GemInfo::GemOrigin::Remote;
  658. }
  659. // If no origin was provided this cannot be remote and would be specified if O3DE so it should be local
  660. else
  661. {
  662. gemInfo.m_gemOrigin = GemInfo::GemOrigin::Local;
  663. }
  664. // As long Base Open3DEngine gems are installed before first startup non-remote gems will be downloaded
  665. if (gemInfo.m_gemOrigin != GemInfo::GemOrigin::Remote)
  666. {
  667. gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded;
  668. }
  669. if (data.contains("user_tags"))
  670. {
  671. for (auto tag : data["user_tags"])
  672. {
  673. gemInfo.m_features.push_back(Py_To_String(tag));
  674. }
  675. }
  676. if (data.contains("dependencies"))
  677. {
  678. for (auto dependency : data["dependencies"])
  679. {
  680. gemInfo.m_dependencies.push_back(Py_To_String(dependency));
  681. }
  682. }
  683. QString gemType = Py_To_String_Optional(data, "type", "");
  684. if (gemType == "Asset")
  685. {
  686. gemInfo.m_types |= GemInfo::Type::Asset;
  687. }
  688. if (gemType == "Code")
  689. {
  690. gemInfo.m_types |= GemInfo::Type::Code;
  691. }
  692. if (gemType == "Tool")
  693. {
  694. gemInfo.m_types |= GemInfo::Type::Tool;
  695. }
  696. }
  697. catch ([[maybe_unused]] const std::exception& e)
  698. {
  699. AZ_Warning("PythonBindings", false, "Failed to get GemInfo for gem %s", Py_To_String(path));
  700. }
  701. }
  702. return gemInfo;
  703. }
  704. ProjectInfo PythonBindings::ProjectInfoFromPath(pybind11::handle path)
  705. {
  706. ProjectInfo projectInfo;
  707. projectInfo.m_path = Py_To_String(path);
  708. projectInfo.m_needsBuild = false;
  709. auto projectData = m_manifest.attr("get_project_json_data")(pybind11::none(), path);
  710. if (pybind11::isinstance<pybind11::dict>(projectData))
  711. {
  712. try
  713. {
  714. projectInfo.m_projectName = Py_To_String(projectData["project_name"]);
  715. projectInfo.m_displayName = Py_To_String_Optional(projectData, "display_name", projectInfo.m_projectName);
  716. projectInfo.m_id = Py_To_String_Optional(projectData, "project_id", projectInfo.m_id);
  717. projectInfo.m_origin = Py_To_String_Optional(projectData, "origin", projectInfo.m_origin);
  718. projectInfo.m_summary = Py_To_String_Optional(projectData, "summary", projectInfo.m_summary);
  719. projectInfo.m_iconPath = Py_To_String_Optional(projectData, "icon", ProjectPreviewImagePath);
  720. if (projectData.contains("user_tags"))
  721. {
  722. for (auto tag : projectData["user_tags"])
  723. {
  724. projectInfo.m_userTags.append(Py_To_String(tag));
  725. }
  726. }
  727. }
  728. catch ([[maybe_unused]] const std::exception& e)
  729. {
  730. AZ_Warning("PythonBindings", false, "Failed to get ProjectInfo for project %s", Py_To_String(path));
  731. }
  732. }
  733. return projectInfo;
  734. }
  735. AZ::Outcome<QVector<ProjectInfo>> PythonBindings::GetProjects()
  736. {
  737. QVector<ProjectInfo> projects;
  738. bool result = ExecuteWithLock([&] {
  739. // external projects
  740. for (auto path : m_manifest.attr("get_projects")())
  741. {
  742. projects.push_back(ProjectInfoFromPath(path));
  743. }
  744. // projects from the engine
  745. for (auto path : m_manifest.attr("get_engine_projects")())
  746. {
  747. projects.push_back(ProjectInfoFromPath(path));
  748. }
  749. });
  750. if (!result)
  751. {
  752. return AZ::Failure();
  753. }
  754. else
  755. {
  756. return AZ::Success(AZStd::move(projects));
  757. }
  758. }
  759. AZ::Outcome<void, AZStd::string> PythonBindings::AddGemToProject(const QString& gemPath, const QString& projectPath)
  760. {
  761. return ExecuteWithLockErrorHandling([&]
  762. {
  763. m_enableGemProject.attr("enable_gem_in_project")(
  764. pybind11::none(), // gem name not needed as path is provided
  765. QString_To_Py_Path(gemPath),
  766. pybind11::none(), // project name not needed as path is provided
  767. QString_To_Py_Path(projectPath)
  768. );
  769. });
  770. }
  771. AZ::Outcome<void, AZStd::string> PythonBindings::RemoveGemFromProject(const QString& gemPath, const QString& projectPath)
  772. {
  773. return ExecuteWithLockErrorHandling([&]
  774. {
  775. m_disableGemProject.attr("disable_gem_in_project")(
  776. pybind11::none(), // gem name not needed as path is provided
  777. QString_To_Py_Path(gemPath),
  778. pybind11::none(), // project name not needed as path is provided
  779. QString_To_Py_Path(projectPath)
  780. );
  781. });
  782. }
  783. bool PythonBindings::RemoveInvalidProjects()
  784. {
  785. bool removalResult = false;
  786. bool result = ExecuteWithLock(
  787. [&]
  788. {
  789. auto pythonRemovalResult = m_register.attr("remove_invalid_o3de_projects")();
  790. // Returns an exit code so boolify it then invert result
  791. removalResult = !pythonRemovalResult.cast<bool>();
  792. });
  793. return result && removalResult;
  794. }
  795. AZ::Outcome<void, AZStd::string> PythonBindings::UpdateProject(const ProjectInfo& projectInfo)
  796. {
  797. bool updateProjectSucceeded = false;
  798. auto result = ExecuteWithLockErrorHandling([&]
  799. {
  800. std::list<std::string> newTags;
  801. for (const auto& i : projectInfo.m_userTags)
  802. {
  803. newTags.push_back(i.toStdString());
  804. }
  805. auto editResult = m_editProjectProperties.attr("edit_project_props")(
  806. QString_To_Py_Path(projectInfo.m_path),
  807. pybind11::none(), // proj_name not used
  808. QString_To_Py_String(projectInfo.m_projectName),
  809. QString_To_Py_String(projectInfo.m_id),
  810. QString_To_Py_String(projectInfo.m_origin),
  811. QString_To_Py_String(projectInfo.m_displayName),
  812. QString_To_Py_String(projectInfo.m_summary),
  813. QString_To_Py_String(projectInfo.m_iconPath), // new_icon
  814. pybind11::none(), // add_tags not used
  815. pybind11::none(), // remove_tags not used
  816. pybind11::list(pybind11::cast(newTags)));
  817. updateProjectSucceeded = (editResult.cast<int>() == 0);
  818. });
  819. if (!result.IsSuccess())
  820. {
  821. return result;
  822. }
  823. else if (!updateProjectSucceeded)
  824. {
  825. return AZ::Failure<AZStd::string>("Failed to update project.");
  826. }
  827. return AZ::Success();
  828. }
  829. ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
  830. {
  831. ProjectTemplateInfo templateInfo;
  832. templateInfo.m_path = Py_To_String(path);
  833. auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath);
  834. if (pybind11::isinstance<pybind11::dict>(data))
  835. {
  836. try
  837. {
  838. // required
  839. templateInfo.m_displayName = Py_To_String(data["display_name"]);
  840. templateInfo.m_name = Py_To_String(data["template_name"]);
  841. templateInfo.m_summary = Py_To_String(data["summary"]);
  842. // optional
  843. if (data.contains("canonical_tags"))
  844. {
  845. for (auto tag : data["canonical_tags"])
  846. {
  847. templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
  848. }
  849. }
  850. if (data.contains("user_tags"))
  851. {
  852. for (auto tag : data["user_tags"])
  853. {
  854. templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
  855. }
  856. }
  857. QString templateProjectPath = QDir(templateInfo.m_path).filePath("Template");
  858. auto enabledGemNames = GetEnabledGemNames(templateProjectPath);
  859. if (enabledGemNames)
  860. {
  861. for (auto gem : enabledGemNames.GetValue())
  862. {
  863. // Exclude the template ${Name} placeholder for the list of included gems
  864. // That Gem gets created with the project
  865. if (!gem.contains("${Name}"))
  866. {
  867. templateInfo.m_includedGems.push_back(Py_To_String(gem.c_str()));
  868. }
  869. }
  870. }
  871. }
  872. catch ([[maybe_unused]] const std::exception& e)
  873. {
  874. AZ_Warning("PythonBindings", false, "Failed to get ProjectTemplateInfo for %s", Py_To_String(path));
  875. }
  876. }
  877. return templateInfo;
  878. }
  879. AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates(const QString& projectPath)
  880. {
  881. QVector<ProjectTemplateInfo> templates;
  882. bool result = ExecuteWithLock([&] {
  883. for (auto path : m_manifest.attr("get_templates_for_project_creation")())
  884. {
  885. templates.push_back(ProjectTemplateInfoFromPath(path, QString_To_Py_Path(projectPath)));
  886. }
  887. });
  888. if (!result)
  889. {
  890. return AZ::Failure();
  891. }
  892. else
  893. {
  894. return AZ::Success(AZStd::move(templates));
  895. }
  896. }
  897. AZ::Outcome<void, AZStd::string> PythonBindings::RefreshGemRepo(const QString& repoUri)
  898. {
  899. bool refreshResult = false;
  900. AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
  901. [&]
  902. {
  903. auto pyUri = QString_To_Py_String(repoUri);
  904. auto pythonRefreshResult = m_repo.attr("refresh_repo")(pyUri);
  905. // Returns an exit code so boolify it then invert result
  906. refreshResult = !pythonRefreshResult.cast<bool>();
  907. });
  908. if (!result.IsSuccess())
  909. {
  910. return result;
  911. }
  912. else if (!refreshResult)
  913. {
  914. return AZ::Failure<AZStd::string>("Failed to refresh repo.");
  915. }
  916. return AZ::Success();
  917. }
  918. bool PythonBindings::RefreshAllGemRepos()
  919. {
  920. bool refreshResult = false;
  921. bool result = ExecuteWithLock(
  922. [&]
  923. {
  924. auto pythonRefreshResult = m_repo.attr("refresh_repos")();
  925. // Returns an exit code so boolify it then invert result
  926. refreshResult = !pythonRefreshResult.cast<bool>();
  927. });
  928. return result && refreshResult;
  929. }
  930. AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> PythonBindings::AddGemRepo(const QString& repoUri)
  931. {
  932. bool registrationResult = false;
  933. bool result = ExecuteWithLock(
  934. [&]
  935. {
  936. auto pyUri = QString_To_Py_String(repoUri);
  937. auto pythonRegistrationResult = m_register.attr("register")(
  938. pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pyUri);
  939. // Returns an exit code so boolify it then invert result
  940. registrationResult = !pythonRegistrationResult.cast<bool>();
  941. });
  942. if (!result || !registrationResult)
  943. {
  944. return AZ::Failure<AZStd::pair<AZStd::string, AZStd::string>>(GetSimpleDetailedErrorPair());
  945. }
  946. return AZ::Success();
  947. }
  948. bool PythonBindings::RemoveGemRepo(const QString& repoUri)
  949. {
  950. bool registrationResult = false;
  951. bool result = ExecuteWithLock(
  952. [&]
  953. {
  954. auto pythonRegistrationResult = m_register.attr("register")(
  955. pybind11::none(), // engine_path
  956. pybind11::none(), // project_path
  957. pybind11::none(), // gem_path
  958. pybind11::none(), // external_subdir_path
  959. pybind11::none(), // template_path
  960. pybind11::none(), // restricted_path
  961. QString_To_Py_String(repoUri), // repo_uri
  962. pybind11::none(), // default_engines_folder
  963. pybind11::none(), // default_projects_folder
  964. pybind11::none(), // default_gems_folder
  965. pybind11::none(), // default_templates_folder
  966. pybind11::none(), // default_restricted_folder
  967. pybind11::none(), // default_third_party_folder
  968. pybind11::none(), // external_subdir_engine_path
  969. pybind11::none(), // external_subdir_project_path
  970. true, // remove
  971. false // force
  972. );
  973. // Returns an exit code so boolify it then invert result
  974. registrationResult = !pythonRegistrationResult.cast<bool>();
  975. });
  976. return result && registrationResult;
  977. }
  978. GemRepoInfo PythonBindings::GetGemRepoInfo(pybind11::handle repoUri)
  979. {
  980. GemRepoInfo gemRepoInfo;
  981. gemRepoInfo.m_repoUri = Py_To_String(repoUri);
  982. auto data = m_manifest.attr("get_repo_json_data")(repoUri);
  983. if (pybind11::isinstance<pybind11::dict>(data))
  984. {
  985. try
  986. {
  987. // required
  988. gemRepoInfo.m_repoUri = Py_To_String(data["repo_uri"]);
  989. gemRepoInfo.m_name = Py_To_String(data["repo_name"]);
  990. gemRepoInfo.m_creator = Py_To_String(data["origin"]);
  991. // optional
  992. gemRepoInfo.m_summary = Py_To_String_Optional(data, "summary", "No summary provided.");
  993. gemRepoInfo.m_additionalInfo = Py_To_String_Optional(data, "additional_info", "");
  994. auto repoPath = m_manifest.attr("get_repo_path")(repoUri);
  995. gemRepoInfo.m_path = gemRepoInfo.m_directoryLink = Py_To_String(repoPath);
  996. QString lastUpdated = Py_To_String_Optional(data, "last_updated", "");
  997. gemRepoInfo.m_lastUpdated = QDateTime::fromString(lastUpdated, RepoTimeFormat);
  998. if (data.contains("enabled"))
  999. {
  1000. gemRepoInfo.m_isEnabled = data["enabled"].cast<bool>();
  1001. }
  1002. else
  1003. {
  1004. gemRepoInfo.m_isEnabled = false;
  1005. }
  1006. if (data.contains("gems"))
  1007. {
  1008. for (auto gemPath : data["gems"])
  1009. {
  1010. gemRepoInfo.m_includedGemUris.push_back(Py_To_String(gemPath));
  1011. }
  1012. }
  1013. }
  1014. catch ([[maybe_unused]] const std::exception& e)
  1015. {
  1016. AZ_Warning("PythonBindings", false, "Failed to get GemRepoInfo for repo %s", Py_To_String(repoUri));
  1017. }
  1018. }
  1019. return gemRepoInfo;
  1020. }
  1021. //#define MOCK_GEM_REPO_INFO true
  1022. AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> PythonBindings::GetAllGemRepoInfos()
  1023. {
  1024. QVector<GemRepoInfo> gemRepos;
  1025. #ifndef MOCK_GEM_REPO_INFO
  1026. auto result = ExecuteWithLockErrorHandling(
  1027. [&]
  1028. {
  1029. for (auto repoUri : m_manifest.attr("get_repos")())
  1030. {
  1031. gemRepos.push_back(GetGemRepoInfo(repoUri));
  1032. }
  1033. });
  1034. if (!result.IsSuccess())
  1035. {
  1036. return AZ::Failure<AZStd::string>(result.GetError().c_str());
  1037. }
  1038. #else
  1039. GemRepoInfo mockJohnRepo("JohnCreates", "John Smith", QDateTime(QDate(2021, 8, 31), QTime(11, 57)), true);
  1040. mockJohnRepo.m_summary = "John's Summary. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitudin dapibus urna";
  1041. mockJohnRepo.m_repoUri = "https://github.com/o3de/o3de";
  1042. mockJohnRepo.m_additionalInfo = "John's additional info. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitu.";
  1043. gemRepos.push_back(mockJohnRepo);
  1044. GemRepoInfo mockJaneRepo("JanesGems", "Jane Doe", QDateTime(QDate(2021, 9, 10), QTime(18, 23)), false);
  1045. mockJaneRepo.m_summary = "Jane's Summary.";
  1046. mockJaneRepo.m_repoUri = "https://github.com/o3de/o3de.org";
  1047. gemRepos.push_back(mockJaneRepo);
  1048. #endif // MOCK_GEM_REPO_INFO
  1049. std::sort(gemRepos.begin(), gemRepos.end());
  1050. return AZ::Success(AZStd::move(gemRepos));
  1051. }
  1052. AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForRepo(const QString& repoUri)
  1053. {
  1054. QVector<GemInfo> gemInfos;
  1055. AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
  1056. [&]
  1057. {
  1058. auto pyUri = QString_To_Py_String(repoUri);
  1059. auto gemPaths = m_repo.attr("get_gem_json_paths_from_cached_repo")(pyUri);
  1060. if (pybind11::isinstance<pybind11::set>(gemPaths))
  1061. {
  1062. for (auto path : gemPaths)
  1063. {
  1064. GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
  1065. gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
  1066. gemInfos.push_back(gemInfo);
  1067. }
  1068. }
  1069. });
  1070. if (!result.IsSuccess())
  1071. {
  1072. return AZ::Failure(result.GetError());
  1073. }
  1074. return AZ::Success(AZStd::move(gemInfos));
  1075. }
  1076. AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForAllRepos()
  1077. {
  1078. QVector<GemInfo> gemInfos;
  1079. AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
  1080. [&]
  1081. {
  1082. auto gemPaths = m_repo.attr("get_gem_json_paths_from_all_cached_repos")();
  1083. if (pybind11::isinstance<pybind11::set>(gemPaths))
  1084. {
  1085. for (auto path : gemPaths)
  1086. {
  1087. GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
  1088. gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
  1089. gemInfos.push_back(gemInfo);
  1090. }
  1091. }
  1092. });
  1093. if (!result.IsSuccess())
  1094. {
  1095. return AZ::Failure(result.GetError());
  1096. }
  1097. return AZ::Success(AZStd::move(gemInfos));
  1098. }
  1099. AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> PythonBindings::DownloadGem(
  1100. const QString& gemName, std::function<void(int, int)> gemProgressCallback, bool force)
  1101. {
  1102. // This process is currently limited to download a single gem at a time.
  1103. bool downloadSucceeded = false;
  1104. m_requestCancelDownload = false;
  1105. auto result = ExecuteWithLockErrorHandling(
  1106. [&]
  1107. {
  1108. auto downloadResult = m_download.attr("download_gem")(
  1109. QString_To_Py_String(gemName), // gem name
  1110. pybind11::none(), // destination path
  1111. false, // skip auto register
  1112. force, // force overwrite
  1113. pybind11::cpp_function(
  1114. [this, gemProgressCallback](int bytesDownloaded, int totalBytes)
  1115. {
  1116. gemProgressCallback(bytesDownloaded, totalBytes);
  1117. return m_requestCancelDownload;
  1118. }) // Callback for download progress and cancelling
  1119. );
  1120. downloadSucceeded = (downloadResult.cast<int>() == 0);
  1121. });
  1122. if (!result.IsSuccess())
  1123. {
  1124. AZStd::pair<AZStd::string, AZStd::string> pythonRunError(result.GetError(), result.GetError());
  1125. return AZ::Failure<AZStd::pair<AZStd::string, AZStd::string>>(AZStd::move(pythonRunError));
  1126. }
  1127. else if (!downloadSucceeded)
  1128. {
  1129. return AZ::Failure<AZStd::pair<AZStd::string, AZStd::string>>(GetSimpleDetailedErrorPair());
  1130. }
  1131. return AZ::Success();
  1132. }
  1133. void PythonBindings::CancelDownload()
  1134. {
  1135. m_requestCancelDownload = true;
  1136. }
  1137. bool PythonBindings::IsGemUpdateAvaliable(const QString& gemName, const QString& lastUpdated)
  1138. {
  1139. bool updateAvaliableResult = false;
  1140. bool result = ExecuteWithLock(
  1141. [&]
  1142. {
  1143. auto pyGemName = QString_To_Py_String(gemName);
  1144. auto pyLastUpdated = QString_To_Py_String(lastUpdated);
  1145. auto pythonUpdateAvaliableResult = m_download.attr("is_o3de_gem_update_available")(pyGemName, pyLastUpdated);
  1146. updateAvaliableResult = pythonUpdateAvaliableResult.cast<bool>();
  1147. });
  1148. return result && updateAvaliableResult;
  1149. }
  1150. AZStd::pair<AZStd::string, AZStd::string> PythonBindings::GetSimpleDetailedErrorPair()
  1151. {
  1152. AZStd::string detailedString = m_pythonErrorStrings.size() == 1
  1153. ? ""
  1154. : AZStd::accumulate(m_pythonErrorStrings.begin(), m_pythonErrorStrings.end(), AZStd::string(""));
  1155. return AZStd::pair<AZStd::string, AZStd::string>(m_pythonErrorStrings.front(), detailedString);
  1156. }
  1157. void PythonBindings::AddErrorString(AZStd::string errorString)
  1158. {
  1159. m_pythonErrorStrings.push_back(errorString);
  1160. }
  1161. void PythonBindings::ClearErrorStrings()
  1162. {
  1163. m_pythonErrorStrings.clear();
  1164. }
  1165. }