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.
This commit is contained in:
Andrej730
2026-09-11 16:07:36 +05:00
parent e7b33a909d
commit d5fd959e6c
8 changed files with 352 additions and 31 deletions
@@ -226,7 +226,7 @@ C++ Build Tools <http://landinghub.visualstudio.com/visual-cpp-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:
+3 -1
View File
@@ -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",
+4 -4
View File
@@ -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)"
),
+2 -9
View File
@@ -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)
+22 -7
View File
@@ -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
+9 -9
View File
@@ -29,21 +29,21 @@ The script will create `_deps\` and `_deps-vs<VERSION>-<PLATFORM>[-<TOOLSET>]-in
> python build-deps.py <GENERATOR> <Release|RelWithDebInfo|MinSizeRel>
```
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<VERSION>-<PLATFORM>[-<TOOLSET>]\` 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<VERSION>-<PLATFORM>[-<TOOLSET>]\` 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<VERSION>-<ARCHITECTURE>\` folder in the project's root folder and the
required IfcOpenShell-Python parts are deployed to the `<PYTHONHOME>\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
+5
View File
@@ -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
+306
View File
@@ -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 <http://www.gnu.org/licenses/>. #
# #
###############################################################################
#
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()