/* * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include namespace O3DE::ProjectManager { namespace ProjectUtils { bool AppendToPath(QString newPath) { QString pathEnv = qEnvironmentVariable("Path"); QStringList pathEnvList = pathEnv.split(";"); if (!pathEnvList.contains(newPath, Qt::CaseInsensitive)) { pathEnv += ";" + newPath; if (!qputenv("Path", pathEnv.toStdString().c_str())) { return false; } } return true; } AZ::Outcome SetupCommandLineProcessEnvironment() { // Use the engine path to insert a path for cmake auto engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); if (!engineInfoResult.IsSuccess()) { return AZ::Failure(QObject::tr("Failed to get engine info")); } auto engineInfo = engineInfoResult.GetValue(); // Append cmake path to the current environment PATH incase it is missing, since if // we are starting CMake itself the current application needs to find it using Path // This also takes affect for all child processes. QDir cmakePath(engineInfo.m_path); cmakePath.cd("cmake/runtime/bin"); if (!AppendToPath(cmakePath.path())) { return AZ::Failure(QObject::tr("Failed to append the path to CMake to the PATH environment variable")); } // if we don't have ninja, use one that might come with the installer auto ninjaQueryResult = ExecuteCommandResult("ninja", QStringList{ "--version" }); if (!ninjaQueryResult.IsSuccess()) { QDir ninjaPath(engineInfo.m_path); ninjaPath.cd("ninja"); if (!AppendToPath(ninjaPath.path())) { return AZ::Failure(QObject::tr("Failed to append the path to ninja to the PATH environment variable")); } } return AZ::Success(); } AZ::Outcome FindSupportedCMake() { // Validate that cmake is installed and is in the path auto cmakeVersionQueryResult = ExecuteCommandResult("cmake", QStringList{ "--version" }); if (!cmakeVersionQueryResult.IsSuccess()) { return AZ::Failure( QObject::tr("CMake not found. \n\n" "Make sure that the minimum version of CMake is installed and available from the command prompt. " "Refer to the O3DE " "requirements for more information.")); } return AZ::Success(QString{ ProjectCMakeCommand }); } AZ::Outcome FindSupportedNinja() { // Validate that cmake is installed and is in the path auto ninjaQueryResult = ExecuteCommandResult("ninja", QStringList{ "--version" }); if (!ninjaQueryResult.IsSuccess()) { return AZ::Failure( QObject::tr("Ninja.exe Build System was not found in the PATH environment variable.
" "Ninja is used to prepare script-only projects and avoid C++ compilation.
" "You can either automatically install it with the Windows Package Manager, or manually download it " "from the official Ninja website.
" "To automatically install it using the Windows Package Manager, use this command in a command window like Powershell:\n\n" "
winget install Ninja-build.Ninja


" "After installation, you may have to restart O3DE's Project Manager.

" "Refer to the O3DE " "requirements for more information.")); } return AZ::Success(QString{ "Ninja" }); } AZ::Outcome FindSupportedCompilerForPlatform(const ProjectInfo& projectInfo) { // Validate that cmake is installed auto cmakeProcessEnvResult = SetupCommandLineProcessEnvironment(); if (!cmakeProcessEnvResult.IsSuccess()) { return AZ::Failure(cmakeProcessEnvResult.GetError()); } if (auto cmakeVersionQueryResult = FindSupportedCMake(); !cmakeVersionQueryResult.IsSuccess()) { return cmakeVersionQueryResult; } if (projectInfo.m_isScriptOnly) { if (auto ninjaVersionQueryResult = FindSupportedNinja(); !ninjaVersionQueryResult.IsSuccess()) { return ninjaVersionQueryResult; } } // we want to help the user here, by showing a helpful error message depending on their situation // 1. Known, supported version of the compiler installed: No message // 2. Known, deprecated version of the compiler installed: Warning message // 3. Future unsupported version of the compiler installed: Warning message // 4. No supported version of the compiler installed: Error message // Validate that the minimal version of visual studio is installed QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); QString programFilesPath = environment.value("ProgramFiles(x86)"); QString vsWherePath = QDir(programFilesPath).filePath("Microsoft Visual Studio/Installer/vswhere.exe"); // Range which represents an ideal supported version of Visual Studio QStringList versionArgumentsIdeal{ "-version", "[17.14, 19)" }; // 17.14 is VS2022, 18.xxxx is VS2026 // Range which represents a deprecated, but still functional version of Visual Studio QStringList versionArgumentsDeprecated{ "-version", "[16.11, 19)" }; // 16.11 is last of VS2019. // Range which represents a future version of Visual Studio which we don't know about and was released // as a surprise. QStringList versionArgumentsFutureVS{ "-version", "[16.11, )" }; QFileInfo vsWhereFile(vsWherePath); if (vsWhereFile.exists() && vsWhereFile.isFile()) { QStringList vsWhereBaseArguments { "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" }; QStringList isCompleteArguments{ "-property", "isComplete" }; // returns a 1 if all components are available. // Check 1: are we in a perfect situation, where we are exactly on the supported versions? QProcess vsWhereIsCompleteProcess; vsWhereIsCompleteProcess.setProcessChannelMode(QProcess::MergedChannels); vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsIdeal + isCompleteArguments); if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished()) { QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput()); if (vsWhereIsCompleteOutput.startsWith("1")) { return AZ::Success(QString()); // No issues, we are in the golden range. } } // If we get here, we are not in the perfect supported version range, but might still be in the deprecated range. vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsDeprecated + isCompleteArguments); if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished()) { QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput()); if (vsWhereIsCompleteOutput.startsWith("1")) { QProcess vsWhereCompilerVersionProcess; vsWhereCompilerVersionProcess.setProcessChannelMode(QProcess::MergedChannels); vsWhereCompilerVersionProcess.start(vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "displayName" }); // note that displayname includes product so will be something like "Visual Studio 2019" if (vsWhereCompilerVersionProcess.waitForStarted() && vsWhereCompilerVersionProcess.waitForFinished()) { QString vsWhereCompilerVersionOutput(vsWhereCompilerVersionProcess.readAllStandardOutput()); return AZ::Success( QObject::tr( "%1 is being used - this will be deprecated in future releases.

" "Please consider upgrading soon.

" "

Refer to the Visual " "Studio requirements for more information.")) .arg(vsWhereCompilerVersionOutput.trimmed()); } } } // if we get here, we might be on an unknown, perhaps supported future version, denoted with // the open range with no parameter `, )` at the end: vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsFutureVS + isCompleteArguments); if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished()) { QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput()); if (vsWhereIsCompleteOutput.startsWith("1")) { QProcess vsWhereCompilerVersionProcess; vsWhereCompilerVersionProcess.setProcessChannelMode(QProcess::MergedChannels); vsWhereCompilerVersionProcess.start(vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "displayName" }); if (vsWhereCompilerVersionProcess.waitForStarted() && vsWhereCompilerVersionProcess.waitForFinished()) { QString vsWhereCompilerVersionOutput(vsWhereCompilerVersionProcess.readAllStandardOutput()); return AZ::Success( QObject::tr( "A version of Visual Studio was found that isn't supported: %1

" "O3DE will attempt to build the project with it, but there may be issues." " O3DE officially supports Visual Studio 2022 v17.14 or higher, and 2026 v18.1 or higher." "

Refer to the Visual " "Studio requirements for more information.")) .arg(vsWhereCompilerVersionOutput.trimmed()); } } } } return AZ::Failure( QObject::tr("No compatible C++ Compiler found.

" "O3DE officially supports Visual Studio 2022 v17.14 or higher, and 2026 v18.1 or higher.
" "Visual Studio does not automatically include the C++ compiler by default, so please ensure that " "the 'Desktop development with C++' workload is installed in the Visual Studio Installer.

" "Refer to the Visual " "Studio requirements for more information.")); } AZ::Outcome OpenCMakeGUI(const QString& projectPath) { AZ::Outcome processEnvResult = SetupCommandLineProcessEnvironment(); if (!processEnvResult.IsSuccess()) { return AZ::Failure(processEnvResult.GetError()); } QString projectBuildPath = QDir(projectPath).filePath(ProjectBuildPathPostfix); AZ::Outcome projectBuildPathResult = GetProjectBuildPath(projectPath); if (projectBuildPathResult.IsSuccess()) { projectBuildPath = projectBuildPathResult.GetValue(); } QProcess process; // if the project build path is relative, it should be relative to the project path process.setWorkingDirectory(projectPath); process.setProgram("cmake-gui"); process.setArguments({ "-S", projectPath, "-B", projectBuildPath }); if(!process.startDetached()) { return AZ::Failure(QObject::tr("Failed to start CMake GUI")); } return AZ::Success(); } AZ::Outcome RunGetPythonScript(const QString& engineRoot) { const QString batPath = QString("%1/python/get_python.bat").arg(engineRoot); return ExecuteCommandResultModalDialog( "cmd.exe", QStringList{"/c", batPath}, QObject::tr("Running get_python script...")); } AZ::IO::FixedMaxPath GetEditorExecutablePath(const AZ::IO::PathView& projectPath) { AZ::IO::FixedMaxPath editorPath; AZ::IO::FixedMaxPath fixedProjectPath{ projectPath }; // First attempt to launch the Editor.exe within the project build directory if it exists AZ::IO::FixedMaxPath buildPathSetregPath = fixedProjectPath / AZ::SettingsRegistryConstants::DevUserRegistryFolder / "Platform" / AZ_TRAIT_OS_PLATFORM_CODENAME / "build_path.setreg"; if (AZ::IO::SystemFile::Exists(buildPathSetregPath.c_str())) { AZ::SettingsRegistryImpl settingsRegistry; // Merge the build_path.setreg into the local SettingsRegistry instance if (AZ::IO::FixedMaxPath projectBuildPath; settingsRegistry.MergeSettingsFile(buildPathSetregPath.Native(), AZ::SettingsRegistryInterface::Format::JsonMergePatch) && settingsRegistry.Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath)) { // local Settings Registry will be used to merge the build_path.setreg for the supplied projectPath AZ::IO::FixedMaxPath buildConfigurationPath = (fixedProjectPath / projectBuildPath).LexicallyNormal(); // First try /bin/$ and if that path doesn't exist // try /bin/$/$ buildConfigurationPath /= "bin"; AZStd::fixed_vector paths = { buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE / "Editor", buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE / "Editor" }; // always try profile config because that is the default if (strcmp(AZ_BUILD_CONFIGURATION_TYPE, "profile") != 0) { paths.emplace_back(buildConfigurationPath / "profile" / "Editor"); paths.emplace_back(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Editor"); } for (auto& path : paths) { if(AZ::IO::SystemFile::Exists(path.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION).c_str())) { return path; } } } } // No Editor executable was found in the project build folder so if this project uses a // different engine we must find the Editor executable for that engine if(auto engineResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath.Native().data()); engineResult) { auto engineInfo = engineResult.GetValue(); if (!engineInfo.m_thisEngine) { AZ::IO::FixedMaxPath fixedEnginePath{ engineInfo.m_path.toUtf8().constData() }; // try the default sdk path // in the future we may be able to use additional .setreg entries to locate an alternate binary path if (editorPath = (fixedEnginePath / "bin" / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Default" / "Editor"). ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); AZ::IO::SystemFile::Exists(editorPath.c_str())) { return editorPath; } return {}; } } // Fall back to checking if an Editor exists in O3DE executable directory editorPath = AZ::IO::FixedMaxPath(AZ::Utils::GetExecutableDirectory()) / "Editor"; editorPath.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); if (AZ::IO::SystemFile::Exists(editorPath.c_str())) { return editorPath; } return {}; } AZ::Outcome CreateDesktopShortcut(const QString& filename, const QString& targetPath, const QStringList& arguments) { const QString cmd{"powershell.exe"}; const QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); const QString shortcutPath = QString("%1/%2.lnk").arg(desktopPath).arg(filename); const QString arg = QString("$s=(New-Object -COM WScript.Shell).CreateShortcut('%1');$s.TargetPath='%2';$s.Arguments='%3';$s.Save();") .arg(shortcutPath) .arg(targetPath) .arg(arguments.join(' ')); auto createShortcutResult = ExecuteCommandResult(cmd, QStringList{"-Command", arg}); if (!createShortcutResult.IsSuccess()) { return AZ::Failure(QObject::tr("Failed to create desktop shortcut %1

" "Please verify you have permission to create files at the specified location.

%2") .arg(shortcutPath) .arg(createShortcutResult.GetError())); } return AZ::Success(QObject::tr("A desktop shortcut has been successfully created.
You can view the file here.").arg(desktopPath)); } } // namespace ProjectUtils } // namespace O3DE::ProjectManager