Skip to content

Moving build system to pixi - #2037

Merged
MJohnson459 merged 96 commits into
masterfrom
pixi
Aug 21, 2026
Merged

Moving build system to pixi#2037
MJohnson459 merged 96 commits into
masterfrom
pixi

Conversation

@MJohnson459

@MJohnson459 MJohnson459 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This moves the entire ODM buildsystem to pixi. This allows us to use prebuild binaries for our libraries massively cutting down the build time, while also introducing a single pixi.toml file to store dependencies.

With this change, anyone should be able to build and run ODM like this:

pixi run build
pixi run odm -- --project-path ~/Projects/datasets/ Block-Michenzani   # e.g.

This should work on Linux (x86, aarch64), Windows, Mac. GPU support is also available by adding a -e gpu argument to the commands.

Changes

  • All non-forked dependencies that are on conda-forge we now use. As much as possible I have pinned the exact version we used before, although that wasn't always possible. GDAL, PDAL, Ceres, and OpenCV are the main ones.
  • In order to preserve versions, there are a few .patch files introduced in SuperBuild. These are mostly lifted from upstream where available or the minimal change I could make to get it working. If we update versions, most of these would go away. I chose to have the patch files here rather than in our forks just to keep this PR consistent and stand-alone. In the future we may prefer to move these patches to our forks.
  • I've updated the github actions to primarily use pixi and added a matrix to test different platforms - Linux (x86, aarch64), Windows, OSX.
  • The Dockerfiles have changed substantially however the final image should be very close to what they were before.
  • The GPU feature enables GPU acceleration for any library that has the option (libarrow, onnxruntime, etc). This might affect behaviour but it definitely affects the size of the installer. There is a trade-off to be made - if the performance difference is worth the size.
  • I've used Ninja on both Linux and Windows to have a single build tool. This works, but I'm not sure if it has a performance impact on Windows. It should give a big speedup to iterative build times as well.

Testing

To ensure this doesn't cause any major regressions, I've been keeping an eye on the following canaries:

  1. Build time from cold - my last test had this go from 40 minutes to 7m on my machine. CI it still takes 20m to 30m depending on the platform.
  2. Checked the output from two datasets, Block-Michenzani and odm_data_aukerman. I have been running these with no additional flags and to my untrained eye the results look pretty consistent.
  3. To maintain feature parity, I've been testing on Linux and Windows with MacOS and ARM64 builds tested in CI. The GPU image I've tested on Windows both natively and in Docker.
  4. The Docker image size has been mostly stable. They have increased slightly but not to a worrying degree.

Missing testing

While I have tested the basics, there are some areas I'm aware I haven't tested enough yet:

  • Various flags and arguments - i.e. non-default features
  • Complicated and larger datasets
  • An experts view on differences on output
  • Mac runtime
  • Arm runtime
  • Runtime performance on all platforms
  • Windows installer

Future work

  • Conda creates portable binaries by default which is good in one sense but will have a performance cost. We could do a pass to improve this using optimized packages if its an issue.
  • For every dependency we had prior, I have tried to tag that exact version in the pixi file. This works but we lose some benefits of pixi such as dependency resolution. We should go through each dependency and bump it as much as we can. We should also loosen the version range as we do this and rely on the pixi.lock file to lock the exact versions.
  • Create and upload ODM to a prefix.dev channel - this would make it a one liner to install and run on any platform.

MJohnson459 added 30 commits May 6, 2026 13:11
A devcontainer is a small wrapper around Docker to make it easy to set
up a clean development environment. Most IDE's and code editors support
these now as well as many cloud environments.

I've included a very simple environment to start with which we can
extend as necessary later to streamline the build process.
This diff adds the pixi file and updates some SuperBuild scripts to
compile. With this we can run `pixi install` and `pixi run build` and
the compilation works.
Output report from before and after pixi are almost identical.
roughly a 6 minute build now
@spwoodcock

Copy link
Copy Markdown
Member

@spwoodcock It's a good point, there's a lot going on in this PR so thanks for bringing it up. I'll try to explain what I've done and you can check me, I spent a bit of time trying to work this out myself.

