Initial public khodo snapshot
publish / publish (push) Successful in 53m2s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 2750dd7f3a
51 changed files with 5515 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
DisableFormat: true
+9
View File
@@ -0,0 +1,9 @@
CompileFlags:
CompilationDatabase: build/build_x86_debug
Add:
- -Wall
- -Wextra
Diagnostics:
ClangTidy:
Remove: ['*']
+14
View File
@@ -0,0 +1,14 @@
.git
.tmp
build
**/__pycache__
package/CMakeCache.txt
package/CMakeFiles
package/metadata
package/.ninja_*
package/*.json
package/*.cmake
package/*.ninja
package/*.sh
package/*-smoke
package/modulus-smoke
+105
View File
@@ -0,0 +1,105 @@
name: publish
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
permissions:
contents: read
packages: write
actions: write
env:
CONAN_HOME: build/build_common/conan_cache
MODULUS_DIRECT: "1"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
if [ -r /etc/os-release ]; then
. /etc/os-release
fi
if [ "${ID:-}" = "ubuntu" ] && [ -n "${VERSION_CODENAME:-}" ]; then
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sed -i '/^Types: deb$/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources
printf '%s\n' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
'' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME}-security" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
> /etc/apt/sources.list.d/ubuntu-arm64.sources
elif [ -f /etc/apt/sources.list ]; then
sed -i 's|^deb http://archive.ubuntu.com/ubuntu|deb [arch=amd64] http://archive.ubuntu.com/ubuntu|' /etc/apt/sources.list
sed -i 's|^deb http://security.ubuntu.com/ubuntu|deb [arch=amd64] http://security.ubuntu.com/ubuntu|' /etc/apt/sources.list
printf '%s\n' \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME} main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-updates main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-backports main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-security main universe restricted multiverse" \
> /etc/apt/sources.list.d/ubuntu-arm64.list
fi
fi
dpkg --add-architecture arm64
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
g++-12 \
g++-aarch64-linux-gnu \
gcc-12 \
gcc-aarch64-linux-gnu \
git \
libdrm-dev \
libdrm-dev:arm64 \
libegl-dev \
libegl-dev:arm64 \
libgbm-dev \
libgbm-dev:arm64 \
libgles-dev \
libgles-dev:arm64 \
libglvnd-dev \
libglvnd-dev:arm64 \
ninja-build \
pkg-config \
python3 \
python3-venv
python3 -m venv build/build_common/ci_python
build/build_common/ci_python/bin/python -m pip install --no-cache-dir conan==2.26.2
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
- name: Restore Conan cache
uses: actions/cache@v4
with:
path: build/build_common/conan_cache
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.py', 'modulus.json', 'tools/conan/profiles/*') }}
restore-keys: conan-${{ runner.os }}-
- name: Configure native release
run: ./tools/configure.py --target x86_64_gcc12_release
- name: Build native release
run: ./tools/build.py --target x86_64_gcc12_release
- name: Test native release
run: ./tools/test.py --target x86_64_gcc12_release
- name: Create and upload packages
if: ${{ gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/master') }}
env:
CONAN_USER: ${{ secrets.CONAN_USER }}
CONAN_TOKEN: ${{ secrets.CONAN_TOKEN }}
MODULUS_CONAN_UPLOAD: "1"
MODULUS_ALLOW_PACKAGE_REPLACE: "1"
run: ./tools/package.py --all --replace
+25
View File
@@ -0,0 +1,25 @@
__pycache__/
**/__pycache__/
*.pyc
*.pyo
*.pyd
.tmp/
build/
CMakeUserPresets.json
package/CMakeCache.txt
package/CMakeFiles/
package/metadata/
package/.ninja_*
package/*.json
package/*.cmake
package/*.ninja
package/*.sh
package/*-smoke
package/Boost*
package/boost*
package/modulus-*
package/modulusTargets.cmake
package/cmakedeps_macros.cmake
package/conan_toolchain.cmake
package/conandeps_legacy.cmake
+71
View File
@@ -0,0 +1,71 @@
cmake_minimum_required(VERSION 3.25)
project(modulus VERSION 0.1.0 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CTest)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
include(cmake/modulus_third_party.cmake)
add_library(modulus
src/config.cpp
src/error.cpp
src/json_config.cpp
src/log.cpp
)
target_include_directories(modulus PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(modulus PUBLIC cxx_std_23)
target_link_libraries(modulus PUBLIC
Boost::boost
Boost::json
)
add_library(modulus::modulus ALIAS modulus)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
install(TARGETS modulus
EXPORT modulus-targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT modulus-targets
NAMESPACE modulus::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/modulus
)
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/modulus_third_party.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/modulus
)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/modulus_config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/modulus-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/modulus
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/modulus-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/modulus-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/modulus-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/modulus
)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Khodo
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.
+69
View File
@@ -0,0 +1,69 @@
# Modulus
Modulus is the shared C++23 foundation library and build tooling used by the khodo Product Vamp repositories. It provides small runtime utilities, Conan profiles, CMake package integration, editor templates, typed LSP compile-database setup, and bridge targets for the external libraries needed by Product Vamp.
The public package intentionally keeps a small dependency surface:
- Boost.JSON
- OpenSSL
- SQLite
- raylib
- miniaudio 0.11.22
## Build
Use the checked-in tools directly inside CI or a compatible build environment:
```sh
MODULUS_DIRECT=1 ./tools/configure.py --target x86_64_gcc12_release
./tools/build.py --target x86_64_gcc12_release
./tools/test.py --target x86_64_gcc12_release
```
Outside that environment, downstream repos use `python3 modulus.py <command>` to fetch the public `modulus/latest` package from the khodo Conan registry and export the tool wrappers into `build/build_tools/`.
`build` automatically configures a target when CMake metadata is missing. Run `configure` explicitly when you want to force a reconfigure or refresh dependencies.
Refresh clangd compile databases with:
```sh
./tools/lsp.py
```
Product repos can add typed extra CMake compile databases under `tooling.lsp.compile_databases`; this is intended for cases like Product Vamp's Pico firmware tree.
## Package
Create Conan packages with:
```sh
./tools/package.py --all
```
Publishing requires credentials only for the upload step:
```sh
CONAN_USER=<user> \
CONAN_TOKEN=<token> \
MODULUS_CONAN_UPLOAD=1 \
MODULUS_ALLOW_PACKAGE_REPLACE=1 \
./tools/package.py --all --replace
```
The default public Conan remote is:
```text
https://git.thyme-labs.com/api/packages/khodo/conan
```
Generic application artifacts default to:
```text
https://git.thyme-labs.com/api/packages/khodo/generic
```
Configure/build/test do not require package credentials for public packages.
## License
Modulus is released under the MIT License.
+6
View File
@@ -0,0 +1,6 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
include("${CMAKE_CURRENT_LIST_DIR}/modulus_third_party.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/modulus-targets.cmake")
+70
View File
@@ -0,0 +1,70 @@
include(CMakeFindDependencyMacro)
find_dependency(Boost CONFIG REQUIRED COMPONENTS json)
find_dependency(OpenSSL CONFIG REQUIRED)
find_dependency(SQLite3 CONFIG REQUIRED)
find_dependency(raylib CONFIG REQUIRED)
find_dependency(miniaudio CONFIG REQUIRED)
function(modulus_define_bridge target)
if(NOT TARGET ${target})
add_library(${target} INTERFACE IMPORTED)
endif()
endfunction()
modulus_define_bridge(modulus::boost_json)
set_target_properties(modulus::boost_json PROPERTIES
INTERFACE_LINK_LIBRARIES "Boost::boost;Boost::json"
)
modulus_define_bridge(modulus::openssl)
set_target_properties(modulus::openssl PROPERTIES
INTERFACE_LINK_LIBRARIES "OpenSSL::SSL;OpenSSL::Crypto"
)
modulus_define_bridge(modulus::sqlite)
set_target_properties(modulus::sqlite PROPERTIES
INTERFACE_LINK_LIBRARIES "SQLite::SQLite3"
)
set(_modulus_raylib_target "")
if(TARGET raylib)
set(_modulus_raylib_target raylib)
elseif(TARGET raylib::raylib)
set(_modulus_raylib_target raylib::raylib)
endif()
if(NOT _modulus_raylib_target)
message(FATAL_ERROR "raylib package did not define a supported CMake target")
endif()
set(_modulus_raylib_link_libraries ${_modulus_raylib_target})
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
list(APPEND _modulus_raylib_link_libraries EGL GLESv2 drm gbm atomic dl)
endif()
modulus_define_bridge(modulus::raylib)
set_target_properties(modulus::raylib PROPERTIES
INTERFACE_LINK_LIBRARIES "${_modulus_raylib_link_libraries}"
)
unset(_modulus_raylib_link_libraries)
unset(_modulus_raylib_target)
set(_modulus_miniaudio_target "")
if(TARGET miniaudio::miniaudio)
set(_modulus_miniaudio_target miniaudio::miniaudio)
elseif(TARGET miniaudio)
set(_modulus_miniaudio_target miniaudio)
endif()
if(NOT _modulus_miniaudio_target)
message(FATAL_ERROR "miniaudio package did not define a supported CMake target")
endif()
modulus_define_bridge(modulus::miniaudio)
set_target_properties(modulus::miniaudio PROPERTIES
INTERFACE_LINK_LIBRARIES "${_modulus_miniaudio_target}"
)
unset(_modulus_miniaudio_target)
+171
View File
@@ -0,0 +1,171 @@
import os
import pathlib
import conan
import conan.tools.build
import conan.tools.cmake
import conan.tools.files
import modulus.config
config = modulus.config
class modulus_recipe(conan.ConanFile):
name = "modulus"
version = "latest"
description = "Shared Conan/CMake tooling and C++23 utility framework"
license = "MIT"
package_type = "library"
settings = "os", "arch", "compiler", "build_type"
options = {
"shared": [
True,
False,
],
"fPIC": [
True,
False,
],
"with_tests": [
True,
False,
],
}
default_options = {
"shared": False,
"fPIC": True,
"with_tests": False,
"raylib/*:camera_system": False,
"raylib/*:custom_frame_control": True,
"raylib/*:customize_build": True,
"raylib/*:gestures_system": True,
"raylib/*:module_raudio": False,
"raylib/*:opengl_version": "ES-2.0",
"raylib/*:rprand_generator": False,
"glfw/*:with_wayland": False,
"glfw/*:with_x11": False,
}
exports = "modulus/*.py", "modulus.json"
exports_sources = (
"CMakeLists.txt",
"cmake/*.cmake",
"cmake/*.in",
"include/*.hpp",
"include/modulus/*.hpp",
"src/*.cpp",
"tests/CMakeLists.txt",
"tests/*.cpp",
"tests/*.hpp",
"schemas/*.json",
"tools/*.py",
"tools/conan/profiles/*",
"tools/docker/*",
"tools/editor/*",
"README.md",
)
def config_options(self):
settings = self.settings
options = self.options
if settings.os == "Windows":
del options.fPIC
def configure(self):
options = self.options
if options.shared:
del options.fPIC
def requirements(self):
config = modulus.config.load_recipe_config(self)
self.requires(
modulus.config.boost_ref(config),
transitive_headers=True,
transitive_libs=True,
options=modulus.config.boost_requirement_options(config),
)
self.requires(
"openssl/4.0.1",
transitive_headers=True,
transitive_libs=True,
)
self.requires(
"sqlite3/3.53.2",
transitive_headers=True,
transitive_libs=True,
)
self.requires(
"raylib/6.0",
transitive_headers=True,
transitive_libs=True,
)
self.requires(
"miniaudio/0.11.22",
transitive_headers=True,
transitive_libs=True,
)
def generate(self):
options = self.options
deps = conan.tools.cmake.CMakeDeps(self)
deps.generate()
toolchain = conan.tools.cmake.CMakeToolchain(self)
if hasattr(toolchain, "user_presets_path"):
toolchain.user_presets_path = False
toolchain.variables["BUILD_SHARED_LIBS"] = bool(options.shared)
toolchain.variables["BUILD_TESTING"] = bool(options.with_tests)
toolchain.generate()
def build(self):
options = self.options
cmake = conan.tools.cmake.CMake(self)
cmake.configure()
cmake.build()
if options.with_tests and conan.tools.build.can_run(self):
cmake.test(target="check")
def package_id(self):
self.info.options.rm_safe("with_tests")
def package(self):
cmake = conan.tools.cmake.CMake(self)
cmake.install()
package = pathlib.Path(self.package_folder)
conan.tools.files.copy(self, "*.py", src=pathlib.Path(self.recipe_folder) / "modulus", dst=package / "modulus", keep_path=True)
conan.tools.files.copy(self, "*", src=pathlib.Path(self.source_folder) / "tools", dst=package / "tools", keep_path=True)
def package_info(self):
cpp_info = self.cpp_info
cpp_info.set_property("cmake_file_name", "modulus")
cpp_info.set_property("cmake_target_name", "modulus::all")
cpp_info.set_property(
"cmake_build_modules",
[os.path.join("lib", "cmake", "modulus", "modulus_third_party.cmake")],
)
components = cpp_info.components
components["modulus"].set_property("cmake_target_name", "modulus::modulus")
components["modulus"].libs = ["modulus"]
components["modulus"].requires = ["boost::json"]
components["boost_json"].set_property("cmake_target_name", "modulus::boost_json")
components["boost_json"].requires = ["boost::json"]
components["openssl"].set_property("cmake_target_name", "modulus::openssl")
components["openssl"].requires = ["openssl::ssl", "openssl::crypto"]
components["sqlite"].set_property("cmake_target_name", "modulus::sqlite")
components["sqlite"].requires = ["sqlite3::sqlite"]
components["raylib"].set_property("cmake_target_name", "modulus::raylib")
components["raylib"].requires = ["raylib::raylib"]
if self.settings.os == "Linux":
components["raylib"].system_libs = [
"EGL",
"GLESv2",
"drm",
"gbm",
"atomic",
"dl",
]
components["miniaudio"].set_property("cmake_target_name", "modulus::miniaudio")
components["miniaudio"].requires = ["miniaudio::miniaudio"]
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <modulus/config.hpp>
#include <modulus/error.hpp>
#include <modulus/json_config.hpp>
#include <modulus/log.hpp>
#include <modulus/result.hpp>
#include <modulus/source_location.hpp>
#include <modulus/types.hpp>
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <expected>
#include <string>
#include <boost/json/value.hpp>
#include <modulus/error.hpp>
#include <modulus/log.hpp>
namespace modulus {
struct logging_config {
modulus::log_level level{modulus::log_level::INFO};
};
struct config {
modulus::logging_config logging{};
modulus::error_config errors{};
};
auto parse_logging_config(boost::json::value const& value) -> std::expected<modulus::logging_config, modulus::error>;
auto parse_error_config(boost::json::value const& value) -> std::expected<modulus::error_config, modulus::error>;
auto parse_config(boost::json::value const& value) -> std::expected<modulus::config, modulus::error>;
auto load_config(std::string const& path) -> std::expected<modulus::config, modulus::error>;
auto to_json(modulus::logging_config const& config) -> boost::json::value;
auto to_json(modulus::error_config const& config) -> boost::json::value;
auto to_json(modulus::config const& config) -> boost::json::value;
auto configure_logger(modulus::logger& logger, modulus::logging_config const& config) -> void;
auto configure(modulus::config const& config) -> void;
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
#include <modulus/source_location.hpp>
#include <modulus/types.hpp>
namespace modulus {
struct error_config {
std::bool_t include_category{true};
std::bool_t include_code{true};
std::bool_t include_message{true};
std::bool_t include_location{true};
};
class error {
public:
error() = default;
error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION()));
auto category() const noexcept -> std::string_view;
auto code() const noexcept -> std::int32_t;
auto message() const noexcept -> std::string_view;
auto location() const noexcept -> const modulus::source_location&;
auto describe(modulus::error_config const& config) const -> std::string;
auto describe() const -> std::string;
private:
std::string category_{"generic"};
std::int32_t code_{};
std::string message_{};
modulus::source_location location_{modulus::source_location::unknown()};
};
auto make_error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
auto set_default_error_config(modulus::error_config config) -> void;
auto default_error_config() -> modulus::error_config;
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <expected>
#include <initializer_list>
#include <limits>
#include <string>
#include <string_view>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus/error.hpp>
#include <modulus/types.hpp>
namespace modulus::json_config {
struct options {
std::string_view category{"modulus.config"};
std::int32_t invalid_argument_code{1};
std::int32_t not_found_code{2};
};
template <typename ENUM>
struct enum_mapping {
std::string_view name{};
ENUM value{};
};
auto invalid(modulus::json_config::options config, std::string message) -> modulus::error;
auto not_found(modulus::json_config::options config, std::string message) -> modulus::error;
auto load_value(std::string const& path, modulus::json_config::options config = {}) -> std::expected<boost::json::value, modulus::error>;
auto require_object(boost::json::value const& value, std::string_view context, modulus::json_config::options config = {}) -> std::expected<boost::json::object const*, modulus::error>;
auto reject_unknown_fields(boost::json::object const& object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config = {}) -> std::expected<void, modulus::error>;
auto optional_bool_field(boost::json::object const& object, std::string_view name, std::bool_t fallback, modulus::json_config::options config = {}) -> std::expected<std::bool_t, modulus::error>;
auto optional_string_field(boost::json::object const& object, std::string_view name, std::string fallback, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
auto required_string_field(boost::json::object const& object, std::string_view name, std::string_view context, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
auto optional_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
auto positive_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
auto optional_int32_field(boost::json::object const& object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config = {}) -> std::expected<std::int32_t, modulus::error>;
auto positive_milliseconds_field(boost::json::object const& object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value = static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), modulus::json_config::options config = {}) -> std::expected<std::chrono::milliseconds, modulus::error>;
template <typename ENUM>
auto parse_enum(std::string_view value, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
template <typename ENUM>
auto optional_enum_field(boost::json::object const& object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
}
template <typename ENUM>
auto modulus::json_config::parse_enum(std::string_view value, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config) -> std::expected<ENUM, modulus::error> {
for (auto mapping : mappings) {
if (value == mapping.name) {
return mapping.value;
}
}
return std::unexpected(modulus::json_config::invalid(config, std::string{message}));
}
template <typename ENUM>
auto modulus::json_config::optional_enum_field(boost::json::object const& object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config) -> std::expected<ENUM, modulus::error> {
auto found = object.find(name);
if (found == object.end()) {
return fallback;
}
if (!found->value().is_string()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be a string"));
}
return modulus::json_config::parse_enum(std::string_view{found->value().as_string()}, mappings, message, config);
}
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include <cassert>
#include <chrono>
#include <cstdint>
#include <functional>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <modulus/source_location.hpp>
#include <modulus/types.hpp>
namespace modulus {
enum class log_level : std::int32_t {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
CRITICAL,
OFF,
};
auto to_string(modulus::log_level level) noexcept -> std::string_view;
struct log_record {
modulus::log_level level{modulus::log_level::INFO};
std::string message{};
modulus::source_location location{modulus::source_location::unknown()};
std::chrono::system_clock::time_point timestamp{std::chrono::system_clock::now()};
std::thread::id thread_id{std::this_thread::get_id()};
};
class logger {
public:
logger();
auto set_level(modulus::log_level level) -> void;
auto level() const -> modulus::log_level;
auto enabled(modulus::log_level level) const -> std::bool_t;
auto set_sink(std::function<void(const modulus::log_record&)> sink) -> void;
auto write(modulus::log_record record) const -> void;
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) const -> void;
private:
mutable std::mutex mutex_;
mutable std::mutex sink_mutex_;
modulus::log_level level_{modulus::log_level::INFO};
std::function<void(const modulus::log_record&)> sink_{};
};
auto default_logger() -> modulus::logger&;
auto set_default_log_level(modulus::log_level level) -> void;
auto is_log_enabled(modulus::log_level level) -> std::bool_t;
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_trace(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_debug(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_info(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_warn(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_error(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
auto log_critical(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
template <typename MESSAGE>
auto log_lazy(modulus::log_level level, MESSAGE&& message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
}
template <typename MESSAGE>
auto modulus::log_lazy(modulus::log_level level, MESSAGE&& message, modulus::source_location location) -> void {
assert(static_cast<std::int32_t>(level) >= static_cast<std::int32_t>(modulus::log_level::TRACE));
assert(static_cast<std::int32_t>(level) <= static_cast<std::int32_t>(modulus::log_level::OFF));
assert(location.file_name() != nullptr);
assert(location.function_name() != nullptr);
if (modulus::is_log_enabled(level)) {
modulus::log(level, std::string(std::forward<MESSAGE>(message)()), location);
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <cassert>
#include <expected>
#include <type_traits>
#include <utility>
#include <modulus/error.hpp>
namespace modulus {
template <typename VALUE>
auto success(VALUE&& value) -> std::expected<std::decay_t<VALUE>, modulus::error>;
template <typename VALUE>
auto fail(modulus::error value) -> std::expected<VALUE, modulus::error>;
}
template <typename VALUE>
auto modulus::success(VALUE&& value) -> std::expected<std::decay_t<VALUE>, modulus::error> {
return std::forward<VALUE>(value);
}
template <typename VALUE>
auto modulus::fail(modulus::error value) -> std::expected<VALUE, modulus::error> {
assert(!value.category().empty());
return std::unexpected<modulus::error>(std::move(value));
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <modulus/types.hpp>
namespace modulus {
class source_location {
public:
constexpr source_location() noexcept = default;
constexpr source_location(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept;
static consteval auto current(std::char_t const* file = __builtin_FILE(), std::uint_least32_t line = __builtin_LINE(), std::char_t const* function = __builtin_FUNCTION()) noexcept -> modulus::source_location;
static constexpr auto unknown() noexcept -> modulus::source_location;
constexpr auto file_name() const noexcept -> std::char_t const*;
constexpr auto line() const noexcept -> std::uint_least32_t;
constexpr auto function_name() const noexcept -> std::char_t const*;
private:
std::char_t const* file_{"<unknown>"};
std::uint_least32_t line_{};
std::char_t const* function_{"<unknown>"};
};
}
constexpr modulus::source_location::source_location(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept : file_(file), line_(line), function_(function) {
assert(file != nullptr);
assert(function != nullptr);
}
consteval auto modulus::source_location::current(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept -> modulus::source_location {
assert(file != nullptr);
assert(function != nullptr);
return modulus::source_location(file, line, function);
}
constexpr auto modulus::source_location::unknown() noexcept -> modulus::source_location {
return modulus::source_location();
}
constexpr auto modulus::source_location::file_name() const noexcept -> std::char_t const* {
assert(file_ != nullptr);
return file_;
}
constexpr auto modulus::source_location::line() const noexcept -> std::uint_least32_t {
assert(file_ != nullptr);
return line_;
}
constexpr auto modulus::source_location::function_name() const noexcept -> std::char_t const* {
assert(function_ != nullptr);
return function_;
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <cstdint>
#if defined(__has_include)
# if __has_include(<stdfloat>)
# include <stdfloat>
# endif
#endif
namespace std {
using bool_t = decltype(true);
using char_t = decltype('x');
using uchar_t = std::uint8_t;
#if !defined(__STDCPP_FLOAT32_T__)
using float32_t = decltype(0.0F);
#endif
#if !defined(__STDCPP_FLOAT64_T__)
using float64_t = decltype(0.0);
#endif
}
+16
View File
@@ -0,0 +1,16 @@
{
"package": {
"name": "modulus"
},
"boost": {
"components": [
"json"
]
},
"tooling": {
"remote": {
"name": "khodo",
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+1077
View File
File diff suppressed because it is too large Load Diff
+639
View File
@@ -0,0 +1,639 @@
import copy
import json
import os
import re
import pathlib
class config_error(RuntimeError):
pass
boost_default_options = {
"boost/*:without_atomic": True,
"boost/*:without_charconv": True,
"boost/*:without_chrono": True,
"boost/*:without_cobalt": True,
"boost/*:without_container": True,
"boost/*:without_context": True,
"boost/*:without_contract": True,
"boost/*:without_coroutine": True,
"boost/*:without_date_time": True,
"boost/*:without_exception": True,
"boost/*:without_fiber": True,
"boost/*:without_filesystem": True,
"boost/*:without_graph": True,
"boost/*:without_graph_parallel": True,
"boost/*:without_iostreams": True,
"boost/*:without_json": True,
"boost/*:without_locale": True,
"boost/*:without_log": True,
"boost/*:without_math": True,
"boost/*:without_mpi": True,
"boost/*:without_nowide": True,
"boost/*:without_process": True,
"boost/*:without_program_options": True,
"boost/*:without_python": True,
"boost/*:without_random": True,
"boost/*:without_regex": True,
"boost/*:without_serialization": True,
"boost/*:without_stacktrace": True,
"boost/*:without_test": True,
"boost/*:without_thread": True,
"boost/*:without_timer": True,
"boost/*:without_type_erasure": True,
"boost/*:without_url": True,
"boost/*:without_wave": True,
}
boost_component_dependencies = {
"json": ["container"],
}
top_level_keys = {
"package",
"boost",
"tests",
"tooling",
}
package_keys = {
"name",
"artifact",
}
artifact_keys = {
"enabled",
"targets",
"install_prefix",
"cmake_definitions",
"conan_options",
"registry_url",
"upload_env",
}
boost_keys = {
"ref",
"components",
"default_options",
}
test_keys = {
"enabled",
"name",
}
tooling_keys = {
"default_target",
"targets",
"docker",
"upload_env",
"remote",
"run",
"lsp",
}
run_keys = {
"enabled",
"target",
"executable",
"env",
"args",
"ports",
}
lsp_keys = {
"enabled",
"compile_databases",
}
lsp_compile_database_keys = {
"name",
"source_dir",
"build_dir",
"generator",
"cmake_definitions",
"env",
"unset_env",
}
docker_keys = {
"image",
"marker",
}
remote_keys = {
"name",
"url",
}
target_keys = {
"profile",
"build_dir",
"can_run",
"build_type",
"cmake_definitions",
}
conan_name_re = re.compile(r"^[a-z0-9_][a-z0-9_+.-]*$")
conan_ref_re = re.compile(r"^[a-z0-9_][a-z0-9_+.-]*/[A-Za-z0-9_+.-]+(?:@[A-Za-z0-9_+.-]+/[A-Za-z0-9_+.-]+)?$")
cmake_name_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_+.-]*$")
path_re = re.compile(r"^[A-Za-z0-9_./+*?\[\]-]+$")
option_re = re.compile(r"^[A-Za-z0-9_+./*:-]+$")
docker_publish_re = re.compile(r"^[A-Za-z0-9_.:-]+(?:/(?:tcp|udp|sctp))?$")
def read_json(path):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as error:
raise config_error(f"missing Modulus config: {path}") from error
except json.JSONDecodeError as error:
raise config_error(f"invalid JSON in {path}: {error}") from error
if not isinstance(data, dict):
raise config_error(f"{path} must contain a JSON object")
return data
def config_path(recipe):
configured = getattr(recipe, "modulus_config", "modulus.json")
path = pathlib.Path(configured)
if path.is_absolute():
return path
recipe_folder = pathlib.Path(getattr(recipe, "recipe_folder", os.getcwd()))
return recipe_folder / path
def load_recipe_config(recipe):
path = config_path(recipe)
return with_profile_overrides(path.parent, normalize(read_json(path)))
def load_repo_config(repo_root):
repo_root = pathlib.Path(repo_root)
return with_profile_overrides(repo_root, normalize(read_json(repo_root / "modulus.json")))
def require_mapping(data, key):
value = data.get(key)
if not isinstance(value, dict):
raise config_error(f"modulus.json field '{key}' must be an object")
return value
def normalize(data):
data = copy.deepcopy(data)
validate_raw_config(data)
package = require_mapping(data, "package")
name = package.get("name")
if not isinstance(name, str) or not name:
raise config_error("modulus.json package.name must be a non-empty string")
package.setdefault(
"artifact",
default_artifact_config(name),
)
artifact = package["artifact"]
artifact.setdefault("enabled", False)
artifact.setdefault("targets", [])
artifact.setdefault("install_prefix", "")
artifact.setdefault("cmake_definitions", {})
artifact.setdefault("conan_options", {})
artifact.setdefault("registry_url", "https://git.thyme-labs.com/api/packages/khodo/generic")
artifact.setdefault("upload_env", f"{environment_prefix(name)}_PACKAGE_UPLOAD")
boost = data.setdefault(
"boost",
{},
)
if not isinstance(boost, dict):
raise config_error("modulus.json boost must be an object")
boost.setdefault("ref", "boost/1.91.0")
boost.setdefault(
"components",
[],
)
boost_options = dict(boost_default_options)
boost_options.update(boost.get("default_options", {}))
for component in boost.get("components", []):
boost_options[f"boost/*:without_{component}"] = False
for dependency in boost_component_dependencies.get(component, []):
boost_options[f"boost/*:without_{dependency}"] = False
boost["default_options"] = boost_options
tests = data.setdefault(
"tests",
{},
)
if not isinstance(tests, dict):
raise config_error("modulus.json tests must be an object")
tests.setdefault("enabled", True)
tests.setdefault("name", name)
tooling = data.setdefault(
"tooling",
{},
)
if not isinstance(tooling, dict):
raise config_error("modulus.json tooling must be an object")
tooling.setdefault("default_target", "x86_64_gcc12_debug")
tooling.setdefault("targets", default_targets())
tooling.setdefault(
"docker",
{},
)
tooling["docker"].setdefault("image", "modulus-build:dev")
tooling["docker"].setdefault("marker", "/opt/modulus-build-image")
tooling.setdefault("upload_env", f"{environment_prefix(name)}_CONAN_UPLOAD")
tooling.setdefault(
"remote",
{},
)
tooling["remote"].setdefault("name", "khodo")
tooling["remote"].setdefault("url", "https://git.thyme-labs.com/api/packages/khodo/conan")
tooling.setdefault(
"run",
default_run_config(name, tooling["default_target"]),
)
run = tooling["run"]
run.setdefault("enabled", False)
run.setdefault("target", tooling["default_target"])
run.setdefault("executable", name)
run.setdefault("env", {})
run.setdefault("args", [])
run.setdefault("ports", [])
tooling.setdefault(
"lsp",
{},
)
lsp = tooling["lsp"]
lsp.setdefault("enabled", False)
lsp.setdefault("compile_databases", [])
for database in lsp["compile_databases"]:
database.setdefault("source_dir", ".")
database.setdefault("generator", "Ninja")
database.setdefault("cmake_definitions", {})
database.setdefault("env", {})
database.setdefault("unset_env", [])
for target in tooling["targets"].values():
target.setdefault("cmake_definitions", {})
validate_config_references(data)
return data
def validate_raw_config(data):
validate_keys(data, top_level_keys, "modulus.json")
package = require_mapping(data, "package")
validate_keys(package, package_keys, "package")
validate_package(package)
if "boost" in data:
boost = require_mapping(data, "boost")
validate_keys(boost, boost_keys, "boost")
validate_boost(boost)
if "tests" in data:
tests = require_mapping(data, "tests")
validate_keys(tests, test_keys, "tests")
validate_tests(tests)
if "tooling" in data:
tooling = require_mapping(data, "tooling")
validate_keys(tooling, tooling_keys, "tooling")
validate_tooling(tooling)
def validate_keys(data, allowed, field):
unknown = sorted(set(data) - allowed)
if unknown:
raise config_error(f"modulus.json field '{field}' has unknown keys: {', '.join(unknown)}")
def validate_package(package):
name = package.get("name")
if not isinstance(name, str) or not conan_name_re.match(name):
raise config_error("modulus.json package.name must be a Conan package name")
artifact = package.get("artifact")
if artifact is not None:
if not isinstance(artifact, dict):
raise config_error("modulus.json package.artifact must be an object")
validate_keys(artifact, artifact_keys, "package.artifact")
validate_artifact(artifact)
def validate_artifact(artifact):
validate_bool(artifact.get("enabled"), "package.artifact.enabled", optional=True)
validate_component_list(
artifact.get(
"targets",
[],
),
"package.artifact.targets",
)
validate_package_relative_path(artifact.get("install_prefix"), "package.artifact.install_prefix", optional=True)
validate_options(
artifact.get(
"cmake_definitions",
{},
),
"package.artifact.cmake_definitions",
)
validate_options(
artifact.get(
"conan_options",
{},
),
"package.artifact.conan_options",
)
validate_string(artifact.get("registry_url"), "package.artifact.registry_url", optional=True)
validate_env_name(artifact.get("upload_env"), "package.artifact.upload_env", optional=True)
def validate_boost(boost):
validate_conan_ref(boost.get("ref"), "boost.ref", optional=True)
validate_component_list(
boost.get(
"components",
[],
),
"boost.components",
)
validate_options(
boost.get(
"default_options",
{},
),
"boost.default_options",
)
def validate_tests(tests):
validate_bool(tests.get("enabled"), "tests.enabled", optional=True)
validate_cmake_name(tests.get("name"), "tests.name", optional=True)
def validate_tooling(tooling):
validate_cmake_name(tooling.get("default_target"), "tooling.default_target", optional=True)
validate_env_name(tooling.get("upload_env"), "tooling.upload_env", optional=True)
run = tooling.get("run")
if run is not None:
if not isinstance(run, dict):
raise config_error("modulus.json tooling.run must be an object")
validate_keys(run, run_keys, "tooling.run")
validate_run(run)
lsp = tooling.get("lsp")
if lsp is not None:
if not isinstance(lsp, dict):
raise config_error("modulus.json tooling.lsp must be an object")
validate_keys(lsp, lsp_keys, "tooling.lsp")
validate_lsp(lsp)
docker = tooling.get("docker")
if docker is not None:
if not isinstance(docker, dict):
raise config_error("modulus.json tooling.docker must be an object")
validate_keys(docker, docker_keys, "tooling.docker")
validate_string(docker.get("image"), "tooling.docker.image", optional=True)
validate_absolute_path(docker.get("marker"), "tooling.docker.marker", optional=True)
remote = tooling.get("remote")
if remote is not None:
if not isinstance(remote, dict):
raise config_error("modulus.json tooling.remote must be an object")
validate_keys(remote, remote_keys, "tooling.remote")
for key in ("name", "url"):
validate_string(remote.get(key), f"tooling.remote.{key}", optional=True)
targets = tooling.get("targets")
if targets is not None:
if not isinstance(targets, dict):
raise config_error("modulus.json tooling.targets must be an object")
for name, target in targets.items():
validate_cmake_name(name, f"tooling.targets.{name}")
if not isinstance(target, dict):
raise config_error(f"modulus.json tooling.targets.{name} must be an object")
validate_keys(target, target_keys, f"tooling.targets.{name}")
validate_path(target.get("profile"), f"tooling.targets.{name}.profile")
validate_path(target.get("build_dir"), f"tooling.targets.{name}.build_dir")
validate_bool(target.get("can_run"), f"tooling.targets.{name}.can_run", optional=True)
build_type = target.get("build_type")
if build_type is not None and build_type not in {"Debug", "Release", "RelWithDebInfo", "MinSizeRel"}:
raise config_error(f"modulus.json tooling.targets.{name}.build_type must be a CMake build type")
validate_options(
target.get(
"cmake_definitions",
{},
),
f"tooling.targets.{name}.cmake_definitions",
)
def validate_run(run):
validate_bool(run.get("enabled"), "tooling.run.enabled", optional=True)
validate_cmake_name(run.get("target"), "tooling.run.target", optional=True)
validate_path(run.get("executable"), "tooling.run.executable", optional=True)
validate_string_map(
run.get(
"env",
{},
),
"tooling.run.env",
env_keys=True,
)
validate_string_list(
run.get(
"args",
[],
),
"tooling.run.args",
)
validate_docker_publish_list(
run.get(
"ports",
[],
),
"tooling.run.ports",
)
def validate_lsp(lsp):
validate_bool(lsp.get("enabled"), "tooling.lsp.enabled", optional=True)
databases = lsp.get("compile_databases", [])
if not isinstance(databases, list):
raise config_error("modulus.json tooling.lsp.compile_databases must be a list")
for index, database in enumerate(databases):
field = f"tooling.lsp.compile_databases[{index}]"
if not isinstance(database, dict):
raise config_error(f"modulus.json {field} must be an object")
validate_keys(database, lsp_compile_database_keys, field)
validate_cmake_name(database.get("name"), f"{field}.name")
validate_path(database.get("source_dir"), f"{field}.source_dir", optional=True)
validate_path(database.get("build_dir"), f"{field}.build_dir")
validate_string(database.get("generator"), f"{field}.generator", optional=True)
validate_options(
database.get(
"cmake_definitions",
{},
),
f"{field}.cmake_definitions",
)
validate_string_map(
database.get(
"env",
{},
),
f"{field}.env",
env_keys=True,
)
validate_string_list(
database.get(
"unset_env",
[],
),
f"{field}.unset_env",
)
for env_name in database.get("unset_env", []):
validate_env_name(env_name, f"{field}.unset_env")
def validate_docker_publish_list(value, field):
validate_string_list(value, field)
for index, item in enumerate(value):
if not docker_publish_re.match(item):
raise config_error(f"modulus.json {field}[{index}] must be a Docker publish mapping")
def validate_string(value, field, optional=False):
if value is None and optional:
return
if not isinstance(value, str) or not value:
raise config_error(f"modulus.json {field} must be a non-empty string")
if any(char in value for char in "\n\r\0"):
raise config_error(f"modulus.json {field} must not contain control characters")
def validate_bool(value, field, optional=False):
if value is None and optional:
return
if not isinstance(value, bool):
raise config_error(f"modulus.json {field} must be a boolean")
def validate_conan_ref(value, field, optional=False):
validate_string(value, field, optional=optional)
if value is not None and not conan_ref_re.match(value):
raise config_error(f"modulus.json {field} must be a Conan reference like name/version")
def validate_cmake_name(value, field, optional=False):
validate_string(value, field, optional=optional)
if value is not None and not cmake_name_re.match(value):
raise config_error(f"modulus.json {field} must be a valid CMake identifier")
def validate_env_name(value, field, optional=False):
validate_string(value, field, optional=optional)
if value is not None and not re.match(r"^[A-Z_][A-Z0-9_]*$", value):
raise config_error(f"modulus.json {field} must be an environment variable name")
def validate_path(value, field, optional=False):
validate_string(value, field, optional=optional)
if value is None:
return
path = pathlib.Path(value)
if path.is_absolute() or ".." in path.parts or not path_re.match(value):
raise config_error(f"modulus.json {field} must be a safe relative path")
def validate_package_relative_path(value, field, optional=False):
if value == "":
return
validate_path(value, field, optional=optional)
def validate_absolute_path(value, field, optional=False):
validate_string(value, field, optional=optional)
if value is not None and not str(value).startswith("/"):
raise config_error(f"modulus.json {field} must be an absolute path")
def validate_string_list(value, field, path=False):
if not isinstance(value, list):
raise config_error(f"modulus.json {field} must be a list")
for index, item in enumerate(value):
if path:
validate_path(item, f"{field}[{index}]")
else:
validate_string(item, f"{field}[{index}]")
def validate_string_map(value, field, env_keys=False):
if not isinstance(value, dict):
raise config_error(f"modulus.json {field} must be an object")
for key, item in value.items():
if env_keys:
validate_env_name(key, f"{field}.{key}")
else:
validate_string(key, f"{field}.{key}")
validate_string(item, f"{field}.{key}")
def validate_component_list(value, field):
validate_string_list(value, field)
for index, item in enumerate(value):
if not cmake_name_re.match(item):
raise config_error(f"modulus.json {field}[{index}] must be a component name")
def validate_options(value, field):
if not isinstance(value, dict):
raise config_error(f"modulus.json {field} must be an object")
for key, option in value.items():
if not isinstance(key, str) or not option_re.match(key):
raise config_error(f"modulus.json {field} has an invalid option name")
if not isinstance(option, (str, int, bool)):
raise config_error(f"modulus.json {field}.{key} must be a string, integer, or boolean")
def default_targets():
return {
"x86_64_gcc12_debug": target_config("x86_64_gcc12_debug", "Debug", "build/build_x86_debug", True),
"x86_64_gcc12_release": target_config("x86_64_gcc12_release", "Release", "build/build_x86_release", True),
"arm_64_gcc12_debug": target_config("arm_64_gcc12_debug", "Debug", "build/build_arm_debug", False),
"arm_64_gcc12_release": target_config("arm_64_gcc12_release", "Release", "build/build_arm_release", False),
}
def target_config(name, build_type, build_dir, can_run):
return {
"profile": f"build/build_tools/conan/profiles/{name}",
"build_dir": build_dir,
"can_run": can_run,
"build_type": build_type,
"cmake_definitions": {},
}
def default_artifact_config(name):
return {
"enabled": False,
"targets": [],
"install_prefix": "",
"cmake_definitions": {},
"conan_options": {},
"registry_url": "https://git.thyme-labs.com/api/packages/khodo/generic",
"upload_env": f"{environment_prefix(name)}_PACKAGE_UPLOAD",
}
def default_run_config(name, default_target):
return {
"enabled": False,
"target": default_target,
"executable": name,
"env": {},
"args": [],
"ports": [],
}
def default_lsp_config():
return {
"enabled": False,
"compile_databases": [],
}
def environment_prefix(name):
return re.sub(r"[^A-Z0-9]+", "_", name.upper()).strip("_")
def validate_config_references(data):
tooling = data["tooling"]
targets = tooling["targets"]
run = tooling["run"]
run_target = run.get("target")
if run_target not in targets:
raise config_error(f"modulus.json tooling.run.target must reference a configured target: {run_target}")
artifact = data["package"]["artifact"]
for target in artifact.get("targets", []):
if target not in targets:
raise config_error(f"modulus.json package.artifact.targets references an unknown target: {target}")
def with_profile_overrides(repo_root, config):
for name, target in config["tooling"]["targets"].items():
profile = local_profile(repo_root, name)
if profile is not None:
target["profile"] = str(profile)
return config
def local_profile(repo_root, target_name):
for path in (
pathlib.Path("conan") / "profiles" / target_name,
pathlib.Path("tools") / "conan" / "profiles" / target_name,
):
if (repo_root / path).is_file():
return path
return None
def boost_ref(config):
return config["boost"]["ref"]
def boost_requirement_options(config):
options = {}
for key, value in config["boost"]["default_options"].items():
option = requirement_option_name(key)
if option is not None:
options[option] = value
return options
def requirement_option_name(key):
if ":" not in key:
return key
pattern, option = key.split(":", 1)
if pattern in {"boost/*", "boost"}:
return option
return None
+88
View File
@@ -0,0 +1,88 @@
import pathlib
import shutil
import modulus.config
build_dir = pathlib.Path("build")
common_dir = build_dir / "build_common"
tools_dir = build_dir / "build_tools"
runtime_dir = common_dir / "modulus"
runtime_payload = ("modulus",)
runtime_ignore_patterns = (
".git",
".tmp",
"__pycache__",
"*.pyc",
"*.pyo",
"*.pyd",
".ninja_*",
"CMakeCache.txt",
"CMakeFiles",
"CMakePresets.json",
"build.ninja",
"cmake_install.cmake",
"conan_toolchain.cmake",
"conanbuild*.sh",
"conanbuildenv-*.sh",
"conandeps_legacy.cmake",
"conanrun*.sh",
"conanrunenv-*.sh",
"deactivate_conan*.sh",
"metadata",
)
editor_files = {
"clangd.yaml": ".clangd",
"clang-format": ".clang-format",
"pyrightconfig.json": "pyrightconfig.json",
}
def sync_project(repo_root, package_root):
repo_root = pathlib.Path(repo_root).resolve()
package_root = pathlib.Path(package_root).resolve()
modulus.config.load_repo_config(repo_root)
sync_runtime(repo_root, package_root)
sync_tools(repo_root, package_root)
sync_editor_files(repo_root, package_root)
def sync_runtime(repo_root, package_root):
dst = repo_root / runtime_dir
replace_directory(dst)
ignore = shutil.ignore_patterns(*runtime_ignore_patterns)
for name in runtime_payload:
src = package_root / name
if src.is_dir():
shutil.copytree(src, dst / name, ignore=ignore)
elif src.is_file():
shutil.copy2(src, dst / name)
def sync_tools(repo_root, package_root):
src = package_root / "tools"
dst = repo_root / tools_dir
if not src.is_dir():
raise RuntimeError(f"Modulus package is missing tools directory: {src}")
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.pyd"))
chmod_python_tools(dst)
def sync_editor_files(repo_root, package_root):
src = package_root / "tools" / "editor"
if not src.is_dir():
raise RuntimeError(f"Modulus package is missing editor templates: {src}")
for source_name, destination_name in editor_files.items():
source = src / source_name
if not source.is_file():
raise RuntimeError(f"Modulus package is missing editor template: {source}")
shutil.copy2(source, repo_root / destination_name)
def replace_directory(path):
if path.exists():
shutil.rmtree(path)
path.mkdir(parents=True)
def chmod_python_tools(root):
for path in root.rglob("*.py"):
path.chmod(path.stat().st_mode | 0o111)
+14
View File
@@ -0,0 +1,14 @@
{
"include": [
"modulus.py",
"package/conanfile.py",
"build/build_tools"
],
"extraPaths": [
"build/build_common/modulus",
"."
],
"typeCheckingMode": "off",
"reportMissingImports": false,
"reportMissingModuleSource": false
}
+311
View File
@@ -0,0 +1,311 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.thyme-labs.com/khodo/modulus/schemas/modulus.schema.json",
"type": "object",
"required": [
"package"
],
"additionalProperties": false,
"properties": {
"package": {
"$ref": "#/$defs/package"
},
"boost": {
"$ref": "#/$defs/boost"
},
"tests": {
"$ref": "#/$defs/tests"
},
"tooling": {
"$ref": "#/$defs/tooling"
}
},
"$defs": {
"package_name": {
"type": "string",
"pattern": "^[a-z0-9_][a-z0-9_+.-]*$"
},
"conan_ref": {
"type": "string",
"pattern": "^[a-z0-9_][a-z0-9_+.-]*/[A-Za-z0-9_+.-]+(?:@[A-Za-z0-9_+.-]+/[A-Za-z0-9_+.-]+)?$"
},
"cmake_name": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_+.-]*$"
},
"relative_path": {
"type": "string",
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9_./+*?\\[\\]-]+$"
},
"option_value": {
"type": [
"boolean",
"integer",
"string"
]
},
"options": {
"type": "object",
"propertyNames": {
"pattern": "^[A-Za-z0-9_+./*:-]+$"
},
"additionalProperties": {
"$ref": "#/$defs/option_value"
}
},
"package": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"$ref": "#/$defs/package_name"
},
"artifact": {
"$ref": "#/$defs/artifact"
}
}
},
"artifact": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"targets": {
"type": "array",
"items": {
"$ref": "#/$defs/cmake_name"
}
},
"install_prefix": {
"type": "string",
"pattern": "^(|(?!(?:/|.*(?:^|/)\\.\\.(?:/|$)))[A-Za-z0-9_./+*?\\[\\]-]+)$"
},
"cmake_definitions": {
"$ref": "#/$defs/options"
},
"conan_options": {
"$ref": "#/$defs/options"
},
"registry_url": {
"type": "string"
},
"upload_env": {
"type": "string",
"pattern": "^[A-Z_][A-Z0-9_]*$"
}
}
},
"boost": {
"type": "object",
"additionalProperties": false,
"properties": {
"ref": {
"$ref": "#/$defs/conan_ref"
},
"components": {
"type": "array",
"items": {
"$ref": "#/$defs/cmake_name"
}
},
"default_options": {
"$ref": "#/$defs/options"
}
}
},
"tests": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"name": {
"$ref": "#/$defs/cmake_name"
}
}
},
"tooling": {
"type": "object",
"additionalProperties": false,
"properties": {
"default_target": {
"$ref": "#/$defs/cmake_name"
},
"upload_env": {
"type": "string",
"pattern": "^[A-Z_][A-Z0-9_]*$"
},
"docker": {
"type": "object",
"additionalProperties": false,
"properties": {
"image": {
"type": "string"
},
"marker": {
"type": "string",
"pattern": "^/"
}
}
},
"remote": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"run": {
"$ref": "#/$defs/run"
},
"lsp": {
"$ref": "#/$defs/lsp"
},
"targets": {
"type": "object",
"propertyNames": {
"$ref": "#/$defs/cmake_name"
},
"additionalProperties": {
"$ref": "#/$defs/target"
}
}
}
},
"lsp": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"compile_databases": {
"type": "array",
"items": {
"$ref": "#/$defs/lsp_compile_database"
}
}
}
},
"lsp_compile_database": {
"type": "object",
"required": [
"name",
"build_dir"
],
"additionalProperties": false,
"properties": {
"name": {
"$ref": "#/$defs/cmake_name"
},
"source_dir": {
"$ref": "#/$defs/relative_path"
},
"build_dir": {
"$ref": "#/$defs/relative_path"
},
"generator": {
"type": "string"
},
"cmake_definitions": {
"$ref": "#/$defs/options"
},
"env": {
"type": "object",
"propertyNames": {
"pattern": "^[A-Z_][A-Z0-9_]*$"
},
"additionalProperties": {
"type": "string"
}
},
"unset_env": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Z_][A-Z0-9_]*$"
}
}
}
},
"run": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"target": {
"$ref": "#/$defs/cmake_name"
},
"executable": {
"$ref": "#/$defs/relative_path"
},
"env": {
"type": "object",
"propertyNames": {
"pattern": "^[A-Z_][A-Z0-9_]*$"
},
"additionalProperties": {
"type": "string"
}
},
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"ports": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9_.:-]+(?:/(?:tcp|udp|sctp))?$"
}
}
}
},
"target": {
"type": "object",
"required": [
"profile",
"build_dir"
],
"additionalProperties": false,
"properties": {
"profile": {
"$ref": "#/$defs/relative_path"
},
"build_dir": {
"$ref": "#/$defs/relative_path"
},
"can_run": {
"type": "boolean"
},
"build_type": {
"type": "string",
"enum": [
"Debug",
"Release",
"RelWithDebInfo",
"MinSizeRel"
]
},
"cmake_definitions": {
"$ref": "#/$defs/options"
}
}
}
}
}
+234
View File
@@ -0,0 +1,234 @@
#include <modulus/config.hpp>
#include <cassert>
#include <expected>
#include <initializer_list>
#include <string>
#include <string_view>
#include <utility>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus/error.hpp>
#include <modulus/json_config.hpp>
#include <modulus/log.hpp>
#include <modulus/result.hpp>
#include <modulus/source_location.hpp>
#include <cstdint>
namespace {
auto config_error(std::string message) -> modulus::error {
assert(!message.empty());
return modulus::json_config::invalid({}, std::move(message));
}
auto parse_log_level(std::string_view value) -> std::expected<modulus::log_level, modulus::error> {
assert(value.data() != nullptr);
if (value == "trace") {
return modulus::success(modulus::log_level::TRACE);
}
if (value == "debug") {
return modulus::success(modulus::log_level::DEBUG);
}
if (value == "info") {
return modulus::success(modulus::log_level::INFO);
}
if (value == "warn") {
return modulus::success(modulus::log_level::WARN);
}
if (value == "error") {
return modulus::success(modulus::log_level::ERROR);
}
if (value == "critical") {
return modulus::success(modulus::log_level::CRITICAL);
}
if (value == "off") {
return modulus::success(modulus::log_level::OFF);
}
return modulus::fail<modulus::log_level>(config_error("logging level must be trace, debug, info, warn, error, critical, or off"));
}
auto log_level_name(modulus::log_level level) -> std::string_view {
assert(static_cast<std::int32_t>(level) >= static_cast<std::int32_t>(modulus::log_level::TRACE));
assert(static_cast<std::int32_t>(level) <= static_cast<std::int32_t>(modulus::log_level::OFF));
switch (level) {
case modulus::log_level::TRACE:
return "trace";
case modulus::log_level::DEBUG:
return "debug";
case modulus::log_level::INFO:
return "info";
case modulus::log_level::WARN:
return "warn";
case modulus::log_level::ERROR:
return "error";
case modulus::log_level::CRITICAL:
return "critical";
case modulus::log_level::OFF:
return "off";
}
return "info";
}
}
auto modulus::parse_logging_config(boost::json::value const& value) -> std::expected<modulus::logging_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "logging config");
if (!object) {
return modulus::fail<modulus::logging_config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {"level"}, "logging config");
if (!unknown) {
return modulus::fail<modulus::logging_config>(unknown.error());
}
auto config = modulus::logging_config{};
auto found = (*object)->find("level");
if (found == (*object)->end()) {
return modulus::success(config);
}
if (!found->value().is_string()) {
return modulus::fail<modulus::logging_config>(config_error("logging level must be a string"));
}
auto level = parse_log_level(std::string_view{found->value().as_string()});
if (!level) {
return modulus::fail<modulus::logging_config>(level.error());
}
config.level = *level;
return modulus::success(config);
}
auto modulus::parse_error_config(boost::json::value const& value) -> std::expected<modulus::error_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "error config");
if (!object) {
return modulus::fail<modulus::error_config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"include_category",
"include_code",
"include_message",
"include_location",
}, "error config");
if (!unknown) {
return modulus::fail<modulus::error_config>(unknown.error());
}
auto config = modulus::error_config{};
auto include_category = modulus::json_config::optional_bool_field(**object, "include_category", config.include_category);
if (!include_category) {
return modulus::fail<modulus::error_config>(include_category.error());
}
auto include_code = modulus::json_config::optional_bool_field(**object, "include_code", config.include_code);
if (!include_code) {
return modulus::fail<modulus::error_config>(include_code.error());
}
auto include_message = modulus::json_config::optional_bool_field(**object, "include_message", config.include_message);
if (!include_message) {
return modulus::fail<modulus::error_config>(include_message.error());
}
auto include_location = modulus::json_config::optional_bool_field(**object, "include_location", config.include_location);
if (!include_location) {
return modulus::fail<modulus::error_config>(include_location.error());
}
config.include_category = *include_category;
config.include_code = *include_code;
config.include_message = *include_message;
config.include_location = *include_location;
return modulus::success(config);
}
auto modulus::parse_config(boost::json::value const& value) -> std::expected<modulus::config, modulus::error> {
auto object = modulus::json_config::require_object(value, "config root");
if (!object) {
return modulus::fail<modulus::config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"logging",
"errors",
}, "config root");
if (!unknown) {
return modulus::fail<modulus::config>(unknown.error());
}
auto config = modulus::config{};
if (auto found = (*object)->find("logging"); found != (*object)->end()) {
auto logging = modulus::parse_logging_config(found->value());
if (!logging) {
return modulus::fail<modulus::config>(logging.error());
}
config.logging = *logging;
}
if (auto found = (*object)->find("errors"); found != (*object)->end()) {
auto errors = modulus::parse_error_config(found->value());
if (!errors) {
return modulus::fail<modulus::config>(errors.error());
}
config.errors = *errors;
}
return modulus::success(config);
}
auto modulus::load_config(std::string const& path) -> std::expected<modulus::config, modulus::error> {
auto value = modulus::json_config::load_value(path, {
.category = "modulus.config",
.invalid_argument_code = 1,
.not_found_code = 1,
});
if (!value) {
return modulus::fail<modulus::config>(value.error());
}
return modulus::parse_config(*value);
}
auto modulus::to_json(modulus::logging_config const& config) -> boost::json::value {
return boost::json::object{
{
"level",
std::string{log_level_name(config.level)},
},
};
}
auto modulus::to_json(modulus::error_config const& config) -> boost::json::value {
return boost::json::object{
{
"include_category",
config.include_category,
},
{
"include_code",
config.include_code,
},
{
"include_message",
config.include_message,
},
{
"include_location",
config.include_location,
},
};
}
auto modulus::to_json(modulus::config const& config) -> boost::json::value {
return boost::json::object{
{
"logging",
modulus::to_json(config.logging),
},
{
"errors",
modulus::to_json(config.errors),
},
};
}
auto modulus::configure_logger(modulus::logger& logger, modulus::logging_config const& config) -> void {
logger.set_level(config.level);
modulus::log_debug("modulus logger configured");
}
auto modulus::configure(modulus::config const& config) -> void {
modulus::configure_logger(modulus::default_logger(), config.logging);
modulus::set_default_error_config(config.errors);
modulus::log_debug("modulus configuration applied");
}
+95
View File
@@ -0,0 +1,95 @@
#include <modulus/error.hpp>
#include <cassert>
#include <mutex>
#include <sstream>
#include <utility>
#include <cstdint>
namespace {
auto config_mutex() -> std::mutex& {
static auto mutex = std::mutex{};
return mutex;
}
auto config_storage() -> modulus::error_config& {
static auto config = modulus::error_config{};
return config;
}
}
modulus::error::error(std::string category, std::int32_t code, std::string message, modulus::source_location location) : category_(std::move(category)), code_(code), message_(std::move(message)), location_(location) {
assert(!category_.empty());
assert(location_.file_name() != nullptr);
assert(location_.function_name() != nullptr);
}
auto modulus::error::category() const noexcept -> std::string_view {
assert(!category_.empty());
return category_;
}
auto modulus::error::code() const noexcept -> std::int32_t {
assert(!category_.empty());
return code_;
}
auto modulus::error::message() const noexcept -> std::string_view {
assert(message_.data() != nullptr);
return message_;
}
auto modulus::error::location() const noexcept -> const modulus::source_location& {
assert(location_.file_name() != nullptr);
return location_;
}
auto modulus::error::describe(modulus::error_config const& config) const -> std::string {
assert(!category_.empty());
assert(location_.file_name() != nullptr);
auto out = std::ostringstream{};
if (config.include_category) {
out << category_;
}
if (config.include_code) {
if (config.include_category) {
out << ':';
}
out << code_;
}
if (config.include_message) {
if (config.include_category || config.include_code) {
out << ": ";
}
out << message_;
}
if (config.include_location) {
if (config.include_category || config.include_code || config.include_message) {
out << ' ';
}
out << '(' << location_.file_name() << ':' << location_.line() << ')';
}
return out.str();
}
auto modulus::error::describe() const -> std::string {
return describe(modulus::default_error_config());
}
auto modulus::make_error(std::string category, std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
assert(!category.empty());
assert(location.file_name() != nullptr);
return modulus::error(std::move(category), code, std::move(message), location);
}
auto modulus::set_default_error_config(modulus::error_config config) -> void {
auto lock = std::lock_guard{config_mutex()};
config_storage() = config;
}
auto modulus::default_error_config() -> modulus::error_config {
auto lock = std::lock_guard{config_mutex()};
return config_storage();
}
+191
View File
@@ -0,0 +1,191 @@
#include <modulus/json_config.hpp>
#include <cassert>
#include <cstdint>
#include <fstream>
#include <initializer_list>
#include <limits>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus/error.hpp>
#include <modulus/types.hpp>
namespace {
auto make_config_error(modulus::json_config::options config, std::int32_t code, std::string message) -> modulus::error {
assert(!config.category.empty());
assert(!message.empty());
return modulus::make_error(std::string{config.category}, code, std::move(message), modulus::source_location::unknown());
}
auto key_allowed(std::string_view key, std::initializer_list<std::string_view> allowed) -> std::bool_t {
assert(key.data() != nullptr);
for (auto candidate : allowed) {
if (key == candidate) {
return true;
}
}
return false;
}
auto integer_value(boost::json::value const& value, std::string_view name, modulus::json_config::options config) -> std::expected<std::int64_t, modulus::error> {
if (!value.is_number() || value.is_double()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be an integer"));
}
if (value.is_uint64()) {
if (value.as_uint64() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " is out of range"));
}
return static_cast<std::int64_t>(value.as_uint64());
}
return value.as_int64();
}
}
auto modulus::json_config::invalid(modulus::json_config::options config, std::string message) -> modulus::error {
return make_config_error(config, config.invalid_argument_code, std::move(message));
}
auto modulus::json_config::not_found(modulus::json_config::options config, std::string message) -> modulus::error {
return make_config_error(config, config.not_found_code, std::move(message));
}
auto modulus::json_config::load_value(std::string const& path, modulus::json_config::options config) -> std::expected<boost::json::value, modulus::error> {
if (path.empty()) {
return std::unexpected(modulus::json_config::invalid(config, "config path must not be empty"));
}
auto input = std::ifstream{path};
if (!input) {
return std::unexpected(modulus::json_config::not_found(config, "config file not found"));
}
auto buffer = std::ostringstream{};
buffer << input.rdbuf();
auto parse_error = boost::system::error_code{};
auto value = boost::json::parse(buffer.str(), parse_error);
if (parse_error) {
return std::unexpected(modulus::json_config::invalid(config, parse_error.message()));
}
return value;
}
auto modulus::json_config::require_object(boost::json::value const& value, std::string_view context, modulus::json_config::options config) -> std::expected<boost::json::object const*, modulus::error> {
assert(context.data() != nullptr);
if (!value.is_object()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " must be an object"));
}
return &value.as_object();
}
auto modulus::json_config::reject_unknown_fields(boost::json::object const& object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config) -> std::expected<void, modulus::error> {
assert(context.data() != nullptr);
for (auto const& field : object) {
auto key = std::string_view{
field.key().data(),
field.key().size(),
};
if (!key_allowed(key, allowed)) {
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " has unknown key: " + std::string{key}));
}
}
return {};
}
auto modulus::json_config::optional_bool_field(boost::json::object const& object, std::string_view name, std::bool_t fallback, modulus::json_config::options config) -> std::expected<std::bool_t, modulus::error> {
assert(name.data() != nullptr);
auto found = object.find(name);
if (found == object.end()) {
return fallback;
}
if (!found->value().is_bool()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be a boolean"));
}
return found->value().as_bool();
}
auto modulus::json_config::optional_string_field(boost::json::object const& object, std::string_view name, std::string fallback, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
assert(name.data() != nullptr);
auto found = object.find(name);
if (found == object.end()) {
return std::move(fallback);
}
if (!found->value().is_string()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be a string"));
}
return std::string{found->value().as_string()};
}
auto modulus::json_config::required_string_field(boost::json::object const& object, std::string_view name, std::string_view context, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
assert(name.data() != nullptr);
assert(context.data() != nullptr);
auto found = object.find(name);
if (found == object.end() || !found->value().is_string()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " " + std::string{name} + " is required"));
}
auto value = std::string{found->value().as_string()};
if (value.empty()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " " + std::string{name} + " must not be empty"));
}
return value;
}
auto modulus::json_config::optional_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
assert(name.data() != nullptr);
auto found = object.find(name);
if (found == object.end()) {
return fallback;
}
if (!found->value().is_number() || found->value().is_double()) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be an integer"));
}
if (found->value().is_int64() && found->value().as_int64() < 0) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must not be negative"));
}
auto number = found->value().is_uint64() ? found->value().as_uint64() : static_cast<std::uint64_t>(found->value().as_int64());
if (number > max_value) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " is out of range"));
}
return number;
}
auto modulus::json_config::positive_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
auto parsed = modulus::json_config::optional_uint64_field(object, name, fallback, max_value, config);
if (!parsed) {
return parsed;
}
if (*parsed == 0) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be positive"));
}
return parsed;
}
auto modulus::json_config::optional_int32_field(boost::json::object const& object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config) -> std::expected<std::int32_t, modulus::error> {
assert(name.data() != nullptr);
auto found = object.find(name);
if (found == object.end()) {
return fallback;
}
auto number = integer_value(found->value(), name, config);
if (!number) {
return std::unexpected(number.error());
}
if (*number < min_value || *number > max_value) {
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " is out of range"));
}
return static_cast<std::int32_t>(*number);
}
auto modulus::json_config::positive_milliseconds_field(boost::json::object const& object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::chrono::milliseconds, modulus::error> {
auto parsed = modulus::json_config::positive_uint64_field(object, name, static_cast<std::uint64_t>(fallback.count()), max_value, config);
if (!parsed) {
return std::unexpected(parsed.error());
}
return std::chrono::milliseconds{static_cast<std::int64_t>(*parsed)};
}
+149
View File
@@ -0,0 +1,149 @@
#include <modulus/log.hpp>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <utility>
#include <modulus/types.hpp>
namespace {
auto is_valid_log_level(modulus::log_level level) noexcept -> std::bool_t {
return static_cast<std::int32_t>(level) >= static_cast<std::int32_t>(modulus::log_level::TRACE) && static_cast<std::int32_t>(level) <= static_cast<std::int32_t>(modulus::log_level::OFF);
}
auto should_log(modulus::log_level record_level, modulus::log_level configured_level) noexcept -> std::bool_t {
assert(is_valid_log_level(record_level));
assert(is_valid_log_level(configured_level));
return configured_level != modulus::log_level::OFF && static_cast<std::int32_t>(record_level) >= static_cast<std::int32_t>(configured_level);
}
auto format_log_record(std::ostream& stream, const modulus::log_record& record) -> void {
assert(is_valid_log_level(record.level));
assert(record.location.file_name() != nullptr);
stream << '[' << modulus::to_string(record.level) << "] " << record.message << " (" << record.location.file_name() << ':' << record.location.line() << ")\n";
}
auto stderr_sink(const modulus::log_record& record) -> void {
assert(is_valid_log_level(record.level));
assert(record.location.file_name() != nullptr);
static auto mutex = std::mutex{};
auto lock = std::lock_guard{mutex};
format_log_record(std::cerr, record);
}
}
auto modulus::to_string(modulus::log_level level) noexcept -> std::string_view {
assert(is_valid_log_level(level));
switch (level) {
case modulus::log_level::TRACE: return "trace";
case modulus::log_level::DEBUG: return "debug";
case modulus::log_level::INFO: return "info";
case modulus::log_level::WARN: return "warn";
case modulus::log_level::ERROR: return "error";
case modulus::log_level::CRITICAL: return "critical";
case modulus::log_level::OFF: return "off";
}
return "unknown";
}
modulus::logger::logger() : sink_(stderr_sink) {}
auto modulus::logger::set_level(modulus::log_level level) -> void {
assert(is_valid_log_level(level));
auto lock = std::lock_guard{mutex_};
level_ = level;
}
auto modulus::logger::level() const -> modulus::log_level {
auto lock = std::lock_guard{mutex_};
assert(is_valid_log_level(level_));
return level_;
}
auto modulus::logger::enabled(modulus::log_level level) const -> std::bool_t {
assert(is_valid_log_level(level));
auto lock = std::lock_guard{mutex_};
return should_log(level, level_) && static_cast<std::bool_t>(sink_);
}
auto modulus::logger::set_sink(std::function<void(const modulus::log_record&)> sink) -> void {
auto lock = std::lock_guard{mutex_};
sink_ = std::move(sink);
}
auto modulus::logger::write(modulus::log_record record) const -> void {
assert(is_valid_log_level(record.level));
assert(record.location.file_name() != nullptr);
assert(record.location.function_name() != nullptr);
auto sink = std::function<void(const modulus::log_record&)>{};
{
auto lock = std::lock_guard{mutex_};
if (!should_log(record.level, level_) || !sink_) {
return;
}
sink = sink_;
}
assert(static_cast<std::bool_t>(sink));
auto sink_lock = std::lock_guard(sink_mutex_);
sink(record);
}
auto modulus::logger::log(modulus::log_level level, std::string message, modulus::source_location location) const -> void {
assert(is_valid_log_level(level));
assert(location.file_name() != nullptr);
assert(location.function_name() != nullptr);
write(modulus::log_record{
.level = level,
.message = std::move(message),
.location = location,
});
}
auto modulus::default_logger() -> modulus::logger& {
static auto instance = modulus::logger{};
return instance;
}
auto modulus::set_default_log_level(modulus::log_level level) -> void {
assert(is_valid_log_level(level));
modulus::default_logger().set_level(level);
}
auto modulus::is_log_enabled(modulus::log_level level) -> std::bool_t {
assert(is_valid_log_level(level));
return modulus::default_logger().enabled(level);
}
auto modulus::log(modulus::log_level level, std::string message, modulus::source_location location) -> void {
assert(is_valid_log_level(level));
assert(location.file_name() != nullptr);
assert(location.function_name() != nullptr);
modulus::default_logger().log(level, std::move(message), location);
}
auto modulus::log_trace(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::TRACE, std::move(message), location);
}
auto modulus::log_debug(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::DEBUG, std::move(message), location);
}
auto modulus::log_info(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::INFO, std::move(message), location);
}
auto modulus::log_warn(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::WARN, std::move(message), location);
}
auto modulus::log_error(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::ERROR, std::move(message), location);
}
auto modulus::log_critical(std::string message, modulus::source_location location) -> void {
modulus::log(modulus::log_level::CRITICAL, std::move(message), location);
}
+26
View File
@@ -0,0 +1,26 @@
add_executable(modulus-test
test.cpp
)
target_include_directories(modulus-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(modulus-test PRIVATE
modulus::modulus
)
target_compile_features(modulus-test PRIVATE cxx_std_23)
target_compile_definitions(modulus-test PRIVATE
$<$<CONFIG:Debug>:MODULUS_EXPECT_DEBUG_ASSERTS>
$<$<CONFIG:Release>:MODULUS_EXPECT_NDEBUG>
$<$<CONFIG:RelWithDebInfo>:MODULUS_EXPECT_NDEBUG>
$<$<CONFIG:MinSizeRel>:MODULUS_EXPECT_NDEBUG>
)
add_test(NAME modulus COMMAND modulus-test)
set_tests_properties(modulus PROPERTIES TIMEOUT 20)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
-R ^modulus$
--output-on-failure
--timeout 20
USES_TERMINAL
)
+271
View File
@@ -0,0 +1,271 @@
#include "test.hpp"
#include <cstdint>
#include <expected>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <set>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <boost/json/parse.hpp>
#include <modulus.hpp>
namespace {
struct stderr_capture {
std::ostringstream stream{};
std::streambuf* previous{std::cerr.rdbuf(stream.rdbuf())};
~stderr_capture();
};
auto parse_positive(std::int32_t value) -> std::expected<std::int32_t, modulus::error> {
if (value <= std::int32_t{0}) {
return modulus::fail<std::int32_t>(modulus::make_error("test", std::int32_t{7}, "value must be positive"));
}
return modulus::success(value);
}
stderr_capture::~stderr_capture() {
std::cerr.rdbuf(previous);
}
}
BOOST_AUTO_TEST_CASE(modulus_result_carries_success_and_failure_error) {
auto ok = parse_positive(std::int32_t{42});
BOOST_REQUIRE(ok.has_value());
BOOST_TEST(*ok == std::int32_t{42});
auto failed = parse_positive(std::int32_t{0});
BOOST_REQUIRE(!failed.has_value());
BOOST_TEST(std::string{failed.error().category()} == "test");
BOOST_TEST(failed.error().code() == std::int32_t{7});
BOOST_TEST(failed.error().describe().find("value must be positive") != std::string::npos);
BOOST_TEST(std::string{failed.error().location().file_name()}.find("test.cpp") != std::string::npos);
}
BOOST_AUTO_TEST_CASE(modulus_asserts_follow_build_type) {
#if defined(MODULUS_EXPECT_NDEBUG) && !defined(NDEBUG)
BOOST_FAIL("release-style builds must define NDEBUG");
#endif
#if defined(MODULUS_EXPECT_DEBUG_ASSERTS) && defined(NDEBUG)
BOOST_FAIL("debug builds must not define NDEBUG");
#endif
BOOST_TEST(true);
}
BOOST_AUTO_TEST_CASE(modulus_types_and_source_locations_are_available) {
auto value32 = std::float32_t{1};
auto value64 = std::float64_t{2};
BOOST_TEST(static_cast<std::float64_t>(value32) < value64);
auto location = modulus::source_location::current();
BOOST_TEST(location.file_name() != nullptr);
BOOST_TEST(std::string{location.file_name()}.find("test.cpp") != std::string::npos);
}
BOOST_AUTO_TEST_CASE(modulus_logger_filters_and_reports_caller_location) {
auto records = std::vector<modulus::log_record>{};
auto logger = modulus::logger{};
logger.set_level(modulus::log_level::DEBUG);
logger.set_sink([&](const modulus::log_record& record) {
records.push_back(record);
});
logger.log(modulus::log_level::TRACE, "hidden");
logger.log(modulus::log_level::DEBUG, "visible");
BOOST_REQUIRE(records.size() == std::size_t{1});
BOOST_TEST(records.front().message == "visible");
BOOST_TEST(std::string{records.front().location.file_name()}.find("test.cpp") != std::string::npos);
}
BOOST_AUTO_TEST_CASE(modulus_logger_serializes_custom_sink_writes) {
auto records = std::vector<modulus::log_record>{};
auto logger = modulus::logger{};
logger.set_level(modulus::log_level::INFO);
logger.set_sink([&](const modulus::log_record& record) {
records.push_back(record);
});
auto threads = std::vector<std::thread>{};
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
threads.emplace_back([thread, &logger]() -> void {
for (auto message = std::int32_t{}; message < std::int32_t{50}; ++message) {
logger.log(modulus::log_level::INFO, "custom-log-" + std::to_string(thread) + "-" + std::to_string(message));
}
});
}
for (auto& thread : threads) {
thread.join();
}
BOOST_TEST(records.size() == std::size_t{200});
auto messages = std::set<std::string>{};
for (const auto& record : records) {
messages.insert(record.message);
}
BOOST_TEST(messages.size() == std::size_t{200});
}
BOOST_AUTO_TEST_CASE(modulus_logger_tolerates_concurrent_configuration_changes) {
auto records = std::vector<modulus::log_record>{};
auto logger = modulus::logger{};
logger.set_level(modulus::log_level::INFO);
logger.set_sink([&](const modulus::log_record& record) {
records.push_back(record);
});
auto sink = [&](const modulus::log_record& record) -> void {
records.push_back(record);
};
auto mutator = std::thread{[&]() -> void {
for (auto iteration = std::int32_t{}; iteration < std::int32_t{50}; ++iteration) {
logger.set_level(modulus::log_level::DEBUG);
logger.set_sink(sink);
logger.set_level(modulus::log_level::INFO);
}
}};
auto threads = std::vector<std::thread>{};
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
threads.emplace_back([thread, &logger]() -> void {
for (auto message = std::int32_t{}; message < std::int32_t{50}; ++message) {
logger.log(modulus::log_level::INFO, "mutation-log-" + std::to_string(thread) + "-" + std::to_string(message));
}
});
}
for (auto& thread : threads) {
thread.join();
}
mutator.join();
BOOST_TEST(records.size() == std::size_t{200});
}
BOOST_AUTO_TEST_CASE(modulus_lazy_logging_skips_disabled_messages) {
modulus::set_default_log_level(modulus::log_level::ERROR);
auto side_effects = std::int32_t{};
modulus::log_lazy(modulus::log_level::DEBUG, [&]() -> std::string {
return std::string{"hidden "} + std::to_string(++side_effects);
});
BOOST_TEST(side_effects == std::int32_t{0});
}
BOOST_AUTO_TEST_CASE(modulus_default_logger_serializes_stderr_lines) {
auto capture = stderr_capture{};
modulus::set_default_log_level(modulus::log_level::INFO);
auto threads = std::vector<std::thread>{};
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
threads.emplace_back([thread]() -> void {
for (auto message = std::int32_t{}; message < std::int32_t{20}; ++message) {
modulus::log_info("thread-log-" + std::to_string(thread) + "-" + std::to_string(message));
}
});
}
for (auto& thread : threads) {
thread.join();
}
auto captured = std::istringstream{capture.stream.str()};
auto messages = std::set<std::string>{};
auto line = std::string{};
while (std::getline(captured, line)) {
BOOST_TEST(line.starts_with("[info] thread-log-"));
auto end = line.find(" (");
BOOST_REQUIRE(end != std::string::npos);
messages.insert(line.substr(7, end - 7));
}
BOOST_TEST(messages.size() == std::size_t{80});
}
BOOST_AUTO_TEST_CASE(modulus_free_logging_reports_caller_location) {
auto capture = stderr_capture{};
modulus::set_default_log_level(modulus::log_level::INFO);
modulus::log_info("free log source");
modulus::log_lazy(modulus::log_level::INFO, []() -> std::string {
return "lazy log source";
});
auto output = capture.stream.str();
BOOST_TEST(output.find("free log source") != std::string::npos);
BOOST_TEST(output.find("lazy log source") != std::string::npos);
BOOST_TEST(output.find("test.cpp") != std::string::npos);
}
BOOST_AUTO_TEST_CASE(modulus_error_describe_honors_formatting_config) {
auto error = modulus::make_error("test", std::int32_t{7}, "value must be positive");
auto message_only = modulus::error_config{
.include_category = false,
.include_code = false,
.include_message = true,
.include_location = false,
};
BOOST_TEST(error.describe(message_only) == "value must be positive");
modulus::set_default_error_config(message_only);
BOOST_TEST(error.describe() == "value must be positive");
modulus::set_default_error_config(modulus::error_config{});
}
BOOST_AUTO_TEST_CASE(modulus_config_parses_defaults_and_logging_level) {
auto defaults = modulus::parse_config(boost::json::parse("{}"));
BOOST_REQUIRE(defaults.has_value());
BOOST_TEST(static_cast<std::int32_t>(defaults->logging.level) == static_cast<std::int32_t>(modulus::log_level::INFO));
auto parsed = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"debug"}})json"));
BOOST_REQUIRE(parsed.has_value());
BOOST_TEST(static_cast<std::int32_t>(parsed->logging.level) == static_cast<std::int32_t>(modulus::log_level::DEBUG));
auto round_trip = modulus::parse_config(modulus::to_json(*parsed));
BOOST_REQUIRE(round_trip.has_value());
BOOST_TEST(static_cast<std::int32_t>(round_trip->logging.level) == static_cast<std::int32_t>(modulus::log_level::DEBUG));
}
BOOST_AUTO_TEST_CASE(modulus_config_rejects_invalid_logging_values) {
auto bad_level = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"loud"}})json"));
BOOST_REQUIRE(!bad_level.has_value());
BOOST_TEST(std::string{bad_level.error().category()} == "modulus.config");
auto bad_shape = modulus::parse_config(boost::json::parse(R"json({"logging":[]})json"));
BOOST_REQUIRE(!bad_shape.has_value());
BOOST_TEST(bad_shape.error().describe().find("logging config must be an object") != std::string::npos);
auto unknown_root = modulus::parse_config(boost::json::parse(R"json({"loging":{"level":"debug"}})json"));
BOOST_REQUIRE(!unknown_root.has_value());
BOOST_TEST(unknown_root.error().describe().find("config root has unknown key: loging") != std::string::npos);
auto unknown_logging = modulus::parse_config(boost::json::parse(R"json({"logging":{"leve":"debug"}})json"));
BOOST_REQUIRE(!unknown_logging.has_value());
BOOST_TEST(unknown_logging.error().describe().find("logging config has unknown key: leve") != std::string::npos);
auto unknown_errors = modulus::parse_config(boost::json::parse(R"json({"errors":{"include_messages":false}})json"));
BOOST_REQUIRE(!unknown_errors.has_value());
BOOST_TEST(unknown_errors.error().describe().find("error config has unknown key: include_messages") != std::string::npos);
}
BOOST_AUTO_TEST_CASE(modulus_config_parses_error_formatting) {
auto parsed = modulus::parse_config(boost::json::parse(R"json({"errors":{"include_category":false,"include_code":false,"include_location":false}})json"));
BOOST_REQUIRE(parsed.has_value());
BOOST_TEST(!parsed->errors.include_category);
BOOST_TEST(!parsed->errors.include_code);
BOOST_TEST(parsed->errors.include_message);
BOOST_TEST(!parsed->errors.include_location);
}
BOOST_AUTO_TEST_CASE(modulus_configure_applies_default_logger_and_error_config) {
auto parsed = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"off"},"errors":{"include_category":false,"include_code":false,"include_location":false}})json"));
BOOST_REQUIRE(parsed.has_value());
modulus::configure(*parsed);
BOOST_TEST(!modulus::is_log_enabled(modulus::log_level::CRITICAL));
auto error = modulus::make_error("test", std::int32_t{1}, "configured");
BOOST_TEST(error.describe() == "configured");
modulus::set_default_log_level(modulus::log_level::INFO);
modulus::set_default_error_config(modulus::error_config{});
}
BOOST_AUTO_TEST_CASE(modulus_load_config_handles_files) {
auto path = std::filesystem::temp_directory_path() / "modulus-config-test.json";
auto output = std::ofstream{path};
output << R"json({"logging":{"level":"warn"}})json";
output.close();
auto loaded = modulus::load_config(path.string());
BOOST_REQUIRE(loaded.has_value());
BOOST_TEST(static_cast<std::int32_t>(loaded->logging.level) == static_cast<std::int32_t>(modulus::log_level::WARN));
auto missing = modulus::load_config((std::filesystem::temp_directory_path() / "modulus-config-missing.json").string());
BOOST_REQUIRE(!missing.has_value());
BOOST_TEST(missing.error().describe().find("config file not found") != std::string::npos);
auto invalid_path = std::filesystem::temp_directory_path() / "modulus-config-invalid.json";
auto invalid_output = std::ofstream{invalid_path};
invalid_output << "{";
invalid_output.close();
auto invalid = modulus::load_config(invalid_path.string());
BOOST_REQUIRE(!invalid.has_value());
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define BOOST_TEST_MODULE modulus
#include <boost/test/included/unit_test.hpp>
+961
View File
@@ -0,0 +1,961 @@
import os
import pathlib
import subprocess
import sys
import tarfile
import tempfile
import unittest
import unittest.mock
import modulus.cli
import modulus.config
class upload_packages_test(unittest.TestCase):
def context(self, repo_root=None, package_name="sample"):
config = {
"package": {
"name": package_name,
"artifact": {
"enabled": False,
"targets": [],
"install_prefix": "",
"cmake_definitions": {},
"conan_options": {},
"registry_url": "https://example.test/api/packages/acme/generic",
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
},
},
"tooling": {
"default_target": "native",
"targets": {
"native": {
"profile": "build/build_tools/conan/profiles/native",
"build_dir": "build/build_native",
"can_run": True,
"build_type": "Release",
},
},
"docker": {
"image": "sample-build:dev",
"marker": "/opt/sample-build-image",
},
"upload_env": "SAMPLE_CONAN_UPLOAD",
"remote": {
"name": "origin",
"url": "https://example.test/conan",
},
"run": {
"enabled": False,
"target": "native",
"executable": "sample",
"env": {},
"args": [],
"ports": [],
},
},
"tests": {
"name": "sample",
},
}
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), config)
def test_cli_import_does_not_import_conan_recipe_module(self):
repo = pathlib.Path(__file__).resolve().parents[1]
command = [
sys.executable,
"-c",
"import sys; import modulus.cli; raise SystemExit(1 if 'modulus.recipe' in sys.modules else 0)",
]
completed = subprocess.run(command, cwd=repo, check=False)
self.assertEqual(completed.returncode, 0)
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
def test_failed_upload_does_not_force_by_default(self, configure, upload):
context = self.context()
with unittest.mock.patch.dict(os.environ, {}, clear=True):
status = modulus.cli.upload_packages(context)
self.assertEqual(status, 37)
configure.assert_called_once_with(context, authenticate=True)
upload.assert_called_once_with(context, "origin", force=False)
@unittest.mock.patch.object(modulus.cli, "upload_all_packages")
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote")
def test_replace_requires_guard_env(self, configure, upload):
with unittest.mock.patch.dict(os.environ, {}, clear=True):
status = modulus.cli.upload_packages(self.context(), replace=True)
self.assertEqual(status, 1)
configure.assert_not_called()
upload.assert_not_called()
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
def test_replace_forces_upload_when_guarded(self, configure, upload):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"MODULUS_ALLOW_PACKAGE_REPLACE": "1",
},
clear=True,
):
status = modulus.cli.upload_packages(context, replace=True)
self.assertEqual(status, 0)
configure.assert_called_once_with(context, authenticate=True)
upload.assert_called_once_with(context, "origin", force=True)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_upload_defaults_to_latest_reference(self, run):
with unittest.mock.patch.dict(os.environ, {}, clear=True):
status = modulus.cli.upload_all_packages(self.context(), "origin")
self.assertEqual(status, 0)
self.assertIn("sample/latest:*", run.call_args.args[1])
self.assertNotIn("--force", run.call_args.args[1])
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_replace_upload_uses_conan_force(self, run):
status = modulus.cli.upload_all_packages(self.context(), "origin", force=True)
self.assertEqual(status, 0)
self.assertIn("--force", run.call_args.args[1])
def test_checked_in_ci_overwrites_latest_on_master(self):
workflow = (pathlib.Path(__file__).resolve().parents[1] / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8")
self.assertIn("gitea.ref == 'refs/heads/master'", workflow)
self.assertIn("workflow_dispatch", workflow)
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", workflow)
self.assertIn("--replace", workflow)
self.assertNotIn("--seed-dependencies", workflow)
self.assertNotIn("--no-remote", workflow)
self.assertNotIn("CONAN_PACKAGE_API_URL", workflow)
self.assertNotIn("VERSION_TAG", workflow)
self.assertIn("CONAN_HOME: build/build_common/conan_cache", workflow)
self.assertNotIn("./tools/bench.py", workflow)
self.assertNotIn("--examples", workflow)
self.assertNotIn("--volume \"${PWD}:${PWD}\"", workflow)
self.assertNotIn("CONAN_PUBLIC_REMOTE_URL", workflow)
self.assertNotIn("docs.py", workflow)
def test_removed_public_commands_are_rejected(self):
with tempfile.TemporaryDirectory() as repo_dir:
repo = pathlib.Path(repo_dir)
(repo / "modulus.json").write_text(
'{"package":{"name":"sample"}}',
encoding="utf-8",
)
previous_cwd = pathlib.Path.cwd()
os.chdir(repo)
try:
for command in ("image", "docs", "shell", "clangd", "docker_support"):
with self.assertRaisesRegex(SystemExit, "unknown modulus command"):
modulus.cli.main(command)
finally:
os.chdir(previous_cwd)
def test_docker_passthrough_includes_replace_env(self):
passthrough = modulus.cli.docker_passthrough_env(self.context())
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough)
self.assertIn("PACKAGE_REGISTRY_USER", passthrough)
self.assertIn("PACKAGE_REGISTRY_TOKEN", passthrough)
self.assertIn("SAMPLE_PACKAGE_UPLOAD", passthrough)
self.assertNotIn("CONAN_PACKAGE_VERSION", passthrough)
self.assertNotIn("MODULUS_EXPORT_RUNTIME", passthrough)
self.assertNotIn("GITHUB_TOKEN", passthrough)
self.assertNotIn("GITEA_TOKEN", passthrough)
def test_run_ports_default_to_empty_list(self):
config = modulus.config.normalize({
"package": {
"name": "sample",
},
})
self.assertEqual(config["tooling"]["run"]["ports"], [])
def test_run_accepts_docker_publish_mapping(self):
config = modulus.config.normalize({
"package": {
"name": "sample",
},
"tooling": {
"run": {
"ports": ["127.0.0.1:8000:8000/tcp"],
},
},
})
self.assertEqual(config["tooling"]["run"]["ports"], ["127.0.0.1:8000:8000/tcp"])
def test_run_rejects_invalid_docker_publish_mapping(self):
with self.assertRaisesRegex(modulus.config.config_error, "tooling.run.ports\\[0\\]"):
modulus.config.normalize({
"package": {
"name": "sample",
},
"tooling": {
"run": {
"ports": ["127.0.0.1:8000:8000/tcp --privileged"],
},
},
})
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_run_is_disabled_by_default(self, docker):
status = modulus.cli.product_run(self.context(), [])
self.assertEqual(status, 1)
docker.assert_not_called()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_run_executes_configured_product_executable(self, docker, run):
with tempfile.TemporaryDirectory() as repo_dir:
repo = pathlib.Path(repo_dir)
context = self.context(repo_root=repo)
context.run_settings.update({
"enabled": True,
"target": "native",
"executable": "sample-app",
"env": {
"SAMPLE_WEB_ROOT": "build/web",
},
"args": ["--serve"],
})
context.default_target.build_dir.mkdir(parents=True)
executable = context.default_target.build_dir / "sample-app"
executable.write_text("#!/bin/sh\n", encoding="utf-8")
status = modulus.cli.product_run(
context,
[
"--",
"--port",
"8000",
],
)
self.assertEqual(status, 0)
self.assertEqual(
run.call_args.args[1],
[
executable,
"--serve",
"--port",
"8000",
],
)
self.assertEqual(run.call_args.kwargs["env_overrides"], {"SAMPLE_WEB_ROOT": "build/web"})
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "docker_run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=False)
def test_run_publishes_configured_ports_when_wrapped_by_docker(self, docker, docker_run):
context = self.context()
context.run_settings.update({
"enabled": True,
"ports": ["127.0.0.1:8000:8000/tcp"],
})
status = modulus.cli.product_run(context, ["--target", "native"])
self.assertEqual(status, 0)
docker_run.assert_called_once_with(
context,
[
f"./{context.tools_rel}/run.py",
"--target",
"native",
],
docker_args=[
"--publish",
"127.0.0.1:8000:8000/tcp",
],
)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_build_image_honors_docker_platform_env(self, run):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"MODULUS_DOCKER_PLATFORM": "linux/arm64",
},
clear=True,
):
status = modulus.cli.build_image(context)
self.assertEqual(status, 0)
self.assertEqual(
run.call_args.args[1],
[
"docker",
"build",
"--platform",
"linux/arm64",
"--file",
context.dockerfile,
"--tag",
"sample-build:dev",
".",
],
)
@unittest.mock.patch.object(modulus.cli, "ensure_build_image", return_value=0)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_container", return_value=False)
def test_docker_run_honors_docker_platform_env(self, in_container, run, ensure_image):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"MODULUS_DOCKER_PLATFORM": "linux/arm64",
},
clear=True,
):
status = modulus.cli.docker_run(context, ["true"])
self.assertEqual(status, 0)
self.assertIn("--platform", run.call_args.args[1])
self.assertIn("linux/arm64", run.call_args.args[1])
ensure_image.assert_called_once_with(context)
in_container.assert_called_once()
def test_conan_remote_url_uses_config_by_default(self):
with unittest.mock.patch.dict(os.environ, {}, clear=True):
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
def test_conan_remote_url_uses_config_in_ci(self):
with unittest.mock.patch.dict(
os.environ,
{
"CI": "true",
},
clear=True,
):
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
def test_conan_credentials_only_uses_conan_environment(self):
with unittest.mock.patch.dict(
os.environ,
{
"CONAN_USER": "dev",
"CONAN_TOKEN": "secret",
"GITEA_TOKEN": "ignored",
},
clear=True,
):
user, token = modulus.cli.conan_credentials()
self.assertEqual(user, "dev")
self.assertEqual(token, "secret")
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_remote_skips_login_without_conan_credentials(self, run):
with unittest.mock.patch.dict(os.environ, {}, clear=True):
status = modulus.cli.configure_conan_remote(self.context())
self.assertEqual(status, 0)
self.assertEqual(len(run.call_args_list), 1)
self.assertEqual(
run.call_args_list[0].args[1],
[
"conan",
"remote",
"add",
"-f",
"origin",
"https://example.test/conan",
],
)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_remote_rejects_partial_conan_credentials(self, run):
with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True):
status = modulus.cli.configure_conan_remote(self.context())
self.assertEqual(status, 1)
run.assert_not_called()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_remote_skips_login_until_authentication_is_requested(self, run):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"CONAN_USER": "dev",
"CONAN_TOKEN": "secret",
},
clear=True,
):
status = modulus.cli.configure_conan_remote(context)
self.assertEqual(status, 0)
self.assertEqual(len(run.call_args_list), 1)
self.assertEqual(
run.call_args_list[0].args[1],
[
"conan",
"remote",
"add",
"-f",
"origin",
"https://example.test/conan",
],
)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_remote_logs_in_with_hidden_token_when_authenticating(self, run):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"CONAN_USER": "dev",
"CONAN_TOKEN": "secret",
},
clear=True,
):
status = modulus.cli.configure_conan_remote(context, authenticate=True)
self.assertEqual(status, 0)
self.assertEqual(
run.call_args_list[0].args[1],
[
"conan",
"remote",
"add",
"-f",
"origin",
"https://example.test/conan",
],
)
self.assertEqual(
run.call_args_list[1].args[1],
[
"conan",
"remote",
"login",
"origin",
"dev",
"-p",
"secret",
],
)
self.assertEqual(
run.call_args_list[1].kwargs["display_command"],
[
"conan",
"remote",
"login",
"origin",
"dev",
"-p",
"<hidden>",
],
)
def multi_target_context(self):
config = {
"package": {
"name": "sample",
"artifact": {
"enabled": False,
"targets": [],
"install_prefix": "",
"cmake_definitions": {},
"conan_options": {},
"registry_url": "https://example.test/api/packages/acme/generic",
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
},
},
"tooling": {
"default_target": "debug",
"targets": {
"debug": {
"profile": "build/build_tools/conan/profiles/debug",
"build_dir": "build/build_debug",
"can_run": True,
"build_type": "Debug",
},
"release": {
"profile": "build/build_tools/conan/profiles/release",
"build_dir": "build/build_release",
"can_run": True,
"build_type": "Release",
},
},
"docker": {
"image": "sample-build:dev",
"marker": "/opt/sample-build-image",
},
"upload_env": "SAMPLE_CONAN_UPLOAD",
"remote": {
"name": "origin",
"url": "https://example.test/conan",
},
"run": {
"enabled": False,
"target": "debug",
"executable": "sample",
"env": {},
"args": [],
},
},
"tests": {
"name": "sample",
},
}
return modulus.cli.build_context(pathlib.Path(os.getcwd()), config)
def test_selected_targets_default_to_default_target_only(self):
context = self.multi_target_context()
selected = context.selected_targets(
[],
False,
)
self.assertEqual([target.name for target in selected], ["debug"])
def test_selected_targets_all_returns_every_target(self):
context = self.multi_target_context()
selected = context.selected_targets(
[],
True,
)
self.assertEqual([target.name for target in selected], ["debug", "release"])
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_build_can_target_one_cmake_target(self, docker, run):
context = self.context()
context.default_target.build_dir.mkdir(parents=True, exist_ok=True)
(context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8")
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
status = modulus.cli.build(
context,
[
"--target",
"native",
"--cmake-target",
"sample-example",
],
)
self.assertEqual(status, 0)
self.assertEqual(
run.call_args.args[1],
[
"cmake",
"--build",
context.default_target.build_dir,
"--target",
"sample-example",
"--parallel",
"2",
],
)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_build_auto_configures_missing_target(self, docker, configure_remote, configure_one, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
status = modulus.cli.build(context, [])
self.assertEqual(status, 0)
configure_remote.assert_called_once_with(context)
configure_one.assert_called_once_with(context, context.default_target, update=False)
self.assertEqual(
run.call_args.args[1],
[
"cmake",
"--build",
context.default_target.build_dir,
"--parallel",
"2",
],
)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_build_skips_configure_when_target_is_configured(self, docker, configure_remote, configure_one, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
context.default_target.build_dir.mkdir(parents=True)
(context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8")
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
status = modulus.cli.build(context, [])
self.assertEqual(status, 0)
configure_remote.assert_not_called()
configure_one.assert_not_called()
self.assertEqual(
run.call_args.args[1],
[
"cmake",
"--build",
context.default_target.build_dir,
"--parallel",
"2",
],
)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_build_reconfigures_when_target_cmake_definition_differs(self, docker, configure_remote, configure_one, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
context.default_target.cmake_definitions = {
"SAMPLE_WITH_WEB": True,
}
context.default_target.build_dir.mkdir(parents=True)
(context.default_target.build_dir / "CMakeCache.txt").write_text("SAMPLE_WITH_WEB:BOOL=OFF\n", encoding="utf-8")
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
status = modulus.cli.build(context, [])
self.assertEqual(status, 0)
configure_remote.assert_called_once_with(context)
configure_one.assert_called_once_with(context, context.default_target, update=False)
self.assertEqual(
run.call_args.args[1],
[
"cmake",
"--build",
context.default_target.build_dir,
"--parallel",
"2",
],
)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=37)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_build_stops_when_auto_configure_fails(self, docker, configure_remote, configure_one, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
status = modulus.cli.build(context, [])
self.assertEqual(status, 37)
configure_remote.assert_called_once_with(context)
configure_one.assert_called_once_with(context, context.default_target, update=False)
run.assert_not_called()
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_configure_no_remote_skips_remote_setup(self, docker, configure_remote, configure_one):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
status = modulus.cli.configure(
context,
[
"--target",
"native",
"--no-remote",
],
)
self.assertEqual(status, 0)
configure_remote.assert_not_called()
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=True)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_package_no_remote_skips_remote_setup_and_upload(self, docker, configure_remote, create_package, upload):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"SAMPLE_CONAN_UPLOAD": "1",
},
clear=True,
):
status = modulus.cli.package(
context,
[
"--target",
"native",
"--no-remote",
],
)
self.assertEqual(status, 0)
configure_remote.assert_not_called()
create_package.assert_called_once_with(context, context.default_target, False, no_remote=True)
upload.assert_not_called()
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_package_configures_remote_once_before_upload(self, docker, configure_remote, create_package, upload):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"CONAN_USER": "dev",
"CONAN_TOKEN": "secret",
"SAMPLE_CONAN_UPLOAD": "1",
},
clear=True,
):
status = modulus.cli.package(
context,
[
"--target",
"native",
],
)
self.assertEqual(status, 0)
configure_remote.assert_called_once_with(context)
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
upload.assert_called_once_with(context, replace=False)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=1)
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_package_defers_conan_upload_credentials_until_after_create(self, docker, configure_remote, create_package, upload):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"SAMPLE_CONAN_UPLOAD": "1",
},
clear=True,
):
status = modulus.cli.package(
context,
[
"--target",
"native",
],
)
self.assertEqual(status, 1)
configure_remote.assert_called_once_with(context)
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
upload.assert_called_once_with(context, replace=False)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_package_rejects_partial_conan_credentials_before_create(self, docker, run, create_package):
context = self.context()
with unittest.mock.patch.dict(
os.environ,
{
"CONAN_USER": "dev",
"SAMPLE_CONAN_UPLOAD": "1",
},
clear=True,
):
status = modulus.cli.package(
context,
[
"--target",
"native",
],
)
self.assertEqual(status, 1)
create_package.assert_not_called()
run.assert_not_called()
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_one_passes_no_remote_to_conan_install(self, run, clear):
context = self.context()
status = modulus.cli.configure_one(context, context.default_target, update=False, no_remote=True)
self.assertEqual(status, 0)
conan_command = run.call_args_list[0].args[1]
self.assertIn("--no-remote", conan_command)
self.assertIn("--build=missing", conan_command)
self.assertNotIn("-s", conan_command)
self.assertNotIn("-c", conan_command)
self.assertNotIn("glfw/*:with_x11=False", conan_command)
self.assertIn(f"-DCMAKE_TOOLCHAIN_FILE={context.default_target.build_dir / 'conan' / 'conan_toolchain.cmake'}", run.call_args_list[1].args[1])
self.assertIn("-DCMAKE_BUILD_TYPE=Release", run.call_args_list[1].args[1])
clear.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_one_passes_target_cmake_definitions(self, run, clear):
context = self.context()
context.default_target.cmake_definitions = {
"SAMPLE_WITH_WEB": True,
"SAMPLE_MODE": "dev",
"SAMPLE_LIMIT": 2,
}
status = modulus.cli.configure_one(context, context.default_target, update=False)
self.assertEqual(status, 0)
cmake_command = run.call_args_list[1].args[1]
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
self.assertIn("-DSAMPLE_MODE=dev", cmake_command)
self.assertIn("-DSAMPLE_LIMIT=2", cmake_command)
clear.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "configure_lsp_database", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
def test_lsp_configures_default_target_and_extra_databases(self, docker, configure_remote, configure_one, configure_lsp):
context = self.context()
database = {
"name": "pico",
"source_dir": ".",
"build_dir": "build/build_pico_debug",
"generator": "Ninja",
"cmake_definitions": {
"VAMP_PICO_FIRMWARE_PROJECT": True,
},
"env": {},
"unset_env": ["CC", "CXX"],
}
context.lsp_settings = {
"enabled": True,
"compile_databases": [database],
}
status = modulus.cli.lsp(context, [])
self.assertEqual(status, 0)
configure_remote.assert_called_once_with(context)
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=False)
configure_lsp.assert_called_once_with(context, database)
docker.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_lsp_database_passes_cmake_definitions_and_unsets_env(self, run):
with tempfile.TemporaryDirectory() as repo_dir:
repo = pathlib.Path(repo_dir)
context = self.context(repo_root=repo)
status = modulus.cli.configure_lsp_database(
context,
{
"name": "pico",
"source_dir": ".",
"build_dir": "build/build_pico_debug",
"generator": "Ninja",
"cmake_definitions": {
"VAMP_PICO_FIRMWARE_PROJECT": True,
},
"env": {
"PICO_SDK_PATH": "/opt/pico-sdk",
},
"unset_env": ["CC", "CXX"],
},
)
self.assertEqual(status, 0)
command = run.call_args.args[1]
self.assertIn("-DVAMP_PICO_FIRMWARE_PROJECT=ON", command)
self.assertEqual(run.call_args.kwargs["env_overrides"], {"PICO_SDK_PATH": "/opt/pico-sdk"})
self.assertEqual(run.call_args.kwargs["env_unset"], ["CC", "CXX"])
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear):
context = self.context()
status = modulus.cli.configure_one(context, context.default_target, update=False)
self.assertEqual(status, 0)
conan_command = run.call_args_list[0].args[1]
self.assertIn("--build=missing", conan_command)
clear.assert_called_once()
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_create_package_passes_no_remote_to_conan_create(self, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
status = modulus.cli.create_package(context, context.default_target, update=False, no_remote=True)
self.assertEqual(status, 0)
self.assertIn("&:with_tests=False", run.call_args.args[1])
self.assertIn("--build=sample/*", run.call_args.args[1])
self.assertIn("--build=missing", run.call_args.args[1])
self.assertIn("--no-remote", run.call_args.args[1])
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_create_package_allows_public_repos_to_build_missing_dependencies(self, run):
with tempfile.TemporaryDirectory() as repo_dir:
context = self.context(repo_root=pathlib.Path(repo_dir))
status = modulus.cli.create_package(context, context.default_target, update=False)
self.assertEqual(status, 0)
self.assertIn("--build=sample/*", run.call_args.args[1])
self.assertIn("--build=missing", run.call_args.args[1])
def test_create_artifact_package_installs_archives_and_checksums_tree(self):
with tempfile.TemporaryDirectory() as repo_dir:
repo = pathlib.Path(repo_dir)
context = self.context(repo_root=repo)
context.package["artifact"].update({
"enabled": True,
"targets": ["native"],
"install_prefix": "usr",
"cmake_definitions": {
"SAMPLE_WITH_WEB": True,
},
"conan_options": {
"with_appliance_adapters": True,
},
})
def fake_run(run_context, command, **kwargs):
if command[:2] == ["cmake", "--install"]:
install_root = modulus.cli.package_artifact_build_root(context, context.default_target) / "stage" / "usr"
(install_root / "bin").mkdir(parents=True)
(install_root / "bin" / "sample").write_text("binary\n", encoding="utf-8")
return 0
with unittest.mock.patch.object(modulus.cli, "run", side_effect=fake_run) as run:
status, artifacts = modulus.cli.create_artifact_package(
context,
context.default_target,
"abc123",
update=False,
no_remote=True,
)
self.assertEqual(status, 0)
archive = repo / "build" / "packages" / "sample-abc123-native.tar.gz"
checksum = archive.with_name(archive.name + ".sha256")
self.assertEqual(artifacts, [archive, checksum])
self.assertTrue(archive.is_file())
self.assertTrue(checksum.is_file())
with tarfile.open(archive, "r:gz") as input_archive:
self.assertIn("usr/bin/sample", input_archive.getnames())
self.assertIn(archive.name, checksum.read_text(encoding="utf-8"))
conan_command = run.call_args_list[0].args[1]
self.assertEqual(conan_command[0:3], ["conan", "install", "."])
self.assertIn("&:with_appliance_adapters=True", conan_command)
self.assertIn("&:with_tests=False", conan_command)
self.assertIn("--no-remote", conan_command)
self.assertIn("--build=missing", conan_command)
cmake_command = run.call_args_list[1].args[1]
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
self.assertIn(f"-DCMAKE_INSTALL_PREFIX={repo / 'build' / 'build_common' / 'artifacts' / 'native' / 'stage' / 'usr'}", cmake_command)
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
def test_generic_artifact_upload_uses_package_registry_credentials(self, run):
context = self.context()
archive = pathlib.Path("build/packages/sample-abc123-native.tar.gz")
with unittest.mock.patch.dict(
os.environ,
{
"PACKAGE_REGISTRY_USER": "dev",
"PACKAGE_REGISTRY_TOKEN": "secret",
},
clear=True,
):
status = modulus.cli.upload_generic_artifact(context, "abc123", archive)
self.assertEqual(status, 0)
self.assertEqual(
run.call_args.args[1],
[
"curl",
"--fail",
"--silent",
"--show-error",
"--user",
"dev:secret",
"--upload-file",
archive,
"https://example.test/api/packages/acme/generic/sample/abc123/sample-abc123-native.tar.gz",
],
)
self.assertEqual(
run.call_args.kwargs["display_command"][5],
"dev:<hidden>",
)
if __name__ == "__main__":
unittest.main()
+352
View File
@@ -0,0 +1,352 @@
import json
import pathlib
import tempfile
import unittest
import modulus.config
import modulus.export
class modulus_export_test(unittest.TestCase):
def config(self):
return {
"package": {
"name": "sample",
},
"boost": {
"ref": "boost/1.91.0",
"components": ["json"],
"default_options": {
"boost/*:without_charconv": False,
},
},
"tests": {
"name": "sample",
},
}
def write_config(self, root, config):
(root / "modulus.json").write_text(json.dumps(config), encoding="utf-8")
def write_package(self, package):
for directory in (
"modulus",
"tools/docker",
"tools/editor",
"include/modulus",
"tests",
"lib/cmake/modulus",
"share/modulus",
"bin",
):
(package / directory).mkdir(parents=True)
(package / "conanfile.py").write_text("", encoding="utf-8")
(package / "modulus.json").write_text("{}", encoding="utf-8")
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
(package / "modulus" / "config.py").write_text("", encoding="utf-8")
(package / "modulus" / "export.py").write_text("", encoding="utf-8")
(package / "tools" / "configure.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
(package / "tools" / "lsp.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
(package / "tools" / "run.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
(package / "tools" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
(package / "tools" / "editor" / "clangd.yaml").write_text("CompileFlags: {}\n", encoding="utf-8")
(package / "tools" / "editor" / "clang-format").write_text("DisableFormat: true\n", encoding="utf-8")
(package / "tools" / "editor" / "pyrightconfig.json").write_text("{}\n", encoding="utf-8")
(package / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
(package / "tests" / "test.cpp").write_text("", encoding="utf-8")
(package / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
(package / "share" / "modulus" / "duplicate.py").write_text("", encoding="utf-8")
(package / "bin" / "modulus-tool").write_text("", encoding="utf-8")
def test_boost_requirement_options_enable_configured_components(self):
config = modulus.config.normalize(self.config())
boost_options = modulus.config.boost_requirement_options(config)
self.assertEqual(modulus.config.boost_ref(config), "boost/1.91.0")
self.assertIs(boost_options["without_json"], False)
self.assertIs(boost_options["without_container"], False)
self.assertIs(boost_options["without_charconv"], False)
self.assertIs(boost_options["without_atomic"], True)
def test_default_targets_include_architecture_and_build_type_matrix(self):
config = modulus.config.normalize(self.config())
targets = config["tooling"]["targets"]
self.assertEqual(
sorted(targets),
[
"arm_64_gcc12_debug",
"arm_64_gcc12_release",
"x86_64_gcc12_debug",
"x86_64_gcc12_release",
],
)
self.assertEqual(config["tooling"]["default_target"], "x86_64_gcc12_debug")
self.assertEqual(targets["x86_64_gcc12_debug"]["build_type"], "Debug")
self.assertFalse(targets["arm_64_gcc12_release"]["can_run"])
self.assertEqual(targets["x86_64_gcc12_release"]["profile"], "build/build_tools/conan/profiles/x86_64_gcc12_release")
self.assertFalse(config["package"]["artifact"]["enabled"])
self.assertFalse(config["tooling"]["run"]["enabled"])
self.assertFalse(config["tooling"]["lsp"]["enabled"])
self.assertEqual(config["tooling"]["lsp"]["compile_databases"], [])
def test_product_artifact_and_run_config_are_validated(self):
config = self.config()
config["package"]["artifact"] = {
"enabled": True,
"targets": ["x86_64_gcc12_release"],
"install_prefix": "usr",
"cmake_definitions": {
"SAMPLE_WITH_WEB": True,
},
"conan_options": {
"with_appliance_adapters": True,
},
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
}
config["tooling"] = {
"run": {
"enabled": True,
"target": "x86_64_gcc12_debug",
"executable": "sample",
"env": {
"SAMPLE_WEB_ROOT": "build/web",
},
},
}
normalized = modulus.config.normalize(config)
self.assertEqual(normalized["package"]["artifact"]["install_prefix"], "usr")
self.assertEqual(normalized["package"]["artifact"]["registry_url"], "https://git.thyme-labs.com/api/packages/khodo/generic")
self.assertEqual(normalized["tooling"]["run"]["args"], [])
config["package"]["artifact"]["targets"] = ["missing"]
with self.assertRaisesRegex(modulus.config.config_error, "unknown target"):
modulus.config.normalize(config)
def test_target_cmake_definitions_are_validated_and_defaulted(self):
config = self.config()
config["tooling"] = {
"default_target": "native",
"targets": {
"native": {
"profile": "build/build_tools/conan/profiles/native",
"build_dir": "build/build_native",
"can_run": True,
"build_type": "Debug",
"cmake_definitions": {
"SAMPLE_WITH_WEB": True,
"SAMPLE_MODE": "dev",
},
},
"release": {
"profile": "build/build_tools/conan/profiles/release",
"build_dir": "build/build_release",
"can_run": True,
"build_type": "Release",
},
},
}
normalized = modulus.config.normalize(config)
self.assertEqual(
normalized["tooling"]["targets"]["native"]["cmake_definitions"],
{
"SAMPLE_WITH_WEB": True,
"SAMPLE_MODE": "dev",
},
)
self.assertEqual(normalized["tooling"]["targets"]["release"]["cmake_definitions"], {})
config["tooling"]["targets"]["native"]["cmake_definitions"] = {
"SAMPLE_BAD": [
"invalid",
],
}
with self.assertRaisesRegex(modulus.config.config_error, "tooling.targets.native.cmake_definitions"):
modulus.config.normalize(config)
def test_lsp_compile_databases_are_validated_and_defaulted(self):
config = self.config()
config["tooling"] = {
"lsp": {
"enabled": True,
"compile_databases": [
{
"name": "pico",
"build_dir": "build/build_pico_debug",
"cmake_definitions": {
"VAMP_PICO_FIRMWARE_PROJECT": True,
},
"unset_env": [
"CC",
"CXX",
],
},
],
},
}
normalized = modulus.config.normalize(config)
database = normalized["tooling"]["lsp"]["compile_databases"][0]
self.assertEqual(database["source_dir"], ".")
self.assertEqual(database["generator"], "Ninja")
self.assertEqual(database["env"], {})
self.assertEqual(database["unset_env"], ["CC", "CXX"])
config["tooling"]["lsp"]["compile_databases"][0]["unset_env"] = ["bad-name"]
with self.assertRaisesRegex(modulus.config.config_error, "tooling.lsp.compile_databases\\[0\\].unset_env"):
modulus.config.normalize(config)
def test_checked_in_profile_overrides_exported_default_profile(self):
with tempfile.TemporaryDirectory() as repo_dir:
repo = pathlib.Path(repo_dir)
self.write_config(repo, self.config())
profile = repo / "conan" / "profiles" / "x86_64_gcc12_release"
profile.parent.mkdir(parents=True)
profile.write_text("[settings]\n", encoding="utf-8")
config = modulus.config.load_repo_config(repo)
self.assertEqual(config["tooling"]["targets"]["x86_64_gcc12_release"]["profile"], "conan/profiles/x86_64_gcc12_release")
def test_docker_defaults_use_shared_modulus_marker(self):
config = modulus.config.normalize(self.config())
self.assertEqual(config["tooling"]["docker"]["image"], "modulus-build:dev")
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus-build-image")
self.assertNotIn("apt_architectures", config["tooling"]["docker"])
self.assertNotIn("apt_packages", config["tooling"]["docker"])
def test_export_project_copies_runtime_and_exported_tools(self):
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
repo = pathlib.Path(repo_dir)
package = pathlib.Path(package_dir)
self.write_config(repo, self.config())
self.write_package(package)
modulus.export.sync_project(repo, package)
self.assertFalse((repo / "modulus.py").exists())
self.assertFalse((repo / "build" / "build_common" / "manifest.json").exists())
self.assertTrue((repo / "build" / "build_tools" / "configure.py").is_file())
self.assertTrue((repo / "build" / "build_tools" / "lsp.py").is_file())
self.assertTrue((repo / "build" / "build_tools" / "run.py").is_file())
self.assertTrue((repo / "build" / "build_tools" / "docker" / "Dockerfile").is_file())
self.assertFalse((repo / "build" / "build_tools" / "docs").exists())
self.assertFalse((repo / "build" / "build_tools" / "lsp").exists())
for source_name, destination_name in modulus.export.editor_files.items():
self.assertEqual((repo / destination_name).read_text(encoding="utf-8"), (package / "tools" / "editor" / source_name).read_text(encoding="utf-8"))
runtime = repo / "build" / "build_common" / "modulus"
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
self.assertTrue((runtime / "modulus" / "config.py").is_file())
self.assertTrue((runtime / "modulus" / "export.py").is_file())
self.assertFalse((runtime / "CMakeLists.txt").exists())
self.assertFalse((runtime / "README.md").exists())
self.assertFalse((runtime / "conanfile.py").exists())
self.assertFalse((runtime / "cmake").exists())
self.assertFalse((runtime / "include").exists())
self.assertFalse((runtime / "schemas").exists())
self.assertFalse((runtime / "src").exists())
self.assertFalse((runtime / "tools").exists())
self.assertFalse((runtime / "tests").exists())
self.assertFalse((runtime / "package").exists())
self.assertFalse((runtime / "lib").exists())
self.assertFalse((runtime / "share").exists())
self.assertFalse((runtime / "bin").exists())
def test_root_editor_files_match_export_templates(self):
root = pathlib.Path(__file__).resolve().parents[1]
for source_name, destination_name in modulus.export.editor_files.items():
self.assertEqual((root / destination_name).read_text(encoding="utf-8"), (root / "tools" / "editor" / source_name).read_text(encoding="utf-8"))
def test_clangd_template_uses_default_target_compile_database_and_disables_clang_tidy(self):
root = pathlib.Path(__file__).resolve().parents[1]
config = modulus.config.normalize(self.config())
default_target = config["tooling"]["targets"][config["tooling"]["default_target"]]
template = (root / "tools" / "editor" / "clangd.yaml").read_text(encoding="utf-8")
self.assertIn(f"CompilationDatabase: {default_target['build_dir']}", template)
self.assertIn("Diagnostics:", template)
self.assertIn("ClangTidy:", template)
self.assertIn("Remove: ['*']", template)
self.assertNotIn("bugprone-*", template)
self.assertNotIn("performance-*", template)
self.assertNotIn("readability-*", template)
def test_export_requires_packaged_tools(self):
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
repo = pathlib.Path(repo_dir)
package = pathlib.Path(package_dir)
self.write_config(repo, self.config())
(package / "modulus").mkdir()
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
with self.assertRaisesRegex(RuntimeError, "missing tools directory"):
modulus.export.sync_project(repo, package)
def test_normalize_rejects_invalid_cmake_values(self):
config = self.config()
config["package"]["name"] = "BadName"
with self.assertRaisesRegex(modulus.config.config_error, "package.name"):
modulus.config.normalize(config)
config = self.config()
config["docs"] = {}
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: docs"):
modulus.config.normalize(config)
def test_unknown_top_level_config_keys_are_rejected(self):
config = self.config()
config["old_config"] = {}
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: old_config"):
modulus.config.normalize(config)
def test_removed_setup_generation_fields_are_rejected(self):
config = self.config()
config["schema_version"] = 1
with self.assertRaisesRegex(modulus.config.config_error, "schema_version"):
modulus.config.normalize(config)
config = self.config()
config["package"]["cmake_target"] = "sample::sample"
with self.assertRaisesRegex(modulus.config.config_error, "cmake_target"):
modulus.config.normalize(config)
config = self.config()
config["exports"] = ["include/*.hpp"]
with self.assertRaisesRegex(modulus.config.config_error, "exports"):
modulus.config.normalize(config)
config = self.config()
config["dependencies"] = [
{
"ref": "openssl/4.0.1",
},
]
with self.assertRaisesRegex(modulus.config.config_error, "dependencies"):
modulus.config.normalize(config)
config = self.config()
config["examples"] = [
{
"target": "sample-example",
},
]
with self.assertRaisesRegex(modulus.config.config_error, "examples"):
modulus.config.normalize(config)
config = self.config()
config["tooling"] = {
"docker": {
"apt_packages": ["libdrm-dev"],
},
}
with self.assertRaisesRegex(modulus.config.config_error, "apt_packages"):
modulus.config.normalize(config)
config = self.config()
config["tooling"] = {
"targets": {
"native": {
"profile": "build/profile",
"build_dir": "build/native",
"options": {
"glfw/*:with_x11": False,
},
},
},
}
with self.assertRaisesRegex(modulus.config.config_error, "options"):
modulus.config.normalize(config)
config = self.config()
config["tooling"] = {
"remote": {
"update_by_default": True,
},
}
with self.assertRaisesRegex(modulus.config.config_error, "update_by_default"):
modulus.config.normalize(config)
if __name__ == "__main__":
unittest.main()
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("build")
+14
View File
@@ -0,0 +1,14 @@
include(raylib_drm_kms)
[settings]
os=Linux
arch=armv8
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=23
build_type=Debug
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
tools.build:compiler_executables={"c": "aarch64-linux-gnu-gcc", "cpp": "aarch64-linux-gnu-g++"}
+14
View File
@@ -0,0 +1,14 @@
include(raylib_drm_kms)
[settings]
os=Linux
arch=armv8
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=23
build_type=Release
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
tools.build:compiler_executables={"c": "aarch64-linux-gnu-gcc", "cpp": "aarch64-linux-gnu-g++"}
+4
View File
@@ -0,0 +1,4 @@
[conf]
raylib/*:tools.info.package_id:confs=["tools.cmake.cmaketoolchain:enabled_blocks", "tools.cmake.cmaketoolchain:extra_variables"]
raylib/*:tools.cmake.cmaketoolchain:enabled_blocks=["user_toolchain", "generic_system", "compilers", "fpic", "arch_flags", "rpath_link_flags", "libcxx", "cppstd", "parallel", "extra_flags", "cmake_flags_init", "extra_variables", "try_compile", "find_paths", "pkg_config", "rpath", "shared", "output_dirs"]
raylib/*:tools.cmake.cmaketoolchain:extra_variables={"PLATFORM": {"value": "DRM", "cache": True, "type": "STRING", "force": True}, "USE_EXTERNAL_GLFW": {"value": "OFF", "cache": True, "type": "BOOL", "force": True}, "OPENGL_VERSION": {"value": "ES 2.0", "cache": True, "type": "STRING", "force": True}, "BUILD_EXAMPLES": {"value": "OFF", "cache": True, "type": "BOOL", "force": True}, "WITH_PIC": {"value": "ON", "cache": True, "type": "BOOL", "force": True}, "CUSTOMIZE_BUILD": {"value": "ON", "cache": True, "type": "BOOL", "force": True}, "SUPPORT_MODULE_RAUDIO": {"value": "OFF", "cache": True, "type": "BOOL", "force": True}, "SUPPORT_CUSTOM_FRAME_CONTROL": {"value": "ON", "cache": True, "type": "BOOL", "force": True}, "SUPPORT_CAMERA_SYSTEM": {"value": "OFF", "cache": True, "type": "BOOL", "force": True}, "SUPPORT_GESTURES_SYSTEM": {"value": "ON", "cache": True, "type": "BOOL", "force": True}, "SUPPORT_RPRAND_GENERATOR": {"value": "OFF", "cache": True, "type": "BOOL", "force": True}}
+14
View File
@@ -0,0 +1,14 @@
include(raylib_drm_kms)
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=23
build_type=Debug
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
tools.build:compiler_executables={"c": "gcc-12", "cpp": "g++-12"}
+14
View File
@@ -0,0 +1,14 @@
include(raylib_drm_kms)
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=23
build_type=Release
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
tools.build:compiler_executables={"c": "gcc-12", "cpp": "g++-12"}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("configure")
+48
View File
@@ -0,0 +1,48 @@
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN dpkg --add-architecture arm64
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
build-essential \
ca-certificates \
clangd \
cmake \
curl \
g++-12 \
g++-aarch64-linux-gnu \
gcc-12 \
gcc-aarch64-linux-gnu \
git \
libdrm-dev \
libdrm-dev:arm64 \
libegl-dev \
libegl-dev:arm64 \
libgbm-dev \
libgbm-dev:arm64 \
libgles-dev \
libgles-dev:arm64 \
libglvnd-dev \
libglvnd-dev:arm64 \
ninja-build \
pkg-config \
python3 \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
ENV CC=/usr/bin/gcc-12
ENV CXX=/usr/bin/g++-12
RUN python3 -m venv /opt/modulus-python
ENV PATH=/opt/modulus-python/bin:${PATH}
RUN python -m pip install --no-cache-dir \
conan==2.26.2
WORKDIR /workspace
RUN mkdir -p /opt && touch /opt/modulus-build-image
+1
View File
@@ -0,0 +1 @@
DisableFormat: true
+9
View File
@@ -0,0 +1,9 @@
CompileFlags:
CompilationDatabase: build/build_x86_debug
Add:
- -Wall
- -Wextra
Diagnostics:
ClangTidy:
Remove: ['*']
+14
View File
@@ -0,0 +1,14 @@
{
"include": [
"modulus.py",
"package/conanfile.py",
"build/build_tools"
],
"extraPaths": [
"build/build_common/modulus",
"."
],
"typeCheckingMode": "off",
"reportMissingImports": false,
"reportMissingModuleSource": false
}
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("lsp")
+20
View File
@@ -0,0 +1,20 @@
import pathlib
import sys
def repo_root():
current = pathlib.Path(__file__).resolve()
for candidate in (current.parent, *current.parents):
if (candidate / "modulus.json").is_file():
return candidate
raise SystemExit("could not find modulus.json from Modulus tool wrapper")
def run(command):
root = repo_root()
runtime_root = root / "build" / "build_common" / "modulus"
source_root = root / "modulus" / "cli.py"
if source_root.is_file():
sys.path.insert(0, str(root))
elif runtime_root.exists():
sys.path.insert(0, str(runtime_root))
import modulus.cli
raise SystemExit(modulus.cli.main(command))
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("package")
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("run")
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import modulus_tool
if __name__ == "__main__":
modulus_tool.run("test")