Conan workflow with OpenImageIO

May 2020

Upon a time ...

... I decided to get the Theia Vision Library (TVL) running on my machine to experiment a bit with camera pose & parameter estimation from images. Already being in my Python prototyping mindset I thought: Just download it and start having fun.

 

Nope. That is not the way it works with C++. In C++ there is a task that needs deep expertise first: Setting up the dependencies.

What happens behind the curtains within seconds or minutes in python can take days or weeks to do in C++. The argument for the C++ way is that you have much more fine-grained control from the beginning, but not being able to quickly setup a project without messing up you system and get an initial feeling for it is a real bummer.

 

conan is a package manager, offering an easy accesible interface via the conanfile(.txt/.py), while build system specific configuration syntax is left to some experts. A well made conanfile allows to setup some default dependencies with a simple "conan install" and get started with writing code immediately. For basic control the most important options should be exposed via the conanfile, some rarely touched detail options may on the contrary be hidden.

 

Now lets change sides from being conan consumer to package developer and lets see if it can make life with personal experiments and open-source projects easier. In the next lines I will describe my experience using conan to get the Theia Vision Library working with particular focus on making the open-source dependency OpenImageIO conan-ready.


Starting the dependency journey

... without conan

The journey started with running "cd build && cmake .. && make" in the TVL base directory. Of course it was bound to fail, but maybe just maybe I got lucky. Eigen missing. No problem, download the header-only library to the right location and try again. Ceres missing. No problem, download and install, it works. Little doubts about what would happen if I work on other projects with different versions of Ceres, or Ceres itself. But never mind the future, I want to get TVL running. OpenImageIO missing. ... So what if this keeps reoccuring, what will be the cost of future projects continuing this way? Very unpleasant times when using python on multiple projects without virtual environments come to my mind. Maybe its time to rethink the dependency approach. Let's try it the conan way.

 


Some  OPEN-SOURCE LIBRARIES ARE ALREADY CONAN-READY

Let's start with Eigen. The first address to check whether a conan package of this library already exists is conan center. Jackpot! Eigen is already available as conan package. Next is Ceres. Again Jackpot! Next OpenImageIO. ... Not so lucky this time. Also checking other places for existing packages did not yield any better results. So it is time to get our hands dirty and make a package ourselves.

Get OpenImageIO conan-ready

Sleves up. To get a feeling of the OpenImageIO (oiio) library and maybe get lucky, let's: "cd build && cmake .. && make". Of course oiio itself has some strict requirements to get it working. But fortunately we don't need to do the manual labour ourselves, seems some very first attempts on conan have already been taken: In the source root we find a conanfile.txt. So we continue with "cd build && conan install ..". Great it works! Running "cmake .." does not work out of the box, but setting the right options lets us successully compile and install the library.

 

So we are ready for the next mile: Make oiio itself a conan package. We follow the recommended workflow for creating conan packages. Let's start from an empty directory:

mkdir OpenImageIO && cd OpenImageIO

conan new

All conan functionality for package creation workflow revolves around the conanfile.py. As first step conan offers to create templates for this file and for a test_package:

conan new oiio/2.2 -t

We need to specify the package name and version, both that can be changed later. The flag -t creates the test_package templates and shall not be of interest until the very end when we want to test whether our package can be linked properly. (conan does not install to the system, but to its local cache, very similar to virtual environments in python).

 

We will get into each section of this file step-by-step adjusting it to work for our use case.

Conan source

The very first step is to decide how our conanfile.py should interact with the source code: Should it be part of the source code or rather act as independent wrapper for it to produce a package?

 

For starters the second option looks more attractive: So far it is not clear whether OpenImageIO wants to include the conanfile.py and patching a repo comes with its costs. We will go with the wrapping approach.

Wrapping source code nicely interplays with github. The simplest solution is to just replace the URL in the template's source() method. For the particular version of OpenImageIO we can also omit the patching of the CMakeLists.txt file. The lines added in the template can already be found in oiio's base CMakeLists.txt:

 

    def source(self):
        self.run("git clone https://github.com/OpenImageIO/oiio.git")

With this adjustment we can now download oiio's sourcecode into the current folder via

conan source .

This will give us oiio's source code in the subfolder oiio. Note that conan is neither restricted to github nor to git as versioning system. Tighter integration for SCM seems to be in active development.

Conan INSTALL

Now just as the conanfile.txt we can use the conanfile.py to install all the dependencies of our package into the conan local cache (~/.conan/data on linux). To make them available we need to make the paths and other conan variables known to cmake by appending the following lines to CMakeLists.txt:

 

