diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 140161e..41d9f6d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,12 +2,11 @@ name: CMake on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release jobs: @@ -18,41 +17,39 @@ jobs: - uses: actions/checkout@v4 - uses: jidicula/clang-format-action@v4.13.0 with: - clang-format-version: '17' - exclude-regex: '(third_party)' + clang-format-version: "17" + exclude-regex: "(third_party)" build: - # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. - # You can convert this to a matrix build if you need cross-platform coverage. - # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} continue-on-error: true steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Packages (Ubuntu) - run: | - sudo apt-get install -y uuid-dev - if: matrix.os == 'ubuntu-latest' - - - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -Dflow-core_BUILD_TESTS=ON - - - name: Build - # Build your program with the given configuration - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 18 - - - name: Test - working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Packages (Ubuntu) + run: | + sudo apt-get install -y uuid-dev + if: matrix.os == 'ubuntu-latest' + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -Dflow-core_BUILD_TESTS=ON + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 18 + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: Test Module (${{matrix.os}}) + path: ${{github.workspace}}/build/tests/test_module.fmod + + - name: Test + working-directory: ${{github.workspace}}/build + run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index b6d22dd..f208656 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (c) 2024, Cisco Systems, Inc. +# All rights reserved. + cmake_minimum_required(VERSION 3.10) project(flow-core VERSION 1.1.1 LANGUAGES CXX) @@ -15,8 +18,9 @@ endif() # Options # ----------------------------------------------------------------------------- -option(flow-core_BUILD_TESTS "Build tests - requires gtest" OFF) -option(flow-core_INSTALL "Add installation targets" OFF) +option(${PROJECT_NAME}_BUILD_TESTS "Build tests (gtest)" OFF) +option(${PROJECT_NAME}_BUILD_TOOLS "Build tools" OFF) +option(${PROJECT_NAME}_INSTALL "Add installation targets" OFF) # ----------------------------------------------------------------------------- # Dependencies @@ -26,15 +30,21 @@ include(cmake/CPM.cmake) CPMAddPackage("gh:nlohmann/json@3.11.3") CPMAddPackage("gh:bshoshany/thread-pool@5.0.0") +CPMAddPackage("gh:Lecrapouille/zipper@3.0.0") +set_target_properties(zipper PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) # ----------------------------------------------------------------------------- # Library # ----------------------------------------------------------------------------- -set(flow-core_HEADERS_DIR "${CMAKE_CURRENT_LIST_DIR}/include") -file(GLOB flow-core_HEADERS "${flow-core_HEADERS_DIR}/flow/core/*.hpp") +set(${PROJECT_NAME}_HEADERS_DIR "${CMAKE_CURRENT_LIST_DIR}/include") +file(GLOB ${PROJECT_NAME}_HEADERS "${${PROJECT_NAME}_HEADERS_DIR}/flow/core/*.hpp") file(GLOB thread-pool_HEADERS "${thread-pool_SOURCE_DIR}/include/*.hpp") -list(APPEND ${flow-core_HEADERS} ${thread-pool_HEADERS}) +list(APPEND ${${PROJECT_NAME}_HEADERS} ${thread-pool_HEADERS}) add_library(${PROJECT_NAME} SHARED src/Connection.cpp @@ -48,10 +58,11 @@ add_library(${PROJECT_NAME} SHARED src/TypeConversion.cpp src/UUID.cpp - ${flow-core_HEADERS} + ${${PROJECT_NAME}_HEADERS} ) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +target_compile_definitions(${PROJECT_NAME} PRIVATE FLOW_CORE_EXPORT) target_include_directories(${PROJECT_NAME} PUBLIC @@ -68,31 +79,36 @@ if(APPLE) target_link_libraries(${PROJECT_NAME} PUBLIC "-framework CoreFoundation" pthread - nlohmann_json::nlohmann_json ) elseif(MSVC) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) target_compile_options(${PROJECT_NAME} PRIVATE /W4 /MP) - target_link_libraries(${PROJECT_NAME} PUBLIC nlohmann_json::nlohmann_json) else() target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) target_link_libraries(${PROJECT_NAME} PUBLIC dl pthread uuid - nlohmann_json::nlohmann_json ) endif() +target_link_libraries(${PROJECT_NAME} PUBLIC + nlohmann_json::nlohmann_json +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + zipper +) + # ----------------------------------------------------------------------------- # Install # ----------------------------------------------------------------------------- -if(flow-core_INSTALL) +if(${PROJECT_NAME}_INSTALL) set(export_destination "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") set(export_targets ${PROJECT_NAME}) - if (NOT flow-core_USE_EXTERNAL_JSON) + if (NOT ${PROJECT_NAME}_USE_EXTERNAL_JSON) list(APPEND export_targets nlohmann_json) endif() @@ -131,7 +147,15 @@ endif() # Tests # ----------------------------------------------------------------------------- -if (flow-core_BUILD_TESTS) +if (${PROJECT_NAME}_BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() + +# ----------------------------------------------------------------------------- +# Tools +# ----------------------------------------------------------------------------- + +if (${PROJECT_NAME}_BUILD_TOOLS) + add_subdirectory(tools) +endif() diff --git a/README.md b/README.md index 10d3c7d..c9d8521 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ [![License: MIT](https://img.shields.io/github/license/InFlowStructure/flow-core)](https://github.com/InFlowStructure/flow-core/blob/main/LICENSE) [![Language: C++20](https://img.shields.io/badge/Language-C%2B%2B20%20-blue)](https://en.cppreference.com/w/cpp/20) +![Linux](https://img.shields.io/badge/OS-Linux-blue) +![Windows](https://img.shields.io/badge/OS-Windows-blue) +![macOS](https://img.shields.io/badge/OS-macOS-blue) + ## Overview Flow Core is a cross-platform C++20 graph-based code engine designed for building dynamically modifiable code flows. It serves as a foundation for Low-Code/No-Code solutions by providing a flexible and extensible graph computation system. @@ -26,18 +30,21 @@ Flow Core is a cross-platform C++20 graph-based code engine designed for buildin ## Dependencies Flow Core relies on these open-source libraries: + - [nlohmann_json](https://github.com/nlohmann/json) - Modern JSON handling - [thread-pool](https://github.com/bshoshany/thread-pool) - Efficient thread management ## Building ### Basic Build + ```bash cmake -B build cmake --build build --parallel ``` ### Build with Tests + ```bash cmake -B build -Dflow-core_BUILD_TESTS=ON cmake --build build --parallel @@ -46,6 +53,7 @@ cmake --build build --parallel ## Installation Configure and install: + ```bash cmake -B build -Dflow-core_INSTALL=ON cmake --build build --parallel @@ -55,6 +63,7 @@ cmake --install build ## Getting Started Check out our [documentation](docs/getting-started.md) for: + - Basic concepts and architecture - Creating your first flow - Building custom nodes diff --git a/cmake/FlowModule.cmake b/cmake/FlowModule.cmake new file mode 100644 index 0000000..1ce47de --- /dev/null +++ b/cmake/FlowModule.cmake @@ -0,0 +1,50 @@ +function(CreateFlowModule module_name) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MODULE_BINARY_DIR "linux") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(MODULE_BINARY_DIR "macos") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(MODULE_BINARY_DIR "windows") + else() + message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") + endif() + + if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "^(x86_64|amd64|AMD64)$") + set(MODULE_BINARY_DIR "${MODULE_BINARY_DIR}/x86_64") + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "^(arm64|aarch64)$") + set(MODULE_BINARY_DIR "${MODULE_BINARY_DIR}/arm64") + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "^(i386|i686)$") + set(MODULE_BINARY_DIR "${MODULE_BINARY_DIR}/x86") + else() + message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}") + endif() + + add_custom_command(TARGET ${module_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/${module_name}/${MODULE_BINARY_DIR}/${module_name}${CMAKE_SHARED_LIBRARY_SUFFIX} + ) + + add_custom_command(TARGET ${module_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/module.json + $/${module_name}/module.json + ) + + add_custom_command(TARGET ${module_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/README.md + $/${module_name}/README.md + ) + + add_custom_command(TARGET ${module_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE + $/${module_name}/LICENSE + ) + + add_custom_command(TARGET ${module_name} POST_BUILD + WORKING_DIRECTORY $ + COMMAND ${CMAKE_COMMAND} -E tar -cfv ${CMAKE_CURRENT_BINARY_DIR}/${module_name}.fmod --format=zip ${module_name}/ + ) +endfunction() diff --git a/include/flow/core/Concepts.hpp b/include/flow/core/Concepts.hpp index 0a3de5b..9572dd1 100644 --- a/include/flow/core/Concepts.hpp +++ b/include/flow/core/Concepts.hpp @@ -20,7 +20,7 @@ FLOW_NAMESPACE_END /** * @brief Type traits for compile-time type information and validation */ -FLOW_SUBNAMESPACE_START(type_traits) +FLOW_SUBNAMESPACE_BEGIN(type_traits) /** * @brief Check if type is specialization of template @@ -44,7 +44,7 @@ FLOW_SUBNAMESPACE_END /** * @brief Concepts for constraining template parameters */ -FLOW_SUBNAMESPACE_START(concepts) +FLOW_SUBNAMESPACE_BEGIN(concepts) /** * @brief Requires type to be arithmetic (integral or floating point) diff --git a/include/flow/core/Core.hpp b/include/flow/core/Core.hpp index ba4a54a..5294133 100644 --- a/include/flow/core/Core.hpp +++ b/include/flow/core/Core.hpp @@ -13,6 +13,16 @@ #error "Platform unsupported" #endif +#if defined(__x86_64__) || defined(_M_X64) || defined(__amd64__) +#define FLOW_X86_64 +#elif defined(__i386__) || defined(_M_IX86) +#define FLOW_X86 +#elif defined(__arm__) || defined(__aarch64__) || defined(_M_ARM64) +#define FLOW_ARM +#else +#error "Architecture unsupported" +#endif + #ifdef FLOW_WINDOWS #define FLOW_CORE_CALL __stdcall #else @@ -21,7 +31,7 @@ // clang-format off #define FLOW_NAMESPACE_BEGIN namespace flow { -#define FLOW_SUBNAMESPACE_START(nested) namespace flow { namespace nested { +#define FLOW_SUBNAMESPACE_BEGIN(nested) namespace flow { namespace nested { #define FLOW_NAMESPACE_END } #define FLOW_SUBNAMESPACE_END } } //clang-format on diff --git a/include/flow/core/Module.hpp b/include/flow/core/Module.hpp index 8a27e85..a094f16 100644 --- a/include/flow/core/Module.hpp +++ b/include/flow/core/Module.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -17,64 +18,118 @@ using json = nlohmann::json; class NodeFactory; +/** + * @brief Structure to hold metadata for a flow module. + */ +struct ModuleMetaData +{ + static void Validate(const json& module_json); + + /// The name of the module. + std::string Name; + + /// The version of the module in semantic versioning format (e.g., "1.0.0"). + std::string Version; + + /// The author of the module. + std::string Author; + + /// A description of the module. + std::string Description; +}; + +/** + * @brief Class representing a flow module that can be loaded and unloaded. + * + * This class is responsible for managing the lifecycle of a flow module, + * including loading and unloading the module, registering and unregistering nodes, + * and validating module metadata. + */ class Module { - struct HandleDelete + struct HandleUnloader { void operator()(void*); }; public: /** - * @brief Constructs a flow module from a given directory. + * @brief Constructs a flow module with a given NodeFactory. + * @param factory The factory to load registered nodes into. + */ + Module(std::shared_ptr factory); + + /** + * @brief Constructs a flow module from a given JSON metadata and directory. * @param dir The directory to try to load. * @param factory The factory to load registered nodes into. */ Module(const std::filesystem::path& dir, std::shared_ptr factory); - Module(const json& module_json, const std::filesystem::path& dir, std::shared_ptr factory); - + /** + * @brief Destructor for the Module class. Unloads the module if loaded. + */ ~Module(); /** * @brief Loads a module from a given directory. * @param dir The directory to try to load. + * @return True if the module was loaded successfully, false otherwise. */ bool Load(const std::filesystem::path& dir); /** * @brief Unloads the currently loaded module handle. + * @return True if the module was unloaded successfully, false otherwise. */ bool Unload(); - bool IsLoaded() const noexcept { return _handle != nullptr; } - - const std::string& GetName() const noexcept { return _name; } + /** + * @brief Registers the module nodes with the factory. + * This function will call the `RegisterModule` function from the module binary. + */ + void RegisterModuleNodes(); - const std::string& GetVersion() const noexcept { return _version; } + /** + * @brief Unregisters the module nodes from the factory. + * This function will call the `UnregisterModule` function from the module binary. + */ + void UnregisterModuleNodes(); - const std::string& GetAuthor() const noexcept { return _author; } + /** + * @brief Registers the module nodes with the provided factory. + * @param factory The factory to register nodes with. + */ + void RegisterModuleNodes(const std::shared_ptr& factory); - const std::string& GetDescription() const noexcept { return _description; } + /** + * @brief Unregisters the module nodes from the provided factory. + * @param factory The factory to unregister nodes from. + */ + void UnregisterModuleNodes(const std::shared_ptr& factory); - const std::vector& GetDependencies() const noexcept { return _dependencies; } + /** + * @brief Checks if the module is currently loaded. + * @return True if loaded, false otherwise. + */ + bool IsLoaded() const noexcept { return _handle != nullptr; } - private: - void Validate(const json& module_json); + /** + * @brief Gets the metadata associated with the module. + * @return A const reference to the ModuleMetaData. + */ + const std::optional& GetMetaData() const noexcept { return _metadata; } public: + /** + * @brief The file extension for module metadata files. + */ static const std::string FileExtension; - static const std::string BinaryExtension; private: - std::string _name; - std::string _version; - std::string _author; - std::string _description; - std::vector _dependencies; - + std::unique_ptr _handle; + std::optional _metadata; std::shared_ptr _factory; - std::unique_ptr _handle; }; FLOW_NAMESPACE_END diff --git a/src/Module.cpp b/src/Module.cpp index 4af2397..78d7c92 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -1,7 +1,11 @@ +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. + #include "flow/core/Module.hpp" #include "flow/core/NodeFactory.hpp" +#include #include #include @@ -12,25 +16,133 @@ #ifdef FLOW_WINDOWS #include #else +#include "Module.hpp" #include #endif FLOW_NAMESPACE_BEGIN -const std::string Module::FileExtension = "flowmod"; +using namespace zipper; + +struct formatted_error : public std::runtime_error +{ + template + formatted_error(const std::format_string fmt, Args&&... args) + : runtime_error(std::format(fmt, std::forward(args)...)) + { + } +}; + +struct runtime_error : public formatted_error +{ + using formatted_error::formatted_error; +}; + +struct invalid_argument : public formatted_error +{ + using formatted_error::formatted_error; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ModuleMetaData, Name, Version, Author, Description); + +const std::string Module::FileExtension = "fmod"; + +#ifdef FLOW_WINDOWS +constexpr const char* platform = "windows"; +#elif defined(FLOW_APPLE) +constexpr const char* platform = "macos"; +#else +constexpr const char* platform = "linux"; +#endif + +#ifdef FLOW_X86_64 +constexpr const char* architecture = "x86_64"; +#elif defined(FLOW_X86) +constexpr const char* architecture = "x86"; +#elif defined(FLOW_ARM) +constexpr const char* architecture = "arm64"; +#else +#error "Unsupported architecture" +#endif #ifdef FLOW_WINDOWS -const std::string Module::BinaryExtension = ".dll"; +constexpr const char* library_extension = ".dll"; #elif defined(FLOW_APPLE) -const std::string Module::BinaryExtension = ".dylib"; +constexpr const char* library_extension = ".dylib"; #else -const std::string Module::BinaryExtension = ".so"; +constexpr const char* library_extension = ".so"; #endif -void Module::HandleDelete::operator()(void* handle) +/** + * @brief Get the Module Binary Path object. + * + * @param dir The directory of the module files. + * @returns The path to the module binary. + */ +std::filesystem::path GetModuleBinaryPath(const std::filesystem::path& dir) +{ + return dir / platform / architecture / dir.stem().replace_extension(library_extension); +} + +/** + * @brief Get the Module Meta Data Path object. + * + * @param dir The directory of the module files. + * @returns The path to the module metadata file. + */ +std::filesystem::path GetModuleMetaDataPath(const std::filesystem::path& dir) { return dir / "module.json"; } + +/** + * @brief Get the temporary module path. + * + * This function creates a directory for temporary modules if it does not exist. + * The path is platform-specific and uses the system's temporary directory. + * + * @returns The path to the temporary module directory. + */ +std::filesystem::path GetTempModulePath() +{ + auto temp_path = std::filesystem::temp_directory_path() / "flow_modules"; + std::filesystem::create_directories(temp_path); + return temp_path; +} + +void ModuleMetaData::Validate(const json& mod_j) +{ + if (!mod_j.contains("Name") || !mod_j["Name"].is_string()) + { + throw std::invalid_argument("Module metadata is missing 'Name' field or it is not a string."); + } + + if (!mod_j.contains("Version") || !mod_j["Version"].is_string()) + { + throw std::invalid_argument("Module metadata is missing 'Version' field or it is not a string."); + } + + std::regex semver_regex(R"(^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$)"); + const std::string& version = mod_j["Version"].get_ref(); + + if (!std::regex_match(version, semver_regex)) + { + throw flow::invalid_argument("Module metadata 'Version' field is not a valid semantic version. (version={})", + version); + } + + if (!mod_j.contains("Author") || !mod_j["Author"].is_string()) + { + throw std::invalid_argument("Module metadata is missing 'Author' field or it is not a string."); + } + + if (!mod_j.contains("Description") || !mod_j["Description"].is_string()) + { + throw std::invalid_argument("Module metadata is missing 'Description' field or it is not a string."); + } +} + +void Module::HandleUnloader::operator()(void* handle) { #ifdef FLOW_WINDOWS - if (!FreeLibrary(std::bit_cast(handle))) + if (!FreeLibrary(reinterpret_cast(handle))) #else if (dlclose(handle) != 0) #endif @@ -52,21 +164,10 @@ auto LoadModuleLibrary(const std::string& name) #endif } -Module::Module(const std::filesystem::path& dir, std::shared_ptr factory) : _factory(std::move(factory)) -{ - Load(dir); -} +Module::Module(std::shared_ptr factory) : _factory(std::move(factory)) {} -Module::Module(const json& module_j, const std::filesystem::path& dir, std::shared_ptr factory) - : _factory(std::move(factory)) +Module::Module(const std::filesystem::path& dir, std::shared_ptr factory) : _factory(std::move(factory)) { - Validate(module_j); - - _name = module_j["Name"]; - _version = module_j["Version"]; - _author = module_j["Author"]; - _description = module_j["Description"]; - Load(dir); } @@ -81,74 +182,44 @@ bool Module::Load(const std::filesystem::path& path) if (!std::filesystem::exists(path)) { - throw std::runtime_error(std::format("Path does not exist. (file={})", path.string())); + throw flow::runtime_error("Path does not exist. (file={})", path.string()); } - if (std::filesystem::is_regular_file(path)) + if (!std::filesystem::is_regular_file(path)) { - if (path.extension() != ("." + FileExtension)) - { - throw std::runtime_error( - std::format("File is not a module. (file={}, extension={})", path.string(), FileExtension)); - } - - std::ifstream module_fs(path); - json module_j = json::parse(module_fs); - - Validate(module_j); - - _name = module_j["Name"]; - _version = module_j["Version"]; - _author = module_j["Author"]; - _description = module_j["Description"]; + throw flow::runtime_error("Path is not a file. (file={})", path.string()); } - const std::string module_file_name = _name + BinaryExtension; - std::filesystem::path module_binary_path; - for (const auto& entry : std::filesystem::recursive_directory_iterator(path.parent_path())) + Unzipper unzipper(path.string()); + if (!unzipper.isOpened()) { - if (!std::filesystem::is_regular_file(entry) || - (entry.path().filename() != module_file_name && entry.path().filename() != ("lib" + module_file_name))) - { - continue; - } - - module_binary_path = entry; - break; + throw flow::runtime_error("Failed to open module archive. (file={})", path.string()); } - if (!std::filesystem::exists(module_binary_path)) - { - throw std::runtime_error( - std::format("Module binary does not exist. (binary_path={})", module_binary_path.string())); - } + unzipper.extractAll(GetTempModulePath().string(), Unzipper::OverwriteMode::Overwrite); + unzipper.close(); + + json module_j = json::parse(std::ifstream(GetModuleMetaDataPath(GetTempModulePath() / path.stem()))); + ModuleMetaData::Validate(module_j); + _metadata = module_j; + + auto binary_path = GetModuleBinaryPath(GetTempModulePath() / path.stem()); #ifdef UNICODE - auto handle = LoadModuleLibrary(module_binary_path.wstring()); + auto handle = LoadModuleLibrary(binary_path.wstring()); #else - auto handle = LoadModuleLibrary(module_binary_path.string()); + auto handle = LoadModuleLibrary(binary_path.string()); #endif if (!handle) { - throw std::runtime_error( - std::format("Failed to load module binary. (binary_path={})", module_binary_path.string())); + throw flow::runtime_error("Failed to load module binary. (binary_path={})", binary_path.string()); } -#ifdef FLOW_WINDOWS - auto register_func = GetProcAddress(handle, NodeFactory::RegisterModuleFuncName); -#else - auto register_func = dlsym(handle, NodeFactory::RegisterModuleFuncName); -#endif - if (auto RegisterModule_func = std::bit_cast(register_func)) - { - RegisterModule_func(_factory); - _handle.reset(std::bit_cast(handle)); - return true; - } + _handle.reset(reinterpret_cast(handle)); - HandleDelete{}(handle); + RegisterModuleNodes(_factory); - throw std::runtime_error(std::format("Failed to load symbols for RegisterModule. (file={})", path.string())); + return true; } bool Module::Unload() @@ -158,35 +229,65 @@ bool Module::Unload() return false; } + UnregisterModuleNodes(_factory); + + _handle.reset(); + return true; +} + +void Module::RegisterModuleNodes() { RegisterModuleNodes(_factory); } + +void Module::UnregisterModuleNodes() { UnregisterModuleNodes(_factory); } + +void Module::RegisterModuleNodes(const std::shared_ptr& factory) +{ + if (!_handle) + { + throw std::runtime_error("Module is not loaded, cannot register nodes."); + } + + if (!factory) + { + throw std::invalid_argument("NodeFactory is null, cannot register nodes."); + } + #ifdef FLOW_WINDOWS - auto unregister = GetProcAddress(std::bit_cast(_handle.get()), NodeFactory::UnregisterModuleFuncName); + auto register_func = + GetProcAddress(reinterpret_cast(_handle.get()), NodeFactory::RegisterModuleFuncName); #else - auto unregister = dlsym(_handle.get(), NodeFactory::UnregisterModuleFuncName); + auto register_func = dlsym(_handle.get(), NodeFactory::RegisterModuleFuncName); #endif - if (auto UnregisterModule_func = std::bit_cast(unregister)) + if (auto RegisterModule_func = reinterpret_cast(register_func)) [[likely]] { - UnregisterModule_func(_factory); + return RegisterModule_func(factory); } - _handle.reset(); - return true; + throw std::runtime_error("Failed to load symbols for RegisterModule."); } -void Module::Validate(const json& mod_j) +void Module::UnregisterModuleNodes(const std::shared_ptr& factory) { - if (!mod_j.contains("Name") || !mod_j.contains("Author") || !mod_j.contains("Version") || - !mod_j.contains("Description")) + if (!_handle) { - throw std::invalid_argument("JSON is not a valid flow::Module"); + throw std::runtime_error("Module is not loaded, cannot unregister nodes."); } - std::regex semver_regex(R"(^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$)"); - const std::string& version = mod_j["Version"].get_ref(); + if (!factory) + { + throw std::invalid_argument("NodeFactory is null, cannot unregister nodes."); + } - if (!std::regex_match(version, semver_regex)) +#ifdef FLOW_WINDOWS + auto unregister = GetProcAddress(reinterpret_cast(_handle.get()), NodeFactory::UnregisterModuleFuncName); +#else + auto unregister = dlsym(_handle.get(), NodeFactory::UnregisterModuleFuncName); +#endif + if (auto UnregisterModule_func = reinterpret_cast(unregister)) [[likely]] { - throw std::invalid_argument(std::format("Version is not in numeric only format (version={})", version)); + return UnregisterModule_func(factory); } + + throw std::runtime_error("Failed to load symbols for UnregisterModule."); } FLOW_NAMESPACE_END diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index eb3361f..18f8fcb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,35 +4,19 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) #=============================================================================# -# Fetch testing dependencies +# Dependencies #=============================================================================# -find_package(GTest CONFIG QUIET) -if (NOT GTest_FOUND) - include(FetchContent) - FetchContent_Declare( - googletest OVERRIDE_FIND_PACKAGE - URL https://github.com/google/googletest/releases/download/v1.15.2/googletest-1.15.2.tar.gz - GIT_TAG v1.15.2 - ) - - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) -endif() - -#=============================================================================# -# Add Test Module -#=============================================================================# - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests/bin) -add_subdirectory(test_module) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +CPMAddPackage("gh:google/googletest@1.15.2") #=============================================================================# # Test Executable #=============================================================================# +set(TEST_EXE flow_core_tests) add_executable( - flow_core_tests + ${TEST_EXE} factory_test.cpp graph_test.cpp @@ -44,18 +28,16 @@ add_executable( if(MSVC) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) - target_compile_options(flow_core_tests PRIVATE /W4 /MP) + target_compile_options(${TEST_EXE} PRIVATE /W4 /MP) endif() -target_link_libraries( - flow_core_tests - +target_link_libraries(${TEST_EXE} flow-core GTest::gtest_main ) -target_include_directories(flow_core_tests PRIVATE - ../include/flow/core +target_include_directories(${TEST_EXE} PRIVATE + ${CMAKE_SOURCE_DIR}/include/flow/core ${thread_pool_SOURCE_DIR}/include ) @@ -63,9 +45,19 @@ if(MSVC) add_custom_command(TARGET flow_core_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $ + $ ) endif() +#=============================================================================# +# Add Test Module +#=============================================================================# + +add_subdirectory(test_module) + +#=============================================================================# +# Discover tests +#=============================================================================# + include(GoogleTest) -gtest_discover_tests(flow_core_tests) +gtest_discover_tests(${TEST_EXE}) diff --git a/tests/factory_test.cpp b/tests/factory_test.cpp index 9d93d52..3be8ced 100644 --- a/tests/factory_test.cpp +++ b/tests/factory_test.cpp @@ -1,9 +1,12 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "flow/core/Env.hpp" #include "flow/core/Node.hpp" #include "flow/core/NodeFactory.hpp" +#include + using namespace flow; namespace diff --git a/tests/graph_test.cpp b/tests/graph_test.cpp index c3e0e0b..f8e150a 100644 --- a/tests/graph_test.cpp +++ b/tests/graph_test.cpp @@ -1,4 +1,5 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "flow/core/Env.hpp" #include "flow/core/Graph.hpp" @@ -6,6 +7,8 @@ #include "flow/core/NodeData.hpp" #include "flow/core/NodeFactory.hpp" +#include + using namespace flow; namespace diff --git a/tests/indexable_name_test.cpp b/tests/indexable_name_test.cpp index 08a7e19..4fb1163 100644 --- a/tests/indexable_name_test.cpp +++ b/tests/indexable_name_test.cpp @@ -1,7 +1,10 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "IndexableName.hpp" +#include + #include #include #include diff --git a/tests/module_test.cpp b/tests/module_test.cpp index ccf683b..bbf86b9 100644 --- a/tests/module_test.cpp +++ b/tests/module_test.cpp @@ -1,58 +1,95 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "flow/core/Env.hpp" #include "flow/core/Module.hpp" #include "flow/core/NodeFactory.hpp" +#include #include #include using namespace flow; -nlohmann::json module_json{ - {"Author", "Cisco Systems, Inc."}, - {"Description", "A test module."}, - {"Name", "test_module"}, - {"Version", "0.0.0"}, -}; - -const std::filesystem::path module_path = std::filesystem::current_path(); +const std::filesystem::path module_path = std::filesystem::current_path() / "test_module.fmod"; auto factory = std::make_shared(); auto env = Env::Create(factory); -TEST(ModuleTest, Load) { ASSERT_NO_THROW(Module(module_json, module_path, factory)); } +TEST(ModuleTest, Load) +{ + Module m(factory); + ASSERT_NO_THROW(ASSERT_TRUE(m.Load(module_path))); + ASSERT_TRUE(m.IsLoaded()); +} -TEST(ModuleTest, RunModuleNodes) +TEST(ModuleTest, Unload) { - Module module(module_json, module_path, factory); + Module module(module_path, factory); ASSERT_TRUE(module.IsLoaded()); + ASSERT_TRUE(module.Unload()); + ASSERT_FALSE(module.IsLoaded()); +} - SharedNode node; - ASSERT_NO_THROW(node = factory->CreateNode("TestNode", UUID{}, "test", env)); - ASSERT_NE(node, nullptr); - node->OnCompute.Bind("Test", [&] { FAIL(); }); - node->OnError.Bind("Test", [&](auto&& e) { ASSERT_THROW(throw e, std::exception); }); - node->InvokeCompute(); +TEST(ModuleTest, LoadInvalidPath) +{ + Module m(factory); + ASSERT_THROW(m.Load("invalid_path.fmod"), std::runtime_error); } -TEST(ModuleTest, Unload) +TEST(ModuleTest, UnloadWithoutLoad) { - Module module(module_json, module_path, factory); + Module m(factory); + ASSERT_FALSE(m.IsLoaded()); + ASSERT_FALSE(m.Unload()); + ASSERT_FALSE(m.IsLoaded()); +} + +TEST(ModuleTest, ValidateMetaData) +{ + Module module(module_path, factory); ASSERT_TRUE(module.IsLoaded()); - ASSERT_NO_THROW(module.Unload()); - ASSERT_FALSE(module.IsLoaded()); + + const auto& meta_data = module.GetMetaData(); + ASSERT_EQ(meta_data->Name, "test_module"); + ASSERT_EQ(meta_data->Version, "0.0.0"); + ASSERT_EQ(meta_data->Author, "Cisco Systems, Inc."); + ASSERT_EQ(meta_data->Description, "A test module."); } -TEST(ModuleTest, LoadUnloadLoad) +TEST(ModuleTest, RegisterModuleNodes) { - Module module(module_json, module_path, factory); + Module module(module_path, factory); ASSERT_TRUE(module.IsLoaded()); - ASSERT_NO_THROW(ASSERT_FALSE(module.Load(module_path))); + + ASSERT_NO_THROW(module.RegisterModuleNodes()); + ASSERT_NE(factory->CreateNode("TestNode", UUID{}, "test", env), nullptr); +} + +TEST(ModuleTest, UnregisterModuleNodes) +{ + Module module(module_path, factory); ASSERT_TRUE(module.IsLoaded()); - ASSERT_NO_THROW(ASSERT_TRUE(module.Unload())); - ASSERT_FALSE(module.IsLoaded()); - ASSERT_NO_THROW(ASSERT_TRUE(module.Load(module_path))); + + module.RegisterModuleNodes(); + ASSERT_NE(factory->CreateNode("TestNode", UUID{}, "test", env), nullptr); + + ASSERT_NO_THROW(module.UnregisterModuleNodes()); + ASSERT_EQ(factory->CreateNode("TestNode", UUID{}, "test", env), nullptr); +} + +TEST(ModuleTest, RunModuleNodes) +{ + Module module(module_path, factory); ASSERT_TRUE(module.IsLoaded()); + + module.RegisterModuleNodes(); + + SharedNode node; + ASSERT_NO_THROW(node = factory->CreateNode("TestNode", UUID{}, "test", env)); + ASSERT_NE(node, nullptr); + node->OnCompute.Bind("Test", [&] { FAIL(); }); + node->OnError.Bind("Test", [&](auto&& e) { ASSERT_THROW(throw e, std::exception); }); + node->InvokeCompute(); } diff --git a/tests/node_test.cpp b/tests/node_test.cpp index b899dde..89b9a6e 100644 --- a/tests/node_test.cpp +++ b/tests/node_test.cpp @@ -1,4 +1,5 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "flow/core/Env.hpp" #include "flow/core/FunctionNode.hpp" @@ -6,6 +7,7 @@ #include "flow/core/NodeData.hpp" #include "flow/core/NodeFactory.hpp" +#include #include using namespace flow; diff --git a/tests/test_module/CMakeLists.txt b/tests/test_module/CMakeLists.txt index f513408..16ac805 100644 --- a/tests/test_module/CMakeLists.txt +++ b/tests/test_module/CMakeLists.txt @@ -21,10 +21,10 @@ add_dependencies(${PROJECT_NAME} flow-core) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(${PROJECT_NAME} PUBLIC flow-core) -# Not typical of a module, but required for these unit tests -add_custom_command(TARGET ${PROJECT_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/test_module.flowmod - $ +include(${CMAKE_SOURCE_DIR}/cmake/FlowModule.cmake) +CreateFlowModule(${PROJECT_NAME}) + +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/test_module.fmod ${CMAKE_BINARY_DIR}/tests + COMMENT "Copy ${CMAKE_CURRENT_BINARY_DIR}/test_module.fmod to ${CMAKE_BINARY_DIR}/tests" ) diff --git a/tests/test_module/LICENSE b/tests/test_module/LICENSE new file mode 100644 index 0000000..6032004 --- /dev/null +++ b/tests/test_module/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 InFlowStructure + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/test_module/README.md b/tests/test_module/README.md new file mode 100644 index 0000000..ff2a43e --- /dev/null +++ b/tests/test_module/README.md @@ -0,0 +1,14 @@ +# Test Flow Module + +This repository contains a simple test module for the [Flow](https://github.com/InFlowStructure/flow-core) framework. +It is intended for demonstration and testing purposes only. + +## Features + +- Example Flow module structure +- Placeholder code for integration and testing +- No production functionality + +## License + +This project is licensed under the MIT License. diff --git a/tests/test_module/test_module.flowmod b/tests/test_module/module.json similarity index 100% rename from tests/test_module/test_module.flowmod rename to tests/test_module/module.json diff --git a/tests/type_name_test.cpp b/tests/type_name_test.cpp index 1d7bec3..e07e6d8 100644 --- a/tests/type_name_test.cpp +++ b/tests/type_name_test.cpp @@ -1,7 +1,10 @@ -#include +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. #include "TypeName.hpp" +#include + using flow::TypeName; TEST(TypeNameTest, LanguageTypes) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..4396939 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(module_manager) diff --git a/tools/module_manager/CMakeLists.txt b/tools/module_manager/CMakeLists.txt new file mode 100644 index 0000000..0733572 --- /dev/null +++ b/tools/module_manager/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.10) + +project(flow_module_manager VERSION 0.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +CPMAddPackage("gh:jarro2783/cxxopts@3.2.0") + +add_executable(${PROJECT_NAME} src/main.cpp) +target_link_libraries(${PROJECT_NAME} PRIVATE flow-core zipper cxxopts) + +if(MSVC) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) +endif() \ No newline at end of file diff --git a/tools/module_manager/src/main.cpp b/tools/module_manager/src/main.cpp new file mode 100644 index 0000000..6cf2bdc --- /dev/null +++ b/tools/module_manager/src/main.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +#include +#include +#include + +using json = nlohmann::json; +using namespace zipper; + +int main(int argc, char** argv) +{ + // clang-format off + cxxopts::Options options("FlowModuleManager"); + options.add_options() + ("f,file", "Flow file to open", cxxopts::value()) + ("h,help", "Print usage"); + // clang-format on + + cxxopts::ParseResult result; + + try + { + result = options.parse(argc, argv); + } + catch (const cxxopts::exceptions::exception& e) + { + std::cerr << "Caught exception while parsing arguments: " << e.what() << std::endl; + return EXIT_FAILURE; + } + + if (result.count("help")) + { + std::cerr << options.help() << std::endl; + return EXIT_SUCCESS; + } + + if (result.count("file") == 0) + { + std::cerr << "No fmod file provided" << std::endl; + return EXIT_FAILURE; + } + + const auto temp_path = std::filesystem::temp_directory_path() / "tmp_flow_modules"; + + try + { + std::filesystem::path fmod_file_path = result["file"].as(); + if (!std::filesystem::exists(fmod_file_path) || !std::filesystem::is_regular_file(fmod_file_path)) + { + std::cerr << fmod_file_path.string() << " is not a file" << std::endl; + return EXIT_FAILURE; + } + + Unzipper unzipper(fmod_file_path.string()); + if (!unzipper.isOpened()) + { + throw std::runtime_error(std::format("Failed to open module archive. (file={})", fmod_file_path.string())); + } + + unzipper.extractAll((temp_path).string(), Unzipper::OverwriteMode::Overwrite); + unzipper.close(); + + flow::ModuleMetaData::Validate(json::parse(std::ifstream(temp_path / fmod_file_path.stem() / "module.json"))); + } + catch (const std::exception& e) + { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file