From d5fd959e6c1dfe227b688307ac2aa05653da7312 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 11 Sep 2026 16:07:36 +0500 Subject: [PATCH] Add run-cmake.py, deprecate run-cmake.bat Also drop-in replacement mostly, except extra cmake args need to be provided now after `--` - `python run-cmake.py vs2022-x64 -- -DGLTF_SUPPORT=ON`. Internally, script relies on env variables much much less. --- .../docs/ifcopenshell/installation.rst | 2 +- win/build-all-win.py | 4 +- win/build-deps.py | 8 +- win/build-ifcopenshell.py | 11 +- win/common.py | 29 +- win/readme.md | 18 +- win/run-cmake.bat | 5 + win/run-cmake.py | 306 ++++++++++++++++++ 8 files changed, 352 insertions(+), 31 deletions(-) create mode 100644 win/run-cmake.py diff --git a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst index f4841b7a99..51ea0696e7 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst @@ -226,7 +226,7 @@ C++ Build Tools `__). cd IfcOpenShell\win python build-deps.py - run-cmake.bat + python run-cmake.py 3. Open and build the solution file in Visual Studio: diff --git a/win/build-all-win.py b/win/build-all-win.py index e5d744140e..ce031b935f 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -226,8 +226,10 @@ def build() -> None: OLD_ADD_COMMIT_SHA = set_env("ADD_COMMIT_SHA", "ON") run( [ - str(REPO_WIN / "run-cmake.bat"), + sys.executable, + str(REPO_WIN / "run-cmake.py"), build_generator(), + "--", "-DENABLE_BUILD_OPTIMIZATIONS=ON", "-DGLTF_SUPPORT=ON", "-DBUILD_EXAMPLES=OFF", diff --git a/win/build-deps.py b/win/build-deps.py index c1c784e8e6..6ba217afd6 100644 --- a/win/build-deps.py +++ b/win/build-deps.py @@ -116,14 +116,14 @@ def print_build_config( logger.info(" - Download and install Python.") logger.info(" Set to something other than TRUE if you wish to use an already installed version of Python.") logger.info( - " But then you'll need to set PYTHONHOME env variable to your Python installation before running run-cmake.bat" + " But then you'll need to set PYTHONHOME env variable to your Python installation before running run-cmake.py" ) logger.info(" to your Python installation path.") logger.info(field(f"* IFCOS_INSTALL_QT6\t= {ifcos_install_qt6}")) logger.info(" - Download and install Qt6 using aqtinstall.") logger.info(" Set to something other than TRUE if you wish to use an already installed version of Qt6.") logger.info( - " But then you'll need to set QT_DIR env variable to your Qt6 installation before running run-cmake.bat." + " But then you'll need to set QT_DIR env variable to your Qt6 installation before running run-cmake.py." ) logger.info(field(f"* IFCOS_NUM_BUILD_PROCS\t= {ifcos_num_build_procs}")) logger.info(" - How many MSBuild.exe processes may be run in parallel.") @@ -212,7 +212,7 @@ def parse_args() -> Args: default=argparse.SUPPRESS, help=( "Download and install Python. If disabled, an already installed Python is used - " - "set the PYTHONHOME env variable to its installation path before running run-cmake.bat. " + "set the PYTHONHOME env variable to its installation path before running run-cmake.py. " "Also can be specified by using IFCOS_INSTALL_PYTHON env variable. " "(default: True)" ), @@ -224,7 +224,7 @@ def parse_args() -> Args: default=argparse.SUPPRESS, help=( "Download and install Qt6 using aqtinstall. If disabled, an already installed Qt6 is used - " - "set the QT_DIR env variable to its installation path before running run-cmake.bat. " + "set the QT_DIR env variable to its installation path before running run-cmake.py. " "Also can be specified by using IFCOS_INSTALL_QT6 env variable. " "(default: True)" ), diff --git a/win/build-ifcopenshell.py b/win/build-ifcopenshell.py index cbfaf94599..84f8b20e54 100644 --- a/win/build-ifcopenshell.py +++ b/win/build-ifcopenshell.py @@ -37,8 +37,8 @@ from common import ( HelpStrings, colorize, ensure_script_dir, - find_cached_gen_shorthand, logger, + resolve_generator, run_streamed, ) from vs_cfg import vs_cfg @@ -145,14 +145,7 @@ def main() -> None: ensure_script_dir() - generator = ARGS.generator - if generator is None: - generator = find_cached_gen_shorthand() - if generator is None: - logger.error( - "BuildDepsCache file does not exist and/or GEN_SHORTHAND missing from it. Run build-deps.py to create it." - ) - sys.exit(1) + generator = resolve_generator(ARGS.generator) vs_cfg_vars = vs_cfg(generator, REPO_ROOT) diff --git a/win/common.py b/win/common.py index 7f4d699d0d..7b39adf1f1 100644 --- a/win/common.py +++ b/win/common.py @@ -99,6 +99,9 @@ def is_on_off(value: str | None, *, default: bool) -> bool: return default +OFF_ON = ("OFF", "ON") + + BuildCfg = Literal["MinSizeRel", "Release", "RelWithDebInfo", "Debug"] DebugOrRelease = Literal["Debug", "Release"] @@ -190,11 +193,23 @@ class BuildDepsCache: return entries -def find_cached_gen_shorthand() -> str | None: - """Read GEN_SHORTHAND from the most recently modified BuildDepsCache-*.txt, if any.""" +def resolve_generator(generator: str | None) -> str: + """Return `generator` as-is, or fall back to the GEN_SHORTHAND from the most recently modified + BuildDepsCache-*.txt. Exits if neither is available. + """ + if generator is not None: + return generator + cache_files = sorted(SCRIPT_DIR.glob("BuildDepsCache-*.txt"), key=lambda p: p.stat().st_mtime, reverse=True) - if not cache_files: - return None - cache_file = cache_files[0] - logger.info(f"Found {cache_file.name}, reading GEN_SHORTHAND from it.") - return BuildDepsCache.parse(cache_file).get("GEN_SHORTHAND") + cached_generator = None + if cache_files: + cache_file = cache_files[0] + logger.info(f"Found {cache_file.name}, reading GEN_SHORTHAND from it.") + cached_generator = BuildDepsCache.parse(cache_file).get("GEN_SHORTHAND") + + if cached_generator is None: + logger.error( + "BuildDepsCache file does not exist and/or GEN_SHORTHAND missing from it. Run build-deps.py to create it." + ) + sys.exit(1) + return cached_generator diff --git a/win/readme.md b/win/readme.md index d53b8bafb7..e641b32606 100644 --- a/win/readme.md +++ b/win/readme.md @@ -29,21 +29,21 @@ The script will create `_deps\` and `_deps-vs-[-]-in > python build-deps.py ``` -After the dependencies are built, execute `run-cmake.bat`. The batch file expects a CMake generator as `%1`, that is interpreted just like the `build-deps.py` script, and the rest of possible parameters are passed as is. If a generator is not provided, the generator is read from the BuildDepsCache file, or tried to be deduced from the location of `cl.exe`. If passing build options for the script, the generator must be always passed as the first option: +After the dependencies are built, execute `python run-cmake.py`. The script expects a CMake generator as the 1st positional argument, that is interpreted just like the `build-deps.py` script. If a generator is not provided, the generator is read from the BuildDepsCache file. CMake options are passed after `--`: ``` -> run-cmake.bat vs2022-x64 -DGLTF_SUPPORT=ON +> python run-cmake.py vs2022-x64 -- -DGLTF_SUPPORT=ON ``` -**If you wish to use any library from a custom location, modify the paths in `run-cmake.bat` accordingly**. The batch script will create a folder of form `_build-vs-[-]\` which will contain the solution and project files for MSVC. +**If you wish to use any library from a custom location, modify the paths in `run-cmake.py` accordingly**. The script will create a folder of form `_build-vs-[-]\` which will contain the solution and project files for MSVC. Note that building IfcOpenShell as 64-bit is recommended as many of real life IFC files has been observed to take easily more than 2 GBs of RAM while converting. After this, one can build the project using the `IfcOpenShell.sln` file in the build folder. Build the `INSTALL` project if wanted. Convenience scripts `python build-ifcopenshell.py` and `python install-ifcopenshell.py` can also be used. The scripts expect the generator and build configuration type in the same fashion as `build-deps.py` and possible extra -parameters are passed for the `MSBuild` call after `--`. `run-cmake.bat` can also be directly invoked from File -Explorer or regular Command Prompt if BuildDepsCache file exists (the last modified version is used). Running the -scripts without extra parameters reads the build options from an existing CMakeCache.txt. +parameters are passed for the `MSBuild` call after `--`. `python run-cmake.py` can also be run without a generator +argument if a BuildDepsCache file exists (the last modified version is used). Running the scripts without extra +parameters reads the build options from an existing CMakeCache.txt. The project will be installed to `_installed-vs-\` folder in the project's root folder and the required IfcOpenShell-Python parts are deployed to the `\Lib\site-packages\` folder. The 3ds Max plug-in, @@ -65,14 +65,14 @@ in `IfcOpenShell\win`: > echo PYTHONHOME=C:\Python3>> BuildDepsCache-x64.txt ``` -After this you should be able to run `run-cmake.bat` normally. If using 32-bit Python, the name of the file must be +After this you should be able to run `python run-cmake.py` normally. If using 32-bit Python, the name of the file must be `BuildDepsCache-x86.txt`. Directory Structure ------------------ ``` .. -+---_build-* - Created by run-cmake.bat, specific for a certain compiler and and target architecture ++---_build-* - Created by run-cmake.py, specific for a certain compiler and and target architecture +---_deps - Created by build-deps.py, common for all compilers +---_deps-*-installed - Created by build-deps.py, specific for a certain compiler and target architecture +---_installed-* - Created by installing the IFCOS project, specific for a certain compiler and target architecture @@ -84,7 +84,7 @@ Directory Structure | build-type-cfg.cmd - Utility file used by the build scripts | install-ifcopenshell.py - Installs/deploys IFCOS using MSVC. | readme.md - This file -| run-cmake.bat - Sets environment variables for the dependencies and runs CMake for IFCOS using MSVC +| run-cmake.py - Sets environment variables for the dependencies and runs CMake for IFCOS using MSVC | vs-cfg.cmd - Utility file used by the build scripts \---patches - Contains patches for the dependencies \---utils - Contains various utilities for the build scripts diff --git a/win/run-cmake.bat b/win/run-cmake.bat index 0538f5a3cc..6bd84c7759 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -151,6 +151,11 @@ echo. echo CMAKE_INSTALL_PREFIX = %CMAKE_INSTALL_PREFIX% echo. +call cecho.cmd 0 12 "WARNING: run-cmake.bat is deprecated since 11 Sep 2026 and will be removed very shortly." +call cecho.cmd 0 12 "Use `python run-cmake.py` instead. It's intended to be a drop-in replacement, so exactly the same args apply," +call cecho.cmd 0 12 "except CMake args now need to be passed after `"--`", e.g. `python run-cmake.py vs2022-x64 -- -DGLTF_SUPPORT=ON`." +echo. + set CMAKELISTS_DIR=..\cmake :: Delete CMakeCache.txt if command-line options were provided for this batch script. if not (%1)==() if exist CMakeCache.txt. del /Q CMakeCache.txt diff --git a/win/run-cmake.py b/win/run-cmake.py new file mode 100644 index 0000000000..ea6851d8e6 --- /dev/null +++ b/win/run-cmake.py @@ -0,0 +1,306 @@ +# /// script +# [tool.ty.environment] +# root = ["."] +# /// +############################################################################### +# # +# This file is part of IfcOpenShell. # +# # +# IfcOpenShell is free software: you can redistribute it and/or modify # +# it under the terms of the Lesser GNU General Public License as published by # +# the Free Software Foundation, either version 3.0 of the License, or # +# (at your option) any later version. # +# # +# IfcOpenShell is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# Lesser GNU General Public License for more details. # +# # +# You should have received a copy of the Lesser GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### +# +import argparse +import os +import sys +from itertools import chain +from pathlib import Path +from typing import Literal, NamedTuple, NoReturn + +from common import ( + OFF_ON, + PROJECT_NAME, + REPO_ROOT, + BuildDepsCache, + C, + HelpStrings, + colorize, + ensure_script_dir, + logger, + resolve_generator, + run, + run_streamed, +) +from vs_cfg import VsCfgResult, vs_cfg + + +class Dep(NamedTuple): + env_var: str + rel_path: Path | None + cmake_prefix: bool = True + """Whether the dep's dir should be added to CMAKE_PREFIX_PATH.""" + pass_to_env: bool = False + """Whether `env_var` should be added to the subprocess env passed to cmake.""" + required: bool = True + """Whether to error out if the dep's value can't be resolved.""" + base: Literal["DEPS", "INSTALL"] = "INSTALL" + """Which base dir `rel_path` is relative to.""" + + +class Deps: + DEPS: dict[str, Dep] = { + "boost": Dep("BOOST_INSTALL_DIR", None), + "occ": Dep("OCC_INSTALL_DIR", None), + "opencollada": Dep("OPENCOLLADA_INSTALL_DIR", Path("OpenCOLLADA")), + # We don't install Eigen currently, + # so there's no Eigen3config.cmake and therefore we provide path explicitly. + "eigen": Dep("EIGEN_DIR", Path("Eigen"), cmake_prefix=False, pass_to_env=True), + "cgal": Dep("CGAL_INSTALL_DIR", Path("cgal")), + "gmp": Dep("GMP_INSTALL_DIR", Path("mpir")), + "mpfr": Dep("MPFR_INSTALL_DIR", Path("mpfr")), + # CCACHE_INSTALL_DIR is only set when ccache wasn't found on PATH. + "ccache": Dep("CCACHE_INSTALL_DIR", None, required=False), + "zstd": Dep("ZSTD_INSTALL_DIR", Path("zstd")), + "swig": Dep("SWIG_INSTALL_DIR", None), + "rocksdb": Dep("ROCKSDB_INSTALL_DIR", Path("rocksdb")), + "json": Dep("JSON_INCLUDE_DIR", Path("json"), cmake_prefix=False, pass_to_env=True), + "libxml2_libraries": Dep( + "LIBXML2_LIBRARIES", Path("OpenCOLLADA/lib/opencollada/xml.lib"), cmake_prefix=False, pass_to_env=True + ), + "libxml2_include_dir": Dep( + "LIBXML2_INCLUDE_DIR", + Path("OpenCOLLADA/Externals/LibXML/include"), + cmake_prefix=False, + pass_to_env=True, + base="DEPS", + ), + # TODO: drop this TRANSITION check once everyone has re-run build-deps.py with manifold support. + "manifold": Dep("MANIFOLD_INSTALL_PATH", None, required=False), + "pythonhome": Dep("PYTHONHOME", None, cmake_prefix=False), + } + + _values: dict[str, Path | None] | None = None + + @classmethod + def values(cls) -> dict[str, Path | None]: + if cls._values is None: + raise RuntimeError("Deps.values() accessed before Deps.init_values() was called.") + return cls._values + + @classmethod + def init_values(cls, vs_cfg_vars: VsCfgResult, deps_cache: dict[str, str]) -> None: + cls._values = {} + has_errors = False + for name, dep in cls.DEPS.items(): + if dep.rel_path is not None: + base_dir = vs_cfg_vars.install_dir if dep.base == "INSTALL" else vs_cfg_vars.deps_dir + value = base_dir / dep.rel_path + else: + value = get_var(deps_cache, dep.env_var, deps_cache_only=True) + if isinstance(value, str): + value = Path(value) + if dep.required: + if value is None: + logger.error(f"{dep.env_var} is required but could not be resolved.") + has_errors = True + elif not value.exists(): + logger.error(f"{dep.env_var} does not exist: {value}") + has_errors = True + cls._values[name] = value + if has_errors: + sys.exit(1) + + @classmethod + def cmake_prefix_paths(cls) -> list[Path]: + return [ + value for name, dep in cls.DEPS.items() if dep.cmake_prefix and (value := cls.values()[name]) is not None + ] + + @classmethod + def env_dict(cls) -> dict[str, str]: + return {dep.env_var: str(cls.values()[name]) for name, dep in cls.DEPS.items() if dep.pass_to_env} + + +def get_var(deps_cache: dict[str, str], key: str, *, deps_cache_only: bool = False) -> str | None: + # TODO: just rely on `deps_cache`? + if deps_cache_only: + return deps_cache.get(key) + return deps_cache.get(key) or os.environ.get(key) + + +class Args(NamedTuple): + generator: str | None + extra_args: list[str] + + +class ArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> NoReturn: + # TODO: same as in build-ifcopenshell.py, can be removed later. + if message.startswith("unrecognized arguments"): + message += ( + "\nHint: put args meant for CMake after '--', e.g. `run-cmake.py vs2022-x64 -- -DGLTF_SUPPORT=ON`." + ) + super().error(message) + + +def parse_args() -> Args: + parser = ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + epilog="Arguments after '--' are passed through as-is to CMake, e.g. -DGLTF_SUPPORT=ON.", + ) + parser.add_argument( + "generator", + nargs="?", + default=None, + help=HelpStrings.generator("GEN_SHORTHAND from the most recently modified BuildDepsCache-*.txt is used"), + ) + parser.add_argument( + "--generator", + dest="generator_flag", + default=None, + help=HelpStrings.GENERATOR_FLAG, + ) + argv = sys.argv[1:] + if "--" in argv: + separator_idx = argv.index("--") + own_argv, extra_args = argv[:separator_idx], argv[separator_idx + 1 :] + else: + own_argv, extra_args = argv, [] + + args = parser.parse_args(own_argv) + + if args.generator is not None and args.generator_flag is not None: + parser.error("generator was specified both as a positional argument and as --generator.") + generator = args.generator or args.generator_flag + + return Args(generator=generator, extra_args=extra_args) + + +def main() -> None: + ARGS = parse_args() + + ensure_script_dir() + + explicit_generator = ARGS.generator is not None + generator = resolve_generator(ARGS.generator) + + vs_cfg_vars = vs_cfg(generator, REPO_ROOT) + + deps_cache = BuildDepsCache.parse(vs_cfg_vars.build_deps_cache_path) + + Deps.init_values(vs_cfg_vars, deps_cache) + + pythonhome = Deps.values()["pythonhome"] + assert pythonhome is not None + python_executable = f"{pythonhome}\\python.exe" + py_ver_major_minor = run( + python_executable, "-c", "import sys; print(f'{sys.version_info[0]}{sys.version_info[1]}')" + ).strip() + python_include_dir = f"{pythonhome}\\include" + python_library = f"{pythonhome}\\libs\\python{py_ver_major_minor}.lib" + + # TODO: add as cli arg. + ADD_COMMIT_SHA = OFF_ON[bool(os.getenv("ADD_COMMIT_SHA"))] + VERSION_OVERRIDE = ADD_COMMIT_SHA + + qt_dir = get_var(deps_cache, "QT_DIR") or get_var(deps_cache, "QT6_INSTALL_DIR") + qt_host_path = get_var(deps_cache, "QT_HOST_PATH") or get_var(deps_cache, "QT6_HOST_INSTALL_DIR") + + cmake_install_prefix = REPO_ROOT / f"_installed-{vs_cfg_vars.gen_shorthand}" + + logger.info("") + logger.info(colorize("Script configuration:", C.PURPLE)) + logger.info(f" Generator = {generator}") + logger.info(f" Architecture = {vs_cfg_vars.vs_platform}") + logger.info(f" Toolset = {vs_cfg_vars.vs_toolset_override}") + logger.info(f" Arguments = {ARGS.extra_args}") + logger.info("") + + # Some deps are a bit less trivial to get, so we calculate them outside `Deps`. + extra_vars: dict[str, object] = { + "PYTHON_INCLUDE_DIR": python_include_dir, + "PYTHON_LIBRARY": python_library, + "PYTHON_EXECUTABLE": python_executable, + "QT_DIR": qt_dir, + "QT_HOST_PATH": qt_host_path, + "CMAKE_INSTALL_PREFIX": cmake_install_prefix, + } + dep_vars = ((dep.env_var, Deps.values()[name]) for name, dep in Deps.DEPS.items()) + logger.info(colorize(f"Dependency Environment Variables for {PROJECT_NAME}:", C.PURPLE)) + for env_var, value in chain(dep_vars, extra_vars.items()): + logger.info(f" {env_var:<23} = {value}") + logger.info("") + + build_dir = REPO_ROOT / vs_cfg_vars.build_dir + build_dir.mkdir(parents=True, exist_ok=True) + + cmakelists_dir = REPO_ROOT / "cmake" + if explicit_generator: + cmake_cache_path = build_dir / "CMakeCache.txt" + if cmake_cache_path.exists(): + cmake_cache_path.unlink() + logger.info(f'"Running CMake for {PROJECT_NAME}."') + + cmake_prefix_path_parts = Deps.cmake_prefix_paths() + if qt_dir: + cmake_prefix_path_parts.append(qt_dir) + cmake_prefix_path = ";".join(str(part) for part in cmake_prefix_path_parts) + + # TODO: add cli arg. + use_ninja = os.getenv("USE_NINJA") + if use_ninja: + cmake_generator = "Ninja" + arch_option = () + else: + cmake_generator = vs_cfg_vars.generator.name + arch_option = ("-A", vs_cfg_vars.vs_platform) + + cmake_args = [ + str(cmakelists_dir), + "-G", + cmake_generator, + *arch_option, + f"-DCMAKE_INSTALL_PREFIX={cmake_install_prefix}", + "-DWITH_ROCKSDB=ON", + "-DWITH_ZSTD=ON", + f"-DCMAKE_PREFIX_PATH={cmake_prefix_path}", + f"-DADD_COMMIT_SHA={ADD_COMMIT_SHA}", + f"-DVERSION_OVERRIDE={VERSION_OVERRIDE}", + ] + if qt_dir: + cmake_args.append(f"-DQT_DIR={qt_dir}") + if qt_host_path: + cmake_args.append(f"-DQT_HOST_PATH={qt_host_path}") + if Deps.values()["manifold"]: + cmake_args.append("-DWITH_MANIFOLD=ON") + cmake_args += ARGS.extra_args + + run_streamed( + "cmake", + *cmake_args, + cwd=build_dir, + env={ + **Deps.env_dict(), + "PYTHONHOME": str(pythonhome), + "PYTHON_EXECUTABLE": str(python_executable), + "PYTHON_INCLUDE_DIR": str(python_include_dir), + "PYTHON_LIBRARY": str(python_library), + }, + ) + + logger.info("") + + +if __name__ == "__main__": + main()