ProjectUtils.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  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 <QFileDialog>
  14. #include <QDir>
  15. #include <QtMath>
  16. #include <QMessageBox>
  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 <AzCore/std/chrono/chrono.h>
  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->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 = static_cast<float>(qFabs(progressDialog->maximum() - progressDialog->minimum()) * 0.5f);
  153. for (const QString& file : original.entryList(QDir::Files))
  154. {
  155. if (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. {
  167. // Weight in the number of already copied files as well as the copied bytes to get a better progress indication
  168. // for cases combining many small files and some really large files.
  169. const float normalizedNumFiles = static_cast<float>(outNumCopiedFiles) / filesToCopyCount;
  170. const float normalizedFileSize = static_cast<float>(outCopiedFileSize) / totalSizeToCopy;
  171. const int progress = static_cast<int>(normalizedNumFiles * progressDialogRangeHalf + normalizedFileSize * progressDialogRangeHalf);
  172. progressDialog->setValue(progress);
  173. const QString copiedFileSizeString = locale.formattedDataSize(outCopiedFileSize);
  174. const QString totalFileSizeString = locale.formattedDataSize(totalSizeToCopy);
  175. progressDialog->setLabelText(QString("Copying file %1 of %2 (%3 of %4) ...").arg(QString::number(outNumCopiedFiles),
  176. QString::number(filesToCopyCount),
  177. copiedFileSizeString,
  178. totalFileSizeString));
  179. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  180. }
  181. const QString toBeCopiedFilePath = origPath + QDir::separator() + file;
  182. const QString copyToFilePath = newPath + QDir::separator() + file;
  183. if (!QFile::copy(toBeCopiedFilePath, copyToFilePath))
  184. {
  185. // Let the user decide to ignore files that failed to copy or cancel the whole operation.
  186. if (showIgnoreFileDialog)
  187. {
  188. QMessageBox ignoreFileMessageBox;
  189. const QString text = QString("Cannot copy <b>%1</b>.<br><br>"
  190. "Source: %2<br>"
  191. "Destination: %3<br><br>"
  192. "Press <b>Yes</b> to ignore the file, <b>YesToAll</b> to ignore all upcoming non-copyable files or "
  193. "<b>Cancel</b> to abort duplicating the project.").arg(file, toBeCopiedFilePath, copyToFilePath);
  194. ignoreFileMessageBox.setModal(true);
  195. ignoreFileMessageBox.setWindowTitle("Cannot copy file");
  196. ignoreFileMessageBox.setText(text);
  197. ignoreFileMessageBox.setIcon(QMessageBox::Question);
  198. ignoreFileMessageBox.setStandardButtons(QMessageBox::YesToAll | QMessageBox::Yes | QMessageBox::Cancel);
  199. int ignoreFile = ignoreFileMessageBox.exec();
  200. if (ignoreFile == QMessageBox::YesToAll)
  201. {
  202. showIgnoreFileDialog = false;
  203. continue;
  204. }
  205. else if (ignoreFile == QMessageBox::Yes)
  206. {
  207. continue;
  208. }
  209. else
  210. {
  211. return false;
  212. }
  213. }
  214. }
  215. else
  216. {
  217. outNumCopiedFiles++;
  218. QFileInfo fileInfo(toBeCopiedFilePath);
  219. outCopiedFileSize += fileInfo.size();
  220. }
  221. }
  222. return true;
  223. }
  224. static bool ClearProjectBuildArtifactsAndCache(const QString& origPath, const QString& newPath, QWidget* parent)
  225. {
  226. QDir buildDirectory = QDir(newPath);
  227. if ((!buildDirectory.cd(ProjectBuildDirectoryName) || !DeleteProjectFiles(buildDirectory.path(), true))
  228. && QDir(origPath).cd(ProjectBuildDirectoryName))
  229. {
  230. QMessageBox::warning(
  231. parent,
  232. QObject::tr("Clear Build Artifacts"),
  233. QObject::tr("Build artifacts failed to delete for moved project. Please manually delete build directory at \"%1\"")
  234. .arg(buildDirectory.path()),
  235. QMessageBox::Close);
  236. return false;
  237. }
  238. QDir cacheDirectory = QDir(newPath);
  239. if ((!cacheDirectory.cd(ProjectCacheDirectoryName) || !DeleteProjectFiles(cacheDirectory.path(), true))
  240. && QDir(origPath).cd(ProjectCacheDirectoryName))
  241. {
  242. QMessageBox::warning(
  243. parent,
  244. QObject::tr("Clear Asset Cache"),
  245. QObject::tr("Asset cache failed to delete for moved project. Please manually delete cache directory at \"%1\"")
  246. .arg(cacheDirectory.path()),
  247. QMessageBox::Close);
  248. return false;
  249. }
  250. return false;
  251. }
  252. bool AddProjectDialog(QWidget* parent)
  253. {
  254. QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory")));
  255. if (!path.isEmpty())
  256. {
  257. return RegisterProject(path);
  258. }
  259. return false;
  260. }
  261. bool RegisterProject(const QString& path)
  262. {
  263. return PythonBindingsInterface::Get()->AddProject(path);
  264. }
  265. bool UnregisterProject(const QString& path)
  266. {
  267. return PythonBindingsInterface::Get()->RemoveProject(path);
  268. }
  269. bool CopyProjectDialog(const QString& origPath, ProjectInfo& newProjectInfo, QWidget* parent)
  270. {
  271. bool copyResult = false;
  272. QDir parentOrigDir(origPath);
  273. parentOrigDir.cdUp();
  274. QString newPath = QDir::toNativeSeparators(
  275. QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path()));
  276. if (!newPath.isEmpty())
  277. {
  278. newProjectInfo.m_path = newPath;
  279. if (!WarnDirectoryOverwrite(newPath, parent))
  280. {
  281. return false;
  282. }
  283. copyResult = CopyProject(origPath, newPath, parent);
  284. }
  285. return copyResult;
  286. }
  287. bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent, bool skipRegister)
  288. {
  289. // Disallow copying from or into subdirectory
  290. if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
  291. {
  292. return false;
  293. }
  294. int filesToCopyCount = 0;
  295. qint64 totalSizeInBytes = 0;
  296. QStringList skippedPaths
  297. {
  298. ProjectBuildDirectoryName,
  299. ProjectCacheDirectoryName
  300. };
  301. QProgressDialog* progressDialog = new QProgressDialog(parent);
  302. progressDialog->setAutoClose(true);
  303. progressDialog->setValue(0);
  304. progressDialog->setRange(0, 1000);
  305. progressDialog->setModal(true);
  306. progressDialog->setWindowTitle(QObject::tr("Copying project ..."));
  307. progressDialog->show();
  308. QLocale locale;
  309. QStringList getFilesSkippedPaths(skippedPaths);
  310. RecursiveGetAllFiles(origPath, getFilesSkippedPaths, filesToCopyCount, totalSizeInBytes, [=](int fileCount, int sizeInBytes)
  311. {
  312. // Create a human-readable version of the file size.
  313. const QString fileSizeString = locale.formattedDataSize(sizeInBytes);
  314. progressDialog->setLabelText(QString("%1 ... %2 %3, %4 %5.")
  315. .arg(QObject::tr("Indexing files"))
  316. .arg(QString::number(fileCount))
  317. .arg(QObject::tr("files found"))
  318. .arg(fileSizeString)
  319. .arg(QObject::tr("to copy")));
  320. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  321. });
  322. int numFilesCopied = 0;
  323. qint64 copiedFileSize = 0;
  324. // Phase 1: Copy files
  325. bool showIgnoreFileDialog = true;
  326. QStringList copyFilesSkippedPaths(skippedPaths);
  327. bool success = CopyDirectory(progressDialog, origPath, newPath, copyFilesSkippedPaths, filesToCopyCount, numFilesCopied,
  328. totalSizeInBytes, copiedFileSize, showIgnoreFileDialog);
  329. if (success && !skipRegister)
  330. {
  331. // Phase 2: Register project
  332. success = RegisterProject(newPath);
  333. }
  334. if (!success)
  335. {
  336. progressDialog->setLabelText(QObject::tr("Duplicating project failed/cancelled, removing already copied files ..."));
  337. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  338. DeleteProjectFiles(newPath, true);
  339. }
  340. progressDialog->deleteLater();
  341. return success;
  342. }
  343. bool DeleteProjectFiles(const QString& path, bool force)
  344. {
  345. QDir projectDirectory(path);
  346. if (projectDirectory.exists())
  347. {
  348. // Check if there is an actual project here or just force it
  349. if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess())
  350. {
  351. return projectDirectory.removeRecursively();
  352. }
  353. }
  354. return false;
  355. }
  356. bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool skipRegister)
  357. {
  358. origPath = QDir::toNativeSeparators(origPath);
  359. newPath = QDir::toNativeSeparators(newPath);
  360. if (!WarnDirectoryOverwrite(newPath, parent) || (!skipRegister && !UnregisterProject(origPath)))
  361. {
  362. return false;
  363. }
  364. QDir newDirectory(newPath);
  365. if (!newDirectory.removeRecursively())
  366. {
  367. return false;
  368. }
  369. if (!newDirectory.rename(origPath, newPath))
  370. {
  371. // Likely failed because trying to move to another partition, try copying
  372. if (!CopyProject(origPath, newPath, parent))
  373. {
  374. return false;
  375. }
  376. DeleteProjectFiles(origPath, true);
  377. }
  378. else
  379. {
  380. // If directoy rename succeeded then build and cache directories need to be deleted seperately
  381. ClearProjectBuildArtifactsAndCache(origPath, newPath, parent);
  382. }
  383. if (!skipRegister && !RegisterProject(newPath))
  384. {
  385. return false;
  386. }
  387. return true;
  388. }
  389. bool ReplaceProjectFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
  390. {
  391. QFileInfo original(origFile);
  392. if (original.exists())
  393. {
  394. if (interactive)
  395. {
  396. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  397. parent,
  398. QObject::tr("Overwrite File?"),
  399. QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
  400. QMessageBox::No | QMessageBox::Yes);
  401. if (warningResult == QMessageBox::No)
  402. {
  403. return false;
  404. }
  405. }
  406. if (!QFile::remove(origFile))
  407. {
  408. return false;
  409. }
  410. }
  411. if (!QFile::copy(newFile, origFile))
  412. {
  413. return false;
  414. }
  415. return true;
  416. }
  417. bool FindSupportedCompiler(QWidget* parent)
  418. {
  419. auto findCompilerResult = FindSupportedCompilerForPlatform();
  420. if (!findCompilerResult.IsSuccess())
  421. {
  422. QMessageBox vsWarningMessage(parent);
  423. vsWarningMessage.setIcon(QMessageBox::Warning);
  424. vsWarningMessage.setWindowTitle(QObject::tr("Create Project"));
  425. // Makes link clickable
  426. vsWarningMessage.setTextFormat(Qt::RichText);
  427. vsWarningMessage.setText(findCompilerResult.GetError());
  428. vsWarningMessage.setStandardButtons(QMessageBox::Close);
  429. QSpacerItem* horizontalSpacer = new QSpacerItem(600, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
  430. QGridLayout* layout = reinterpret_cast<QGridLayout*>(vsWarningMessage.layout());
  431. layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
  432. vsWarningMessage.exec();
  433. }
  434. return findCompilerResult.IsSuccess();
  435. }
  436. ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
  437. {
  438. auto iter = s_ProjectManagerStringNames.find(screen);
  439. if (iter != s_ProjectManagerStringNames.end())
  440. {
  441. return iter.value();
  442. }
  443. return ProjectManagerScreen::Invalid;
  444. }
  445. AZ::Outcome<QString, QString> ExecuteCommandResultModalDialog(
  446. const QString& cmd,
  447. const QStringList& arguments,
  448. const QString& title)
  449. {
  450. QString resultOutput;
  451. QProcess execProcess;
  452. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  453. QProgressDialog dialog(title, QObject::tr("Cancel"), /*minimum=*/0, /*maximum=*/0);
  454. dialog.setMinimumWidth(500);
  455. dialog.setAutoClose(false);
  456. QProgressBar* bar = new QProgressBar(&dialog);
  457. bar->setTextVisible(false);
  458. bar->setMaximum(0); // infinite
  459. dialog.setBar(bar);
  460. QLabel* progressLabel = new QLabel(&dialog);
  461. QVBoxLayout* layout = new QVBoxLayout();
  462. // pre-fill the field with the title and command
  463. const QString commandOutput = QString("%1<br>%2 %3<br>").arg(title).arg(cmd).arg(arguments.join(' '));
  464. // replace the label with a scrollable text edit
  465. QTextEdit* detailTextEdit = new QTextEdit(commandOutput, &dialog);
  466. detailTextEdit->setReadOnly(true);
  467. layout->addWidget(detailTextEdit);
  468. layout->setMargin(0);
  469. progressLabel->setLayout(layout);
  470. progressLabel->setMinimumHeight(150);
  471. dialog.setLabel(progressLabel);
  472. auto readConnection = QObject::connect(&execProcess, &QProcess::readyReadStandardOutput,
  473. [&]()
  474. {
  475. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  476. bool autoScroll = scrollBar->value() == scrollBar->maximum();
  477. QString output = execProcess.readAllStandardOutput();
  478. detailTextEdit->append(output);
  479. resultOutput.append(output);
  480. if (autoScroll)
  481. {
  482. scrollBar->setValue(scrollBar->maximum());
  483. }
  484. });
  485. auto exitConnection = QObject::connect(&execProcess,
  486. QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
  487. [&](int exitCode, [[maybe_unused]] QProcess::ExitStatus exitStatus)
  488. {
  489. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  490. dialog.setMaximum(100);
  491. dialog.setValue(dialog.maximum());
  492. if (exitCode == 0 && scrollBar->value() == scrollBar->maximum())
  493. {
  494. dialog.close();
  495. }
  496. else
  497. {
  498. // keep the dialog open so the user can look at the output
  499. dialog.setCancelButtonText(QObject::tr("Continue"));
  500. }
  501. });
  502. execProcess.start(cmd, arguments);
  503. dialog.exec();
  504. QObject::disconnect(readConnection);
  505. QObject::disconnect(exitConnection);
  506. if (execProcess.state() == QProcess::Running)
  507. {
  508. execProcess.kill();
  509. return AZ::Failure(QObject::tr("Process for command '%1' was canceled").arg(cmd));
  510. }
  511. int resultCode = execProcess.exitCode();
  512. if (resultCode != 0)
  513. {
  514. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2").arg(cmd).arg(resultCode));
  515. }
  516. return AZ::Success(resultOutput);
  517. }
  518. AZ::Outcome<QString, QString> ExecuteCommandResult(
  519. const QString& cmd,
  520. const QStringList& arguments,
  521. int commandTimeoutSeconds /*= ProjectCommandLineTimeoutSeconds*/)
  522. {
  523. QProcess execProcess;
  524. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  525. execProcess.start(cmd, arguments);
  526. if (!execProcess.waitForStarted())
  527. {
  528. return AZ::Failure(QObject::tr("Unable to start process for command '%1'").arg(cmd));
  529. }
  530. if (!execProcess.waitForFinished(commandTimeoutSeconds * 1000 /* Milliseconds per second */))
  531. {
  532. return AZ::Failure(QObject::tr("Process for command '%1' timed out at %2 seconds").arg(cmd).arg(commandTimeoutSeconds));
  533. }
  534. int resultCode = execProcess.exitCode();
  535. QString resultOutput = execProcess.readAllStandardOutput();
  536. if (resultCode != 0)
  537. {
  538. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2) %3").arg(cmd).arg(resultCode).arg(resultOutput));
  539. }
  540. return AZ::Success(resultOutput);
  541. }
  542. AZ::Outcome<QString, QString> GetProjectBuildPath(const QString& projectPath)
  543. {
  544. auto registry = AZ::SettingsRegistry::Get();
  545. // the project_build_path should be in the user settings registry inside the project folder
  546. AZ::IO::FixedMaxPath projectUserPath(projectPath.toUtf8().constData());
  547. projectUserPath /= AZ::SettingsRegistryInterface::DevUserRegistryFolder;
  548. if (!QDir(projectUserPath.c_str()).exists())
  549. {
  550. return AZ::Failure(QObject::tr("Failed to find the user registry folder %1").arg(projectUserPath.c_str()));
  551. }
  552. AZ::SettingsRegistryInterface::Specializations specializations;
  553. if(!registry->MergeSettingsFolder(projectUserPath.Native(), specializations, AZ_TRAIT_OS_PLATFORM_CODENAME))
  554. {
  555. return AZ::Failure(QObject::tr("Failed to merge registry settings in user registry folder %1").arg(projectUserPath.c_str()));
  556. }
  557. AZ::IO::FixedMaxPath projectBuildPath;
  558. if (!registry->Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath))
  559. {
  560. return AZ::Failure(QObject::tr("No project build path setting was found in the user registry folder %1").arg(projectUserPath.c_str()));
  561. }
  562. return AZ::Success(QString(projectBuildPath.c_str()));
  563. }
  564. } // namespace ProjectUtils
  565. } // namespace O3DE::ProjectManager