diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 197ec09849..f1daa33078 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -326,7 +326,7 @@ if (WITH_ROCKSDB) else() message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists") endif() - # Our win/build-deps.cmd builds RocksDB separately per Debug/Release config into the + # Our win/build-deps.py builds RocksDB separately per Debug/Release config into the # same install prefix, so the imported target only ever has DEBUG and RELEASE listed in # IMPORTED_CONFIGURATIONS. On a multi-config generator (Visual Studio), CMake maps any # unmatched build config to the *first* entry of that list, which happens to be DEBUG diff --git a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst index f12d5c3545..92a489fe58 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst @@ -225,7 +225,7 @@ C++ Build Tools `__). .. code-block:: bat cd IfcOpenShell\win - build-deps.cmd + python build-deps.py run-cmake.bat 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 4139a60bb3..bf1e6e619f 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -9,13 +9,14 @@ import platform import re import shutil import subprocess +import sys import zipfile from pathlib import Path from zipfile import ZipFile def is_arm64() -> bool: - arch = os.environ.get("TARGET_ARCH", "").lower() + arch = os.environ.get("VS_PLATFORM", "").lower() if arch in ("arm64", "aarch64"): return True if arch in ("x64", "amd64", "x86_64"): @@ -217,7 +218,7 @@ def build() -> None: os.environ["PYTHON_VERSION"] = python_version print(f"Building for Python {python_version}...") subprocess.run( - [str(REPO_WIN / "build-deps.cmd"), build_generator(), "Release"], + [sys.executable, str(REPO_WIN / "build-deps.py"), build_generator(), "Release"], check=True, text=True, input="y\n", diff --git a/win/build-all.cmd b/win/build-all.cmd index 28ff229907..d39fd807b7 100644 --- a/win/build-all.cmd +++ b/win/build-all.cmd @@ -29,8 +29,8 @@ setlocal EnableDelayedExpansion call vs-cfg.cmd %1 if not %ERRORLEVEL%==0 GOTO :Error -:: Use "yes" trick to break the pause in build-deps.cmd -echo y | call .\build-deps %1 %2 +:: Use "yes" trick to break the pause in build-deps.py +echo y | python build-deps.py %1 %2 if not %ERRORLEVEL%==0 goto :EOF :: Same trick as in run-cmake.bat set ARGUMENTS=%* diff --git a/win/build-deps.cmd b/win/build-deps.cmd index f096395e42..688a89f064 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -162,6 +162,10 @@ call :PrintUsage call cecho.cmd 0 14 "Warning: You will need roughly 8 GB of disk space to proceed." echo. +call cecho.cmd 0 12 "WARNING: build-deps.cmd is deprecated since 09 Sep 2026 and will be removed very shortly." +call cecho.cmd 0 12 "Use `python build-deps.py` instead. It's intended to be a drop-in replacement, so exactly the same args apply." +echo. + call cecho.cmd black cyan "If you are not ready with the above: type `'n`' in the prompt below. Build proceeds on all other inputs!" set /p do_continue="> " diff --git a/win/build-deps.py b/win/build-deps.py new file mode 100644 index 0000000000..9dec3941f9 --- /dev/null +++ b/win/build-deps.py @@ -0,0 +1,290 @@ +# /// script +# [tool.ty.environment] +# # Lets ty resolve sibling imports (common, installers, etc). +# 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 shutil +import sys +from datetime import datetime +from pathlib import Path +from typing import NamedTuple + +from common import ( + BUILD_CFG_DEFAULT, + BUILD_CFGS, + BUILD_TYPE_DEFAULT, + BUILD_TYPES, + PROJECT_NAME, + REPO_ROOT, + SCRIPT_DIR, + BuildCfg, + BuildDepsCache, + BuildType, + C, + colorize, + is_on_off, + logger, + require_command, + validate_cmake_version, +) +from installers import ( + install_boost, + install_ccache, + install_cgal, + install_eigen, + install_json, + install_manifold, + install_mpfr, + install_mpir, + install_nuget, + install_occt, + install_opencollada, + install_proj, + install_python, + install_qt6, + install_rocksdb, + install_swig, + install_zstd, +) +from vs_cfg import VsCfgResult, get_vs_var, vs_cfg + + +class Args(NamedTuple): + generator: str | None + build_type_cfg: BuildCfg + build_type: BuildType + reuse_boost: bool + + +def print_build_config( + vs_cfg_vars: VsCfgResult, + build_type_cfg: BuildCfg, + build_type: BuildType, + ifcos_install_python: bool, + ifcos_install_qt6: bool, + ifcos_num_build_procs: int, +) -> None: + def field(text: str) -> str: + return colorize(text, C.PURPLE) + + logger.info(colorize("Script configuration:", C.GREEN)) + logger.info(field(f"* CMake Generator\t= '{vs_cfg_vars.generator.name}'")) + logger.info(" - Passed to CMake -G option.") + logger.info(field(f"* Target Platform\t= {vs_cfg_vars.vs_platform}")) + logger.info(" - Whether were doing 32-bit (Win32) or 64-bit (x64, ARM64) build. Passed to CMake -A option.") + logger.info(field(f"* Target Toolset Override\t= {vs_cfg_vars.vs_toolset_override}")) + logger.info(" - Passed to CMake -T option.") + logger.info(field(f"* Dependency Directory\t= {vs_cfg_vars.deps_dir}")) + logger.info(f" - The directory where {PROJECT_NAME} dependencies are fetched and built.") + logger.info(field(f"* Installation Directory = {vs_cfg_vars.install_dir}")) + logger.info(f" - The directory where {PROJECT_NAME} dependencies are installed.") + logger.info(field(f"* Build Config Type\t= {build_type_cfg}")) + logger.info(" - The used build configuration type for the dependencies.") + logger.info(" Defaults to RelWithDebInfo if not specified.") + if build_type_cfg == "MinSizeRel": + logger.warning(" WARNING: MinSizeRel build can suffer from a significant performance loss.") + logger.info(field(f"* Build Type\t\t= {build_type}")) + logger.info(" - The used build type for the dependencies (Build, Rebuild, Clean).") + logger.info(" Defaults to Build if not specified.") + logger.info(field(f"* IFCOS_INSTALL_PYTHON\t= {ifcos_install_python}")) + 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" + ) + 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." + ) + 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.") + logger.info(" Defaults to NUMBER_OF_PROCESSORS. Used also by other IfcOpenShell build scripts.\n") + + +def print_success(start_time: datetime) -> None: + logger.info("") + logger.info(colorize(f"{PROJECT_NAME} dependencies built.", C.GREEN)) + + end_time = datetime.now().replace(microsecond=0) + logger.info("") + logger.info(f"Build ended at {end_time}. Time elapsed {end_time - start_time}.") + + +def parse_args() -> Args: + parser = argparse.ArgumentParser() + parser.add_argument( + "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'." + ), + ) + parser.add_argument( + "build_type_cfg", + nargs="?", + default=BUILD_CFG_DEFAULT, + choices=BUILD_CFGS, + help="Build configuration type. Uses default if not provided.", + ) + parser.add_argument( + "build_type", + nargs="?", + default=BUILD_TYPE_DEFAULT, + choices=BUILD_TYPES, + help="Build type.", + ) + parser.add_argument( + "--log-level", + # TODO: relax default to INFO once things get more stable. + default="DEBUG", + choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), + help="Logging verbosity.", + ) + parser.add_argument( + "--reuse-boost", + action="store_true", + help=( + "Skip building Boost if it's already installed from this script's previous runs, instead of always rebuilding it. " + "Speeds up the build a bit when iterating/debugging this script." + ), + ) + args = parser.parse_args() + logger.setLevel(args.log_level) + return Args( + generator=args.generator, + build_type_cfg=args.build_type_cfg, + build_type=args.build_type, + reuse_boost=args.reuse_boost, + ) + + +def main() -> None: + ARGS = parse_args() + + 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) + + # Make sure vcvarsall.bat is called and dev env set is up. + get_vs_var("VSINSTALLDIR") + + # Check for cl.exe - at least the "Typical" Visual Studio 2015 installation does not include the C++ toolset by default, + # http://blogs.msdn.com/b/vcblog/archive/2015/07/24/setup-changes-in-visual-studio-2015-affecting-c-developers.aspx + if not shutil.which("cl"): + logger.error( + "cl.exe not in PATH. Make sure to select the C++ toolset when installing Visual Studio- cannot proceed." + ) + sys.exit(1) + + vs_cfg_vars = vs_cfg(ARGS.generator, REPO_ROOT) + build_deps_cache = BuildDepsCache(vs_cfg_vars) + + # Cache last used CMake generator and configurable dependency dirs for other scripts to use. + build_deps_cache.add_entry("GEN_SHORTHAND", vs_cfg_vars.gen_shorthand) + + # Make sure deps and install folders exists. + vs_cfg_vars.deps_dir.mkdir(parents=True, exist_ok=True) + vs_cfg_vars.install_dir.mkdir(parents=True, exist_ok=True) + + # User-configurable build options. + # TODO: add as cli options to make them appear in --help. + IFCOS_INSTALL_PYTHON = is_on_off(os.getenv("IFCOS_INSTALL_PYTHON"), default=True) + IFCOS_INSTALL_QT6 = is_on_off(os.getenv("IFCOS_INSTALL_QT6"), default=True) + IFCOS_NUM_BUILD_PROCS = int(os.getenv("IFCOS_NUM_BUILD_PROCS") or multiprocessing.cpu_count()) + + # Note BUILD_TYPE not passed, Clean e.g. wouldn't delete the installed files. + # TODO: consider inlining. + MSBUILD_MULTIPROC = ( + "/m", + f"/p:CL_MPCount={IFCOS_NUM_BUILD_PROCS}", + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true", + ) + MSBUILD_CMD = ("MSBuild.exe", "/nologo", *MSBUILD_MULTIPROC) + + # Check that required tools are in PATH. + # TODO: drop "powershell" later. + REQUIRED_COMMANDS = ("powershell", "git", "cmake", "7z") + for command in REQUIRED_COMMANDS: + require_command(command) + + validate_cmake_version() + + print_build_config( + vs_cfg_vars, + ARGS.build_type_cfg, + ARGS.build_type, + IFCOS_INSTALL_PYTHON, + IFCOS_INSTALL_QT6, + IFCOS_NUM_BUILD_PROCS, + ) + + logger.warning("Warning: You will need roughly 8 GB of disk space to proceed.\n") + logger.info( + "If you are not ready with the above: type 'n' in the prompt below. Build proceeds on all other inputs!" + ) + # TODO: add a `-y` option to skip this prompt. + do_continue = input("> ") + if do_continue == "n": + sys.exit(0) + + START_TIME = datetime.now().replace(microsecond=0) + logger.info(f"Build started at {START_TIME}.") + + nuget_exe = install_nuget(vs_cfg_vars.deps_dir) + install_ccache(vs_cfg_vars.deps_dir, nuget_exe, build_deps_cache) + install_proj(vs_cfg_vars, ARGS.build_type, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + install_mpir(vs_cfg_vars, vs_cfg_vars.deps_dir, vs_cfg_vars.install_dir, ARGS.build_type_cfg) + install_mpfr( + vs_cfg_vars, vs_cfg_vars.deps_dir, vs_cfg_vars.install_dir, ARGS.build_type_cfg, ARGS.build_type, MSBUILD_CMD + ) + install_boost(vs_cfg_vars, build_deps_cache, ARGS.build_type_cfg, IFCOS_NUM_BUILD_PROCS, ARGS.reuse_boost) + install_json(vs_cfg_vars.install_dir) + install_opencollada(vs_cfg_vars, ARGS.build_type, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + install_occt(vs_cfg_vars, ARGS.build_type, build_deps_cache, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + pythonhome = install_python(vs_cfg_vars, IFCOS_INSTALL_PYTHON, build_deps_cache, nuget_exe) + install_swig(vs_cfg_vars, ARGS.build_type, build_deps_cache, MSBUILD_MULTIPROC) + install_cgal(vs_cfg_vars, ARGS.build_type, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + install_eigen(vs_cfg_vars) + install_zstd(vs_cfg_vars, ARGS.build_type, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + install_rocksdb(vs_cfg_vars, ARGS.build_type, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + install_qt6(vs_cfg_vars, build_deps_cache, ARGS.build_type_cfg, IFCOS_INSTALL_QT6, pythonhome) + install_manifold(vs_cfg_vars, ARGS.build_type, build_deps_cache, ARGS.build_type_cfg, MSBUILD_MULTIPROC) + + print_success(START_TIME) + + +if __name__ == "__main__": + main() diff --git a/win/build-ifcopenshell.bat b/win/build-ifcopenshell.bat index fc66747747..7cf8b1065f 100755 --- a/win/build-ifcopenshell.bat +++ b/win/build-ifcopenshell.bat @@ -40,7 +40,7 @@ for /f "tokens=*" %%f in ('dir BuildDepsCache-*.txt /o:-n /t:a /b') do ( set GENERATOR=%1 if (%1)==() ( if not defined GEN_SHORTHAND ( - echo BuildDepsCache file does and/or GEN_SHORTHAND missing from it. Run build-deps.cmd to create it. + echo BuildDepsCache file does and/or GEN_SHORTHAND missing from it. Run build-deps.py to create it. set IFCOS_PAUSE_ON_ERROR=pause goto :Error ) diff --git a/win/common.py b/win/common.py new file mode 100644 index 0000000000..d691fa63cb --- /dev/null +++ b/win/common.py @@ -0,0 +1,151 @@ +############################################################################### +# # +# 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 . # +# # +############################################################################### +# +from __future__ import annotations + +import logging +import os +import re +import shlex +import shutil +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Literal, get_args + +if TYPE_CHECKING: + from vs_cfg import VsCfgResult + + +class C: + GREY = "\033[90m" + GREEN = "\033[92m" + PURPLE = "\033[95m" + YELLOW = "\033[33m" + RED = "\033[31m" + RESET = "\033[0m" + + +def colorize(text: str, color: str) -> str: + return f"{color}{text}{C.RESET}" + + +class ColorFormatter(logging.Formatter): + COLORS = { + logging.DEBUG: C.GREY, + logging.WARNING: C.YELLOW, + logging.ERROR: C.RED, + } + + def format(self, record: logging.LogRecord) -> str: + color = self.COLORS.get(record.levelno, C.RESET) + return f"{color}{super().format(record)}{C.RESET}" + + +handler = logging.StreamHandler() +handler.setFormatter(ColorFormatter("%(message)s")) +logging.basicConfig(level=logging.INFO, handlers=[handler]) +logger = logging.getLogger() + +SCRIPT_DIR = Path(__file__).resolve().parent +# TODO: used to access nix patch. +REPO_ROOT = SCRIPT_DIR.parent +PROJECT_NAME = "IfcOpenShell" + + +def run( + *cmd: str, + cwd: Path | None = None, + env: dict[str, str] | None = None, +) -> str: + logger.debug(f"$ {shlex.join(cmd)}") + return subprocess.check_output(cmd, cwd=cwd, env=env, text=True) + + +def run_streamed( + *cmd: str, + cwd: Path | None = None, + env: dict[str, str] | None = None, +) -> None: + logger.info(f"$ {shlex.join(cmd)}") + full_env = {**os.environ, **env} if env is not None else None + subprocess.check_call(cmd, cwd=cwd, env=full_env) + + +def is_on_off(value: str | None, *, default: bool) -> bool: + if value is None: + return default + lowered = value.lower() + if lowered in {"1", "on", "true", "yes"}: + return True + if lowered in {"0", "off", "false", "no"}: + return False + return default + + +BuildCfg = Literal["MinSizeRel", "Release", "RelWithDebInfo", "Debug"] +DebugOrRelease = Literal["Debug", "Release"] + +BUILD_CFGS = get_args(BuildCfg) +BUILD_CFG_DEFAULT: BuildCfg = "RelWithDebInfo" + +BuildType = Literal["Build", "Rebuild", "Clean"] + +BUILD_TYPES = get_args(BuildType) +BUILD_TYPE_DEFAULT: BuildType = "Build" + + +def debug_or_release(build_cfg: BuildCfg) -> DebugOrRelease: + return "Debug" if build_cfg == "Debug" else "Release" + + +def require_command(command: str) -> str: + path = shutil.which(command) + if not path: + logger.error(f"Required tool '{command}' not installed or not added to PATH.") + sys.exit(1) + return path + + +def validate_cmake_version() -> None: + MIN_CMAKE_VERSION = (3, 21, 0) + error_msg = f"CMake v{'.'.join(map(str, MIN_CMAKE_VERSION))} or higher is required" + + if not shutil.which("cmake"): + logger.error(error_msg) + sys.exit(1) + + cmake_version_output = run("cmake", "--version") + match = re.search(r"cmake version (\d+)\.(\d+)\.(\d+)", cmake_version_output) + if not match or tuple(map(int, match.groups())) < MIN_CMAKE_VERSION: + logger.error(error_msg) + sys.exit(1) + + +class BuildDepsCache: + def __init__(self, vs_cfg_vars: VsCfgResult) -> None: + if vs_cfg_vars.vs_toolset_override: + self.path = SCRIPT_DIR / f"BuildDepsCache-{vs_cfg_vars.vs_platform}-{vs_cfg_vars.vs_toolset_override}.txt" + else: + self.path = SCRIPT_DIR / f"BuildDepsCache-{vs_cfg_vars.vs_platform}.txt" + self.path.write_text("") + + def add_entry(self, key: str, value: str) -> None: + with self.path.open("a") as f: + f.write(f"{key}={value}\n") diff --git a/win/install-ifcopenshell.bat b/win/install-ifcopenshell.bat index ee6ed3b53c..54fb337d4a 100755 --- a/win/install-ifcopenshell.bat +++ b/win/install-ifcopenshell.bat @@ -39,7 +39,7 @@ for /f "tokens=*" %%f in ('dir BuildDepsCache-*.txt /o:-n /t:a /b') do ( set GENERATOR=%1 if (%1)==() ( if not defined GEN_SHORTHAND ( - echo BuildDepsCache file does and/or GEN_SHORTHAND missing from it. Run build-deps.cmd to create it. + echo BuildDepsCache file does and/or GEN_SHORTHAND missing from it. Run build-deps.py to create it. set IFCOS_PAUSE_ON_ERROR=pause goto :Error ) diff --git a/win/installers.py b/win/installers.py new file mode 100644 index 0000000000..e7f2e2d3be --- /dev/null +++ b/win/installers.py @@ -0,0 +1,1250 @@ +# /// 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 os +import re +import shutil +import subprocess +import sys +from pathlib import Path +from urllib.request import urlretrieve + +from common import ( + REPO_ROOT, + SCRIPT_DIR, + BuildCfg, + BuildDepsCache, + BuildType, + debug_or_release, + logger, + require_command, + run, + run_streamed, +) +from vs_cfg import CMAKE_GENERATORS, VS_TOOLSET_TO_VS_VER, VsCfgResult, get_vs_var + + +def build_cfg_marker_filepath(dependency_install_dir: Path, build_cfg: BuildCfg) -> Path: + return dependency_install_dir / f".{debug_or_release(build_cfg).lower()}_installation" + + +def is_already_installed(dependency_install_dir: Path, *, expected_build_cfg: BuildCfg | None = None) -> bool: + if not dependency_install_dir.exists(): + return False + + if expected_build_cfg is not None: + marker_filepath = build_cfg_marker_filepath(dependency_install_dir, expected_build_cfg) + if not marker_filepath.exists(): + return False + + logger.info(f"Found existing '{dependency_install_dir}', skipping") + return True + + +def mark_installation(installation_dir: Path, build_cfg: BuildCfg) -> None: + if not installation_dir.exists(): + logger.error(f"Directory '{installation_dir}' does not exist.") + sys.exit(1) + + marker_filepath = build_cfg_marker_filepath(installation_dir, build_cfg) + if marker_filepath.exists(): + return + + logger.info(f"Marking installation in '{installation_dir}' with '{marker_filepath.name}'.") + marker_filepath.touch() + + +def download_file(log_dependency_name: str, url: str, destination: Path) -> None: + destination = Path(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + + if destination.exists(): + logger.info(f"{log_dependency_name} already downloaded. Skipping.") + return + + logger.info(f"Downloading {log_dependency_name} into '{destination.parent}'") + urlretrieve(url, destination) + + +def extract_file(log_dependency_name: str, filename: Path, destination_dir: Path, dir_after_extraction: Path) -> None: + if dir_after_extraction.exists(): + logger.info(f"{log_dependency_name} already extracted into '{dir_after_extraction}'. Skipping.") + return + + logger.info(f"Extracting {log_dependency_name} into '{destination_dir}' from '{filename}'.") + sevenzip = require_command("7z") + subprocess.check_call([sevenzip, "x", "-bso0", "-bsp0", str(filename), f"-o{destination_dir}"]) + # TODO: assert that dir_after_extraction exists + + +def git_clone_and_checkout_revision( + log_dependency_name: str, + git_url: str, + dest_dir: Path, + revision: str | None = None, +) -> None: + if dest_dir.exists(): + logger.info(f"Cloning {log_dependency_name} is already cloned.") + return + + logger.info(f"Cloning {log_dependency_name} into '{dest_dir}'.") + run_streamed("git", "clone", git_url, str(dest_dir)) + + if revision: + run_streamed("git", "fetch", cwd=dest_dir) + logger.info(f"Checking out {log_dependency_name} revision {revision}.") + run_streamed("git", "reset", "--hard", cwd=dest_dir) + run_streamed("git", "checkout", revision, cwd=dest_dir) + + +def run_cmake( + log_dependency_name: str, + dependency_dir: Path, + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + *extra_args: str, + env: dict[str, str] | None = None, +) -> None: + logger.info(f"Running CMake for {log_dependency_name}.") + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_path.mkdir(parents=True, exist_ok=True) + + # TODO make deleting cache a parameter for this subroutine? We probably want to delete the + # cache always e.g. when we've had new changes in the repository. + cmake_cache_path = build_path / "CMakeCache.txt" + if build_type == "Rebuild" and cmake_cache_path.exists(): + cmake_cache_path.unlink() + + run_streamed( + "cmake", + "..", + "-G", + vs_cfg_vars.generator.name, + "-A", + vs_cfg_vars.vs_platform, + *extra_args, + cwd=build_path, + env=env, + ) + + +def build_cmake_project( + log_dependency_name: str, + build_dir: Path, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + logger.info(f"Building {log_dependency_name}. Please be patient, this will take a while.") + + run_streamed("cmake", "--build", ".", "--config", build_cfg, "--", *msbuild_multiproc, cwd=build_dir) + + +def install_cmake_project(log_dependency_name: str, build_dir: Path, build_cfg: BuildCfg) -> None: + logger.info(f"Installing {log_dependency_name} ({build_cfg}). Please be patient, this may take a while.") + + run_streamed("cmake", "--install", ".", "--config", build_cfg, cwd=build_dir) + + +def build_solution( + log_dependency_name: str, + msbuild_cmd: tuple[str, ...], + solution_path: Path, + build_cfg: BuildCfg, + vs_platform: str, + build_type: BuildType, + target: str | None = None, + compile_with_wpo: bool = False, +) -> None: + if target is None: + msbuild_target = build_type + elif build_type == "Build": + msbuild_target = target + else: + msbuild_target = f"{target}:{build_type}" + + logger.info(f"Building {msbuild_target} of {log_dependency_name}. Please be patient, this will take a while.") + + # Whole program optimization avoids Visual C++ hanging when compiling 32-bit release OCCT up to version 7.4.0. + properties = f"configuration={build_cfg};platform={vs_platform}" + if compile_with_wpo: + properties += ";WholeProgramOptimization=TRUE" + + run_streamed(*msbuild_cmd, str(solution_path), f"/p:{properties}", f"/t:{msbuild_target}") + + +def install_json(install_dir: Path) -> None: + # TODO: sync it with build-all + JSON_VERSION = "3.6.1" + DEPENDENCY_NAME = f"json-{JSON_VERSION}" + download_file( + DEPENDENCY_NAME, + f"https://github.com/nlohmann/json/releases/download/v{JSON_VERSION}/json.hpp", + install_dir / "json" / "nlohmann" / "json.hpp", + ) + + +def install_nuget(deps_dir: Path) -> Path: + NUGET_VERSION = "6.14.0" + DEPENDENCY_NAME = "nuget" + + nuget_in_path = shutil.which("nuget") + if nuget_in_path: + logger.info("Found existing nuget in PATH. Skipping.") + return Path(nuget_in_path) + + nuget_exe = deps_dir / f"nuget-{NUGET_VERSION}" / "nuget.exe" + download_file( + DEPENDENCY_NAME, + f"https://dist.nuget.org/win-x86-commandline/v{NUGET_VERSION}/nuget.exe", + nuget_exe, + ) + return nuget_exe + + +def install_ccache(deps_dir: Path, nuget_exe: Path, build_deps_cache: BuildDepsCache) -> Path: + CCACHE_VERSION = "4.12.1" + DEPENDENCY_NAME = "ccache" + ccache_install_dir = deps_dir / f"{DEPENDENCY_NAME}.{CCACHE_VERSION}" / "tools" + + if shutil.which("ccache"): + logger.info("Found existing ccache in PATH. Skipping.") + return ccache_install_dir + + build_deps_cache.add_entry("CCACHE_INSTALL_DIR", str(ccache_install_dir)) + + if ccache_install_dir.exists(): + logger.info(f"Found existing '{ccache_install_dir}', skipping") + return ccache_install_dir + + run(str(nuget_exe), "install", "ccache", "-Version", CCACHE_VERSION, "-OutputDirectory", str(deps_dir)) + return ccache_install_dir + + +def install_boost( + vs_cfg_vars: VsCfgResult, + build_deps_cache: BuildDepsCache, + build_cfg: BuildCfg, + ifcos_num_build_procs: int, + reuse_boost: bool, +) -> None: + # NOTE Boost < 1.64 doesn't work without tricks if the user has only VS 2017 installed and no earlier versions. + BOOST_VERSION = "1.92.0" + DEPENDENCY_NAME = f"Boost {BOOST_VERSION}" + + dependency_dir = vs_cfg_vars.deps_dir / f"boost-{BOOST_VERSION}" + dependency_install_dir = dependency_dir / "stage" / vs_cfg_vars.gen_shorthand + + # Remove leftover dir from before the switch to the archive's actual top-level folder naming. + # TODO: remove it a bit later. + old_dependency_dir = vs_cfg_vars.deps_dir / f"boost_{BOOST_VERSION.replace('.', '_')}" + if old_dependency_dir.exists(): + logger.info(f"Removing outdated '{old_dependency_dir}'.") + shutil.rmtree(old_dependency_dir) + + build_deps_cache.add_entry("BOOST_INSTALL_DIR", str(dependency_install_dir)) + + # NOTE Boost is fast to build with a limited set of libraries, so it's rebuilt by default. + # TODO: consider reuse_boost and release/debug installations. + if reuse_boost and is_already_installed(dependency_install_dir): + return + + BOOST_ZIP = f"boost-{BOOST_VERSION}-b2-nodocs.7z" + + download_file( + DEPENDENCY_NAME, + f"https://github.com/boostorg/boost/releases/download/boost-{BOOST_VERSION}/{BOOST_ZIP}", + vs_cfg_vars.deps_dir / BOOST_ZIP, + ) + + extract_file( + DEPENDENCY_NAME, + vs_cfg_vars.deps_dir / BOOST_ZIP, + vs_cfg_vars.deps_dir, + dependency_dir, + ) + + # Build Boost build script + if not (dependency_dir / "project-config.jam").exists(): + boost_css = dependency_dir / "boost.css" + if not boost_css.exists(): + logger.error(f"'{boost_css}' not found.") + sys.exit(1) + logger.info("Building Boost build script.") + run_streamed( + str(dependency_dir / "bootstrap.bat"), vs_cfg_vars.generator.boost_bootstrap_ver, cwd=dependency_dir + ) + + # TODO: this means that 'arm' and 'win32' platforms are not actually supported + # and can be dropped. + if vs_cfg_vars.is_vs_platform("x64"): + B2_ARCH_FEATURE = "x86" + elif vs_cfg_vars.is_vs_platform("ARM64"): + B2_ARCH_FEATURE = "arm" + else: + logger.error("Failed to identify architecture") + sys.exit(1) + + BOOST_LIBS = ( + "--with-system", + "--with-program_options", + "--with-regex", + "--with-thread", + "--with-date_time", + "--with-iostreams", + "--with-filesystem", + ) + + logger.info(f"Building {DEPENDENCY_NAME} {' '.join(BOOST_LIBS)}. Please be patient, this will take a while.") + + project_cache_jam = dependency_dir / "bin.v2" / "project-cache.jam" + if project_cache_jam.exists(): + project_cache_jam.unlink() + + run_streamed( + str(dependency_dir / "b2.exe"), + f"toolset={vs_cfg_vars.boost_toolset}", + f"architecture={B2_ARCH_FEATURE}", + f"address-model={vs_cfg_vars.arch_bits}", + "--abbreviate-paths", + f"-j{ifcos_num_build_procs}", + f"variant={debug_or_release(build_cfg).lower()}", + *BOOST_LIBS, + "stage", + f"--stagedir={dependency_install_dir}", + cwd=dependency_dir, + ) + + +def install_opencollada( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + DEPENDENCY_NAME = "OpenCOLLADA" + dependency_dir = deps_dir / "OpenCOLLADA" + + # TODO: we probably can install + # Always clone it, even if it's installed, because it contains xml headers we need. + # Use a fixed revision in order to prevent introducing breaking changes + # TODO: commit is almost 3 years behind the latest version used in nix/build-all.py, need to test and bump. + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://github.com/KhronosGroup/OpenCOLLADA.git", + dependency_dir, + "064a60b65c2c31b94f013820856bc84fb1937cc6", + ) + + if is_already_installed(install_dir / DEPENDENCY_NAME, expected_build_cfg=build_cfg): + return + + # TODO: add git reset and apply patches more cleanly. + + # Debug build of OpenCOLLADAValidator fails (https://github.com/KhronosGroup/OpenCOLLADA/issues/377) so + # disable it from the build altogether as we have no use for it. + if "#add_subdirectory(COLLADAValidator)" not in (dependency_dir / "CMakeLists.txt").read_text(): + run_streamed( + "git", + "apply", + "--reject", + "--whitespace=fix", + str(SCRIPT_DIR / "patches" / "OpenCOLLADA_CMakeLists.txt.patch"), + "--ignore-whitespace", + cwd=dependency_dir, + ) + + # std::tr1::unordered_map was a legacy MSVC compatibility shim kept around through VS2022's STL, but newer + # toolsets (e.g. VS2026/v145) no longer provide it, breaking the build with error C2039: 'tr1' is not a member of 'std'. + common_f_write_buffer_flusher = dependency_dir / "common" / "libBuffer" / "include" / "CommonFWriteBufferFlusher.h" + if ( + "typedef std::unordered_map MarkIdToFilePos;" + not in common_f_write_buffer_flusher.read_text() + ): + run_streamed( + "git", + "apply", + "--reject", + "--whitespace=fix", + str(REPO_ROOT / "nix" / "patches" / "opencollada" / "remove_tr1.patch"), + "--ignore-whitespace", + cwd=dependency_dir, + ) + + dependency_install_dir = install_dir / DEPENDENCY_NAME + + # TODO: inconsistency with nix/build-all - there we prepare pcre and libxml2 separately, + # while here we rely on the versions bundled with the OpenCOLLADA repo (Externals/pcre, + # Externals/LibXML). Worth reconciling at some point. + # + # NOTE Enforce that the embedded LibXml2 and PCRE are used as there might be problems with + # arbitrary versions of the libraries. + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DUSE_STATIC_MSVC_RUNTIME=0", + "-DCMAKE_DEBUG_POSTFIX=d", + "-DLIBXML2_LIBRARIES=", + "-DLIBXML2_INCLUDE_DIR=", + "-DPCRE_INCLUDE_DIR=", + "-DPCRE_LIBRARIES=", + # OpenCOLLADA is ancient at this point and allows cmake 2.6+, which results in an error + # in cmake 4, so we override the minimum cmake version. + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + ) + + # OpenCOLLADA's vcxproj files only define Debug/Release configurations (no RelWithDebInfo/MinSizeRel). + debug_or_release_cfg = debug_or_release(build_cfg) + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, debug_or_release_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, debug_or_release_cfg) + + mark_installation(dependency_install_dir, build_cfg) + + +def install_occt( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_deps_cache: BuildDepsCache, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + OCCT_VERSION = "7.8.1" + OCCT_VER = f"V{OCCT_VERSION.replace('.', '_')}" + + DEPENDENCY_NAME = f"Open CASCADE {OCCT_VERSION}" + # TODO: `new-layout` suffix can be dropped on the next OCCT version update, it's only needed + # to separate the legacy layout installation (used by version 7.8.1) from the new one. + new_layout_suffix = "-new-layout" if OCCT_VERSION == "7.8.1" else "" + OCCT_DEPENDENCY_INSTALL_NAME = f"opencascade-{OCCT_VERSION}{new_layout_suffix}" + dependency_install_dir = install_dir / OCCT_DEPENDENCY_INSTALL_NAME + + build_deps_cache.add_entry("OCC_INSTALL_DIR", str(dependency_install_dir)) + + if is_already_installed(dependency_install_dir, expected_build_cfg=build_cfg): + return + + dependency_dir = deps_dir / "occt_git" + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://github.com/Open-Cascade-SAS/OCCT", + dependency_dir, + OCCT_VER, + ) + + # Patching always blindly would trigger a rebuild each time. + cmake_lists_path = dependency_dir / "CMakeLists.txt" + # TODO: probably can use `git apply --reverse --check` for better validation. + if "IfcOpenShell" not in cmake_lists_path.read_text(): + run_streamed( + "git", + "apply", + "--ignore-whitespace", + str(SCRIPT_DIR / "patches" / f"{OCCT_VER}.patch"), + cwd=dependency_dir, + ) + assert "IfcOpenShell" in cmake_lists_path.read_text() + + # TODO: remove CMAKE_DEBUG_POSTFIX setting later. + # Temporarily explicitly set `CMAKE_DEBUG_POSTFIX` to empty to override it's previously being set to `d`. + # OCCT don't need it, since it's layout is separating debug and release build by different folders. + # + # OCCT 7.8.1 we're using is becoming old and it was targeting cmake 3.1+. + # To make it buildable on cmake 4, we override policy version, but it may have some quirks in the future + # and we may consider version bump. + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DBUILD_LIBRARY_TYPE=Static", + "-DCMAKE_DEBUG_POSTFIX=", + "-DBUILD_MODULE_Draw=0", + "-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF", + "-DUSE_XLIB=OFF", + "-DUSE_FREETYPE=OFF", + "-DUSE_OPENGL=OFF", + "-DUSE_GLES2=OFF", + "-DBUILD_USE_PCH=ON", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) + + # Fix upstream bug in cmake config file with unescaped quotes preventing configuration. + # The issue is fixed in 7.9.0+. + # See https://github.com/Open-Cascade-SAS/OCCT/pull/373 + occt_config_cmake = dependency_install_dir / "cmake" / "OpenCASCADEConfig.cmake" + occt_config_cmake.write_text(re.sub(r'/wd"(\d+)"', r"/wd\1", occt_config_cmake.read_text())) + + mark_installation(dependency_install_dir, build_cfg) + + +def install_proj( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + PROJ_VERSION = "9.4.1" + + if is_already_installed(install_dir / f"proj-{PROJ_VERSION}"): + return + + def install_sqlite3() -> None: + DEPENDENCY_NAME = "sqlite3" + SQLITE_VERSION = "3430100" + + (install_dir / "sqlite3" / "lib").mkdir(parents=True, exist_ok=True) + (install_dir / "sqlite3" / "bin").mkdir(parents=True, exist_ok=True) + (install_dir / "sqlite3" / "include").mkdir(parents=True, exist_ok=True) + + download_file( + DEPENDENCY_NAME, + f"https://www.sqlite.org/2023/sqlite-amalgamation-{SQLITE_VERSION}.zip", + deps_dir / f"sqlite-amalgamation-{SQLITE_VERSION}.zip", + ) + + extract_file( + DEPENDENCY_NAME, + deps_dir / f"sqlite-amalgamation-{SQLITE_VERSION}.zip", + deps_dir, + deps_dir / f"sqlite-amalgamation-{SQLITE_VERSION}", + ) + + sqlite_dir = deps_dir / f"sqlite-amalgamation-{SQLITE_VERSION}" + run("cl", "/c", "sqlite3.c", cwd=sqlite_dir) + run("lib", f"/OUT:{install_dir / 'sqlite3' / 'lib' / 'sqlite3.lib'}", "sqlite3.obj", cwd=sqlite_dir) + run( + "cl", + "sqlite3.c", + "shell.c", + "/link", + f"/out:{install_dir / 'sqlite3' / 'bin' / 'sqlite3.exe'}", + cwd=sqlite_dir, + ) + os.environ["PATH"] += os.pathsep + str(install_dir / "sqlite3" / "bin") + shutil.copy(sqlite_dir / "sqlite3.h", install_dir / "sqlite3" / "include") + + def _install_proj() -> None: + DEPENDENCY_NAME = "proj" + dependency_dir = deps_dir / f"proj-{PROJ_VERSION}" + + download_file( + DEPENDENCY_NAME, + f"https://download.osgeo.org/proj/proj-{PROJ_VERSION}.zip", + deps_dir / f"proj-{PROJ_VERSION}.zip", + ) + + extract_file( + DEPENDENCY_NAME, + deps_dir / f"proj-{PROJ_VERSION}.zip", + deps_dir, + dependency_dir, + ) + + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f'-DCMAKE_INSTALL_PREFIX={install_dir / f"proj-{PROJ_VERSION}"}', + f'-DCMAKE_PREFIX_PATH={install_dir / "sqlite3"}', + f'-DSQLite3_INCLUDE_DIR={install_dir / "sqlite3" / "include"}', + f'-DSQLite3_LIBRARY={install_dir / "sqlite3" / "lib" / "sqlite3.lib"}', + "-DENABLE_TIFF=OFF", + "-DENABLE_CURL=OFF", + "-DBUILD_PROJSYNC=OFF", + "-DBUILD_SHARED_LIBS=OFF", + "-DBUILD_TESTING=OFF", + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) + + install_sqlite3() + _install_proj() + + +def install_mpir(vs_cfg_vars: VsCfgResult, deps_dir: Path, install_dir: Path, build_cfg: BuildCfg) -> None: + DEPENDENCY_NAME = "mpir" + # `mpfr` depends on relative path `..\mpir\config.h`, so dependency name should match exactly. + dependency_dir = deps_dir / "mpir" + + if is_already_installed(install_dir / "mpir"): + return + + git_clone_and_checkout_revision(DEPENDENCY_NAME, "https://github.com/Andrej730/mpir-vs2026.git", dependency_dir) + run_streamed("git", "reset", "--hard", cwd=dependency_dir) + run_streamed("git", "clean", "-fdx", cwd=dependency_dir) + + ucrt_version = get_vs_var("UCRTVersion") + mpir_patch = (SCRIPT_DIR / "patches" / "mpir.patch").read_text() + for project_name in ("lib_mpir_cxx", "lib_mpir_gc"): + patch = mpir_patch.replace("sdk", ucrt_version).replace("fn", project_name) + logger.info(f"Applying mpir patch for {project_name}.") + subprocess.run( + ["git", "apply", "--unidiff-zero", "--ignore-whitespace"], + input=patch, + text=True, + cwd=dependency_dir, + check=True, + ) + + run_streamed( + "git", + "apply", + str(SCRIPT_DIR / "patches" / "mpir_runtime.patch"), + "--unidiff-zero", + "--ignore-whitespace", + cwd=dependency_dir, + ) + + if vs_cfg_vars.is_vs_platform("ARM64"): + logger.info("Applying ARM64 patches for mpir.") + run_streamed( + "git", + "apply", + str(SCRIPT_DIR / "patches" / "mpir-arm64-changes.patch"), + "--unidiff-zero", + "--ignore-whitespace", + cwd=dependency_dir, + ) + + vs_ver_short = str(vs_cfg_vars.generator.vs_ver)[2:] + msvc_dir = dependency_dir / "msvc" / f"vs{vs_ver_short}" + # mpir's vcxproj files only define Debug/Release configurations (no RelWithDebInfo/MinSizeRel). + debug_or_release_cfg = debug_or_release(build_cfg) + run_streamed( + str(msvc_dir / "msbuild.bat"), "gc", "LIB", vs_cfg_vars.vs_platform, debug_or_release_cfg, cwd=msvc_dir + ) + + lib_dir = dependency_dir / "lib" / vs_cfg_vars.vs_platform / debug_or_release_cfg + shutil.copytree(lib_dir, install_dir / "mpir", dirs_exist_ok=True) + + +def install_mpfr( + vs_cfg_vars: VsCfgResult, + deps_dir: Path, + install_dir: Path, + build_cfg: BuildCfg, + build_type: BuildType, + msbuild_cmd: tuple[str, ...], +) -> None: + DEPENDENCY_NAME = "mpfr" + dependency_dir = deps_dir / "mpfr" + + if is_already_installed(install_dir / "mpfr"): + return + + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://github.com/aothms/mpfr.git", + dependency_dir, + "2ebbe10fd029a480cf6e8a64c493afa9f3654251", + ) + run_streamed("git", "reset", "--hard", cwd=dependency_dir) + run_streamed("git", "clean", "-fdx", cwd=dependency_dir) + + ucrt_version = get_vs_var("UCRTVersion") + mpfr_patch = (SCRIPT_DIR / "patches" / "mpfr.patch").read_text() + patch = mpfr_patch.replace("sdk", ucrt_version).replace("fn", "lib_mpfr") + logger.info("Applying mpfr patch.") + subprocess.run( + ["git", "apply", "--unidiff-zero", "--ignore-whitespace"], + input=patch, + text=True, + cwd=dependency_dir, + check=True, + ) + + run_streamed( + "git", + "apply", + str(SCRIPT_DIR / "patches" / "mpfr_runtime.patch"), + "--unidiff-zero", + "--ignore-whitespace", + cwd=dependency_dir, + ) + + if vs_cfg_vars.is_vs_platform("ARM64"): + logger.info("Applying ARM64 patches for mpfr.") + run_streamed( + "git", + "apply", + str(SCRIPT_DIR / "patches" / "mpfr-arm64-changes.patch"), + "--unidiff-zero", + "--ignore-whitespace", + cwd=dependency_dir, + ) + + # mpfr's repo only ships these two prebaked solution folders, regardless of the actual VS version in use. + if vs_cfg_vars.generator.vs_ver == 2017: + mpfr_sln_dir = "build.vc15" + orig_generator = CMAKE_GENERATORS["Visual Studio 15 2017"] + else: + mpfr_sln_dir = "build.vs19" + orig_generator = CMAKE_GENERATORS["Visual Studio 16 2019"] + orig_platform_toolset = orig_generator.vs_toolset + + target_toolset = vs_cfg_vars.vs_toolset_override or vs_cfg_vars.generator.vs_toolset + for vcxproj in (dependency_dir / mpfr_sln_dir).rglob("*.vcxproj"): + vcxproj.write_text(vcxproj.read_text().replace(orig_platform_toolset, target_toolset)) + + # mpfr's vcxproj files only define Debug/Release configurations (no RelWithDebInfo/MinSizeRel). + debug_or_release_cfg = debug_or_release(build_cfg) + build_solution( + DEPENDENCY_NAME, + msbuild_cmd, + dependency_dir / mpfr_sln_dir / "lib_mpfr.sln", + debug_or_release_cfg, + vs_cfg_vars.vs_platform, + build_type, + target="lib_mpfr", + ) + + # Not all msvc projects in the solution are patched with the right sdk version, so the build + # can report success even when mpfr.lib itself failed to build. + lib_dir = dependency_dir / "lib" / vs_cfg_vars.vs_platform / debug_or_release_cfg + if not (lib_dir / "mpfr.lib").exists(): + logger.error(f"{lib_dir / 'mpfr.lib'} was not built.") + sys.exit(1) + + shutil.copytree(lib_dir, install_dir / "mpfr", dirs_exist_ok=True) + + +def install_qt6( + vs_cfg_vars: VsCfgResult, + build_deps_cache: BuildDepsCache, + build_cfg: BuildCfg, + ifcos_install_qt6: bool, + pythonhome: Path | None, +) -> None: + DEPENDENCY_NAME = "qt6" + QT6_VERSION = os.getenv("QT6_VERSION") + if QT6_VERSION: + logger.info(f"Using overridden QT6_VERSION: '{QT6_VERSION}'") + else: + QT6_VERSION = "6.8.3" + + build_deps_cache.add_entry("QT6_VERSION", QT6_VERSION) + + vs_toolset = vs_cfg_vars.vs_toolset_override or vs_cfg_vars.generator.vs_toolset + QT6_MSVC_YEAR = VS_TOOLSET_TO_VS_VER[vs_toolset] + # Qt has not published prebuilt msvc2026 binaries yet (aqt only lists win64_msvc2022_64 as of + # Qt 6.7-6.10). The v14x MSVC toolsets share a stable ABI/CRT, so fall back to the msvc2022 + # binaries until Qt ships msvc2026 ones. Revisit once `aqt list-qt windows desktop --arch ` + # shows a msvc2026 entry. + if QT6_MSVC_YEAR == 2026: + QT6_MSVC_YEAR = 2022 + + QT6_CROSS_COMPILING = False + QT6_HOST_ARCH = None + QT6_HOST_INSTALL_SUFFIX = None + if vs_cfg_vars.is_vs_platform("x64"): + QT6_ARCH = f"win64_msvc{QT6_MSVC_YEAR}_64" + QT6_INSTALL_SUFFIX = f"msvc{QT6_MSVC_YEAR}_64" + elif vs_cfg_vars.is_vs_platform("ARM64"): + QT6_CROSS_COMPILING = True + QT6_ARCH = f"win64_msvc{QT6_MSVC_YEAR}_arm64_cross_compiled" + QT6_INSTALL_SUFFIX = f"msvc{QT6_MSVC_YEAR}_arm64" + # Qt publishes Windows ARM64 packages as cross-compiled Qt. Even on the + # windows-11-arm runner, Qt CMake requires host tools such as moc/rcc. + # Use the x64 host tools; Windows 11 on Arm runs them through x64 + # emulation while cl.exe still builds ARM64 binaries against target Qt. + QT6_HOST_ARCH = f"win64_msvc{QT6_MSVC_YEAR}_64" + QT6_HOST_INSTALL_SUFFIX = f"msvc{QT6_MSVC_YEAR}_64" + else: + logger.error( + f"Automatic Qt6 installation is only supported for x64 and arm64 builds, " + f"got '{vs_cfg_vars.vs_platform}'." + ) + sys.exit(1) + + DEPENDENCY_INSTALL_NAME = f"qt6-{QT6_VERSION}-{QT6_INSTALL_SUFFIX}" + QT6_AQT_OUTPUT_DIR = vs_cfg_vars.install_dir / DEPENDENCY_INSTALL_NAME + QT6_INSTALL_DIR = QT6_AQT_OUTPUT_DIR / QT6_VERSION / QT6_INSTALL_SUFFIX + QT_DIR = QT6_INSTALL_DIR + + QT6_HOST_AQT_OUTPUT_DIR = None + QT6_HOST_INSTALL_DIR = None + QT_HOST_PATH = None + if QT6_CROSS_COMPILING: + assert QT6_HOST_INSTALL_SUFFIX is not None + QT6_HOST_AQT_OUTPUT_DIR = vs_cfg_vars.install_dir / f"qt6-{QT6_VERSION}-{QT6_HOST_INSTALL_SUFFIX}" + QT6_HOST_INSTALL_DIR = QT6_HOST_AQT_OUTPUT_DIR / QT6_VERSION / QT6_HOST_INSTALL_SUFFIX + QT_HOST_PATH = QT6_HOST_INSTALL_DIR + + QT6_CONFIG_DLL = "Qt6Cored.dll" if debug_or_release(build_cfg) == "Debug" else "Qt6Core.dll" + + if not ifcos_install_qt6: + logger.info("IFCOS_INSTALL_QT6 not 'TRUE', skipping installation of Qt6.") + return + + build_deps_cache.add_entry("QT6_INSTALL_DIR", str(QT6_INSTALL_DIR)) + build_deps_cache.add_entry("QT_DIR", str(QT_DIR)) + if QT6_CROSS_COMPILING: + assert QT6_HOST_INSTALL_DIR is not None + assert QT_HOST_PATH is not None + build_deps_cache.add_entry("QT6_HOST_INSTALL_DIR", str(QT6_HOST_INSTALL_DIR)) + build_deps_cache.add_entry("QT_HOST_PATH", str(QT_HOST_PATH)) + + QT6_TARGET_EXPECTED_FILES = [ + QT6_INSTALL_DIR / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake", + QT6_INSTALL_DIR / "bin" / QT6_CONFIG_DLL, + QT6_INSTALL_DIR / "lib" / "cmake" / "Qt6Svg" / "Qt6SvgConfig.cmake", + ] + QT6_TARGET_INSTALLED = all(path.exists() for path in QT6_TARGET_EXPECTED_FILES) + + QT6_HOST_EXPECTED_FILES = None + QT6_HOST_INSTALLED = True + if QT6_CROSS_COMPILING: + assert QT6_HOST_INSTALL_DIR is not None + QT6_HOST_EXPECTED_FILES = [ + QT6_HOST_INSTALL_DIR / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake", + QT6_HOST_INSTALL_DIR / "bin" / "moc.exe", + QT6_HOST_INSTALL_DIR / "bin" / "rcc.exe", + QT6_HOST_INSTALL_DIR / "lib" / "cmake" / "Qt6Svg" / "Qt6SvgConfig.cmake", + ] + QT6_HOST_INSTALLED = all(path.exists() for path in QT6_HOST_EXPECTED_FILES) + + if QT6_TARGET_INSTALLED and QT6_HOST_INSTALLED: + logger.info(f"Found existing '{QT6_INSTALL_DIR}' for {build_cfg}, skipping") + if QT6_CROSS_COMPILING: + logger.info(f"Found existing Qt host tools at '{QT6_HOST_INSTALL_DIR}', skipping") + mark_installation(QT6_INSTALL_DIR, build_cfg) + return + + if pythonhome is not None and (pythonhome / "python.exe").exists(): + AQT_PYTHON = str(pythonhome / "python.exe") + else: + AQT_PYTHON = require_command("python") + + run_streamed(AQT_PYTHON, "-m", "pip", "install", "--upgrade", "aqtinstall") + + def aqt_install_qt(arch: str, output_dir: Path) -> None: + run_streamed( + AQT_PYTHON, + "-m", + "aqt", + "install-qt", + "windows", + "desktop", + QT6_VERSION, + arch, + "-O", + str(output_dir), + "--archives", + "qtbase", + "qtsvg", + ) + + if not QT6_TARGET_INSTALLED: + # Keep the install lean by filtering archives: qtbase provides + # Core/Gui/Widgets (and the Qt6::CorePrivate target), qtsvg provides + # Qt6::Svg. Both are base-Qt archives, not add-on modules. + aqt_install_qt(QT6_ARCH, QT6_AQT_OUTPUT_DIR) + + if QT6_CROSS_COMPILING and not QT6_HOST_INSTALLED: + assert QT6_HOST_ARCH is not None + assert QT6_HOST_AQT_OUTPUT_DIR is not None + # windeployqt runs from the host Qt when cross-compiling ARM64, so the + # host Qt needs qtsvg too to deploy the Bonsai Viewer's Qt6Svg dependency. + aqt_install_qt(QT6_HOST_ARCH, QT6_HOST_AQT_OUTPUT_DIR) + + def require_exists(label: str, path: Path) -> None: + if not path.exists(): + logger.error(f"{label} did not produce '{path.name}' at '{path.parent}'.") + sys.exit(1) + + for path in QT6_TARGET_EXPECTED_FILES: + require_exists("Qt6 installation", path) + + if QT6_CROSS_COMPILING: + assert QT6_HOST_EXPECTED_FILES is not None + for path in QT6_HOST_EXPECTED_FILES: + require_exists("Qt6 host installation", path) + + # TODO: check if it's actually needed, since we don't use check it. + mark_installation(QT6_INSTALL_DIR, build_cfg) + + +def install_python( + vs_cfg_vars: VsCfgResult, ifcos_install_python: bool, build_deps_cache: BuildDepsCache, nuget_exe: Path +) -> Path | None: + """Returns PYTHONHOME, or None if IFCOS_INSTALL_PYTHON is not set.""" + PYTHON_VERSION = os.getenv("PYTHON_VERSION") + if PYTHON_VERSION: + logger.info(f"Using overridden PYTHON_VERSION: '{PYTHON_VERSION}'") + else: + PYTHON_VERSION = "3.11.7" + + if not ifcos_install_python: + logger.info("IFCOS_INSTALL_PYTHON not 'TRUE', skipping installation of Python.") + return None + + if not vs_cfg_vars.is_vs_platform("ARM64") and not vs_cfg_vars.is_vs_platform("x64"): + # nuget doesn't support providing architecture for packages. + logger.error("Automatic installation of Python for x86 builds is not supported,") + logger.error(f"please install Python {PYTHON_VERSION} manually and ensure that it is available in PATH.") + logger.error(f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}.exe") + sys.exit(1) + + if vs_cfg_vars.is_vs_platform("ARM64"): + PYTHONHOME = vs_cfg_vars.deps_dir / f"pythonarm64.{PYTHON_VERSION}" / "tools" + else: + PYTHONHOME = vs_cfg_vars.deps_dir / f"python.{PYTHON_VERSION}" / "tools" + + build_deps_cache.add_entry("PYTHONHOME", str(PYTHONHOME)) + + if PYTHONHOME.exists(): + logger.info(f"Found existing '{PYTHONHOME}', skipping installation.") + return PYTHONHOME + + nuget_package = "pythonarm64" if vs_cfg_vars.is_vs_platform("ARM64") else "Python" + run( + str(nuget_exe), + "install", + nuget_package, + "-Version", + PYTHON_VERSION, + "-OutputDirectory", + str(vs_cfg_vars.deps_dir), + ) + + return PYTHONHOME + + +def install_swig( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_deps_cache: BuildDepsCache, + msbuild_multiproc: tuple[str, ...], +) -> None: + SWIG_VERSION = "4.4.1" + DEPENDENCY_NAME = "SWIG" + dependency_dir = vs_cfg_vars.deps_dir / f"swig-{SWIG_VERSION}" + dependency_install_dir = vs_cfg_vars.install_dir / f"swig-{SWIG_VERSION}" + + build_deps_cache.add_entry("SWIG_INSTALL_DIR", str(dependency_install_dir)) + + if is_already_installed(dependency_install_dir): + return + + WIN_FLEX_BISON = "win_flex_bison-2.5.25" + + def install_bison() -> None: + DEPENDENCY_NAME = "win_flex_bison" + WIN_FLEX_BISON_ZIP = f"{WIN_FLEX_BISON}.zip" + + download_file( + DEPENDENCY_NAME, + f"https://github.com/lexxmark/winflexbison/releases/download/v2.5.25/{WIN_FLEX_BISON_ZIP}", + vs_cfg_vars.deps_dir / WIN_FLEX_BISON_ZIP, + ) + + extract_file( + DEPENDENCY_NAME, + vs_cfg_vars.deps_dir / WIN_FLEX_BISON_ZIP, + vs_cfg_vars.deps_dir / WIN_FLEX_BISON, + vs_cfg_vars.deps_dir / WIN_FLEX_BISON, + ) + + install_bison() + + SWIG_ZIP = f"swig-{SWIG_VERSION}.zip" + download_file( + DEPENDENCY_NAME, + f"https://github.com/swig/swig/archive/refs/tags/v{SWIG_VERSION}.zip", + vs_cfg_vars.deps_dir / SWIG_ZIP, + ) + + extract_file( + DEPENDENCY_NAME, + vs_cfg_vars.deps_dir / SWIG_ZIP, + vs_cfg_vars.deps_dir, + dependency_dir, + ) + + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DWITH_PCRE=OFF", + f"-DBISON_EXECUTABLE={vs_cfg_vars.deps_dir / WIN_FLEX_BISON / 'win_bison.exe'}", + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, "Release", msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, "Release") + + +def install_cgal( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + # TODO: bump to v5.6.3 to match nix/build-all.py. + # TODO: add CGAL_VERSION to the install path during the next version bump. + CGAL_VERSION = "5.5.5" + DEPENDENCY_NAME = "cgal" + dependency_dir = deps_dir / "cgal" + dependency_install_dir = install_dir / "cgal" + + if is_already_installed(dependency_install_dir): + return + + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://github.com/CGAL/cgal.git", + dependency_dir, + f"v{CGAL_VERSION}", + ) + + run_streamed("git", "reset", "--hard", cwd=dependency_dir) + run_streamed( + "git", + "apply", + "--ignore-whitespace", + str(SCRIPT_DIR / "patches" / "cgal_no_zlib.patch"), + cwd=dependency_dir, + ) + + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) + + +def install_eigen(vs_cfg_vars: VsCfgResult) -> None: + install_dir = vs_cfg_vars.install_dir + + # TODO: bump to 3.4.0 to match nix/build-all.py. + # TODO: add EIGEN_VERSION to the install path during the next version bump. + EIGEN_VERSION = "3.3.9" + DEPENDENCY_NAME = "Eigen" + dependency_dir = install_dir / DEPENDENCY_NAME + + if is_already_installed(dependency_dir): + return + + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://gitlab.com/libeigen/eigen.git", + dependency_dir, + EIGEN_VERSION, + ) + + +def install_zstd( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + ZSTD_VERSION = "1.5.7" + DEPENDENCY_NAME = "zstd" + dependency_dir = deps_dir / f"{DEPENDENCY_NAME}-{ZSTD_VERSION}" + dependency_install_dir = install_dir / DEPENDENCY_NAME + + if is_already_installed(dependency_install_dir): + return + + ZSTD_ZIP = f"zstd-{ZSTD_VERSION}.zip" + download_file( + DEPENDENCY_NAME, + f"https://github.com/facebook/zstd/archive/refs/tags/v{ZSTD_VERSION}.zip", + deps_dir / ZSTD_ZIP, + ) + + extract_file( + DEPENDENCY_NAME, + deps_dir / ZSTD_ZIP, + deps_dir, + dependency_dir, + ) + + cmake_source_dir = dependency_dir / "build" / "cmake" + run_cmake( + DEPENDENCY_NAME, + cmake_source_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DZSTD_BUILD_STATIC=ON", + "-DZSTD_BUILD_SHARED=OFF", + ) + + build_path = cmake_source_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) + + +def install_rocksdb( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + # TODO: bump to 10.4.2 to match nix/build-all.py. + ROCKSDB_VERSION = "9.11.2" + DEPENDENCY_NAME = "rocksdb" + dependency_dir = deps_dir / f"{DEPENDENCY_NAME}-{ROCKSDB_VERSION}" + dependency_install_dir = install_dir / DEPENDENCY_NAME + + if is_already_installed(dependency_install_dir, expected_build_cfg=build_cfg): + return + + ROCKSDB_ZIP = f"rocksdb-{ROCKSDB_VERSION}.zip" + download_file( + DEPENDENCY_NAME, + f"https://github.com/facebook/rocksdb/archive/refs/tags/v{ROCKSDB_VERSION}.zip", + deps_dir / ROCKSDB_ZIP, + ) + + extract_file( + DEPENDENCY_NAME, + deps_dir / ROCKSDB_ZIP, + deps_dir, + dependency_dir, + ) + + # see rocksdb/thirdparty.inc + # providing package is not supported on Windows. + # ZSTD_INCLUDE / ZSTD_LIB_DEBUG / ZSTD_LIB_RELEASE must be env vars - as cmake -D args they have no effect on MSVC. + zstd_include = install_dir / "zstd" / "include" + zstd_lib = install_dir / "zstd" / "lib" / "zstd_static.lib" + + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DROCKSDB_INSTALL_ON_WINDOWS=ON", + "-DFAIL_ON_WARNINGS=OFF", + "-DWITH_TESTS=OFF", + "-DWITH_TOOLS=OFF", + "-DWITH_BENCHMARK_TOOLS=OFF", + "-DWITH_CORE_TOOLS=OFF", + "-DROCKSDB_BUILD_SHARED=OFF", + "-DWITH_ZSTD=ON", + "-DPORTABLE=1", + "-DCMAKE_DEBUG_POSTFIX=_d", + env={ + "ZSTD_INCLUDE": str(zstd_include), + "ZSTD_LIB_DEBUG": str(zstd_lib), + "ZSTD_LIB_RELEASE": str(zstd_lib), + }, + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) + mark_installation(dependency_install_dir, build_cfg) + + +def install_manifold( + vs_cfg_vars: VsCfgResult, + build_type: BuildType, + build_deps_cache: BuildDepsCache, + build_cfg: BuildCfg, + msbuild_multiproc: tuple[str, ...], +) -> None: + deps_dir = vs_cfg_vars.deps_dir + install_dir = vs_cfg_vars.install_dir + + MANIFOLD_VERSION = "3.2.1" + DEPENDENCY_NAME = "manifold" + dependency_dir = deps_dir / f"{DEPENDENCY_NAME}-{MANIFOLD_VERSION}" + dependency_install_dir = install_dir / f"{DEPENDENCY_NAME}-{MANIFOLD_VERSION}" + + # TODO: test whether manifold links the debug CRT for Debug builds and needs separate + # Release/Debug install dirs instead of sharing one. + build_deps_cache.add_entry("MANIFOLD_INSTALL_PATH", str(dependency_install_dir)) + + if is_already_installed(dependency_install_dir): + return + + git_clone_and_checkout_revision( + DEPENDENCY_NAME, + "https://github.com/elalish/manifold.git", + dependency_dir, + f"v{MANIFOLD_VERSION}", + ) + + run_cmake( + DEPENDENCY_NAME, + dependency_dir, + vs_cfg_vars, + build_type, + f"-DCMAKE_INSTALL_PREFIX={dependency_install_dir}", + "-DBUILD_SHARED_LIBS=OFF", + "-DMANIFOLD_PAR=OFF", + "-DMANIFOLD_CROSS_SECTION=OFF", + "-DMANIFOLD_PYBIND=OFF", + "-DMANIFOLD_JSBIND=OFF", + "-DMANIFOLD_CBIND=OFF", + "-DMANIFOLD_TEST=OFF", + "-DMANIFOLD_EXPORT=OFF", + "-DMANIFOLD_DOWNLOADS=OFF", + ) + + build_path = dependency_dir / vs_cfg_vars.build_dir + build_cmake_project(DEPENDENCY_NAME, build_path, build_cfg, msbuild_multiproc) + install_cmake_project(DEPENDENCY_NAME, build_path, build_cfg) diff --git a/win/readme.md b/win/readme.md index 8efc322679..2370390a37 100644 --- a/win/readme.md +++ b/win/readme.md @@ -9,34 +9,32 @@ files that can also be invoked e.g. by double-clicking in the File Explorer. Usage Instructions ------------------ -Launch the proper Visual Studio command prompt, cd to the 'win' directory inside the IfcOpenShell directory and execute `build-deps.cmd` to fetch, build and install the dependencies. The batch file will print the requirements for a successful execution. The script allows a few user-configurable build options which are listed below. +Launch the proper Visual Studio command prompt, cd to the 'win' directory inside the IfcOpenShell directory and execute `python build-deps.py` to fetch, build and install the dependencies. The script will print the requirements for a successful execution. It allows a few user-configurable build options which are listed below (run `python build-deps.py --help` for the full list). -`build-deps.cmd` expects a CMake generator as `%1` and a build configuration type (`RelWithDebInfo`, `Release`, `MinSizeRel`, or `Debug`, defaults to `RelWithDebInfo`) as `%2`. If the generator is not provided, the generator is deduced from the MSVC environment variables. +`build-deps.py` expects a CMake generator as the 1st positional argument and a build configuration type (`RelWithDebInfo`, `Release`, `MinSizeRel`, or `Debug`, defaults to `RelWithDebInfo`) as the 2nd. If the generator is not provided, it is deduced from the MSVC environment variables. User-friendly CMake Visual Studio generator shorthands are supported. They are converted to the appropriate CMake generators and options. Shorthands are indeed the preferable way to specify the generator, since they allow a more accurate platform and toolset configuration. Here are some examples: ``` -"vs2013" => cmake -G "Visual Studio 12 2013" -A Win32 -"vs2013-x86" => cmake -G "Visual Studio 12 2013" -A Win32 -"vs2015-x64" => cmake -G "Visual Studio 14 2015" -A x64 -"vs2017-ARM64" => cmake -G "Visual Studio 15 2017" -A ARM64 "vs2019-x86-v141" => cmake -G "Visual Studio 16 2019" -A Win32 -T v141 +"vs2022-x64" => cmake -G "Visual Studio 17 2022" -A x64 +"vs2022-ARM64" => cmake -G "Visual Studio 17 2022" -A ARM64 ``` Of course not all Visual C++ compilers support any platform or toolset, refer to the Visual Studio and CMake documentation for this. If you do not specify a toolset, the compiler will use the default toolset for the version, i.e. vs2019 will use the v142 toolset. -A build type (`Build`, `Rebuild`, or `Clean`, defaults to `Build`) can be provided as `%3`. +A build type (`Build`, `Rebuild`, or `Clean`, defaults to `Build`) can be provided as the 3rd positional argument. -See `vs-cfg.cmd` if you wish to change the defaults. The batch file will create `deps\` and `deps-vs-[-]-installed\` directories to the project root. Debug and release builds of the dependencies can co-exist by simply running: +The script will create `_deps\` and `_deps-vs-[-]-installed\` directories in the project root. Debug and release builds of the dependencies can co-exist by simply running: ``` -> build-deps.cmd Debug -> build-deps.cmd +> python build-deps.py Debug +> python build-deps.py ``` -After the dependencies are build, execute `run-cmake.bat`. The batch file expects a CMake generator as `%1`, that is interpreted just like the `build-deps.cmd` 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 `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: ``` -> run-cmake.bat vs2015-x64 -DUSE_IFC4=1 -DBUILD_IFCPYTHON=0 +> run-cmake.bat 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.bat` accordingly**. The batch 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. @@ -47,18 +45,18 @@ files expect `%1` and `%2` in same fashion as above and possible extra parameter 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 +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, `IfcMax.dli`, needs to be copied manually to the 3ds Max's `plugins` folder. Using an already existing Python installation --------------------------------------------- -Let's say you have already installed 64-bit Python 3.5.1 to `C:\Python3`. +Let's say you have already installed 64-bit Python 3.13 to `C:\Python3`. Before building the dependencies, disable the script from installing Python: ``` > set IFCOS_INSTALL_PYTHON=FALSE -> buid-deps.cmd +> python build-deps.py ``` After building the dependencies, append Python installation directory information to the BuildDepsCache file @@ -74,14 +72,14 @@ Directory Structure ------------------ ``` .. -+---build-* - Created by run-cmake.bat, specific for a certain compiler and and target architecture -+---deps - Created by build-deps.cmd, common for all compilers -+---deps-*-installed - Created by build-deps.cmd, specific for a certain compiler and target architecture -+---installed-* - Created by installing the IFCOS project, specific for a certain compiler and target architecture ++---_build-* - Created by run-cmake.bat, 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 \---win | build-all.cmd - Runs all of the build scripts for IFCOS and it dependencies in a row without pauses -| build-deps.cmd - Fetches and builds all needed dependencies for IFCOS using MSVC -| BuildDepsCache-.txt - Cache file created by build-deps.cmd +| 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-type-cfg.cmd - Utility file used by the build scripts | install-ifcopenshell.bat - Installs/deploys IFCOS using MSVC. diff --git a/win/run-cmake.bat b/win/run-cmake.bat index bb8e90275f..185e6d2f2e 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -44,7 +44,7 @@ if (%1)==() ( ) if not defined GEN_SHORTHAND ( - echo BuildDepsCache file does not exist and/or GEN_SHORTHAND missing from it. Run build-deps.cmd to create it. + echo BuildDepsCache file does not exist and/or GEN_SHORTHAND missing from it. Run build-deps.py to create it. set IFCOS_PAUSE_ON_ERROR=pause goto :Error ) @@ -135,7 +135,7 @@ echo Arguments = %ARGUMENTS% echo. call cecho.cmd 0 10 "Dependency Environment Variables for %PROJECT_NAME%:" echo BOOST_INSTALL_DIR = %BOOST_INSTALL_DIR% -:: OCC_INCLUDE_DIR / OCC_LIBRARY_DIR are legacy vars, they're not defined by build-deps.cmd anymore. +:: OCC_INCLUDE_DIR / OCC_LIBRARY_DIR are legacy vars, they're not defined by build-deps.py anymore. echo OCC_INCLUDE_DIR = %OCC_INCLUDE_DIR% echo OCC_LIBRARY_DIR = %OCC_LIBRARY_DIR% echo OCC_INSTALL_DIR = %OCC_INSTALL_DIR% @@ -177,7 +177,7 @@ set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%BOOST_INSTALL_DIR%;%CCACHE_INSTALL_DI set CMake_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%USD_INSTALL_DIR%;%TBB_INSTALL_DIR% set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%OCC_INSTALL_DIR%;%CGAL_INSTALL_DIR% set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%GMP_INSTALL_DIR%;%MPFR_INSTALL_DIR% -:: TODO: drop this TRANSITION check once everyone has re-run build-deps.cmd with manifold support. +:: TODO: drop this TRANSITION check once everyone has re-run build-deps.py with manifold support. if defined MANIFOLD_INSTALL_PATH set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%MANIFOLD_INSTALL_PATH% if defined QT_DIR set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%QT_DIR% @@ -185,7 +185,7 @@ set QT_DIR_OPTION= if defined QT_DIR set QT_DIR_OPTION=-DQT_DIR="%QT_DIR%" set QT_HOST_PATH_OPTION= if defined QT_HOST_PATH set QT_HOST_PATH_OPTION=-DQT_HOST_PATH="%QT_HOST_PATH%" -:: TODO: drop this TRANSITION check once everyone has re-run build-deps.cmd with manifold support. +:: TODO: drop this TRANSITION check once everyone has re-run build-deps.py with manifold support. set WITH_MANIFOLD_OPTION= if defined MANIFOLD_INSTALL_PATH set WITH_MANIFOLD_OPTION=-DWITH_MANIFOLD=ON diff --git a/win/vs-cfg.cmd b/win/vs-cfg.cmd index d9bef94033..5cf0cdcbe4 100644 --- a/win/vs-cfg.cmd +++ b/win/vs-cfg.cmd @@ -188,7 +188,7 @@ IF NOT "%CMAKE_PATH%"=="" ( FOR /f "delims=" %%i in ('cmake --version ^| findstr /C:"cmake version 4"') DO GOTO :CMake3AndNewer ) -:: reject older CMake, see also build-deps.cmd +:: reject older CMake, see also build-deps.py echo "CMake v3.21.0 or higher is required" exit /b 1 diff --git a/win/vs_cfg.py b/win/vs_cfg.py new file mode 100644 index 0000000000..a17cbbffb5 --- /dev/null +++ b/win/vs_cfg.py @@ -0,0 +1,287 @@ +# /// 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 os +import sys +from pathlib import Path +from typing import Literal, NamedTuple + +from common import SCRIPT_DIR, logger, run + +ArchBits = Literal[32, 64] +# TODO: according to cmake docs, there's no `ARM` platform, only `ARM64`. +# So it can be deleted? +VsPlatform = Literal["Win32", "x64", "ARM", "ARM64"] +"""Platform names as used by Visual Studio (e.g. `cmake -A`), but fine to use for general platform checks too.""" +GeneratorType = Literal["EMPTY", "SHORTHAND", "FULL_NAME"] + + +class CMakeGeneratorInfo(NamedTuple): + name: str + """E.g. "Visual Studio 16 2019".""" + generator_num: int + """E.g. 16 for "Visual Studio 16 2019".""" + vs_ver: int + """E.g. 2019.""" + vc_ver: str + """E.g. "14.2".""" + vs_toolset: str + """Default toolset for this generator, e.g. "v142". + See `VsCfgResult.vs_toolset_override` for an explicit override. + """ + boost_bootstrap_ver: str + """E.g. "vc142".""" + + +CMAKE_GENERATORS = { + "Visual Studio 12 2013": CMakeGeneratorInfo("Visual Studio 12 2013", 12, 2013, "12.0", "v120", "vc120"), + "Visual Studio 14 2015": CMakeGeneratorInfo("Visual Studio 14 2015", 14, 2015, "14.0", "v140", "vc140"), + "Visual Studio 15 2017": CMakeGeneratorInfo("Visual Studio 15 2017", 15, 2017, "14.1", "v141", "vc141"), + "Visual Studio 16 2019": CMakeGeneratorInfo("Visual Studio 16 2019", 16, 2019, "14.2", "v142", "vc142"), + "Visual Studio 17 2022": CMakeGeneratorInfo("Visual Studio 17 2022", 17, 2022, "14.3", "v143", "vc143"), + "Visual Studio 18 2026": CMakeGeneratorInfo("Visual Studio 18 2026", 18, 2026, "14.5", "v145", "vc145"), +} + + +class VsCfgResult(NamedTuple): + generator: CMakeGeneratorInfo + vs_platform: VsPlatform + """E.g. "x64".""" + vs_toolset_override: str | None + """E.g. "v142", or None if not explicitly overridden via generator shorthand.""" + arch_bits: ArchBits + boost_toolset: str + """E.g. "msvc-14.2".""" + gen_shorthand: str + """E.g. "vs2019-x64".""" + deps_dir: Path + """E.g. Path("_deps").""" + install_dir: Path + """E.g. Path("_deps-vs2019-x64-installed").""" + build_dir: str + """E.g. "_build-vs2019-x64".""" + build_deps_cache_path: Path + """E.g. Path("BuildDepsCache-x64-v142.txt").""" + + PRINT_VARS_SKIP = {"gen_shorthand", "deps_dir", "install_dir", "build_dir", "build_deps_cache_path"} + + def print_vars(self) -> None: + fields = [f for f in self._fields if f not in self.PRINT_VARS_SKIP] + name_width = max(len(f) for f in fields) + for field in fields: + logger.info(f"{field.upper():<{name_width}}: [{getattr(self, field)}]") + + def is_vs_platform(self, platform: VsPlatform) -> bool: + """Compare against `vs_platform`, just to prevent typos.""" + return self.vs_platform == platform + + +VS_TOOLSET_TO_VC_VER = {info.vs_toolset: info.vc_ver for info in CMAKE_GENERATORS.values()} +"""E.g. "v142" -> "14.2".""" + +VS_TOOLSET_TO_VS_VER = {info.vs_toolset: info.vs_ver for info in CMAKE_GENERATORS.values()} +"""E.g. "v142" -> 2019.""" + + +class VSArchInfo(NamedTuple): + vs_platform: VsPlatform + arch_bits: ArchBits + + +VSCMD_ARG_TGT_ARCH_TO_INFO = { + "x86": VSArchInfo("Win32", 32), + "x64": VSArchInfo("x64", 64), + "arm": VSArchInfo("ARM", 32), + "arm64": VSArchInfo("ARM64", 64), +} + +VS_PLATFORM_TO_INFO = {info.vs_platform: info for info in VSCMD_ARG_TGT_ARCH_TO_INFO.values()} + + +VSVar = Literal[ + "VSINSTALLDIR", + "VisualStudioVersion", + "VSCMD_ARG_TGT_ARCH", + "UCRTVersion", +] + + +def get_vs_var(var: VSVar) -> str: + value = os.getenv(var) + if value is None: + logger.error("Visual Studio environment variables not set - cannot proceed.") + sys.exit(1) + return value + + +class VsCfg: + """Determines the CMake generator, VS platform/toolset, and derived paths for a build. + + Usage: `VsCfg.build(generator, repo_root)`. + """ + + @staticmethod + def build(generator: str | None, repo_root: Path) -> VsCfgResult: + generator = generator or "" + vs_platform: VsPlatform | None = None + vs_toolset: str | None = None + boost_toolset_ver: str | None = None + + generator_type = VsCfg._determine_generator_type(generator) + if generator_type in ("EMPTY", "SHORTHAND"): + generator, vs_platform, vs_toolset, boost_toolset_ver = VsCfg._parse_generator_shorthand( + generator, generator_type + ) + + # Just to be double sure. + assert generator in CMAKE_GENERATORS + VsCfg._ensure_cmake_supports_generator(generator) + + vs_platform = vs_platform or VsCfg._vs_platform_from_env() + generator_info = CMAKE_GENERATORS[generator] + arch_bits = VS_PLATFORM_TO_INFO[vs_platform].arch_bits + + boost_toolset = f"msvc-{boost_toolset_ver or generator_info.vc_ver}" + + gen_shorthand = f"vs{generator_info.vs_ver}-{vs_platform}" + if vs_toolset is not None: + gen_shorthand += f"-{vs_toolset}" + + # NOTE For IfcOpenShell we can build all of our deps both x86 and x64 using different VS versions in the same + # directories so no need for "-{vs_ver}-{vs_platform}" postfix. + deps_dir = repo_root / "_deps" + install_dir = repo_root / f"_deps-{gen_shorthand}-installed" + # build_dir is a relative build directory used for CMake-based projects. + build_dir = f"_build-{gen_shorthand}" + + if vs_toolset is not None: + build_deps_cache_path = SCRIPT_DIR / f"BuildDepsCache-{vs_platform}-{vs_toolset}.txt" + else: + build_deps_cache_path = SCRIPT_DIR / f"BuildDepsCache-{vs_platform}.txt" + + result = VsCfgResult( + generator=generator_info, + vs_platform=vs_platform, + vs_toolset_override=vs_toolset, + arch_bits=arch_bits, + boost_toolset=boost_toolset, + gen_shorthand=gen_shorthand, + deps_dir=deps_dir, + install_dir=install_dir, + build_dir=build_dir, + build_deps_cache_path=build_deps_cache_path, + ) + result.print_vars() + return result + + @staticmethod + def _generator_from_visual_studio_version() -> str: + # E.g. '17.0' -> 17. + vs_version = get_vs_var("VisualStudioVersion") + generator_num = int(vs_version.replace(".0", "")) + + for candidate, info in CMAKE_GENERATORS.items(): + if info.generator_num == generator_num: + logger.info( + f"Generator not passed, but VisualStudioVersion={vs_version} environment variable detected:" + ) + logger.info(f"using '{candidate}' as the generator.") + return candidate + + logger.error( + f"Generator is not provided and VisualStudioVersion='{vs_version}' is not supported - cannot proceed." + ) + sys.exit(1) + + @staticmethod + def _vs_platform_from_env() -> VsPlatform: + # Fall back to `VSCMD_ARG_TGT_ARCH`, the command prompt's target platform, set by vsvarsall. + VSCMD_ARG_TGT_ARCH = get_vs_var("VSCMD_ARG_TGT_ARCH") + return VSCMD_ARG_TGT_ARCH_TO_INFO[VSCMD_ARG_TGT_ARCH].vs_platform + + @staticmethod + def _ensure_cmake_supports_generator(generator: str) -> None: + cmake_help = run("cmake", "--help") + if generator not in cmake_help: + logger.error(f"The used CMake version does not support generator '{generator}' - cannot proceed.") + sys.exit(1) + + @staticmethod + def _determine_generator_type(generator: str) -> GeneratorType: + match generator: + case "": + return "EMPTY" + case _ if "vs20" in generator: + # E.g. 'vs2022-x64'. + return "SHORTHAND" + case _: + if generator not in CMAKE_GENERATORS: + supported_generators = ", ".join(repr(g) for g in CMAKE_GENERATORS) + logger.error( + f"Invalid or unsupported CMake generator string passed: '{generator}' - cannot proceed." + ) + logger.error(f"Supported CMake generator strings: {supported_generators}") + sys.exit(1) + return "FULL_NAME" + + @staticmethod + def _parse_generator_shorthand( + generator: str, generator_type: GeneratorType + ) -> tuple[str, VsPlatform | None, str | None, str | None]: + # E.g. '2022-x64'. + gen_shorthand = generator.replace("vs", "") + + vs_platform = None + vs_toolset = None + boost_toolset_ver = None + + # Order matters, e.g. "-arm64" also matches "-arm", so it must come last. + for arch, info in VSCMD_ARG_TGT_ARCH_TO_INFO.items(): + if f"-{arch}" in gen_shorthand.lower(): + vs_platform = info.vs_platform + + for toolset, vc_ver in VS_TOOLSET_TO_VC_VER.items(): + if f"-{toolset}" in gen_shorthand: + vs_toolset = toolset + boost_toolset_ver = vc_ver + + vs_ver = gen_shorthand[:4] + + if generator_type == "SHORTHAND": + for candidate, info in CMAKE_GENERATORS.items(): + if info.vs_ver == int(vs_ver): + generator = candidate + break + else: + supported_generators = ", ".join(repr(g) for g in CMAKE_GENERATORS) + logger.error(f"Invalid or unsupported CMake generator string passed: '{generator}' - cannot proceed.") + logger.error(f"Supported CMake generator strings: {supported_generators}") + sys.exit(1) + else: + generator = VsCfg._generator_from_visual_studio_version() + + return generator, vs_platform, vs_toolset, boost_toolset_ver + + +def vs_cfg(generator: str | None, repo_root: Path) -> VsCfgResult: + return VsCfg.build(generator, repo_root)