From bf33aad15eb202d6f74d892df7fae53e6d40f790 Mon Sep 17 00:00:00 2001 From: insleker Date: Tue, 19 May 2026 11:49:11 +0800 Subject: [PATCH] feat: enhance `webots-controller` Windows build support with MinGW and MSYS2 requirements --- .gitignore | 1 + recipes/webots-controller/all/conanfile.py | 286 +++++++++++++++++- .../all/test_package/CMakeLists.txt | 39 +++ .../all/test_package/conanfile.py | 4 +- .../all/test_package/test_package.cpp | 9 +- 5 files changed, 320 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 1ffbdd5..de805d6 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,4 @@ Session.vim !/.vscode /.env* !/.env*.example +**/test_package/bin \ No newline at end of file diff --git a/recipes/webots-controller/all/conanfile.py b/recipes/webots-controller/all/conanfile.py index dda923d..c465950 100644 --- a/recipes/webots-controller/all/conanfile.py +++ b/recipes/webots-controller/all/conanfile.py @@ -1,9 +1,116 @@ from conan import ConanFile -from conan.errors import ConanInvalidConfiguration +from conan.errors import ConanException, ConanInvalidConfiguration from conan.tools.env import Environment from conan.tools.files import chdir, copy, get from conan.tools.scm import Git +from pathlib import Path import os +import shutil +import subprocess +import platform + + +def find_webots_install_path() -> str | None: + """Locate an existing Webots installation directory. + + Windows strategy (in order): + 1. Respect WEBOTS_HOME environment variable if it points to an existing path. + 2. Query uninstall registry keys (both 64-bit and 32-bit views) for InstallLocation: + HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Webots + 3. Look in common default install directories (Program Files, Program Files (x86)). + 4. Fallback: derive from a discovered `webots-controller.exe` on PATH. + + macOS strategy: + * If a `webots` or `webots-controller` binary is found inside a Webots.app bundle, + return the bundle root (…/Webots.app). + + Linux / Other: + * Attempt to locate `webots-controller` or `webots` via PATH and return its parent directory. + + Returns None if nothing suitable is found. + """ + + system = platform.system() + + # 1. Explicit env var (all platforms) + env_home = os.getenv("WEBOTS_HOME") + if env_home and os.path.exists(env_home): + return env_home + + if system == "Windows": + # 2. Registry (both views) + try: # pragma: no cover (import guarded for non-Windows) + import winreg # type: ignore + except ImportError: # pragma: no cover + winreg = None # type: ignore + + if winreg: + reg_paths = [ + ( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Webots", + ), + # 32-bit view on 64-bit systems + ( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Webots", + ), + ] + for hive, subkey in reg_paths: + try: + with winreg.OpenKey(hive, subkey) as key: # type: ignore[attr-defined] + install_path, _ = winreg.QueryValueEx(key, "InstallLocation") # type: ignore[attr-defined] + if install_path and os.path.exists(install_path): + return install_path + except FileNotFoundError: + pass + except OSError: + pass + + # 3. Common default directories + program_files = os.environ.get("ProgramFiles", r"C:\\Program Files") + program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\\Program Files (x86)") + candidates = [ + os.path.join(program_files, "Webots"), + os.path.join(program_files_x86, "Webots"), + ] + for c in candidates: + if os.path.exists(c): + return c + + # 4. PATH discovery of controller exe + exe = shutil.which("webots-controller.exe") or shutil.which("webots.exe") + if exe: + parent = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(exe))) + ) # heuristic + # Explanation: typical Windows structure: + # \msys64\mingw64\bin\webots-controller.exe + # So we ascend four levels to reach WEBOTS_HOME. + if os.path.exists(parent): + return parent + return None + + # Non-Windows + candidate = shutil.which("webots-controller") or shutil.which("webots") + if not candidate: + return None + + # Resolve symlinks to get actual installation root. This matters on Linux + # distributions where Webots may be installed under /opt/webots but only a + # symlink placed into /usr/local/bin or similar locations. + resolved = os.path.realpath(candidate) + + if system == "Darwin": + token = "Webots.app" + # If the resolved path sits inside an app bundle, return the bundle root. + if token in resolved: + prefix, _sep, _rest = resolved.partition(token) + return os.path.join(prefix, token) + # Fallback: just use the directory of the (possibly resolved) binary. + return os.path.dirname(resolved) + + return os.path.dirname(resolved) class WebotsControllerConan(ConanFile): @@ -29,15 +136,48 @@ class WebotsControllerConan(ConanFile): def _source_subfolder(self): return "source_subfolder" + @property + def _is_system_package(self): + return str(self.version) == "system" + + @property + def _system_webots_home(self): + webots_home = find_webots_install_path() + if not webots_home: + raise ConanInvalidConfiguration( + f"{self.ref} could not locate an installed Webots root. " + "Set WEBOTS_HOME or install Webots in a standard location discoverable by find_webots_install_path()." + ) + return os.path.abspath(os.path.expanduser(webots_home)) + + def _validate_system_webots_home(self): + webots_home = self._system_webots_home + required_paths = [ + os.path.join(webots_home, "include", "controller", "c", "webots", "robot.h"), + os.path.join(webots_home, "include", "controller", "cpp", "webots", "Robot.hpp"), + os.path.join(webots_home, "lib", "controller"), + ] + missing = [path for path in required_paths if not os.path.exists(path)] + if missing: + formatted = ", ".join(f"'{path}'" for path in missing) + raise ConanInvalidConfiguration( + f"{self.ref} could not find the expected Webots controller files under WEBOTS_HOME='{webots_home}': {formatted}" + ) + def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") def validate(self): + if self._is_system_package: + self._validate_system_webots_home() + return if str(self.settings.compiler) == "msvc": raise ConanInvalidConfiguration("Webots controller libraries are built with GCC/Clang toolchains, not MSVC") def source(self): + if self._is_system_package: + return source_data = self.conan_data["sources"][self.version] if "git_url" in source_data: git = Git(self) @@ -55,40 +195,154 @@ class WebotsControllerConan(ConanFile): return get(self, **source_data, strip_root=True, destination=self._source_subfolder) + def requirements(self): + # [Using a MinGW as tool_requires to build with gcc in Windows](https://docs.conan.io/2/examples/dev_flow/tool_requires/mingw.html#examples-dev-flow-tool-requires-mingw) + if not self._is_system_package and self.settings.os == "Windows": + self.tool_requires("msys2/cci.latest") + + def package_id(self): + if self._is_system_package: + self.info.clear() + def build(self): - build_env = Environment() + if self._is_system_package: + return source_root = os.path.join(self.source_folder, self._source_subfolder) - build_env.define("WEBOTS_HOME", source_root) - with build_env.vars(self).apply(): - with chdir(self, os.path.join(source_root, "src", "controller", "c")): - self.run("make release") - with chdir(self, os.path.join(source_root, "src", "controller", "cpp")): - self.run("make release") + if self.settings.os == "Windows": + # Webots makefiles invoke POSIX tools and shell fragments, so run MSYS make from bash. + bash_exe = None + make_exe = None + for dep in self.dependencies.build.values(): + if not dep.ref or not dep.package_folder: + continue + if dep.ref.name == "msys2": + bash_matches = list(Path(dep.package_folder).glob("**/bash.exe")) + make_matches = list(Path(dep.package_folder).glob("**/make.exe")) + if bash_matches and not bash_exe: + bash_exe = str(bash_matches[0]) + if make_matches and not make_exe: + make_exe = str(make_matches[0]) + if not bash_exe or not make_exe: + raise ConanInvalidConfiguration( + "Windows build requires bash.exe and make.exe from tool_requires 'msys2'." + ) + mingw_dep = next((dep for dep in self.dependencies.build.values() if dep.ref and dep.ref.name == "mingw-builds"), None) + if mingw_dep is None: + raise ConanInvalidConfiguration("Windows build requires tool_requires 'mingw-builds'.") + env = os.environ.copy() + env["WEBOTS_HOME"] = source_root + env["PATH"] = os.pathsep.join([ + os.path.join(mingw_dep.package_folder, "bin"), + str(Path(make_exe).parent), + env.get("PATH", ""), + ]) + drive, tail = os.path.splitdrive(make_exe) + msys_make = f"/{drive[0].lower()}{tail.replace('\\', '/')}" + for controller_dir in ("c", "cpp"): + cwd = os.path.join(source_root, "src", "controller", controller_dir) + subprocess.run([bash_exe, "--noprofile", "--norc", "-c", f'"{msys_make}" clean'], cwd=cwd, env=env, check=True) + subprocess.run([bash_exe, "--noprofile", "--norc", "-c", f'"{msys_make}" release'], cwd=cwd, env=env, check=True) + else: + build_env = Environment() + build_env.define("WEBOTS_HOME", source_root) + with build_env.vars(self).apply(): + with chdir(self, os.path.join(source_root, "src", "controller", "c")): + self.run("make clean") + self.run("make release") + with chdir(self, os.path.join(source_root, "src", "controller", "cpp")): + self.run("make clean") + self.run("make release") + + expected_artifacts = { + "Linux": ["libController.so", "libCppController.so"], + "Macos": ["libController.dylib", "libCppController.dylib"], + "Windows": ["Controller.dll", "libController.a", "CppController.dll", "libCppController.a"], + } + lib_dir = os.path.join(source_root, "lib", "controller") + missing = [name for name in expected_artifacts.get(str(self.settings.os), []) if not os.path.exists(os.path.join(lib_dir, name))] + if missing: + raise ConanException( + f"Expected Webots controller artifacts were not produced in '{lib_dir}': {', '.join(missing)}. " + "On Windows, the upstream Webots controller build may be incompatible with the current MinGW/MSYS toolchain setup." + ) def package(self): + if self._is_system_package: + return source_root = os.path.join(self.source_folder, self._source_subfolder) + build_root = os.path.join(self.build_folder, self._source_subfolder) copy(self, "LICENSE", src=source_root, dst=os.path.join(self.package_folder, "licenses")) copy(self, "*.h", src=os.path.join(source_root, "include", "controller", "c"), dst=os.path.join(self.package_folder, "include", "controller", "c"), keep_path=True) copy(self, "*.hpp", src=os.path.join(source_root, "include", "controller", "cpp"), dst=os.path.join(self.package_folder, "include", "controller", "cpp"), keep_path=True) - lib_src = os.path.join(source_root, "lib", "controller") - copy(self, "*.so*", src=lib_src, dst=os.path.join(self.package_folder, "lib"), keep_path=False) - copy(self, "*.dylib*", src=lib_src, dst=os.path.join(self.package_folder, "lib"), keep_path=False) - copy(self, "*.dll", src=lib_src, dst=os.path.join(self.package_folder, "bin"), keep_path=False) - copy(self, "*.lib", src=lib_src, dst=os.path.join(self.package_folder, "lib"), keep_path=False) + lib_dst = os.path.join(self.package_folder, "lib") + bin_dst = os.path.join(self.package_folder, "bin") + lib_src_candidates = [ + os.path.join(build_root, "lib", "controller"), + os.path.join(source_root, "lib", "controller"), + ] + for lib_src in lib_src_candidates: + copy(self, "*.so*", src=lib_src, dst=lib_dst, keep_path=False) + copy(self, "*.dylib*", src=lib_src, dst=lib_dst, keep_path=False) + copy(self, "*.a", src=lib_src, dst=lib_dst, keep_path=False) + copy(self, "*.dll", src=lib_src, dst=bin_dst, keep_path=False) + copy(self, "*.lib", src=lib_src, dst=lib_dst, keep_path=False) + + if self.settings.os == "Linux": + patchelf = shutil.which("patchelf") + if patchelf: + package_lib = os.path.join(self.package_folder, "lib") + for soname in ("libController.so", "libCppController.so"): + lib_path = os.path.join(package_lib, soname) + if os.path.exists(lib_path): + self.run(f'"{patchelf}" --set-soname "{soname}" "{lib_path}"') def package_info(self): self.cpp_info.set_property("cmake_file_name", "webots-controller") + if self._is_system_package: + webots_home = self._system_webots_home + controller_lib_dir = os.path.join(webots_home, "lib", "controller") + controller_include_dir = os.path.join(webots_home, "include", "controller", "c") + cpp_controller_include_dir = os.path.join(webots_home, "include", "controller", "cpp") + extra_libdirs = [] + extra_bindirs = [] + + self.cpp_info.bindirs = [] + self.cpp_info.includedirs = [] + self.cpp_info.libdirs = [] + + self.buildenv_info.define_path("WEBOTS_HOME", webots_home) + self.runenv_info.define_path("WEBOTS_HOME", webots_home) + + if self.settings.os == "Windows": + mingw_bin_dir = os.path.join(webots_home, "msys64", "mingw64", "bin") + extra_libdirs.append(mingw_bin_dir) + extra_bindirs.append(mingw_bin_dir) + elif self.settings.os == "Linux": + extra_libdirs.append(os.path.join(webots_home, "lib")) + + self.cpp_info.components["controller"].includedirs = [controller_include_dir] + self.cpp_info.components["controller"].libdirs = [controller_lib_dir, *extra_libdirs] + if self.settings.os == "Windows": + self.cpp_info.components["controller"].bindirs = [controller_lib_dir, *extra_bindirs] + + self.cpp_info.components["cpp_controller"].includedirs = [ + cpp_controller_include_dir, + controller_include_dir, + ] + self.cpp_info.components["cpp_controller"].libdirs = [controller_lib_dir, *extra_libdirs] + if self.settings.os == "Windows": + self.cpp_info.components["cpp_controller"].bindirs = [controller_lib_dir, *extra_bindirs] + else: + self.cpp_info.components["controller"].includedirs = ["include/controller/c"] + self.cpp_info.components["cpp_controller"].includedirs = ["include/controller/cpp", "include/controller/c"] self.cpp_info.components["controller"].libs = ["Controller"] self.cpp_info.components["controller"].set_property("cmake_target_name", "Webots::Controller") - self.cpp_info.components["controller"].includedirs = ["include/controller/c"] - self.cpp_info.components["cpp_controller"].libs = ["CppController"] self.cpp_info.components["cpp_controller"].set_property("cmake_target_name", "Webots::CppController") self.cpp_info.components["cpp_controller"].requires = ["controller"] - self.cpp_info.components["cpp_controller"].includedirs = ["include/controller/cpp", "include/controller/c"] if self.settings.os == "Linux": self.cpp_info.components["controller"].system_libs = ["m", "pthread", "dl", "rt"] diff --git a/recipes/webots-controller/all/test_package/CMakeLists.txt b/recipes/webots-controller/all/test_package/CMakeLists.txt index 724872d..ddbb225 100644 --- a/recipes/webots-controller/all/test_package/CMakeLists.txt +++ b/recipes/webots-controller/all/test_package/CMakeLists.txt @@ -5,3 +5,42 @@ find_package(webots-controller REQUIRED CONFIG) add_executable(test_package test_package.cpp) target_link_libraries(test_package PRIVATE Webots::CppController) + +if(POLICY CMP0207) + cmake_policy(SET CMP0207 NEW) +endif() + +# Webots controllers rely on seeing shared deps in the same directory as the executable. +install(TARGETS test_package + RUNTIME_DEPENDENCY_SET test_package_runtime_deps + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) + +if(WIN32) + # for single-config generators. + set(_runtime_dependency_directories) + string(TOUPPER "${CMAKE_BUILD_TYPE}" _conan_build_type) + set(_webots_bin_dirs_var "webots-controller_BIN_DIRS_${_conan_build_type}") + if(DEFINED ${_webots_bin_dirs_var}) + list(APPEND _runtime_dependency_directories ${${_webots_bin_dirs_var}}) + endif() + if(DEFINED ENV{MINGW_HOME}) + list(APPEND _runtime_dependency_directories "$ENV{MINGW_HOME}/bin") + endif() + install( + RUNTIME_DEPENDENCY_SET test_package_runtime_deps + DIRECTORIES ${_runtime_dependency_directories} + PRE_EXCLUDE_REGEXES + "^api-ms-win-.*" + "^ext-ms-.*" + POST_EXCLUDE_REGEXES + ".*/system32/.*" + ".*/Windows/.*" + ".*/SysWOW64/.*" + DESTINATION bin) +else() + install( + RUNTIME_DEPENDENCY_SET test_package_runtime_deps + DESTINATION bin) +endif() diff --git a/recipes/webots-controller/all/test_package/conanfile.py b/recipes/webots-controller/all/test_package/conanfile.py index 7f7100c..dcae494 100644 --- a/recipes/webots-controller/all/test_package/conanfile.py +++ b/recipes/webots-controller/all/test_package/conanfile.py @@ -21,6 +21,8 @@ class TestPackageConan(ConanFile): cmake.build() def test(self): + cmake = CMake(self) + cmake.install() if can_run(self): - exe = os.path.join(self.cpp.build.bindirs[0], "test_package") + exe = os.path.join(self.source_folder, "bin", "test_package") self.run(exe, env="conanrun") diff --git a/recipes/webots-controller/all/test_package/test_package.cpp b/recipes/webots-controller/all/test_package/test_package.cpp index 57700fc..d044c5a 100644 --- a/recipes/webots-controller/all/test_package/test_package.cpp +++ b/recipes/webots-controller/all/test_package/test_package.cpp @@ -1,7 +1,12 @@ #include +#include +#include int main() { - webots::Robot *robot = nullptr; - (void)robot; + auto cpp_symbol = &webots::Robot::getBasicTimeStep; + auto c_symbol = &wb_robot_get_time; + (void)cpp_symbol; + (void)c_symbol; + std::cout << "Success!" << std::endl; return 0; }