ソースを参照

Project Manager Build Project from Projects Page (#1142)

* Added loading bar mode to project button

* Added ProjectBuilder files

* commmit current progress for project building

* Push current project building work

* Full build commands built out and message boxes for lots of situation

* Replaced defaultProjectImage placeholder

* Added installed cmake path to builder process env PATH
AMZN-nggieber 4 年 前
コミット
3b60bcc0f1

+ 2 - 2
Code/Tools/ProjectManager/Resources/DefaultProjectImage.png

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:f82f22df64b93d4bec91e56b60efa3d5ce2915ce388a2dc627f1ab720678e3d5
-size 334987
+oid sha256:4a5881b8d6cfbc4ceefb14ab96844484fe19407ee030824768f9fcce2f729d35
+size 2949

+ 13 - 0
Code/Tools/ProjectManager/Resources/ProjectManager.qss

@@ -362,6 +362,7 @@ QTabBar::tab:pressed
 #projectButton > #labelButton  {
     border:1px solid white;
 }
+
 #projectButton > #labelButton:hover,
 #projectButton > #labelButton:pressed  {
     border:1px solid #1e70eb;
@@ -401,6 +402,18 @@ QTabBar::tab:pressed
     max-height:278px;
 }
 
+QProgressBar {
+    border: none;
+    background-color: transparent;
+    padding: 0px;
+    min-height: 14px;
+    font-size: 2px;
+}
+
+QProgressBar::chunk {
+    background-color: #1E70EB;
+}
+
 /************** Gem Catalog **************/
 
 #GemCatalogTitle {

+ 2 - 0
Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp

@@ -206,6 +206,8 @@ namespace O3DE::ProjectManager
                 m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path);
 #endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
 
+                projectInfo.m_needsBuild = true;
+                emit NotifyBuildProject(projectInfo);
                 emit ChangeScreenRequest(ProjectManagerScreen::Projects);
             }
             else

+ 250 - 0
Code/Tools/ProjectManager/Source/ProjectBuilder.cpp

