ProjectUtils.cpp 35 KB

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