Files
XCEngine/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp

373 lines
13 KiB
C++

#include "Scripting/EditorScriptAssemblyBuilder.h"
#include "Platform/Win32Utf8.h"
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
#include <windows.h>
#include <filesystem>
#include <string>
#include <vector>
#ifndef XCENGINE_EDITOR_REPO_ROOT
#define XCENGINE_EDITOR_REPO_ROOT ""
#endif
#ifndef XCENGINE_EDITOR_MONO_ROOT_DIR
#define XCENGINE_EDITOR_MONO_ROOT_DIR ""
#endif
namespace XCEngine {
namespace Editor {
namespace Scripting {
namespace {
std::filesystem::path GetFallbackRepositoryRoot() {
std::filesystem::path repoRoot = std::filesystem::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8()));
for (int i = 0; i < 3; ++i) {
if (repoRoot.has_parent_path()) {
repoRoot = repoRoot.parent_path();
}
}
return repoRoot.lexically_normal();
}
std::filesystem::path GetRepositoryRoot() {
const std::string configuredRoot = XCENGINE_EDITOR_REPO_ROOT;
if (!configuredRoot.empty()) {
return std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
}
return GetFallbackRepositoryRoot();
}
std::filesystem::path FindBundledMonoRootDirectory(const std::filesystem::path& repositoryRoot) {
std::error_code ec;
if (repositoryRoot.empty() || !std::filesystem::exists(repositoryRoot, ec)) {
return {};
}
for (std::filesystem::directory_iterator it(repositoryRoot, ec), end; it != end && !ec; it.increment(ec)) {
if (ec || !it->is_directory(ec)) {
continue;
}
const std::filesystem::path candidate =
it->path() / "Fermion" / "Fermion" / "external" / "mono";
if (std::filesystem::exists(candidate / "binary" / "mscorlib.dll", ec)) {
return candidate.lexically_normal();
}
}
return {};
}
std::filesystem::path GetMonoRootDirectory() {
const std::filesystem::path repositoryRoot = GetRepositoryRoot();
const std::filesystem::path bundledMonoRoot = FindBundledMonoRootDirectory(repositoryRoot);
if (!bundledMonoRoot.empty()) {
return bundledMonoRoot;
}
const std::string configuredRoot = XCENGINE_EDITOR_MONO_ROOT_DIR;
if (!configuredRoot.empty()) {
std::error_code ec;
const std::filesystem::path configuredPath =
std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
if (std::filesystem::exists(configuredPath / "binary" / "mscorlib.dll", ec)) {
return configuredPath;
}
}
return (repositoryRoot / "managed" / "mono").lexically_normal();
}
std::wstring QuotePath(const std::filesystem::path& path) {
return L"\"" + path.wstring() + L"\"";
}
bool FindExecutableOnPath(const wchar_t* executableName, std::filesystem::path& outPath) {
DWORD requiredLength = SearchPathW(nullptr, executableName, nullptr, 0, nullptr, nullptr);
if (requiredLength == 0) {
return false;
}
std::wstring buffer(requiredLength, L'\0');
const DWORD resolvedLength = SearchPathW(
nullptr,
executableName,
nullptr,
static_cast<DWORD>(buffer.size()),
buffer.data(),
nullptr);
if (resolvedLength == 0 || resolvedLength >= buffer.size()) {
return false;
}
buffer.resize(resolvedLength);
outPath = std::filesystem::path(buffer).lexically_normal();
return true;
}
bool RunProcessAndCapture(
const std::filesystem::path& executablePath,
const std::wstring& arguments,
const std::filesystem::path& workingDirectory,
DWORD& outExitCode,
std::string& outOutput) {
SECURITY_ATTRIBUTES securityAttributes = {};
securityAttributes.nLength = sizeof(securityAttributes);
securityAttributes.bInheritHandle = TRUE;
HANDLE readPipe = nullptr;
HANDLE writePipe = nullptr;
if (!CreatePipe(&readPipe, &writePipe, &securityAttributes, 0)) {
return false;
}
SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW startupInfo = {};
startupInfo.cb = sizeof(startupInfo);
startupInfo.dwFlags = STARTF_USESTDHANDLES;
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startupInfo.hStdOutput = writePipe;
startupInfo.hStdError = writePipe;
PROCESS_INFORMATION processInfo = {};
std::wstring commandLine = QuotePath(executablePath) + L" " + arguments;
std::vector<wchar_t> commandLineBuffer(commandLine.begin(), commandLine.end());
commandLineBuffer.push_back(L'\0');
const wchar_t* currentDirectory = workingDirectory.empty() ? nullptr : workingDirectory.c_str();
const BOOL created = CreateProcessW(
nullptr,
commandLineBuffer.data(),
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
currentDirectory,
&startupInfo,
&processInfo);
CloseHandle(writePipe);
writePipe = nullptr;
if (!created) {
CloseHandle(readPipe);
return false;
}
char buffer[4096] = {};
DWORD bytesRead = 0;
while (ReadFile(readPipe, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) {
outOutput.append(buffer, bytesRead);
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
GetExitCodeProcess(processInfo.hProcess, &outExitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
CloseHandle(readPipe);
return true;
}
std::wstring BuildCompilerArguments(
const std::filesystem::path& outputPath,
const std::vector<std::filesystem::path>& referencePaths,
const std::vector<std::filesystem::path>& sourcePaths) {
std::wstring arguments = L"/nologo /target:library /langversion:latest /nostdlib+ ";
arguments += L"/out:" + QuotePath(outputPath);
for (const std::filesystem::path& referencePath : referencePaths) {
arguments += L" /reference:" + QuotePath(referencePath);
}
for (const std::filesystem::path& sourcePath : sourcePaths) {
arguments += L" " + QuotePath(sourcePath);
}
return arguments;
}
EditorScriptAssemblyBuildResult BuildFailure(const std::string& message) {
return EditorScriptAssemblyBuildResult{false, message};
}
bool RunCSharpCompiler(
const std::filesystem::path& dotnetExecutable,
const std::filesystem::path& cscDllPath,
const std::filesystem::path& workingDirectory,
const std::filesystem::path& outputPath,
const std::vector<std::filesystem::path>& referencePaths,
const std::vector<std::filesystem::path>& sourcePaths,
std::string& outError) {
std::wstring arguments = QuotePath(cscDllPath);
arguments += L" ";
arguments += BuildCompilerArguments(outputPath, referencePaths, sourcePaths);
DWORD exitCode = 0;
std::string processOutput;
if (!RunProcessAndCapture(dotnetExecutable, arguments, workingDirectory, exitCode, processOutput)) {
outError = "Failed to launch dotnet to compile managed script assemblies.";
return false;
}
if (exitCode != 0) {
outError = processOutput.empty()
? "The C# compiler failed to build managed script assemblies."
: processOutput;
return false;
}
return true;
}
} // namespace
EditorScriptAssemblyBuildResult EditorScriptAssemblyBuilder::RebuildProjectAssemblies(const std::string& projectPath) {
namespace fs = std::filesystem;
try {
if (projectPath.empty()) {
return BuildFailure("Cannot rebuild script assemblies without a loaded project.");
}
const fs::path projectRoot = fs::path(Platform::Utf8ToWide(projectPath)).lexically_normal();
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path monoRoot = GetMonoRootDirectory();
const fs::path managedRoot = repositoryRoot / "managed";
const fs::path scriptCoreSourceRoot = managedRoot / "XCEngine.ScriptCore";
const fs::path outputDirectory = projectRoot / "Library" / "ScriptAssemblies";
const fs::path generatedDirectory = outputDirectory / "Generated";
const fs::path scriptCoreOutputPath = outputDirectory / "XCEngine.ScriptCore.dll";
const fs::path gameScriptsOutputPath = outputDirectory / "GameScripts.dll";
const fs::path corlibOutputPath = outputDirectory / "mscorlib.dll";
const fs::path monoCorlibSourcePath = monoRoot / "binary" / "mscorlib.dll";
const fs::path frameworkReferenceDirectory =
L"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2";
std::error_code ec;
fs::create_directories(outputDirectory, ec);
if (ec) {
return BuildFailure("Failed to create the project script assembly directory: " +
ScriptBuilderPathToUtf8(outputDirectory));
}
fs::path dotnetExecutable;
if (!FindExecutableOnPath(L"dotnet.exe", dotnetExecutable)) {
return BuildFailure("dotnet.exe was not found on PATH.");
}
std::string sdkListOutput;
DWORD sdkListExitCode = 0;
if (!RunProcessAndCapture(dotnetExecutable, L"--list-sdks", projectRoot, sdkListExitCode, sdkListOutput) ||
sdkListExitCode != 0) {
return BuildFailure("Failed to query installed .NET SDKs with dotnet --list-sdks.");
}
const std::string sdkVersion = ParseLatestDotnetSdkVersion(sdkListOutput);
if (sdkVersion.empty()) {
return BuildFailure("Failed to resolve a usable .NET SDK version from dotnet --list-sdks.");
}
const fs::path cscDllPath =
fs::path(L"C:\\Program Files\\dotnet\\sdk") /
fs::path(Platform::Utf8ToWide(sdkVersion)) /
"Roslyn" /
"bincore" /
"csc.dll";
if (!fs::exists(cscDllPath, ec)) {
return BuildFailure("Roslyn csc.dll was not found: " + ScriptBuilderPathToUtf8(cscDllPath));
}
const std::vector<fs::path> frameworkReferences = {
frameworkReferenceDirectory / "mscorlib.dll",
frameworkReferenceDirectory / "System.dll",
frameworkReferenceDirectory / "System.Core.dll"
};
for (const fs::path& referencePath : frameworkReferences) {
if (!fs::exists(referencePath, ec)) {
return BuildFailure("Required .NET Framework reference is missing: " +
ScriptBuilderPathToUtf8(referencePath));
}
}
if (!fs::exists(monoCorlibSourcePath, ec)) {
return BuildFailure("Mono corlib was not found: " + ScriptBuilderPathToUtf8(monoCorlibSourcePath));
}
std::vector<fs::path> scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot);
if (scriptCoreSources.empty()) {
return BuildFailure("No ScriptCore C# source files were found under: " +
ScriptBuilderPathToUtf8(scriptCoreSourceRoot));
}
std::vector<fs::path> projectScriptSources = CollectCSharpSourceFiles(projectRoot / "Assets");
std::string placeholderError;
if (!EnsurePlaceholderProjectScriptSource(
projectScriptSources,
generatedDirectory / "EmptyProjectGameScripts.cs",
placeholderError)) {
return BuildFailure(placeholderError);
}
std::string compileError;
if (!RunCSharpCompiler(
dotnetExecutable,
cscDllPath,
projectRoot,
scriptCoreOutputPath,
frameworkReferences,
scriptCoreSources,
compileError)) {
return BuildFailure("Failed to build XCEngine.ScriptCore.dll: " + compileError);
}
// Mono can keep the project-local corlib mapped for the lifetime of the process.
// Once it exists in the output folder, reuse it across incremental rebuilds.
ec.clear();
const bool hasProjectCorlib = fs::exists(corlibOutputPath, ec);
if (ec) {
return BuildFailure("Failed to inspect the project script assembly corlib path.");
}
if (!hasProjectCorlib) {
fs::copy_file(monoCorlibSourcePath, corlibOutputPath, fs::copy_options::overwrite_existing, ec);
if (ec) {
return BuildFailure("Failed to copy mscorlib.dll into the project script assembly directory.");
}
}
std::vector<fs::path> gameScriptReferences = frameworkReferences;
gameScriptReferences.push_back(scriptCoreOutputPath);
if (!RunCSharpCompiler(
dotnetExecutable,
cscDllPath,
projectRoot,
gameScriptsOutputPath,
gameScriptReferences,
projectScriptSources,
compileError)) {
return BuildFailure("Failed to build GameScripts.dll: " + compileError);
}
return EditorScriptAssemblyBuildResult{
true,
"Rebuilt script assemblies in " + ScriptBuilderPathToUtf8(outputDirectory)
};
} catch (const std::exception& exception) {
return BuildFailure("Script assembly rebuild threw an exception: " + std::string(exception.what()));
} catch (...) {
return BuildFailure("Script assembly rebuild threw an unknown exception.");
}
}
} // namespace Scripting
} // namespace Editor
} // namespace XCEngine