Build & Publish
This page is for developers who want to use OpenNest from source or contribute to it. It covers the toolchains, how to build the three native C++ engines and the three managed plug-ins (Grasshopper 1, the Rhino command, and Grasshopper 2), how to load the result into Rhino, and how publishing + docs deploy.
1. Prerequisites
| Tool | Version | Used for |
|---|---|---|
| CMake | 3.20+ | Configuring/building the native engines |
| MSVC | Visual Studio 2022 (Windows) | Compiling the C++ DLLs on Windows |
| Clang | default toolchain (macOS) | Compiling the C++ dylibs on macOS |
| .NET SDK | 8.0.x | Building all three managed plug-ins |
Note
Use the .NET 8.0.x SDK even though most targets are net7.0 / net48 — that's what CI uses, and the Rhino-9 command + Grasshopper 2 targets are net8.0. Building the net48 target on Windows works because the projects set EnableWindowsTargeting=true.
All three engines are fully self-contained — no external dependencies. Everything they need is vendored in-repo: Clipper2 and a minimal Boost.Polygon subset in src/opennest_cpp, pocketfft in src/nest_spectral_cpp.
2. Repository layout
| Folder | Builds / contains |
|---|---|
src/opennest_cpp |
nfp_nest.dll — NFP/SVGnest GA engine. Vendored Clipper2 (static) + a minimal Boost.Polygon subset. C++17. The C ABI is in src/capi/nfp_nest_capi.{h,cpp}. |
src/nest_physics_cpp |
nest_physics.dll — physics/overlap-relaxation (collision) solver. Header-only, threads only, no external deps. C++20. C ABI in nest_physics_capi.{h,cpp}. |
src/nest_spectral_cpp |
nest_spectral.dll — 3D mesh nesting by spectral/FFT packing (the OpenNest3D component). Vendored header-only pocketfft; CPU by default, optional cuFFT backend with -DNEST_SPECTRAL_CUDA=ON. C++17. C ABI in nest_spectral_capi.{h,cpp}. |
src/opennest_2 |
opennest_2.gha — the C# Grasshopper 1 plug-in (components) + the shared nesting/nest_geo/nest_sheets logic the other managed projects reuse. |
src/opennest_commands |
opennest_commands.rhp — the Rhino command plug-in (OpenNest command). Reuses opennest_2 via a project reference. |
src/opennest_gh2 |
opennest_gh2.rhp — the Grasshopper 2 plug-in (Rhino 9 WIP only). Built against the Rhino-9 GH2 SDK. |
tools/ |
gen_example_docs.py (generates the C++/C# example pages) + pack_examples.py (zips the per-language download projects). |
examples/downloads/{cpp,csharp,python} |
Self-contained, runnable example projects (one per API example) that are zipped into docs/api/*/downloads/. |
docs/ |
MkDocs Material source. overrides/ holds the header template override (inline section tabs). |
grasshopper_plugin/ |
Committed Yak manifests + .yak binaries per platform (opennest_win/, opennest_mac/). |
.github/ |
CI workflows (publish.yml, docs.yml), PR/issue templates. |
The C++ engine is the single source of truth
The Python package compas_nest does not keep its own copy of the engine — it vendors this repo as a git submodule and compiles src/opennest_cpp + src/nest_physics_cpp directly. Updating compas_nest to a new engine is just bumping that submodule, so the C++ here is the one source both packages build from.
3. Build the native engines
Each engine is its own standalone CMake project, so you can configure and build them one folder at a time. Each produces a self-contained shared library (no lib prefix, to match the P/Invoke names). Build all three — the .gha copies whichever ones it finds and warns about the rest (see §4).
Or build all three at once
The repo-root CMakeLists.txt is an ExternalProject superbuild over the three standalone projects (their own CMakeLists.txt files are used unchanged), and it drops each library in exactly the src/<engine>/build/Release/ path §4 expects:
cmake -S . -B build -A x64
cmake --build build --config Release
Drop -A x64 on macOS. The per-folder commands below are still the way to pass per-engine flags such as -DNEST_SPECTRAL_CUDA=ON or -DCMAKE_OSX_ARCHITECTURES.
Self-contained DLLs
Every engine links the static MSVC runtime (/MT) on Windows (and -static-libgcc -static-libstdc++ under GCC), so the resulting DLL/dylib has no external runtime dependency and loads in Rhino without the VC++ redistributable.
Windows (MSVC, x64)
# nfp_nest.dll -> src/opennest_cpp/build/Release/nfp_nest.dll
cmake -S src/opennest_cpp -B src/opennest_cpp/build -A x64
cmake --build src/opennest_cpp/build --config Release
# nest_physics.dll -> src/nest_physics_cpp/build/Release/nest_physics.dll
cmake -S src/nest_physics_cpp -B src/nest_physics_cpp/build -A x64
cmake --build src/nest_physics_cpp/build --config Release
# nest_spectral.dll -> src/nest_spectral_cpp/build/Release/nest_spectral.dll
cmake -S src/nest_spectral_cpp -B src/nest_spectral_cpp/build -A x64
cmake --build src/nest_spectral_cpp/build --config Release
macOS (Clang)
Drop -A x64. To match CI's universal build, pass the OSX arch flag (every engine needs it explicitly):
cmake -S src/opennest_cpp -B src/opennest_cpp/build -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build src/opennest_cpp/build --config Release
cmake -S src/nest_physics_cpp -B src/nest_physics_cpp/build -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build src/nest_physics_cpp/build --config Release
cmake -S src/nest_spectral_cpp -B src/nest_spectral_cpp/build -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build src/nest_spectral_cpp/build --config Release
Optional flags
nfp_nest vendors a minimal Boost.Polygon at src/opennest_cpp/third_party/boost_min; override it with -DBOOST_MIN_INCLUDE_DIR=<dir> to point at a full Boost install. nest_physics can additionally build a standalone CLI with -DNEST_PHYSICS_BUILD_CLI=ON. nest_spectral builds CPU-only by default; -DNEST_SPECTRAL_CUDA=ON adds the cuFFT backend (needs the CUDA toolkit to build, but not to run — the DLL still loads and falls back to the CPU path on machines without a GPU).
4. Build the managed plug-ins
All three are built with the .NET 8.0.x SDK:
dotnet build src/opennest_2/opennest_2.csproj -c Release # GH1 components (.gha)
dotnet build src/opennest_commands/opennest_commands.csproj -c Release # Rhino command (.rhp)
dotnet build src/opennest_gh2/opennest_gh2.csproj -c Release # Grasshopper 2 (.rhp, Rhino 9)
| Project | Output | Target frameworks | Loaded by |
|---|---|---|---|
opennest_2 |
opennest_2.gha |
net7.0-windows, net7.0, net48 |
Grasshopper 1 (Rhino 8, and classic GH in Rhino 9). All three TFMs build against the Grasshopper 8 SDK — net48 is the Rhino-8/.NET-Framework build, not a Rhino 7 build |
opennest_commands |
opennest_commands.rhp |
net8.0-windows, net8.0, net7.0-windows, net7.0, net48 |
Rhino's command line (the net8/RhinoCommon-9 flavor is what loads on Rhino 9) |
opennest_gh2 |
opennest_gh2.rhp |
net8.0-windows, net8.0 |
Grasshopper 2 (Rhino 9 WIP only), against the R9 GH2 SDK |
A PostBuild target copies each native engine library next to the .gha, straight out of its CMake build directory (guarded by Exists(...), so a missing source is skipped — with a build warning, see below):
| Native lib | Windows source | macOS source |
|---|---|---|
nest_physics |
../nest_physics_cpp/build/Release/nest_physics.dll |
../nest_physics_cpp/build/nest_physics.dylib |
nfp_nest |
../opennest_cpp/build/Release/nfp_nest.dll |
../opennest_cpp/build/nfp_nest.dylib |
nest_spectral |
../nest_spectral_cpp/build/Release/nest_spectral.dll |
../nest_spectral_cpp/build/nest_spectral.dylib |
Build the engines before the plug-in
Nothing is staged by hand any more. nest_physics.dll used to be copied from a checked-in DLL next to the csproj, so a rebuilt engine never reached the .gha (and never reached users); that file is gone and the copy now reads build/Release/ like its siblings. Skip step 3 and each missing engine produces a build warning — <engine> engine not built - <component> will fail at runtime — and the corresponding P/Invoke throws DllNotFoundException once you run the component in Rhino.
5. Run it locally
Users — Rhino Package Manager
Run _PackageManager, search OpenNest, install. The Package Manager picks the distribution matching your Rhino (it installs on Rhino 8 and 9), delivering the Grasshopper 1 components and the OpenNest Rhino command. No build required.
Both Rhino 8 .NET runtimes are covered
Rhino 8 on Windows runs on either .NET Core or .NET Framework 4.8 (_SetDotNetRuntime), and a .NET Core .gha cannot load under .NET Framework. The rh8_0-win package therefore ships both builds in per-framework folders (net48/ and net7.0/), so the components appear whichever runtime Rhino is on — no manual runtime switch. See §6.
Grasshopper 2 (Rhino 9 WIP) — manual load
GH2 is not auto-registered (a Yak-registered GH2 .rhp crashes Rhino's plug-in manager). The package drops the GH2 files in a grasshopper2/ subfolder; you load opennest_gh2.rhp by hand inside Grasshopper 2. See the Grasshopper 2 page for the walkthrough.
Developers — load your local build
Close Rhino, then copy the built .gha + the native DLLs into the Grasshopper Libraries folder (%AppData%\Grasshopper\Libraries), and the .rhps where Rhino/GH2 expect them.
Quit Rhino fully before refreshing
Rhino file-locks the loaded .gha/.rhp and native DLLs while running, so a rebuild cannot overwrite the installed copy. Close Rhino completely before copying a new build in, then restart.
6. How publishing works
Publishing is automated by the Build & Publish (Windows + macOS → Yak) workflow (.github/workflows/publish.yml).
- Trigger: every push to
mainauto-publishes a new version. A manual Actions → Run workflow is also available, with abumpinput (minor/patch/major). - Version auto-bump: the canonical version is
grasshopper_plugin/opennest_win/manifest.yml; the base is the max of that and the live Yak version, bumped to a 4-partX.Y.Z.0string in both manifests. - Build matrix: the
macjob builds the engines as universal (x86_64;arm64) dylibs; thewindowsjob (needs: mac) builds the Windows x64 DLLs + all three plug-ins, then assembles four Yak distributions of one package on the Windows runner. - Four distributions, one package: the same
OpenNestversion ships asrh8_0-win,rh8_0-mac(net7 / RhinoCommon-8 command) andrh9_0-win,rh9_0-mac(net8 / RhinoCommon-9 command). The Package Manager installs the one matching the running Rhino — so GH1 + the command auto-install on Rhino 8 and 9, Windows and macOS. -
rh8_0-winbecomes .NET multi-targeted in the next release: it is the only configuration where two runtimes exist, so that package uses Yak's per-framework folders (supported since Rhino 8.2, RH-76604) instead of a flat root:opennest-<next version>-rh8_0-win.yak # yak lowercases the package name and drops the manifest's # trailing 4th component: 2.93.0.0 -> opennest-2.93.0-… ├── manifest.yml # at the ROOT, outside the framework folders ├── icon.png ├── net48/ # .NET Framework 4.8 payload — .gha + managed deps + 3 engines + .rhp ├── net7.0/ # .NET Core payload — .gha + deps.json/runtimeconfig + 3 engines + .rhp └── grasshopper2/ # unchanged manual-load download (not a framework name, so Rhino skips it)The committed 2.93.0 packages are still flat
This tree describes what the workflow stages from the next release onwards. Unzip the
opennest-2.93.0-rh8_0-win.yakcommitted undergrasshopper_plugin/opennest_win/and it is flat —manifest.yml,icon.png,LICENSE,README.md,opennest_2.gha+ its.deps.json/.runtimeconfig.json/.pdb, the three engine DLLs,opennest_commands.rhp+ its two JSON files, andgrasshopper2/. Nonet48/, nonet7.0/.Rhino picks the folder matching the running runtime. The package root must contain no
.rhp/.gha— a single loadable file there switches Rhino back to root-only probing and thenet48/build is ignored again. The workflow asserts this after staging.net7.0is the folder name from McNeel's own guide and rolls forward to newer .NET (the payload is thenet7.0-windowsbuild). - The other three stay flat, on purpose:rh9_0-winbecauseopennest_commandshas no RhinoCommon-9net48build — itsnet48TFM resolves RhinoCommon through theGrasshopper 8.0.xpackage, so anet48/folder there would ship a.rhpbuilt against RhinoCommon 8, which Rhino 9 refuses to load (a.rhp's load is gated on the SDK it was built against, unlike a.gha). Rhino 9's/netfxpath is deprecated by McNeel anyway.rh8_0-mac/rh9_0-macare flat because macOS has no .NET Framework at all — Rhino for Mac is .NET Core only, so there is no second runtime to target. - Grasshopper 2 rides along, never auto-loaded:opennest_gh2.rhpis staged into each package'sgrasshopper2/subfolder (a plain download), so Rhino's plug-in manager never registers it — the user loads it manually in GH2 (Rhino 9 only). There is no separateOpenNest_GH2Yak package; a GH2.rhpregistered as a Yak plug-in makes Rhino's plug-in manager crash on startup. - The guid keywords are hand-written:yak buildderives its guid keyword by inspecting the package root only, and the multi-targetedrh8_0-winroot is empty by design — so yak emitskeywords: [], warns once and still exits 0. Grasshopper's Package Restore matches a missing component by plug-in name and then plug-in ID, and our name (opennest_2) never matches the package name (OpenNest), so those ids are the only restore path. They are declared by hand in bothgrasshopper_plugin/*/manifest.yml, and the Verify Yak package contents step re-reads them out of each built.yakand fails the release if either is gone. - Every shipped package is re-opened and checked before it is pushed. Releases up to and including 2.93.0.0 shipped anrh8_0-winpackage with a single .NET Core payload in a flat root —opennest_2.ghaandopennest_commands.rhpboth carryingTargetFrameworkAttribute .NETCoreApp,Version=v7.0, and nonet48/anywhere — which is why Rhino 8 Windows users left on the .NET Framework runtime saw the package install but no components, and had to run_SetDotNetRuntime. Nothing in the release failed while producing it. So Verify Yak package contents now unzips each.yakand, for every framework payload the workflow staged, asserts: the.gha, the.rhpand all three native engines are present; the assembly's ownTargetFrameworkAttributematches the folder it sits in (anet48/folder holding a .NET Core build is rejected, as is anet48/folder containing aruntimeconfig.json); thenet48/payload carries the six BCL assemblies .NET Framework 4.8 does not provide (System.Buffers,System.Memory,System.Numerics.Vectors,System.Runtime.CompilerServices.Unsafe,System.Drawing.Common,System.Resources.Extensions) — without them the.ghathrowsFileNotFoundExceptionon load, which fails exactly as visibly as shipping nonet48/at all; and that every file the build emitted for that TFM survived into the zip. A hard-coded policy list additionally requiresrh8_0-winto be multi-targeted, so deleting the framework-folder arguments from itsBuild-Pkgcall fails the release instead of quietly reintroducing the bug. - Release + tag: the run prepends this version's entry toCHANGELOG.md(the commit subjects since the previous tag — so don't hand-maintain that file, just write clear commit subjects), commits the bumped manifests + refreshed.yakbinaries + the changelog, tagsv<version>, and creates a GitHub Release with the.yakfiles attached. The Yak push is skipped whenYAK_TOKENis absent; everything else still runs. -[skip ci]guard: the bot's release commit ends with[skip ci], so its own commit tomaindoesn't re-trigger the run.
git pull after every push to main
CI pushes a release commit (version bump + refreshed packages) back to main, so your local main falls behind after each push. Run git pull --rebase before your next push.
7. Maintainer setup
- Workflow permissions — Settings → Actions → General → Read and write permissions (both workflows need
contents: write). YAK_TOKENsecret — Settings → Secrets and variables → Actions. Generate withyak login --ci. Without it the run still builds, bumps, tags, and releases — only the Yak push is skipped. Yak tokens expire; re-runyak login --ciif pushes start being skipped.- Pages source — Settings → Pages → Source = Deploy from a branch, branch =
gh-pages.
8. How the docs deploy
The Docs workflow (.github/workflows/docs.yml) builds and deploys this site.
- Trigger: push to
maintouchingdocs/**,mkdocs.yml, or the workflow (alsoworkflow_dispatch). - What it does: on
ubuntu-latest, installsmkdocs-materialand runsmkdocs gh-deploy --force --no-history, force-pushing a single-commitgh-pagesbranch that GitHub Pages serves. - Example pages are generated: the C++/C# example pages + the per-language download zips are produced by
tools/gen_example_docs.pyandtools/pack_examples.py— run them after changing an example underexamples/downloads/.
The published site is at https://petrasvestartas.github.io/OpenNest/.
9. Contributing
- Work on a feature/fix branch (or a fork).
- Open a pull request using
.github/PULL_REQUEST_TEMPLATE.md: builds locally (native engines from source + the.gha), tested in Rhino with real geometry, docs updated if behavior/inputs/options changed, and no prebuilt binaries committed (CI builds from source). - File issues with the templates in
.github/ISSUE_TEMPLATE/(attach a.gh/.ghxor.3dmfor bugs).
Note
Don't commit built binaries — CI builds everything from source and publishes on merge to main.
OpenNest is released under the MIT License (2019–2026 Petras Vestartas).