@@ -1 +0,0 @@
|
|||||||
DisableFormat: true
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CompileFlags:
|
|
||||||
CompilationDatabase: build/build_x86_debug
|
|
||||||
Add:
|
|
||||||
- -Wall
|
|
||||||
- -Wextra
|
|
||||||
|
|
||||||
Diagnostics:
|
|
||||||
ClangTidy:
|
|
||||||
Remove: ['*']
|
|
||||||
+2
-43
@@ -23,39 +23,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
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 update
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
@@ -66,16 +33,6 @@ jobs:
|
|||||||
gcc-12 \
|
gcc-12 \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
git \
|
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 \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
python3 \
|
python3 \
|
||||||
@@ -91,6 +48,8 @@ jobs:
|
|||||||
restore-keys: conan-${{ runner.os }}-
|
restore-keys: conan-${{ runner.os }}-
|
||||||
- name: Configure native release
|
- name: Configure native release
|
||||||
run: ./tools/configure.py --target x86_64_gcc12_release
|
run: ./tools/configure.py --target x86_64_gcc12_release
|
||||||
|
- name: Test tooling
|
||||||
|
run: python3 -m unittest discover -s tests -p 'test_*.py'
|
||||||
- name: Build native release
|
- name: Build native release
|
||||||
run: ./tools/build.py --target x86_64_gcc12_release
|
run: ./tools/build.py --target x86_64_gcc12_release
|
||||||
- name: Test native release
|
- name: Test native release
|
||||||
|
|||||||
+15
-19
@@ -1,25 +1,21 @@
|
|||||||
__pycache__/
|
build/
|
||||||
|
.clangd
|
||||||
|
.clang-format
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyd
|
*.pyd
|
||||||
|
*.log
|
||||||
.tmp/
|
.tmp/
|
||||||
build/
|
|
||||||
CMakeUserPresets.json
|
|
||||||
|
|
||||||
package/CMakeCache.txt
|
CMakeFiles/
|
||||||
package/CMakeFiles/
|
CMakeCache.txt
|
||||||
package/metadata/
|
CMakeUserPresets.json
|
||||||
package/.ninja_*
|
cmake_install.cmake
|
||||||
package/*.json
|
CTestTestfile.cmake
|
||||||
package/*.cmake
|
Testing/
|
||||||
package/*.ninja
|
build.ninja
|
||||||
package/*.sh
|
.ninja_*
|
||||||
package/*-smoke
|
compile_commands.json
|
||||||
package/Boost*
|
|
||||||
package/boost*
|
|
||||||
package/modulus-*
|
|
||||||
package/modulusTargets.cmake
|
|
||||||
package/cmakedeps_macros.cmake
|
|
||||||
package/conan_toolchain.cmake
|
|
||||||
package/conandeps_legacy.cmake
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Modulus
|
# 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.
|
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 configuration generation, typed LSP compile-database setup, and bridge targets for the external libraries needed by Product Vamp.
|
||||||
|
|
||||||
The core `modulus::modulus` target requires only Boost.JSON. Optional bridges are loaded when requested:
|
The core `modulus::modulus` target requires only Boost.JSON. Optional bridges are loaded when requested:
|
||||||
|
|
||||||
@@ -35,6 +35,29 @@ Refresh clangd compile databases with:
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## Editor Configuration
|
||||||
|
|
||||||
|
`modulus.json` is the source for generated `.clangd`, `.clang-format`, and `pyrightconfig.json` files. Configure, LSP refresh, and tooling export regenerate them only when their content changes. Keep these generated files ignored; customize `tooling.editor` instead:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tooling": {
|
||||||
|
"editor": {
|
||||||
|
"clangd": [
|
||||||
|
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
||||||
|
{"If": {"PathMatch": "src/firmware/.*"}, "CompileFlags": {"CompilationDatabase": "build/firmware"}}
|
||||||
|
],
|
||||||
|
"clang_format": {"DisableFormat": true},
|
||||||
|
"pyright": {"include": ["tools", "conanfile.py"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each supplied entry replaces that file's entire configuration. Omitted entries use Modulus defaults: the default target's build directory for clangd, `-Wall`/`-Wextra`, disabled clang-tidy and formatting, and Python paths for the bootstrap, recipe, and exported tools. Entries use the editors' own configuration keys. LLVM files use JSON-compatible YAML, with separate documents for clangd fragments.
|
||||||
|
|
||||||
|
The authored `cmake/` directory belongs in Git. `CMakeFiles/`, compiler caches, and generated sources belong in build directories. Modulus rejects project/source-root output directories before clearing configure or LSP caches. Conan may still use its own isolated build folders. Existing build caches are retained; they are not source files.
|
||||||
|
|
||||||
## Package
|
## Package
|
||||||
|
|
||||||
Create Conan packages with:
|
Create Conan packages with:
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
"tools/*.py",
|
"tools/*.py",
|
||||||
"tools/conan/profiles/*",
|
"tools/conan/profiles/*",
|
||||||
"tools/docker/*",
|
"tools/docker/*",
|
||||||
"tools/editor/*",
|
|
||||||
"README.md",
|
"README.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tooling": {
|
"tooling": {
|
||||||
|
"editor": {
|
||||||
|
"pyright": {
|
||||||
|
"include": ["modulus", "tools", "tests", "conanfile.py"],
|
||||||
|
"extraPaths": ["."],
|
||||||
|
"typeCheckingMode": "off",
|
||||||
|
"reportMissingImports": false,
|
||||||
|
"reportMissingModuleSource": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
"name": "khodo",
|
"name": "khodo",
|
||||||
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
|
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import tarfile
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import modulus.config
|
import modulus.config
|
||||||
|
import modulus.export
|
||||||
|
|
||||||
build_root = pathlib.Path("build")
|
build_root = pathlib.Path("build")
|
||||||
common_dir = build_root / "build_common"
|
common_dir = build_root / "build_common"
|
||||||
@@ -28,6 +29,8 @@ def main(command):
|
|||||||
repo_root = find_repo_root(pathlib.Path.cwd())
|
repo_root = find_repo_root(pathlib.Path.cwd())
|
||||||
config = modulus.config.load_repo_config(repo_root)
|
config = modulus.config.load_repo_config(repo_root)
|
||||||
context = build_context(repo_root, config)
|
context = build_context(repo_root, config)
|
||||||
|
if command in {"configure", "lsp"}:
|
||||||
|
modulus.export.sync_editor_files(repo_root, config)
|
||||||
if command == "configure":
|
if command == "configure":
|
||||||
return configure(context, sys.argv[1:])
|
return configure(context, sys.argv[1:])
|
||||||
if command == "build":
|
if command == "build":
|
||||||
@@ -78,6 +81,7 @@ class build_context:
|
|||||||
result = {}
|
result = {}
|
||||||
targets = self.tooling["targets"]
|
targets = self.tooling["targets"]
|
||||||
for name, target in targets.items():
|
for name, target in targets.items():
|
||||||
|
validate_build_directory(self.repo_root, self.repo_root, self.repo_root / str(target["build_dir"]))
|
||||||
result[name] = build_target(
|
result[name] = build_target(
|
||||||
name=name,
|
name=name,
|
||||||
profile=self.repo_root / str(target["profile"]),
|
profile=self.repo_root / str(target["profile"]),
|
||||||
@@ -610,9 +614,15 @@ def clear_lsp_cmake_cache(build_dir):
|
|||||||
if cmake_files.exists():
|
if cmake_files.exists():
|
||||||
shutil.rmtree(cmake_files)
|
shutil.rmtree(cmake_files)
|
||||||
|
|
||||||
|
def validate_build_directory(repo_root, source_dir, build_dir):
|
||||||
|
resolved = pathlib.Path(build_dir).resolve()
|
||||||
|
if resolved in {pathlib.Path(repo_root).resolve(), pathlib.Path(source_dir).resolve()}:
|
||||||
|
raise modulus.config.config_error(f"build directory must differ from the project and source roots: {build_dir}")
|
||||||
|
|
||||||
def configure_lsp_database(context, database):
|
def configure_lsp_database(context, database):
|
||||||
build_dir = context.repo_root / str(database["build_dir"])
|
build_dir = context.repo_root / str(database["build_dir"])
|
||||||
source_dir = context.repo_root / str(database.get("source_dir", "."))
|
source_dir = context.repo_root / str(database.get("source_dir", "."))
|
||||||
|
validate_build_directory(context.repo_root, source_dir, build_dir)
|
||||||
build_dir.mkdir(parents=True, exist_ok=True)
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
clear_lsp_cmake_cache(build_dir)
|
clear_lsp_cmake_cache(build_dir)
|
||||||
return run(
|
return run(
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ test_keys = {
|
|||||||
"name",
|
"name",
|
||||||
}
|
}
|
||||||
tooling_keys = {
|
tooling_keys = {
|
||||||
|
"editor",
|
||||||
"default_target",
|
"default_target",
|
||||||
"targets",
|
"targets",
|
||||||
"docker",
|
"docker",
|
||||||
@@ -255,8 +256,47 @@ def normalize(data):
|
|||||||
for target in tooling["targets"].values():
|
for target in tooling["targets"].values():
|
||||||
target.setdefault("cmake_definitions", {})
|
target.setdefault("cmake_definitions", {})
|
||||||
validate_config_references(data)
|
validate_config_references(data)
|
||||||
|
editor = tooling.setdefault("editor", {})
|
||||||
|
for name, value in default_editor_config(tooling).items():
|
||||||
|
editor.setdefault(name, value)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def default_editor_config(tooling):
|
||||||
|
target = tooling["targets"][tooling["default_target"]]
|
||||||
|
return {
|
||||||
|
"clangd": [{
|
||||||
|
"CompileFlags": {
|
||||||
|
"CompilationDatabase": target["build_dir"],
|
||||||
|
"Add": ["-Wall", "-Wextra"],
|
||||||
|
},
|
||||||
|
"Diagnostics": {"ClangTidy": {"Remove": ["*"]}},
|
||||||
|
}],
|
||||||
|
"clang_format": {"DisableFormat": True},
|
||||||
|
"pyright": {
|
||||||
|
"include": ["modulus.py", "conanfile.py", "build/build_tools"],
|
||||||
|
"extraPaths": ["build/build_common/modulus", "."],
|
||||||
|
"typeCheckingMode": "off",
|
||||||
|
"reportMissingImports": False,
|
||||||
|
"reportMissingModuleSource": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_editor(editor):
|
||||||
|
if not isinstance(editor, dict):
|
||||||
|
raise config_error("modulus.json tooling.editor must be an object")
|
||||||
|
validate_keys(editor, {"clangd", "clang_format", "pyright"}, "tooling.editor")
|
||||||
|
for name, value in editor.items():
|
||||||
|
field = f"tooling.editor.{name}"
|
||||||
|
if name == "clangd":
|
||||||
|
if not isinstance(value, list) or not value or not all(isinstance(fragment, dict) for fragment in value):
|
||||||
|
raise config_error(f"modulus.json {field} must be a non-empty list of configuration objects")
|
||||||
|
elif not isinstance(value, dict):
|
||||||
|
raise config_error(f"modulus.json {field} must be an object")
|
||||||
|
try:
|
||||||
|
json.dumps(value, allow_nan=False)
|
||||||
|
except (ValueError, TypeError) as error:
|
||||||
|
raise config_error(f"modulus.json {field} must contain valid JSON values") from error
|
||||||
|
|
||||||
def validate_raw_config(data):
|
def validate_raw_config(data):
|
||||||
validate_keys(data, top_level_keys, "modulus.json")
|
validate_keys(data, top_level_keys, "modulus.json")
|
||||||
package = require_mapping(data, "package")
|
package = require_mapping(data, "package")
|
||||||
@@ -340,6 +380,8 @@ def validate_tests(tests):
|
|||||||
validate_cmake_name(tests.get("name"), "tests.name", optional=True)
|
validate_cmake_name(tests.get("name"), "tests.name", optional=True)
|
||||||
|
|
||||||
def validate_tooling(tooling):
|
def validate_tooling(tooling):
|
||||||
|
if "editor" in tooling:
|
||||||
|
validate_editor(tooling["editor"])
|
||||||
validate_cmake_name(tooling.get("default_target"), "tooling.default_target", optional=True)
|
validate_cmake_name(tooling.get("default_target"), "tooling.default_target", optional=True)
|
||||||
validate_env_name(tooling.get("upload_env"), "tooling.upload_env", optional=True)
|
validate_env_name(tooling.get("upload_env"), "tooling.upload_env", optional=True)
|
||||||
run = tooling.get("run")
|
run = tooling.get("run")
|
||||||
|
|||||||
+17
-14
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@@ -34,18 +35,18 @@ runtime_ignore_patterns = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
editor_files = {
|
editor_files = {
|
||||||
"clangd.yaml": ".clangd",
|
"clangd": ".clangd",
|
||||||
"clang-format": ".clang-format",
|
"clang_format": ".clang-format",
|
||||||
"pyrightconfig.json": "pyrightconfig.json",
|
"pyright": "pyrightconfig.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
def sync_project(repo_root, package_root):
|
def sync_project(repo_root, package_root):
|
||||||
repo_root = pathlib.Path(repo_root).resolve()
|
repo_root = pathlib.Path(repo_root).resolve()
|
||||||
package_root = pathlib.Path(package_root).resolve()
|
package_root = pathlib.Path(package_root).resolve()
|
||||||
modulus.config.load_repo_config(repo_root)
|
config = modulus.config.load_repo_config(repo_root)
|
||||||
sync_runtime(repo_root, package_root)
|
sync_runtime(repo_root, package_root)
|
||||||
sync_tools(repo_root, package_root)
|
sync_tools(repo_root, package_root)
|
||||||
sync_editor_files(repo_root, package_root)
|
sync_editor_files(repo_root, config)
|
||||||
|
|
||||||
def sync_runtime(repo_root, package_root):
|
def sync_runtime(repo_root, package_root):
|
||||||
dst = repo_root / runtime_dir
|
dst = repo_root / runtime_dir
|
||||||
@@ -68,15 +69,17 @@ def sync_tools(repo_root, package_root):
|
|||||||
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.pyd"))
|
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.pyd"))
|
||||||
chmod_python_tools(dst)
|
chmod_python_tools(dst)
|
||||||
|
|
||||||
def sync_editor_files(repo_root, package_root):
|
def sync_editor_files(repo_root, config):
|
||||||
src = package_root / "tools" / "editor"
|
editor = config["tooling"]["editor"]
|
||||||
if not src.is_dir():
|
for name, destination in editor_files.items():
|
||||||
raise RuntimeError(f"Modulus package is missing editor templates: {src}")
|
value = editor[name]
|
||||||
for source_name, destination_name in editor_files.items():
|
fragments = value if name == "clangd" else [value]
|
||||||
source = src / source_name
|
content = "\n---\n".join(json.dumps(fragment, indent=2, allow_nan=False) for fragment in fragments) + "\n"
|
||||||
if not source.is_file():
|
if name != "pyright":
|
||||||
raise RuntimeError(f"Modulus package is missing editor template: {source}")
|
content = "# Generated by Modulus from modulus.json tooling.editor.\n" + content
|
||||||
shutil.copy2(source, repo_root / destination_name)
|
path = pathlib.Path(repo_root) / destination
|
||||||
|
if not path.exists() or path.read_text(encoding="utf-8") != content:
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
def replace_directory(path):
|
def replace_directory(path):
|
||||||
if path.exists():
|
if path.exists():
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"modulus.py",
|
|
||||||
"package/conanfile.py",
|
|
||||||
"build/build_tools"
|
|
||||||
],
|
|
||||||
"extraPaths": [
|
|
||||||
"build/build_common/modulus",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"typeCheckingMode": "off",
|
|
||||||
"reportMissingImports": false,
|
|
||||||
"reportMissingModuleSource": false
|
|
||||||
}
|
|
||||||
@@ -137,6 +137,19 @@
|
|||||||
"default_target": {
|
"default_target": {
|
||||||
"$ref": "#/$defs/cmake_name"
|
"$ref": "#/$defs/cmake_name"
|
||||||
},
|
},
|
||||||
|
"editor": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"clangd": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "type": "object" }
|
||||||
|
},
|
||||||
|
"clang_format": { "type": "object" },
|
||||||
|
"pyright": { "type": "object" }
|
||||||
|
}
|
||||||
|
},
|
||||||
"upload_env": {
|
"upload_env": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
||||||
|
|||||||
+47
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -69,6 +70,49 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
completed = subprocess.run(command, cwd=repo, check=False)
|
completed = subprocess.run(command, cwd=repo, check=False)
|
||||||
self.assertEqual(completed.returncode, 0)
|
self.assertEqual(completed.returncode, 0)
|
||||||
|
|
||||||
|
def test_project_root_build_is_rejected(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
context = self.context(root)
|
||||||
|
context.config["tooling"]["targets"]["native"]["build_dir"] = "."
|
||||||
|
with self.assertRaisesRegex(modulus.config.config_error, "project and source roots"):
|
||||||
|
modulus.cli.build_context(root, context.config)
|
||||||
|
|
||||||
|
def test_configure_and_lsp_refresh_project_editor_settings(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
fragments = [
|
||||||
|
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
||||||
|
{"If": {"PathMatch": "src/pico/.*"}, "CompileFlags": {"CompilationDatabase": "build/pico"}},
|
||||||
|
]
|
||||||
|
config = {"package": {"name": "sample"}, "tooling": {"editor": {"clangd": fragments}}}
|
||||||
|
(root / "modulus.json").write_text(json.dumps(config), encoding="utf-8")
|
||||||
|
with unittest.mock.patch.object(modulus.cli, "find_repo_root", return_value=root):
|
||||||
|
for command in ("configure", "lsp"):
|
||||||
|
with self.subTest(command=command), unittest.mock.patch.object(modulus.cli, command, return_value=0):
|
||||||
|
(root / ".clangd").write_text("stale", encoding="utf-8")
|
||||||
|
self.assertEqual(modulus.cli.main(command), 0)
|
||||||
|
content = (root / ".clangd").read_text(encoding="utf-8").split("\n", 1)[1]
|
||||||
|
self.assertEqual([json.loads(part) for part in content.split("\n---\n")], fragments)
|
||||||
|
before = {name: (root / name).stat().st_mtime_ns for name in modulus.export.editor_files.values()}
|
||||||
|
self.assertEqual(modulus.cli.main(command), 0)
|
||||||
|
self.assertEqual(before, {name: (root / name).stat().st_mtime_ns for name in before})
|
||||||
|
|
||||||
|
def test_lsp_rejects_source_root_before_cache_cleanup(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
context = self.context(root)
|
||||||
|
cache = root / "CMakeCache.txt"
|
||||||
|
cache.write_text("keep", encoding="utf-8")
|
||||||
|
with unittest.mock.patch.object(modulus.cli, "clear_lsp_cmake_cache") as clear:
|
||||||
|
for build_dir in (".", "alias", "subproject"):
|
||||||
|
if build_dir == "alias":
|
||||||
|
(root / "alias").symlink_to(root, target_is_directory=True)
|
||||||
|
with self.subTest(build_dir=build_dir), self.assertRaises(modulus.config.config_error):
|
||||||
|
modulus.cli.configure_lsp_database(context, {"source_dir": "subproject", "build_dir": build_dir})
|
||||||
|
clear.assert_not_called()
|
||||||
|
self.assertEqual(cache.read_text(encoding="utf-8"), "keep")
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
||||||
@@ -511,7 +555,9 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
def test_build_can_target_one_cmake_target(self, docker, run):
|
def test_build_can_target_one_cmake_target(self, docker, run):
|
||||||
context = self.context()
|
directory = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(directory.cleanup)
|
||||||
|
context = self.context(pathlib.Path(directory.name))
|
||||||
context.default_target.build_dir.mkdir(parents=True, exist_ok=True)
|
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 / "CMakeCache.txt").write_text("", encoding="utf-8")
|
||||||
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
|
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
|
||||||
|
|||||||
+46
-22
@@ -31,7 +31,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
for directory in (
|
for directory in (
|
||||||
"modulus",
|
"modulus",
|
||||||
"tools/docker",
|
"tools/docker",
|
||||||
"tools/editor",
|
|
||||||
"include/modulus",
|
"include/modulus",
|
||||||
"tests",
|
"tests",
|
||||||
"lib/cmake/modulus",
|
"lib/cmake/modulus",
|
||||||
@@ -48,9 +47,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
(package / "tools" / "lsp.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" / "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" / "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 / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
|
||||||
(package / "tests" / "test.cpp").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 / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
|
||||||
@@ -224,8 +220,8 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertTrue((repo / "build" / "build_tools" / "docker" / "Dockerfile").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" / "docs").exists())
|
||||||
self.assertFalse((repo / "build" / "build_tools" / "lsp").exists())
|
self.assertFalse((repo / "build" / "build_tools" / "lsp").exists())
|
||||||
for source_name, destination_name in modulus.export.editor_files.items():
|
for destination in modulus.export.editor_files.values():
|
||||||
self.assertEqual((repo / destination_name).read_text(encoding="utf-8"), (package / "tools" / "editor" / source_name).read_text(encoding="utf-8"))
|
self.assertTrue((repo / destination).is_file())
|
||||||
runtime = repo / "build" / "build_common" / "modulus"
|
runtime = repo / "build" / "build_common" / "modulus"
|
||||||
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
|
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
|
||||||
self.assertTrue((runtime / "modulus" / "config.py").is_file())
|
self.assertTrue((runtime / "modulus" / "config.py").is_file())
|
||||||
@@ -244,23 +240,51 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertFalse((runtime / "share").exists())
|
self.assertFalse((runtime / "share").exists())
|
||||||
self.assertFalse((runtime / "bin").exists())
|
self.assertFalse((runtime / "bin").exists())
|
||||||
|
|
||||||
def test_root_editor_files_match_export_templates(self):
|
def test_editor_defaults_use_configured_target(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())
|
config = modulus.config.normalize(self.config())
|
||||||
default_target = config["tooling"]["targets"][config["tooling"]["default_target"]]
|
config["tooling"].pop("editor")
|
||||||
template = (root / "tools" / "editor" / "clangd.yaml").read_text(encoding="utf-8")
|
config["tooling"]["targets"][config["tooling"]["default_target"]]["build_dir"] = "build/custom"
|
||||||
self.assertIn(f"CompilationDatabase: {default_target['build_dir']}", template)
|
config = modulus.config.normalize(config)
|
||||||
self.assertIn("Diagnostics:", template)
|
editor = config["tooling"]["editor"]
|
||||||
self.assertIn("ClangTidy:", template)
|
self.assertEqual(editor["clangd"][0]["CompileFlags"]["CompilationDatabase"], "build/custom")
|
||||||
self.assertIn("Remove: ['*']", template)
|
self.assertEqual(editor["clangd"][0]["Diagnostics"]["ClangTidy"]["Remove"], ["*"])
|
||||||
self.assertNotIn("bugprone-*", template)
|
self.assertIn("conanfile.py", editor["pyright"]["include"])
|
||||||
self.assertNotIn("performance-*", template)
|
self.assertNotIn("package/conanfile.py", editor["pyright"]["include"])
|
||||||
self.assertNotIn("readability-*", template)
|
|
||||||
|
def test_editor_replacements_and_refresh_are_idempotent(self):
|
||||||
|
config = self.config()
|
||||||
|
editor = {
|
||||||
|
"clangd": [
|
||||||
|
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
||||||
|
{"If": {"PathMatch": "src/pico/.*"}, "CompileFlags": {"CompilationDatabase": "build/pico"}},
|
||||||
|
],
|
||||||
|
"clang_format": {"BasedOnStyle": "LLVM", "IndentWidth": 2},
|
||||||
|
"pyright": {"include": ["generator"]},
|
||||||
|
}
|
||||||
|
config["tooling"] = {"editor": editor}
|
||||||
|
normalized = modulus.config.normalize(config)
|
||||||
|
self.assertEqual(normalized["tooling"]["editor"], editor)
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
modulus.export.sync_editor_files(root, normalized)
|
||||||
|
for name, filename in modulus.export.editor_files.items():
|
||||||
|
path = root / filename
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
if name != "pyright":
|
||||||
|
content = content.split("\n", 1)[1]
|
||||||
|
documents = [json.loads(part) for part in content.split("\n---\n")]
|
||||||
|
self.assertEqual(documents if name == "clangd" else documents[0], editor[name])
|
||||||
|
before = {path.name: path.stat().st_mtime_ns for path in root.iterdir()}
|
||||||
|
modulus.export.sync_editor_files(root, normalized)
|
||||||
|
self.assertEqual(before, {path.name: path.stat().st_mtime_ns for path in root.iterdir()})
|
||||||
|
|
||||||
|
def test_editor_config_rejects_invalid_shapes(self):
|
||||||
|
for editor in (None, [], {"unknown": {}}, {"clangd": {}}, {"clangd": []}, {"clangd": ["bad"]}, {"pyright": []}, {"clang_format": None}, {"pyright": {"value": float("nan")}}):
|
||||||
|
with self.subTest(editor=editor):
|
||||||
|
config = self.config()
|
||||||
|
config["tooling"] = {"editor": editor}
|
||||||
|
with self.assertRaises(modulus.config.config_error):
|
||||||
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_export_requires_packaged_tools(self):
|
def test_export_requires_packaged_tools(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
DisableFormat: true
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CompileFlags:
|
|
||||||
CompilationDatabase: build/build_x86_debug
|
|
||||||
Add:
|
|
||||||
- -Wall
|
|
||||||
- -Wextra
|
|
||||||
|
|
||||||
Diagnostics:
|
|
||||||
ClangTidy:
|
|
||||||
Remove: ['*']
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"modulus.py",
|
|
||||||
"package/conanfile.py",
|
|
||||||
"build/build_tools"
|
|
||||||
],
|
|
||||||
"extraPaths": [
|
|
||||||
"build/build_common/modulus",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"typeCheckingMode": "off",
|
|
||||||
"reportMissingImports": false,
|
|
||||||
"reportMissingModuleSource": false
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user