OpenCV Conan 包的制作与未来的挑战
什么是OpenCV?
如果您还不熟悉OpenCV,请查看我们之前关于OpenCV 4.0.0的博客文章。考虑到OpenCV是一个拥有大量功能且适用于各种用例的庞大库,它是一个很好的例子,可以用来演示一些典型的包管理挑战(以及一些更具体的挑战)。
OpenCV的Conan包
最近,我们终于将OpenCV的配方接受到了conan-center。我们支持所有主要版本,因此在Bintray上提供了以下版本:
- 4.x - opencv/4.0.1@conan/stable。
- 3.x - opencv/3.4.5@conan/stable。
- 2.x - opencv/2.4.13.5@conan/stable。
使用Conan进行安装应该非常简单,例如,您可以使用以下conanfile.txt来使用OpenCV 4.0.0。
[requires]
opencv/4.0.1@conan/stable
[generators]
cmake
像往常一样,预构建的包适用于主要平台(Windows/Linux/MacOS)和编译器(Visual Studio/GCC/Clang)。
构建OpenCV
OpenCV使用CMake,因此我们的配方使用了CMake构建助手。构建基于CMake的项目的流程对于许多配方来说都是典型的,OpenCV也不例外。
第一步是配置CMake
def _configure_cmake(self):
cmake = CMake(self)
# configure various OpenCV options via cmake.definitions
cmake.configure(build_folder=self._build_subfolder)
return cmake
这里真的没有什么特别之处,除了有很多选项需要管理,这就是代码占用这么多行的原因。cmake.configure(…) 检测编译器及其功能,然后生成特定于平台的构建文件。
在这里,我们还禁用了我们想要避免的一些功能
cmake.definitions['BUILD_EXAMPLES'] = False
cmake.definitions['BUILD_DOCS'] = False
cmake.definitions['BUILD_TESTS'] = False
cmake.definitions['BUILD_PERF_TEST'] = False
cmake.definitions['WITH_IPP'] = False
cmake.definitions['BUILD_opencv_apps'] = False
cmake.definitions['BUILD_opencv_java'] = False
cmake.definitions 是一个字典,它被转换为传递给cmake的命令行参数,例如,cmake.definitions[‘BUILD_EXAMPLES’] = False 映射到 -DBUILD_EXAMPLES=OFF。
一些特定变量的解释
- BUILD_EXAMPLES - 不要构建OpenCV示例,因为它们不需要用于使用OpenCV,但会增加构建时间和包大小。
- BUILD_DOCS - 跳过文档,原因与示例相同,我们通常只保留链接包所需的内容,并且构建文档可能需要其他工具(例如doxygen)。
- BUILD_TESTS - 同理,因为我们不会运行这些测试,所以跳过它们。
- BUILD_PERF_TEST - 另一组要跳过的测试。
- BUILD_opencv_apps - 跳过OpenCV附带的一些演示和实用程序应用程序。
- BUILD_opencv_java - 因为我们正在构建C++的包,所以也禁用Java绑定。此外,安装它们需要JDK、Apache ANT等,如果没有找到,可能会失败。
一旦CMake配置完成,我们就可以构建项目了
def build(self):
# intentionally skipped code to patch OpenEXR here
cmake = self._configure_cmake()
cmake.build()
cmake.build() 根据CMake 生成器执行构建工具,它可能是MSBuild、GNU Make、Ninja等。这非常好,因为我们不必处理特定于平台的项目构建细节。作为反例,许多项目仍然使用不同的构建系统来编译各种平台,例如在Windows上使用Visual Studio解决方案,而在其他平台上使用makefile - 对于此类项目,配方需要有几种build方法的实现,当然,还要处理所有选项。
此外,我们配方的package方法也非常简单
def package(self):
cmake = self._configure_cmake()
cmake.install()
cmake.patch_config_paths()
它没有典型的复制特定于平台的文件(如.dll、.so、.dylib等)的代码。相反,它使用了CMake install功能。CMake可以生成一个名为INSTALL的特殊目标,它会复制项目的头文件、库、CMake 配置文件、pkg-config文件,以及其他数据文件,例如OpenCV中的Haar Cascades。因此,如果项目本身知道要分发哪些文件以及如何正确布局它们,那么在conanfile中复制此逻辑就没有多大意义,对吧?此外,CMake.install方法会自动将CMAKE_INSTALL_PREFIX指向包文件夹。
但是什么是cmake.patch_config_paths(),为什么我们需要它?好吧,CMake生成的配置文件可能包含绝对路径,这是我们想要避免的,因为这样的路径特定于构建配方的机器,并且使用者通常不会在相同的路径中安装依赖项。例如,在Windows上,Conan目录通常位于USERPROFILE目录中,其中包含用户名(例如AppVeyor)。考虑到这一点,使用生成的CMake配置文件可能会导致无法构建项目,因此Conan中有一个解决此问题的变通方法。
依赖项
OpenCV是一个非常复杂的库,并且有很多不同的依赖项。当前的Conan配方有以下依赖项:
图表是由conan info命令生成的
$ conan info opencv/4.0.0@conan/stable --graph opencv
如您所见,它目前主要依赖于图像库,例如libjpeg、libtiff、libpng、libwepb、jasper和OpenEXR。
所有这些库也作为Conan包在conan-center中可用。感谢bincrafters打包了所有这些库。
这些库主要由OpenCV imgcodecs使用,以支持读取和写入各种图像格式。
所有提到的库都可以使用选项启用或禁用(它们目前默认启用)。例如,要禁用OpenEXR支持,请使用以下方法:
[requires]
opencv/4.0.1@conan/stable
[options]
opencv:openexr=False
[generators]
cmake
声明依赖项
为了声明对其他第三方库的动态依赖,OpenCV 配方使用requirements方法
def requirements(self):
self.requires.add('zlib/1.2.11@conan/stable')
if self.options.jpeg:
self.requires.add('libjpeg/9c@bincrafters/stable')
if self.options.tiff:
self.requires.add('libtiff/4.0.9@bincrafters/stable')
if self.options.webp:
self.requires.add('libwebp/1.0.0@bincrafters/stable')
if self.options.png:
self.requires.add('libpng/1.6.34@bincrafters/stable')
if self.options.jasper:
self.requires.add('jasper/2.0.14@conan/stable')
if self.options.openexr:
self.requires.add('openexr/2.3.0@conan/stable')
上面的代码根据配方声明的选项添加条件依赖项
options = {"shared": [True, False],
"fPIC": [True, False],
"contrib": [True, False],
"jpeg": [True, False],
"tiff": [True, False],
"webp": [True, False],
"png": [True, False],
"jasper": [True, False],
"openexr": [True, False],
"gtk": [None, 2, 3]}
default_options = {"shared": False,
"fPIC": True,
"contrib": False,
"jpeg": True,
"tiff": True,
"webp": True,
"png": True,
"jasper": True,
"openexr": True,
"gtk": 3}
提到的技术在文章精通Conan:条件设置、选项和需求中进行了说明。
由于我们现在使用的是来自Conan的第三方库,因此保留OpenCV源代码的3rdparty目录没有意义,因此我们在source方法中将其删除
shutil.rmtree(os.path.join(self._source_subfolder, '3rdparty'))
为什么这很重要?有几个好处:
- 使用者可以更好地控制依赖项,例如,他们可以通过简单地编辑他们的conanfile.txt轻松升级或降级OpenCV的第三方依赖项,如libpng。
- 它可以节省构建时间,因为如果更改某些OpenCV选项,则不需要重新构建这些依赖项。
- 它减少了包的大小。
- 它有助于避免链接或运行时错误,因为如果两个库都包含libpng源代码(例如OpenCV和wxWidgets),并且您将两者都链接到您的项目中,您可能会遇到非常难以调试的问题。
最后,这些选项被传递给构建系统(OpenCV的情况是CMake)
cmake.definitions['WITH_JPEG'] = self.options.jpeg
cmake.definitions['WITH_TIFF'] = self.options.tiff
cmake.definitions['WITH_WEBP'] = self.options.webp
cmake.definitions['WITH_PNG'] = self.options.png
cmake.definitions['WITH_JASPER'] = self.options.jasper
cmake.definitions['WITH_OPENEXR'] = self.options.openexr
我们也始终禁用第三方库的构建
# disable builds for all 3rd-party components, use libraries from conan only
cmake.definitions['BUILD_ZLIB'] = False
cmake.definitions['BUILD_TIFF'] = False
cmake.definitions['BUILD_JASPER'] = False
cmake.definitions['BUILD_JPEG'] = False
cmake.definitions['BUILD_PNG'] = False
cmake.definitions['BUILD_OPENEXR'] = False
cmake.definitions['BUILD_WEBP'] = False
cmake.definitions['BUILD_TBB'] = False
cmake.definitions['BUILD_IPP_IW'] = False
cmake.definitions['BUILD_ITT'] = False
cmake.definitions['BUILD_JPEG_TURBO_DISABLE'] = True
因为它们是从Conan包中使用的,所以没有必要在OpenCV的上下文中从源代码构建它们。
OpenEXR的修补
CMake使用所谓的查找模块来定位各种库。大多数流行库都有很多查找模块,但是,许多库仍然缺少查找模块,OpenEXR就是其中之一。
OpenCV有一个它自己的查找模块集合,其中有一个用于OpenEXR - OpenCVFindOpenEXR。
但是,OpenCV的OpenEXR模块存在几个问题
- 它将OPENEXR_ROOT变量硬编码为Windows上的C:\deploy,因此它无法在不寻常的位置(例如Conan缓存目录)找到OpenEXR。
- 它总是优先在系统位置(例如/usr/lib)中查找库,而OPENEXR_ROOT的优先级最低。
- 它没有考虑OpenEXR库的所有可能名称。例如,它总是查找IlmImf,而库可能名为IlmImf-2_3_s。
这很不幸。但实际上,Conan配方经常需要解决构建脚本的各种限制。令人遗憾的事实是,许多库在设计时没有考虑包管理用例,而是对路径、库名称、版本和其他重要内容进行了硬编码。这使得打包人员的工作变得更加困难,但随着C++世界中包管理的普及,我们希望这种情况发生的频率越来越低。
无论如何,目前配方中有一段代码可以删除硬编码的内容
# allow to find conan-supplied OpenEXR
if self.options.openexr:
find_openexr = os.path.join(self._source_subfolder, 'cmake', 'OpenCVFindOpenEXR.cmake')
tools.replace_in_file(find_openexr,
r'SET(OPENEXR_ROOT "C:/Deploy" CACHE STRING "Path to the OpenEXR \"Deploy\" folder")',
'')
tools.replace_in_file(find_openexr, r'set(OPENEXR_ROOT "")', '')
tools.replace_in_file(find_openexr, 'SET(OPENEXR_LIBSEARCH_SUFFIXES x64/Release x64 x64/Debug)', '')
tools.replace_in_file(find_openexr, 'SET(OPENEXR_LIBSEARCH_SUFFIXES Win32/Release Win32 Win32/Debug)',
'')
我们在这里使用tools.replace_in_file删除几行CMake代码。在更复杂的情况下,可以使用tools.patch助手。
幸运的是,OpenEXR是唯一需要修改的情况,其他库(libpng、libjpeg等)使用标准的CMake查找模块,并且它们没有上述限制。
OpenCV contrib
除了内置功能外,OpenCV还提供了一组额外的模块,称为OpenCV contrib。目前,它大约有100个额外的模块!仅举几例:
默认情况下,我们的包没有启用OpenCV contrib模块。但是,您可以通过传递opencv:contrib选项轻松获得它们:
[requires]
opencv/4.0.1@conan/stable
[options]
opencv:contrib=True
[generators]
cmake
从配方的角度来看,contrib添加了额外的源代码tarball
tools.get("https://github.com/opencv/opencv_contrib/archive/%s.zip" % self.version)
os.rename('opencv_contrib-%s' % self.version, 'contrib')
并将切换contrib的选项传递给构建系统(CMake)
if self.options.contrib:
cmake.definitions['OPENCV_EXTRA_MODULES_PATH'] = os.path.join(self.build_folder, 'contrib', 'modules')
OPENCV_EXTRA_MODULES_PATH是一个CMake变量,用于指定要构建的其他OpenCV模块,在这种情况下,我们将路径传递给contrib。
系统要求
有时配方可能需要依赖于系统包管理器提供的库,例如apt、yum或pacman,而不是Conan提供的库。这通常是针对一些底层内容,例如VDPAU或VAAPI,但在OpenCV的情况下,它可能依赖于GTK。
不幸的是,系统要求非常难以维护,因此我们的建议是,如果可能,请避免使用它们。系统要求有以下限制,这使得它们难以扩展:
- 配方必须为每个包管理器使用其自己的分支,例如yum和apt将为相同的库/包使用不同的名称(gtk2-devel vs libgtk2.0-dev)。
- 有时,即使使用相同的包管理器,不同Linux发行版的包名也不同(例如Fedora和CentOS都使用yum,但pkg-config的包名不同)。
- 即使是同一Linux发行版的次要版本,包名也可能不同!(例如Ubuntu 16.04 vs Ubuntu 12.04)。
- 软件包架构的名称也各不相同,例如yum使用i686和x86_64后缀,而apt使用i386和amd64。
例如,我们目前正在使用以下代码来仅仅指定GTK依赖项
def system_requirements(self):
if self.settings.os == 'Linux' and tools.os_info.is_linux:
if tools.os_info.with_apt:
installer = tools.SystemPackageTool()
arch_suffix = ''
if self.settings.arch == 'x86':
arch_suffix = ':i386'
elif self.settings.arch == 'x86_64':
arch_suffix = ':amd64'
packages = []
if self.options.gtk == 2:
packages.append('libgtk2.0-dev%s' % arch_suffix)
elif self.options.gtk == 3:
packages.append('libgtk-3-dev%s' % arch_suffix)
for package in packages:
installer.install(package)
elif tools.os_info.with_yum:
installer = tools.SystemPackageTool()
arch_suffix = ''
if self.settings.arch == 'x86':
arch_suffix = '.i686'
elif self.settings.arch == 'x86_64':
arch_suffix = '.x86_64'
packages = []
if self.options.gtk == 2:
packages.append('gtk2-devel%s' % arch_suffix)
elif self.options.gtk == 3:
packages.append('gtk3-devel%s' % arch_suffix)
for package in packages:
installer.install(package)
这看起来非常冗余,不是吗?但是,如果我们决定为更多Linux发行版或更多架构添加支持,代码量将呈指数级增长。
如您所见,Conan使用system_requirements方法来指定系统特定的需求,并且还有一个SystemPackageTool助手,它可以自动安装软件包。在幕后,它会调用特定于给定包管理器的命令,例如apt-get install -y libgtk2.0-dev:i386。
包信息
有一些平台特定的系统库,必须在conanfile的package_info方法中显式指定。
- pthread,或POSIX线程,为兼容POSIX的系统提供多线程支持。
- libm,C数学函数。
- libdl,用于动态链接支持。
- Vfw32,或Video for Windows,来自Windows 95时代的古老视频播放技术,至今仍在使用。
此外,特别是对于Apple macOS,还有许多框架正在使用。为了指定框架,我们使用以下代码
for framework in ['OpenCL',
'Accelerate',
'CoreMedia',
'CoreVideo',
'CoreGraphics',
'AVFoundation',
'QuartzCore',
'Cocoa']:
self.cpp_info.exelinkflags.append('-framework %s' % framework)
self.cpp_info.sharedlinkflags = self.cpp_info.exelinkflags
因为它们的链接方式与库不同。大多数情况下,这些框架用于Apple平台上可用的多媒体相关技术。
未来:其他选项和依赖项
如前所述,OpenCV是一个非常庞大而复杂的库,它确实有大量的选项。目前,我们的Conan包并不支持所有这些选项。您可以在他们的GitHub仓库中查看可用选项的列表。仅仅声明所有这些选项就需要近300行的CMake代码!这实际上很难一次性建模。此外,大多数选项都依赖于其他第三方库。
仅举几个例子
- Google Protocol Buffers可用于读取来自Caffe网络的数据。
- OpenCL和CUDA可用于在异构系统上加速OpenCV算法。
- FFMPEG和GStreamer可用于读取和写入视频文件。
Google Protocol Buffers (Protobuf)
OpenCV模块DNN(深度神经网络)可以与Google Protobuf支持一起编译。
我们目前正在积极努力添加Google Protobuf的配方,并将其纳入conan-center。该库本身极具挑战性,尤其是在交叉编译的情况下。一旦被接受,我们将启用我们的OpenCV包以默认使用Protocol Buffers。
OpenCL
OpenCV还可以配置为使用OpenCL,但是,它在各个平台上的支持差异很大,例如
- MacOS通过提供OpenCL.framework内置了OpenCL支持。
- Linux需要安装开发包(例如,在Debian系统上安装ocl-icd-opencl-dev)。
- Windows需要由其中一个供应商提供的SDK包(例如,来自Intel或来自nVidia)。
- Android也需要供应商提供的SDK包(例如,来自Mali)。
因此,为了为OpenCV包提供OpenCL支持,我们需要开发一种方法来建模这种类型的依赖关系。一种可能性可能是类似于虚拟包的东西。
CUDA
与OpenCL类似,但是,只有一个供应商,很明显——构建CUDA应用程序需要nVidia CUDA Toolkit。该工具包非常庞大,除了库和头文件外,还包含CUDA编译器。我们要么要求用户在构建期间在机器上安装CUDA,要么为工具包提供一个包。
FFMPEG
通常使用OpenCV不仅用于图像处理,还用于视频处理,例如水印、绿屏替换等。为了使OpenCV能够读取或写入视频文件,OpenCV的Video I/O模块可以使用ffmpeg库。但是,FFmpeg本身可能与OpenCV一样复杂(它的configure脚本大约有400行代码只是为了声明可用的选项!),因此它的打包也具有挑战性。希望它很快就会在conan-center中可用,这样OpenCV用户就可以捕获和写入视频流。
例如,当前的配方支持各种编码库(也使用conan打包):libx264、libx265、libvpx、libopenh264等。我们希望列表能够显著增长,添加诸如libaom(也称为AV1)之类的现代格式。
此外,FFmpeg也可以使用CUDA和OpenCL来加速视频编码和过滤,因此它也将受益于Conan解决CUDA和OpenCL支持。
GStreamer
我们目前正在打包GStramer库。与FFMPEG和Google Protobuf类似,GStreamer本身非常庞大,并且需要先打包一些其他库,例如libffi和GLib。与FFMPEG一起,GStreamer是Conan中要求打包最多的库之一,它显然也在我们的关注范围内。
经验教训和建议
由于OpenCV的打包是一项耗费大量时间的大型任务,因此我们学到了一些经验教训,想与大家分享:
- 对可选依赖项使用动态需求。
- 如果可能,使用构建助手,它们可以自动化许多事情,并使配方代码保持简洁。
- patch_config_paths可能需要用于CMake库。
- 使用exelinkflags/sharedlinkflags来指定Apple框架。
- 如果可能,避免使用系统需求,而是使用Conan打包库。
结论
尽管OpenCV包在conan-center中可用,但它们在支持的选项和依赖项方面并不完整,我们正在考虑以小迭代的方式添加更多选项,以满足更多用例并支持更多功能。
但我们仍然鼓励用户尝试我们的OpenCV包,并将任何问题和功能请求报告给我们的GitHub。我们将根据反馈优先考虑添加缺失的部分。
总的来说,Conan已经足够灵活和成熟,可以处理非常复杂的库(例如OpenCV)的打包,并且conanfile可以处理所有需求、选项、修补等。此外,Conan提供了一些工具和助手,使打包人员的生活更加轻松,节省时间。
Conan还清楚地分离了conanfile中的逻辑,使读取和编写配方代码变得更加容易,并且Conan允许逐步调试配方,逐个调用其步骤:source -> build -> package -> test。