@@ -0,0 +1,250 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#include <ProjectBuilder.h>
+#include <ProjectButtonWidget.h>
+#include <PythonBindingsInterface.h>
+
+#include <QProcess>
+#include <QFile>
+#include <QTextStream>
+#include <QMessageBox>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QDir>
+#include <QProcessEnvironment>
+
+
+//#define MOCK_BUILD_PROJECT true 
+
+namespace O3DE::ProjectManager
+{
+    // 10 Minutes
+    constexpr int MaxBuildTimeMSecs = 600000;
+    static const QString BuildPathPostfix = "windows_vs2019";
+    static const QString ErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
+
+    ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
+        : QObject()
+        , m_projectInfo(projectInfo)
+    {
+    }
+
+    void ProjectBuilderWorker::BuildProject()
+    {
+#ifdef MOCK_BUILD_PROJECT
+        for (int i = 0; i < 10; ++i)
+        {
+            QThread::sleep(1);
+            UpdateProgress(i * 10);
+        }
+        Done(m_projectPath);
+#else
+        EngineInfo engineInfo;
+
+        AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
+        if (engineInfoResult.IsSuccess())
+        {
+            engineInfo = engineInfoResult.GetValue();
+        }
+        else
+        {
+            emit Done(tr("Failed to get engine info."));
+            return;
+        }
+
+        // Show some kind of progress with very approximate estimates
+        UpdateProgress(1);
+
+        QProcessEnvironment currentEnvironment(QProcessEnvironment::systemEnvironment());
+        // Append cmake path to PATH incase it is missing
+        QDir cmakePath(engineInfo.m_path);
+        cmakePath.cd("cmake/runtime/bin");
+        QString pathValue = currentEnvironment.value("PATH");
+        pathValue += ";" + cmakePath.path();
+        currentEnvironment.insert("PATH", pathValue);
+
+        QProcess configProjectProcess;
+        configProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
+        configProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
+        configProjectProcess.setProcessEnvironment(currentEnvironment);
+
+        configProjectProcess.start(
+            "cmake",
+            QStringList
+            {
+                "-B",
+                QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
+                "-S",
+                m_projectInfo.m_path,
+                "-G",
+                "Visual Studio 16",
+                "-DLY_3RDPARTY_PATH=" + engineInfo.m_thirdPartyPath
+            });
+
+        if (!configProjectProcess.waitForStarted())
+        {
+            emit Done(tr("Configuring project failed to start."));
+            return;
+        }
+        if (!configProjectProcess.waitForFinished(MaxBuildTimeMSecs))
+        {
+            WriteErrorLog(configProjectProcess.readAllStandardOutput());
+            emit Done(tr("Configuring project timed out. See log for details"));
+            return;
+        }
+
+        QString configProjectOutput(configProjectProcess.readAllStandardOutput());
+        if (configProjectProcess.exitCode() != 0 || !configProjectOutput.contains("Generating done"))
+        {
+            WriteErrorLog(configProjectOutput);
+            emit Done(tr("Configuring project failed. See log for details."));
+            return;
+        }
+
+        UpdateProgress(20);
+
+        QProcess buildProjectProcess;
+        buildProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
+        buildProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
+        buildProjectProcess.setProcessEnvironment(currentEnvironment);
+
+        buildProjectProcess.start(
+            "cmake",
+            QStringList
+            {
+                "--build",
+                QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
+                "--target",
+                m_projectInfo.m_projectName + ".GameLauncher",
+                "Editor",
+                "--config",
+                "profile"
+            });
+
+        if (!buildProjectProcess.waitForStarted())
+        {
+            emit Done(tr("Building project failed to start."));
+            return;
+        }
+        if (!buildProjectProcess.waitForFinished(MaxBuildTimeMSecs))
+        {
+            WriteErrorLog(configProjectProcess.readAllStandardOutput());
+            emit Done(tr("Building project timed out. See log for details"));
+            return;
+        }
+
+        QString buildProjectOutput(buildProjectProcess.readAllStandardOutput());
+        if (configProjectProcess.exitCode() != 0)
+        {
+            WriteErrorLog(buildProjectOutput);
+            emit Done(tr("Building project failed. See log for details."));
+        }
+        else
+        {
+            emit Done("");
+        }
+#endif
+    }
+
+    QString ProjectBuilderWorker::LogFilePath() const
+    {
+        QDir logFilePath(m_projectInfo.m_path);
+        logFilePath.cd(BuildPathPostfix);
+        return logFilePath.filePath(ErrorLogPathPostfix);
+    }
+
+    void ProjectBuilderWorker::WriteErrorLog(const QString& log)
+    {
+        QFile logFile(LogFilePath());
+        // Overwrite file with truncate
+        if (logFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
+        {
+            QTextStream output(&logFile);
+            output << log;
+            logFile.close();
+        }
+    }
+
+    ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent)
+        : QObject()
+        , m_projectInfo(projectInfo)
+        , m_projectButton(projectButton)
+        , m_parent(parent)
+    {
+        m_worker = new ProjectBuilderWorker(m_projectInfo);
+        m_worker->moveToThread(&m_workerThread);
+
+        connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater);
+        connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject);
+        connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults);
+        connect(m_worker, &ProjectBuilderWorker::UpdateProgress, this, &ProjectBuilderController::UpdateUIProgress);
+    }
+
+    ProjectBuilderController::~ProjectBuilderController()
+    {
+        m_workerThread.quit();
+        m_workerThread.wait();
+    }
+
+    void ProjectBuilderController::Start()
+    {
+        m_workerThread.start();
+        UpdateUIProgress(0);
+    }
+
+    void ProjectBuilderController::SetProjectButton(ProjectButton* projectButton)
+    {
+        m_projectButton = projectButton;
+    }
+
+    QString ProjectBuilderController::GetProjectPath() const
+    {
+        return m_projectInfo.m_path;
+    }
+
+    void ProjectBuilderController::UpdateUIProgress(int progress)
+    {
+        if (m_projectButton)
+        {
+            m_projectButton->SetButtonOverlayText(QString("%1 (%2%)\n\n").arg(tr("Building Project..."), QString::number(progress)));
+            m_projectButton->SetProgressBarValue(progress);
+        }
+    }
+
+    void ProjectBuilderController::HandleResults(const QString& result)
+    {
+        if (!result.isEmpty())
+        {
+            if (result.contains(tr("log")))
+            {
+                QMessageBox::StandardButton openLog = QMessageBox::critical(
+                    m_parent,
+                    tr("Project Failed to Build!"),
+                    result + tr("\n\nWould you like to view log?"),
+                    QMessageBox::No | QMessageBox::Yes);
+
+                if (openLog == QMessageBox::Yes)
+                {
+                    // Open application assigned to this file type
+                    QDesktopServices::openUrl(QUrl("file:///" + m_worker->LogFilePath()));
+                }
+            }
+            else
+            {
+                QMessageBox::critical(m_parent, tr("Project Failed to Build!"), result);
+            }
+        }
+
+        emit Done();
+    }
+} // namespace O3DE::ProjectManager

+ 73 - 0
Code/Tools/ProjectManager/Source/ProjectBuilder.h

@@ -0,0 +1,73 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <ProjectInfo.h>
+
+#include <QThread>
+#endif
+
+namespace O3DE::ProjectManager
+{
+    QT_FORWARD_DECLARE_CLASS(ProjectButton)
+
+    class ProjectBuilderWorker : public QObject
+    {
+        Q_OBJECT
+
+    public:
+        explicit ProjectBuilderWorker(const ProjectInfo& projectInfo);
+        ~ProjectBuilderWorker() = default;
+
+        QString LogFilePath() const;
+
+    public slots:
+        void BuildProject();
+
+    signals:
+        void UpdateProgress(int progress);
+        void Done(QString result);
+
+    private:
+        void WriteErrorLog(const QString& log);
+
+        ProjectInfo m_projectInfo;
+    };
+
+    class ProjectBuilderController : public QObject
+    {
+        Q_OBJECT
+
+    public:
+        explicit ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent = nullptr);
+        ~ProjectBuilderController();
+
+        void SetProjectButton(ProjectButton* projectButton);
+        QString GetProjectPath() const;
+
+    public slots:
+        void Start();
+        void UpdateUIProgress(int progress);
+        void HandleResults(const QString& result);
+
+    signals:
+        void Done();
+
+    private:
+        ProjectInfo m_projectInfo;
+        ProjectBuilderWorker* m_worker;
+        QThread m_workerThread;
+        ProjectButton* m_projectButton;
+        QWidget* m_parent;
+    };
+} // namespace O3DE::ProjectManager

+ 90 - 19
Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp

@@ -21,6 +21,7 @@
 #include <QPixmap>
 #include <QMenu>
 #include <QSpacerItem>
