You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Allows fast iteration of C++ changes without restarting the application. To use, select the "Live Coding (Experimental)" mode from the drop down menu next to the editor's compile button, or type "LiveCoding" into the console for a monolithic build. Press Ctrl+Alt+F11 to find changes and compile.
Changes vs standalone Live++ version:
* UBT is used to execute builds. This allows standard UE4 adaptive unity mode, allows us to reuse object files when we do regular builds, supports using any build executor allowed by UBT (XGE, SNDBS, etc..).
* Adding new source files is supported.
* Custom visualizer for FNames is supported via a weakly linked symbol in a static library (Engine/Extras/NatvisHelpers).
* Settings are exposed in the editor's project settings dialog.
* Standalone application has been rewritten as a Slate app ("LiveCodingConsole"). There is an additional option to start the program as hidden, where it will not be visible until Ctrl+Alt+F11 is hit. Similarly, closing the window will hide it instead of closing the application.
* Does not require a standalone licensed version of Live++.
Known issues:
* Does not currently support class layout changes / object reinstancing
#rb none
[FYI] Marc.Audy, Stefan.Boberg, Nick.Penwarden
#jira
#ROBOMERGE-SOURCE: CL 5304722 in //UE4/Release-4.22/...
#ROBOMERGE-BOT: RELEASE (Release-4.22 -> Main)
[CL 5309051 by ben marsh in Main branch]
545 lines
14 KiB
C++
545 lines
14 KiB
C++
// Copyright 2011-2019 Molecular Matters GmbH, all rights reserved.
|
|
|
|
#include "LC_Amalgamation.h"
|
|
#include "LC_StringUtil.h"
|
|
#include "LC_FileUtil.h"
|
|
#include "LC_GrowingMemoryBlock.h"
|
|
#include "LC_Logging.h"
|
|
|
|
|
|
namespace
|
|
{
|
|
static const char* const LPP_AMALGAMATION_PART= ".lpp_part.";
|
|
static const wchar_t* const LPP_AMALGAMATION_PART_WIDE = L".lpp_part.";
|
|
|
|
struct Database
|
|
{
|
|
static const uint32_t MAGIC_NUMBER = 0x4C505020; // "LPP "
|
|
static const uint32_t VERSION = 8u;
|
|
|
|
struct Dependency
|
|
{
|
|
std::string filename;
|
|
uint64_t timestamp;
|
|
};
|
|
};
|
|
|
|
|
|
// helper function to generate the database path for an .obj file
|
|
static std::wstring GenerateDatabasePath(const symbols::ObjPath& objPath)
|
|
{
|
|
std::wstring path = string::ToWideString(objPath.c_str());
|
|
path = file::RemoveExtension(path);
|
|
path += L".ldb";
|
|
|
|
return path;
|
|
}
|
|
|
|
|
|
// helper function to generate a timestamp for a file
|
|
static uint64_t GenerateTimestamp(const wchar_t* path)
|
|
{
|
|
const file::Attributes& attributes = file::GetAttributes(path);
|
|
return file::GetLastModificationTime(attributes);
|
|
}
|
|
|
|
|
|
// helper function to generate a database dependency for a file
|
|
static Database::Dependency GenerateDatabaseDependency(const ImmutableString& path)
|
|
{
|
|
return Database::Dependency { path.c_str(), GenerateTimestamp(string::ToWideString(path).c_str()) };
|
|
}
|
|
|
|
|
|
// helper function to generate a database dependency for a file, normalizing the path to the file
|
|
static Database::Dependency GenerateNormalizedDatabaseDependency(const ImmutableString& path)
|
|
{
|
|
const std::wstring widePath = string::ToWideString(path);
|
|
const std::wstring normalizedPath = file::NormalizePath(widePath.c_str());
|
|
return Database::Dependency { string::ToUtf8String(normalizedPath).c_str(), GenerateTimestamp(normalizedPath.c_str()) };
|
|
}
|
|
}
|
|
|
|
|
|
// serializes values and databases into an in-memory representation
|
|
namespace serializationToMemory
|
|
{
|
|
bool Write(const void* buffer, size_t size, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
return dbInMemory->Insert(buffer, size);
|
|
}
|
|
|
|
template <typename T>
|
|
bool Write(const T& value, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
return dbInMemory->Insert(&value, sizeof(T));
|
|
}
|
|
|
|
bool Write(const ImmutableString& str, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
// write length without null terminator and then the string
|
|
const uint32_t lengthWithoutNull = str.GetLength();
|
|
if (!Write(lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Write(str.c_str(), lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Write(const std::string& str, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
// write length without null terminator and then the string
|
|
const uint32_t lengthWithoutNull = static_cast<uint32_t>(str.length() * sizeof(char));
|
|
if (!Write(lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Write(str.c_str(), lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Write(const std::wstring& str, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
// write length without null terminator and then the string
|
|
const uint32_t lengthWithoutNull = static_cast<uint32_t>(str.length() * sizeof(wchar_t));
|
|
if (!Write(lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Write(str.c_str(), lengthWithoutNull, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Write(const Database::Dependency& dependency, GrowingMemoryBlock* dbInMemory)
|
|
{
|
|
if (!Write(dependency.filename, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Write(dependency.timestamp, dbInMemory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
// serializes values and databases from disk
|
|
namespace serializationFromDisk
|
|
{
|
|
struct ReadBuffer
|
|
{
|
|
const void* data;
|
|
uint64_t leftToRead;
|
|
};
|
|
|
|
|
|
bool Read(void* memory, size_t size, ReadBuffer* buffer)
|
|
{
|
|
// is there enough data left to read?
|
|
if (buffer->leftToRead < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
memcpy(memory, buffer->data, size);
|
|
buffer->data = static_cast<const char*>(buffer->data) + size;
|
|
buffer->leftToRead -= size;
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T>
|
|
bool Read(T& value, ReadBuffer* buffer)
|
|
{
|
|
return Read(&value, sizeof(T), buffer);
|
|
}
|
|
|
|
bool Read(std::string& str, ReadBuffer* buffer)
|
|
{
|
|
// read length first
|
|
uint32_t length = 0u;
|
|
if (!Read(length, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// read data
|
|
str.resize(length + 1u, '\0');
|
|
return Read(&str[0], length, buffer);
|
|
}
|
|
|
|
bool Read(Database::Dependency& dependency, ReadBuffer* buffer)
|
|
{
|
|
// read filename first
|
|
if (!Read(dependency.filename, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// read timestamp
|
|
return Read(dependency.timestamp, buffer);
|
|
}
|
|
|
|
|
|
bool Compare(const void* memory, size_t size, ReadBuffer* buffer)
|
|
{
|
|
// is there enough data left to read?
|
|
if (buffer->leftToRead < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const bool identical = (memcmp(memory, buffer->data, size) == 0);
|
|
if (!identical)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
buffer->data = static_cast<const char*>(buffer->data) + size;
|
|
buffer->leftToRead -= size;
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T>
|
|
bool Compare(const T& value, ReadBuffer* buffer)
|
|
{
|
|
return Compare(&value, sizeof(T), buffer);
|
|
}
|
|
|
|
bool Compare(const ImmutableString& str, ReadBuffer* buffer)
|
|
{
|
|
// compare length first
|
|
const uint32_t length = str.GetLength();
|
|
if (!Compare(length, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// compare data
|
|
return Compare(str.c_str(), length, buffer);
|
|
}
|
|
|
|
bool Compare(const std::string& str, ReadBuffer* buffer)
|
|
{
|
|
// compare length first
|
|
const uint32_t length = static_cast<uint32_t>(str.length() * sizeof(char));
|
|
if (!Compare(length, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// compare data
|
|
return Compare(str.c_str(), length, buffer);
|
|
}
|
|
|
|
bool Compare(const std::wstring& str, ReadBuffer* buffer)
|
|
{
|
|
// compare length first
|
|
const uint32_t length = static_cast<uint32_t>(str.length() * sizeof(wchar_t));
|
|
if (!Compare(length, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// compare data
|
|
return Compare(str.c_str(), length, buffer);
|
|
}
|
|
|
|
bool Compare(const Database::Dependency& dependency, ReadBuffer* buffer)
|
|
{
|
|
// compare filename first
|
|
if (!Compare(dependency.filename, buffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// compare timestamp
|
|
return Compare(dependency.timestamp, buffer);
|
|
}
|
|
}
|
|
|
|
|
|
bool amalgamation::IsPartOfAmalgamation(const char* normalizedObjPath)
|
|
{
|
|
return string::Contains(normalizedObjPath, LPP_AMALGAMATION_PART);
|
|
}
|
|
|
|
|
|
bool amalgamation::IsPartOfAmalgamation(const wchar_t* normalizedObjPath)
|
|
{
|
|
return string::Contains(normalizedObjPath, LPP_AMALGAMATION_PART_WIDE);
|
|
}
|
|
|
|
|
|
std::wstring amalgamation::CreateObjPart(const std::wstring& normalizedFilename)
|
|
{
|
|
std::wstring newObjPart(LPP_AMALGAMATION_PART_WIDE);
|
|
newObjPart += file::RemoveExtension(file::GetFilename(normalizedFilename));
|
|
newObjPart += L".obj";
|
|
|
|
return newObjPart;
|
|
}
|
|
|
|
|
|
std::wstring amalgamation::CreateObjPath(const std::wstring& normalizedAmalgamatedObjPath, const std::wstring& objPart)
|
|
{
|
|
std::wstring newObjPath(normalizedAmalgamatedObjPath);
|
|
newObjPath = file::RemoveExtension(newObjPath);
|
|
newObjPath += objPart;
|
|
|
|
return newObjPath;
|
|
}
|
|
|
|
|
|
bool amalgamation::ReadAndCompareDatabase(const symbols::ObjPath& objPath, const std::wstring& compilerPath, const symbols::Compiland* compiland, const std::wstring& additionalCompilerOptions)
|
|
{
|
|
// check if the .obj is there. if not, there is no need to check the database at all.
|
|
{
|
|
const file::Attributes& objAttributes = file::GetAttributes(string::ToWideString(objPath).c_str());
|
|
if (!file::DoesExist(objAttributes))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const std::wstring databasePath = GenerateDatabasePath(objPath);
|
|
const file::Attributes& fileAttributes = file::GetAttributes(databasePath.c_str());
|
|
if (!file::DoesExist(fileAttributes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const uint64_t bytesLeftToRead = file::GetSize(fileAttributes);
|
|
if (bytesLeftToRead == 0u)
|
|
{
|
|
LC_LOG_DEV("Failed to retrieve size of database file %S", databasePath.c_str());
|
|
return false;
|
|
}
|
|
|
|
file::MemoryFile* memoryFile = file::Open(databasePath.c_str(), file::OpenMode::READ_ONLY);
|
|
if (!memoryFile)
|
|
{
|
|
// database cannot be opened, treat as if a change was detected
|
|
return false;
|
|
}
|
|
|
|
// start reading the database from disk, comparing against the compiland's database at the same time
|
|
serializationFromDisk::ReadBuffer readBuffer { file::GetData(memoryFile), bytesLeftToRead };
|
|
if (!serializationFromDisk::Compare(Database::MAGIC_NUMBER, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Wrong magic number in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(Database::VERSION, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Version has changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(compilerPath, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Compiler path has changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(GenerateTimestamp(compilerPath.c_str()), &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Compiler timestamp has changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(compiland->commandLine, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Compiland compiler options have changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(additionalCompilerOptions, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Additional compiler options have changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
if (!serializationFromDisk::Compare(GenerateNormalizedDatabaseDependency(compiland->srcPath), &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Source file has changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
// dependencies need to be treated differently, because the current list of files might differ from the one
|
|
// stored in the database. this is not a problem however, because the database is always kept up-to-date as
|
|
// soon as a file was compiled.
|
|
// we need to read all files from the database and check their timestamp against the timestamp of the file
|
|
// on disk.
|
|
{
|
|
uint32_t count = 0u;
|
|
if (!serializationFromDisk::Read(count, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Failed to read dependency count in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0u; i < count; ++i)
|
|
{
|
|
Database::Dependency dependency = {};
|
|
if (!serializationFromDisk::Read(dependency, &readBuffer))
|
|
{
|
|
LC_LOG_DEV("Failed to read dependency in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
|
|
// check dependency timestamp
|
|
const file::Attributes& attributes = file::GetAttributes(string::ToWideString(dependency.filename).c_str());
|
|
if (file::GetLastModificationTime(attributes) != dependency.timestamp)
|
|
{
|
|
LC_LOG_DEV("Dependency has changed in database file %S", databasePath.c_str());
|
|
|
|
file::Close(memoryFile);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// no change detected
|
|
file::Close(memoryFile);
|
|
return true;
|
|
}
|
|
|
|
|
|
void amalgamation::WriteDatabase(const symbols::ObjPath& objPath, const std::wstring& compilerPath, const symbols::Compiland* compiland, const std::wstring& additionalCompilerOptions)
|
|
{
|
|
// first serialize the database to memory and then write it to disk in one go.
|
|
// note that we write the database to a temporary file first, and then move it to its final destination.
|
|
// because moving is atomic, this ensures that databases are either fully written or not at all.
|
|
GrowingMemoryBlock dbInMemory(1u * 1024u * 1024u);
|
|
if (!serializationToMemory::Write(Database::MAGIC_NUMBER, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!serializationToMemory::Write(Database::VERSION, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!serializationToMemory::Write(compilerPath, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!serializationToMemory::Write(GenerateTimestamp(compilerPath.c_str()), &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!serializationToMemory::Write(compiland->commandLine, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!serializationToMemory::Write(additionalCompilerOptions, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
// the source file itself is treated as a dependency
|
|
if (!serializationToMemory::Write(GenerateNormalizedDatabaseDependency(compiland->srcPath), &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
// write all file dependencies
|
|
{
|
|
const bool hasDependencies = (compiland->sourceFiles != nullptr);
|
|
const uint32_t count = hasDependencies
|
|
? static_cast<uint32_t>(compiland->sourceFiles->files.size())
|
|
: 0u;
|
|
|
|
if (!serializationToMemory::Write(count, &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
if (hasDependencies)
|
|
{
|
|
const types::vector<ImmutableString>& sourceFiles = compiland->sourceFiles->files;
|
|
for (uint32_t i = 0u; i < count; ++i)
|
|
{
|
|
const ImmutableString& sourcePath = sourceFiles[i];
|
|
if (!serializationToMemory::Write(GenerateDatabaseDependency(sourcePath), &dbInMemory))
|
|
{
|
|
LC_LOG_DEV("Failed to serialize database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::wstring databasePath = GenerateDatabasePath(objPath);
|
|
std::wstring tempDatabasePath = databasePath;
|
|
tempDatabasePath += L".tmp";
|
|
|
|
if (!file::CreateFileWithData(tempDatabasePath.c_str(), dbInMemory.GetData(), dbInMemory.GetSize()))
|
|
{
|
|
LC_LOG_DEV("Failed to write database for compiland %s", objPath.c_str());
|
|
return;
|
|
}
|
|
|
|
file::Move(tempDatabasePath.c_str(), databasePath.c_str());
|
|
}
|
|
|
|
|
|
void amalgamation::DeleteDatabase(const symbols::ObjPath& objPath)
|
|
{
|
|
const std::wstring& databasePath = GenerateDatabasePath(objPath);
|
|
file::DeleteIfExists(databasePath.c_str());
|
|
}
|