feat: expand editor scripting asset and viewport flow
This commit is contained in:
372
editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
Normal file
372
editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user