First docker build environment

First functional version, but it needs some improvements and fixes
identified as I've used it personally on one thing, and when an AI
(Claude) used it to work through the CI test errors.

I had the AI make a SKILL.md file. If the AI indicates it needs to
build the ifcopenshell binary, use this and let it rip.

(cherry picked from commit fa98aad469)
This commit is contained in:
Stephen Boddy
2026-07-12 18:43:27 +01:00
committed by Dion Moult
parent bda66c9ec2
commit 68b234f59b
9 changed files with 546 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
---
name: ifcopenshell-docker-build
description: >-
Build a real ifcopenshell_wrapper (.so + .py) and IfcConvert locally via
the docker/ifcos_env toolchain, then wire them into a checkout for
running C++-dependent parts of the test suite (geometry, the SWIG
wrapper stub, the C++ parser). Use whenever a task needs to compile
IfcOpenShell's C++ core rather than just read/patch source - e.g.
reproducing or fixing a bug in src/ifcgeom, src/ifcparse, src/ifcwrap,
or validating util/scripts/validate_stub.py against the actual
generated wrapper.
---
# Building IfcOpenShell locally with docker/ifcos_env
`docker/` is a small toolchain (see `docker/README.md` for the original
author's own description and design rationale - read that first for the
*why*; this file is the practical *how*, distilled from actually driving it
end-to-end) that mirrors the project's GitHub Actions build environment
locally, with a persistent container and ccache so repeat builds are fast.
Pure-Python changes don't need any of this - only reach for it when you need
a real compiled `_ifcopenshell_wrapper*.so` or `IfcConvert` binary.
## Placement
This `docker/` folder must live as a direct child of the repo root you want
to build (sibling of `src/`, `cmake/`, etc.) - `compose.yaml` and
`ifcos_env` resolve the repo via `../` relative to wherever `docker/`
itself sits, and bind-mount it into the container. If you're setting this
up in a fresh clone, copy the whole `docker/` directory there first.
## First-time setup
```bash
cd docker
./ifcos_env create # build the base image (shared across all your clones/checkouts by name, so usually instant after the first time anywhere)
./ifcos_env up # start the container, clone+unpack the third-party dependency cache (~10GB, one-time per container)
./ifcos_env build # full build: all deps + IfcParse + IfcGeom + IfcConvert + the Python wrapper, for one Python version
```
`PY_TGT` and `UNIQUE_ID` live in `docker/.env` - `PY_TGT` (e.g. `py-311`)
restricts the build to one Python version instead of building five;
`UNIQUE_ID` is a hash of the folder path, recalculated on every `up`, so
each checkout gets its own container/volumes automatically.
A full first build takes ~1.5 hours (mostly compiling IfcOpenShell's own
C++, not the cached third-party deps). After that, ccache makes incremental
rebuilds of a couple of touched `.cpp` files **under a minute**.
## Fast iteration
Pass a target to `build` to skip the parts you don't need:
```bash
./ifcos_env build IfcConvert # only the executables (IfcConvert, IfcGeomServer) - skips the Python wrapper entirely
./ifcos_env build IfcOpenShell-Python # only the SWIG Python wrapper - skips executables entirely
./ifcos_env build # no target = everything (needed the first time, or after touching shared headers)
```
Use this to keep the edit -> rebuild -> test loop fast when debugging: if
you're only touching `src/ifcgeom/`, build `IfcConvert`; if you're only
exercising the Python API, build `IfcOpenShell-Python`.
## Where the artifacts land
Build output goes to `<repo_root>/build/Linux/x86_64/install/` on the host
(bind-mounted, not just inside the container):
- `ifcopenshell/bin/IfcConvert` - the CLI binary
- `python-<version>/lib/python<X.Y>/site-packages/ifcopenshell/_ifcopenshell_wrapper*.so`
and `ifcopenshell_wrapper.py` - the compiled wrapper + its generated
Python glue
## Wiring the build into a checkout for testing
`_ifcopenshell_wrapper*.so` and `ifcopenshell_wrapper.py` are already
gitignored under `src/ifcopenshell-python/ifcopenshell/`, which is exactly
where a normal in-tree build would put them - copy the two files there:
```bash
SRC=build/Linux/x86_64/install/python-3.11.8/lib/python3.11/site-packages/ifcopenshell
cp "$SRC/_ifcopenshell_wrapper.cpython-311-x86_64-linux-gnu.so" src/ifcopenshell-python/ifcopenshell/
cp "$SRC/ifcopenshell_wrapper.py" src/ifcopenshell-python/ifcopenshell/
```
Then, to run the test suite against it:
```bash
export PATH="$PWD/build/Linux/x86_64/install/ifcopenshell/bin:$PATH" # for IfcConvert-dependent tests
cd src/ifcopenshell-python/test
PYTHONPATH="$PWD/.." python3.11 -m pytest -p no:pytest-blender .
```
(`-p no:pytest-blender` avoids the pytest-blender plugin trying to find a
`blender` executable and failing collection entirely, even for non-Blender
tests.) You'll need the matching Python version's `pip install`s too
(numpy, shapely, isodate, lark, tabulate, pytest, ... - whatever the
modules under test import) since this is a bare interpreter, not the
project's pixi env.
## Known gotchas (some fixed in this copy, watch for them if you're on an
## older/different copy of this script)
- **`try` is an unimplemented stub** - it prints a message and does
nothing. If you want the wrapper pushed straight into a Blender
extensions folder for manual testing, do the copy yourself (see the
README's example path) rather than relying on `try`.
- **`stop`/`down` removes the container**, it does not pause it (it's
literally `docker compose down`). Named volumes (ccache) and the
bind-mounted `build/` survive, so nothing is really lost - `up` just has
to recreate the container - but don't expect `docker ps -a` to still
show it afterwards.
- **The final "Package .zip archives" step of `build()` has a pre-existing
bash syntax error** unrelated to compilation - the actual build already
succeeded by that point (look for `Built IfcOpenShell...` in the output),
so this is safe to ignore if you only need the raw artifacts under
`build/.../install/`, not packaged release zips.
- **`ready_repo` originally cloned the third-party dependency cache one
directory level too shallow** (`../build` instead of `build`, relative to
the repo root), so `nix/build-all.py` would never find it and silently
rebuild every dependency (boost, OCCT, CGAL, ...) from source - "did the
build finish in ~1 minute, or is it grinding for 40+ minutes reconfiguring
OCCT" is the tell. Fixed in this copy; if `up` seems to be building
dependencies that should already be cached, check `ready_repo`'s `cd`
targets first.
- **On a brand-new `UNIQUE_ID`/folder, `up` used to fail on the very first
run** because `ready_repo` tried to `docker exec` into the container
before `docker compose up -d` had created it. Also fixed in this copy
(container creation now happens first); if you see
`Error response from daemon: No such container` right after "Getting the
repo ready to build...", just run `up` again.
- **Root-partition disk space**: only the bind-mounted `<repo>/build` lives
on the host filesystem your repo is checked out on. Anything the
container writes *outside* that mount (stray files, apt/dnf state, etc.)
lives in the container's own writable layer under Docker's data root
(commonly `/var/lib/docker`, i.e. usually your root partition) - keep an
eye on `df -h /` if you're running several of these containers at once.