ProjectUtils.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  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 <ProjectUtils.h>
  9. #include <ProjectManagerDefs.h>
  10. #include <ProjectManager_Traits_Platform.h>
  11. #include <PythonBindingsInterface.h>
  12. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  13. #include <AzCore/IO/Path/Path.h>
  14. #include <AzCore/std/chrono/chrono.h>
  15. #include <QFileDialog>
  16. #include <QDir>
  17. #include <QtMath>
  18. #include <QFileInfo>
  19. #include <QProcess>
  20. #include <QProcessEnvironment>
  21. #include <QGuiApplication>
  22. #include <QProgressDialog>
  23. #include <QSpacerItem>
  24. #include <QStandardPaths>
  25. #include <QGridLayout>
  26. #include <QTextEdit>
  27. #include <QByteArray>
  28. #include <QScrollBar>
  29. #include <QProgressBar>
  30. #include <QLabel>
  31. #include <QStandardPaths>
  32. namespace O3DE::ProjectManager
  33. {
  34. namespace ProjectUtils
  35. {
  36. static bool WarnDirectoryOverwrite(const QString& path, QWidget* parent)
  37. {
  38. if (!QDir(path).isEmpty())
  39. {
  40. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  41. parent, QObject::tr("Overwrite Directory"),
  42. QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"), QMessageBox::No | QMessageBox::Yes);
  43. if (warningResult != QMessageBox::Yes)
  44. {
  45. return false;
  46. }
  47. }
  48. return true;
  49. }
  50. static bool IsDirectoryDescedent(const QString& possibleAncestorPath, const QString& possibleDecedentPath)
  51. {
  52. QDir ancestor(possibleAncestorPath);
  53. QDir descendent(possibleDecedentPath);
  54. do
  55. {
  56. if (ancestor == descendent)
  57. {
  58. return true;
  59. }
  60. descendent.cdUp();
  61. } while (!descendent.isRoot());
  62. return false;
  63. }
  64. static bool SkipFilePaths(const QString& curPath, QStringList& skippedPaths, QStringList& deeperSkippedPaths)
  65. {
  66. bool skip = false;
  67. for (const QString& skippedPath : skippedPaths)
  68. {
  69. QString nativeSkippedPath = QDir::toNativeSeparators(skippedPath);
  70. QString firstSectionSkippedPath = nativeSkippedPath.section(QDir::separator(), 0, 0);
  71. if (curPath == firstSectionSkippedPath)
  72. {
  73. // We are at the end of the path to skip, so skip it
  74. if (nativeSkippedPath == firstSectionSkippedPath)
  75. {
  76. skippedPaths.removeAll(skippedPath);
  77. skip = true;
  78. break;
  79. }
  80. // Append the next section of the skipped path
  81. else
  82. {
  83. deeperSkippedPaths.append(nativeSkippedPath.section(QDir::separator(), 1));
  84. }
  85. }
  86. }
  87. return skip;
  88. }
  89. typedef AZStd::function<void(/*fileCount=*/int, /*totalSizeInBytes=*/int)> StatusFunction;
  90. static void RecursiveGetAllFiles(const QDir& directory, QStringList& skippedPaths, int& outFileCount, qint64& outTotalSizeInBytes, StatusFunction statusCallback)
  91. {
  92. const QStringList entries = directory.entryList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
  93. for (const QString& entryPath : entries)
  94. {
  95. const QString filePath = QDir::toNativeSeparators(QString("%1/%2").arg(directory.path()).arg(entryPath));
  96. QStringList deeperSkippedPaths;
  97. if (SkipFilePaths(entryPath, skippedPaths, deeperSkippedPaths))
  98. {
  99. continue;
  100. }
  101. QFileInfo fileInfo(filePath);
  102. if (fileInfo.isDir())
  103. {
  104. QDir subDirectory(filePath);
  105. RecursiveGetAllFiles(subDirectory, deeperSkippedPaths, outFileCount, outTotalSizeInBytes, statusCallback);
  106. }
  107. else
  108. {
  109. ++outFileCount;
  110. outTotalSizeInBytes += fileInfo.size();
  111. const int updateStatusEvery = 64;
  112. if (outFileCount % updateStatusEvery == 0)
  113. {
  114. statusCallback(outFileCount, static_cast<int>(outTotalSizeInBytes));
  115. }
  116. }
  117. }
  118. }
  119. static bool CopyDirectory(QProgressDialog* progressDialog,
  120. const QString& origPath,
  121. const QString& newPath,
  122. QStringList& skippedPaths,
  123. int filesToCopyCount,
  124. int& outNumCopiedFiles,
  125. qint64 totalSizeToCopy,
  126. qint64& outCopiedFileSize,
  127. bool& showIgnoreFileDialog)
  128. {
  129. QDir original(origPath);
  130. if (!original.exists())
  131. {
  132. return false;
  133. }
  134. for (const QString& directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
  135. {
  136. if (progressDialog && progressDialog->wasCanceled())
  137. {
  138. return false;
  139. }
  140. QStringList deeperSkippedPaths;
  141. if (SkipFilePaths(directory, skippedPaths, deeperSkippedPaths))
  142. {
  143. continue;
  144. }
  145. QString newDirectoryPath = newPath + QDir::separator() + directory;
  146. original.mkpath(newDirectoryPath);
  147. if (!CopyDirectory(progressDialog, origPath + QDir::separator() + directory, newDirectoryPath, deeperSkippedPaths,
  148. filesToCopyCount, outNumCopiedFiles, totalSizeToCopy, outCopiedFileSize, showIgnoreFileDialog))
  149. {
  150. return false;
  151. }
  152. }
  153. QLocale locale;
  154. const float progressDialogRangeHalf = progressDialog ? static_cast<float>(qFabs(progressDialog->maximum() - progressDialog->minimum()) * 0.5f) : 0.f;
  155. for (const QString& file : original.entryList(QDir::Files))
  156. {
  157. if (progressDialog && progressDialog->wasCanceled())
  158. {
  159. return false;
  160. }
  161. // Unused by this function but neccesary to pass in to SkipFilePaths
  162. QStringList deeperSkippedPaths;
  163. if (SkipFilePaths(file, skippedPaths, deeperSkippedPaths))
  164. {
  165. continue;
  166. }
  167. // Progress window update
  168. if (progressDialog)
  169. {
  170. // Weight in the number of already copied files as well as the copied bytes to get a better progress indication
  171. // for cases combining many small files and some really large files.
  172. const float normalizedNumFiles = static_cast<float>(outNumCopiedFiles) / filesToCopyCount;
  173. const float normalizedFileSize = static_cast<float>(outCopiedFileSize) / totalSizeToCopy;
  174. const int progress = static_cast<int>(normalizedNumFiles * progressDialogRangeHalf + normalizedFileSize * progressDialogRangeHalf);
  175. progressDialog->setValue(progress);
  176. const QString copiedFileSizeString = locale.formattedDataSize(outCopiedFileSize);
  177. const QString totalFileSizeString = locale.formattedDataSize(totalSizeToCopy);
  178. progressDialog->setLabelText(QString("Copying file %1 of %2 (%3 of %4) ...").arg(QString::number(outNumCopiedFiles),
  179. QString::number(filesToCopyCount),
  180. copiedFileSizeString,
  181. totalFileSizeString));
  182. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  183. }
  184. const QString toBeCopiedFilePath = origPath + QDir::separator() + file;
  185. const QString copyToFilePath = newPath + QDir::separator() + file;
  186. if (!QFile::copy(toBeCopiedFilePath, copyToFilePath))
  187. {
  188. // Let the user decide to ignore files that failed to copy or cancel the whole operation.
  189. if (showIgnoreFileDialog)
  190. {
  191. QMessageBox ignoreFileMessageBox;
  192. const QString text = QString("Cannot copy <b>%1</b>.<br><br>"
  193. "Source: %2<br>"
  194. "Destination: %3<br><br>"
  195. "Press <b>Yes</b> to ignore the file, <b>YesToAll</b> to ignore all upcoming non-copyable files or "
  196. "<b>Cancel</b> to abort duplicating the project.").arg(file, toBeCopiedFilePath, copyToFilePath);
  197. ignoreFileMessageBox.setModal(true);
  198. ignoreFileMessageBox.setWindowTitle("Cannot copy file");
  199. ignoreFileMessageBox.setText(text);
  200. ignoreFileMessageBox.setIcon(QMessageBox::Question);
  201. ignoreFileMessageBox.setStandardButtons(QMessageBox::YesToAll | QMessageBox::Yes | QMessageBox::Cancel);
  202. int ignoreFile = ignoreFileMessageBox.exec();
  203. if (ignoreFile == QMessageBox::YesToAll)
  204. {
  205. showIgnoreFileDialog = false;
  206. continue;
  207. }
  208. else if (ignoreFile == QMessageBox::Yes)
  209. {
  210. continue;
  211. }
  212. else
  213. {
  214. return false;
  215. }
  216. }
  217. }
  218. else
  219. {
  220. outNumCopiedFiles++;
  221. QFileInfo fileInfo(toBeCopiedFilePath);
  222. outCopiedFileSize += fileInfo.size();
  223. }
  224. }
  225. return true;
  226. }
  227. static bool ClearProjectBuildArtifactsAndCache(const QString& origPath, const QString& newPath, QWidget* parent)
  228. {
  229. QDir buildDirectory = QDir(newPath);
  230. if ((!buildDirectory.cd(ProjectBuildDirectoryName) || !DeleteProjectFiles(buildDirectory.path(), true))
  231. && QDir(origPath).cd(ProjectBuildDirectoryName))
  232. {
  233. QMessageBox::warning(
  234. parent,
  235. QObject::tr("Clear Build Artifacts"),
  236. QObject::tr("Build artifacts failed to delete for moved project. Please manually delete build directory at \"%1\"")
  237. .arg(buildDirectory.path()),
  238. QMessageBox::Close);
  239. return false;
  240. }
  241. QDir cacheDirectory = QDir(newPath);
  242. if ((!cacheDirectory.cd(ProjectCacheDirectoryName) || !DeleteProjectFiles(cacheDirectory.path(), true))
  243. && QDir(origPath).cd(ProjectCacheDirectoryName))
  244. {
  245. QMessageBox::warning(
  246. parent,
  247. QObject::tr("Clear Asset Cache"),
  248. QObject::tr("Asset cache failed to delete for moved project. Please manually delete cache directory at \"%1\"")
  249. .arg(cacheDirectory.path()),
  250. QMessageBox::Close);
  251. return false;
  252. }
  253. return false;
  254. }
  255. bool RegisterProject(const QString& path, QWidget* parent)
  256. {
  257. auto incompatibleObjectsResult = PythonBindingsInterface::Get()->GetProjectEngineIncompatibleObjects(path);
  258. AZStd::string errorTitle, generalError, detailedError;
  259. if (!incompatibleObjectsResult)
  260. {
  261. errorTitle = "Failed to check project compatibility";
  262. generalError = incompatibleObjectsResult.GetError().first;
  263. generalError.append("\nDo you still want to add this project?");
  264. detailedError = incompatibleObjectsResult.GetError().second;
  265. }
  266. else if (const auto& incompatibleObjects = incompatibleObjectsResult.GetValue(); !incompatibleObjects.isEmpty())
  267. {
  268. // provide a couple more user friendly error messages for uncommon cases
  269. if (incompatibleObjects.at(0).contains(EngineJsonFilename.data(), Qt::CaseInsensitive))
  270. {
  271. errorTitle = errorTitle.format("Failed to read %s", EngineJsonFilename.data());
  272. generalError = "The projects compatibility with this engine could not be checked because the engine.json could not be read";
  273. }
  274. else if (incompatibleObjects.at(0).contains(ProjectJsonFilename.data(), Qt::CaseInsensitive))
  275. {
  276. errorTitle = errorTitle.format("Invalid project, failed to read %s", ProjectJsonFilename.data());
  277. generalError = "The projects compatibility with this engine could not be checked because the project.json could not be read.";
  278. }
  279. else
  280. {
  281. // could be gems, apis or both
  282. errorTitle = "Project may not be compatible with this engine";
  283. generalError = incompatibleObjects.join("\n").toUtf8().constData();
  284. generalError.append("\nDo you still want to add this project?");
  285. }
  286. }
  287. if (!generalError.empty())
  288. {
  289. QMessageBox warningDialog(QMessageBox::Warning, errorTitle.c_str(), generalError.c_str(), QMessageBox::Yes | QMessageBox::No, parent);
  290. warningDialog.setDetailedText(detailedError.c_str());
  291. if(warningDialog.exec() == QMessageBox::No)
  292. {
  293. return false;
  294. }
  295. AZ_Warning("ProjectManager", false, "Proceeding with project registration after compatibility check failed.");
  296. }
  297. if (auto addProjectResult = PythonBindingsInterface::Get()->AddProject(path, /*force=*/true); !addProjectResult)
  298. {
  299. DisplayDetailedError(QObject::tr("Failed to add project"), addProjectResult, parent);
  300. AZ_Error("ProjectManager", false, "Failed to register project at path '%s'", path.toUtf8().constData());
  301. return false;
  302. }
  303. return true;
  304. }
  305. bool UnregisterProject(const QString& path, QWidget* parent)
  306. {
  307. if (auto result = PythonBindingsInterface::Get()->RemoveProject(path); !result)
  308. {
  309. DisplayDetailedError("Failed to unregister project", result, parent);
  310. return false;
  311. }
  312. return true;
  313. }
  314. bool CopyProjectDialog(const QString& origPath, ProjectInfo& newProjectInfo, QWidget* parent)
  315. {
  316. bool copyResult = false;
  317. QDir parentOrigDir(origPath);
  318. parentOrigDir.cdUp();
  319. QString newPath = QDir::toNativeSeparators(
  320. QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path()));
  321. if (!newPath.isEmpty())
  322. {
  323. newProjectInfo.m_path = newPath;
  324. if (!WarnDirectoryOverwrite(newPath, parent))
  325. {
  326. return false;
  327. }
  328. copyResult = CopyProject(origPath, newPath, parent);
  329. }
  330. return copyResult;
  331. }
  332. bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent, bool skipRegister, bool showProgress)
  333. {
  334. // Disallow copying from or into subdirectory
  335. if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
  336. {
  337. return false;
  338. }
  339. int filesToCopyCount = 0;
  340. qint64 totalSizeInBytes = 0;
  341. QStringList skippedPaths
  342. {
  343. ProjectBuildDirectoryName,
  344. ProjectCacheDirectoryName
  345. };
  346. QProgressDialog* progressDialog = nullptr;
  347. if (showProgress)
  348. {
  349. progressDialog = new QProgressDialog(parent);
  350. progressDialog->setAutoClose(true);
  351. progressDialog->setValue(0);
  352. progressDialog->setRange(0, 1000);
  353. progressDialog->setModal(true);
  354. progressDialog->setWindowTitle(QObject::tr("Copying project ..."));
  355. progressDialog->show();
  356. }
  357. QLocale locale;
  358. QStringList getFilesSkippedPaths(skippedPaths);
  359. RecursiveGetAllFiles(origPath, getFilesSkippedPaths, filesToCopyCount, totalSizeInBytes, [=](int fileCount, int sizeInBytes)
  360. {
  361. if (progressDialog)
  362. {
  363. // Create a human-readable version of the file size.
  364. const QString fileSizeString = locale.formattedDataSize(sizeInBytes);
  365. progressDialog->setLabelText(QString("%1 ... %2 %3, %4 %5.")
  366. .arg(QObject::tr("Indexing files"))
  367. .arg(QString::number(fileCount))
  368. .arg(QObject::tr("files found"))
  369. .arg(fileSizeString)
  370. .arg(QObject::tr("to copy")));
  371. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  372. }
  373. });
  374. int numFilesCopied = 0;
  375. qint64 copiedFileSize = 0;
  376. // Phase 1: Copy files
  377. bool showIgnoreFileDialog = true;
  378. QStringList copyFilesSkippedPaths(skippedPaths);
  379. bool success = CopyDirectory(progressDialog, origPath, newPath, copyFilesSkippedPaths, filesToCopyCount, numFilesCopied,
  380. totalSizeInBytes, copiedFileSize, showIgnoreFileDialog);
  381. if (success && !skipRegister)
  382. {
  383. // Phase 2: Register project
  384. success = RegisterProject(newPath);
  385. }
  386. if (!success)
  387. {
  388. if (progressDialog)
  389. {
  390. progressDialog->setLabelText(QObject::tr("Duplicating project failed/cancelled, removing already copied files ..."));
  391. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  392. }
  393. DeleteProjectFiles(newPath, true);
  394. }
  395. if (progressDialog)
  396. {
  397. progressDialog->deleteLater();
  398. }
  399. return success;
  400. }
  401. bool DeleteProjectFiles(const QString& path, bool force)
  402. {
  403. QDir projectDirectory(path);
  404. if (projectDirectory.exists())
  405. {
  406. // Check if there is an actual project here or just force it
  407. auto pythonBindingsPtr = PythonBindingsInterface::Get();
  408. if (pythonBindingsPtr)
  409. {
  410. // if we can obtain the python interface, then we will ONLY delete the folder
  411. // if its a real project, unless force is specified.
  412. AZ::Outcome<ProjectInfo> pInfo = pythonBindingsPtr->GetProject(path);
  413. if (force || pInfo.IsSuccess())
  414. {
  415. if (pInfo.IsSuccess())
  416. {
  417. //determine if we have a restricted directory to worry about
  418. if (!pInfo.GetValue().m_restricted.isEmpty())
  419. {
  420. QDir restrictedDirectory(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first());
  421. if (restrictedDirectory.cd("O3DE/Restricted/Projects") &&
  422. restrictedDirectory.cd(pInfo.GetValue().m_restricted) &&
  423. !restrictedDirectory.isEmpty())
  424. {
  425. restrictedDirectory.removeRecursively();
  426. }
  427. }
  428. }
  429. return projectDirectory.removeRecursively();
  430. }
  431. }
  432. else
  433. {
  434. // we don't have any python bindings available, we're likely in test mode.
  435. return projectDirectory.removeRecursively();
  436. }
  437. }
  438. return false;
  439. }
  440. bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool skipRegister, bool showProgress)
  441. {
  442. origPath = QDir::toNativeSeparators(origPath);
  443. newPath = QDir::toNativeSeparators(newPath);
  444. if (!WarnDirectoryOverwrite(newPath, parent) || (!skipRegister && !UnregisterProject(origPath)))
  445. {
  446. return false;
  447. }
  448. QDir newDirectory(newPath);
  449. if (!newDirectory.removeRecursively())
  450. {
  451. return false;
  452. }
  453. if (!newDirectory.rename(origPath, newPath))
  454. {
  455. // Likely failed because trying to move to another partition, try copying
  456. if (!CopyProject(origPath, newPath, parent, skipRegister, showProgress))
  457. {
  458. return false;
  459. }
  460. DeleteProjectFiles(origPath, true);
  461. }
  462. else
  463. {
  464. // If directoy rename succeeded then build and cache directories need to be deleted seperately
  465. ClearProjectBuildArtifactsAndCache(origPath, newPath, parent);
  466. }
  467. if (!skipRegister && !RegisterProject(newPath))
  468. {
  469. return false;
  470. }
  471. return true;
  472. }
  473. bool ReplaceProjectFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
  474. {
  475. QFileInfo original(origFile);
  476. if (original.exists())
  477. {
  478. if (interactive)
  479. {
  480. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  481. parent,
  482. QObject::tr("Overwrite File?"),
  483. QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
  484. QMessageBox::No | QMessageBox::Yes);
  485. if (warningResult == QMessageBox::No)
  486. {
  487. return false;
  488. }
  489. }
  490. if (!QFile::remove(origFile))
  491. {
  492. return false;
  493. }
  494. }
  495. if (!QFile::copy(newFile, origFile))
  496. {
  497. return false;
  498. }
  499. return true;
  500. }
  501. bool FindSupportedCompiler(const ProjectInfo& projectInfo, QWidget* parent)
  502. {
  503. auto findCompilerResult = FindSupportedCompilerForPlatform(projectInfo);
  504. if (!findCompilerResult.IsSuccess())
  505. {
  506. QMessageBox vsWarningMessage(parent);
  507. vsWarningMessage.setIcon(QMessageBox::Warning);
  508. vsWarningMessage.setWindowTitle(QObject::tr("Create Project"));
  509. // Makes link clickable
  510. vsWarningMessage.setTextFormat(Qt::RichText);
  511. vsWarningMessage.setText(findCompilerResult.GetError());
  512. vsWarningMessage.setStandardButtons(QMessageBox::Close);
  513. QSpacerItem* horizontalSpacer = new QSpacerItem(600, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
  514. QGridLayout* layout = reinterpret_cast<QGridLayout*>(vsWarningMessage.layout());
  515. layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
  516. vsWarningMessage.exec();
  517. }
  518. return findCompilerResult.IsSuccess();
  519. }
  520. ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
  521. {
  522. auto iter = s_ProjectManagerStringNames.find(screen);
  523. if (iter != s_ProjectManagerStringNames.end())
  524. {
  525. return iter.value();
  526. }
  527. return ProjectManagerScreen::Invalid;
  528. }
  529. AZ::Outcome<QString, QString> ExecuteCommandResultModalDialog(
  530. const QString& cmd,
  531. const QStringList& arguments,
  532. const QString& title)
  533. {
  534. QString resultOutput;
  535. QProcess execProcess;
  536. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  537. QProgressDialog dialog(title, QObject::tr("Cancel"), /*minimum=*/0, /*maximum=*/0);
  538. dialog.setMinimumWidth(500);
  539. dialog.setAutoClose(false);
  540. QProgressBar* bar = new QProgressBar(&dialog);
  541. bar->setTextVisible(false);
  542. bar->setMaximum(0); // infinite
  543. dialog.setBar(bar);
  544. QLabel* progressLabel = new QLabel(&dialog);
  545. QVBoxLayout* layout = new QVBoxLayout();
  546. // pre-fill the field with the title and command
  547. const QString commandOutput = QString("%1<br>%2 %3<br>").arg(title).arg(cmd).arg(arguments.join(' '));
  548. // replace the label with a scrollable text edit
  549. QTextEdit* detailTextEdit = new QTextEdit(commandOutput, &dialog);
  550. detailTextEdit->setReadOnly(true);
  551. layout->addWidget(detailTextEdit);
  552. layout->setMargin(0);
  553. progressLabel->setLayout(layout);
  554. progressLabel->setMinimumHeight(150);
  555. dialog.setLabel(progressLabel);
  556. auto readConnection = QObject::connect(&execProcess, &QProcess::readyReadStandardOutput,
  557. [&]()
  558. {
  559. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  560. bool autoScroll = scrollBar->value() == scrollBar->maximum();
  561. QString output = execProcess.readAllStandardOutput();
  562. detailTextEdit->append(output);
  563. resultOutput.append(output);
  564. if (autoScroll)
  565. {
  566. scrollBar->setValue(scrollBar->maximum());
  567. }
  568. });
  569. auto exitConnection = QObject::connect(&execProcess,
  570. QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
  571. [&](int exitCode, [[maybe_unused]] QProcess::ExitStatus exitStatus)
  572. {
  573. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  574. dialog.setMaximum(100);
  575. dialog.setValue(dialog.maximum());
  576. if (exitCode == 0 && scrollBar->value() == scrollBar->maximum())
  577. {
  578. dialog.close();
  579. }
  580. else
  581. {
  582. // keep the dialog open so the user can look at the output
  583. dialog.setCancelButtonText(QObject::tr("Continue"));
  584. }
  585. });
  586. execProcess.start(cmd, arguments);
  587. dialog.exec();
  588. QObject::disconnect(readConnection);
  589. QObject::disconnect(exitConnection);
  590. if (execProcess.state() == QProcess::Running)
  591. {
  592. execProcess.kill();
  593. return AZ::Failure(QObject::tr("Process for command '%1' was canceled").arg(cmd));
  594. }
  595. int resultCode = execProcess.exitCode();
  596. if (resultCode != 0)
  597. {
  598. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2").arg(cmd).arg(resultCode));
  599. }
  600. return AZ::Success(resultOutput);
  601. }
  602. AZ::Outcome<QString, QString> ExecuteCommandResult(
  603. const QString& cmd,
  604. const QStringList& arguments,
  605. int commandTimeoutSeconds /*= ProjectCommandLineTimeoutSeconds*/)
  606. {
  607. QProcess execProcess;
  608. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  609. execProcess.start(cmd, arguments);
  610. if (!execProcess.waitForStarted())
  611. {
  612. return AZ::Failure(QObject::tr("Unable to start process for command '%1'").arg(cmd));
  613. }
  614. if (!execProcess.waitForFinished(commandTimeoutSeconds * 1000 /* Milliseconds per second */))
  615. {
  616. return AZ::Failure(QObject::tr("Process for command '%1' timed out at %2 seconds").arg(cmd).arg(commandTimeoutSeconds));
  617. }
  618. int resultCode = execProcess.exitCode();
  619. QString resultOutput = execProcess.readAllStandardOutput();
  620. if (resultCode != 0)
  621. {
  622. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2) %3").arg(cmd).arg(resultCode).arg(resultOutput));
  623. }
  624. return AZ::Success(resultOutput);
  625. }
  626. AZ::Outcome<QString, QString> GetProjectBuildPath(const QString& projectPath)
  627. {
  628. auto registry = AZ::SettingsRegistry::Get();
  629. // the project_build_path should be in the user settings registry inside the project folder
  630. AZ::IO::FixedMaxPath projectUserPath(projectPath.toUtf8().constData());
  631. projectUserPath /= AZ::SettingsRegistryInterface::DevUserRegistryFolder;
  632. if (!QDir(projectUserPath.c_str()).exists())
  633. {
  634. return AZ::Failure(QObject::tr("Failed to find the user registry folder %1").arg(projectUserPath.c_str()));
  635. }
  636. AZ::SettingsRegistryInterface::Specializations specializations;
  637. if(!registry->MergeSettingsFolder(projectUserPath.Native(), specializations, AZ_TRAIT_OS_PLATFORM_CODENAME))
  638. {
  639. return AZ::Failure(QObject::tr("Failed to merge registry settings in user registry folder %1").arg(projectUserPath.c_str()));
  640. }
  641. AZ::IO::FixedMaxPath projectBuildPath;
  642. if (!registry->Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath))
  643. {
  644. return AZ::Failure(QObject::tr("No project build path setting was found in the user registry folder %1").arg(projectUserPath.c_str()));
  645. }
  646. return AZ::Success(QString(projectBuildPath.c_str()));
  647. }
  648. QString GetPythonExecutablePath(const QString& enginePath)
  649. {
  650. // append lib path to Python paths
  651. AZ::IO::FixedMaxPath libPath = enginePath.toUtf8().constData();
  652. libPath /= AZ::IO::FixedMaxPathString(AZ_TRAIT_PROJECT_MANAGER_PYTHON_EXECUTABLE_SUBPATH);
  653. libPath = libPath.LexicallyNormal();
  654. return QString(libPath.String().c_str());
  655. }
  656. QString GetDefaultProjectPath()
  657. {
  658. QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
  659. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  660. if (engineInfoResult.IsSuccess())
  661. {
  662. QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder));
  663. if (path.exists())
  664. {
  665. defaultPath = path.absolutePath();
  666. }
  667. }
  668. return defaultPath;
  669. }
  670. QString GetDefaultTemplatePath()
  671. {
  672. QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
  673. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  674. if (engineInfoResult.IsSuccess())
  675. {
  676. QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultTemplatesFolder));
  677. if (path.exists())
  678. {
  679. defaultPath = path.absolutePath();
  680. }
  681. }
  682. return defaultPath;
  683. }
  684. int DisplayDetailedError(
  685. const QString& title, const AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>>& outcome, QWidget* parent)
  686. {
  687. return DisplayDetailedError(title, outcome.GetError().first, outcome.GetError().second, parent);
  688. }
  689. int DisplayDetailedError(
  690. const QString& title,
  691. const AZStd::string& generalError,
  692. const AZStd::string& detailedError,
  693. QWidget* parent,
  694. QMessageBox::StandardButtons buttons)
  695. {
  696. if (!detailedError.empty())
  697. {
  698. QMessageBox errorDialog(parent);
  699. errorDialog.setIcon(QMessageBox::Critical);
  700. errorDialog.setWindowTitle(title);
  701. errorDialog.setText(generalError.c_str());
  702. errorDialog.setDetailedText(detailedError.c_str());
  703. errorDialog.setStandardButtons(buttons);
  704. return errorDialog.exec();
  705. }
  706. else
  707. {
  708. return QMessageBox::critical(parent, title, generalError.c_str(), buttons);
  709. }
  710. }
  711. int VersionCompare(const QString& a, const QString&b)
  712. {
  713. auto outcomeA = AZ::SemanticVersion::ParseFromString(a.toUtf8().constData());
  714. auto outcomeB = AZ::SemanticVersion::ParseFromString(b.toUtf8().constData());
  715. auto versionA = outcomeA ? outcomeA.GetValue() : AZ::SemanticVersion(0, 0, 0);
  716. auto versionB = outcomeB ? outcomeB.GetValue() : AZ::SemanticVersion(0, 0, 0);
  717. return AZ::SemanticVersion::Compare(versionA, versionB);
  718. }
  719. QString GetDependencyString(const QString& dependencyString)
  720. {
  721. using Dependency = AZ::Dependency<AZ::SemanticVersion::parts_count>;
  722. using Comparison = Dependency::Bound::Comparison;
  723. Dependency dependency;
  724. QString result;
  725. if(auto parseOutcome = dependency.ParseVersions({ dependencyString.toUtf8().constData() }); parseOutcome)
  726. {
  727. // dependency name
  728. result.append(dependency.GetName().c_str());
  729. if (const auto& bounds = dependency.GetBounds(); !bounds.empty())
  730. {
  731. // we only support a single specifier
  732. const auto& bound = bounds[0];
  733. Comparison comparison = bound.GetComparison();
  734. if (comparison == Comparison::GreaterThan)
  735. {
  736. result.append(QObject::tr(" versions greater than"));
  737. }
  738. else if (comparison == Comparison::LessThan)
  739. {
  740. result.append(QObject::tr(" versions less than"));
  741. }
  742. else if ((comparison& Comparison::TwiddleWakka) != Comparison::None)
  743. {
  744. // don't try to explain the twiddle wakka in short form
  745. result.append(QObject::tr(" versions ~="));
  746. }
  747. result.append(" ");
  748. result.append(bound.GetVersion().ToString().c_str());
  749. if ((comparison & Comparison::EqualTo) != Comparison::None)
  750. {
  751. if ((comparison & Comparison::GreaterThan) != Comparison::None)
  752. {
  753. result.append(QObject::tr(" or higher "));
  754. }
  755. else if ((comparison & Comparison::LessThan) != Comparison::None)
  756. {
  757. result.append(QObject::tr(" or lower "));
  758. }
  759. }
  760. }
  761. }
  762. return result;
  763. }
  764. void GetDependencyNameAndVersion(const QString& dependencyString, QString& objectName, Comparison& comparator, QString& version)
  765. {
  766. Dependency dependency;
  767. if (auto parseOutcome = dependency.ParseVersions({ dependencyString.toUtf8().constData() }); parseOutcome)
  768. {
  769. objectName = dependency.GetName().c_str();
  770. if (const auto& bounds = dependency.GetBounds(); !bounds.empty())
  771. {
  772. comparator = dependency.GetBounds().at(0).GetComparison();
  773. version = dependency.GetBounds().at(0).GetVersion().ToString().c_str();
  774. }
  775. }
  776. else
  777. {
  778. objectName = dependencyString;
  779. }
  780. }
  781. QString GetDependencyName(const QString& dependency)
  782. {
  783. QString objectName, version;
  784. ProjectUtils::Comparison comparator;
  785. GetDependencyNameAndVersion(dependency, objectName, comparator, version);
  786. return objectName;
  787. }
  788. } // namespace ProjectUtils
  789. } // namespace O3DE::ProjectManager