ProjectUtils_windows.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. // we want to help the user here, by showing a helpful error message depending on their situation
  119. // 1. Known, supported version of the compiler installed: No message
  120. // 2. Known, deprecated version of the compiler installed: Warning message
  121. // 3. Future unsupported version of the compiler installed: Warning message
  122. // 4. No supported version of the compiler installed: Error message
  123. // Validate that the minimal version of visual studio is installed
  124. QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
  125. QString programFilesPath = environment.value("ProgramFiles(x86)");
  126. QString vsWherePath = QDir(programFilesPath).filePath("Microsoft Visual Studio/Installer/vswhere.exe");
  127. // Range which represents an ideal supported version of Visual Studio
  128. QStringList versionArgumentsIdeal{ "-version", "[17.14, 19)" }; // 17.14 is VS2022, 18.xxxx is VS2026
  129. // Range which represents a deprecated, but still functional version of Visual Studio
  130. QStringList versionArgumentsDeprecated{ "-version", "[16.11, 19)" }; // 16.11 is last of VS2019.
  131. // Range which represents a future version of Visual Studio which we don't know about and was released
  132. // as a surprise.
  133. QStringList versionArgumentsFutureVS{ "-version", "[16.11, )" };
  134. QFileInfo vsWhereFile(vsWherePath);
  135. if (vsWhereFile.exists() && vsWhereFile.isFile())
  136. {
  137. QStringList vsWhereBaseArguments { "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" };
  138. QStringList isCompleteArguments{ "-property", "isComplete" }; // returns a 1 if all components are available.
  139. // Check 1: are we in a perfect situation, where we are exactly on the supported versions?
  140. QProcess vsWhereIsCompleteProcess;
  141. vsWhereIsCompleteProcess.setProcessChannelMode(QProcess::MergedChannels);
  142. vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsIdeal + isCompleteArguments);
  143. if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished())
  144. {
  145. QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput());
  146. if (vsWhereIsCompleteOutput.startsWith("1"))
  147. {
  148. return AZ::Success(QString()); // No issues, we are in the golden range.
  149. }
  150. }
  151. // If we get here, we are not in the perfect supported version range, but might still be in the deprecated range.
  152. vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsDeprecated + isCompleteArguments);
  153. if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished())
  154. {
  155. QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput());
  156. if (vsWhereIsCompleteOutput.startsWith("1"))
  157. {
  158. QProcess vsWhereCompilerVersionProcess;
  159. vsWhereCompilerVersionProcess.setProcessChannelMode(QProcess::MergedChannels);
  160. vsWhereCompilerVersionProcess.start(vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "displayName" });
  161. // note that displayname includes product so will be something like "Visual Studio 2019"
  162. if (vsWhereCompilerVersionProcess.waitForStarted() && vsWhereCompilerVersionProcess.waitForFinished())
  163. {
  164. QString vsWhereCompilerVersionOutput(vsWhereCompilerVersionProcess.readAllStandardOutput());
  165. return AZ::Success(
  166. QObject::tr(
  167. "%1 is being used - this will be deprecated in future releases.<br><br>"
  168. "Please consider upgrading soon.<br><br>"
  169. "<br><br>Refer to the <a "
  170. "href='https://o3de.org/docs/welcome-guide/requirements/#microsoft-visual-studio'>Visual "
  171. "Studio requirements</a> for more information."))
  172. .arg(vsWhereCompilerVersionOutput.trimmed());
  173. }
  174. }
  175. }
  176. // if we get here, we might be on an unknown, perhaps supported future version, denoted with
  177. // the open range with no parameter `, )` at the end:
  178. vsWhereIsCompleteProcess.start(vsWherePath, vsWhereBaseArguments + versionArgumentsFutureVS + isCompleteArguments);
  179. if (vsWhereIsCompleteProcess.waitForStarted() && vsWhereIsCompleteProcess.waitForFinished())
  180. {
  181. QString vsWhereIsCompleteOutput(vsWhereIsCompleteProcess.readAllStandardOutput());
  182. if (vsWhereIsCompleteOutput.startsWith("1"))
  183. {
  184. QProcess vsWhereCompilerVersionProcess;
  185. vsWhereCompilerVersionProcess.setProcessChannelMode(QProcess::MergedChannels);
  186. vsWhereCompilerVersionProcess.start(vsWherePath, vsWhereBaseArguments + QStringList{ "-property", "displayName" });
  187. if (vsWhereCompilerVersionProcess.waitForStarted() && vsWhereCompilerVersionProcess.waitForFinished())
  188. {
  189. QString vsWhereCompilerVersionOutput(vsWhereCompilerVersionProcess.readAllStandardOutput());
  190. return AZ::Success(
  191. QObject::tr(
  192. "A version of Visual Studio was found that isn't supported: %1<br><br>"
  193. "O3DE will attempt to build the project with it, but there may be issues."
  194. " O3DE officially supports Visual Studio 2022 v17.14 or higher, and 2026 v18.1 or higher."
  195. "<br><br>Refer to the <a "
  196. "href='https://o3de.org/docs/welcome-guide/requirements/#microsoft-visual-studio'>Visual "
  197. "Studio requirements</a> for more information."))
  198. .arg(vsWhereCompilerVersionOutput.trimmed());
  199. }
  200. }
  201. }
  202. }
  203. return AZ::Failure(
  204. QObject::tr("No compatible C++ Compiler found.<br><br>"
  205. "O3DE officially supports Visual Studio 2022 v17.14 or higher, and 2026 v18.1 or higher.<br>"
  206. "Visual Studio does not automatically include the C++ compiler by default, so please ensure that "
  207. "the 'Desktop development with C++' workload is installed in the Visual Studio Installer.<br><br>"
  208. "Refer to the <a href='https://o3de.org/docs/welcome-guide/requirements/#microsoft-visual-studio'>Visual "
  209. "Studio requirements</a> for more information."));
  210. }
  211. AZ::Outcome<void, QString> OpenCMakeGUI(const QString& projectPath)
  212. {
  213. AZ::Outcome processEnvResult = SetupCommandLineProcessEnvironment();
  214. if (!processEnvResult.IsSuccess())
  215. {
  216. return AZ::Failure(processEnvResult.GetError());
  217. }
  218. QString projectBuildPath = QDir(projectPath).filePath(ProjectBuildPathPostfix);
  219. AZ::Outcome projectBuildPathResult = GetProjectBuildPath(projectPath);
  220. if (projectBuildPathResult.IsSuccess())
  221. {
  222. projectBuildPath = projectBuildPathResult.GetValue();
  223. }
  224. QProcess process;
  225. // if the project build path is relative, it should be relative to the project path
  226. process.setWorkingDirectory(projectPath);
  227. process.setProgram("cmake-gui");
  228. process.setArguments({ "-S", projectPath, "-B", projectBuildPath });
  229. if(!process.startDetached())
  230. {
  231. return AZ::Failure(QObject::tr("Failed to start CMake GUI"));
  232. }
  233. return AZ::Success();
  234. }
  235. AZ::Outcome<QString, QString> RunGetPythonScript(const QString& engineRoot)
  236. {
  237. const QString batPath = QString("%1/python/get_python.bat").arg(engineRoot);
  238. return ExecuteCommandResultModalDialog(
  239. "cmd.exe",
  240. QStringList{"/c", batPath},
  241. QObject::tr("Running get_python script..."));
  242. }
  243. AZ::IO::FixedMaxPath GetEditorExecutablePath(const AZ::IO::PathView& projectPath)
  244. {
  245. AZ::IO::FixedMaxPath editorPath;
  246. AZ::IO::FixedMaxPath fixedProjectPath{ projectPath };
  247. // First attempt to launch the Editor.exe within the project build directory if it exists
  248. AZ::IO::FixedMaxPath buildPathSetregPath = fixedProjectPath /
  249. AZ::SettingsRegistryConstants::DevUserRegistryFolder
  250. / "Platform" / AZ_TRAIT_OS_PLATFORM_CODENAME / "build_path.setreg";
  251. if (AZ::IO::SystemFile::Exists(buildPathSetregPath.c_str()))
  252. {
  253. AZ::SettingsRegistryImpl settingsRegistry;
  254. // Merge the build_path.setreg into the local SettingsRegistry instance
  255. if (AZ::IO::FixedMaxPath projectBuildPath;
  256. settingsRegistry.MergeSettingsFile(buildPathSetregPath.Native(),
  257. AZ::SettingsRegistryInterface::Format::JsonMergePatch)
  258. && settingsRegistry.Get(projectBuildPath.Native(), AZ::SettingsRegistryMergeUtils::ProjectBuildPath))
  259. {
  260. // local Settings Registry will be used to merge the build_path.setreg for the supplied projectPath
  261. AZ::IO::FixedMaxPath buildConfigurationPath = (fixedProjectPath / projectBuildPath).LexicallyNormal();
  262. // First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
  263. // try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
  264. buildConfigurationPath /= "bin";
  265. AZStd::fixed_vector<AZ::IO::FixedMaxPath, 4> paths = {
  266. buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE / "Editor",
  267. buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE / "Editor"
  268. };
  269. // always try profile config because that is the default
  270. if (strcmp(AZ_BUILD_CONFIGURATION_TYPE, "profile") != 0)
  271. {
  272. paths.emplace_back(buildConfigurationPath / "profile" / "Editor");
  273. paths.emplace_back(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Editor");
  274. }
  275. for (auto& path : paths)
  276. {
  277. if(AZ::IO::SystemFile::Exists(path.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION).c_str()))
  278. {
  279. return path;
  280. }
  281. }
  282. }
  283. }
  284. // No Editor executable was found in the project build folder so if this project uses a
  285. // different engine we must find the Editor executable for that engine
  286. if(auto engineResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath.Native().data()); engineResult)
  287. {
  288. auto engineInfo = engineResult.GetValue<EngineInfo>();
  289. if (!engineInfo.m_thisEngine)
  290. {
  291. AZ::IO::FixedMaxPath fixedEnginePath{ engineInfo.m_path.toUtf8().constData() };
  292. // try the default sdk path
  293. // in the future we may be able to use additional .setreg entries to locate an alternate binary path
  294. if (editorPath = (fixedEnginePath / "bin" / AZ_TRAIT_OS_PLATFORM_CODENAME / "profile" / "Default" / "Editor").
  295. ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
  296. AZ::IO::SystemFile::Exists(editorPath.c_str()))
  297. {
  298. return editorPath;
  299. }
  300. return {};
  301. }
  302. }
  303. // Fall back to checking if an Editor exists in O3DE executable directory
  304. editorPath = AZ::IO::FixedMaxPath(AZ::Utils::GetExecutableDirectory()) / "Editor";
  305. editorPath.ReplaceExtension(AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
  306. if (AZ::IO::SystemFile::Exists(editorPath.c_str()))
  307. {
  308. return editorPath;
  309. }
  310. return {};
  311. }
  312. AZ::Outcome<QString, QString> CreateDesktopShortcut(const QString& filename, const QString& targetPath, const QStringList& arguments)
  313. {
  314. const QString cmd{"powershell.exe"};
  315. const QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
  316. const QString shortcutPath = QString("%1/%2.lnk").arg(desktopPath).arg(filename);
  317. const QString arg = QString("$s=(New-Object -COM WScript.Shell).CreateShortcut('%1');$s.TargetPath='%2';$s.Arguments='%3';$s.Save();")
  318. .arg(shortcutPath)
  319. .arg(targetPath)
  320. .arg(arguments.join(' '));
  321. auto createShortcutResult = ExecuteCommandResult(cmd, QStringList{"-Command", arg});
  322. if (!createShortcutResult.IsSuccess())
  323. {
  324. return AZ::Failure(QObject::tr("Failed to create desktop shortcut %1 <br><br>"
  325. "Please verify you have permission to create files at the specified location.<br><br> %2")
  326. .arg(shortcutPath)
  327. .arg(createShortcutResult.GetError()));
  328. }
  329. 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));
  330. }
  331. } // namespace ProjectUtils
  332. } // namespace O3DE::ProjectManager