+#include <QProgressBar>
 
 namespace O3DE::ProjectManager
 {
@@ -31,11 +32,26 @@ namespace O3DE::ProjectManager
         : QLabel(parent)
     {
         setObjectName("labelButton");
+
+        QVBoxLayout* vLayout = new QVBoxLayout(this);
+        vLayout->setContentsMargins(0, 0, 0, 0);
+        vLayout->setSpacing(5);
+
+        setLayout(vLayout);
         m_overlayLabel = new QLabel("", this);
         m_overlayLabel->setObjectName("labelButtonOverlay");
         m_overlayLabel->setWordWrap(true);
         m_overlayLabel->setAlignment(Qt::AlignCenter);
         m_overlayLabel->setVisible(false);
+        vLayout->addWidget(m_overlayLabel);
+
+        m_buildButton = new QPushButton(tr("Build Project"), this);
+        m_buildButton->setVisible(false);
+
+        m_progressBar = new QProgressBar(this);
+        m_progressBar->setObjectName("labelButtonProgressBar");
+        m_progressBar->setVisible(false);
+        vLayout->addWidget(m_progressBar);
     }
 
     void LabelButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
@@ -57,7 +73,22 @@ namespace O3DE::ProjectManager
         m_overlayLabel->setText(text);
     }
 
-    ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent)
+    QLabel* LabelButton::GetOverlayLabel()
+    {
+        return m_overlayLabel;
+    }
+
+    QProgressBar* LabelButton::GetProgressBar()
+    {
+        return m_progressBar;
+    }
+
+    QPushButton* LabelButton::GetBuildButton()
+    {
+        return m_buildButton;
+    }
+
+    ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent, bool processing)
         : QFrame(parent)
         , m_projectInfo(projectInfo)
     {
@@ -66,10 +97,18 @@ namespace O3DE::ProjectManager
             m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
         }
 
-        Setup();
+        BaseSetup();
+        if (processing)
+        {
+            ProcessingSetup();
+        }
+        else
+        {
+            ReadySetup();
+        }
     }
 
-    void ProjectButton::Setup()
+    void ProjectButton::BaseSetup()
     {
         setObjectName("projectButton");
 
@@ -87,8 +126,37 @@ namespace O3DE::ProjectManager
         m_projectImageLabel->setPixmap(
             QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
 
+        m_projectFooter = new QFrame(this);
+        QHBoxLayout* hLayout = new QHBoxLayout();
+        hLayout->setContentsMargins(0, 0, 0, 0);
+        m_projectFooter->setLayout(hLayout);
+        {
+            QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
+            hLayout->addWidget(projectNameLabel);
+        }
+
+        vLayout->addWidget(m_projectFooter);
+    }
+
+    void ProjectButton::ProcessingSetup()
+    {
+        m_projectImageLabel->GetOverlayLabel()->setAlignment(Qt::AlignHCenter | Qt::AlignBottom);
+        m_projectImageLabel->SetEnabled(false);
+        m_projectImageLabel->SetOverlayText(tr("Processing...\n\n"));
+
+        QProgressBar* progressBar = m_projectImageLabel->GetProgressBar();
+        progressBar->setVisible(true);
+        progressBar->setValue(0);
+    }
+
+    void ProjectButton::ReadySetup()
+    {
+        connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
+        connect(m_projectImageLabel->GetBuildButton(), &QPushButton::clicked, [this](){ emit BuildProject(m_projectInfo); });
+
         QMenu* menu = new QMenu(this);
         menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
+        menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
         menu->addSeparator();
         menu->addAction(tr("Open Project folder..."), this, [this]()
         { 
@@ -100,30 +168,33 @@ namespace O3DE::ProjectManager
         menu->addAction(tr("Remove from O3DE"), this, [this]() { emit RemoveProject(m_projectInfo.m_path); });
         menu->addAction(tr("Delete this Project"), this, [this]() { emit DeleteProject(m_projectInfo.m_path); });
 
-        QFrame* footer = new QFrame(this);
-        QHBoxLayout* hLayout = new QHBoxLayout();
-        hLayout->setContentsMargins(0, 0, 0, 0);
-        footer->setLayout(hLayout);
-        {
-            QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
-            hLayout->addWidget(projectNameLabel);
-
-            QPushButton* projectMenuButton = new QPushButton(this);
-            projectMenuButton->setObjectName("projectMenuButton");
-            projectMenuButton->setMenu(menu);
-            hLayout->addWidget(projectMenuButton);
-        }
-
-        vLayout->addWidget(footer);
+        QPushButton* projectMenuButton = new QPushButton(this);
+        projectMenuButton->setObjectName("projectMenuButton");
+        projectMenuButton->setMenu(menu);
+        m_projectFooter->layout()->addWidget(projectMenuButton);
     }
 
-    void ProjectButton::SetButtonEnabled(bool enabled)
+    void ProjectButton::SetLaunchButtonEnabled(bool enabled)
     {
         m_projectImageLabel->SetEnabled(enabled);
     }
 
+    void ProjectButton::ShowBuildButton(bool show)
+    {
+        QSpacerItem* buttonSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+        m_projectImageLabel->layout()->addItem(buttonSpacer);
+        m_projectImageLabel->layout()->addWidget(m_projectImageLabel->GetBuildButton());
+        m_projectImageLabel->GetBuildButton()->setVisible(show);
+    }
+
     void ProjectButton::SetButtonOverlayText(const QString& text)
     {
         m_projectImageLabel->SetOverlayText(text);
     }
+
+    void ProjectButton::SetProgressBarValue(int progress)
+    {
+        m_projectImageLabel->GetProgressBar()->setValue(progress);
+    }
 } // namespace O3DE::ProjectManager

+ 16 - 3
Code/Tools/ProjectManager/Source/ProjectButtonWidget.h

@@ -21,6 +21,7 @@
 QT_FORWARD_DECLARE_CLASS(QPixmap)
 QT_FORWARD_DECLARE_CLASS(QPushButton)
 QT_FORWARD_DECLARE_CLASS(QAction)
+QT_FORWARD_DECLARE_CLASS(QProgressBar)
 
 namespace O3DE::ProjectManager
 {
@@ -36,6 +37,10 @@ namespace O3DE::ProjectManager
         void SetEnabled(bool enabled);
         void SetOverlayText(const QString& text);
 
+        QLabel* GetOverlayLabel();
+        QProgressBar* GetProgressBar();
+        QPushButton* GetBuildButton();
+
     signals:
         void triggered();
 
@@ -44,6 +49,8 @@ namespace O3DE::ProjectManager
 
     private:
         QLabel* m_overlayLabel;
+        QProgressBar* m_progressBar;
+        QPushButton* m_buildButton;
         bool m_enabled = true;
     };
 
@@ -53,11 +60,13 @@ namespace O3DE::ProjectManager
         Q_OBJECT // AUTOMOC
 
     public:
-        explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr);
+        explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr, bool processing = false);
         ~ProjectButton() = default;
 
