3
0

ProjectUtils.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  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. AZ::Outcome<ProjectInfo> pInfo = PythonBindingsInterface::Get()->GetProject(path);
  408. if (force || pInfo.IsSuccess())
  409. {
  410. //determine if we have a restricted directory to worry about
  411. if (!pInfo.GetValue().m_restricted.isEmpty())
  412. {
  413. QDir restrictedDirectory(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first());
  414. if (restrictedDirectory.cd("O3DE/Restricted/Projects") &&
  415. restrictedDirectory.cd(pInfo.GetValue().m_restricted) &&
  416. !restrictedDirectory.isEmpty())
  417. {
  418. restrictedDirectory.removeRecursively();
  419. }
  420. }
  421. return projectDirectory.removeRecursively();
  422. }
  423. }
  424. return false;
  425. }
  426. bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool skipRegister, bool showProgress)
  427. {
  428. origPath = QDir::toNativeSeparators(origPath);
  429. newPath = QDir::toNativeSeparators(newPath);
  430. if (!WarnDirectoryOverwrite(newPath, parent) || (!skipRegister && !UnregisterProject(origPath)))
  431. {
  432. return false;
  433. }
  434. QDir newDirectory(newPath);
  435. if (!newDirectory.removeRecursively())
  436. {
  437. return false;
  438. }
  439. if (!newDirectory.rename(origPath, newPath))
  440. {
  441. // Likely failed because trying to move to another partition, try copying
  442. if (!CopyProject(origPath, newPath, parent, skipRegister, showProgress))
  443. {
  444. return false;
  445. }
  446. DeleteProjectFiles(origPath, true);
  447. }
  448. else
  449. {
  450. // If directoy rename succeeded then build and cache directories need to be deleted seperately
  451. ClearProjectBuildArtifactsAndCache(origPath, newPath, parent);
  452. }
  453. if (!skipRegister && !RegisterProject(newPath))
  454. {
  455. return false;
  456. }
  457. return true;
  458. }
  459. bool ReplaceProjectFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
  460. {
  461. QFileInfo original(origFile);
  462. if (original.exists())
  463. {
  464. if (interactive)
  465. {
  466. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  467. parent,
  468. QObject::tr("Overwrite File?"),
  469. QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
  470. QMessageBox::No | QMessageBox::Yes);
  471. if (warningResult == QMessageBox::No)
  472. {
  473. return false;
  474. }
  475. }
  476. if (!QFile::remove(origFile))
  477. {
  478. return false;
  479. }
  480. }
  481. if (!QFile::copy(newFile, origFile))
  482. {
  483. return false;
  484. }
  485. return true;
  486. }
  487. bool FindSupportedCompiler(QWidget* parent)
  488. {
  489. auto findCompilerResult = FindSupportedCompilerForPlatform();
  490. if (!findCompilerResult.IsSuccess())
  491. {
  492. QMessageBox vsWarningMessage(parent);
  493. vsWarningMessage.setIcon(QMessageBox::Warning);
  494. vsWarningMessage.setWindowTitle(QObject::tr("Create Project"));
  495. // Makes link clickable
  496. vsWarningMessage.setTextFormat(Qt::RichText);
  497. vsWarningMessage.setText(findCompilerResult.GetError());
  498. vsWarningMessage.setStandardButtons(QMessageBox::Close);
  499. QSpacerItem* horizontalSpacer = new QSpacerItem(600, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
  500. QGridLayout* layout = reinterpret_cast<QGridLayout*>(vsWarningMessage.layout());
  501. layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
  502. vsWarningMessage.exec();
  503. }
  504. return findCompilerResult.IsSuccess();
  505. }
  506. ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
  507. {
  508. auto iter = s_ProjectManagerStringNames.find(screen);
  509. if (iter != s_ProjectManagerStringNames.end())
  510. {
  511. return iter.value();
  512. }
  513. return ProjectManagerScreen::Invalid;
  514. }
  515. AZ::Outcome<QString, QString> ExecuteCommandResultModalDialog(
  516. const QString& cmd,
  517. const QStringList& arguments,
  518. const QString& title)
  519. {
  520. QString resultOutput;
  521. QProcess execProcess;
  522. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  523. QProgressDialog dialog(title, QObject::tr("Cancel"), /*minimum=*/0, /*maximum=*/0);
  524. dialog.setMinimumWidth(500);
  525. dialog.setAutoClose(false);
  526. QProgressBar* bar = new QProgressBar(&dialog);
  527. bar->setTextVisible(false);
  528. bar->setMaximum(0); // infinite
  529. dialog.setBar(bar);
  530. QLabel* progressLabel = new QLabel(&dialog);
  531. QVBoxLayout* layout = new QVBoxLayout();
  532. // pre-fill the field with the title and command
  533. const QString commandOutput = QString("%1<br>%2 %3<br>").arg(title).arg(cmd).arg(arguments.join(' '));
  534. // replace the label with a scrollable text edit
  535. QTextEdit* detailTextEdit = new QTextEdit(commandOutput, &dialog);
  536. detailTextEdit->setReadOnly(true);
  537. layout->addWidget(detailTextEdit);
  538. layout->setMargin(0);
  539. progressLabel->setLayout(layout);
  540. progressLabel->setMinimumHeight(150);
  541. dialog.setLabel(progressLabel);
  542. auto readConnection = QObject::connect(&execProcess, &QProcess::readyReadStandardOutput,
  543. [&]()
  544. {
  545. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  546. bool autoScroll = scrollBar->value() == scrollBar->maximum();
  547. QString output = execProcess.readAllStandardOutput();
  548. detailTextEdit->append(output);
  549. resultOutput.append(output);
  550. if (autoScroll)
  551. {
  552. scrollBar->setValue(scrollBar->maximum());
  553. }
  554. });
  555. auto exitConnection = QObject::connect(&execProcess,
  556. QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
  557. [&](int exitCode, [[maybe_unused]] QProcess::ExitStatus exitStatus)
  558. {
  559. QScrollBar* scrollBar = detailTextEdit->verticalScrollBar();
  560. dialog.setMaximum(100);
  561. dialog.setValue(dialog.maximum());
  562. if (exitCode == 0 && scrollBar->value() == scrollBar->maximum())
  563. {
  564. dialog.close();
  565. }
  566. else
  567. {
  568. // keep the dialog open so the user can look at the output
  569. dialog.setCancelButtonText(QObject::tr("Continue"));
  570. }
  571. });
  572. execProcess.start(cmd, arguments);
  573. dialog.exec();
  574. QObject::disconnect(readConnection);
  575. QObject::disconnect(exitConnection);
  576. if (execProcess.state() == QProcess::Running)
  577. {
  578. execProcess.kill();
  579. return AZ::Failure(QObject::tr("Process for command '%1' was canceled").arg(cmd));
  580. }
  581. int resultCode = execProcess.exitCode();
  582. if (resultCode != 0)
  583. {
  584. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2").arg(cmd).arg(resultCode));
  585. }
  586. return AZ::Success(resultOutput);
  587. }
  588. AZ::Outcome<QString, QString> ExecuteCommandResult(
  589. const QString& cmd,
  590. const QStringList& arguments,
  591. int commandTimeoutSeconds /*= ProjectCommandLineTimeoutSeconds*/)
  592. {
  593. QProcess execProcess;
  594. execProcess.setProcessChannelMode(QProcess::MergedChannels);
  595. execProcess.start(cmd, arguments);
  596. if (!execProcess.waitForStarted())
  597. {
  598. return AZ::Failure(QObject::tr("Unable to start process for command '%1'").arg(cmd));
  599. }
  600. if (!execProcess.waitForFinished(commandTimeoutSeconds * 1000 /* Milliseconds per second */))
  601. {
  602. return AZ::Failure(QObject::tr("Process for command '%1' timed out at %2 seconds").arg(cmd).arg(commandTimeoutSeconds));
  603. }
  604. int resultCode = execProcess.exitCode();
  605. QString resultOutput = execProcess.readAllStandardOutput();
  606. if (resultCode != 0)
  607. {
  608. return AZ::Failure(QObject::tr("Process for command '%1' failed (result code %2) %3").arg(cmd).arg(resultCode).arg(resultOutput));
  609. }
  610. return AZ::Success(resultOutput);
  611. }
  612. AZ::Outcome<QString, QString> GetProjectBuildPath(const QString& projectPath)
  613. {
  614. auto registry = AZ::SettingsRegistry::Get();
  615. // the project_build_path should be in the user settings registry inside the project folder
  616. AZ::IO::FixedMaxPath projectUserPath(projectPath.toUtf8().constData());
  617. projectUserPath /= AZ::SettingsRegistryInterface::DevUserRegistryFolder;
  618. if (!QDir(projectUserPath.c_str()).exists())
  619. {
  620. return AZ::Failure(QObject::tr("Failed to find the user registry folder %1").arg(projectUserPath.c_str()));
  621. }
  622. AZ::SettingsRegistryInterface::Specializations specializations;
  623. if(!registry->MergeSettingsFolder(projectUserPath.Native(), specializations, AZ_TRAIT_OS_PLATFORM_CODENAME))
  624. {
  625. return AZ::Failure(QObject::tr("Failed to merge registry settings in user registry folder %1").arg(projectUserPath.c_str()));
  626. }
  627. AZ::IO::FixedMaxPath projectBuildPath;
  628. if (!registry->Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath))
  629. {
  630. return AZ::Failure(QObject::tr("No project build path setting was found in the user registry folder %1").arg(projectUserPath.c_str()));
  631. }
  632. return AZ::Success(QString(projectBuildPath.c_str()));
  633. }
  634. QString GetPythonExecutablePath(const QString& enginePath)
  635. {
  636. // append lib path to Python paths
  637. AZ::IO::FixedMaxPath libPath = enginePath.toUtf8().constData();
  638. libPath /= AZ::IO::FixedMaxPathString(AZ_TRAIT_PROJECT_MANAGER_PYTHON_EXECUTABLE_SUBPATH);
  639. libPath = libPath.LexicallyNormal();
  640. return QString(libPath.String().c_str());
  641. }
  642. QString GetDefaultProjectPath()
  643. {
  644. QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
  645. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  646. if (engineInfoResult.IsSuccess())
  647. {
  648. QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder));
  649. if (path.exists())
  650. {
  651. defaultPath = path.absolutePath();
  652. }
  653. }
  654. return defaultPath;
  655. }
  656. QString GetDefaultTemplatePath()
  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_defaultTemplatesFolder));
  663. if (path.exists())
  664. {
  665. defaultPath = path.absolutePath();
  666. }
  667. }
  668. return defaultPath;
  669. }
  670. int DisplayDetailedError(
  671. const QString& title, const AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>>& outcome, QWidget* parent)
  672. {
  673. return DisplayDetailedError(title, outcome.GetError().first, outcome.GetError().second, parent);
  674. }
  675. int DisplayDetailedError(
  676. const QString& title,
  677. const AZStd::string& generalError,
  678. const AZStd::string& detailedError,
  679. QWidget* parent,
  680. QMessageBox::StandardButtons buttons)
  681. {
  682. if (!detailedError.empty())
  683. {
  684. QMessageBox errorDialog(parent);
  685. errorDialog.setIcon(QMessageBox::Critical);
  686. errorDialog.setWindowTitle(title);
  687. errorDialog.setText(generalError.c_str());
  688. errorDialog.setDetailedText(detailedError.c_str());
  689. errorDialog.setStandardButtons(buttons);
  690. return errorDialog.exec();
  691. }
  692. else
  693. {
  694. return QMessageBox::critical(parent, title, generalError.c_str(), buttons);
  695. }
  696. }
  697. int VersionCompare(const QString& a, const QString&b)
  698. {
  699. auto outcomeA = AZ::SemanticVersion::ParseFromString(a.toUtf8().constData());
  700. auto outcomeB = AZ::SemanticVersion::ParseFromString(b.toUtf8().constData());
  701. auto versionA = outcomeA ? outcomeA.GetValue() : AZ::SemanticVersion(0, 0, 0);
  702. auto versionB = outcomeB ? outcomeB.GetValue() : AZ::SemanticVersion(0, 0, 0);
  703. return AZ::SemanticVersion::Compare(versionA, versionB);
  704. }
  705. QString GetDependencyString(const QString& dependencyString)
  706. {
  707. using Dependency = AZ::Dependency<AZ::SemanticVersion::parts_count>;
  708. using Comparison = Dependency::Bound::Comparison;
  709. Dependency dependency;
  710. QString result;
  711. if(auto parseOutcome = dependency.ParseVersions({ dependencyString.toUtf8().constData() }); parseOutcome)
  712. {
  713. // dependency name
  714. result.append(dependency.GetName().c_str());
  715. if (const auto& bounds = dependency.GetBounds(); !bounds.empty())
  716. {
  717. // we only support a single specifier
  718. const auto& bound = bounds[0];
  719. Comparison comparison = bound.GetComparison();
  720. if (comparison == Comparison::GreaterThan)
  721. {
  722. result.append(QObject::tr(" versions greater than"));
  723. }
  724. else if (comparison == Comparison::LessThan)
  725. {
  726. result.append(QObject::tr(" versions less than"));
  727. }
  728. else if ((comparison& Comparison::TwiddleWakka) != Comparison::None)
  729. {
  730. // don't try to explain the twiddle wakka in short form
  731. result.append(QObject::tr(" versions ~="));
  732. }
  733. result.append(" ");
  734. result.append(bound.GetVersion().ToString().c_str());
  735. if ((comparison & Comparison::EqualTo) != Comparison::None)
  736. {
  737. if ((comparison & Comparison::GreaterThan) != Comparison::None)
  738. {
  739. result.append(QObject::tr(" or higher "));
  740. }
  741. else if ((comparison & Comparison::LessThan) != Comparison::None)
  742. {
  743. result.append(QObject::tr(" or lower "));
  744. }
  745. }
  746. }
  747. }
  748. return result;
  749. }
  750. void GetDependencyNameAndVersion(const QString& dependencyString, QString& objectName, Comparison& comparator, QString& version)
  751. {
  752. Dependency dependency;
  753. if (auto parseOutcome = dependency.ParseVersions({ dependencyString.toUtf8().constData() }); parseOutcome)
  754. {
  755. objectName = dependency.GetName().c_str();
  756. if (const auto& bounds = dependency.GetBounds(); !bounds.empty())
  757. {
  758. comparator = dependency.GetBounds().at(0).GetComparison();
  759. version = dependency.GetBounds().at(0).GetVersion().ToString().c_str();
  760. }
  761. }
  762. else
  763. {
  764. objectName = dependencyString;
  765. }
  766. }
  767. QString GetDependencyName(const QString& dependency)
  768. {
  769. QString objectName, version;
  770. ProjectUtils::Comparison comparator;
  771. GetDependencyNameAndVersion(dependency, objectName, comparator, version);
  772. return objectName;
  773. }
  774. } // namespace ProjectUtils
  775. } // namespace O3DE::ProjectManager