可执行文件和应用程序 (包括共享库) 也可以与Conan一起分发、部署和运行。与部署其他系统相比,这可能具有一些优势:
- 适用于所有系统和平台的统一开发和分发工具。
- 以与管理开发配置相同的方式管理任意数量的不同部署配置。
- 使用Conan服务器远程存储所有操作系统、平台和目标的所有应用程序和运行时间。
使用部署生成器
部署生成器用于将应用程序的所有依赖项复制到一个位置。然后,所有文件都可以重新打包为所选的分发格式。
例如,如果应用程序依赖于boost,我们可能不知道它还需要许多其他的第三方库,如zlib、bzip2 、lzma、zstd、iconv等。
$ conan install . -g deploy
这有助于将所有依赖项收集到一个位置,将它们移出Conan缓存。
使用json生成器
更高级的方法是使用json生成器。该生成器的工作方式与部署生成器类似,尽管它不会将文件复制到目录。相反,它生成一个JSON文件,其中包含有关依赖项的所有信息,包括文件在Conan缓存中的位置。
$ conan install . -g json
生成的conanbuildinfo.json文件是完全机器可读的,脚本可以使用它来准备文件并重新创建适当的格式以进行分发。以下代码显示如何从conanbuildinfo.json中读取库和二进制目录:
import os
import json
data = json.load(open("conanbuildinfo.json"))
dep_lib_dirs = dict()
dep_bin_dirs = dict()
for dep in data["dependencies"]:
root = dep["rootpath"]
lib_paths = dep["lib_paths"]
bin_paths = dep["bin_paths"]
for lib_path in lib_paths:
if os.listdir(lib_path):
lib_dir = os.path.relpath(lib_path, root)
dep_lib_dirs[lib_path] = lib_dir
for bin_path in bin_paths:
if os.listdir(bin_path):
bin_dir = os.path.relpath(bin_path, root)
dep_bin_dirs[bin_path] = bin_dir
使用部署生成器时,所有文件都被复制到一个文件夹中。Json one的优势在于,您可以对文件进行细粒度控制,并且这些文件可以直接复制到所需的布局。
从这个意义上说,上面的脚本可以很容易地修改以应用某种过滤 (例如g.仅复制共享库,并省略任何静态库或辅助文件,如pkg-config。电脑文件)。
此外,您还可以使用提取的信息为应用程序编写一个简单的启动脚本,如下所示:
executable = "MyApp" # just an example
varname = "$APPDIR"
def _format_dirs(dirs):
return ":".join(["%s/%s" % (varname, d) for d in dirs])
path = _format_dirs(set(dep_bin_dirs.values()))
ld_library_path = _format_dirs(set(dep_lib_dirs.values()))
exe = varname + "/" + executable
content = """#!/usr/bin/env bash
set -ex
export PATH=$PATH:{path}
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{ld_library_path}
pushd $(dirname {exe})
$(basename {exe})
popd
""".format(path=path,
ld_library_path=ld_library_path,
exe=exe)
通过包运行
如果依赖项具有要在conanfile中运行的可执行文件,则可以使用run_environment = True参数直接在代码中完成此操作。它在内部使用RunEnvironment() 助手。例如,如果我们要在构建消费者包时执行问候语应用程序:
from conans import ConanFile, tools, RunEnvironment
class ConsumerConan(ConanFile):
name = "Consumer"
version = "0.1"
settings = "os", "compiler", "build_type", "arch"
requires = "hello/0.1@user/testing"
def build(self):
self.run("greet", run_environment=True)
现在运行此消费配方的conan install和conan build:
$ conan install . && conan build .
...
Project: Running build()
Hello World Release!
除了使用环境,还可以显式访问依赖项的路径:
def build(self):
path = os.path.join(self.deps_cpp_info["Hello"].rootpath, "bin")
self.run(["%s/greet" % path])
请注意,如果存在共享库,这可能还不够。使用上面的run_environment = True help是一个更完整的解决方案。
此示例还演示了如何使用list指定要运行的命令。这绕过了系统外壳,即使路径包含特殊字符,如空格或引号,否则将由外壳解释,它也能正常工作。但是,这也意味着在使用此方法时,替换环境变量或其他命令 (通常由shell完成) 的输出将不起作用。当您需要此功能时,使用上面所示的纯字符串指定您的命令。
最后,还有另一种方法: 包含可执行文件的包可以将其bin文件夹直接添加到路径中。在这种情况下,Hello包conanfile将包含:
def package_info(self):
self.cpp_info.libs = ["hello"]
self.env_info.PATH = os.path.join(self.package_folder, "bin")
如果可执行文件需要DYLD_LIBRARY_PATH和LD_LIBRARY_PATH,我们也可以定义它们。
消费者包很简单,因为PATH环境变量包含问候语可执行文件:
def build(self):
self.run("greet")
请阅读下一节,了解有关在配方方法中使用打包可执行文件的更全面的解释。
运行时包和重新打包
可以创建仅包含运行时二进制文件的包,从而摆脱所有构建时依赖项。如果我们想从上面的 “你好” 创建一个包,但只包含可执行文件 (记住上面的包还包含一个库和标题),我们可以这样做:
from conans import ConanFile
class HellorunConan(ConanFile):
name = "hello_run"
version = "0.1"
build_requires = "hello/0.1@user/testing"
keep_imports = True
def imports(self):
self.copy("greet*", src="bin", dst="bin")
def package(self):
self.copy("*")
这个食谱有以下特点:
- 它包括hello/0.1 @ user/testpackage (按build_要求)。这意味着它将用于构建hello_run包,但是一旦构建了hello_run包,就没有必要检索它。
- 它使用import () 从依赖项复制,在这种情况下,可执行文件
- 它使用keep_exports属性来定义在build() 步骤 (未定义,然后使用默认的空对象) 期间导入的工件在构建后保留且不删除
- Package () 方法打包将在build文件夹中创建的导入工件。
创建此包并将其上传到远程:
$ conan create . user/testing
$ conan upload hello_run* --all -r=my-remote
可以使用上面介绍的任何方法来安装和运行此软件包。例如:
$ conan install hello_run/0.1@user/testing -g virtualrunenv
# You can specify the remote with -r=my-remote
# It will not install hello/0.1@...
$ activate_run.sh # $ source activate_run.sh in Unix/Linux
$ greet
> Hello World Release!
$ deactivate_run.sh # $ source deactivate_run.sh in Unix/Linux
部署挑战
在部署C/C + + 应用程序时,在分发应用程序时,有一些特定的挑战需要解决。在这里你会找到最常见的和一些克服它们的建议。
The C standard library
对于所有应用程序来说,无论它们是用纯C还是用C + + 编写的,一个共同的挑战是对C标准库的依赖。这个库最广泛的变体是GNU C库或者只是glibc。
Glibc不仅仅是一个C标准库,因为它提供:
- C函数 (如malloc() 、sin() 等) 用于各种语言标准,包括c99。
- POSIX函数 (如pthread库中的posix线程)。
- BSD功能 (如BSD套接字)。
- 用于操作系统特定api的包装器 (如Linux系统调用)
即使您的应用程序不直接使用这些功能中的任何一个,它们也经常被其他库使用,因此,实际上,它几乎总是在实际使用中。
C标准库的其他实现也提出了同样的挑战,例如用于嵌入式开发的newlib或musl。
为了说明这个问题,在现代Ubuntu发行版中编译的一个简单的hello-world应用程序在Centos 6 版本中运行时会出现以下错误:
$ /hello
/hello: /lib64/libc.so.6: version 'GLIBC_2.14' not found (required by /hello)
这是因为这些Linux发行版之间的glibc版本不同。
这个问题有几种解决方法:
- LibcWrapGenerator
- Glibc_version_header
- Bingcc
有些人也建议使用静态的glibc,但强烈不鼓励这样做。原因之一是较新的glibc可能正在使用以前版本中不可用的系统调用,因此它将在运行时随机失败,这很难调试 (系统调用的情况如下所述)。
通过在设置中定义自定义Conan子设置,可以在Conan中为glibc版本或Linux发行名称建模。yml文件 (签出添加新设置和添加新子设置的部分)。该过程将类似于:
- 定义新的子设置,例如操作系统。发行版,如添加新的子设置一节中所述。
- 定义兼容模式,如package_id() 和build_id() 部分所述 (例如,您可能认为某些Ubuntu和Debian软件包彼此兼容)
- 为每个分发生成不同的包。
- 为每个发行版生成可部署的工件。
C++ standard library
通常,默认的C + + 标准库是libstdc + +,但是libc + + 和stlport是其他众所周知的实现。
与标准C库glibc类似,在旧系统中运行与libstdc + + 链接的应用程序可能会导致错误:
$ /hello
/hello: /usr/lib64/libstdc++.so.6: version 'GLIBCXX_3.4.21' not found (required by /hello)
/hello: /usr/lib64/libstdc++.so.6: version 'GLIBCXX_3.4.26' not found (required by /hello)
幸运的是,通过添加-static-libstdc + + 编译器标志,解决这个问题要容易得多。与C运行时不同,C + + 运行时可以静态安全地链接,因为它不直接使用系统调用,而是依赖libc来提供所需的包装器。
Compiler runtime
除了C和C + + 运行时库,编译器运行时库也被应用程序使用。这些库通常提供较低级别的函数,例如编译器内部函数或对异常处理的支持。来自这些运行时库的函数很少在代码中直接引用,并且大多由编译器本身隐式插入。
$ ldd ./a.out
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f6626aee000)
您可以通过使用-static-libgcc编译器标志来避免这种依赖关系。然而,这并不总是明智的做法,因为在某些情况下,应用程序应该使用共享运行时。最常见的情况是应用程序希望跨不同共享库引发和捕获异常。查看GCC手册以获取详细信息。
System API (system calls)
新的系统调用通常在新版本的linux内核中引入。如果应用程序或第3 方库想要利用这些新功能,它们有时会直接引用此类系统调用 (而不是使用glibc提供的包装器)。
结果,如果应用程序是在具有较新内核的计算机上编译的,并且构建了用于自动检测可用系统调用的系统,则它可能无法在具有较旧内核的计算机上正确执行。
解决方案是使用具有最低支持内核的构建机器,或者使用模型支持的操作系统 (就像在glibc的情况下一样)。查看添加新设置和添加新子设置的部分,以获取有关如何在conan设置中建模分布的信息。