-        void SetButtonEnabled(bool enabled);
+        void SetLaunchButtonEnabled(bool enabled);
+        void ShowBuildButton(bool show);
         void SetButtonOverlayText(const QString& text);
+        void SetProgressBarValue(int progress);
 
     signals:
         void OpenProject(const QString& projectName);
@@ -65,11 +74,15 @@ namespace O3DE::ProjectManager
         void CopyProject(const QString& projectName);
         void RemoveProject(const QString& projectName);
         void DeleteProject(const QString& projectName);
+        void BuildProject(const ProjectInfo& projectInfo);
 
     private:
-        void Setup();
+        void BaseSetup();
+        void ProcessingSetup();
+        void ReadySetup();
 
         ProjectInfo m_projectInfo;
         LabelButton* m_projectImageLabel;
+        QFrame* m_projectFooter;
     };
 } // namespace O3DE::ProjectManager

+ 2 - 2
Code/Tools/ProjectManager/Source/ProjectInfo.cpp

@@ -15,13 +15,13 @@
 namespace O3DE::ProjectManager
 {
     ProjectInfo::ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
-        const QString& imagePath, const QString& backgroundImagePath, bool isNew)
+        const QString& imagePath, const QString& backgroundImagePath, bool needsBuild)
         : m_path(path)
         , m_projectName(projectName)
         , m_displayName(displayName)
         , m_imagePath(imagePath)
         , m_backgroundImagePath(backgroundImagePath)
-        , m_isNew(isNew)
+        , m_needsBuild(needsBuild)
     {
     }
 

+ 2 - 2
Code/Tools/ProjectManager/Source/ProjectInfo.h

@@ -24,7 +24,7 @@ namespace O3DE::ProjectManager
     public:
         ProjectInfo() = default;
         ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
-            const QString& imagePath, const QString& backgroundImagePath, bool isNew);
+            const QString& imagePath, const QString& backgroundImagePath, bool needsBuild);
         bool operator==(const ProjectInfo& rhs);
         bool operator!=(const ProjectInfo& rhs);
 
@@ -42,6 +42,6 @@ namespace O3DE::ProjectManager
         QString m_backgroundImagePath;
 
         // Used in project creation
-        bool m_isNew = false; //! Is this a new project or existing
+        bool m_needsBuild = false; //! Does this project need to be built
     };
 } // namespace O3DE::ProjectManager

+ 46 - 2
Code/Tools/ProjectManager/Source/ProjectUtils.cpp

@@ -16,7 +16,9 @@
 #include <QFileDialog>
 #include <QDir>
 #include <QMessageBox>
-#include <QProgressDialog>
+#include <QFileInfo>
+#include <QProcess>
+#include <QProcessEnvironment>
 
 namespace O3DE::ProjectManager
 {
@@ -192,6 +194,49 @@ namespace O3DE::ProjectManager
             return true;
         }
 
+        static bool IsVS2019Installed_internal()
+        {
+            QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
+            QString programFilesPath = environment.value("ProgramFiles(x86)");
+            QString vsWherePath = programFilesPath + "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
+
+            QFileInfo vsWhereFile(vsWherePath);
+            if (vsWhereFile.exists() && vsWhereFile.isFile())
+            {
+                QProcess vsWhereProcess;
+                vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels);
+
+                vsWhereProcess.start(
+                    vsWherePath,
+                    QStringList{ "-version", "16.0", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+                                 "-property", "isComplete" });
+
+                if (!vsWhereProcess.waitForStarted())
+                {
+                    return false;
+                }
+
+                while (vsWhereProcess.waitForReadyRead())
+                {
+                }
+
+                QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
+                if (vsWhereOutput.startsWith("1"))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        bool IsVS2019Installed()
+        {
+            static bool vs2019Installed = IsVS2019Installed_internal();
+
+            return vs2019Installed;
+        }
+
         ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
         {
             auto iter = s_ProjectManagerStringNames.find(screen);
@@ -202,6 +247,5 @@ namespace O3DE::ProjectManager
 
             return ProjectManagerScreen::Invalid;
         }
-
     } // namespace ProjectUtils
 } // namespace O3DE::ProjectManager