include (${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)

conan_basic_setup()

 

oiio already does this to use the dependencies installed via the conaninfo.txt. To use this mechanism with the conanfile.py we need to add a section

 

def requirements(self):
        self.requires("zlib/1.2.11")
        self.requires("libtiff/4.0.9")

        ...

 

We can propagate options to all of the dependencies by adding another section configure(self), but we skip this for now. To install the dependencies and prepare the build directory, we use the same command as for the conanfile.txt:

conan install --install-folder build .

Note that conan can differentiate between requirements and build requirements that are not used in the shipped package (think gtest). It also supports ranges to allow for different versions in the dependencies. This however can cause other headaches, so we will stay with fixed versions here.

Conan BUILd

The next step is building from source, which can be instrumentalized through conan. Conan is agnostic to how we build, but there is a particularly user friendly integration for cmake. Let's fill the build section:

 

    def build(self):

        cmake = CMake(self)
        cmake.definitions["STOP_ON_WARNING"]=0
        ...

        #specify projects base CMakeLists.txt location relative to the conanfile.py
        cmake.configure(source_folder="oiio")
        cmake.build()

I had to fiddle around a bit to find suitable cmake options to "get the build done", you will see my choices at the end of the article. But finally conan was able to configure and build oiio from source using

conan build --install-folder build .

Conan Package

Also the package creation can be instrumentalized using cmake: Conan runs cmake install with CMAKE_INSTALL_PREFIX pointing to a package folder. To use the same cmake settings as in the build step, we factor out the cmake configure step in the conanfile.py to its own function:

 

def configure_cmake(self):
        cmake = CMake(self)
        cmake.definitions["STOP_ON_WARNING"]=0

        ...
        cmake.configure(source_folder="oiio")
        return cmake

def build(self):
        cmake = self.configure_cmake()
        cmake.build()

 def package(self):
        cmake = self.configure_cmake()
        cmake.install()

After giving the package a name for linking


    def package_info(self):
        self.cpp_info.libs = ["OpenImageIO"]

 

We can now create a package with

conan package --build-folder build .

conan export-pkg & Conan Test

Finally the package needs to go into the local conan cache, either by simply copying the package from the previous step via

conan export-pkg --package-folder build/package .

or (re)starting the all the above steps inside conan cache by providing source & build folder.

 

Finally we can test our package by adjusting the example.cpp in the test_package folder in step 1 and calling

conan test test_package oiio/2.2

A final word

The above step by step workflow makes a lot of sense to debug problems potentially occuring at each of the steps. Alse testing added complexity (options, settings) will be easy to do in the respective small unit. Once each step works the whole workflow can be abbreviated by simply calling

conan create . 

Optionally username and channel can be added to the package description (for sharing the package).

 

Given a cmake based c++ project whose (required) dependencies are already given as conan packages you can see the minimal overhead we get to transform OpenImageIO into a conan package itself. With (required) dependencies that are not conan packages yet, this workflow needs to be repeated.

 

In a follow-up step the conanfile.py can now be extended to support more fine grain control over the libraries possible options.

Code of The "First Shot" ConanFILE.PY

from conans import ConanFile, CMake, tools


class OiioConan(ConanFile):
    name = "oiio"
    version = "2.1"
    settings = "os", "compiler", "build_type", "arch"
    generators = "cmake"

    def configure_cmake(self):
        cmake = CMake(self)
        cmake.definitions["CMAKE_CXX_FLAGS"]="-std=c++0x"
        cmake.definitions["USE_PYTHON"]=0
        cmake.definitions["STOP_ON_WARNING"]=0
        cmake.definitions["OIIO_BUILD_TESTS"]=0
        cmake.definitions["OIIO_BUILD_TOOLS"]=0
        cmake.definitions["ENABLE_ffmpeg"]=0
        cmake.configure(source_folder="oiio")

        return cmake

    def source(self):
        self.run("git clone https://github.com/OpenImageIO/oiio.git")
        # oiio already has conan_basic_setup() included in root CMakeLists.txt

    def requirements(self):
        self.requires("zlib/1.2.11")
        self.requires("libtiff/4.0.9")
        self.requires("libpng/1.6.37")
        self.requires("openexr/2.4.0")
        self.requires("boost/1.70.0")
        self.requires("libjpeg/9c")
        self.requires("libjpeg-turbo/2.0.2")
        self.requires("giflib/5.1.4")
        self.requires("freetype/2.10.0")
        self.requires("openjpeg/2.3.1")
        self.requires("tsl-robin-map/0.6.1@tessil/stable")
        self.requires("tbb/2020.0")
    def build(self):
        cmake = self.configure_cmake()
        cmake.build()

    def package(self):
        cmake = self.configure_cmake()
        cmake.install()

    def package_info(self):
        self.cpp_info.libs = ["OpenImageIO"]

                                         

 

Lausanne, May 9th 2020