ProjectUtils_windows.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /*
  2. * 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.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include <ProjectUtils.h>
  8. #include <PythonBindingsInterface.h>
  9. #include <QDir>
  10. #include <QFileInfo>
  11. #include <QProcess>
  12. #include <QProcessEnvironment>
  13. #include <QStandardPaths>
  14. #include <AzCore/Settings/SettingsRegistryImpl.h>
  15. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  16. #include <AzCore/Utils/Utils.h>
  17. namespace O3DE::ProjectManager
  18. {
  19. namespace ProjectUtils
  20. {
  21. bool AppendToPath(QString newPath)
  22. {
  23. QString pathEnv = qEnvironmentVariable("Path");
  24. QStringList pathEnvList = pathEnv.split(";");
  25. if (!pathEnvList.contains(newPath, Qt::CaseInsensitive))
  26. {
  27. pathEnv += ";" + newPath;
  28. if (!qputenv("Path", pathEnv.toStdString().c_str()))
  29. {
  30. return false;
  31. }
  32. }
  33. return true;
  34. }
  35. AZ::Outcome<void, QString> SetupCommandLineProcessEnvironment()
  36. {
  37. // Use the engine path to insert a path for cmake
  38. auto engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  39. if (!engineInfoResult.IsSuccess())
  40. {
  41. return AZ::Failure(QObject::tr("Failed to get engine info"));
  42. }
  43. auto engineInfo = engineInfoResult.GetValue();
  44. // Append cmake path to the current environment PATH incase it is missing, since if
  45. // we are starting CMake itself the current application needs to find it using Path
  46. // This also takes affect for all child processes.
  47. QDir cmakePath(engineInfo.m_path);
  48. cmakePath.cd("cmake/runtime/bin");
  49. if (!AppendToPath(cmakePath.path()))
  50. {
  51. return AZ::Failure(QObject::tr("Failed to append the path to CMake to the PATH environment variable"));
  52. }
  53. // if we don't have ninja, use one that might come with the installer
  54. auto ninjaQueryResult = ExecuteCommandResult("ninja", QStringList{ "--version" });
  55. if (!ninjaQueryResult.IsSuccess())
  56. {
  57. QDir ninjaPath(engineInfo.m_path);
  58. ninjaPath.cd("ninja");
  59. if (!AppendToPath(ninjaPath.path()))
  60. {
  61. return AZ::Failure(QObject::tr("Failed to append the path to ninja to the PATH environment variable"));
  62. }
  63. }
  64. return AZ::Success();
  65. }
  66. AZ::Outcome<QString, QString> FindSupportedCMake()
  67. {
  68. // Validate that cmake is installed and is in the path
  69. auto cmakeVersionQueryResult = ExecuteCommandResult("cmake", QStringList{ "--version" });
  70. if (!cmakeVersionQueryResult.IsSuccess())
  71. {
  72. return AZ::Failure(
  73. QObject::tr("CMake not found. \n\n"
  74. "Make sure that the minimum version of CMake is installed and available from the command prompt. "
  75. "Refer to the <a href='https://o3de.org/docs/welcome-guide/setup/requirements/#cmake'>O3DE "
  76. "requirements</a> for more information."));
  77. }
  78. return AZ::Success(QString{ ProjectCMakeCommand });
  79. }
  80. AZ::Outcome<QString, QString> FindSupportedNinja()
  81. {
  82. // Validate that cmake is installed and is in the path
  83. auto ninjaQueryResult = ExecuteCommandResult("ninja", QStringList{ "--version" });
  84. if (!ninjaQueryResult.IsSuccess())
  85. {
  86. return AZ::Failure(
  87. QObject::tr("Ninja.exe Build System was not found in the PATH environment variable.<br>"
  88. "Ninja is used to prepare script-only projects and avoid C++ compilation.<br>"
  89. "You can either automatically install it with the Windows Package Manager, or manually download it "
  90. "from the <a href='https://ninja-build.org/'>official Ninja website</a>.<br>"
  91. "To automatically install it using the Windows Package Manager, use this command in a command window like Powershell:\n\n"
  92. "<pre>winget install Ninja-build.Ninja</pre><br><br>"
  93. "After installation, you may have to restart O3DE's Project Manager.<br><br>"
  94. "Refer to the <a href='https://o3de.org/docs/welcome-guide/setup/requirements/#cmake'>O3DE "
  95. "requirements</a> for more information."));
  96. }
  97. return AZ::Success(QString{ "Ninja" });
  98. }
  99. AZ::Outcome<QString, QString> FindSupportedCompilerForPlatform(const ProjectInfo& projectInfo)
  100. {
  101. // Validate that cmake is installed
  102. auto cmakeProcessEnvResult = SetupCommandLineProcessEnvironment();
  103. if (!cmakeProcessEnvResult.IsSuccess())
  104. {
  105. return AZ::Failure(cmakeProcessEnvResult.GetError());
  106. }
  107. if (auto cmakeVersionQueryResult = FindSupportedCMake(); !cmakeVersionQueryResult.IsSuccess())
  108. {
  109. return cmakeVersionQueryResult;
  110. }
  111. if (projectInfo.m_isScriptOnly)
  112. {
  113. if (auto ninjaVersionQueryResult = FindSupportedNinja(); !ninjaVersionQueryResult.IsSuccess())
  114. {
  115. return ninjaVersionQueryResult;
  116. }
  117. }
  118. // Validate that the minimal version of visual studio is installed
  119. QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
  120. QString programFilesPath = environment.value("ProgramFiles(x86)");
  121. QString vsWherePath = QDir(programFilesPath).filePath("Microsoft Visual Studio/Installer/vswhere.exe");
  122. QFileInfo vsWhereFile(vsWherePath);
  123. if (vsWhereFile.exists() && vsWhereFile.isFile())
  124. {
  125. QStringList vsWhereBaseArguments =
  126. QStringList{ "-version", "[16.11,18)", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" };
  127. QProcess vsWhereIsCompleteProcess;
  128. vsWhereIsCompleteProcess.setProcessChannelMode(QProcess::MergedChannels);
  129. vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "isComplete" });
  130. if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished())
  131. {
  132. QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput());
  133. if (vsWhereIsCompleteOutput.startsWith("1"))
  134. {
  135. QProcess vsWhereCompilerVersionProcess;
  136. vsWhereCompilerVersionProcess.setProcessChannelMode(QProcess::MergedChannels);
  137. vsWhereCompilerVersionProcess.start(
  138. vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "catalog_productDisplayVersion" });
  139. if (vsWhereCompilerVersionProcess.waitForStarted() && vsWhereCompilerVersionProcess.waitForFinished())
  140. {
  141. QString vsWhereCompilerVersionOutput(vsWhereCompilerVersionProcess.readAllStandardOutput());
  142. return AZ::Success(vsWhereCompilerVersionOutput);
  143. }
  144. }
  145. }
  146. }
  147. return AZ::Failure(
  148. QObject::tr("Visual Studio 2019 version 16.11 or higher or Visual Studio 2022 version 17.0 or higher not found.<br><br>"
  149. "A compatible version of Visual Studio is required to build this project.<br>"
  150. "Refer to the <a href='https://o3de.org/docs/welcome-guide/requirements/#microsoft-visual-studio'>Visual "
  151. "Studio requirements</a> for more information."));
  152. }
  153. AZ::Outcome<void, QString> OpenCMakeGUI(const QString& projectPath)
  154. {
  155. AZ::Outcome processEnvResult = SetupCommandLineProcessEnvironment();
  156. if (!processEnvResult.IsSuccess())
  157. {
  158. return AZ::Failure(processEnvResult.GetError());
  159. }
  160. QString projectBuildPath = QDir(projectPath).filePath(ProjectBuildPathPostfix);
  161. AZ::Outcome projectBuildPathResult = GetProjectBuildPath(projectPath);
  162. if (projectBuildPathResult.IsSuccess())
  163. {
  164. projectBuildPath = projectBuildPathResult.GetValue();
  165. }
  166. QProcess process;
  167. // if the project build path is relative, it should be relative to the project path
  168. process.setWorkingDirectory(projectPath);
  169. process.setProgram("cmake-gui");
  170. process.setArguments({ "-S", projectPath, "-B", projectBuildPath });
  171. if(!process.startDetached())
  172. {
  173. return AZ::Failure(QObject::tr("Failed to start CMake GUI"));
  174. }
  175. return AZ::Success();
  176. }
  177. AZ::Outcome<QString, QString> RunGetPythonScript(const QString& engineRoot)
  178. {
  179. const QString batPath = QString("%1/python/get_python.bat").arg(engineRoot);
  180. return ExecuteCommandResultModalDialog(
  181. "cmd.exe",
  182. QStringList{"/c", batPath},
  183. QObject::tr("Running get_python script..."));
  184. }
  185. AZ::IO::FixedMaxPath GetEditorExecutablePath(const AZ::IO::PathView& projectPath)
  186. {
  187. AZ::IO::FixedMaxPath editorPath;
  188. AZ::IO::FixedMaxPath fixedProjectPath{ projectPath };
  189. // First attempt to launch the Editor.exe within the project build directory if it exists
  190. AZ::IO::FixedMaxPath buildPathSetregPath = fixedProjectPath
  191. / AZ::SettingsRegistryInterface::DevUserRegistryFolder
  192. / "Platform" / AZ_TRAIT_OS_PLATFORM_CODENAME / "build_path.setreg";
  193. if (AZ::IO::SystemFile::Exists(buildPathSetregPath.c_str()))
  194. {
  195. AZ::SettingsRegistryImpl settingsRegistry;
  196. // Merge the build_path.setreg into the local SettingsRegistry instance
  197. if (AZ::IO::FixedMaxPath projectBuildPath;
  198. settingsRegistry.MergeSettingsFile(buildPathSetregPath.Native(),
  199. AZ::SettingsRegistryInterface::Format::JsonMergePatch)
  200. && settingsRegistry.Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath))
  201. {
  202. // local Settings Registry will be used to merge the build_path.setreg for the supplied projectPath
  203. AZ::IO::FixedMaxPath buildConfigurationPath = (fixedProjectPath / projectBuildPath).LexicallyNormal();
  204. // First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
  205. // try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
  206. buildConfigurationPath /= "bin";
  207. AZStd::fixed_vector<AZ::IO::FixedMaxPath, 4> paths = {
  208. buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE / "Editor",
  209. buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE / "Editor"
  210. };
  211. // always try profile config because that is the default
  212. if (strcmp(AZ_BUILD_CONFIGURATION_TYPE, "profile") != 0)
  213. {
  214. paths.emplace_back(buildConfigurationPath / "profile" / "Editor");
  215. paths.emplace_back(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Editor");
  216. }
  217. for (auto& path : paths)
  218. {
  219. if(AZ::IO::SystemFile::Exists(path.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION).c_str()))
  220. {
  221. return path;
  222. }
  223. }
  224. }
  225. }
  226. // No Editor executable was found in the project build folder so if this project uses a
  227. // different engine we must find the Editor executable for that engine
  228. if(auto engineResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath.Native().data()); engineResult)
  229. {
  230. auto engineInfo = engineResult.GetValue<EngineInfo>();
  231. if (!engineInfo.m_thisEngine)
  232. {
  233. AZ::IO::FixedMaxPath fixedEnginePath{ engineInfo.m_path.toUtf8().constData() };
  234. // try the default sdk path
  235. // in the future we may be able to use additional .setreg entries to locate an alternate binary path
  236. if (editorPath = (fixedEnginePath / "bin" / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Default" / "Editor").
  237. ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
  238. AZ::IO::SystemFile::Exists(editorPath.c_str()))
  239. {
  240. return editorPath;
  241. }
  242. return {};
  243. }
  244. }
  245. // Fall back to checking if an Editor exists in O3DE executable directory
  246. editorPath = AZ::IO::FixedMaxPath(AZ::Utils::GetExecutableDirectory()) / "Editor";
  247. editorPath.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
  248. if (AZ::IO::SystemFile::Exists(editorPath.c_str()))
  249. {
  250. return editorPath;
  251. }
  252. return {};
  253. }
  254. AZ::Outcome<QString, QString> CreateDesktopShortcut(const QString& filename, const QString& targetPath, const QStringList& arguments)
  255. {
  256. const QString cmd{"powershell.exe"};
  257. const QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
  258. const QString shortcutPath = QString("%1/%2.lnk").arg(desktopPath).arg(filename);
  259. const QString arg = QString("$s=(New-Object -COM WScript.Shell).CreateShortcut('%1');$s.TargetPath='%2';$s.Arguments='%3';$s.Save();")
  260. .arg(shortcutPath)
  261. .arg(targetPath)
  262. .arg(arguments.join(' '));
  263. auto createShortcutResult = ExecuteCommandResult(cmd, QStringList{"-Command", arg});
  264. if (!createShortcutResult.IsSuccess())
  265. {
  266. return AZ::Failure(QObject::tr("Failed to create desktop shortcut %1 <br><br>"
  267. "Please verify you have permission to create files at the specified location.<br><br> %2")
  268. .arg(shortcutPath)
  269. .arg(createShortcutResult.GetError()));
  270. }
  271. return AZ::Success(QObject::tr("A desktop shortcut has been successfully created.<br>You can view the file <a href=\"%1\">here</a>.").arg(desktopPath));
  272. }
  273. } // namespace ProjectUtils
  274. } // namespace O3DE::ProjectManager