+ 3 - 0
Code/Tools/ProjectManager/Source/ProjectUtils.h

@@ -25,6 +25,9 @@ namespace O3DE::ProjectManager
         bool CopyProject(const QString& origPath, const QString& newPath);
         bool DeleteProjectFiles(const QString& path, bool force = false);
         bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr);
+
+        bool IsVS2019Installed();
+
         ProjectManagerScreen GetProjectManagerScreen(const QString& screen);
     } // namespace ProjectUtils
 } // namespace O3DE::ProjectManager

+ 288 - 73
Code/Tools/ProjectManager/Source/ProjectsScreen.cpp

@@ -15,6 +15,8 @@
 #include <ProjectButtonWidget.h>
 #include <PythonBindingsInterface.h>
 #include <ProjectUtils.h>
+#include <ProjectBuilder.h>
+#include <ScreensCtrl.h>
 
 #include <AzQtComponents/Components/FlowLayout.h>
 #include <AzCore/Platform.h>
@@ -42,6 +44,8 @@
 #include <QSettings>
 #include <QMessageBox>
 #include <QTimer>
+#include <QQueue>
+#include <QDir>
 
 //#define DISPLAY_PROJECT_DEV_DATA true 
 
@@ -66,6 +70,14 @@ namespace O3DE::ProjectManager
         m_stack->addWidget(m_projectsContent);
 
         vLayout->addWidget(m_stack);
+
+        connect(reinterpret_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
+    }
+
+    ProjectsScreen::~ProjectsScreen()
+
+    {
+        delete m_currentBuilder;
     }
 
     QFrame* ProjectsScreen::CreateFirstTimeContent()
@@ -110,7 +122,7 @@ namespace O3DE::ProjectManager
         return frame;
     }
 
-    QFrame* ProjectsScreen::CreateProjectsContent()
+    QFrame* ProjectsScreen::CreateProjectsContent(QString buildProjectPath, ProjectButton** projectButton)
     {
         QFrame* frame = new QFrame(this);
         frame->setObjectName("projectsContent");
@@ -158,30 +170,43 @@ namespace O3DE::ProjectManager
                 projectsScrollArea->setWidgetResizable(true);
 
 #ifndef DISPLAY_PROJECT_DEV_DATA
+                // Iterate once to insert building project first
+                if (!buildProjectPath.isEmpty())
+                {
+                    buildProjectPath = QDir::fromNativeSeparators(buildProjectPath);
+                    for (auto project : projectsResult.GetValue())
+                    {
+                        if (QDir::fromNativeSeparators(project.m_path) == buildProjectPath)
+                        {
+                            ProjectButton* buildingProjectButton = CreateProjectButton(project, flowLayout, true);
+
+                            if (projectButton)
+                            {
+                                *projectButton = buildingProjectButton;
+                            }
+
+                            break;
+                        }
+                    }
+                }
+
                 for (auto project : projectsResult.GetValue())
 #else
                 ProjectInfo project = projectsResult.GetValue().at(0);
                 for (int i = 0; i < 15; i++)
 #endif
                 {
-                    ProjectButton* projectButton;
-
-                    QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
-                    QFileInfo doesPreviewExist(projectPreviewPath);
-                    if (doesPreviewExist.exists() && doesPreviewExist.isFile())
+                    // Add all other projects skipping building project
+                    // Safe if no building project because it is just an empty string
+                    if (project.m_path != buildProjectPath)
                     {
-                        project.m_imagePath = projectPreviewPath;
-                    }
-
-                    projectButton = new ProjectButton(project, this);
-
-                    flowLayout->addWidget(projectButton);
+                        ProjectButton* projectButtonWidget = CreateProjectButton(project, flowLayout);
 
-                    connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
-                    connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
-                    connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
-                    connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
-                    connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
+                        if (RequiresBuildProjectIterator(project.m_path) != m_requiresBuild.end())
+                        {
+                            projectButtonWidget->ShowBuildButton(true);
+                        }
+                    }
                 }
 
                 layout->addWidget(projectsScrollArea);
@@ -191,6 +216,60 @@ namespace O3DE::ProjectManager
         return frame;
     }
 
