diff --git a/src/Linking/LinkerArgs.cpp b/src/Linking/LinkerArgs.cpp index 654bda1f..a9222937 100644 --- a/src/Linking/LinkerArgs.cpp +++ b/src/Linking/LinkerArgs.cpp @@ -225,50 +225,50 @@ bool LinkerArgs::ParseArgs(const int argc, const char** argv, bool& shouldContin // --asset-search-path if (m_argument_parser.IsOptionSpecified(OPTION_ASSET_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_ASSET_SEARCH_PATH), m_asset_search_paths)) + if (!utils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_ASSET_SEARCH_PATH), m_asset_search_paths)) return false; } else { - if (!FileUtils::ParsePathsString(DEFAULT_ASSET_SEARCH_PATH, m_asset_search_paths)) + if (!utils::ParsePathsString(DEFAULT_ASSET_SEARCH_PATH, m_asset_search_paths)) return false; } // --add-assets-search-path for (const auto& specifiedValue : m_argument_parser.GetParametersForOption(OPTION_ADD_ASSET_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(specifiedValue, m_asset_search_paths)) + if (!utils::ParsePathsString(specifiedValue, m_asset_search_paths)) return false; } // --gdt-search-path if (m_argument_parser.IsOptionSpecified(OPTION_GDT_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_GDT_SEARCH_PATH), m_gdt_search_paths)) + if (!utils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_GDT_SEARCH_PATH), m_gdt_search_paths)) return false; } else { - if (!FileUtils::ParsePathsString(DEFAULT_GDT_SEARCH_PATH, m_gdt_search_paths)) + if (!utils::ParsePathsString(DEFAULT_GDT_SEARCH_PATH, m_gdt_search_paths)) return false; } // --source-search-path if (m_argument_parser.IsOptionSpecified(OPTION_SOURCE_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_SOURCE_SEARCH_PATH), m_source_search_paths)) + if (!utils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_SOURCE_SEARCH_PATH), m_source_search_paths)) return false; } else { - if (!FileUtils::ParsePathsString(DEFAULT_SOURCE_SEARCH_PATH, m_source_search_paths)) + if (!utils::ParsePathsString(DEFAULT_SOURCE_SEARCH_PATH, m_source_search_paths)) return false; } // --add-source-search-path for (const auto& specifiedValue : m_argument_parser.GetParametersForOption(OPTION_ADD_SOURCE_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(specifiedValue, m_source_search_paths)) + if (!utils::ParsePathsString(specifiedValue, m_source_search_paths)) return false; } diff --git a/src/ObjCommon/ObjContainer/IPak/IPakTypes.h b/src/ObjCommon/ObjContainer/IPak/IPakTypes.h index 93f489a1..441e438c 100644 --- a/src/ObjCommon/ObjContainer/IPak/IPakTypes.h +++ b/src/ObjCommon/ObjContainer/IPak/IPakTypes.h @@ -6,12 +6,12 @@ namespace ipak_consts { - static constexpr uint32_t IPAK_MAGIC = FileUtils::MakeMagic32('K', 'A', 'P', 'I'); + static constexpr uint32_t IPAK_MAGIC = utils::MakeMagic32('K', 'A', 'P', 'I'); static constexpr uint32_t IPAK_VERSION = 0x50000; static constexpr uint32_t IPAK_INDEX_SECTION = 1; static constexpr uint32_t IPAK_DATA_SECTION = 2; - static constexpr uint32_t IPAK_BRANDING_SECTION = FileUtils::MakeMagic32('M', 'E', 'T', 'A'); + static constexpr uint32_t IPAK_BRANDING_SECTION = utils::MakeMagic32('M', 'E', 'T', 'A'); static constexpr size_t IPAK_CHUNK_SIZE = 0x8000; static constexpr size_t IPAK_CHUNK_COUNT_PER_READ = 0x8; diff --git a/src/ObjCommon/Shader/D3D11ShaderAnalyser.cpp b/src/ObjCommon/Shader/D3D11ShaderAnalyser.cpp index 10a5f353..c7053491 100644 --- a/src/ObjCommon/Shader/D3D11ShaderAnalyser.cpp +++ b/src/ObjCommon/Shader/D3D11ShaderAnalyser.cpp @@ -41,8 +41,8 @@ namespace d3d11 namespace { - constexpr auto TAG_RDEF = FileUtils::MakeMagic32('R', 'D', 'E', 'F'); - constexpr auto TAG_SHDR = FileUtils::MakeMagic32('S', 'H', 'D', 'R'); + constexpr auto TAG_RDEF = utils::MakeMagic32('R', 'D', 'E', 'F'); + constexpr auto TAG_SHDR = utils::MakeMagic32('S', 'H', 'D', 'R'); constexpr auto CHUNK_TABLE_OFFSET = 28u; diff --git a/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp b/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp index 4872c82b..2755b788 100644 --- a/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp +++ b/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp @@ -222,7 +222,7 @@ namespace d3d9 const char* constantTableComment; size_t constantTableCommentSize; - if (!FindComment(shaderByteCode, shaderByteCodeSize, FileUtils::MakeMagic32('C', 'T', 'A', 'B'), constantTableComment, constantTableCommentSize)) + if (!FindComment(shaderByteCode, shaderByteCodeSize, utils::MakeMagic32('C', 'T', 'A', 'B'), constantTableComment, constantTableCommentSize)) return false; if (constantTableCommentSize < sizeof(ConstantTable)) diff --git a/src/ObjCommon/Sound/FlacDecoder.cpp b/src/ObjCommon/Sound/FlacDecoder.cpp index a7c4fc0e..696ccd37 100644 --- a/src/ObjCommon/Sound/FlacDecoder.cpp +++ b/src/ObjCommon/Sound/FlacDecoder.cpp @@ -12,7 +12,7 @@ namespace { - constexpr auto FLAC_MAGIC = FileUtils::MakeMagic32('f', 'L', 'a', 'C'); + constexpr auto FLAC_MAGIC = utils::MakeMagic32('f', 'L', 'a', 'C'); enum class MetaDataBlockType : unsigned { diff --git a/src/ObjCommon/Sound/WavTypes.h b/src/ObjCommon/Sound/WavTypes.h index 1a997816..1e150410 100644 --- a/src/ObjCommon/Sound/WavTypes.h +++ b/src/ObjCommon/Sound/WavTypes.h @@ -1,12 +1,13 @@ #pragma once + #include "Utils/FileUtils.h" #include -constexpr uint32_t WAV_WAVE_ID = FileUtils::MakeMagic32('W', 'A', 'V', 'E'); -constexpr uint32_t WAV_CHUNK_ID_RIFF = FileUtils::MakeMagic32('R', 'I', 'F', 'F'); -constexpr uint32_t WAV_CHUNK_ID_FMT = FileUtils::MakeMagic32('f', 'm', 't', ' '); -constexpr uint32_t WAV_CHUNK_ID_DATA = FileUtils::MakeMagic32('d', 'a', 't', 'a'); +constexpr uint32_t WAV_WAVE_ID = utils::MakeMagic32('W', 'A', 'V', 'E'); +constexpr uint32_t WAV_CHUNK_ID_RIFF = utils::MakeMagic32('R', 'I', 'F', 'F'); +constexpr uint32_t WAV_CHUNK_ID_FMT = utils::MakeMagic32('f', 'm', 't', ' '); +constexpr uint32_t WAV_CHUNK_ID_DATA = utils::MakeMagic32('d', 'a', 't', 'a'); struct WavChunkHeader { diff --git a/src/ObjCommon/XModel/Gltf/GltfConstants.h b/src/ObjCommon/XModel/Gltf/GltfConstants.h index d4338588..413c5144 100644 --- a/src/ObjCommon/XModel/Gltf/GltfConstants.h +++ b/src/ObjCommon/XModel/Gltf/GltfConstants.h @@ -7,12 +7,12 @@ namespace gltf { - constexpr uint32_t GLTF_MAGIC = FileUtils::MakeMagic32('g', 'l', 'T', 'F'); + constexpr uint32_t GLTF_MAGIC = utils::MakeMagic32('g', 'l', 'T', 'F'); constexpr uint32_t GLTF_VERSION = 2u; constexpr auto GLTF_VERSION_STRING = "2.0"; - constexpr uint32_t CHUNK_MAGIC_JSON = FileUtils::MakeMagic32('J', 'S', 'O', 'N'); - constexpr uint32_t CHUNK_MAGIC_BIN = FileUtils::MakeMagic32('B', 'I', 'N', '\x00'); + constexpr uint32_t CHUNK_MAGIC_JSON = utils::MakeMagic32('J', 'S', 'O', 'N'); + constexpr uint32_t CHUNK_MAGIC_BIN = utils::MakeMagic32('B', 'I', 'N', '\x00'); constexpr auto GLTF_LENGTH_OFFSET = 8u; constexpr auto GLTF_JSON_CHUNK_LENGTH_OFFSET = 12u; diff --git a/src/ObjImage/Image/D3DFormat.h b/src/ObjImage/Image/D3DFormat.h index 90f671cf..27647442 100644 --- a/src/ObjImage/Image/D3DFormat.h +++ b/src/ObjImage/Image/D3DFormat.h @@ -41,15 +41,15 @@ namespace oat D3DFMT_V16U16 = 64, D3DFMT_A2W10V10U10 = 67, - D3DFMT_UYVY = FileUtils::MakeMagic32('U', 'Y', 'V', 'Y'), - D3DFMT_R8G8_B8G8 = FileUtils::MakeMagic32('R', 'G', 'B', 'G'), - D3DFMT_YUY2 = FileUtils::MakeMagic32('Y', 'U', 'Y', '2'), - D3DFMT_G8R8_G8B8 = FileUtils::MakeMagic32('G', 'R', 'G', 'B'), - D3DFMT_DXT1 = FileUtils::MakeMagic32('D', 'X', 'T', '1'), - D3DFMT_DXT2 = FileUtils::MakeMagic32('D', 'X', 'T', '2'), - D3DFMT_DXT3 = FileUtils::MakeMagic32('D', 'X', 'T', '3'), - D3DFMT_DXT4 = FileUtils::MakeMagic32('D', 'X', 'T', '4'), - D3DFMT_DXT5 = FileUtils::MakeMagic32('D', 'X', 'T', '5'), + D3DFMT_UYVY = utils::MakeMagic32('U', 'Y', 'V', 'Y'), + D3DFMT_R8G8_B8G8 = utils::MakeMagic32('R', 'G', 'B', 'G'), + D3DFMT_YUY2 = utils::MakeMagic32('Y', 'U', 'Y', '2'), + D3DFMT_G8R8_G8B8 = utils::MakeMagic32('G', 'R', 'G', 'B'), + D3DFMT_DXT1 = utils::MakeMagic32('D', 'X', 'T', '1'), + D3DFMT_DXT2 = utils::MakeMagic32('D', 'X', 'T', '2'), + D3DFMT_DXT3 = utils::MakeMagic32('D', 'X', 'T', '3'), + D3DFMT_DXT4 = utils::MakeMagic32('D', 'X', 'T', '4'), + D3DFMT_DXT5 = utils::MakeMagic32('D', 'X', 'T', '5'), D3DFMT_D16_LOCKABLE = 70, D3DFMT_D32 = 71, @@ -78,7 +78,7 @@ namespace oat D3DFMT_Q16W16V16U16 = 110, - D3DFMT_MULTI2_ARGB8 = FileUtils::MakeMagic32('M', 'E', 'T', '1'), + D3DFMT_MULTI2_ARGB8 = utils::MakeMagic32('M', 'E', 'T', '1'), // Floating point surface formats diff --git a/src/ObjImage/Image/DdsLoader.cpp b/src/ObjImage/Image/DdsLoader.cpp index eb85379a..7a49cf2f 100644 --- a/src/ObjImage/Image/DdsLoader.cpp +++ b/src/ObjImage/Image/DdsLoader.cpp @@ -1,7 +1,6 @@ #include "DdsLoader.h" #include "Image/DdsTypes.h" -#include "Utils/ClassUtils.h" #include "Utils/FileUtils.h" #include "Utils/Logging/Log.h" @@ -15,7 +14,7 @@ namespace { class DdsLoaderInternal { - static constexpr auto DDS_MAGIC = FileUtils::MakeMagic32('D', 'D', 'S', ' '); + static constexpr auto DDS_MAGIC = utils::MakeMagic32('D', 'D', 'S', ' '); public: explicit DdsLoaderInternal(std::istream& stream) @@ -105,29 +104,29 @@ namespace { switch (pf.dwFourCC) { - case FileUtils::MakeMagic32('D', 'X', 'T', '1'): + case utils::MakeMagic32('D', 'X', 'T', '1'): m_format = &ImageFormat::FORMAT_BC1; return true; - case FileUtils::MakeMagic32('D', 'X', 'T', '3'): + case utils::MakeMagic32('D', 'X', 'T', '3'): m_format = &ImageFormat::FORMAT_BC2; return true; - case FileUtils::MakeMagic32('D', 'X', 'T', '5'): + case utils::MakeMagic32('D', 'X', 'T', '5'): m_format = &ImageFormat::FORMAT_BC3; return true; - case FileUtils::MakeMagic32('A', 'T', 'I', '1'): - case FileUtils::MakeMagic32('B', 'C', '4', 'U'): + case utils::MakeMagic32('A', 'T', 'I', '1'): + case utils::MakeMagic32('B', 'C', '4', 'U'): m_format = &ImageFormat::FORMAT_BC4; return true; - case FileUtils::MakeMagic32('A', 'T', 'I', '2'): - case FileUtils::MakeMagic32('B', 'C', '5', 'U'): + case utils::MakeMagic32('A', 'T', 'I', '2'): + case utils::MakeMagic32('B', 'C', '5', 'U'): m_format = &ImageFormat::FORMAT_BC5; return true; - case FileUtils::MakeMagic32('D', 'X', '1', '0'): + case utils::MakeMagic32('D', 'X', '1', '0'): return ReadDxt10Header(); default: diff --git a/src/ObjLoading/ObjContainer/SoundBank/SoundBank.h b/src/ObjLoading/ObjContainer/SoundBank/SoundBank.h index c95586f9..acf48020 100644 --- a/src/ObjLoading/ObjContainer/SoundBank/SoundBank.h +++ b/src/ObjLoading/ObjContainer/SoundBank/SoundBank.h @@ -25,7 +25,7 @@ public: class SoundBank final : public ObjContainerReferenceable { - static constexpr uint32_t MAGIC = FileUtils::MakeMagic32('2', 'U', 'X', '#'); + static constexpr uint32_t MAGIC = utils::MakeMagic32('2', 'U', 'X', '#'); static constexpr uint32_t VERSION = 14u; std::string m_file_name; diff --git a/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp index 67f8547e..992ec66d 100644 --- a/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp +++ b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp @@ -32,7 +32,7 @@ class SoundBankWriterImpl : public SoundBankWriter { static constexpr char BRANDING[] = "Created with OAT - OpenAssetTools"; static constexpr int64_t DATA_OFFSET = 0x800; - static constexpr uint32_t MAGIC = FileUtils::MakeMagic32('2', 'U', 'X', '#'); + static constexpr uint32_t MAGIC = utils::MakeMagic32('2', 'U', 'X', '#'); static constexpr uint32_t VERSION = 14u; inline static const std::string PAD_DATA = std::string(16, '\x00'); diff --git a/src/ObjWriting/XModel/Gltf/GltfBinOutput.cpp b/src/ObjWriting/XModel/Gltf/GltfBinOutput.cpp index f57356c5..1f01b24e 100644 --- a/src/ObjWriting/XModel/Gltf/GltfBinOutput.cpp +++ b/src/ObjWriting/XModel/Gltf/GltfBinOutput.cpp @@ -23,7 +23,7 @@ void BinOutput::AlignToFour(const char value) const const auto offset = m_stream.tellp(); if (offset % 4 > 0) { - const uint32_t alignmentValue = FileUtils::MakeMagic32(value, value, value, value); + const uint32_t alignmentValue = utils::MakeMagic32(value, value, value, value); Write(&alignmentValue, 4u - (offset % 4u)); } } diff --git a/src/RawTemplater/RawTemplaterArguments.h b/src/RawTemplater/RawTemplaterArguments.h index a0b2696d..7ff67338 100644 --- a/src/RawTemplater/RawTemplaterArguments.h +++ b/src/RawTemplater/RawTemplaterArguments.h @@ -20,6 +20,8 @@ public: std::vector m_input_files; std::string m_output_directory; + // Generate a build log that is always written for the compiler to determine + // the last output time. std::string m_build_log_file; std::vector> m_defines; diff --git a/src/RawTemplater/Templating/Templater.cpp b/src/RawTemplater/Templating/Templater.cpp index 40c2203f..9e4eda61 100644 --- a/src/RawTemplater/Templating/Templater.cpp +++ b/src/RawTemplater/Templating/Templater.cpp @@ -8,6 +8,7 @@ #include "SetDefineStreamProxy.h" #include "TemplatingStreamProxy.h" #include "Utils/ClassUtils.h" +#include "Utils/FileUtils.h" #include "Utils/Logging/Log.h" #include @@ -198,9 +199,9 @@ namespace templating if (m_first_line) m_first_line = false; else - m_output_stream << '\n'; + m_output.Stream() << '\n'; - m_output_stream << nextLine.m_line; + m_output.Stream() << nextLine.m_line; } else { @@ -229,19 +230,35 @@ namespace templating const auto cachedData = m_output_cache.str(); if (!cachedData.empty()) - m_output_stream << cachedData; + m_output.Stream() << cachedData; } - con::info("Templated file \"{}\"", m_output_file); + const auto outputResult = m_output.Close(); + switch (outputResult) + { + case utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN: + con::info("Templated file: \"{}\"", m_output_file); + if (buildLogFile) + *buildLogFile << "Templated file: \"" << m_output_file << "\"\n"; + break; - if (buildLogFile) - *buildLogFile << "Templated file \"" << m_output_file << "\"\n"; + case utils::TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE: + con::info("File was up to date: \"{}\"", m_output_file); + if (buildLogFile) + *buildLogFile << "File was up to date: \"" << m_output_file << "\"\n"; + break; + + case utils::TextFileCheckDirtyResult::FAILURE: + con::error("Failed to write file: \"{}\"", m_output_file); + if (buildLogFile) + *buildLogFile << "Failed to write file: \"" << m_output_file << "\"\n"; + break; + } m_first_line = true; m_write_output_to_file = false; m_output_cache.clear(); m_output_cache.str(std::string()); - m_output_stream.close(); return true; } @@ -327,7 +344,7 @@ namespace templating m_write_output_to_file = true; const auto cachedData = m_output_cache.str(); if (!cachedData.empty()) - m_output_stream << cachedData; + m_output.Stream() << cachedData; m_output_cache.clear(); m_output_cache.str(std::string()); @@ -353,8 +370,8 @@ namespace templating if (!parentDir.empty()) create_directories(parentDir); - m_output_stream = std::ofstream(m_output_file, std::ios::out | std::ios::binary); - if (!m_output_stream.is_open()) + m_output = utils::TextFileCheckDirtyOutput(m_output_file); + if (!m_output.Open()) { con::error("Failed to open output file \"{}\"", m_output_file); return false; @@ -371,12 +388,12 @@ namespace templating std::string m_filename; std::string m_output_file; std::string m_default_output_file; - const fs::path m_output_directory; + fs::path m_output_directory; bool m_first_line; bool m_skip_pass; bool m_write_output_to_file; - std::ofstream m_output_stream; + utils::TextFileCheckDirtyOutput m_output; std::ostringstream m_output_cache; }; } // namespace templating diff --git a/src/Unlinking/UnlinkerArgs.cpp b/src/Unlinking/UnlinkerArgs.cpp index 86fc3486..f857265e 100644 --- a/src/Unlinking/UnlinkerArgs.cpp +++ b/src/Unlinking/UnlinkerArgs.cpp @@ -330,7 +330,7 @@ bool UnlinkerArgs::ParseArgs(const int argc, const char** argv, bool& shouldCont // --search-path if (m_argument_parser.IsOptionSpecified(OPTION_SEARCH_PATH)) { - if (!FileUtils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_SEARCH_PATH), m_user_search_paths)) + if (!utils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_SEARCH_PATH), m_user_search_paths)) { return false; } diff --git a/src/Utils/Utils/FileUtils.cpp b/src/Utils/Utils/FileUtils.cpp index 24b8a612..f3703019 100644 --- a/src/Utils/Utils/FileUtils.cpp +++ b/src/Utils/Utils/FileUtils.cpp @@ -1,63 +1,179 @@ #include "FileUtils.h" +#include #include -bool FileUtils::ParsePathsString(const std::string& pathsString, std::set& output) +namespace fs = std::filesystem; + +namespace utils { - std::ostringstream currentPath; - auto pathStart = true; - auto quotationPos = 0; // 0 = before quotations, 1 = in quotations, 2 = after quotations - - for (auto character : pathsString) + bool ParsePathsString(const std::string& pathsString, std::set& output) { - switch (character) + std::ostringstream currentPath; + auto pathStart = true; + auto quotationPos = 0; // 0 = before quotations, 1 = in quotations, 2 = after quotations + + for (const auto character : pathsString) { - case '"': - if (quotationPos == 0 && pathStart) + switch (character) { - quotationPos = 1; - } - else if (quotationPos == 1) - { - quotationPos = 2; - pathStart = false; - } - else - { - return false; - } - break; - - case ';': - if (quotationPos != 1) - { - auto path = currentPath.str(); - if (!path.empty()) + case '"': + if (quotationPos == 0 && pathStart) { - output.insert(path); - currentPath = std::ostringstream(); + quotationPos = 1; } + else if (quotationPos == 1) + { + quotationPos = 2; + pathStart = false; + } + else + { + return false; + } + break; - pathStart = true; - quotationPos = 0; - } - else - { + case ';': + if (quotationPos != 1) + { + auto path = currentPath.str(); + if (!path.empty()) + { + output.insert(path); + currentPath = std::ostringstream(); + } + + pathStart = true; + quotationPos = 0; + } + else + { + currentPath << character; + } + break; + + default: currentPath << character; + pathStart = false; + break; } - break; - - default: - currentPath << character; - pathStart = false; - break; } + + if (currentPath.tellp() > 0) + { + output.insert(currentPath.str()); + } + + return true; } - if (currentPath.tellp() > 0) + TextFileCheckDirtyOutput::TextFileCheckDirtyOutput() + : m_open(false), + m_has_existing_file(false) { - output.insert(currentPath.str()); } - return true; -} + TextFileCheckDirtyOutput::TextFileCheckDirtyOutput(fs::path path) + : m_path(std::move(path)), + m_open(false), + m_has_existing_file(false) + { + } + + TextFileCheckDirtyOutput::~TextFileCheckDirtyOutput() + { + Close(); + } + + bool TextFileCheckDirtyOutput::Open() + { + if (m_open) + return false; + + auto parentFolder(m_path); + parentFolder.remove_filename(); + create_directories(parentFolder); + + m_has_existing_file = fs::is_regular_file(m_path); + + if (!m_has_existing_file) + { + m_file_stream = std::ofstream(m_path, std::fstream::out | std::fstream::binary); + if (!m_file_stream.is_open()) + return false; + } + + m_open = true; + return true; + } + + std::ostream& TextFileCheckDirtyOutput::Stream() + { + if (!m_has_existing_file) + return m_file_stream; + + return m_memory; + } + + TextFileCheckDirtyResult TextFileCheckDirtyOutput::Close() + { + if (!m_open) + return TextFileCheckDirtyResult::FAILURE; + + m_open = false; + + if (m_has_existing_file) + { + const auto renderedContent = std::move(m_memory).str(); + m_memory = std::ostringstream(); + + if (!FileIsDirty(renderedContent)) + return TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE; + + std::ofstream stream(m_path, std::fstream::out | std::fstream::binary); + if (!stream.is_open()) + return TextFileCheckDirtyResult::FAILURE; + + stream.write(renderedContent.data(), renderedContent.size()); + stream.close(); + } + + return TextFileCheckDirtyResult::OUTPUT_WRITTEN; + } + + [[nodiscard]] bool TextFileCheckDirtyOutput::FileIsDirty(const std::string& renderedContent) const + { + const auto fileSize = static_cast(fs::file_size(m_path)); + const size_t contentSize = renderedContent.size(); + if (fileSize != contentSize) + return true; + + std::ifstream oldFileStream(m_path, std::fstream::in | std::fstream::binary); + if (!oldFileStream.is_open()) + return true; + + char buffer[4096]; + size_t currentContentOffset = 0; + while (currentContentOffset < contentSize) + { + const auto expectedReadCount = std::min(sizeof(buffer), contentSize - currentContentOffset); + oldFileStream.read(buffer, expectedReadCount); + if (oldFileStream.gcount() != expectedReadCount) + { + oldFileStream.close(); + return true; + } + + if (memcmp(buffer, renderedContent.data() + currentContentOffset, expectedReadCount) != 0) + { + oldFileStream.close(); + return true; + } + + currentContentOffset += expectedReadCount; + } + + oldFileStream.close(); + return false; + } +} // namespace utils diff --git a/src/Utils/Utils/FileUtils.h b/src/Utils/Utils/FileUtils.h index 6a053fff..faa196fb 100644 --- a/src/Utils/Utils/FileUtils.h +++ b/src/Utils/Utils/FileUtils.h @@ -1,12 +1,14 @@ #pragma once #include +#include +#include #include +#include #include -class FileUtils +namespace utils { -public: static constexpr uint32_t MakeMagic32(const char ch0, const char ch1, const char ch2, const char ch3) { return static_cast(ch0) | static_cast(ch1) << 8 | static_cast(ch2) << 16 | static_cast(ch3) << 24; @@ -18,5 +20,38 @@ public: * \param output A set for strings to save the output to. * \return \c true if the user input was valid and could be processed successfully, otherwise \c false. */ - static bool ParsePathsString(const std::string& pathsString, std::set& output); -}; + bool ParsePathsString(const std::string& pathsString, std::set& output); + + enum class TextFileCheckDirtyResult : std::uint8_t + { + OUTPUT_WRITTEN, + OUTPUT_WAS_UP_TO_DATE, + FAILURE + }; + + class TextFileCheckDirtyOutput final + { + public: + TextFileCheckDirtyOutput(); + explicit TextFileCheckDirtyOutput(std::filesystem::path path); + ~TextFileCheckDirtyOutput(); + TextFileCheckDirtyOutput(const TextFileCheckDirtyOutput& other) = delete; + TextFileCheckDirtyOutput(TextFileCheckDirtyOutput&& other) noexcept = default; + TextFileCheckDirtyOutput& operator=(const TextFileCheckDirtyOutput& other) = delete; + TextFileCheckDirtyOutput& operator=(TextFileCheckDirtyOutput&& other) noexcept = default; + + bool Open(); + std::ostream& Stream(); + TextFileCheckDirtyResult Close(); + + private: + [[nodiscard]] bool FileIsDirty(const std::string& renderedContent) const; + + std::filesystem::path m_path; + + bool m_open; + bool m_has_existing_file; + std::ofstream m_file_stream; + std::ostringstream m_memory; + }; +} // namespace utils diff --git a/src/ZoneCode.lua b/src/ZoneCode.lua index 61f6694f..9d648279 100644 --- a/src/ZoneCode.lua +++ b/src/ZoneCode.lua @@ -347,6 +347,7 @@ function ZoneCode:project() .. ' -h "' .. path.join(path.getabsolute(ProjectFolder()), 'ZoneCode/Game/%{file.basename}/%{file.basename}_ZoneCode.h') .. '"' .. ' -c "' .. path.join(path.getabsolute(ProjectFolder()), 'ZoneCode/Game/%{file.basename}/%{file.basename}_Commands.txt') .. '"' .. ' -o "%{wks.location}/src/ZoneCode/Game/%{file.basename}"' + .. ' --build-log "%{wks.location}/src/ZoneCode/Game/%{file.basename}.log"' .. ' -g ZoneLoad' .. ' -g ZoneMark' .. ' -g ZoneWrite' @@ -358,6 +359,9 @@ function ZoneCode:project() path.join(ProjectFolder(), "Common/Game/%{file.basename}/%{file.basename}_Assets.h"), TargetDirectoryBuildTools .. "/" .. ExecutableByOs('ZoneCodeGenerator') } + buildoutputs { + "%{wks.location}/src/ZoneCode/Game/%{file.basename}.log" + } filter {} filter "files:**/IW3.gen" diff --git a/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.cpp b/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.cpp index 00439e85..71c403ea 100644 --- a/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.cpp +++ b/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.cpp @@ -5,6 +5,7 @@ #include "Templates/ZoneLoadTemplate.h" #include "Templates/ZoneMarkTemplate.h" #include "Templates/ZoneWriteTemplate.h" +#include "Utils/FileUtils.h" #include "Utils/Logging/Log.h" #include "Utils/StringUtils.h" @@ -28,58 +29,66 @@ void CodeGenerator::SetupTemplates() m_template_mapping["assetstructtests"] = std::make_unique(); } -bool CodeGenerator::GenerateCodeOncePerTemplate(const OncePerTemplateRenderingContext& context, ICodeTemplate* codeTemplate) const +utils::TextFileCheckDirtyResult CodeGenerator::GenerateCodeOncePerTemplate(const OncePerTemplateRenderingContext& context, ICodeTemplate* codeTemplate) const { + bool wroteAtLeastOneFile = false; for (const auto& codeFile : codeTemplate->GetFilesToRenderOncePerTemplate(context)) { - fs::path p(m_args->m_output_directory); - p.append(codeFile.m_file_name); + fs::path outputPath(m_args->m_output_directory); + outputPath.append(codeFile.m_file_name); - auto parentFolder(p); - parentFolder.remove_filename(); - create_directories(parentFolder); - - std::ofstream stream(p, std::fstream::out | std::fstream::binary); - - if (!stream.is_open()) + utils::TextFileCheckDirtyOutput out(outputPath); + if (!out.Open()) { - con::error("Failed to open file '{}'", p.string()); - return false; + con::error("Failed to open file '{}'", outputPath.string()); + return utils::TextFileCheckDirtyResult::FAILURE; } - codeTemplate->RenderOncePerTemplateFile(stream, codeFile.m_tag, context); + codeTemplate->RenderOncePerTemplateFile(out.Stream(), codeFile.m_tag, context); - stream.close(); + const auto fileResult = out.Close(); + if (fileResult == utils::TextFileCheckDirtyResult::FAILURE) + { + con::error("Failed to write file '{}'", outputPath.string()); + return utils::TextFileCheckDirtyResult::FAILURE; + } + + if (fileResult == utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN) + wroteAtLeastOneFile = true; } - return true; + return wroteAtLeastOneFile ? utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN : utils::TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE; } -bool CodeGenerator::GenerateCodeOncePerAsset(const OncePerAssetRenderingContext& context, ICodeTemplate* codeTemplate) const +utils::TextFileCheckDirtyResult CodeGenerator::GenerateCodeOncePerAsset(const OncePerAssetRenderingContext& context, ICodeTemplate* codeTemplate) const { + bool wroteAtLeastOneFile = false; for (const auto& codeFile : codeTemplate->GetFilesToRenderOncePerAsset(context)) { - fs::path p(m_args->m_output_directory); - p.append(codeFile.m_file_name); + fs::path outputPath(m_args->m_output_directory); + outputPath.append(codeFile.m_file_name); - auto parentFolder(p); - parentFolder.remove_filename(); - create_directories(parentFolder); - - std::ofstream stream(p, std::fstream::out | std::fstream::binary); - - if (!stream.is_open()) + utils::TextFileCheckDirtyOutput out(outputPath); + if (!out.Open()) { - con::error("Failed to open file '{}'", p.string()); - return false; + con::error("Failed to open file '{}'", outputPath.string()); + return utils::TextFileCheckDirtyResult::FAILURE; } - codeTemplate->RenderOncePerAssetFile(stream, codeFile.m_tag, context); + codeTemplate->RenderOncePerAssetFile(out.Stream(), codeFile.m_tag, context); - stream.close(); + const auto fileResult = out.Close(); + if (fileResult == utils::TextFileCheckDirtyResult::FAILURE) + { + con::error("Failed to write file '{}'", outputPath.string()); + return utils::TextFileCheckDirtyResult::FAILURE; + } + + if (fileResult == utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN) + wroteAtLeastOneFile = true; } - return true; + return wroteAtLeastOneFile ? utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN : utils::TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE; } bool CodeGenerator::GetAssetWithName(const IDataRepository* repository, const std::string& name, StructureInformation*& asset) @@ -135,28 +144,55 @@ bool CodeGenerator::GenerateCode(const IDataRepository* repository) for (auto* asset : assets) { auto context = OncePerAssetRenderingContext::BuildContext(repository, asset); - if (!GenerateCodeOncePerAsset(*context, foundTemplate->second.get())) + const auto result = GenerateCodeOncePerAsset(*context, foundTemplate->second.get()); + switch (result) { + case utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN: + con::info("Successfully generated code for asset '{}' with preset '{}'", asset->m_definition->GetFullName(), foundTemplate->first); + break; + case utils::TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE: + con::info("Code was up to date for asset '{}' with preset '{}'", asset->m_definition->GetFullName(), foundTemplate->first); + break; + case utils::TextFileCheckDirtyResult::FAILURE: con::error("Failed to generate code for asset '{}' with preset '{}'", asset->m_definition->GetFullName(), foundTemplate->first); return false; } - - con::info("Successfully generated code for asset '{}' with preset '{}'", asset->m_definition->GetFullName(), foundTemplate->first); } { auto context = OncePerTemplateRenderingContext::BuildContext(repository); - if (!GenerateCodeOncePerTemplate(*context, foundTemplate->second.get())) + const auto result = GenerateCodeOncePerTemplate(*context, foundTemplate->second.get()); + switch (result) { + case utils::TextFileCheckDirtyResult::OUTPUT_WRITTEN: + con::info("Successfully generated code with preset '{}'", foundTemplate->first); + break; + case utils::TextFileCheckDirtyResult::OUTPUT_WAS_UP_TO_DATE: + con::info("Code was up to date for preset '{}'", foundTemplate->first); + break; + case utils::TextFileCheckDirtyResult::FAILURE: con::error("Failed to generate code with preset '{}'", foundTemplate->first); return false; } - - con::info("Successfully generated code with preset '{}'", foundTemplate->first); } } const auto end = std::chrono::steady_clock::now(); - con::debug("Generating code took {}ms", std::chrono::duration_cast(end - start).count()); + const auto timeInMs = std::chrono::duration_cast(end - start).count(); + con::debug("Generating code took {}ms", timeInMs); + + if (!m_args->m_build_log_file.empty()) + { + std::ofstream buildLogFile(m_args->m_build_log_file); + if (buildLogFile.is_open()) + { + buildLogFile << "Generating code took " << timeInMs << "ms\n"; + buildLogFile.close(); + } + else + { + con::error("Failed to open build log file"); + } + } return true; } diff --git a/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.h b/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.h index 727f12e6..8fca5eae 100644 --- a/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.h +++ b/src/ZoneCodeGeneratorLib/Generating/CodeGenerator.h @@ -1,6 +1,7 @@ #pragma once #include "ICodeTemplate.h" +#include "Utils/FileUtils.h" #include "ZoneCodeGeneratorArguments.h" #include @@ -17,8 +18,8 @@ public: private: void SetupTemplates(); - bool GenerateCodeOncePerTemplate(const OncePerTemplateRenderingContext& context, ICodeTemplate* codeTemplate) const; - bool GenerateCodeOncePerAsset(const OncePerAssetRenderingContext& context, ICodeTemplate* codeTemplate) const; + utils::TextFileCheckDirtyResult GenerateCodeOncePerTemplate(const OncePerTemplateRenderingContext& context, ICodeTemplate* codeTemplate) const; + utils::TextFileCheckDirtyResult GenerateCodeOncePerAsset(const OncePerAssetRenderingContext& context, ICodeTemplate* codeTemplate) const; static bool GetAssetWithName(const IDataRepository* repository, const std::string& name, StructureInformation*& asset); diff --git a/src/ZoneCodeGeneratorLib/ZoneCodeGenerator.cpp b/src/ZoneCodeGeneratorLib/ZoneCodeGenerator.cpp index 8039663e..c0a5e5b8 100644 --- a/src/ZoneCodeGeneratorLib/ZoneCodeGenerator.cpp +++ b/src/ZoneCodeGeneratorLib/ZoneCodeGenerator.cpp @@ -49,7 +49,7 @@ public: } private: - bool ReadHeaderData() + [[nodiscard]] bool ReadHeaderData() const { for (const auto& headerFile : m_args.m_header_paths) { @@ -62,7 +62,7 @@ private: return true; } - bool ReadCommandsData() + [[nodiscard]] bool ReadCommandsData() const { for (const auto& commandsFile : m_args.m_command_paths) { diff --git a/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.cpp b/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.cpp index fb5c354f..ec2d3d45 100644 --- a/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.cpp +++ b/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.cpp @@ -85,6 +85,13 @@ const CommandLineOption* const OPTION_GENERATE = .WithParameter("preset") .Reusable() .Build(); + +const CommandLineOption* const OPTION_BUILD_LOG = + CommandLineOption::Builder::Create() + .WithLongName("build-log") + .WithDescription("Specify a file to write a build log to.") + .WithParameter("logFilePath") + .Build(); // clang-format on const CommandLineOption* const COMMAND_LINE_OPTIONS[]{ @@ -97,6 +104,7 @@ const CommandLineOption* const COMMAND_LINE_OPTIONS[]{ OPTION_OUTPUT_FOLDER, OPTION_PRINT, OPTION_GENERATE, + OPTION_BUILD_LOG, }; namespace @@ -170,6 +178,10 @@ bool ZoneCodeGeneratorArguments::ParseArgs(const int argc, const char** argv, bo else m_output_directory = "."; + // --build-log + if (m_argument_parser.IsOptionSpecified(OPTION_BUILD_LOG)) + m_build_log_file = m_argument_parser.GetValueForOption(OPTION_BUILD_LOG); + // -h; --header if (m_argument_parser.IsOptionSpecified(OPTION_HEADER)) { diff --git a/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.h b/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.h index f4b21dfb..4df497ae 100644 --- a/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.h +++ b/src/ZoneCodeGeneratorLib/ZoneCodeGeneratorArguments.h @@ -18,6 +18,10 @@ public: std::vector m_command_paths; std::string m_output_directory; + // Generate a build log that is always written for the compiler to determine + // the last output time. + std::string m_build_log_file; + std::vector m_template_names; private: diff --git a/tools/scripts/source_templating.lua b/tools/scripts/source_templating.lua index a87d4d53..db6b7264 100644 --- a/tools/scripts/source_templating.lua +++ b/tools/scripts/source_templating.lua @@ -8,6 +8,7 @@ function useSourceTemplating(projectName) local templateFile = templateFiles[i] local relativeTemplatePath = path.getrelative(projectFolder, templateFile) local relativeResultPath = path.replaceextension(relativeTemplatePath, "") + local relativeLogFilePath = path.replaceextension(relativeTemplatePath, ".log") local resultExtension = path.getextension(relativeResultPath) local data = io.readfile(templateFile) @@ -38,8 +39,12 @@ function useSourceTemplating(projectName) buildcommands { '"' .. TargetDirectoryBuildTools .. '/' .. ExecutableByOs('RawTemplater') .. '"' .. ' -o "%{prj.location}/"' + .. " --build-log \"" .. relativeLogFilePath .. "\"" .. " %{file.relpath}" } + buildoutputs { + "%{prj.location}/" .. relativeLogFilePath + } for i = 1, #games do local gameName = games[i] local outputFileName = path.replaceextension(path.replaceextension(relativeResultPath, "") .. gameName, resultExtension)