373 lines
13 KiB
C++
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
|