+    ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
+    {
+        ProjectButton* projectButton;
+
+        QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
+        QFileInfo doesPreviewExist(projectPreviewPath);
+        if (doesPreviewExist.exists() && doesPreviewExist.isFile())
+        {
+            project.m_imagePath = projectPreviewPath;
+        }
+
+        projectButton = new ProjectButton(project, this, processing);
+
+        flowLayout->addWidget(projectButton);
+
+        if (!processing)
+        {
+            connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
+            connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
+            connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
+            connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
+            connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
+        }
+        connect(projectButton, &ProjectButton::BuildProject, this, &ProjectsScreen::QueueBuildProject);
+
+        return projectButton;
+    }
+
+    void ProjectsScreen::ResetProjectsContent()
+    {
+        // refresh the projects content by re-creating it for now
+        if (m_projectsContent)
+        {
+            m_stack->removeWidget(m_projectsContent);
+            m_projectsContent->deleteLater();
+        }
+
+        // Make sure to update builder with latest Project Button
+        if (m_currentBuilder)
+        {
+            ProjectButton* projectButtonPtr;
+
+            m_projectsContent = CreateProjectsContent(m_currentBuilder->GetProjectPath(), &projectButtonPtr);
+            m_currentBuilder->SetProjectButton(projectButtonPtr);
+        }
+        else
+        {
+            m_projectsContent = CreateProjectsContent();
+        }
+
+        m_stack->addWidget(m_projectsContent);
+        m_stack->setCurrentWidget(m_projectsContent);
+    }
+
     ProjectManagerScreen ProjectsScreen::GetScreenEnum()
     {
         return ProjectManagerScreen::Projects;
@@ -237,7 +316,7 @@ namespace O3DE::ProjectManager
     {
         if (ProjectUtils::AddProjectDialog(this))
         {
-            emit ResetScreenRequest(ProjectManagerScreen::Projects);
+            ResetProjectsContent();
             emit ChangeScreenRequest(ProjectManagerScreen::Projects);
         }
     }
@@ -245,38 +324,47 @@ namespace O3DE::ProjectManager
     {
         if (!projectPath.isEmpty())
         {
-            AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
-            AZStd::string executableFilename = "Editor";
-            AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
-            auto cmdPath = AZ::IO::FixedMaxPathString::format("%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(), projectPath.toStdString().c_str());
-
-            AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
-            processLaunchInfo.m_commandlineParameters = cmdPath;
-            bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
-            if (!launchSucceeded)
+            if (!WarnIfInBuildQueue(projectPath))
             {
-                AZ_Error("ProjectManager", false, "Failed to launch editor");
-                QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
-            }
-            else
-            {
-                // prevent the user from accidentally pressing the button while the editor is launching
-                // and let them know what's happening
-                ProjectButton* button = qobject_cast<ProjectButton*>(sender());
-                if (button)
+                AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
+                AZStd::string executableFilename = "Editor";
+                AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
+                auto cmdPath = AZ::IO::FixedMaxPathString::format(
+                    "%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(),
+                    projectPath.toStdString().c_str());
+
+                AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
+                processLaunchInfo.m_commandlineParameters = cmdPath;
+                bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
+                if (!launchSucceeded)
                 {
-                    button->SetButtonEnabled(false);
-                    button->SetButtonOverlayText(tr("Opening Editor..."));
+                    AZ_Error("ProjectManager", false, "Failed to launch editor");
+                    QMessageBox::critical(
+                        this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
                 }
+                else
+                {
+                    // prevent the user from accidentally pressing the button while the editor is launching
+                    // and let them know what's happening
+                    ProjectButton* button = qobject_cast<ProjectButton*>(sender());
+                    if (button)
+                    {
+                        button->SetLaunchButtonEnabled(false);
+                        button->SetButtonOverlayText(tr("Opening Editor..."));
+                    }
 
-                // enable the button after 3 seconds 
-                constexpr int waitTimeInMs = 3000;
-                QTimer::singleShot(waitTimeInMs, this, [this, button] {
-                        if (button)
+                    // enable the button after 3 seconds
+                    constexpr int waitTimeInMs = 3000;
+                    QTimer::singleShot(
+                        waitTimeInMs, this,
+                        [this, button]
                         {
-                            button->SetButtonEnabled(true);
-                        }
-                    });
+                            if (button)
+                            {
+                                button->SetLaunchButtonEnabled(true);
+                            }
+                        });
+                }
             }
         }
         else
@@ -288,38 +376,90 @@ namespace O3DE::ProjectManager
     }
     void ProjectsScreen::HandleEditProject(const QString& projectPath)
     {
-        emit NotifyCurrentProject(projectPath);
-        emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
+        if (!WarnIfInBuildQueue(projectPath))
+        {
+            emit NotifyCurrentProject(projectPath);
+            emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
+        }
     }
     void ProjectsScreen::HandleCopyProject(const QString& projectPath)
     {
-        // Open file dialog and choose location for copied project then register copy with O3DE
-        if (ProjectUtils::CopyProjectDialog(projectPath, this))
+        if (!WarnIfInBuildQueue(projectPath))
         {
-            emit ResetScreenRequest(ProjectManagerScreen::Projects);
-            emit ChangeScreenRequest(ProjectManagerScreen::Projects);
+            // Open file dialog and choose location for copied project then register copy with O3DE
+            if (ProjectUtils::CopyProjectDialog(projectPath, this))
+            {
+                ResetProjectsContent();
+                emit ChangeScreenRequest(ProjectManagerScreen::Projects);
+            }
         }
     }
     void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
     {
-        // Unregister Project from O3DE and reload projects
-        if (ProjectUtils::UnregisterProject(projectPath))
+        if (!WarnIfInBuildQueue(projectPath))
         {
-            emit ResetScreenRequest(ProjectManagerScreen::Projects);
-            emit ChangeScreenRequest(ProjectManagerScreen::Projects);
+            // Unregister Project from O3DE and reload projects
+            if (ProjectUtils::UnregisterProject(projectPath))
+            {
+                ResetProjectsContent();
+                emit ChangeScreenRequest(ProjectManagerScreen::Projects);
+            }
         }
     }
     void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
     {
-        QMessageBox::StandardButton warningResult = QMessageBox::warning(
-            this, tr("Delete Project"), tr("Are you sure?\nProject will be removed from O3DE and directory will be deleted!"),
-            QMessageBox::No | QMessageBox::Yes);
+        if (!WarnIfInBuildQueue(projectPath))
+        {
+            QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
+                tr("Delete Project"),
+                tr("Are you sure?\nProject will be unregistered from O3DE and project directory will be deleted from your disk."),
+                QMessageBox::No | QMessageBox::Yes);
+
+            if (warningResult == QMessageBox::Yes)
+            {
+                // Remove project from O3DE and delete from disk
+                HandleRemoveProject(projectPath);
+                ProjectUtils::DeleteProjectFiles(projectPath);
+            }
+        }
+    }
 
-        if (warningResult == QMessageBox::Yes)
+    void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
+    {
+        if (projectInfo.m_needsBuild)
         {
-            // Remove project from O3DE and delete from disk
-            HandleRemoveProject(projectPath);
-            ProjectUtils::DeleteProjectFiles(projectPath);
+            if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end())
+            {
+                m_requiresBuild.append(projectInfo);
+            }
+            ResetProjectsContent();
+        }
+        else
+        {
+            QMessageBox::information(this,
+                tr("Project Should be rebuilt."),
+                projectInfo.m_projectName + tr(" project likely needs to be rebuilt."));
+        }
+    }
+
+    void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo)
+    {
+        auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
+        if (requiredIter != m_requiresBuild.end())
+        {
+            m_requiresBuild.erase(requiredIter);
+        }
+
+        if (!BuildQueueContainsProject(projectInfo.m_path))
+        {
+            if (m_buildQueue.empty() && !m_currentBuilder)
+            {
+                StartProjectBuild(projectInfo);
+            }
+            else
+            {
+                m_buildQueue.append(projectInfo);
+            }
         }
     }
 
