ProjectUtils.cpp 35 KB

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