Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ on:
- '[0-9].[0-9][0-9]*'

jobs:
check_actor:
uses: ultimaker/cura-workflows/.github/workflows/check-actor.yml@main
secrets: inherit

conan-package:
needs: [check_actor]
if: needs.check_actor.outputs.proceed == 'true'
uses: ultimaker/cura-workflows/.github/workflows/conan-package.yml@main
with:
platform_wasm: true
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,10 @@ applications and integrate it into your own app.
[Button Internals]: https://img.shields.io/badge/Internals-00979D?style=for-the-badge&logoColor=white&logo=CodeReview
[Button Install]: https://img.shields.io/badge/Installation-e23345?style=for-the-badge&logoColor=white&logo=DocuSign

## Building on Windows

See the [Windows build guide](docs/building-windows.md) for Visual Studio 2022 and
an opt-in Visual Studio 2026 setup. The `scripts/build_windows.py` launcher defaults
to VS 2022; pass `--vs 2026` to select VS 2026. The guide covers prerequisites,
isolated build environments, and validation commands. Existing VS 2022 builds can keep their
current configuration. The [general build guide][Install] covers other platforms.
201 changes: 201 additions & 0 deletions docs/building-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Building CuraEngine on Windows

CuraEngine uses Conan 2 and CMake. Its Conan recipe accepts newer MSVC versions;
there is no VS 2022-only compiler check. The shared
[UltiMaker Conan configuration](https://github.com/Ultimaker/conan-config) uses
Ninja and inherits the compiler from the detected `default` profile.

## Choose a compiler

Install the **Desktop development with C++** workload, its MSVC tools, and a
Windows SDK. These instructions target Windows x86_64 with the selected IDE's
native toolset:

| Visual Studio | Conan `compiler.version` | Toolset | Developer command prompt |
| --- | --- | --- | --- |
| 2022 | `193` (MSVC 19.3x) or `194` (MSVC 19.4x) | `v143` | x64 Native Tools Command Prompt for VS 2022 |
| 2026 | `195` (MSVC 19.5x) | `v145` | x64 Native Tools Command Prompt for VS 2026 |

Existing VS 2022 users can keep their current tools, Conan home, profiles, and
build commands. Adding VS 2026 does not require migrating that environment.

For VS 2026, use Python 3.12 or newer, Conan **2.24 or newer within Conan 2**, CMake
**4.2 or newer**, and Ninja. The older `conan==2.7.1` command in the
[general build guide](https://github.com/Ultimaker/CuraEngine/wiki/Building-CuraEngine-From-Source)
predates MSVC 195. Conan 2.24 includes a
[VS 2026 detection fix](https://docs.conan.io/2/changelog.html).
CMake 4.2 also adds the
[Visual Studio 18 2026 generator](https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2018%202026.html).
The commands below retain the shared configuration's Ninja generator, which can
also be used when opening the checkout as a folder in Visual Studio.

## Build with the Windows helper

The optional `scripts/build_windows.py` launcher checks the active compiler and
prerequisite tool versions, installs the shared Conan configuration into a
compiler-specific cache on first use, detects a profile there, and invokes the
existing `conan build` recipe. Activate a Python environment containing Conan,
CMake, and Ninja, then run from the matching **x64 Native Tools Command Prompt**:

```bat
rem VS 2022 is the default. Use Conan 2.24+ in the helper environment.
python scripts\build_windows.py
python scripts\build_windows.py --vs 2022 --build-type Debug
```

From the VS 2026 prompt, with Conan >=2.24,<3 and CMake >=4.2 installed:

```bat
python scripts\build_windows.py --vs 2026
python scripts\build_windows.py --vs 2026 --build-type Debug --with-tests
```

The helper requires Conan >=2.24,<3 for **both** compiler selections so it can
disable root preset generation, and CMake >=3.23 for VS 2022. Keep an older
VS 2022 Conan environment for direct builds if needed; create a separate Python
environment for the helper instead of upgrading it in place. It rejects a
mismatched compiler instead of silently selecting another installation. It sets
`CC`/`CXX` and `CONAN_HOME` only for its subprocesses. It does not modify the
caller's Conan home, profiles, or root `CMakeUserPresets.json`.
It also pins Conan package builds to the active prompt's `VSINSTALLDIR`, so a
different installation of the same Visual Studio version is not selected.

Helper outputs are isolated under `build/windows-vs2022` and
`build/windows-vs2026`; the existing recipe places binaries in the nested
`build/Release` or `build/Debug` directory. Conan profiles and remotes stay in
`conan-home` under the corresponding output root. Package sources and binaries use
a shorter, per-checkout and per-compiler store below
`%LOCALAPPDATA%\CuraEngine\conan-storage`; this avoids dependency path-length
failures without sharing packages between checkouts or compilers. Both compiler
paths can therefore use the same checkout. Do not run two helper builds for the
same compiler concurrently.

`--with-tests` enables compilation of CuraEngine's unit tests without enabling
tests in dependencies built from source; run CuraEngine's tests separately after a
successful build. For the VS 2026 Debug example above:

```bat
call build\windows-vs2026\build\Debug\generators\conanrun.bat
ctest --test-dir build/windows-vs2026/build/Debug --output-on-failure --no-tests=error
build\windows-vs2026\build\Debug\CuraEngine.exe help
```

Use `windows-vs2022` or `Release` for the other selections. Errors from Conan,
CMake, or compilation stop the helper with a nonzero exit status. The helper's
own portable checks can be run with
`python -m unittest discover -s scripts -p "test_*.py"`; they simulate external
tools and do not replace native Windows build validation.

## Manual setup and builds

The direct Conan/CMake path remains available below. It uses the active shell's
Conan home and generates root presets, so keep its compiler checkouts separate.
The helper already isolates these files and does not require a separate checkout.

## Set up a separate VS 2026 environment

Use a separate checkout for each compiler so generated presets and CMake caches
do not overwrite those of an existing VS 2022 build. Open the **x64 Native Tools
Command Prompt for VS 2026** and change to the new checkout. All command blocks
below use **cmd.exe**, not PowerShell.

Create a Python environment and Conan home dedicated to this compiler. Choose
unused paths if these names already contain another environment:

```bat
py -3.12 -m venv "%USERPROFILE%\.venvs\cura-vs2026"
call "%USERPROFILE%\.venvs\cura-vs2026\Scripts\activate.bat"
python -m pip install "conan>=2.24,<3" "cmake>=4.2" ninja
set "CONAN_HOME=%USERPROFILE%\.conan2-cura-vs2026"
conan config install https://github.com/Ultimaker/conan-config.git
set "CC=cl"
set "CXX=cl"
where cl
conan profile detect --force
conan profile show
```

If using a newer Python, replace `-3.12` with its installed version. `CC` and
`CXX` make Conan detect `cl` from the chosen developer prompt, rather than select
another installed Visual Studio. Verify that both host and build profiles show
`os=Windows`, `arch=x86_64`, `compiler=msvc`, and `compiler.version=195`. The host
profile must also retain `curaengine*:compiler.cppstd=20` from `cura.jinja`.
If the compiler is wrong, stop and reopen the matching developer prompt before
regenerating the profile. Do not label a VS 2022 compiler as `195` manually.

For a new, isolated **VS 2022** environment, use its developer prompt and replace
`cura-vs2026` with `cura-vs2022` in the paths above. The newer Conan 2 tooling also
recognizes VS 2022; verify `compiler.version=193` or `194` instead. This optional
setup does not change the tools required by an existing VS 2022 environment.

In subsequent sessions, open the same developer prompt, activate the matching
Python environment, and set its `CONAN_HOME`, `CC`, and `CXX` again. Installing the
configuration and detecting the profile are only needed during setup or after an
intentional toolchain change. Close the prompt before switching compiler versions.

## Build Release or Debug

Run from the checkout root, stopping if any command fails. Release:

```bat
conan install . --build=missing --update
cmake --preset conan-release
cmake --build --preset conan-release
```

Debug uses a separate configuration directory:

```bat
conan install . --build=missing --update -s build_type=Debug
cmake --preset conan-debug
cmake --build --preset conan-debug
```

To run the Release executable with dependency DLLs available:

```bat
call build\Release\generators\conanrun.bat
build\Release\CuraEngine.exe help
```

Use `Debug` instead of `Release` for the Debug executable. Use a fresh developer
prompt when changing configurations so runtime DLL paths from the previous build
are not retained.

## Validate both compiler paths

The shared Conan configuration skips tests by default. To enable the existing
CuraEngine unit tests, repeat installation and configuration with testing enabled:

```bat
conan install . --build=missing -c "&:tools.build:skip_test=False"
cmake --preset conan-release
cmake --build --preset conan-release
call build\Release\generators\conanrun.bat
ctest --test-dir build/Release --output-on-failure --no-tests=error
build\Release\CuraEngine.exe help
```

The `&:` consumer scope enables tests only for CuraEngine, not for dependencies
built from source. Run this in each compiler's separate checkout/environment.
For Debug tests, add `-s build_type=Debug` to `conan install`, select
`conan-debug` for both CMake
commands, and use `build\Debug` for the runtime script, tests, and executable.
Record the compiler, Conan, and CMake versions with the results. Native Windows
builds and tests with both VS 2022 and VS 2026 are needed before treating a new
VS 2026 setup as validated; profile detection alone does not prove compatibility.

## Troubleshooting

- **MSVC 195 is not a valid setting:** check `conan --version`, `where conan`, and
`conan config home`. VS 2026 needs the newer Conan installation and its own
configuration, not the older VS 2022 environment.
- **Visual Studio 18 is not installed:** install VS 2026's C++ build tools or use
the VS 2022 environment/profile. Changing only the version number in a profile
does not install a compiler.
- **CMake reports a generator or compiler mismatch:** use the separate checkout
for that compiler. Do not reuse a build directory created by another toolchain.
- **A dependency fails:** `--build=missing` compiles packages without a matching
binary; it cannot fix an incompatible dependency. Save the first failing
package's name/version and error. Resolve that failure before claiming a full
CuraEngine build, without changing the working VS 2022 environment.
112 changes: 112 additions & 0 deletions scripts/build_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright (c) 2026 UltiMaker
# CuraEngine is released under the terms of the AGPLv3 or higher.
"""Build with the selected MSVC toolchain from its x64 Native Tools prompt."""

import argparse
import hashlib
import os
from pathlib import Path
import re
import subprocess
import sys


def runCommand(command: list[str], source: Path, environment: dict[str, str], capture: bool = False) -> str:
result = subprocess.run(
command,
cwd=source,
env=environment,
check=True,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.STDOUT
)
return result.stdout or ""


def getVersion(command: list[str], source: Path, environment: dict[str, str]) -> tuple[int, int, int]:
output = runCommand(command, source, environment, capture=True)
match = re.search(r"\b(\d+)\.(\d+)\.(\d+)\b", output)
if not match:
raise RuntimeError("Cannot read version from {}: {}".format(command[0], output.strip()))
return int(match[1]), int(match[2]), int(match[3])


def getConanStorage(source: Path, visual_studio: str, environment: dict[str, str]) -> Path:
local_app_data = environment.get("LOCALAPPDATA", "")
if not local_app_data or not Path(local_app_data).is_absolute():
raise RuntimeError("Cannot find LOCALAPPDATA for the isolated Conan package cache.")
source_id = hashlib.sha256(str(source.resolve()).casefold().encode("utf-8")).hexdigest()[:12]
return Path(local_app_data) / "CuraEngine" / "conan-storage" / source_id / "vs{}".format(visual_studio)


def buildWindows(source: Path, visual_studio: str, build_type: str, with_tests: bool) -> None:
if sys.platform != "win32":
raise RuntimeError("Run this script on Windows from an x64 Native Tools Command Prompt.")
environment = os.environ.copy()
if environment.get("VSCMD_ARG_TGT_ARCH", "").lower() != "x64":
raise RuntimeError("Open the x64 Native Tools Command Prompt for VS {}.".format(visual_studio))

compiler = getVersion(["cl", "/?"], source, environment)
valid_compiler = compiler[0] == 19 and (
30 <= compiler[1] < 50 if visual_studio == "2022" else 50 <= compiler[1] < 60)
if not valid_compiler:
raise RuntimeError("Active MSVC {} does not match VS {}. Open its developer prompt.".format(
".".join(map(str, compiler)), visual_studio))
visual_studio_path = environment.get("VSINSTALLDIR", "")
if not visual_studio_path or not Path(visual_studio_path).is_dir():
raise RuntimeError("Cannot find the active Visual Studio installation. Reopen its developer prompt.")
visual_studio_path = str(Path(visual_studio_path))

output = source / "build" / "windows-vs{}".format(visual_studio)
conan_storage = getConanStorage(source, visual_studio, environment)
# Keep profile detection and generated files away from existing direct Conan builds.
environment["CONAN_HOME"] = str(output / "conan-home")
environment["CC"] = "cl"
environment["CXX"] = "cl"
conan = getVersion(["conan", "--version"], source, environment)
minimum_conan = (2, 24, 0)
if conan[0] != 2 or conan < minimum_conan:
raise RuntimeError("VS {} requires Conan >= {} and < 3 for this build helper.".format(
visual_studio, ".".join(map(str, minimum_conan))))
minimum_cmake = (3, 23, 0) if visual_studio == "2022" else (4, 2, 0)
if getVersion(["cmake", "--version"], source, environment) < minimum_cmake:
raise RuntimeError("VS {} requires CMake >= {} for this build helper.".format(
visual_studio, ".".join(map(str, minimum_cmake))))
runCommand(["ninja", "--version"], source, environment)

profiles = Path(environment["CONAN_HOME"]) / "profiles"
if not all((profiles / name).is_file() for name in ("cura.jinja", "cura_build.jinja")):
runCommand(["conan", "config", "install", "https://github.com/Ultimaker/conan-config.git"],
source, environment)
runCommand(["conan", "profile", "detect", "--force"], source, environment)
runCommand([
"conan", "build", str(source), "--build=missing", "--output-folder", str(output),
"-cc", "core.cache:storage_path={}".format(conan_storage),
"-pr:h", "cura.jinja", "-pr:b", "cura_build.jinja", "-s:h", "build_type={}".format(build_type),
"-c:h", "tools.cmake.cmaketoolchain:generator=Ninja",
"-c:b", "tools.cmake.cmaketoolchain:generator=Ninja",
"-c:h", "tools.cmake.cmaketoolchain:user_presets=",
"-c:h", "&:tools.build:skip_test={}".format(not with_tests),
"-c:h", "tools.microsoft.msbuild:installation_path={}".format(visual_studio_path),
"-c:b", "tools.microsoft.msbuild:installation_path={}".format(visual_studio_path),
], source, environment)
print("Build completed: {}".format(output / "build" / build_type))


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vs", choices=("2022", "2026"), default="2022", help="Visual Studio version (default: 2022)")
parser.add_argument("--build-type", choices=("Release", "Debug"), default="Release")
parser.add_argument("--with-tests", action="store_true", help="Build unit tests as well as CuraEngine")
args = parser.parse_args()
try:
buildWindows(Path(__file__).resolve().parents[1], args.vs, args.build_type, args.with_tests)
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
print("Build failed: {}".format(error), file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading