From 51a86cdd866202ea5deb0d287263b6fffe9dbe04 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 11 Sep 2026 15:20:12 +0500 Subject: [PATCH] Add install-/build-ifcopenshell.py, deprecate corresponding .bat files Mainly drop-in replacement for `build-ifcopenshell.bat` with `--help`, kw args and args validation, but there's a small caveat. Previously it was possible to pass args to the underlying build tool as simple positionals - e.g. `build-ifcopenshell vs2022-x64 Release /p:Foo=bar`. This behaviour is disabled now, because it doesn't allow validating provided args - it's impossible to tell whether `--config Release` is meant to be passed to msbuild or was meant as `--build-cfg Release` for `build-ifcopenshell`. But it's still possible to pass args to msbuild by using `--` - `python build-ifcopenshell.py vs2022-x64 Release -- /p:Foo=bar` `install-ifcopenshell.py` is now just a small wrapper passing `--target INSTALL` arg. --- .../docs/ifcopenshell/installation.rst | 4 +- win/build-all-win.py | 2 +- win/build-deps.py | 27 +-- win/build-ifcopenshell.bat | 5 + win/build-ifcopenshell.py | 196 ++++++++++++++++++ win/common.py | 49 +++++ win/install-ifcopenshell.bat | 5 + win/install-ifcopenshell.py | 34 +++ win/readme.md | 14 +- 9 files changed, 307 insertions(+), 29 deletions(-) create mode 100644 win/build-ifcopenshell.py create mode 100644 win/install-ifcopenshell.py diff --git a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst index 92a489fe58..f4841b7a99 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst @@ -240,13 +240,13 @@ C++ Build Tools `__). Only) to deploy the headers and binaries into a single location if wanted/needed. - Alternatively, one can use the utility batch file(s) to build and install the + Alternatively, one can use the utility script(s) to build and install the project easily from the command-line (installing a project will build it also, if required): .. code-block:: bat - install-ifcopenshell.bat + python install-ifcopenshell.py .. seealso:: diff --git a/win/build-all-win.py b/win/build-all-win.py index bf1e6e619f..e5d744140e 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -236,7 +236,7 @@ def build() -> None: ] ) restore_env(*OLD_ADD_COMMIT_SHA) - run([str(REPO_WIN / "install-ifcopenshell.bat"), build_generator(), "Release"]) + run([sys.executable, str(REPO_WIN / "install-ifcopenshell.py"), build_generator(), "Release"]) def archive_executables() -> None: diff --git a/win/build-deps.py b/win/build-deps.py index 00969ad870..c1c784e8e6 100644 --- a/win/build-deps.py +++ b/win/build-deps.py @@ -28,7 +28,6 @@ import os import shutil import sys from datetime import datetime -from pathlib import Path from typing import NamedTuple from common import ( @@ -43,7 +42,9 @@ from common import ( BuildDepsCache, BuildType, C, + HelpStrings, colorize, + ensure_script_dir, is_on_off, logger, require_command, @@ -144,19 +145,13 @@ def parse_args() -> Args: "generator", nargs="?", default=None, - help=( - "CMake generator to use. Accepts 3 forms: " - "(1) omitted - deduced from the active Visual Studio environment; " - "(2) shorthand, e.g. 'vs2022', 'vs2022-x64', 'vs2019-x86-v141' - optionally provide platform/toolset " - "using the suffix; " - "(3) full CMake generator name, e.g. 'Visual Studio 17 2022'." - ), + help=HelpStrings.generator("deduced from the active Visual Studio environment"), ) parser.add_argument( "--generator", dest="generator_flag", default=None, - help="Alternative way to specify the generator, instead of the positional argument. See above for accepted forms.", + help=HelpStrings.GENERATOR_FLAG, ) # SUPPRESS avoids a misleading "(default: None)" in `--help`, # though then arg might not be set and we use `getattr` to get it. @@ -165,14 +160,14 @@ def parse_args() -> Args: nargs="?", default=argparse.SUPPRESS, choices=BUILD_CFGS, - help=f"Build configuration type. (default: {BUILD_CFG_DEFAULT})", + help=HelpStrings.BUILD_CFG, ) parser.add_argument( "--build-cfg", dest="build_cfg_flag", default=BUILD_CFG_DEFAULT, choices=BUILD_CFGS, - help="Alternative way to specify the build configuration type, instead of the positional argument.", + help=HelpStrings.BUILD_CFG_FLAG, ) parser.add_argument( "build_type", @@ -208,11 +203,7 @@ def parse_args() -> Args: dest="num_build_procs", type=int, default=argparse.SUPPRESS, - help=( - "How many build processes may be run in parallel. " - "Also can be specified by using IFCOS_NUM_BUILD_PROCS env variable. " - "(default: NUMBER_OF_PROCESSORS)" - ), + help=HelpStrings.NUM_BUILD_PROCS, ) parser.add_argument( "--install-python", @@ -274,9 +265,7 @@ def main() -> None: logger.info(f"This script fetches and builds all {PROJECT_NAME} dependencies\n") - if Path.cwd() != SCRIPT_DIR: - logger.error(f"This script must be run from '{SCRIPT_DIR}'.") - sys.exit(1) + ensure_script_dir() # Make sure vcvarsall.bat is called and dev env set is up. get_vs_var("VSINSTALLDIR") diff --git a/win/build-ifcopenshell.bat b/win/build-ifcopenshell.bat index 7cf8b1065f..2b7cd773b1 100755 --- a/win/build-ifcopenshell.bat +++ b/win/build-ifcopenshell.bat @@ -58,6 +58,11 @@ if not defined IFCOS_NUM_BUILD_PROCS set IFCOS_NUM_BUILD_PROCS=%NUMBER_OF_PROCES call cecho.cmd 0 13 "* IFCOS_NUM_BUILD_PROCS`t= %IFCOS_NUM_BUILD_PROCS%" echo. +call cecho.cmd 0 12 "WARNING: build-ifcopenshell.bat is deprecated since 11 Sep 2026 and will be removed very shortly." +call cecho.cmd 0 12 "Use `python build-ifcopenshell.py` instead. It's intended to be a drop-in replacement, so exactly the same args apply," +call cecho.cmd 0 12 "except MSBuild args now need to be passed after `"--`", e.g. `python build-ifcopenshell.py vs2022-x64 -- /p:Foo=bar`." +echo. + call cecho.cmd 0 13 "Building %VS_PLATFORM% %BUILD_CFG% %PROJECT_NAME%" set MSBUILD_MULTIPROC=/m /p:CL_MPCount=%IFCOS_NUM_BUILD_PROCS% /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true cmake --build ..\%BUILD_DIR% -- /nologo %MSBUILD_MULTIPROC% /p:Platform=%VS_PLATFORM% /p:Configuration=%BUILD_CFG% ^ diff --git a/win/build-ifcopenshell.py b/win/build-ifcopenshell.py new file mode 100644 index 0000000000..cbfaf94599 --- /dev/null +++ b/win/build-ifcopenshell.py @@ -0,0 +1,196 @@ +# /// 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 multiprocessing +import os +import sys +from typing import NamedTuple, NoReturn + +from common import ( + BUILD_CFG_DEFAULT, + BUILD_CFGS, + PROJECT_NAME, + REPO_ROOT, + BuildCfg, + C, + HelpStrings, + colorize, + ensure_script_dir, + find_cached_gen_shorthand, + logger, + run_streamed, +) +from vs_cfg import vs_cfg + + +class Args(NamedTuple): + generator: str | None + build_cfg: BuildCfg + num_build_procs: int + target: str | None + extra_args: list[str] + + +class ArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> NoReturn: + # TODO: this hint can be removed later, it's just for anyone transitioning from build-ifcopenshell.bat, + # which allowed providing additional args as positionals. We disallow it here (they can be passed after + # '--') to ensure we can validate the provided args. + if message.startswith("unrecognized arguments"): + message += ( + "\nHint: put args meant for the underlying build tool (e.g. MSBuild) after '--', " + "e.g. `build-ifcopenshell.py -- /p:Foo=bar`." + ) + super().error(message) + + +def parse_args() -> Args: + parser = ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + epilog="Arguments after '--' are passed through as-is to the underlying build tool (e.g. MSBuild).", + ) + 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, + ) + parser.add_argument( + "build_cfg", + nargs="?", + default=argparse.SUPPRESS, + choices=BUILD_CFGS, + help=HelpStrings.BUILD_CFG, + ) + parser.add_argument( + "--build-cfg", + dest="build_cfg_flag", + default=BUILD_CFG_DEFAULT, + choices=BUILD_CFGS, + help=HelpStrings.BUILD_CFG_FLAG, + ) + parser.add_argument( + "--num-build-procs", + dest="num_build_procs", + type=int, + default=argparse.SUPPRESS, + help=HelpStrings.NUM_BUILD_PROCS, + ) + parser.add_argument( + "--target", + dest="target", + default=None, + help=( + "cmake --build target, passed as-is. E.g. 'INSTALL' to also install after building. " + "By default no target is passed, which builds the whole solution." + ), + ) + 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 + + build_cfg = getattr(args, "build_cfg", None) or args.build_cfg_flag + + num_build_procs = getattr(args, "num_build_procs", None) or int( + os.getenv("IFCOS_NUM_BUILD_PROCS") or multiprocessing.cpu_count() + ) + + return Args( + generator=generator, + build_cfg=build_cfg, + num_build_procs=num_build_procs, + target=args.target, + extra_args=extra_args, + ) + + +def main() -> None: + ARGS = parse_args() + + 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) + + vs_cfg_vars = vs_cfg(generator, REPO_ROOT) + + logger.info("") + logger.info(colorize(f"* IFCOS_NUM_BUILD_PROCS\t= {ARGS.num_build_procs}", C.PURPLE)) + logger.info("") + + if ARGS.target: + target_suffix = f" (target: {ARGS.target})" + target_args = ("--target", ARGS.target) + else: + target_suffix = "" + target_args = () + logger.info( + colorize(f"Building {vs_cfg_vars.vs_platform} {ARGS.build_cfg} {PROJECT_NAME}{target_suffix}", C.PURPLE) + ) + MSBUILD_MULTIPROC = ( + "/m", + f"/p:CL_MPCount={ARGS.num_build_procs}", + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true", + ) + run_streamed( + "cmake", + "--build", + str(REPO_ROOT / vs_cfg_vars.build_dir), + *target_args, + "--", + "/nologo", + *MSBUILD_MULTIPROC, + f"/p:Platform={vs_cfg_vars.vs_platform}", + f"/p:Configuration={ARGS.build_cfg}", + *ARGS.extra_args, + ) + + logger.info("") + logger.info(colorize(f"{vs_cfg_vars.vs_platform} {ARGS.build_cfg} {PROJECT_NAME} build finished.", C.GREEN)) + + +if __name__ == "__main__": + main() diff --git a/win/common.py b/win/common.py index d691fa63cb..7f4d699d0d 100644 --- a/win/common.py +++ b/win/common.py @@ -111,10 +111,41 @@ BUILD_TYPES = get_args(BuildType) BUILD_TYPE_DEFAULT: BuildType = "Build" +class HelpStrings: + NUM_BUILD_PROCS = ( + "How many build processes may be run in parallel. " + "Also can be specified by using IFCOS_NUM_BUILD_PROCS env variable. " + "(default: NUMBER_OF_PROCESSORS)" + ) + + GENERATOR_FLAG = ( + "Alternative way to specify the generator, instead of the positional argument. See above for accepted forms." + ) + + BUILD_CFG = f"Build configuration type. (default: {BUILD_CFG_DEFAULT})" + BUILD_CFG_FLAG = "Alternative way to specify the build configuration type, instead of the positional argument." + + @staticmethod + def generator(omitted_behavior: str) -> str: + return ( + "CMake generator to use. Accepts 3 forms: " + f"(1) omitted - {omitted_behavior}; " + "(2) shorthand, e.g. 'vs2022', 'vs2022-x64', 'vs2019-x86-v141' - optionally provide platform/toolset " + "using the suffix; " + "(3) full CMake generator name, e.g. 'Visual Studio 17 2022'." + ) + + def debug_or_release(build_cfg: BuildCfg) -> DebugOrRelease: return "Debug" if build_cfg == "Debug" else "Release" +def ensure_script_dir() -> None: + if Path.cwd() != SCRIPT_DIR: + logger.error(f"This script must be run from '{SCRIPT_DIR}'.") + sys.exit(1) + + def require_command(command: str) -> str: path = shutil.which(command) if not path: @@ -149,3 +180,21 @@ class BuildDepsCache: def add_entry(self, key: str, value: str) -> None: with self.path.open("a") as f: f.write(f"{key}={value}\n") + + @staticmethod + def parse(path: Path) -> dict[str, str]: + entries: dict[str, str] = {} + for line in path.read_text().splitlines(): + key, _, value = line.partition("=") + entries[key] = value + return entries + + +def find_cached_gen_shorthand() -> str | None: + """Read GEN_SHORTHAND from the most recently modified BuildDepsCache-*.txt, if any.""" + 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") diff --git a/win/install-ifcopenshell.bat b/win/install-ifcopenshell.bat index 54fb337d4a..2f76143266 100755 --- a/win/install-ifcopenshell.bat +++ b/win/install-ifcopenshell.bat @@ -57,6 +57,11 @@ IF "%IFCOS_NUM_BUILD_PROCS%"=="" set IFCOS_NUM_BUILD_PROCS=%NUMBER_OF_PROCESSORS call cecho.cmd 0 13 "* IFCOS_NUM_BUILD_PROCS`t= %IFCOS_NUM_BUILD_PROCS%" echo. +call cecho.cmd 0 12 "WARNING: install-ifcopenshell.bat is deprecated since 11 Sep 2026 and will be removed very shortly." +call cecho.cmd 0 12 "Use `python install-ifcopenshell.py` instead. It's intended to be a drop-in replacement, so exactly the same args apply," +call cecho.cmd 0 12 "except MSBuild args now need to be passed after `"--`", e.g. `python install-ifcopenshell.py vs2022-x64 -- /p:Foo=bar`." +echo. + call cecho.cmd 0 13 "Installing %VS_PLATFORM% %BUILD_CFG% %PROJECT_NAME%" set MSBUILD_MULTIPROC=/m /p:CL_MPCount=%IFCOS_NUM_BUILD_PROCS% /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true cmake --build ..\%BUILD_DIR% --target INSTALL -- /nologo %MSBUILD_MULTIPROC% /p:Platform=%VS_PLATFORM% ^ diff --git a/win/install-ifcopenshell.py b/win/install-ifcopenshell.py new file mode 100644 index 0000000000..0b854a48e3 --- /dev/null +++ b/win/install-ifcopenshell.py @@ -0,0 +1,34 @@ +# /// 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 sys + +from common import SCRIPT_DIR, run_streamed + + +def main() -> None: + run_streamed(sys.executable, str(SCRIPT_DIR / "build-ifcopenshell.py"), "--target", "INSTALL", *sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/win/readme.md b/win/readme.md index 2370390a37..d53b8bafb7 100644 --- a/win/readme.md +++ b/win/readme.md @@ -39,11 +39,11 @@ After the dependencies are built, execute `run-cmake.bat`. The batch file expect 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 batch files `build-ifcopenshell.bat` and `install-ifcopenshell.bat` can also be used. The batch -files expect `%1` and `%2` in same fashion as above and possible extra parameters are passed for the `MSBuild` call. -`run-cmake.bat`, `build-ifcopenshell.bat`, and `install-ifcopenshell.bat` can also be directly invoked from Filer 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. +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. 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, @@ -80,9 +80,9 @@ Directory Structure | build-all.cmd - Runs all of the build scripts for IFCOS and it dependencies in a row without pauses | build-deps.py - Fetches and builds all needed dependencies for IFCOS using MSVC | BuildDepsCache-.txt - Cache file created by build-deps.py -| build-ifcopenshell.bat - Builds IFCOS using MSVC +| build-ifcopenshell.py - Builds IFCOS using MSVC | build-type-cfg.cmd - Utility file used by the build scripts -| install-ifcopenshell.bat - Installs/deploys IFCOS using MSVC. +| 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 | vs-cfg.cmd - Utility file used by the build scripts