This commit is contained in:
@@ -0,0 +1 @@
|
||||
DisableFormat: true
|
||||
@@ -0,0 +1,9 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: build/build_x86_debug
|
||||
Add:
|
||||
- -Wall
|
||||
- -Wextra
|
||||
|
||||
Diagnostics:
|
||||
ClangTidy:
|
||||
Remove: ['*']
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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_;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"package": {
|
||||
"name": "modulus"
|
||||
},
|
||||
"boost": {
|
||||
"components": [
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"tooling": {
|
||||
"remote": {
|
||||
"name": "khodo",
|
||||
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
__all__ = []
|
||||
+1077
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include": [
|
||||
"modulus.py",
|
||||
"package/conanfile.py",
|
||||
"build/build_tools"
|
||||
],
|
||||
"extraPaths": [
|
||||
"build/build_common/modulus",
|
||||
"."
|
||||
],
|
||||
"typeCheckingMode": "off",
|
||||
"reportMissingImports": false,
|
||||
"reportMissingModuleSource": false
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define BOOST_TEST_MODULE modulus
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
@@ -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()
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("build")
|
||||
Executable
+14
@@ -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++"}
|
||||
Executable
+14
@@ -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++"}
|
||||
@@ -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}}
|
||||
Executable
+14
@@ -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"}
|
||||
Executable
+14
@@ -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"}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("configure")
|
||||
Executable
+48
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
DisableFormat: true
|
||||
@@ -0,0 +1,9 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: build/build_x86_debug
|
||||
Add:
|
||||
- -Wall
|
||||
- -Wextra
|
||||
|
||||
Diagnostics:
|
||||
ClangTidy:
|
||||
Remove: ['*']
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("lsp")
|
||||
@@ -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))
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("package")
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("run")
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import modulus_tool
|
||||
|
||||
if __name__ == "__main__":
|
||||
modulus_tool.run("test")
|
||||
Reference in New Issue
Block a user