@@ -331,17 +471,7 @@ namespace O3DE::ProjectManager
         }
         else
         {
-            // refresh the projects content by re-creating it for now
-            if (m_projectsContent)
-            {
-                m_stack->removeWidget(m_projectsContent);
-                m_projectsContent->deleteLater();
-            }
-
-            m_projectsContent = CreateProjectsContent();
-
-            m_stack->addWidget(m_projectsContent);
-            m_stack->setCurrentWidget(m_projectsContent);
+            ResetProjectsContent();
         }
     }
 
@@ -363,4 +493,89 @@ namespace O3DE::ProjectManager
         return displayFirstTimeContent;
     }
 
+    void ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo)
+    {
+        if (ProjectUtils::IsVS2019Installed())
+        {
+            QMessageBox::StandardButton buildProject = QMessageBox::information(
+                this,
+                tr("Building \"%1\"").arg(projectInfo.m_projectName),
+                tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName),
+                QMessageBox::No | QMessageBox::Yes);
+
+            if (buildProject == QMessageBox::Yes)
+            {
+                m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
+                ResetProjectsContent();
+                connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
+
+                m_currentBuilder->Start();
+            }
+            else
+            {
+                ProjectBuildDone();
+            }
+        }
+    }
+
+    void ProjectsScreen::ProjectBuildDone()
+    {
+        delete m_currentBuilder;
+        m_currentBuilder = nullptr;
+
+        if (!m_buildQueue.empty())
+        {
+            StartProjectBuild(m_buildQueue.front());
+            m_buildQueue.pop_front();
+        }
+        else
+        {
+            ResetProjectsContent();
+        }
+    }
+
+    QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
+    {
+        QString nativeProjPath(QDir::toNativeSeparators(projectPath));
+        auto projectIter = m_requiresBuild.begin();
+        for (; projectIter != m_requiresBuild.end(); ++projectIter)
+        {
+            if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
+            {
+                break;
+            }
+        }
+
+        return projectIter;
+    }
+
+    bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
+    {
+        QString nativeProjPath(QDir::toNativeSeparators(projectPath));
+        for (const ProjectInfo& project : m_buildQueue)
+        {
+            if (QDir::toNativeSeparators(project.m_path) == nativeProjPath)
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
+    {
+        if (BuildQueueContainsProject(projectPath))
+        {
+            QMessageBox::warning(
+                this,
+                tr("Action Temporarily Disabled!"),
+                tr("Action not allowed on projects in build queue."));
+
+            return true;
+        }
+
+        return false;
+    }
+
 } // namespace O3DE::ProjectManager

+ 28 - 7
Code/Tools/ProjectManager/Source/ProjectsScreen.h

@@ -13,21 +13,28 @@
 
 #if !defined(Q_MOC_RUN)
 #include <ScreenWidget.h>
+#include <ProjectInfo.h>
+
+#include <QQueue>
 #endif
 
 QT_FORWARD_DECLARE_CLASS(QPaintEvent)
 QT_FORWARD_DECLARE_CLASS(QFrame)
 QT_FORWARD_DECLARE_CLASS(QStackedWidget)
+QT_FORWARD_DECLARE_CLASS(QLayout)
 
 namespace O3DE::ProjectManager
 {
+    QT_FORWARD_DECLARE_CLASS(ProjectBuilderController);
+    QT_FORWARD_DECLARE_CLASS(ProjectButton);
+
     class ProjectsScreen
         : public ScreenWidget
     {
 
     public:
         explicit ProjectsScreen(QWidget* parent = nullptr);
-        ~ProjectsScreen() = default;
+        ~ProjectsScreen();
 
         ProjectManagerScreen GetScreenEnum() override;
         QString GetTabText() override;
@@ -35,6 +42,7 @@ namespace O3DE::ProjectManager
 
     protected:
         void NotifyCurrentScreen() override;
+        void ProjectBuildDone();
 
     protected slots:
         void HandleNewProjectButton();
@@ -45,19 +53,32 @@ namespace O3DE::ProjectManager
         void HandleRemoveProject(const QString& projectPath);
         void HandleDeleteProject(const QString& projectPath);
 
+        void SuggestBuildProject(const ProjectInfo& projectInfo);
+        void QueueBuildProject(const ProjectInfo& projectInfo);
+
         void paintEvent(QPaintEvent* event) override;
 
     private:
         QFrame* CreateFirstTimeContent();
-        QFrame* CreateProjectsContent();
+        QFrame* CreateProjectsContent(QString buildProjectPath = "", ProjectButton** projectButton = nullptr);
+        ProjectButton* CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing = false);
+        void ResetProjectsContent();
         bool ShouldDisplayFirstTimeContent();
 
-        QAction* m_createNewProjectAction;
-        QAction* m_addExistingProjectAction;
+        void StartProjectBuild(const ProjectInfo& projectInfo);
+        QList<ProjectInfo>::iterator RequiresBuildProjectIterator(const QString& projectPath);
+        bool BuildQueueContainsProject(const QString& projectPath);
+        bool WarnIfInBuildQueue(const QString& projectPath);
+
+        QAction* m_createNewProjectAction = nullptr;
+        QAction* m_addExistingProjectAction = nullptr;
         QPixmap m_background;
-        QFrame* m_firstTimeContent;
-        QFrame* m_projectsContent;
-        QStackedWidget* m_stack;
+        QFrame* m_firstTimeContent = nullptr;
+        QFrame* m_projectsContent = nullptr;
+        QStackedWidget* m_stack = nullptr;
+        QList<ProjectInfo> m_requiresBuild;
+        QQueue<ProjectInfo> m_buildQueue;
+        ProjectBuilderController* m_currentBuilder = nullptr;
 
         const QString m_projectPreviewImagePath = "/preview.png";
 

+ 1 - 1
Code/Tools/ProjectManager/Source/PythonBindings.cpp

@@ -667,7 +667,7 @@ namespace O3DE::ProjectManager
     {
         ProjectInfo projectInfo;
         projectInfo.m_path = Py_To_String(path);
-        projectInfo.m_isNew = false;
+        projectInfo.m_needsBuild = false;
 
         auto projectData = m_manifest.attr("get_project_json_data")(pybind11::none(), path);
         if (pybind11::isinstance<pybind11::dict>(projectData))

+ 2 - 0
Code/Tools/ProjectManager/Source/ScreenWidget.h

@@ -13,6 +13,7 @@
 
 #if !defined(Q_MOC_RUN)
 #include <ScreenDefs.h>
+#include <ProjectInfo.h>
 
 #include <QWidget>
 #include <QStyleOption>
@@ -61,6 +62,7 @@ namespace O3DE::ProjectManager
         void GotoPreviousScreenRequest();
         void ResetScreenRequest(ProjectManagerScreen screen);
         void NotifyCurrentProject(const QString& projectPath);
+        void NotifyBuildProject(const ProjectInfo& projectInfo);
 
     };
 

+ 1 - 0
Code/Tools/ProjectManager/Source/ScreensCtrl.cpp

@@ -177,6 +177,7 @@ namespace O3DE::ProjectManager
         connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen);
         connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen);
         connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject);
