diff --git a/CMakeLists.txt b/CMakeLists.txt index c057b31..1d6b6a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,312 +9,7 @@ option(B_EMBED_SILENCE_DEVMODE_WARNING "Disable the battery::embed devmode warni # Remember the binary dir for later set(EMBED_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/embed CACHE INTERNAL "binary directory of the battery::embed library" FORCE) - -# The template for how to generate the .cpp file -set(EMBED_SOURCE_FILE_TEMPLATE [==[ -// File generated using battery::embed (https://github.com/batterycenter/embed) -// Embedded file: '${FILENAME}' as '${IDENTIFIER}' -// Filesize: ${FILESIZE} bytes -// DO NOT EDIT THIS FILE!!! - -#include "battery/embed.hpp" - -namespace b { - - EmbedInternal::EmbeddedFile EmbedInternal::${IDENTIFIER} = { - std::string_view( - ${GENERATED_BYTE_ARRAY}, - ${FILESIZE} - ) - , "${FILENAME}" -#ifndef B_PRODUCTION_MODE - , "${FULL_PATH}" -#endif // !B_PRODUCTION_MODE - }; - -} // namespace b -]==]) -file(WRITE ${EMBED_BINARY_DIR}/embed_source_file_template.cpp "${EMBED_SOURCE_FILE_TEMPLATE}") - -# The single b::embed master source file which is compiled into the app -set(EMBED_MASTER_SOURCE_FILE [==[ -// File generated using battery::embed (https://github.com/batterycenter/embed) -// DO NOT EDIT THIS FILE!!! - -#include "battery/embed.hpp" - -#if !defined(B_PRODUCTION_MODE) && !defined(B_OS_WEB) -#include -#include -#include -#include -#include -#include -#include -#endif // !B_PRODUCTION_MODE and !B_OS_WEB - -namespace b { - -#if !defined(B_PRODUCTION_MODE) && !defined(B_OS_WEB) - - // All this crazyness is necessary because we need to use std::string_view because of constexpr, - // and the string_view only points to a char array, so we need a string that outlives - // the string_view it is assigned to. - - struct FileData { - std::string filename; - std::string newContent; - std::string filepath; - std::filesystem::file_time_type lastWriteTime; - std::function callback; - }; - - static std::mutex embeddedFilesMapMutex; - static std::unordered_map embeddedFiles; - -#ifdef _WIN32 -#ifdef __clang__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#endif - - static std::wstring embed_widen(const std::string& str) { - std::wstring_convert> converter; - return converter.from_bytes(str); - } - -#ifdef __clang__ -#pragma GCC diagnostic pop -#endif - -#endif // _WIN32 - - static std::optional embed_read_file(const std::string_view& filename) { -#ifdef _WIN32 - std::ifstream file(embed_widen(std::string(filename)).c_str(), std::ios::binary); -#else // _WIN32 - std::ifstream file(std::string(filename), std::ios::binary); -#endif // _WIN32 - if (file.fail()) { - return {}; - } - return std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - } - - // Apple-Clang does not support std::jthread yet, hence we make our own version of jthread here: - class AutoThread { - public: - template - AutoThread(Tfunc&& function) - : m_thread([&]() { function(m_shouldStop); }) - {} - - ~AutoThread() { - m_shouldStop = true; - m_thread.join(); - } - - AutoThread(const AutoThread& other) = delete; - AutoThread(AutoThread&& other) = delete; - AutoThread& operator=(const AutoThread& other) = delete; - AutoThread& operator=(AutoThread&& other) = delete; - - private: - std::thread m_thread; - std::atomic m_shouldStop = false; - }; - - // This is a global file watcher thread that is created at program start and automatically destroyed at program end - // All embedded files are hot-reloaded by this thread. - AutoThread embeddedFileWatcherThread = AutoThread([](std::atomic& shouldStop) { - while (!shouldStop) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - std::lock_guard lock(embeddedFilesMapMutex); - for (auto& [key, filedata] : embeddedFiles) { - std::filesystem::path filepath(key); - std::error_code ec; - auto newWriteTime = std::filesystem::last_write_time(filepath, ec); - if (!ec) { - if (newWriteTime != filedata.lastWriteTime) { - filedata.lastWriteTime = newWriteTime; - auto fileresult = embed_read_file(key); - if (!fileresult) { - continue; - } - filedata.newContent = *fileresult; - EmbedInternal::EmbeddedFile newFile(filedata.newContent, filedata.filename, filedata.filepath); - filedata.callback(newFile); - } - } - } - } - }); - -#endif // !B_PRODUCTION_MODE and !B_OS_WEB - - void EmbedInternal::EmbeddedFile::get(const std::function& callback) { -#if defined(B_PRODUCTION_MODE) || defined(B_OS_WEB) - callback(*this); -#else // B_PRODUCTION_MODE or B_OS_WEB - auto fileresult = embed_read_file(m_fullFilepath); - if (fileresult) { - m_data = *fileresult; - } - callback(*this); - - FileData fileData; - fileData.filename = m_filename; - fileData.newContent = m_data; - fileData.filepath = m_fullFilepath; - fileData.callback = callback; - fileData.lastWriteTime = std::filesystem::last_write_time(m_fullFilepath); - - std::lock_guard lock(embeddedFilesMapMutex); - embeddedFiles[std::string(m_fullFilepath)] = fileData; -#endif // B_PRODUCTION_MODE or B_OS_WEB - } - -} // namespace b -]==]) -file(WRITE ${EMBED_BINARY_DIR}/embed_impl.cpp "${EMBED_MASTER_SOURCE_FILE}") - -# The common battery::embed header and source files -set(EMBED_HEADER_FILE [=[ -// File generated by battery::embed -// DO NOT EDIT THIS FILE!!! -#ifndef BATTERY_EMBED_HPP -#define BATTERY_EMBED_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef __cpp_constexpr_dynamic_alloc -# error "battery::embed requires C++20 (Your compiler does not provide __cpp_constexpr_dynamic_alloc, which is needed for battery::embed)" -#endif - -namespace b { - - struct EmbedInternal { - - class EmbeddedFile { - public: - constexpr EmbeddedFile() = default; - - constexpr EmbeddedFile( - const std::string_view& data, - const std::string_view& filename -#ifndef B_PRODUCTION_MODE - , const std::string_view& fullFilepath -#endif // !B_PRODUCTION_MODE - ) - : m_data(data) - , m_filename(filename) -#ifndef B_PRODUCTION_MODE - , m_fullFilepath(fullFilepath) -#endif // !B_PRODUCTION_MODE - {} - - [[nodiscard]] std::string str() const { - return m_data.data(); - } - - [[nodiscard]] const char* data() const { - return m_data.data(); - } - - [[nodiscard]] std::vector vec() const { - return { m_data.begin(), m_data.end() }; - } - - [[nodiscard]] size_t length() const { - return m_data.size(); - } - - [[nodiscard]] size_t size() const { - return m_data.size(); - } - - operator std::string() const { - return str(); - } - - operator std::vector() const { - return vec(); - } - - void get(const std::function& callback); - - private: - std::string_view m_data; - std::string_view m_filename; -#ifndef B_PRODUCTION_MODE - std::string_view m_fullFilepath; -#endif - }; // class EmbeddedFile - - ${EMBEDDED_FILES_DECLARATIONS} - }; // struct embedded_files - - template - struct embed_string_literal { - constexpr embed_string_literal(const char (&str)[N]) { - std::copy_n(str, N, value); - } - constexpr bool operator!=(const embed_string_literal& other) const { - return std::equal(value, value + N, other.value); - } - [[nodiscard]] std::string str() const { - return std::string(value, N); - } - [[nodiscard]] constexpr bool _false() const { - return false; - } - char value[N]; - }; - - template - constexpr bool operator==(const embed_string_literal& left, const char (&right)[M]) { - return std::equal(left.value, left.value + N, right); - } - - template - constexpr EmbedInternal::EmbeddedFile embed() { - ${EMBEDDED_FILES_RETURNS}{ - static_assert(identifier._false(), "[b::embed<>] No such file or directory"); - } - } - -} // namespace b - -inline std::ostream& operator<<(std::ostream& stream, const b::EmbedInternal::EmbeddedFile& file) { - stream << file.str(); - return stream; -} - -#endif // BATTERY_EMBED_HPP -]=]) -file(WRITE ${EMBED_BINARY_DIR}/embed_header_file_template.hpp "${EMBED_HEADER_FILE}") - -# The cmake script to embed the files. This is called later on-demand, in a separate process -set(EMBED_GENERATE_SCRIPT [=[ -file(READ "${FULL_PATH}" GENERATED_BYTE_ARRAY HEX) -string(LENGTH "${GENERATED_BYTE_ARRAY}" FILESIZE) -math(EXPR FILESIZE "${FILESIZE} / 2") - -string(REPEAT "[0-9a-f]" 32 PATTERN) -set(GENERATED_BYTE_ARRAY "\"${GENERATED_BYTE_ARRAY}") -string(REGEX REPLACE "${PATTERN}" "\\0\"\n \"" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY}) -string(REGEX REPLACE "([0-9a-f][0-9a-f])" "\\\\x\\1" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY}) -set(GENERATED_BYTE_ARRAY "${GENERATED_BYTE_ARRAY}\"") -configure_file(${INFILE} ${OUTFILE}) -]=]) -file(WRITE ${EMBED_BINARY_DIR}/generate.cmake ${EMBED_GENERATE_SCRIPT}) +set(EMBED_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "source directory of the battery::embed library" FORCE) set(EMBED_IDENTIFIERS "" CACHE INTERNAL "list of all identifiers used by battery::embed") set(EMBED_FILENAMES "" CACHE INTERNAL "list of all filenames used by battery::embed") @@ -353,10 +48,10 @@ function(_embed_generate_hpp TARGET) set(EMBEDDED_FILES_RETURNS "${EMBEDDED_FILES_RETURNS}if constexpr (identifier == \"${FILENAME}\") { return EmbedInternal::${IDENTIFIER}; }\n else ") endif() endforeach() - file(READ ${EMBED_BINARY_DIR}/embed_header_file_template.hpp EMBED_HEADER_FILE) - string(CONFIGURE "${EMBED_HEADER_FILE}" EMBED_HEADER_FILE_GENERATED) set(EMBED_HPP "${EMBED_BINARY_DIR}/autogen/${TARGET}/include/battery/embed.hpp") - file(WRITE "${EMBED_HPP}" "${EMBED_HEADER_FILE_GENERATED}") + configure_file( + "${EMBED_SOURCE_DIR}/script/embed_header_file_template.hpp" + "${EMBED_HPP}") endfunction(_embed_generate_hpp) # Internal function for validating an identifier @@ -395,7 +90,7 @@ function(b_embed TARGET FILENAME) # This action generates both files and is called on-demand whenever the resource file changes set(EMBED_HPP "${EMBED_BINARY_DIR}/autogen/${TARGET_ID}/include/battery/embed.hpp") - set(EMBED_CPP_TEMPLATE "${EMBED_BINARY_DIR}/embed_source_file_template.cpp") + set(EMBED_CPP_TEMPLATE "${EMBED_SOURCE_DIR}/script/embed_source_file_template.cpp") add_custom_command( COMMAND ${CMAKE_COMMAND} -DINFILE=${EMBED_CPP_TEMPLATE} @@ -403,8 +98,8 @@ function(b_embed TARGET FILENAME) -DFULL_PATH=${FULL_PATH} -DIDENTIFIER=${IDENTIFIER} -DFILENAME=${FILENAME} - -P "${EMBED_BINARY_DIR}/generate.cmake" - DEPENDS "${FULL_PATH}" "${EMBED_HPP}" "${EMBED_BINARY_DIR}/generate.cmake" "${EMBED_BINARY_DIR}/embed_source_file_template.cpp" + -P "${EMBED_SOURCE_DIR}/script/generate.cmake" + DEPENDS "${FULL_PATH}" "${EMBED_HPP}" "${EMBED_SOURCE_DIR}/script/generate.cmake" "${EMBED_SOURCE_DIR}/script/embed_source_file_template.cpp" OUTPUT ${CPP_FILE} VERBATIM ) @@ -414,7 +109,7 @@ function(b_embed TARGET FILENAME) $ $ ) - target_sources(${TARGET} PRIVATE ${CPP_FILE} ${EMBED_BINARY_DIR}/embed_impl.cpp ${EMBED_HPP}) + target_sources(${TARGET} PRIVATE ${CPP_FILE} ${EMBED_SOURCE_DIR}/script/embed_impl.cpp ${EMBED_HPP}) target_compile_definitions(${TARGET} PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) if (NOT B_PRODUCTION_MODE) @@ -427,7 +122,7 @@ function(b_embed TARGET FILENAME) if (MSVC) target_sources(${TARGET} PRIVATE ${FULL_PATH}) source_group(TREE ${EMBED_BINARY_DIR}/autogen/${TARGET_ID}/src PREFIX "embed/autogen" FILES ${CPP_FILE}) - source_group(TREE ${EMBED_BINARY_DIR} PREFIX "embed/autogen" FILES ${EMBED_BINARY_DIR}/embed_impl.cpp) + source_group(TREE ${EMBED_SOURCE_DIR} PREFIX "embed/autogen" FILES ${EMBED_SOURCE_DIR}/script/embed_impl.cpp) source_group(TREE ${EMBED_BINARY_DIR}/autogen/${TARGET_ID}/include/battery PREFIX "embed/autogen" FILES ${EMBED_HPP}) source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "/embed" FILES ${FULL_PATH}) endif() diff --git a/script/embed_header_file_template.hpp b/script/embed_header_file_template.hpp new file mode 100644 index 0000000..7320f93 --- /dev/null +++ b/script/embed_header_file_template.hpp @@ -0,0 +1,118 @@ +// File generated by battery::embed +// DO NOT EDIT THIS FILE!!! +#ifndef BATTERY_EMBED_HPP +#define BATTERY_EMBED_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef __cpp_constexpr_dynamic_alloc +# error "battery::embed requires C++20 (Your compiler does not provide __cpp_constexpr_dynamic_alloc, which is needed for battery::embed)" +#endif + +namespace b { + + struct EmbedInternal { + + class EmbeddedFile { + public: + constexpr EmbeddedFile() = default; + + constexpr EmbeddedFile( + const std::string_view& data, + const std::string_view& filename +#ifndef B_PRODUCTION_MODE + , const std::string_view& fullFilepath +#endif // !B_PRODUCTION_MODE + ) + : m_data(data) + , m_filename(filename) +#ifndef B_PRODUCTION_MODE + , m_fullFilepath(fullFilepath) +#endif // !B_PRODUCTION_MODE + {} + + [[nodiscard]] std::string str() const { + return m_data.data(); + } + + [[nodiscard]] const char* data() const { + return m_data.data(); + } + + [[nodiscard]] std::vector vec() const { + return { m_data.begin(), m_data.end() }; + } + + [[nodiscard]] size_t length() const { + return m_data.size(); + } + + [[nodiscard]] size_t size() const { + return m_data.size(); + } + + operator std::string() const { + return str(); + } + + operator std::vector() const { + return vec(); + } + + void get(const std::function& callback); + + private: + std::string_view m_data; + std::string_view m_filename; +#ifndef B_PRODUCTION_MODE + std::string_view m_fullFilepath; +#endif + }; // class EmbeddedFile + + ${EMBEDDED_FILES_DECLARATIONS} + }; // struct embedded_files + + template + struct embed_string_literal { + constexpr embed_string_literal(const char (&str)[N]) { + std::copy_n(str, N, value); + } + constexpr bool operator!=(const embed_string_literal& other) const { + return std::equal(value, value + N, other.value); + } + [[nodiscard]] std::string str() const { + return std::string(value, N); + } + [[nodiscard]] constexpr bool _false() const { + return false; + } + char value[N]; + }; + + template + constexpr bool operator==(const embed_string_literal& left, const char (&right)[M]) { + return std::equal(left.value, left.value + N, right); + } + + template + constexpr EmbedInternal::EmbeddedFile embed() { + ${EMBEDDED_FILES_RETURNS}{ + static_assert(identifier._false(), "[b::embed<>] No such file or directory"); + } + } + +} // namespace b + +inline std::ostream& operator<<(std::ostream& stream, const b::EmbedInternal::EmbeddedFile& file) { + stream << file.str(); + return stream; +} + +#endif // BATTERY_EMBED_HPP diff --git a/script/embed_impl.cpp b/script/embed_impl.cpp new file mode 100644 index 0000000..2aa3cd7 --- /dev/null +++ b/script/embed_impl.cpp @@ -0,0 +1,137 @@ +// File generated using battery::embed (https://github.com/batterycenter/embed) +// DO NOT EDIT THIS FILE!!! + +#include "battery/embed.hpp" + +#if !defined(B_PRODUCTION_MODE) && !defined(B_OS_WEB) +#include +#include +#include +#include +#include +#include +#include +#endif // !B_PRODUCTION_MODE and !B_OS_WEB + +namespace b { + +#if !defined(B_PRODUCTION_MODE) && !defined(B_OS_WEB) + + // All this crazyness is necessary because we need to use std::string_view because of constexpr, + // and the string_view only points to a char array, so we need a string that outlives + // the string_view it is assigned to. + + struct FileData { + std::string filename; + std::string newContent; + std::string filepath; + std::filesystem::file_time_type lastWriteTime; + std::function callback; + }; + + static std::mutex embeddedFilesMapMutex; + static std::unordered_map embeddedFiles; + +#ifdef _WIN32 +#ifdef __clang__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + + static std::wstring embed_widen(const std::string& str) { + std::wstring_convert> converter; + return converter.from_bytes(str); + } + +#ifdef __clang__ +#pragma GCC diagnostic pop +#endif + +#endif // _WIN32 + + static std::optional embed_read_file(const std::string_view& filename) { +#ifdef _WIN32 + std::ifstream file(embed_widen(std::string(filename)).c_str(), std::ios::binary); +#else // _WIN32 + std::ifstream file(std::string(filename), std::ios::binary); +#endif // _WIN32 + if (file.fail()) { + return {}; + } + return std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + } + + // Apple-Clang does not support std::jthread yet, hence we make our own version of jthread here: + class AutoThread { + public: + template + AutoThread(Tfunc&& function) + : m_thread([&]() { function(m_shouldStop); }) + {} + + ~AutoThread() { + m_shouldStop = true; + m_thread.join(); + } + + AutoThread(const AutoThread& other) = delete; + AutoThread(AutoThread&& other) = delete; + AutoThread& operator=(const AutoThread& other) = delete; + AutoThread& operator=(AutoThread&& other) = delete; + + private: + std::thread m_thread; + std::atomic m_shouldStop = false; + }; + + // This is a global file watcher thread that is created at program start and automatically destroyed at program end + // All embedded files are hot-reloaded by this thread. + AutoThread embeddedFileWatcherThread = AutoThread([](std::atomic& shouldStop) { + while (!shouldStop) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::lock_guard lock(embeddedFilesMapMutex); + for (auto& [key, filedata] : embeddedFiles) { + std::filesystem::path filepath(key); + std::error_code ec; + auto newWriteTime = std::filesystem::last_write_time(filepath, ec); + if (!ec) { + if (newWriteTime != filedata.lastWriteTime) { + filedata.lastWriteTime = newWriteTime; + auto fileresult = embed_read_file(key); + if (!fileresult) { + continue; + } + filedata.newContent = *fileresult; + EmbedInternal::EmbeddedFile newFile(filedata.newContent, filedata.filename, filedata.filepath); + filedata.callback(newFile); + } + } + } + } + }); + +#endif // !B_PRODUCTION_MODE and !B_OS_WEB + + void EmbedInternal::EmbeddedFile::get(const std::function& callback) { +#if defined(B_PRODUCTION_MODE) || defined(B_OS_WEB) + callback(*this); +#else // B_PRODUCTION_MODE or B_OS_WEB + auto fileresult = embed_read_file(m_fullFilepath); + if (fileresult) { + m_data = *fileresult; + } + callback(*this); + + FileData fileData; + fileData.filename = m_filename; + fileData.newContent = m_data; + fileData.filepath = m_fullFilepath; + fileData.callback = callback; + fileData.lastWriteTime = std::filesystem::last_write_time(m_fullFilepath); + + std::lock_guard lock(embeddedFilesMapMutex); + embeddedFiles[std::string(m_fullFilepath)] = fileData; +#endif // B_PRODUCTION_MODE or B_OS_WEB + } + +} // namespace b diff --git a/script/embed_source_file_template.cpp b/script/embed_source_file_template.cpp new file mode 100644 index 0000000..c2cb848 --- /dev/null +++ b/script/embed_source_file_template.cpp @@ -0,0 +1,21 @@ +// File generated using battery::embed (https://github.com/batterycenter/embed) +// Embedded file: '${FILENAME}' as '${IDENTIFIER}' +// Filesize: ${FILESIZE} bytes +// DO NOT EDIT THIS FILE!!! + +#include "battery/embed.hpp" + +namespace b { + +EmbedInternal::EmbeddedFile EmbedInternal::${IDENTIFIER} = { + std::string_view( + ${GENERATED_BYTE_ARRAY}, + ${FILESIZE} + ) + , "${FILENAME}" +#ifndef B_PRODUCTION_MODE + , "${FULL_PATH}" +#endif // !B_PRODUCTION_MODE +}; + +} // namespace b diff --git a/script/generate.cmake b/script/generate.cmake new file mode 100644 index 0000000..c9c4628 --- /dev/null +++ b/script/generate.cmake @@ -0,0 +1,10 @@ +file(READ "${FULL_PATH}" GENERATED_BYTE_ARRAY HEX) +string(LENGTH "${GENERATED_BYTE_ARRAY}" FILESIZE) +math(EXPR FILESIZE "${FILESIZE} / 2") + +string(REPEAT "[0-9a-f]" 32 PATTERN) +set(GENERATED_BYTE_ARRAY "\"${GENERATED_BYTE_ARRAY}") +string(REGEX REPLACE "${PATTERN}" "\\0\"\n \"" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY}) +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "\\\\x\\1" GENERATED_BYTE_ARRAY ${GENERATED_BYTE_ARRAY}) +set(GENERATED_BYTE_ARRAY "${GENERATED_BYTE_ARRAY}\"") +configure_file(${INFILE} ${OUTFILE})