Files
FlaxEngine/Source/Editor/Scripting/ScriptsBuilder.cpp

678 lines
20 KiB
C++

// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
#include "ScriptsBuilder.h"
#include "CodeEditor.h"
#include "Editor/Editor.h"
#include "Editor/ProjectInfo.h"
#include "Editor/Managed/ManagedEditor.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringBuilder.h"
#include "Engine/Debug/Exceptions/FileNotFoundException.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/FileSystemWatcher.h"
#include "Engine/Threading/ThreadPool.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Scripting/MainThreadManagedInvokeAction.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Level/Level.h"
#include "FlaxEngine.Gen.h"
enum class EventType
{
Unknown = -1,
CompileBegin = 0,
CompileStarted = 1,
CompileEndGood = 2,
CompileEndFailed = 3,
ReloadCalled = 4,
ReloadBegin = 5,
Reload = 6,
ReloadEnd = 7,
};
struct EventData
{
EventType Type;
};
namespace ScriptsBuilderImpl
{
CriticalSection _locker;
bool _isInited = false;
bool _isCompileRequested = false;
bool _isCompileRunning = false;
bool _wasProjectStructureChanged = false;
bool _lastCompilationFailed = false;
int32 _compilationsCount = 0;
DateTime _lastSourceCodeEdited = 0;
DateTime _lastCompileAction = 0;
Array<FileSystemWatcher*> SourceFoldersWatchers;
CriticalSection _compileEventsLocker;
Array<EventData> _compileEvents;
MMethod* Internal_OnEvent = nullptr;
MMethod* Internal_OnCompileEvent = nullptr;
MMethod* Internal_OnCodeEditorEvent = nullptr;
void CallEvent(EventType type);
void CallCompileEvent(EventData& data);
void CallCodeEditorEvent(bool isEnd);
void sourceDirEvent(const String& path, FileSystemAction action);
void onEditorAssemblyUnloading(MAssembly* assembly);
void onScriptsReloadStart();
void onScriptsReload();
void onScriptsReloadEnd();
void GetClassName(const MString& fullname, MString& className);
void onCodeEditorAsyncOpenBegin()
{
CallCodeEditorEvent(false);
}
void onCodeEditorAsyncOpenEnd()
{
CallCodeEditorEvent(true);
}
bool compileGameScriptsAsyncInner();
bool compileGameScriptsAsync();
}
using namespace ScriptsBuilderImpl;
class ScriptsBuilderService : public EngineService
{
public:
ScriptsBuilderService()
: EngineService(TEXT("Scripts Builder"))
{
}
bool Init() override;
void Update() override;
void Dispose() override;
};
ScriptsBuilderService ScriptsBuilderServiceInstance;
Delegate<bool> ScriptsBuilder::OnCompilationEnd;
Action ScriptsBuilder::OnCompilationSuccess;
Action ScriptsBuilder::OnCompilationFailed;
void ScriptsBuilderImpl::sourceDirEvent(const String& path, FileSystemAction action)
{
// Discard non-source files or generated files
if ((!path.EndsWith(TEXT(".cs")) &&
!path.EndsWith(TEXT(".cpp")) &&
!path.EndsWith(TEXT(".h"))) ||
path.EndsWith(TEXT(".Gen.cs")))
return;
ScopeLock scopeLock(_locker);
_lastSourceCodeEdited = DateTime::Now();
}
int32 ScriptsBuilder::GetCompilationsCount()
{
Platform::MemoryBarrier();
return _compilationsCount;
}
bool ScriptsBuilder::LastCompilationFailed()
{
return _lastCompilationFailed;
}
void ScriptsBuilder::FilterNamespaceText(String& value)
{
value.Replace(TEXT(" "), TEXT(""), StringSearchCase::CaseSensitive);
value.Replace(TEXT("."), TEXT(""), StringSearchCase::CaseSensitive);
value.Replace(TEXT("-"), TEXT(""), StringSearchCase::CaseSensitive);
}
bool ScriptsBuilder::IsSourceDirty()
{
ScopeLock scopeLock(_locker);
return _lastSourceCodeEdited > _lastCompileAction;
}
bool ScriptsBuilder::IsSourceWorkspaceDirty()
{
ScopeLock scopeLock(_locker);
return _wasProjectStructureChanged;
}
bool ScriptsBuilder::IsSourceDirtyFor(const TimeSpan& timeout)
{
ScopeLock scopeLock(_locker);
return _lastSourceCodeEdited > (_lastCompileAction + timeout);
}
bool ScriptsBuilder::IsCompiling()
{
ScopeLock scopeLock(_locker);
return _isCompileRunning;
}
bool ScriptsBuilder::IsReady()
{
ScopeLock scopeLock(_locker);
return !IsSourceDirty() && !_isCompileRequested && !_isCompileRunning;
}
void ScriptsBuilder::MarkWorkspaceDirty()
{
SCRIPTING_EXPORT("FlaxEditor.ScriptsBuilder::Internal_MarkWorkspaceDirty")
ScopeLock scopeLock(_locker);
_lastSourceCodeEdited = DateTime::Now();
_wasProjectStructureChanged = true;
}
void ScriptsBuilder::CheckForCompile()
{
SCRIPTING_EXPORT("FlaxEditor.ScriptsBuilder::Internal_CheckForCompile")
ScopeLock scopeLock(_locker);
if (IsSourceDirty())
Compile();
}
void ScriptsBuilderImpl::onScriptsReloadStart()
{
CallEvent(EventType::ReloadBegin);
}
void ScriptsBuilderImpl::onScriptsReload()
{
CallEvent(EventType::Reload);
}
void ScriptsBuilderImpl::onScriptsReloadEnd()
{
CallEvent(EventType::ReloadEnd);
}
void ScriptsBuilder::Compile()
{
SCRIPTING_EXPORT("FlaxEditor.ScriptsBuilder::Internal_Compile")
ScopeLock scopeLock(_locker);
// Request compile job
_isCompileRequested = true;
}
bool ScriptsBuilder::RunBuildTool(const StringView& args, const StringView& workingDir)
{
PROFILE_CPU();
const String buildToolPath = Globals::StartupFolder / TEXT("Binaries/Tools/Flax.Build.exe");
if (!FileSystem::FileExists(buildToolPath))
{
Log::FileNotFoundException(buildToolPath).SetLevel(LogType::Fatal);
return true;
}
// Prepare build options
StringBuilder cmdLine(args.Length() + buildToolPath.Length() + 200);
#if PLATFORM_LINUX || PLATFORM_MAC
const String monoPath = Globals::MonoPath / TEXT("bin/mono");
if (!FileSystem::FileExists(monoPath))
{
Log::FileNotFoundException(monoPath).SetLevel(LogType::Fatal);
return true;
}
//const String monoPath = TEXT("mono");
cmdLine.Append(TEXT("\""));
cmdLine.Append(monoPath);
cmdLine.Append(TEXT("\" "));
#endif
cmdLine.Append(TEXT("\""));
cmdLine.Append(buildToolPath);
cmdLine.Append(TEXT("\" "));
cmdLine.Append(args.Get(), args.Length());
// TODO: Set env var for the mono MONO_GC_PARAMS=nursery-size64m to boost build performance -> profile it
// Call build tool
const int32 result = Platform::RunProcess(StringView(*cmdLine, cmdLine.Length()), workingDir);
return result != 0;
}
bool ScriptsBuilder::GenerateProject(const StringView& customArgs)
{
String args(TEXT("-log -genproject "));
args += customArgs;
_wasProjectStructureChanged = false;
return RunBuildTool(args);
}
void ScriptsBuilderImpl::GetClassName(const MString& fullname, MString& className)
{
const auto lastDotIndex = fullname.FindLast('.');
if (lastDotIndex != -1)
{
//namespaceName = fullname.Substring(0, lastDotIndex);
className = fullname.Substring(lastDotIndex + 1);
}
else
{
className = fullname;
}
}
MClass* ScriptsBuilder::FindScript(const StringView& scriptName)
{
PROFILE_CPU();
const MString scriptNameStd = scriptName.ToStringAnsi();
const ScriptingTypeHandle scriptingType = Scripting::FindScriptingType(scriptNameStd);
if (scriptingType)
{
MClass* mclass = scriptingType.GetType().ManagedClass;
if (mclass)
{
return mclass;
}
}
// Check all assemblies (ignoring the typename namespace)
auto& modules = BinaryModule::GetModules();
MString className;
GetClassName(scriptNameStd, className);
MClass* scriptClass = Script::GetStaticClass();
for (int32 j = 0; j < modules.Count(); j++)
{
auto managedModule = dynamic_cast<ManagedBinaryModule*>(modules[j]);
if (!managedModule)
continue;
auto assembly = managedModule->Assembly;
auto& classes = assembly->GetClasses();
for (auto i = classes.Begin(); i.IsNotEnd(); ++i)
{
MClass* mclass = i->Value;
// Managed scripts
if (mclass->IsSubClassOf(scriptClass) && !mclass->IsStatic() && !mclass->IsAbstract() && !mclass->IsInterface())
{
MString mclassName;
GetClassName(mclass->GetFullName(), mclassName);
if (className == mclassName)
{
LOG(Info, "Found {0} type for type {1} (assembly {2})", String(mclass->GetFullName()), String(scriptName.Get(), scriptName.Length()), assembly->ToString());
return mclass;
}
}
}
}
LOG(Warning, "Failed to find script class of name {0}", String(scriptNameStd));
return nullptr;
}
void ScriptsBuilder::GetExistingEditors(int32* result, int32 count)
{
auto& editors = CodeEditingManager::GetEditors();
for (int32 i = 0; i < editors.Count() && i < count; i++)
{
result[static_cast<int32>(editors[i]->GetType())] = 1;
}
}
void ScriptsBuilder::GetBinariesConfiguration(StringView& target, StringView& platform, StringView& architecture, StringView& configuration)
{
const Char *targetPtr, *platformPtr, *architecturePtr, *configurationPtr;
GetBinariesConfiguration(targetPtr, platformPtr, architecturePtr, configurationPtr);
target = targetPtr;
platform = platformPtr;
architecture = architecturePtr;
configuration = configurationPtr;
}
void ScriptsBuilder::GetBinariesConfiguration(const Char*& target, const Char*& platform, const Char*& architecture, const Char*& configuration)
{
// Special case when opening engine project
if (Editor::Project->ProjectFolderPath == Globals::StartupFolder)
{
target = platform = architecture = configuration = nullptr;
return;
}
// Pick game target
if (Editor::Project->EditorTarget.HasChars())
{
target = Editor::Project->EditorTarget.Get();
}
else if (Editor::Project->GameTarget.HasChars())
{
target = Editor::Project->GameTarget.Get();
}
else
{
target = TEXT("");
LOG(Warning, "Missing editor/game targets in project. Please specify EditorTarget and GameTarget properties in .flaxproj file.");
}
#if PLATFORM_WINDOWS
platform = TEXT("Windows");
#elif PLATFORM_LINUX
platform = TEXT("Linux");
#elif PLATFORM_MAC
platform = TEXT("Mac");
#else
#error "Unknown platform"
#endif
#if PLATFORM_ARCH_X64
architecture = TEXT("x64");
#elif PLATFORM_ARCH_X86
architecture = TEXT("x86");
#else
#error "Unknown architecture"
#endif
#if BUILD_DEBUG
configuration = TEXT("Debug");
#elif BUILD_DEVELOPMENT
configuration = TEXT("Development");
#elif BUILD_RELEASE
configuration = TEXT("Release");
#else
#error "Unknown configuration"
#endif
}
bool ScriptsBuilderImpl::compileGameScriptsAsyncInner()
{
LOG(Info, "Starting scripts compilation...");
CallEvent(EventType::CompileStarted);
// Call compilation
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (StringUtils::Length(target) == 0)
{
LOG(Info, "Missing EditorTarget in project. Skipping compilation.");
CallEvent(EventType::ReloadCalled);
Scripting::Reload();
return false;
}
auto args = String::Format(
TEXT("-log -logfile= -build -mutex -buildtargets={0} -skiptargets=FlaxEditor -platform={1} -arch={2} -configuration={3}"),
target, platform, architecture, configuration);
if (Scripting::HasGameModulesLoaded())
{
// Add postfix to output binaries to prevent file locking collisions when doing hot-reload in Editor
args += String::Format(TEXT(" -hotreload=\".HotReload.{0}\""), _compilationsCount - 1);
}
if (ScriptsBuilder::RunBuildTool(args))
return true;
// Reload scripts
CallEvent(EventType::ReloadCalled);
Scripting::Reload();
return false;
}
void ScriptsBuilderImpl::CallEvent(EventType type)
{
ScopeLock lock(_compileEventsLocker);
const int32 index = _compileEvents.Count();
_compileEvents.AddDefault(1);
auto& data = _compileEvents[index];
data.Type = type;
// Flush events on a main tread
if (IsInMainThread())
{
for (int32 i = 0; i < _compileEvents.Count(); i++)
CallCompileEvent(_compileEvents[i]);
_compileEvents.Clear();
}
}
void ScriptsBuilderImpl::CallCompileEvent(EventData& data)
{
ASSERT(IsInMainThread());
// Special case for a single events with no data
if (data.Type != EventType::Unknown)
{
// Call C# event
if (Internal_OnEvent == nullptr)
{
auto scriptsBuilderClass = ScriptsBuilder::GetStaticClass();
if (scriptsBuilderClass)
Internal_OnEvent = scriptsBuilderClass->GetMethod("Internal_OnEvent", 1);
if (Internal_OnEvent == nullptr)
{
LOG(Fatal, "Invalid Editor assembly!");
}
}
/*MObject* exception = nullptr;
void* args[1];
args[0] = &data.Type;
Internal_OnEvent->Invoke(nullptr, args, &exception);
if (exception)
{
DebugLog::LogException(exception);
}*/
MainThreadManagedInvokeAction::ParamsBuilder params;
params.AddParam(data.Type);
MainThreadManagedInvokeAction::Invoke(Internal_OnEvent, params);
}
}
void ScriptsBuilderImpl::CallCodeEditorEvent(bool isEnd)
{
if (Internal_OnCodeEditorEvent == nullptr)
{
auto scriptsBuilderClass = ScriptsBuilder::GetStaticClass();
if (scriptsBuilderClass)
Internal_OnCodeEditorEvent = scriptsBuilderClass->GetMethod("Internal_OnCodeEditorEvent", 1);
ASSERT(Internal_OnCodeEditorEvent);
}
MainThreadManagedInvokeAction::ParamsBuilder params;
params.AddParam(isEnd);
MainThreadManagedInvokeAction::Invoke(Internal_OnCodeEditorEvent, params);
}
void ScriptsBuilderImpl::onEditorAssemblyUnloading(MAssembly* assembly)
{
Internal_OnEvent = nullptr;
Internal_OnCompileEvent = nullptr;
Internal_OnCodeEditorEvent = nullptr;
}
bool ScriptsBuilderImpl::compileGameScriptsAsync()
{
// Start
{
ScopeLock scopeLock(_locker);
_isCompileRequested = false;
_lastCompileAction = DateTime::Now();
_compilationsCount++;
_isCompileRunning = true;
ScriptsBuilderServiceInstance.Init();
CallEvent(EventType::CompileBegin);
}
// Do work
const bool success = !compileGameScriptsAsyncInner();
// End
{
ScopeLock scopeLock(_locker);
_lastCompilationFailed = !success;
ScriptsBuilder::OnCompilationEnd(success);
if (success)
ScriptsBuilder::OnCompilationSuccess();
else
ScriptsBuilder::OnCompilationFailed();
if (success)
CallEvent(EventType::CompileEndGood);
else
CallEvent(EventType::CompileEndFailed);
_isCompileRunning = false;
}
return false;
}
bool ScriptsBuilderService::Init()
{
// Check flag
if (_isInited)
return false;
_isInited = true;
// Link for Editor assembly unload event to clear cached Internal_OnCompilationEnd to prevent errors
auto editorAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly;
editorAssembly->Unloading.Bind(onEditorAssemblyUnloading);
// Listen to scripts reloading events and forward them to c#
Level::ScriptsReloadStart.Bind(onScriptsReloadStart);
Level::ScriptsReload.Bind(onScriptsReload);
Level::ScriptsReloadEnd.Bind(onScriptsReloadEnd);
// Listen to code editors manager events
CodeEditingManager::AsyncOpenBegin.Bind(onCodeEditorAsyncOpenBegin);
CodeEditingManager::AsyncOpenEnd.Bind(onCodeEditorAsyncOpenEnd);
// Create source folder watcher to handle scripts modification events (create/delete scripts events are handled by the editor itself)
auto project = Editor::Project;
HashSet<ProjectInfo*> projects;
project->GetAllProjects(projects);
for (auto e : projects)
{
ProjectInfo* project = e.Item;
if (project->Name == TEXT("Flax"))
continue;
auto watcher = New<FileSystemWatcher>(project->ProjectFolderPath / TEXT("Source"), true);
watcher->OnEvent.Bind(sourceDirEvent);
SourceFoldersWatchers.Add(watcher);
}
// Verify project
if (project->EditorTarget.IsEmpty())
{
LOG(Warning, "Missing {0} property in opened project", TEXT("EditorTarget"));
}
if (project->GameTarget.IsEmpty())
{
LOG(Warning, "Missing {0} property in opened project", TEXT("GameTarget"));
}
// Remove any remaining files from previous Editor run hot-reloads
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (StringUtils::Length(target) != 0)
{
const String targetOutput = Globals::ProjectFolder / TEXT("Binaries") / target / platform / architecture / configuration;
Array<String> files;
FileSystem::DirectoryGetFiles(files, targetOutput, TEXT("*.HotReload.*"), DirectorySearchOption::TopDirectoryOnly);
if (files.HasItems())
LOG(Info, "Removing {0} files from previous Editor run hot-reloads", files.Count());
for (auto& file : files)
{
FileSystem::DeleteFile(file);
}
}
bool forceRecompile = false;
// Check last Editor version that was using a project is different from current
if (Editor::IsOldProjectOpened)
forceRecompile = true;
// Check if need to force recompile game scripts
if (forceRecompile)
{
LOG(Warning, "Forcing scripts recompilation");
FileSystem::DeleteDirectory(Globals::ProjectCacheFolder / TEXT("Intermediate"));
ScriptsBuilder::Compile();
}
return false;
}
void ScriptsBuilderService::Update()
{
// Send compilation events
{
ScopeLock scopeLock(_compileEventsLocker);
for (int32 i = 0; i < _compileEvents.Count(); i++)
CallCompileEvent(_compileEvents[i]);
_compileEvents.Clear();
}
// Check if compile code (if has been edited)
const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(50);
auto mainWindow = Engine::MainWindow;
if (ScriptsBuilder::IsSourceDirtyFor(timeToCallCompileIfDirty) && mainWindow && mainWindow->IsFocused())
{
// Check if auto reload is enabled
if (Editor::Managed->CanAutoReloadScripts())
ScriptsBuilder::Compile();
}
ScopeLock scopeLock(_locker);
// Check if compilation has been requested (some time ago since we want to batch calls to reduce amount of compilations)
if (_isCompileRequested)
{
// Check if compilation isn't running
if (!_isCompileRunning)
{
// Check if editor state can perform scripts reloading
if (Editor::Managed->CanReloadScripts())
{
// Call compilation (and switch flags)
_isCompileRequested = false;
_isCompileRunning = true;
Function<bool()> action(compileGameScriptsAsync);
Task::StartNew(action);
}
}
}
}
void ScriptsBuilderService::Dispose()
{
// Don't exit while scripts compilation is still running
if (ScriptsBuilder::IsCompiling())
{
LOG(Warning, "Scripts compilation is running, waiting for the end...");
int32 timeOutMilliseconds = 5000;
while (ScriptsBuilder::IsCompiling() && timeOutMilliseconds > 0)
{
const int sleepTimeMilliseconds = 50;
timeOutMilliseconds -= sleepTimeMilliseconds;
Platform::Sleep(sleepTimeMilliseconds);
}
LOG(Warning, "Scripts compilation wait ended");
}
SourceFoldersWatchers.ClearDelete();
}