#include "Scripting/EditorScriptAssemblyBuilder.h" #include "Platform/Win32Utf8.h" #include "Scripting/EditorScriptAssemblyBuilderUtils.h" #include #include #include #include #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(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 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& referencePaths, const std::vector& 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& referencePaths, const std::vector& 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 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 scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot); if (scriptCoreSources.empty()) { return BuildFailure("No ScriptCore C# source files were found under: " + ScriptBuilderPathToUtf8(scriptCoreSourceRoot)); } std::vector 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 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