Conda

For the first piece of the puzzle, Conda compiles any C++ on Linux with these flags (trimmed down for clarity):

CXXFLAGS=-std=c++17 -march=nocona -mtune=haswell -O2

(I will note the documentation on this is lacking right now)

The -march=nocona is circa 2004 CPU which I believe is actually even older than the nehalem one ODM used. This means the pre-compiled libs won't use AVX, etc.

However, unlike the previous ODM build it specifies the -mtune flag which allows certain optimisations for a haswell to be used, without breaking backwards compatibility. This combined with -O2 means I don't really know how the performance compares - I've at least run it locally and the difference isn't noticeable but I haven't measured the performance yet (I'm extending oats to capture more info for this).

Finally, this applies to the generic conda-forge build farm. It's not clear to me if individual libraries can override this or how strict they are about enforcing these defaults.

Superbuild

For the second piece, there are still a few libraries we compile because we have forks. Two of these have -march=native set in their cmake files which overrides anything we do - openmvs and OpenPointClass. The former we patch out in SuperBuild/cmake/mvstexturing.patch. The latter has a PORTABLE_BUILD flag we set in SuperBuild/cmake/External-OpenPointClass.cmake.

I've not explicitly set an -march or -mtune for our Superbuild yet (default for gcc is normally x86-64 on ubuntu) but with OpenPointClass setting nehalem that becomes our effective base.

Summary

The best-practice for C++/cmake libraries is to omit -march entirely and leave it up to the end application to supply flags. I've tried to catch the ones that don't do that and patch them where I can.

Conda sets -march=nocona, Superbuild sets -march=nehalem at most. Overall that means nehalem is likely our baseline, the same as it was before.

It also means we are likely missing some optimisations, but its not as clear cut how much the -mtune makes up for the older instruction set. I mentioned as a next step but there is a way for conda-forge libs to be optimised for architecture but I haven't looked into it much yet. I was holding off until we have a way to measure it.

Does that all make (some) sense?

It certainly does! That's much clearer 😄 Thank you - I should probably close the linked issue then, as I think it's handled well.

MJohnson459 and others added 7 commits July 30, 2026 13:52
jmeshlib's vertex ring traversals advance with Triangle::oppositeEdge(),
which returns NULL when the vertex is not part of the triangle, but only
test against the start edge. The NULL is carried into the next iteration
and dereferenced, segfaulting the -edgeSwapThreshold path used to build
the 2.5D mesh.
This fixes a build failure in CI for MacOS which failed as it couldn't
resolve rasterio which was only available on MacOS 14. This locks that
as the minimum version and reworks the platform format support to avoid
errors on the latest pixi.
- opendm/remote.py and stages/splitmerge.py, on windows this would
fetch the host python env, patching to force the same interpreter.
- opendm/osfm.py - we do not need win.bat anymore.
@MJohnson459 MJohnson459 mentioned this pull request Aug 4, 2026
The runtime stage copies a curated set of paths out of the builder, which did
not include contrib, so the tools are absent from the image.

.dockerignore also excludes contrib. That has no effect on the published
images, which are built from a Git context where it is not applied, but it does
apply to a local `docker build`, where the copy would then fail. Drop the entry
so both paths behave the same.

The documented invocations used --entrypoint, which bypasses the pixi
activation the tools need. Pass the interpreter as the command instead: the
entrypoint execs an argument that resolves on PATH, so the tool runs inside
the activated environment.
This is a temporary fix to avoid clobbering working images.
It seems most big projects keep the "primary" version first and the
variant second. This flips the tag to follow. This also means it should
be for example :3.6.2-gpu which I did differently but we can retag them.
The gpu and gpu-prod environments target the linux-64-cuda-12-0 platform, so
pixi requires the __cuda virtual package to install them. No NVIDIA driver is
visible inside docker build, so both pixi install steps aborted with
unsupported-platform. Set CONDA_OVERRIDE_CUDA in the shared dev stage, which
covers builder and prod-env alike.
The gpu and gpu-prod environments target win-64-cuda-12-0, so pixi requires the
__cuda virtual package to install them. The windows-2022 runner has no NVIDIA
driver, so setup-pixi and both pixi run steps would abort the same way the GPU
docker build did.
OpenMVS resolves CUDA_CUDA_LIBRARY through FindCUDA, whose toolkit-local search
covers lib and lib64 but not stubs, and whose default-path search only reaches a
real driver library. Neither exists inside docker build, so the configure step
failed with CUDA_CUDA_LIBRARY-NOTFOUND linked by target MVS.

The existing CMAKE_LIBRARY_PATH hint only covered the toolkit layout used by the
NVIDIA base images; the conda toolkit keeps its stub under the environment
prefix instead. Look there too, and only when no real driver library is found,
so builds on a CUDA machine keep linking the real one.
The smoke test launches DensifyPointCloud and ReconstructMesh, which carry a
NEEDED libcuda.so.1 once OpenMVS is built with CUDA. Nothing injects a driver
during a build, and the gpu-prod environment ships only the CUDA runtime, so the
launch check had no libcuda to resolve.

The base image already carries a compat driver, just not on the loader path.
Point the smoke step at it and no further: the pixi hook appends this to
LD_LIBRARY_PATH, so it resolves only what the environment cannot, and a
container started with the NVIDIA runtime still binds the host driver.
The PR matrix builds the default CPU environment and the docker job builds the
CPU image, so nothing exercised gpu.Dockerfile before a merge. Build it the same
way the CPU image is built, through the runtime target so the smoke test runs
with it.

Cancel superseded runs for the ref, since the GPU image takes about an hour.
A hosted runner could not finish it: the job died 58 minutes in with the runner
losing contact, its log stopping at [129/152] Completed 'lastools', immediately
before OpenMVS compiles across 19 GPU architectures. Fitting the build into a
hosted runner meant deleting parts of its filesystem and repointing its docker
daemon, none of which belongs in a workflow.

Build through the daemon's own store so layers survive between runs, which spares
the two pixi install steps on a rerun, and age out old layers afterwards so a
shared runner does not fill up.

The repository is public, so gate the job to branches pushed to the repository
itself: a self-hosted runner must not execute code from a fork.
A hosted runner cannot build this image: the equivalent pull request job died 58
minutes in with the runner losing contact, just as OpenMVS began compiling across
19 GPU architectures. The same build finishes in 15 minutes on the self-hosted
runner, which is where the CPU image is already published from.
Two GPU builds filled the runner's disk, to the point that the next job could not
write its own log. Both came from building through the daemon's own store to keep
layers as cache, which on a build this size means tens of GB kept per run against
a reclaim that dropped nothing under a fortnight old.

The cache was not worth it: it spared the two pixi install steps, about ten
minutes of a fifteen minute build. Build through the default container driver
instead, which is torn down with the job, and reclaim unconditionally afterwards.

The publish workflows never reclaimed anything at all, and the GPU one builds
with no cache on every push to master, so give them the same step.
The build ran the runner out of disk again with 34GB free, dying under four
minutes in, while still unpacking environments. The builder and prod-env stages
are independent, so buildkit materialises both at once, and each carries a
multi-GB conda environment before anything is compiled. Hold buildkit to one
worker, as the CPU publish already does on this runner.

Reclaim before building as well as after, since the trailing reclaim is skipped
when a job is killed outright, which is how the runner filled in the first place.

Report free space either side so the cost of a build is a number in the log
rather than an inference from what broke.
@MJohnson459

Copy link
Copy Markdown
Contributor Author

Sorry the CI jobs are causing problems due to disk space but I reverted to the last full successful commit. That gives us a good baseline. I want to do a full CI review anyway but I don't want to hold this branch up. Testing of it has been good with no noticable drop in performance. It passes the full oats suite with no obvious differences to the existing master.

@MJohnson459
MJohnson459 merged commit 29d68e3 into master Aug 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modernise Python setup for repo

4 participants