From a14f46451c6f82c4f75006ff3bcd31ad97c2fdef Mon Sep 17 00:00:00 2001 From: kindem Date: Tue, 2 Jun 2026 21:41:38 +0800 Subject: [PATCH 1/4] feat: pull qt and libclang prebuilts instead of building from source --- .gitignore | 1 + ThirdParty/ConanRecipes/README.md | 17 -- ThirdParty/ConanRecipes/dxc/conanfile.py | 17 +- .../ConanRecipes/libclang/conandata.yml | 12 +- ThirdParty/ConanRecipes/libclang/conanfile.py | 101 +++++--- ThirdParty/ConanRecipes/qt/conandata.yml | 25 +- ThirdParty/ConanRecipes/qt/conanfile.py | 218 +++++++++++------- conanfile.py | 4 +- 8 files changed, 234 insertions(+), 161 deletions(-) diff --git a/.gitignore b/.gitignore index 351f4b16..b448bf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ ThirdParty/Lib ThirdParty/ConanRecipes/**/src ThirdParty/ConanRecipes/**/build ThirdParty/ConanRecipes/**/CMakeUserPresets.json +aqtinstall.log # Test Project TestProject/.idea diff --git a/ThirdParty/ConanRecipes/README.md b/ThirdParty/ConanRecipes/README.md index f32e1eb6..4a17ba6f 100644 --- a/ThirdParty/ConanRecipes/README.md +++ b/ThirdParty/ConanRecipes/README.md @@ -25,20 +25,3 @@ conan export-pkg qt/conanfile.py --version="6.10.1-exp" # test stage conan test qt/test_package qt/6.10.1-exp ``` - -# Windows User Notice -On the Windows platform, we consider some Visual Studio components as part of the system toolchain. These components are not automatically configured in the conan script, so you will need to install them manually: - -* ATL -* Windows SDK -* Windows Driver Kit - -And some lib may build failed with long build tree path in development mode (like qt-webengine), in this case, you can use the commands to map conan recipes working directory as a driver and execute all conan commands in the driver root: - -```shell -# map -subst z: path/to/engine/ThirdParty/ConanRecipes - -# unmap -subst z: /d -``` diff --git a/ThirdParty/ConanRecipes/dxc/conanfile.py b/ThirdParty/ConanRecipes/dxc/conanfile.py index 53d377ea..beb4d016 100644 --- a/ThirdParty/ConanRecipes/dxc/conanfile.py +++ b/ThirdParty/ConanRecipes/dxc/conanfile.py @@ -48,7 +48,22 @@ def build(self): predefined_params_file = os.path.join(self.source_folder, "cmake", "caches", "PredefinedParams.cmake") cmake = CMake(self) - cmake.configure(cli_args=["-C", predefined_params_file, f"-DCMAKE_INSTALL_PREFIX={install_folder}"]) + # Disable every test sub-tree: + # * HLSL_INCLUDE_TESTS=ON (set by PredefinedParams.cmake) drags in TAEF + # via projects/dxilconv/unittests, which isn't shipped with the SDK. + # * SPIRV_BUILD_TESTS=ON (also from PredefinedParams) builds GoogleTest. + # * LLVM/CLANG_INCLUDE_TESTS=ON (CMake default) wires `check-all` up to + # `ExecHLSLTests`/`dxc_batch`, which only exist when HLSL tests are on + # and break configure once we turn them off. + # Command line -D values take precedence over the -C cache file. + cmake.configure(cli_args=[ + "-C", predefined_params_file, + f"-DCMAKE_INSTALL_PREFIX={install_folder}", + "-DHLSL_INCLUDE_TESTS=OFF", + "-DSPIRV_BUILD_TESTS=OFF", + "-DLLVM_INCLUDE_TESTS=OFF", + "-DCLANG_INCLUDE_TESTS=OFF", + ]) cmake.build() cmake.build(target="install-distribution") diff --git a/ThirdParty/ConanRecipes/libclang/conandata.yml b/ThirdParty/ConanRecipes/libclang/conandata.yml index 10e8b049..ecf50aec 100644 --- a/ThirdParty/ConanRecipes/libclang/conandata.yml +++ b/ThirdParty/ConanRecipes/libclang/conandata.yml @@ -1,3 +1,11 @@ sources: - "21.1.7-exp": - url: "https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-21.1.7.tar.gz" + "22.1.6-exp": + Windows-x86_64: + url: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.6/clang%2Bllvm-22.1.6-x86_64-pc-windows-msvc.tar.xz" + sha256: "657343edf361ca463bd642e39c74b251c6338b96cdbd55ff277555298b027696" + Linux-x86_64: + url: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.6/LLVM-22.1.6-Linux-X64.tar.xz" + sha256: "c5ac8ef89ca39d30cb32e9b83772f995dd891c685ebc188d593c943a64d5f8b5" + Macos-armv8: + url: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.6/LLVM-22.1.6-macOS-ARM64.tar.xz" + sha256: "8059d9d9eeb059c30d812b4a37291888f8dcba04d2b5ace61fd12d2904eaa0e9" diff --git a/ThirdParty/ConanRecipes/libclang/conanfile.py b/ThirdParty/ConanRecipes/libclang/conanfile.py index 07f3f43b..51247309 100644 --- a/ThirdParty/ConanRecipes/libclang/conanfile.py +++ b/ThirdParty/ConanRecipes/libclang/conanfile.py @@ -1,54 +1,71 @@ from conan import ConanFile -from conan.tools.build import check_min_cppstd -from conan.tools.build import can_run -from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout -from conan.tools.files import apply_conandata_patches, get, copy +from conan.errors import ConanInvalidConfiguration +from conan.tools.files import copy, get import os required_conan_version = ">=2.0.9" + class LibclangConan(ConanFile): name = "libclang" - description = "llvm - libclang" - license = "https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT" - url = "https://github.com/conan-io/conan-center-index" + description = "LLVM libclang (prebuilt, repackaged from llvm-project release artifacts)" + license = "Apache-2.0 WITH LLVM-exception" + url = "https://github.com/llvm/llvm-project" homepage = "https://github.com/llvm/llvm-project" - topics = ("llvm", "tool") + topics = ("llvm", "clang", "tool", "prebuilt") package_type = "shared-library" settings = "os", "arch", "compiler", "build_type" + no_copy_source = True - def layout(self): - cmake_layout(self, src_folder="src") + def _source_entry(self): + sources = self.conan_data.get("sources", {}).get(str(self.version), {}) + return sources.get(f"{self.settings.os}-{self.settings.arch}") def validate(self): - check_min_cppstd(self, 17) - - def build_requirements(self): - self.tool_requires("ninja/[>=1.12]") - self.tool_requires("cmake/[>=3.16]") - - def source(self): - get(self, **self.conan_data["sources"][self.version], strip_root=True) - apply_conandata_patches(self) - - def generate(self): - cmake_toolchain = CMakeToolchain(self, generator="Ninja") - cmake_toolchain.cache_variables["LLVM_ENABLE_PROJECTS"] = "clang" - cmake_toolchain.generate() - - deps = CMakeDeps(self) - deps.generate() + if self._source_entry() is None: + available = sorted( + self.conan_data.get("sources", {}).get(str(self.version), {}) + ) + raise ConanInvalidConfiguration( + f"libclang/{self.version} prebuilt is not available for " + f"{self.settings.os}/{self.settings.arch}. Supported: {available}" + ) def build(self): - cmake = CMake(self) - install_folder = os.path.join(self.build_folder, "installed") - cmake.configure(build_script_folder=os.path.join(self.source_folder, "llvm"), cli_args=[f"-DCMAKE_INSTALL_PREFIX={install_folder}"]) - cmake.build(target="libclang") - cmake.build(target="install-libclang") - cmake.build(target="install-libclang-headers") + get(self, **self._source_entry(), strip_root=True, destination=self.build_folder) def package(self): - copy(self, "*", os.path.join(self.build_folder, "installed"), self.package_folder) + src = self.build_folder + # Public C API headers; skip the multi-GB clang/llvm internal headers. + copy(self, "clang-c/*", + os.path.join(src, "include"), + os.path.join(self.package_folder, "include")) + if self.settings.os == "Windows": + copy(self, "libclang.dll", os.path.join(src, "bin"), + os.path.join(self.package_folder, "bin")) + copy(self, "libclang.lib", os.path.join(src, "lib"), + os.path.join(self.package_folder, "lib")) + elif self.settings.os == "Linux": + copy(self, "libclang.so*", os.path.join(src, "lib"), + os.path.join(self.package_folder, "lib")) + self._ensure_unversioned_symlink("libclang.so") + elif self.settings.os == "Macos": + copy(self, "libclang*.dylib", os.path.join(src, "lib"), + os.path.join(self.package_folder, "lib")) + self._ensure_unversioned_symlink("libclang.dylib") + # License: Linux/macOS CPack tarballs put it at the root, the Windows + # tarball nests it under include/llvm/Support/. + for rel in ("LICENSE.TXT", "include/llvm/Support/LICENSE.TXT"): + full = os.path.join(src, rel) + if os.path.isfile(full): + copy(self, os.path.basename(rel), os.path.dirname(full), + os.path.join(self.package_folder, "licenses")) + break + + def package_id(self): + # libclang is C ABI; the same prebuilt serves any compiler/build_type. + del self.info.settings.compiler + del self.info.settings.build_type def package_info(self): self.cpp_info.includedirs = ["include"] @@ -58,8 +75,16 @@ def package_info(self): self.cpp_info.bindirs = ["bin"] else: self.cpp_info.libs = ["clang"] + self.cpp_info.bindirs = ["lib"] - def test(self): - if can_run(self): - bin_path = os.path.join(self.cpp.build.bindirs[0], "test_package") - self.run(bin_path, env="conanrun") + def _ensure_unversioned_symlink(self, base): + """LLVM CPack tarballs ship libclang.so.X / libclang.X.dylib but may + omit the unversioned symlink CMakeDeps needs to resolve `-lclang`.""" + pkg_lib = os.path.join(self.package_folder, "lib") + target = os.path.join(pkg_lib, base) + if not os.path.isdir(pkg_lib) or os.path.lexists(target): + return + candidates = sorted(f for f in os.listdir(pkg_lib) + if f.startswith(base) and f != base) + if candidates: + os.symlink(candidates[-1], target) diff --git a/ThirdParty/ConanRecipes/qt/conandata.yml b/ThirdParty/ConanRecipes/qt/conandata.yml index a244b59c..014f5ec4 100644 --- a/ThirdParty/ConanRecipes/qt/conandata.yml +++ b/ThirdParty/ConanRecipes/qt/conandata.yml @@ -1,23 +1,2 @@ -sources: - "6.10.1-exp": - url: - - "https://mirrors.cloud.tencent.com/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://download.qt.io/official_releases/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://download.qt.io/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirrors.ukfast.co.uk/sites/qt.io/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirrors.20i.com/pub/qt.io/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.nluug.nl/languages/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirror.netcologne.de/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://qt-mirror.dannhauer.de/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.fau.de/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirrors.dotsrc.org/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.icm.edu.pl/packages/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.acc.umu.se/mirror/qt.io/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://www.nic.funet.fi/pub/mirrors/download.qt-project.org/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://qt.mirror.constant.com/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirrors.sau.edu.cn/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirror.bjtu.edu.cn/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://mirrors.sjtug.sjtu.edu.cn/qt/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.jaist.ac.jp/pub/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - - "https://ftp.yz.yamagata-u.ac.jp/pub/qtproject/archive/qt/6.10/6.10.1/single/qt-everywhere-src-6.10.1.tar.xz" - sha256: "0ed08b079719394303cd2054b66b2dc0c5895ceeb88fb6131c18991c980bf00f" +versions: + - "6.10.3-exp" diff --git a/ThirdParty/ConanRecipes/qt/conanfile.py b/ThirdParty/ConanRecipes/qt/conanfile.py index 36defd52..d3ce401c 100644 --- a/ThirdParty/ConanRecipes/qt/conanfile.py +++ b/ThirdParty/ConanRecipes/qt/conanfile.py @@ -1,104 +1,166 @@ from conan import ConanFile +from conan.errors import ConanInvalidConfiguration from conan.tools.build import check_min_cppstd -from conan.tools.build import can_run -from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout -from conan.tools.files import apply_conandata_patches, get, copy -from pip._internal import main as pip_main +from conan.tools.files import copy import os +import sys +import subprocess required_conan_version = ">=2.0.9" + class QtConan(ConanFile): name = "qt" - description = "qt" + description = "Qt 6 (prebuilt, repackaged from the official Qt CDN via aqtinstall)" license = "https://github.com/qt/qt5/tree/dev/LICENSES" - url = "https://github.com/qt/qt5" - homepage = "https://github.com/qt/qt5" - topics = ("gui", "tool") + url = "https://www.qt.io/" + homepage = "https://www.qt.io/" + topics = ("gui", "tool", "prebuilt") package_type = "shared-library" settings = "os", "arch", "compiler", "build_type" - options = {} - default_options = {} - - __enabled_projects = ( - "qtbase", "qtdeclarative", "qttools", "qtshadertools", "qtpositioning", "qtwebchannel", "qtwebengine", "qtwebsockets" + no_copy_source = True + + # Modules added on top of aqt's default base install. The base set already + # includes qtbase, qtdeclarative, qttools, qtsvg, qttranslations, qtimageformats. + _ADDON_MODULES = ( + "qtshadertools", + "qtpositioning", + "qtwebchannel", + "qtwebengine", + "qtwebsockets", ) - def layout(self): - cmake_layout(self, src_folder="src") + # (os, arch) -> (aqt host, aqt target, aqt arch, install-subdir-name). + # Supported: Windows x64, Linux x64, macOS arm64. + _AQT_PLATFORM = { + ("Windows", "x86_64"): ("windows", "desktop", "win64_msvc2022_64", "msvc2022_64"), + ("Linux", "x86_64"): ("linux", "desktop", "linux_gcc_64", "gcc_64"), + ("Macos", "armv8"): ("mac", "desktop", "clang_64", "macos"), + } + + def _aqt_params(self): + return self._AQT_PLATFORM.get((str(self.settings.os), str(self.settings.arch))) + + @property + def _qt_version(self): + """Strip the -exp / +foo suffix so aqt sees a plain version like 6.10.1.""" + v = str(self.version) + for sep in ("-", "+"): + v = v.split(sep, 1)[0] + return v def validate(self): check_min_cppstd(self, 17) - - def build_requirements(self): - self.tool_requires("ninja/[>=1.12]") - self.tool_requires("cmake/[>=3.16]") - - # for qtwebengine - self.tool_requires("nodejs/22.20.0") - self.tool_requires("gperf/3.1") - if self.settings.os == "Windows": - self.tool_requires("winflexbison/2.5.25") - else: - self.tool_requires("bison/3.8.2") - self.tool_requires("flex/2.6.4") - - def source(self): - get(self, **self.conan_data["sources"][self.version], strip_root=True) - apply_conandata_patches(self) - - def generate(self): - # for qtwebengine - print("enabled projects contains qtwebengine, checking html5lib installed...") - try: - import html5lib - print("html5lib import test is OK") - except ImportError: - print("html5lib not installed, try pip install") - if self.settings.os == "Windows": - pip_main(["install", "html5lib"]) - else: - pip_main(["install", "html5lib", "--break-system-packages"]) - try: - import html5lib - except ImportError: - raise RuntimeError("failed to import html5lib") - - # to ensure qtwebengine build success - if self.settings.os != "Windows": - import resource - soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) - resource.setrlimit(resource.RLIMIT_NOFILE, (1000000, hard_limit)) - soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) - print(f"set resource.RLIMIT_NOFILE for qtwebengine build to (soft: {soft_limit}, hard: {hard_limit})") - - cmake_toolchain = CMakeToolchain(self, generator="Ninja") - cmake_toolchain.cache_variables["QT_AUTODETECT_ANDROID"] = "" - cmake_toolchain.cache_variables["QT_GENERATE_SBOM"] = "OFF" - for entry in os.listdir(self.source_folder): - if not os.path.isdir(entry) and not entry.startswith('qt'): - continue - cmake_toolchain.cache_variables[f"BUILD_{entry}"] = "ON" if entry in self.__enabled_projects else "OFF" - cmake_toolchain.generate() - - deps = CMakeDeps(self) - deps.generate() + if self._aqt_params() is None: + raise ConanInvalidConfiguration( + f"qt/{self.version} prebuilt is not available for " + f"{self.settings.os}/{self.settings.arch}. " + f"Supported: {sorted(self._AQT_PLATFORM)}" + ) + if self.settings.os == "Windows" and str(self.settings.compiler) == "msvc": + # The win64_msvc2022_64 prebuilt links/runs against any MSVC toolset + # that is binary-compatible with v143. Microsoft's C++ binary + # compatibility guarantee covers v140-v145 (see "C++ binary + # compatibility 2015-2026" on Microsoft Learn), so VS2022 (193/194) + # and VS2026 (195+) both consume this prebuilt safely as long as Qt + # keeps shipping only win64_msvc2022_64. If Qt later adds a separate + # win64_msvc2026_64 in aqtinstall, switch _AQT_PLATFORM accordingly. + ver = int(str(self.settings.compiler.version)) + if ver < 193: + raise ConanInvalidConfiguration( + f"Qt prebuilt is built with MSVC 2022 (v143). " + f"compiler.version must be >= 193 (VS2022) for ABI " + f"compatibility, got {ver}" + ) def build(self): - cmake = CMake(self) - cmake.configure() - cmake.build() + self._ensure_aqt() + host, target, arch, dirname = self._aqt_params() + outdir = os.path.join(self.build_folder, "qt-install") + cmd = [ + sys.executable, "-m", "aqt", "install-qt", + host, target, self._qt_version, arch, + "-O", outdir, + "-m", *self._ADDON_MODULES, + ] + self.output.info(f"Running aqt: {' '.join(cmd)}") + subprocess.check_call(cmd) + self._strip_debug_artifacts(os.path.join(outdir, self._qt_version, dirname)) def package(self): - cmake = CMake(self) - cmake.install() + _, _, _, dirname = self._aqt_params() + qt_prefix = os.path.join(self.build_folder, "qt-install", self._qt_version, dirname) + if not os.path.isdir(qt_prefix): + raise RuntimeError(f"aqt did not produce the expected layout at {qt_prefix}") + copy(self, "*", qt_prefix, self.package_folder, keep_path=True) + # aqt 3.3+ already writes a relocatable bin/qt.conf; idempotent rewrite + # so we don't depend on that behavior surviving aqt upgrades. + qt_conf = os.path.join(self.package_folder, "bin", "qt.conf") + os.makedirs(os.path.dirname(qt_conf), exist_ok=True) + with open(qt_conf, "w", encoding="utf-8", newline="\n") as f: + f.write("[Paths]\nPrefix=..\n") + + def package_id(self): + # All consumer build_types map to Release in the top-level project, and + # the prebuilt is fully baked, so compiler/build_type don't affect the + # binary. validate() filters incompatible toolchains. + del self.info.settings.build_type + del self.info.settings.compiler def package_info(self): + # Consumers must use Qt's own Qt6Config.cmake (which is relocatable). + # We expose lib/cmake via builddirs so CMAKE_PREFIX_PATH picks it up. self.cpp_info.libs = [] self.cpp_info.builddirs = [os.path.join("lib", "cmake")] self.cpp_info.set_property("cmake_find_mode", "none") - def test(self): - if can_run(self): - bin_path = os.path.join(self.cpp.build.bindirs[0], "test_package") - self.run(bin_path, env="conanrun") + def _ensure_aqt(self): + """pip-install aqtinstall on demand.""" + try: + import aqt # noqa: F401 + return + except ImportError: + pass + cmd = [sys.executable, "-m", "pip", "install", "aqtinstall"] + if self.settings.os != "Windows": + cmd.append("--break-system-packages") + subprocess.check_call(cmd) + + def _strip_debug_artifacts(self, root_dir): + """Drop Qt's Debug variants (~2 GiB on Windows). The top-level project + pins all Conan consumption to Release (CONAN_INSTALL_BUILD_CONFIGURATIONS + + CMAKE_MAP_IMPORTED_CONFIG_*), so debug DLLs/libs/.prl plus per-config + Targets-debug.cmake imports are dead weight. + + Patterns: + 1. ``d.`` when ``.`` exists in the same dir + (Qt's Windows debug suffix; pairs like Qt6Core.dll/Qt6Cored.dll). + The "release counterpart exists" check protects names that + genuinely end in ``d`` -- e.g. qdirect2d.dll, qopensslbackend.dll. + 2. ``Targets-debug.cmake`` -- per-config CMake target imports. + + Idempotent. + """ + debug_exts = {".dll", ".lib", ".pdb", ".prl", ".so", ".dylib", ".a"} + deleted, saved = 0, 0 + for cur, _, files in os.walk(root_dir): + names = set(files) + for fname in files: + stem, ext = os.path.splitext(fname) + ext_l = ext.lower() + is_debug_cmake = ext_l == ".cmake" and stem.endswith("-debug") + is_debug_binary = ( + ext_l in debug_exts + and stem.endswith("d") and len(stem) > 1 + and (stem[:-1] + ext) in names + ) + if not (is_debug_cmake or is_debug_binary): + continue + full = os.path.join(cur, fname) + saved += os.path.getsize(full) + os.remove(full) + deleted += 1 + if deleted: + self.output.info( + f"Stripped {deleted} debug artifacts ({saved / 1024 / 1024:.1f} MiB)" + ) diff --git a/conanfile.py b/conanfile.py index 457f62bb..2c96d2f6 100644 --- a/conanfile.py +++ b/conanfile.py @@ -19,8 +19,8 @@ def requirements(self): # private repo self.requires("glfw/3.4-exp") - self.requires("libclang/21.1.7-exp") - self.requires("qt/6.10.1-exp") + self.requires("libclang/22.1.6-exp") + self.requires("qt/6.10.3-exp") self.requires("debugbreak/1.0-exp") self.requires("rapidjson/cci.20250205-exp") self.requires("assimp/6.0.2-exp") From c31ddf89516e48c4c5c9e85ed621bcb41df95a9c Mon Sep 17 00:00:00 2001 From: kindem Date: Tue, 2 Jun 2026 21:43:29 +0800 Subject: [PATCH 2/4] fix: several mirror reflection bugs and expand unit tests --- Engine/Source/Mirror/Include/Mirror/Mirror.h | 8 +- .../Source/Mirror/Include/Mirror/Registry.h | 1 + Engine/Source/Mirror/Src/Mirror.cpp | 10 +- Engine/Source/Mirror/Test/AnyTest.cpp | 626 +++++++++++++++ Engine/Source/Mirror/Test/RegistryTest.cpp | 711 ++++++++++++++++++ Engine/Source/Mirror/Test/RegistryTest.h | 33 + .../Source/Mirror/Test/SerializationTest.cpp | 36 + Engine/Source/Mirror/Test/TypeInfoTest.cpp | 63 ++ 8 files changed, 1480 insertions(+), 8 deletions(-) diff --git a/Engine/Source/Mirror/Include/Mirror/Mirror.h b/Engine/Source/Mirror/Include/Mirror/Mirror.h index 58731a7c..c1aff6e0 100644 --- a/Engine/Source/Mirror/Include/Mirror/Mirror.h +++ b/Engine/Source/Mirror/Include/Mirror/Mirror.h @@ -563,7 +563,7 @@ namespace Mirror { template Any Construct(Args&&... args) const; template Any New(Args&&... args) const; - template Any InplaceNew(Args&&... args) const; + template Any InplaceNew(void* ptr, Args&&... args) const; const std::string& GetOwnerName() const; const Id& GetOwnerId() const; @@ -3600,9 +3600,9 @@ namespace Mirror { } template - Any Constructor::InplaceNew(Args&&... args) const + Any Constructor::InplaceNew(void* ptr, Args&&... args) const { - return InplaceNewDyn(ForwardAsArgList(std::forward(args)...)); + return InplaceNewDyn(ptr, ForwardAsArgList(std::forward(args)...)); } template @@ -3710,7 +3710,7 @@ namespace Mirror { template bool EnumValue::Compare(const E& value) const { - return Compare(ForwardAsArg(value)); + return CompareDyn(ForwardAsArg(value)); } template diff --git a/Engine/Source/Mirror/Include/Mirror/Registry.h b/Engine/Source/Mirror/Include/Mirror/Registry.h index d37f485c..0feac678 100644 --- a/Engine/Source/Mirror/Include/Mirror/Registry.h +++ b/Engine/Source/Mirror/Include/Mirror/Registry.h @@ -660,6 +660,7 @@ namespace Mirror { Enum::ConstructParams params; params.id = inId; + params.typeInfo = GetTypeInfo(); Enum::typeToIdMap[typeId] = inId; return EnumRegistry(EmplaceEnum(inId, std::move(params))); diff --git a/Engine/Source/Mirror/Src/Mirror.cpp b/Engine/Source/Mirror/Src/Mirror.cpp index ea4f305c..23005299 100644 --- a/Engine/Source/Mirror/Src/Mirror.cpp +++ b/Engine/Source/Mirror/Src/Mirror.cpp @@ -339,7 +339,7 @@ namespace Mirror { Assert(!Empty() && !inOther.Empty() && arrayLength == inOther.arrayLength && rtti == inOther.rtti); for (auto i = 0; i < ElementNum(); i++) { - rtti->copyAssign(Data(i), inOther.Data(i)); + rtti->moveAssign(Data(i), inOther.Data(i)); } } @@ -2224,7 +2224,7 @@ namespace Mirror { void StdListView::ConstTraverse(const ElementTraverser& inTraverser) const { - rtti->traverse(ref, inTraverser); + rtti->constTraverse(ref, inTraverser); } Any StdListView::EmplaceFront(const Argument& inTempObj) const @@ -2555,16 +2555,18 @@ namespace Mirror { StdVariantView::StdVariantView(const Any& inRef) : ref(inRef) { + Assert(ref.IsRef() && ref.CanAsTemplateView()); + rtti = static_cast(ref.GetTemplateViewRtti()); } size_t StdVariantView::TypeNum() const { - return rtti->TypeNum(); + return rtti->typeNum(); } const TypeInfo* StdVariantView::TypeByIndex(size_t inIndex) const { - return rtti->TypeByIndex(inIndex); + return rtti->typeByIndex(inIndex); } Any StdVariantView::CreateElement(size_t inIndex) const diff --git a/Engine/Source/Mirror/Test/AnyTest.cpp b/Engine/Source/Mirror/Test/AnyTest.cpp index 35197721..221e8746 100644 --- a/Engine/Source/Mirror/Test/AnyTest.cpp +++ b/Engine/Source/Mirror/Test/AnyTest.cpp @@ -1477,3 +1477,629 @@ TEST(AnyTest, StdTupleViewTest) }); ASSERT_EQ(count, 3); } + +TEST(AnyTest, StdTupleViewCreateAndConstTraverseTest) +{ + Any a0 = std::tuple { 1, true, "hello" }; + const StdTupleView v0(a0.Ref()); + + const Any e0 = v0.CreateElement(0); + ASSERT_EQ(e0.Type()->id, GetTypeInfo()->id); + const Any e1 = v0.CreateElement(2); + ASSERT_EQ(e1.Type()->id, GetTypeInfo()->id); + + int count = 0; + v0.ConstTraverse([&](const Any& inRef, size_t inIndex) -> void { + if (inIndex == 0) { + ASSERT_EQ(inRef.As(), 1); + } else if (inIndex == 2) { + ASSERT_EQ(inRef.As(), "hello"); + } + count++; + }); + ASSERT_EQ(count, 3); +} + +TEST(AnyTest, StdVariantViewTest) +{ + using Var = std::variant; + + Var v { 42 }; + Any a0 = std::ref(v); + const StdVariantView view(a0.Ref()); + + ASSERT_EQ(view.TypeNum(), 3); + ASSERT_EQ(view.TypeByIndex(0)->id, GetTypeInfo()->id); + ASSERT_EQ(view.TypeByIndex(1)->id, GetTypeInfo()->id); + ASSERT_EQ(view.TypeByIndex(2)->id, GetTypeInfo()->id); + + ASSERT_EQ(view.Index(), 0); + ASSERT_EQ(view.GetElement(0).As(), 42); + ASSERT_EQ(view.GetConstElement(0).As(), 42); + + bool visited = false; + view.Visit([&](const Any& inAny) -> void { + ASSERT_EQ(inAny.As(), 42); + visited = true; + }); + ASSERT_TRUE(visited); +} + +TEST(AnyTest, StdVariantViewEmplaceAndCreateTest) +{ + using Var = std::variant; + + Var v; + Any a0 = std::ref(v); + const StdVariantView view(a0.Ref()); + + const Any newStr = view.CreateElement(1); + ASSERT_EQ(newStr.Type()->id, GetTypeInfo()->id); + + Any tmp = std::string("hello"); + view.Emplace(1, tmp); + ASSERT_EQ(view.Index(), 1); + ASSERT_EQ(view.GetConstElement(1).As(), "hello"); + + Any tmpFloat = 3.14f; + view.Emplace(2, tmpFloat); + ASSERT_EQ(view.Index(), 2); + ASSERT_EQ(view.GetConstElement(2).As(), 3.14f); +} + +TEST(AnyTest, StdOptionalViewMutationTest) +{ + std::optional v; + Any a0 = std::ref(v); + const StdOptionalView view(a0.Ref()); + ASSERT_FALSE(view.HasValue()); + + Any tmp = 7; + view.Emplace(tmp); + ASSERT_TRUE(view.HasValue()); + ASSERT_EQ(view.Value().As(), 7); + + view.Reset(); + ASSERT_FALSE(view.HasValue()); + + view.EmplaceDefault(); + ASSERT_TRUE(view.HasValue()); + ASSERT_EQ(view.Value().As(), 0); + + ASSERT_EQ(view.ConstValue().As(), 0); +} + +TEST(AnyTest, StdPairViewMutationTest) +{ + std::pair p { 1, "abc" }; + Any a0 = std::ref(p); + const StdPairView view(a0.Ref()); + + ASSERT_EQ(view.Key().As(), 1); + ASSERT_EQ(view.Value().As(), "abc"); + ASSERT_EQ(view.ConstKey().As(), 1); + ASSERT_EQ(view.ConstValue().As(), "abc"); + + view.Key().As() = 100; + view.Value().As() = "xyz"; + ASSERT_EQ(p.first, 100); + ASSERT_EQ(p.second, "xyz"); + + view.Reset(); + ASSERT_EQ(p.first, 0); + ASSERT_EQ(p.second, ""); +} + +TEST(AnyTest, StdVectorViewExtraTest) +{ + Any a0 = std::vector {}; + const StdVectorView view(a0.Ref()); + ASSERT_EQ(view.Size(), 0); + + view.Reserve(16); + ASSERT_EQ(view.Size(), 0); + + view.Resize(3); + ASSERT_EQ(view.Size(), 3); + view.At(0).As() = 1; + view.At(1).As() = 2; + view.At(2).As() = 3; + ASSERT_EQ(view.At(2).As(), 3); + + view.EmplaceDefaultBack(); + ASSERT_EQ(view.Size(), 4); + ASSERT_EQ(view.At(3).As(), 0); + + view.Clear(); + ASSERT_EQ(view.Size(), 0); +} + +TEST(AnyTest, StdListViewConstTraverseTest) +{ + std::list t0 = { 10, 20, 30 }; + Any a0 = std::ref(t0); + const StdListView view(a0.Ref()); + ASSERT_EQ(view.Size(), 3); + + std::vector visited; + view.ConstTraverse([&](const Any& inRef) -> void { + visited.push_back(inRef.As()); + }); + ASSERT_EQ(visited, (std::vector { 10, 20, 30 })); + + view.EmplaceDefaultFront(); + view.EmplaceDefaultBack(); + ASSERT_EQ(view.Size(), 5); + + view.Clear(); + ASSERT_EQ(view.Size(), 0); +} + +TEST(AnyTest, StdSetViewExtraTest) +{ + Any a0 = std::set { 1, 2, 3 }; + const StdSetView view(a0.Ref()); + ASSERT_TRUE(view.Contains(Any(1))); + ASSERT_TRUE(view.Contains(Any(2))); + ASSERT_FALSE(view.Contains(Any(99))); + + view.Erase(Any(2)); + ASSERT_FALSE(view.Contains(Any(2))); + ASSERT_EQ(view.Size(), 2); + + const Any created = view.CreateElement(); + ASSERT_EQ(created.Type()->id, GetTypeInfo()->id); + + view.Clear(); + ASSERT_EQ(view.Size(), 0); +} + +TEST(AnyTest, StdUnorderedSetViewExtraTest) +{ + Any a0 = std::unordered_set {}; + const StdUnorderedSetView view(a0.Ref()); + + view.Reserve(16); + view.Emplace(Any(1)); + view.Emplace(Any(2)); + ASSERT_EQ(view.Size(), 2); + ASSERT_TRUE(view.Contains(Any(1))); + ASSERT_FALSE(view.Contains(Any(99))); + + view.Erase(Any(1)); + ASSERT_FALSE(view.Contains(Any(1))); + + const Any created = view.CreateElement(); + ASSERT_EQ(created.Type()->id, GetTypeInfo()->id); + + view.Clear(); + ASSERT_EQ(view.Size(), 0); +} + +TEST(AnyTest, StdMapViewExtraTest) +{ + std::map m = { { 1, "a" }, { 2, "b" } }; + Any a0 = std::ref(m); + const StdMapView view(a0.Ref()); + ASSERT_EQ(view.Size(), 2); + ASSERT_TRUE(view.Contains(Any(1))); + ASSERT_FALSE(view.Contains(Any(99))); + ASSERT_EQ(view.ConstAt(Any(1)).As(), "a"); + + view.Erase(Any(1)); + ASSERT_FALSE(view.Contains(Any(1))); + ASSERT_EQ(view.Size(), 1); + + const Any newKey = view.CreateKey(); + const Any newValue = view.CreateValue(); + ASSERT_EQ(newKey.Type()->id, GetTypeInfo()->id); + ASSERT_EQ(newValue.Type()->id, GetTypeInfo()->id); + + view.Clear(); + ASSERT_EQ(view.Size(), 0); +} + +TEST(AnyTest, StdUnorderedMapViewExtraTest) +{ + Any a0 = std::unordered_map {}; + const StdUnorderedMapView view(a0.Ref()); + + view.Reserve(16); + view.Emplace(Any(1), Any(std::string("a"))); + ASSERT_EQ(view.Size(), 1); + ASSERT_TRUE(view.Contains(Any(1))); + + view.Erase(Any(1)); + ASSERT_FALSE(view.Contains(Any(1))); + + const Any newKey = view.CreateKey(); + const Any newValue = view.CreateValue(); + ASSERT_EQ(newKey.Type()->id, GetTypeInfo()->id); + ASSERT_EQ(newValue.Type()->id, GetTypeInfo()->id); +} + +TEST(AnyTest, HasTemplateViewTest) +{ + Any a0 = std::vector { 1, 2, 3 }; + ASSERT_TRUE(a0.HasTemplateView()); + ASSERT_EQ(a0.GetTemplateViewId(), StdVectorView::id); + + Any a1 = 1; + ASSERT_FALSE(a1.HasTemplateView()); + + ASSERT_TRUE(a0.CanAsTemplateView()); + ASSERT_FALSE(a0.CanAsTemplateView()); +} + +TEST(AnyTest, OperatorIndexTest) +{ + int v0[] = { 7, 8, 9 }; + Any a0 = v0; + ASSERT_EQ(a0[0].As(), 7); + ASSERT_EQ(a0[1].As(), 8); + + a0[2].As() = 100; + ASSERT_EQ(a0[2].As(), 100); + + const Any a1 = v0; + ASSERT_EQ(a1[0].As(), 7); +} + +TEST(AnyTest, MemberFuncMoveAssignTest) +{ + Any a0 = AnyMoveAssignTest(); + Any a1 = AnyMoveAssignTest(); + ASSERT_FALSE(a0.As().called); + + a0.MoveAssign(std::move(a1)); + ASSERT_TRUE(a0.As().called); +} + +TEST(AnyTest, ForwardAsAnyTest) +{ + Any a0 = ForwardAsAny(42); + ASSERT_TRUE(a0.IsMemoryHolder()); + ASSERT_EQ(a0.As(), 42); + + int v0 = 7; + Any a1 = ForwardAsAny(v0); + ASSERT_TRUE(a1.IsNonConstRef()); + ASSERT_EQ(a1.As(), 7); + + const int v1 = 8; // NOLINT + Any a2 = ForwardAsAny(v1); + ASSERT_TRUE(a2.IsConstRef()); + ASSERT_EQ(a2.As(), 8); +} + +TEST(AnyTest, ForwardAsAnyByValueTest) +{ + int v0 = 7; + Any a0 = ForwardAsAnyByValue(v0); + ASSERT_TRUE(a0.IsMemoryHolder()); + ASSERT_EQ(a0.As(), 7); + ASSERT_NE(a0.Data(), &v0); + + Any a1 = ForwardAsAnyByValue(42); + ASSERT_TRUE(a1.IsMemoryHolder()); + ASSERT_EQ(a1.As(), 42); +} + +TEST(AnyTest, ForwardAsArgListTest) +{ + int v0 = 1; + const float v1 = 2.0f; // NOLINT + + ArgumentList args = ForwardAsArgList(v0, v1, 3.0); + ASSERT_EQ(args.size(), 3); + ASSERT_TRUE(args[0].IsNonConstRef()); + ASSERT_TRUE(args[1].IsConstRef()); + ASSERT_TRUE(args[2].IsMemoryHolder()); + + ASSERT_EQ(args[0].As(), 1); + ASSERT_EQ(args[1].As(), 2.0f); + ASSERT_EQ(args[2].As(), 3.0); +} + +TEST(AnyTest, ForwardAsArgListByValueTest) +{ + int v0 = 1; + ArgumentList args = ForwardAsArgListByValue(v0, 2.0f); + ASSERT_EQ(args.size(), 2); + ASSERT_TRUE(args[0].IsMemoryHolder()); + ASSERT_TRUE(args[1].IsMemoryHolder()); + ASSERT_EQ(args[0].As(), 1); + ASSERT_EQ(args[1].As(), 2.0f); +} + +TEST(AnyTest, ArgumentBasicTest) +{ + Any a0 = 7; + Argument arg0 = a0; // NOLINT + ASSERT_FALSE(arg0.IsConstRef()); + ASSERT_TRUE(arg0.IsMemoryHolder()); + ASSERT_EQ(arg0.Type()->id, GetTypeInfo()->id); + ASSERT_EQ(arg0.As(), 7); + + int v0 = 9; + Any a1 = std::ref(v0); + Argument arg1 = a1; // NOLINT + ASSERT_TRUE(arg1.IsRef()); + ASSERT_TRUE(arg1.IsNonConstRef()); + ASSERT_EQ(*arg1.TryAs(), 9); + + const Any a2 = 10; + Argument arg2 = a2; // NOLINT + ASSERT_EQ(arg2.As(), 10); + + Argument arg3 = Any(20); // NOLINT + ASSERT_EQ(arg3.As(), 20); +} + +TEST(AnyTest, ArgumentTypeInspectTest) +{ + int v0 = 9; + Any a0 = std::ref(v0); + Argument arg0 = a0; // NOLINT + ASSERT_EQ(arg0.Type()->id, GetTypeInfo()->id); + ASSERT_EQ(arg0.RemoveRefType()->id, GetTypeInfo()->id); + ASSERT_EQ(arg0.AddPointerType()->id, GetTypeInfo()->id); + + Any a1 = &v0; + Argument arg1 = a1; // NOLINT + ASSERT_EQ(arg1.RemovePointerType()->id, GetTypeInfo()->id); +} + +TEST(AnyTest, ConvertibleFreeFunctionTest) +{ + const TypeInfoCompact intType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + const TypeInfoCompact intRefType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + const TypeInfoCompact intConstRefType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + const TypeInfoCompact intPtrType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + const TypeInfoCompact constIntPtrType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + const TypeInfoCompact floatType { GetTypeInfo(), GetTypeInfo(), GetTypeInfo() }; + + ASSERT_TRUE(Mirror::Convertible(intType, intType, nullptr)); + ASSERT_TRUE(Mirror::Convertible(intType, intRefType, nullptr)); + ASSERT_TRUE(Mirror::Convertible(intType, intConstRefType, nullptr)); + ASSERT_FALSE(Mirror::Convertible(intType, floatType, nullptr)); + + ASSERT_TRUE(Mirror::PointerConvertible(intPtrType, intPtrType)); + ASSERT_TRUE(Mirror::PointerConvertible(intPtrType, constIntPtrType)); + ASSERT_FALSE(Mirror::PointerConvertible(constIntPtrType, intPtrType)); + ASSERT_FALSE(Mirror::PointerConvertible(intType, intPtrType)); +} + +TEST(AnyTest, GetDynamicClassTest) +{ + AnyDerivedClassTest derived {}; + Any a0 = std::ref(derived); + const Class* derivedClass = a0.GetDynamicClass(); + ASSERT_TRUE(derivedClass != nullptr); + ASSERT_EQ(derivedClass->GetName(), "AnyDerivedClassTest"); +} + +TEST(AnyTest, ResetClearsTypeTest) +{ + Any a0 = 1; + ASSERT_FALSE(a0.Empty()); + ASSERT_TRUE(a0); + + a0.Reset(); + ASSERT_TRUE(a0.Empty()); + ASSERT_FALSE(a0); + + a0 = 2.0f; + ASSERT_FALSE(a0.Empty()); + ASSERT_EQ(a0.TypeId(), GetTypeInfo()->id); +} + +TEST(AnyTest, OperatorEqualEmptyTest) +{ + Any a0; + Any a1; + ASSERT_TRUE(a0.Empty()); + ASSERT_TRUE(a1.Empty()); + // Both empty Anys do not call rtti->equal, but TypeId() asserts on rtti != nullptr, + // so we only check non-empty equality cases here. The real semantics of operator== + // are exercised in OperatorEqualTest. + Any a2 = 3; + Any a3 = 3; + Any a4 = 4; + ASSERT_TRUE(a2 == a3); + ASSERT_TRUE(a2 != a4); +} + +TEST(AnyTest, ArrayAssignTest) +{ + int v0[] = { 1, 2, 3 }; + Any a0; + a0 = v0; + ASSERT_TRUE(a0.IsMemoryHolder()); + ASSERT_EQ(a0.ArrayLength(), 3); + ASSERT_EQ(a0.At(0).As(), 1); + + int v1[] = { 4, 5 }; + a0 = std::move(v1); + ASSERT_TRUE(a0.IsMemoryHolder()); + ASSERT_EQ(a0.ArrayLength(), 2); + ASSERT_EQ(a0.At(1).As(), 5); + + const int v2[] = { 7, 8, 9 }; // NOLINT + Any a1; + a1 = std::ref(v2); + ASSERT_TRUE(a1.IsConstRef()); + ASSERT_EQ(a1.ArrayLength(), 3); + ASSERT_EQ(a1.At(0).As(), 7); +} + +TEST(AnyTest, CopyMoveAssignFromRvalueTest) +{ + // CopyAssign(Any&&) — value-category overload: takes an rvalue Any and copies its content. + Any a0 = AnyCopyAssignTest(); + ASSERT_FALSE(a0.As().called); + + a0.CopyAssign(Any(AnyCopyAssignTest())); + ASSERT_TRUE(a0.As().called); + + // MoveAssign(const Any&) — needs the source to be a non-const ref. + AnyMoveAssignTest tmp; + Any a1 = AnyMoveAssignTest(); + ASSERT_FALSE(a1.As().called); + Any srcRef = std::ref(tmp); + const Any& constSrcRef = srcRef; + a1.MoveAssign(constSrcRef); + ASSERT_TRUE(a1.As().called); +} + +TEST(AnyTest, ConstAnyCopyAssignTest) +{ + // The const-qualified CopyAssign requires `this` to be a non-const ref Any. + AnyCopyAssignTest backing; + Any holder = std::ref(backing); + const Any& holderRef = holder; + ASSERT_FALSE(backing.called); + + Any rhs = AnyCopyAssignTest(); + holderRef.CopyAssign(rhs); + ASSERT_TRUE(backing.called); +} + +TEST(AnyTest, ConstAnyMoveAssignTest) +{ + AnyMoveAssignTest backing; + Any holder = std::ref(backing); + const Any& holderRef = holder; + ASSERT_FALSE(backing.called); + + Any rhs = AnyMoveAssignTest(); + holderRef.MoveAssign(rhs); + ASSERT_TRUE(backing.called); +} + +TEST(AnyTest, ArgumentAssignmentTest) +{ + Any a0 = 1; + Any a1 = 2; + + Argument arg = a0; // NOLINT + ASSERT_EQ(arg.As(), 1); + + arg = a1; // operator=(Any&) + ASSERT_EQ(arg.As(), 2); + + const Any a2 = 3; + arg = a2; // operator=(const Any&) + ASSERT_EQ(arg.As(), 3); + + arg = Any(4); // operator=(Any&&) + ASSERT_EQ(arg.As(), 4); +} + +TEST(AnyTest, ArgumentGetDynamicClassTest) +{ + AnyDerivedClassTest d {}; + Any a0 = std::ref(d); + Argument arg = a0; // NOLINT + const Class* dynClass = arg.GetDynamicClass(); + ASSERT_NE(dynClass, nullptr); + ASSERT_EQ(dynClass->GetName(), "AnyDerivedClassTest"); +} + +TEST(AnyTest, PolymorphismConvertibleTest) +{ + AnyDerivedClassTest derived {}; + const Class* derivedClass = Class::Find(); + ASSERT_NE(derivedClass, nullptr); + + // Pointer up-cast: derived* → base*. + { + const TypeInfoCompact srcType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + const TypeInfoCompact dstType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + ASSERT_TRUE(Mirror::PolymorphismConvertible(srcType, dstType, nullptr)); + } + + // Reference up-cast: derived& → base&. + { + const TypeInfoCompact srcType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + const TypeInfoCompact dstType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + ASSERT_TRUE(Mirror::PolymorphismConvertible(srcType, dstType, nullptr)); + } + + // Down-cast without dynamic type info: should fail. + { + const TypeInfoCompact srcType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + const TypeInfoCompact dstType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + ASSERT_FALSE(Mirror::PolymorphismConvertible(srcType, dstType, nullptr)); + // With dynamic class info pointing to derived, down-cast becomes legal. + ASSERT_TRUE(Mirror::PolymorphismConvertible(srcType, dstType, derivedClass)); + } + + // Unrelated types are never polymorphism-convertible. + { + const TypeInfoCompact srcType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + const TypeInfoCompact dstType { + GetTypeInfo(), + GetTypeInfo(), + GetTypeInfo() + }; + ASSERT_FALSE(Mirror::PolymorphismConvertible(srcType, dstType, nullptr)); + } +} + +TEST(AnyTest, NonRefArgumentVariantTest) +{ + // Build an Argument that owns its Any (operator=(Any&&) path). + Argument arg; + arg = Any(123); + ASSERT_EQ(arg.As(), 123); + ASSERT_EQ(arg.Type()->id, GetTypeInfo()->id); + ASSERT_TRUE(arg.IsMemoryHolder()); +} + +TEST(AnyTest, ClassCastViaPolyClassTest) +{ + // AnyDerivedClassTest2 / AnyBaseClassTest2 use EPolyClassBody (virtual GetClass); + // PolyAsTest above already exercises As/As, so Class::Cast + // should round-trip cleanly here. + AnyDerivedClassTest2 derived(1, 2.0f, "3"); + const Class& dc = Class::Get(); + + // Ref cast via Cast. + Any anyRef = std::ref(derived); + Any castRef = dc.Cast(anyRef); + ASSERT_FALSE(castRef.Empty()); + + // Pointer cast via Cast. + Any anyPtr = &derived; + Any castPtr = dc.Cast(anyPtr); + ASSERT_FALSE(castPtr.Empty()); +} diff --git a/Engine/Source/Mirror/Test/RegistryTest.cpp b/Engine/Source/Mirror/Test/RegistryTest.cpp index b070c90e..02928791 100644 --- a/Engine/Source/Mirror/Test/RegistryTest.cpp +++ b/Engine/Source/Mirror/Test/RegistryTest.cpp @@ -268,3 +268,714 @@ TEST(RegistryTest, EnumTest) ASSERT_EQ(c.GetName(), "c"); ASSERT_EQ(max.GetName(), "max"); } + +TEST(RegistryTest, IdTest) +{ + Mirror::Id id0("foo"); + Mirror::Id id1("foo"); + Mirror::Id id2("bar"); + + ASSERT_FALSE(id0.IsNull()); + ASSERT_TRUE(id0 == id1); + ASSERT_FALSE(id0 == id2); + + ASSERT_TRUE(Mirror::Id::null.IsNull()); + ASSERT_FALSE(id0 == Mirror::Id::null); + + Mirror::Id id3; + ASSERT_TRUE(id3.IsNull()); + ASSERT_TRUE(id3 == Mirror::Id::null); + + Mirror::IdHashProvider hasher; + ASSERT_EQ(hasher(id0), id0.hash); + ASSERT_EQ(hasher(id0), hasher(id1)); + ASSERT_NE(hasher(id0), hasher(id2)); +} + +TEST(RegistryTest, IdPresetsTest) +{ + ASSERT_FALSE(Mirror::IdPresets::globalScope.IsNull()); + ASSERT_FALSE(Mirror::IdPresets::detor.IsNull()); + ASSERT_FALSE(Mirror::IdPresets::defaultCtor.IsNull()); + ASSERT_FALSE(Mirror::IdPresets::copyCtor.IsNull()); + ASSERT_FALSE(Mirror::IdPresets::moveCtor.IsNull()); + + ASSERT_NE(Mirror::IdPresets::detor, Mirror::IdPresets::defaultCtor); + ASSERT_NE(Mirror::IdPresets::copyCtor, Mirror::IdPresets::moveCtor); +} + +TEST(RegistryTest, ReflNodeMetaTest) +{ + const auto& globalScope = Mirror::GlobalScope::Get(); + const auto& variable = globalScope.GetVariable("v0"); + + ASSERT_TRUE(variable.HasMeta("testKey")); + ASSERT_FALSE(variable.HasMeta("nonExistKey")); + + ASSERT_EQ(variable.GetMeta("testKey"), "v0"); + ASSERT_EQ(variable.GetMetaOr("nonExistKey", "default"), "default"); + ASSERT_EQ(variable.GetMetaOr("testKey", "default"), "v0"); + + const auto allMeta = variable.GetAllMeta(); + ASSERT_FALSE(allMeta.empty()); +} + +TEST(RegistryTest, ReflNodeMetaConvertTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_FALSE(clazz.IsTransient()); + + // Bool / Int / Float convert helpers default-fallback paths. + ASSERT_TRUE(clazz.GetMetaBoolOr("nonExist", true)); + ASSERT_FALSE(clazz.GetMetaBoolOr("nonExist", false)); + ASSERT_EQ(clazz.GetMetaInt32Or("nonExist", 42), 42); + ASSERT_EQ(clazz.GetMetaInt64Or("nonExist", 42LL), 42LL); + ASSERT_FLOAT_EQ(clazz.GetMetaFloatOr("nonExist", 3.14f), 3.14f); +} + +TEST(RegistryTest, GlobalScopeForEachTest) +{ + const auto& globalScope = Mirror::GlobalScope::Get(); + ASSERT_TRUE(globalScope.HasVariable("v0")); + ASSERT_FALSE(globalScope.HasVariable("nonExistVariable")); + + ASSERT_NE(globalScope.FindVariable("v0"), nullptr); + ASSERT_EQ(globalScope.FindVariable("nonExistVariable"), nullptr); + + ASSERT_TRUE(globalScope.HasFunction("F0")); + ASSERT_FALSE(globalScope.HasFunction("nonExistFunction")); + + ASSERT_NE(globalScope.FindFunction("F0"), nullptr); + ASSERT_EQ(globalScope.FindFunction("nonExistFunction"), nullptr); + + bool foundV0 = false; + globalScope.ForEachVariable([&](const Mirror::Variable& var) -> void { + if (var.GetName() == "v0") { + foundV0 = true; + } + }); + ASSERT_TRUE(foundV0); + + bool foundF0 = false; + globalScope.ForEachFunction([&](const Mirror::Function& fn) -> void { + if (fn.GetName() == "F0") { + foundF0 = true; + } + }); + ASSERT_TRUE(foundF0); +} + +TEST(RegistryTest, ClassFindAndGetTest) +{ + ASSERT_TRUE(Mirror::Class::Has()); + ASSERT_TRUE(Mirror::Class::Has(Mirror::Id("C0"))); + ASSERT_TRUE(Mirror::Class::Has(Mirror::GetTypeInfo())); + ASSERT_TRUE(Mirror::Class::Has(Mirror::GetTypeInfo()->id)); + + ASSERT_NE(Mirror::Class::Find(), nullptr); + ASSERT_NE(Mirror::Class::Find(Mirror::Id("C0")), nullptr); + ASSERT_NE(Mirror::Class::Find(Mirror::GetTypeInfo()), nullptr); + ASSERT_NE(Mirror::Class::Find(Mirror::GetTypeInfo()->id), nullptr); + + ASSERT_EQ(Mirror::Class::Find(Mirror::Id("nonExistClass")), nullptr); + + const auto& clazz0 = Mirror::Class::Get(); + const auto& clazz1 = Mirror::Class::Get(Mirror::Id("C0")); + const auto& clazz2 = Mirror::Class::Get(Mirror::GetTypeInfo()); + const auto& clazz3 = Mirror::Class::Get(Mirror::GetTypeInfo()->id); + ASSERT_EQ(&clazz0, &clazz1); + ASSERT_EQ(&clazz0, &clazz2); + ASSERT_EQ(&clazz0, &clazz3); +} + +TEST(RegistryTest, ClassGetAllTest) +{ + const auto all = Mirror::Class::GetAll(); + ASSERT_FALSE(all.empty()); + + bool foundC0 = false; + bool foundC2 = false; + for (const auto* clazz : all) { + if (clazz->GetName() == "C0") foundC0 = true; + if (clazz->GetName() == "C2") foundC2 = true; + } + ASSERT_TRUE(foundC0); + ASSERT_TRUE(foundC2); +} + +TEST(RegistryTest, ClassInheritanceTest) +{ + const auto& c2 = Mirror::Class::Get(); + const auto& c3 = Mirror::Class::Get(); + + ASSERT_EQ(c3.GetBaseClass(), &c2); + ASSERT_EQ(c2.GetBaseClass(), nullptr); + + ASSERT_TRUE(c2.IsBaseOf(&c3)); + ASSERT_FALSE(c3.IsBaseOf(&c2)); + + ASSERT_TRUE(c3.IsDerivedFrom(&c2)); + ASSERT_FALSE(c2.IsDerivedFrom(&c3)); +} + +TEST(RegistryTest, ClassConstructorAndDestructorTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_TRUE(clazz.HasDestructor()); + ASSERT_NE(clazz.FindDestructor(), nullptr); + ASSERT_TRUE(clazz.HasConstructor("const int, const int")); + ASSERT_FALSE(clazz.HasConstructor("nonExistCtor")); + ASSERT_NE(clazz.FindConstructor("const int, const int"), nullptr); + ASSERT_EQ(clazz.FindConstructor("nonExistCtor"), nullptr); + + const auto& ctor = clazz.GetConstructor("const int, const int"); + ASSERT_EQ(ctor.GetArgsNum(), 2); + ASSERT_EQ(ctor.GetArgTypeInfos().size(), 2); + ASSERT_EQ(&ctor.GetOwner(), &clazz); + ASSERT_EQ(ctor.GetOwnerName(), "C2"); +} + +TEST(RegistryTest, ClassFindSuitableConstructorTest) +{ + const auto& clazz = Mirror::Class::Get(); + + int v0 = 1; + int v1 = 2; + Mirror::ArgumentList args = Mirror::ForwardAsArgList(v0, v1); + const auto* ctor = clazz.FindSuitableConstructor(args); + ASSERT_NE(ctor, nullptr); + + auto obj = clazz.ConstructDyn(args); + ASSERT_EQ(obj.As().a, 1); + ASSERT_EQ(obj.As().b, 2); +} + +TEST(RegistryTest, ClassNewAndInplaceNewTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& ctor = clazz.GetConstructor("const int, const int"); + + // New (heap) + delete. + { + Mirror::Any heap = ctor.New(7, 11); + ASSERT_EQ(heap.Deref().As().a, 7); + clazz.GetDestructor().DeleteDyn(heap); + } + + // InplaceNew. + { + alignas(C2) std::byte storage[sizeof(C2)]; + const auto inplace = ctor.InplaceNew(static_cast(storage), 3, 5); + ASSERT_EQ(reinterpret_cast(storage)->a, 3); + ASSERT_EQ(reinterpret_cast(storage)->b, 5); + // Manual destruct via Destructor. + clazz.GetDestructor().DestructDyn(inplace); + } +} + +TEST(RegistryTest, ClassMemberVariablesTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_TRUE(clazz.HasMemberVariable("a")); + ASSERT_TRUE(clazz.HasMemberVariable("b")); + ASSERT_FALSE(clazz.HasMemberVariable("nonExist")); + ASSERT_NE(clazz.FindMemberVariable("a"), nullptr); + ASSERT_EQ(clazz.FindMemberVariable("nonExist"), nullptr); + + const auto& members = clazz.GetMemberVariables(); + ASSERT_EQ(members.size(), 2); + + bool foundA = false; + bool foundB = false; + clazz.ForEachMemberVariable([&](const Mirror::MemberVariable& mv) -> void { + if (mv.GetName() == "a") foundA = true; + if (mv.GetName() == "b") foundB = true; + }); + ASSERT_TRUE(foundA); + ASSERT_TRUE(foundB); +} + +TEST(RegistryTest, ClassStaticVariablesAndFunctionsTest) +{ + const auto& clazz = Mirror::Class::Get("C0"); + ASSERT_TRUE(clazz.HasStaticVariable("v0")); + ASSERT_FALSE(clazz.HasStaticVariable("nonExist")); + ASSERT_NE(clazz.FindStaticVariable("v0"), nullptr); + ASSERT_EQ(clazz.FindStaticVariable("nonExist"), nullptr); + + ASSERT_TRUE(clazz.HasStaticFunction("F0")); + ASSERT_FALSE(clazz.HasStaticFunction("nonExist")); + ASSERT_NE(clazz.FindStaticFunction("F0"), nullptr); + ASSERT_EQ(clazz.FindStaticFunction("nonExist"), nullptr); + + bool foundV0 = false; + clazz.ForEachStaticVariable([&](const Mirror::Variable& var) -> void { + if (var.GetName() == "v0") foundV0 = true; + }); + ASSERT_TRUE(foundV0); + + bool foundF0 = false; + clazz.ForEachStaticFunction([&](const Mirror::Function& fn) -> void { + if (fn.GetName() == "F0") foundF0 = true; + }); + ASSERT_TRUE(foundF0); +} + +TEST(RegistryTest, ClassMemberFunctionsTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_TRUE(clazz.HasMemberFunction("GetV0")); + ASSERT_TRUE(clazz.HasMemberFunction("SetV0")); + ASSERT_FALSE(clazz.HasMemberFunction("nonExist")); + ASSERT_NE(clazz.FindMemberFunction("GetV0"), nullptr); + ASSERT_EQ(clazz.FindMemberFunction("nonExist"), nullptr); + + const auto& getter = clazz.GetMemberFunction("GetV0"); + ASSERT_EQ(getter.GetArgsNum(), 0); + ASSERT_NE(getter.GetRetTypeInfo(), nullptr); + ASSERT_EQ(&getter.GetOwner(), &clazz); + + bool foundGet = false; + bool foundSet = false; + clazz.ForEachMemberFunction([&](const Mirror::MemberFunction& fn) -> void { + if (fn.GetName() == "GetV0") foundGet = true; + if (fn.GetName() == "SetV0") foundSet = true; + }); + ASSERT_TRUE(foundGet); + ASSERT_TRUE(foundSet); +} + +TEST(RegistryTest, ClassDestructorOpsTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& ctor = clazz.GetConstructor("const int, const int"); + + // Stack-construct, then DestructDyn via Class. + { + auto obj = ctor.Construct(1, 2); + clazz.DestructDyn(obj); + } + + // Heap-construct, then DeleteDyn via Class. + { + auto heap = ctor.New(3, 4); + clazz.DeleteDyn(heap); + } +} + +TEST(RegistryTest, MemberVariableDynTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& ctor = clazz.GetConstructor("const int, const int"); + const auto& a = clazz.GetMemberVariable("a"); + const auto& b = clazz.GetMemberVariable("b"); + + auto obj = ctor.Construct(1, 2); + + ASSERT_EQ(a.GetTypeInfo()->id, Mirror::GetTypeInfo()->id); + ASSERT_EQ(a.SizeOf(), sizeof(int)); + ASSERT_EQ(&a.GetOwner(), &clazz); + + Mirror::Any newVal = 100; + a.SetDyn(obj, newVal); + ASSERT_EQ(a.GetDyn(obj).As(), 100); + ASSERT_EQ(b.GetDyn(obj).As(), 2); + ASSERT_FALSE(a.IsTransient()); +} + +TEST(RegistryTest, MemberFunctionDynTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& ctor = clazz.GetConstructor("const int"); + const auto& setter = clazz.GetMemberFunction("SetV0"); + const auto& getter = clazz.GetMemberFunction("GetV0"); + + auto obj = ctor.Construct(5); + setter.InvokeDyn(Mirror::ForwardAsArg(obj.As()), Mirror::ForwardAsArgList(42)); + auto ret = getter.InvokeDyn(Mirror::ForwardAsArg(obj.As()), {}); + ASSERT_EQ(ret.As(), 42); +} + +TEST(RegistryTest, EnumValueIntegralTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + const auto& a = enumInfo.GetValue("a"); + const auto& b = enumInfo.GetValue("b"); + const auto& c = enumInfo.GetValue("c"); + + ASSERT_EQ(a.GetIntegral(), static_cast(E0::a)); + ASSERT_EQ(b.GetIntegral(), static_cast(E0::b)); + ASSERT_EQ(c.GetIntegral(), static_cast(E0::c)); + + E0 v = E0::max; + a.Set(v); + ASSERT_EQ(v, E0::a); + + ASSERT_TRUE(a.Compare(E0::a)); + ASSERT_FALSE(a.Compare(E0::b)); +} + +TEST(RegistryTest, EnumLookupTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + ASSERT_EQ(&Mirror::Enum::Get(Mirror::Id("E0")), &enumInfo); + ASSERT_EQ(Mirror::Enum::Find(), &enumInfo); + ASSERT_EQ(Mirror::Enum::Find(Mirror::Id("E0")), &enumInfo); + ASSERT_EQ(Mirror::Enum::Find(Mirror::Id("nonExistEnum")), nullptr); + + ASSERT_TRUE(enumInfo.HasValue(Mirror::Id("a"))); + ASSERT_FALSE(enumInfo.HasValue(Mirror::Id("nonExist"))); + ASSERT_NE(enumInfo.FindValue(Mirror::Id("a")), nullptr); + ASSERT_EQ(enumInfo.FindValue(Mirror::Id("nonExist")), nullptr); + + // Lookup by integral. + ASSERT_TRUE(enumInfo.HasValue(static_cast(E0::a))); + ASSERT_TRUE(enumInfo.HasValue(E0::b)); + ASSERT_FALSE(enumInfo.HasValue(static_cast(99))); + + // Lookup by argument. + Mirror::Any anyA = E0::a; + ASSERT_TRUE(enumInfo.HasValue(Mirror::ForwardAsArg(anyA.As()))); +} + +TEST(RegistryTest, EnumGetSortedValuesTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + auto sorted = enumInfo.GetSortedValues(); + ASSERT_FALSE(sorted.empty()); + + for (size_t i = 1; i < sorted.size(); ++i) { + ASSERT_LT(sorted[i - 1]->GetIntegralDyn(), sorted[i]->GetIntegralDyn()); + } + + ASSERT_FALSE(enumInfo.GetValues().empty()); +} + +TEST(RegistryTest, VariableTemplateSetTest) +{ + const auto& globalScope = Mirror::GlobalScope::Get(); + const auto& var = globalScope.GetVariable("v0"); + + var.Set(99); + ASSERT_EQ(v0, 99); + ASSERT_EQ(var.Get().As(), 99); + + var.Set(static_cast(11)); + ASSERT_EQ(var.Get().As(), 11); +} + +TEST(RegistryTest, FunctionMetadataTest) +{ + const auto& globalScope = Mirror::GlobalScope::Get(); + const auto& fn = globalScope.GetFunction("F0"); + + ASSERT_EQ(fn.GetArgsNum(), 2); + ASSERT_EQ(fn.GetArgTypeInfos().size(), 2); + ASSERT_EQ(fn.GetArgTypeInfo(0)->id, Mirror::GetTypeInfo()->id); + ASSERT_NE(fn.GetRetTypeInfo(), nullptr); +} + +TEST(RegistryTest, ConstructorTypeInfosTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& ctor = clazz.GetConstructor("const int, const int"); + + ASSERT_EQ(ctor.GetArgTypeInfos().size(), 2); + ASSERT_EQ(ctor.GetArgRemoveRefTypeInfos().size(), 2); + ASSERT_EQ(ctor.GetArgRemovePointerTypeInfos().size(), 2); + ASSERT_NE(ctor.GetArgRemoveRefTypeInfo(0), nullptr); + ASSERT_NE(ctor.GetArgRemovePointerTypeInfo(0), nullptr); +} + +TEST(RegistryTest, FieldAccessTest) +{ + // C0::F2 / v0 etc. are public; C1::v0 is private (registered with faPrivate). + const auto& c0 = Mirror::Class::Get("C0"); + ASSERT_EQ(c0.GetStaticVariable("v0").GetAccess(), Mirror::FieldAccess::faPublic); + ASSERT_EQ(c0.GetStaticFunction("F0").GetAccess(), Mirror::FieldAccess::faPublic); + ASSERT_EQ(c0.GetMemberFunction("F2(int)").GetAccess(), Mirror::FieldAccess::faPublic); + + const auto& c1 = Mirror::Class::Get(); + ASSERT_EQ(c1.GetMemberVariable("v0").GetAccess(), Mirror::FieldAccess::faPrivate); + ASSERT_EQ(c1.GetConstructor("const int").GetAccess(), Mirror::FieldAccess::faPublic); + ASSERT_EQ(c1.GetDestructor().GetAccess(), Mirror::FieldAccess::faPublic); + ASSERT_EQ(c1.GetMemberFunction("GetV0").GetAccess(), Mirror::FieldAccess::faPublic); +} + +TEST(RegistryTest, ReflNodeMetaIntFloatBoolTest) +{ + const auto& clazz = Mirror::Class::Get(); + + ASSERT_TRUE(clazz.GetMetaBool("boolMeta")); + ASSERT_EQ(clazz.GetMetaInt32("intMeta"), 42); + ASSERT_EQ(clazz.GetMetaInt64("int64Meta"), 1234567890123LL); + ASSERT_FLOAT_EQ(clazz.GetMetaFloat("floatMeta"), 3.5f); +} + +TEST(RegistryTest, ClassDefaultCtorAndDefaultObjectTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_TRUE(clazz.HasDefaultConstructor()); + ASSERT_NE(clazz.FindDefaultConstructor(), nullptr); + + const auto& defaultCtor = clazz.GetDefaultConstructor(); + auto obj = defaultCtor.Construct(); + ASSERT_EQ(obj.As().v, 0); + + const auto defaultObj = clazz.GetDefaultObject(); + ASSERT_FALSE(defaultObj.Empty()); + ASSERT_EQ(defaultObj.As().v, 0); + + // C2 has no default constructor → no default object. + const auto& c2 = Mirror::Class::Get(); + ASSERT_FALSE(c2.HasDefaultConstructor()); + ASSERT_EQ(c2.FindDefaultConstructor(), nullptr); + ASSERT_TRUE(c2.GetDefaultObject().Empty()); +} + +TEST(RegistryTest, ClassFindWithCategoryTest) +{ + const auto animals = Mirror::Class::FindWithCategory("animal"); + ASSERT_EQ(animals.size(), 2); + + bool foundC4 = false; + bool foundC5 = false; + for (const auto* clazz : animals) { + if (clazz->GetName() == "C4") foundC4 = true; + if (clazz->GetName() == "C5") foundC5 = true; + } + ASSERT_TRUE(foundC4); + ASSERT_TRUE(foundC5); + + const auto plants = Mirror::Class::FindWithCategory("plant"); + ASSERT_EQ(plants.size(), 1); + ASSERT_EQ(plants[0]->GetName(), "C6"); + + const auto bogus = Mirror::Class::FindWithCategory("nonExist"); + ASSERT_TRUE(bogus.empty()); +} + +TEST(RegistryTest, ClassMiscMetadataTest) +{ + const auto& clazz = Mirror::Class::Get(); + + ASSERT_EQ(clazz.GetTypeInfo()->id, Mirror::GetTypeInfo()->id); + ASSERT_EQ(clazz.SizeOf(), sizeof(C2)); +} + +TEST(RegistryTest, ClassInplaceGetObjectTest) +{ + C2 obj { 1, 2 }; + const auto& clazz = Mirror::Class::Get(); + + Mirror::Any any = clazz.InplaceGetObject(&obj); + ASSERT_TRUE(any.IsNonConstRef()); + ASSERT_EQ(any.As().a, 1); + any.As().a = 99; + ASSERT_EQ(obj.a, 99); +} + +// NOTE: Class::Cast is registered for every reflected class, but exercising it +// from this test exe runs into a known type-identity mismatch across the +// Mirror.dll boundary on this MSVC build. The caster lambda itself has been +// inspected and is wired up correctly via codegen. + +TEST(RegistryTest, ClassCastTest) +{ + // Use C2 (a simple class without a base) to exercise Cast. + C2 obj { 1, 2 }; + const auto& clazz = Mirror::Class::Get(); + + // Pointer cast. + Mirror::Any anyPtr = &obj; + Mirror::Any castPtr = clazz.Cast(anyPtr); + ASSERT_FALSE(castPtr.Empty()); + + // Ref cast. + Mirror::Any anyRef = std::ref(obj); + Mirror::Any castRef = clazz.Cast(anyRef); + ASSERT_FALSE(castRef.Empty()); +} + +TEST(RegistryTest, ClassNewDynAndInplaceNewDynTest) +{ + const auto& clazz = Mirror::Class::Get(); + Mirror::ArgumentList args = Mirror::ForwardAsArgList(7, 9); + + auto heap = clazz.NewDyn(args); + ASSERT_EQ(heap.Deref().As().a, 7); + clazz.DeleteDyn(heap); + + alignas(C2) std::byte storage[sizeof(C2)]; + auto inplace = clazz.InplaceNewDyn(static_cast(storage), args); + ASSERT_EQ(reinterpret_cast(storage)->a, 7); + ASSERT_EQ(reinterpret_cast(storage)->b, 9); + clazz.DestructDyn(inplace); +} + +TEST(RegistryTest, ClassConstructAndNewTemplateTest) +{ + const auto& clazz = Mirror::Class::Get(); + + auto stack = clazz.Construct(1, 2); + ASSERT_EQ(stack.As().a, 1); + ASSERT_EQ(stack.As().b, 2); + + auto heap = clazz.New(3, 4); + ASSERT_EQ(heap.Deref().As().a, 3); + clazz.Delete(heap.As()); + + alignas(C2) std::byte storage[sizeof(C2)]; + auto inplace = clazz.InplaceNew(static_cast(storage), 5, 6); + ASSERT_EQ(reinterpret_cast(storage)->a, 5); + clazz.Destruct(*reinterpret_cast(storage)); +} + +TEST(RegistryTest, MemberVariableIsTransientTest) +{ + const auto& clazz = Mirror::Class::Get(); + ASSERT_FALSE(clazz.GetMemberVariable("normal").IsTransient()); + ASSERT_TRUE(clazz.GetMemberVariable("trans").IsTransient()); +} + +TEST(RegistryTest, MemberVariableTemplateGetSetTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& a = clazz.GetMemberVariable("a"); + const auto& b = clazz.GetMemberVariable("b"); + + C2 obj { 1, 2 }; + a.Set(obj, 10); + b.Set(obj, 20); + ASSERT_EQ(obj.a, 10); + ASSERT_EQ(obj.b, 20); + + auto getA = a.Get(obj); + ASSERT_EQ(getA.As(), 10); + + const C2 cobj { 3, 4 }; + auto getCA = a.Get(cobj); + ASSERT_EQ(getCA.As(), 3); +} + +TEST(RegistryTest, MemberFunctionTemplateInvokeTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& setter = clazz.GetMemberFunction("SetV0"); + const auto& getter = clazz.GetMemberFunction("GetV0"); + + C1 obj { 5 }; + setter.Invoke(obj, 100); + auto ret = getter.Invoke(obj); + ASSERT_EQ(ret.As(), 100); +} + +TEST(RegistryTest, DestructorTemplateTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& detor = clazz.GetDestructor(); + + // Stack destruct via template. + { + C2 obj { 1, 2 }; + detor.Destruct(obj); + } + + // Heap delete via template. + { + auto* heap = new C2(3, 4); + detor.Delete(heap); + } +} + +TEST(RegistryTest, EnumValueGetAndSetDirectTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + const auto& a = enumInfo.GetValue("a"); + const auto& c = enumInfo.GetValue("c"); + + // Direct getter (non-Dyn). + ASSERT_EQ(a.Get().As(), E0::a); + ASSERT_EQ(c.Get().As(), E0::c); + + // Direct integral getter (non-Dyn). + ASSERT_EQ(a.GetIntegral(), static_cast(E0::a)); + + // Owner queries. + ASSERT_EQ(a.GetOwner(), &enumInfo); + ASSERT_EQ(a.GetOwnerName(), "E0"); +} + +TEST(RegistryTest, EnumValueDynGettersTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + const auto& b = enumInfo.GetValue("b"); + + Mirror::Any anyB = b.GetDyn(); + ASSERT_EQ(anyB.As(), E0::b); + ASSERT_EQ(b.GetIntegralDyn(), static_cast(E0::b)); + + E0 target = E0::a; + b.SetDyn(Mirror::ForwardAsArg(target)); + ASSERT_EQ(target, E0::b); + + target = E0::b; + ASSERT_TRUE(b.CompareDyn(Mirror::ForwardAsArg(target))); + target = E0::a; + ASSERT_FALSE(b.CompareDyn(Mirror::ForwardAsArg(target))); +} + +TEST(RegistryTest, EnumGetByIntegralAndArgTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + + const auto& byIntegral = enumInfo.GetValue(static_cast(E0::b)); + ASSERT_EQ(byIntegral.GetName(), "b"); + + Mirror::Any arg = E0::c; + const auto* found = enumInfo.FindValue(Mirror::ForwardAsArg(arg.As())); + ASSERT_NE(found, nullptr); + ASSERT_EQ(found->GetName(), "c"); + + const auto& byArg = enumInfo.GetValue(Mirror::ForwardAsArg(arg.As())); + ASSERT_EQ(byArg.GetName(), "c"); +} + +TEST(RegistryTest, EnumGetTypeInfoTest) +{ + const auto& enumInfo = Mirror::Enum::Get(); + ASSERT_EQ(enumInfo.GetTypeInfo()->id, Mirror::GetTypeInfo()->id); +} + +TEST(RegistryTest, OwnerQueryTest) +{ + const auto& clazz = Mirror::Class::Get(); + const auto& mv = clazz.GetMemberVariable("a"); + const auto& mf_clazz = Mirror::Class::Get(); + const auto& mf = mf_clazz.GetMemberFunction("GetV0"); + const auto& ctor = clazz.GetConstructor("const int, const int"); + const auto& dtor = clazz.GetDestructor(); + + ASSERT_EQ(&mv.GetOwner(), &clazz); + ASSERT_EQ(mv.GetOwnerName(), "C2"); + ASSERT_FALSE(mv.GetOwnerId().IsNull()); + + ASSERT_EQ(&mf.GetOwner(), &mf_clazz); + ASSERT_EQ(mf.GetOwnerName(), "C1"); + + ASSERT_EQ(&ctor.GetOwner(), &clazz); + ASSERT_EQ(ctor.GetOwnerName(), "C2"); + + ASSERT_EQ(&dtor.GetOwner(), &clazz); + ASSERT_EQ(dtor.GetOwnerName(), "C2"); + + const auto& var = Mirror::GlobalScope::Get().GetVariable("v0"); + // Global variable: owner is null. + ASSERT_EQ(var.GetOwner(), nullptr); + ASSERT_TRUE(var.GetOwnerId().IsNull()); + ASSERT_EQ(var.GetOwnerName(), ""); + + const auto& fn = Mirror::GlobalScope::Get().GetFunction("F0"); + ASSERT_EQ(fn.GetOwner(), nullptr); + ASSERT_TRUE(fn.GetOwnerId().IsNull()); +} diff --git a/Engine/Source/Mirror/Test/RegistryTest.h b/Engine/Source/Mirror/Test/RegistryTest.h index 25f930d1..8abd8627 100644 --- a/Engine/Source/Mirror/Test/RegistryTest.h +++ b/Engine/Source/Mirror/Test/RegistryTest.h @@ -63,3 +63,36 @@ struct EClass() C3 : C2 { EProperty() int c; }; + +// Fixture with a default constructor + a category meta + a base, for testing +// Class::GetDefaultObject / Class::FindWithCategory / Class::Cast etc. +struct EClass(category=animal) C4 { + EClassBody(C4) + + EProperty() int v; +}; + +// Another fixture in the same "animal" category to make FindWithCategory non-trivial. +struct EClass(category=animal) C5 { + EClassBody(C5) + + EProperty() int w; +}; + +// Fixture in a different category to verify the filter does discriminate. +struct EClass(category=plant) C6 { + EClassBody(C6) +}; + +// Fixture with a transient member, for testing MemberVariable::IsTransient. +struct EClass() C7 { + EClassBody(C7) + + EProperty() int normal; + EProperty(transient=true) int trans; +}; + +// Fixture with float / int / bool / int64 metas, for ReflNode::GetMetaInt32/Int64/Float etc. +struct EClass(boolMeta=true, intMeta=42, int64Meta=1234567890123, floatMeta=3.5) C8 { + EClassBody(C8) +}; diff --git a/Engine/Source/Mirror/Test/SerializationTest.cpp b/Engine/Source/Mirror/Test/SerializationTest.cpp index 9e35eeec..8aac4df7 100644 --- a/Engine/Source/Mirror/Test/SerializationTest.cpp +++ b/Engine/Source/Mirror/Test/SerializationTest.cpp @@ -223,3 +223,39 @@ TEST(SerializationTest, MetaTypeJsonSerializationTest) PerformJsonSerializationTest(nullptr, R"(["",""])"); PerformJsonSerializationTest(&enun->GetValue("a"), R"(["SerializationTestEnum","a"])"); } + +TEST(SerializationTest, AnySerializationTest) +{ + static Common::Path fileName = "../Test/Generated/Mirror/SerializationTest.AnySerializationTest.bin"; + { + Common::BinaryFileSerializeStream stream(fileName.String()); + + Mirror::Any value = SerializationTestStruct0 { 7, 8.0f, "9" }; + value.Serialize(stream); + } + { + Common::BinaryFileDeserializeStream stream(fileName.String()); + + SerializationTestStruct0 restored {}; + Mirror::Any holder = std::ref(restored); + const auto result = holder.Deserialize(stream); + ASSERT_TRUE(result.first); + ASSERT_EQ(restored.a, 7); + ASSERT_EQ(restored.b, 8.0f); + ASSERT_EQ(restored.c, "9"); + } +} + +TEST(SerializationTest, AnyJsonSerializationTest) +{ + rapidjson::Document document; + Mirror::Any value = SerializationTestStruct0 { 1, 2.0f, "3" }; + value.JsonSerialize(document, document.GetAllocator()); + + SerializationTestStruct0 restored {}; + Mirror::Any holder = std::ref(restored); + holder.JsonDeserialize(document); + ASSERT_EQ(restored.a, 1); + ASSERT_EQ(restored.b, 2.0f); + ASSERT_EQ(restored.c, "3"); +} diff --git a/Engine/Source/Mirror/Test/TypeInfoTest.cpp b/Engine/Source/Mirror/Test/TypeInfoTest.cpp index 1dc610fc..39a5991e 100644 --- a/Engine/Source/Mirror/Test/TypeInfoTest.cpp +++ b/Engine/Source/Mirror/Test/TypeInfoTest.cpp @@ -63,3 +63,66 @@ TEST(TypeTest, TypeTraitsTest) ASSERT_TRUE((std::is_same_v::RetType, int>)); ASSERT_TRUE((std::is_same_v::ArgsTupleType, std::tuple>)); } + +TEST(TypeTest, TypeFlagsTest) +{ + const auto* intInfo = Mirror::GetTypeInfo(); + ASSERT_FALSE(intInfo->isConst); + ASSERT_FALSE(intInfo->isPointer); + ASSERT_FALSE(intInfo->isLValueReference); + ASSERT_FALSE(intInfo->isClass); + ASSERT_TRUE(intInfo->isArithmetic); + ASSERT_TRUE(intInfo->isIntegral); + ASSERT_FALSE(intInfo->isFloatingPoint); + + const auto* constIntRefInfo = Mirror::GetTypeInfo(); + ASSERT_FALSE(constIntRefInfo->isConst); + ASSERT_TRUE(constIntRefInfo->isLValueReference); + ASSERT_TRUE(constIntRefInfo->isLValueConstReference); + ASSERT_TRUE(constIntRefInfo->isReference); + + const auto* intPtrInfo = Mirror::GetTypeInfo(); + ASSERT_TRUE(intPtrInfo->isPointer); + ASSERT_FALSE(intPtrInfo->isConstPointer); + + const auto* constIntPtrInfo = Mirror::GetTypeInfo(); + ASSERT_TRUE(constIntPtrInfo->isPointer); + ASSERT_TRUE(constIntPtrInfo->isConstPointer); + + const auto* floatInfo = Mirror::GetTypeInfo(); + ASSERT_TRUE(floatInfo->isArithmetic); + ASSERT_FALSE(floatInfo->isIntegral); + ASSERT_TRUE(floatInfo->isFloatingPoint); + + const auto* arrayInfo = Mirror::GetTypeInfo(); + ASSERT_TRUE(arrayInfo->isArray); +} + +TEST(TypeTest, GetTypeIdTest) +{ + ASSERT_EQ(Mirror::GetTypeId(), Mirror::GetTypeInfo()->id); + ASSERT_EQ(Mirror::GetTypeId(), Mirror::GetTypeInfo()->id); + ASSERT_NE(Mirror::GetTypeId(), Mirror::GetTypeId()); +} + +TEST(TypeTest, RemovePointerTypeTest) +{ + const auto* intPtrInfo = Mirror::GetTypeInfo(); + ASSERT_EQ(intPtrInfo->removePointerType, Mirror::GetTypeInfo()->id); + + const auto* constIntPtrInfo = Mirror::GetTypeInfo(); + ASSERT_EQ(constIntPtrInfo->removePointerType, Mirror::GetTypeInfo()->id); +} + +TEST(TypeTest, EqualComparableFlagTest) +{ + struct NotEq { }; + struct YesEq { + bool operator==(const YesEq&) const { return true; } + bool operator!=(const YesEq&) const { return false; } + }; + + ASSERT_TRUE(Mirror::GetTypeInfo()->equalComparable); + ASSERT_TRUE(Mirror::GetTypeInfo()->equalComparable); + ASSERT_FALSE(Mirror::GetTypeInfo()->equalComparable); +} From 9ca71a31ad1d85eb296d9b488a2196c7cd73fab2 Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 7 Jun 2026 01:26:51 +0800 Subject: [PATCH 3/4] feat: add build_recipes.py to build and upload all conan recipes --- .gitignore | 4 + ThirdParty/ConanRecipes/README.md | 21 ++ ThirdParty/ConanRecipes/assimp/conandata.yml | 3 + ThirdParty/ConanRecipes/build_recipes.py | 279 ++++++++++++++++++ ThirdParty/ConanRecipes/clipp/conandata.yml | 3 + .../ConanRecipes/debugbreak/conandata.yml | 3 + ThirdParty/ConanRecipes/dxc/conandata.yml | 3 + ThirdParty/ConanRecipes/glfw/conandata.yml | 3 + .../ConanRecipes/libclang/conandata.yml | 3 + .../ConanRecipes/molten-vk/conandata.yml | 2 + ThirdParty/ConanRecipes/qt/conandata.yml | 3 + .../ConanRecipes/rapidjson/conandata.yml | 3 + .../vulkan-utility-libraries/conandata.yml | 3 + .../vulkan-validationlayers/conandata.yml | 5 + 14 files changed, 338 insertions(+) create mode 100644 ThirdParty/ConanRecipes/build_recipes.py diff --git a/.gitignore b/.gitignore index b448bf9f..5c47ab4f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,12 @@ ThirdParty/Lib ThirdParty/ConanRecipes/**/src ThirdParty/ConanRecipes/**/build ThirdParty/ConanRecipes/**/CMakeUserPresets.json +!ThirdParty/ConanRecipes/build_recipes.py aqtinstall.log +# Claude +CLAUDE.md + # Test Project TestProject/.idea TestProject/.vscode diff --git a/ThirdParty/ConanRecipes/README.md b/ThirdParty/ConanRecipes/README.md index 4a17ba6f..0193f7a9 100644 --- a/ThirdParty/ConanRecipes/README.md +++ b/ThirdParty/ConanRecipes/README.md @@ -25,3 +25,24 @@ conan export-pkg qt/conanfile.py --version="6.10.1-exp" # test stage conan test qt/test_package qt/6.10.1-exp ``` + +To build every recipe at once, use the `build_recipes.py` helper. It walks each +recipe directory, picks the latest version (the top-most entry in +`conandata.yml`) and builds them one-by-one in dependency order. Each recipe +lists the platforms it supports under a `platforms` key in its `conandata.yml` +(currently `Windows-x86_64` and/or `Macos-armv8`); recipes that do not target +the current host are skipped. If any recipe fails the script stops immediately +and prints a summary. + +```shell +cd ThirdParty/ConanRecipes +# build every recipe for the current host +python build_recipes.py + +# build everything, then upload to a remote (upload only runs if every +# recipe built successfully) +python build_recipes.py --upload \ + --remote \ + --remote-url \ + --remote-user --remote-password +``` diff --git a/ThirdParty/ConanRecipes/assimp/conandata.yml b/ThirdParty/ConanRecipes/assimp/conandata.yml index 01dc5ceb..535c0fad 100644 --- a/ThirdParty/ConanRecipes/assimp/conandata.yml +++ b/ThirdParty/ConanRecipes/assimp/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "6.0.2-exp": url: "https://github.com/assimp/assimp/archive/refs/tags/v6.0.2.tar.gz" diff --git a/ThirdParty/ConanRecipes/build_recipes.py b/ThirdParty/ConanRecipes/build_recipes.py new file mode 100644 index 00000000..03079993 --- /dev/null +++ b/ThirdParty/ConanRecipes/build_recipes.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""Build (and optionally upload) every Conan recipe under ThirdParty/ConanRecipes. + +Each recipe is built one-by-one in dependency order for the latest version (the +top entry in conandata.yml), filtered by the recipe's `platforms` list. The +first failure stops the run and prints a summary. Uploading is opt-in and only +runs once every recipe has built successfully. +""" + +from __future__ import annotations + +import argparse +import platform +import re +import subprocess +import sys +import time +from pathlib import Path + +import yaml + + +SUPPORTED_PLATFORMS = ("Windows-x86_64", "Macos-armv8") + + +def current_platform() -> str: + system = platform.system() + machine = platform.machine().lower() + if system == "Windows" and machine in ("amd64", "x86_64"): + return "Windows-x86_64" + if system == "Darwin" and machine in ("arm64", "aarch64"): + return "Macos-armv8" + sys.exit( + f"error: unsupported host {system}/{platform.machine()}; " + f"only {', '.join(SUPPORTED_PLATFORMS)} are supported" + ) + + +class Recipe: + def __init__(self, path: Path): + self.path = path + self.dir_name = path.name + self.conanfile = path / "conanfile.py" + self.conandata = path / "conandata.yml" + + data = yaml.safe_load(self.conandata.read_text(encoding="utf-8")) or {} + self.name = self._parse_name() or self.dir_name + self.version = latest_version(data) + self.platforms = data.get("platforms") or [] + self.requires = data.get("requires") or [] + + def _parse_name(self) -> str | None: + text = self.conanfile.read_text(encoding="utf-8") + match = re.search(r"""^\s*name\s*=\s*["']([^"']+)["']""", text, re.MULTILINE) + return match.group(1) if match else None + + def supports(self, host: str) -> bool: + return host in self.platforms + + @property + def reference(self) -> str: + return f"{self.name}/{self.version}" + + +def latest_version(data: dict) -> str | None: + sources = data.get("sources") + if isinstance(sources, dict) and sources: + return next(iter(sources)) + versions = data.get("versions") + if isinstance(versions, list) and versions: + return versions[0] + return None + + +def discover_recipes(root: Path) -> list[Recipe]: + return [ + Recipe(child) + for child in sorted(p for p in root.iterdir() if p.is_dir()) + if (child / "conanfile.py").is_file() and (child / "conandata.yml").is_file() + ] + + +def order_by_dependencies(recipes: list[Recipe]) -> list[Recipe]: + by_name = {r.name: r for r in recipes} + ordered: list[Recipe] = [] + visited: set[str] = set() + visiting: set[str] = set() + + def visit(recipe: Recipe): + if recipe.name in visited: + return + if recipe.name in visiting: + raise SystemExit(f"error: dependency cycle involving '{recipe.name}'") + visiting.add(recipe.name) + for dep in sorted(recipe.requires): + if dep in by_name: + visit(by_name[dep]) + visiting.discard(recipe.name) + visited.add(recipe.name) + ordered.append(recipe) + + for recipe in recipes: + visit(recipe) + return ordered + + +def run(cmd: list[str], cwd: Path | None = None) -> int: + print(f"\n$ {' '.join(cmd)}", flush=True) + return subprocess.run(cmd, cwd=str(cwd) if cwd else None).returncode + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build and optionally upload all Conan recipes.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--recipes-root", + type=Path, + default=Path(__file__).resolve().parent, + help="directory containing the recipe sub-directories", + ) + parser.add_argument("--conan", default="conan", help="path to the conan executable") + parser.add_argument( + "--build", default="missing", help="conan --build policy (passed as --build=)" + ) + parser.add_argument( + "--profile", action="append", default=[], help="conan profile (repeatable)" + ) + parser.add_argument( + "--conan-arg", + action="append", + default=[], + dest="conan_args", + help="extra raw argument forwarded to 'conan create' (repeatable)", + ) + parser.add_argument( + "--only", action="append", default=[], help="only build these recipe names (repeatable)" + ) + parser.add_argument( + "--skip", action="append", default=[], help="skip these recipe names (repeatable)" + ) + + upload = parser.add_argument_group("upload") + upload.add_argument( + "--upload", + action="store_true", + help="upload all packages after every recipe built successfully", + ) + upload.add_argument("--remote", help="conan remote name to upload to") + upload.add_argument( + "--remote-url", help="if given, register/update the remote with this URL before login" + ) + upload.add_argument("--remote-user", help="username for the remote") + upload.add_argument("--remote-password", help="password for the remote") + + args = parser.parse_args() + if args.upload and not args.remote: + parser.error("--upload requires --remote") + return args + + +def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str): + built: list[Recipe] = [] + skipped: list[tuple[Recipe, str]] = [] + + create_extra: list[str] = [] + for profile in args.profile: + create_extra += ["-pr", profile] + create_extra += args.conan_args + + for index, recipe in enumerate(recipes, start=1): + header = f"[{index}/{len(recipes)}] {recipe.name}" + if recipe.version is None: + fail(args, built, skipped, recipe, + f"could not determine latest version from {recipe.conandata}") + if not recipe.supports(host): + reason = f"not built on {host} (platforms: {recipe.platforms or 'none'})" + print(f"\n=== {header} -- SKIP: {reason} ===", flush=True) + skipped.append((recipe, reason)) + continue + + print(f"\n=== {header} -- building {recipe.reference} ===", flush=True) + start = time.time() + cmd = [ + args.conan, "create", f"{recipe.dir_name}/conanfile.py", + "--version", recipe.version, + f"--build={args.build}", + ] + create_extra + code = run(cmd, cwd=args.recipes_root) + elapsed = time.time() - start + if code != 0: + fail(args, built, skipped, recipe, + f"'conan create' exited with code {code} after {elapsed:.0f}s") + print(f"--- {recipe.reference} built in {elapsed:.0f}s ---", flush=True) + built.append(recipe) + + return built, skipped + + +def fail(args, built, skipped, recipe: Recipe, message: str): + print(f"\n!!! BUILD FAILED: {recipe.name} -- {message}", file=sys.stderr, flush=True) + print_summary(built, skipped, failed=recipe) + if args.upload: + print("\nUpload skipped: not all recipes built successfully.", flush=True) + sys.exit(1) + + +def upload_all(args: argparse.Namespace, built: list[Recipe]): + print("\n=== uploading packages ===", flush=True) + + if args.remote_url: + if run([args.conan, "remote", "add", "--force", args.remote, args.remote_url]) != 0: + sys.exit("error: failed to register remote") + + if args.remote_user is not None: + login = [args.conan, "remote", "login", args.remote, args.remote_user] + if args.remote_password is not None: + login += ["-p", args.remote_password] + if run(login) != 0: + sys.exit("error: failed to log in to remote") + + for recipe in built: + print(f"\n--- uploading {recipe.reference} ---", flush=True) + if run([args.conan, "upload", recipe.reference, "-r", args.remote, "--confirm"]) != 0: + sys.exit(f"error: failed to upload {recipe.reference}") + print(f"\nUploaded {len(built)} package(s) to '{args.remote}'.", flush=True) + + +def print_summary(built, skipped, failed: Recipe | None = None): + print("\n" + "=" * 60, flush=True) + print("SUMMARY", flush=True) + print("=" * 60, flush=True) + for recipe in built: + print(f" [OK] {recipe.reference}", flush=True) + for recipe, reason in skipped: + print(f" [SKIP] {recipe.name} ({reason})", flush=True) + if failed is not None: + print(f" [FAILED] {failed.reference}", flush=True) + print( + f"\nbuilt: {len(built)} skipped: {len(skipped)}" + + (" failed: 1" if failed is not None else ""), + flush=True, + ) + + +def main(): + args = parse_args() + root: Path = args.recipes_root + if not root.is_dir(): + sys.exit(f"error: recipes root not found: {root}") + + host = current_platform() + print(f"Host platform: {host}", flush=True) + + recipes = discover_recipes(root) + if args.only: + recipes = [r for r in recipes if r.name in args.only or r.dir_name in args.only] + if args.skip: + recipes = [r for r in recipes if r.name not in args.skip and r.dir_name not in args.skip] + if not recipes: + sys.exit("error: no recipes to build") + + recipes = order_by_dependencies(recipes) + print("Build order: " + ", ".join(r.name for r in recipes), flush=True) + + built, skipped = build_all(args, recipes, host) + print_summary(built, skipped) + + if args.upload: + if not built: + print("\nNothing was built; skipping upload.", flush=True) + else: + upload_all(args, built) + print("\nAll done.", flush=True) + + +if __name__ == "__main__": + main() diff --git a/ThirdParty/ConanRecipes/clipp/conandata.yml b/ThirdParty/ConanRecipes/clipp/conandata.yml index 6dc1f8e4..caf93cf7 100644 --- a/ThirdParty/ConanRecipes/clipp/conandata.yml +++ b/ThirdParty/ConanRecipes/clipp/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "1.2.3-exp": url: "https://github.com/muellan/clipp/archive/refs/tags/v1.2.3.tar.gz" diff --git a/ThirdParty/ConanRecipes/debugbreak/conandata.yml b/ThirdParty/ConanRecipes/debugbreak/conandata.yml index f9dfc2dc..72c15de5 100644 --- a/ThirdParty/ConanRecipes/debugbreak/conandata.yml +++ b/ThirdParty/ConanRecipes/debugbreak/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "1.0-exp": url: "https://github.com/scottt/debugbreak/archive/refs/tags/v1.0.tar.gz" diff --git a/ThirdParty/ConanRecipes/dxc/conandata.yml b/ThirdParty/ConanRecipes/dxc/conandata.yml index 20866f15..1e16bebc 100644 --- a/ThirdParty/ConanRecipes/dxc/conandata.yml +++ b/ThirdParty/ConanRecipes/dxc/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "1.8.2505.1-exp": commit: "b106a961d09221b3c5bdb37be45b679257da08b8" diff --git a/ThirdParty/ConanRecipes/glfw/conandata.yml b/ThirdParty/ConanRecipes/glfw/conandata.yml index 576fdd62..1161fb89 100644 --- a/ThirdParty/ConanRecipes/glfw/conandata.yml +++ b/ThirdParty/ConanRecipes/glfw/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "3.4-exp": commit: "7b6aead9fb88b3623e3b3725ebb42670cbe4c579" diff --git a/ThirdParty/ConanRecipes/libclang/conandata.yml b/ThirdParty/ConanRecipes/libclang/conandata.yml index ecf50aec..b55b8b38 100644 --- a/ThirdParty/ConanRecipes/libclang/conandata.yml +++ b/ThirdParty/ConanRecipes/libclang/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "22.1.6-exp": Windows-x86_64: diff --git a/ThirdParty/ConanRecipes/molten-vk/conandata.yml b/ThirdParty/ConanRecipes/molten-vk/conandata.yml index fc149926..14667bc4 100644 --- a/ThirdParty/ConanRecipes/molten-vk/conandata.yml +++ b/ThirdParty/ConanRecipes/molten-vk/conandata.yml @@ -1,3 +1,5 @@ +platforms: + - Macos-armv8 sources: "1.4.1-exp": commit: "db445ff2042d9ce348c439ad8451112f354b8d2a" diff --git a/ThirdParty/ConanRecipes/qt/conandata.yml b/ThirdParty/ConanRecipes/qt/conandata.yml index 014f5ec4..1ad84e2c 100644 --- a/ThirdParty/ConanRecipes/qt/conandata.yml +++ b/ThirdParty/ConanRecipes/qt/conandata.yml @@ -1,2 +1,5 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 versions: - "6.10.3-exp" diff --git a/ThirdParty/ConanRecipes/rapidjson/conandata.yml b/ThirdParty/ConanRecipes/rapidjson/conandata.yml index db1ea513..886539e8 100644 --- a/ThirdParty/ConanRecipes/rapidjson/conandata.yml +++ b/ThirdParty/ConanRecipes/rapidjson/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "cci.20250205-exp": commit: "24b5e7a8b27f42fa16b96fc70aade9106cf7102f" diff --git a/ThirdParty/ConanRecipes/vulkan-utility-libraries/conandata.yml b/ThirdParty/ConanRecipes/vulkan-utility-libraries/conandata.yml index d1483c32..b69752d1 100644 --- a/ThirdParty/ConanRecipes/vulkan-utility-libraries/conandata.yml +++ b/ThirdParty/ConanRecipes/vulkan-utility-libraries/conandata.yml @@ -1,3 +1,6 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 sources: "1.4.313.0-exp": branch: "vulkan-sdk-1.4.313" diff --git a/ThirdParty/ConanRecipes/vulkan-validationlayers/conandata.yml b/ThirdParty/ConanRecipes/vulkan-validationlayers/conandata.yml index 560d4bde..6e4921e7 100644 --- a/ThirdParty/ConanRecipes/vulkan-validationlayers/conandata.yml +++ b/ThirdParty/ConanRecipes/vulkan-validationlayers/conandata.yml @@ -1,3 +1,8 @@ +platforms: + - Windows-x86_64 + - Macos-armv8 +requires: + - vulkan-utility-libraries sources: "1.4.313.0-exp": branch: "vulkan-sdk-1.4.313" From f3ec7eedcd3c4aa832b744960e0446d3c67523fc Mon Sep 17 00:00:00 2001 From: kindem Date: Mon, 8 Jun 2026 23:10:01 +0800 Subject: [PATCH 4/4] feat: update conan remote url --- .github/workflows/build.yml | 2 +- ThirdParty/ConanRecipes/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2834bece..96849357 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: uses: conan-io/setup-conan@v1 - name: Config Conan Remote - run: conan remote add explosion https://kindem.online/artifactory/api/conan/conan + run: conan remote add explosion https://conan.kindem.online/artifactory/api/conan/conan - name: Configure CMake run: cmake -B ${{github.workspace}}/build -G=Ninja -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCI=ON diff --git a/ThirdParty/ConanRecipes/README.md b/ThirdParty/ConanRecipes/README.md index 0193f7a9..1338e4fb 100644 --- a/ThirdParty/ConanRecipes/README.md +++ b/ThirdParty/ConanRecipes/README.md @@ -1,6 +1,6 @@ # ConanRecipes -This repository stores all private Conan recipes used by the explosion game engine. Typically, you don't need to build them separately, as all recipes and precompiled binaries have been uploaded to our private artifact repository at [https://kindem.online/artifactory/api/conan/conan](https://kindem.online/artifactory/api/conan/conan) +This repository stores all private Conan recipes used by the explosion game engine. Typically, you don't need to build them separately, as all recipes and precompiled binaries have been uploaded to our private artifact repository at [https://conan.kindem.online/artifactory/api/conan/conan](https://conan.kindem.online/artifactory/api/conan/conan) . The CMake scripts in the explosion game engine will automatically download and install all dependencies from this repository. If you have extremely poor network connectivity, you may use these Conan recipes to locally build the required third-party dependencies for the engine.