+        connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject);
     }
 
     void ScreensCtrl::ResetAllScreens()

+ 2 - 0
Code/Tools/ProjectManager/Source/ScreensCtrl.h

@@ -13,6 +13,7 @@
 
 #if !defined(Q_MOC_RUN)
 #include <ScreenDefs.h>
+#include <ProjectInfo.h>
 
 #include <QStackedWidget>
 #include <QStack>
@@ -39,6 +40,7 @@ namespace O3DE::ProjectManager
 
     signals:
         void NotifyCurrentProject(const QString& projectPath);
+        void NotifyBuildProject(const ProjectInfo& projectInfo);
 
     public slots:
         bool ChangeToScreen(ProjectManagerScreen screen);

+ 11 - 3
Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp

@@ -119,7 +119,9 @@ namespace O3DE::ProjectManager
 
     void UpdateProjectCtrl::HandleNextButton()
     {
-        if (m_stack->currentIndex() == ScreenOrder::Settings)
+        bool shouldRebuild = false;
+
+        if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen)
         {
             if (m_updateSettingsScreen)
             {
@@ -155,11 +157,17 @@ namespace O3DE::ProjectManager
                 m_projectInfo = newProjectSettings;
             }
         }
-
-        if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
+        else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
         {
             // Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
             m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
+
+            shouldRebuild = true;
+        }
+
+        if (shouldRebuild)
+        {
+            emit NotifyBuildProject(m_projectInfo);
         }
 
         emit ChangeScreenRequest(ProjectManagerScreen::Projects);

+ 2 - 0
Code/Tools/ProjectManager/project_manager_files.cmake

@@ -38,6 +38,8 @@ set(FILES
     Source/ProjectInfo.cpp
     Source/ProjectUtils.h
     Source/ProjectUtils.cpp
+    Source/ProjectBuilder.h
+    Source/ProjectBuilder.cpp
     Source/UpdateProjectSettingsScreen.h
     Source/UpdateProjectSettingsScreen.cpp
     Source/NewProjectSettingsScreen.h