前言
最近在搭建 c++ 的基础库,使用了 conan 来进行管理;其中团队几个项目同时使用了 cpp-httplib 库,另外一个项目由于设备限制,只能通过系统提供的代理方式来进行网络请求,所以还是手搓的网络请求;现在目标是修改 cpp-httplib 以支持被限制的设备,同时使用 conan 管理 cpp-httplib。
什么是 conan
conan 是 c++ 的包管理器,类似于 android 的 maven、node 的 npm、python 的 pip。
借用官网的一张图:
本地指定依赖某个库时,conan 会根据我们本地指定的 settings、options等:
- 先从本地缓存中找对应的构建配置文件
- 如果本地没找到,就去指定的远程仓库找;如果在远程仓库找到了,就会下载构建配置文件到本地缓存中
- 根据构建配置文件,从本地缓存中找有没有对应的二进制文件
- 如果本地没有,就去远程仓库找;如果在远程仓库找到了,就会下载二进制文件到本地缓存中
- 最后,会为我们指定的 generators 去生成对应的用于构建的文件。
详情请查看 官方文档
什么是 cpp-httplib
cpp-httplib 是一个 header-only 的跨平台 HTTP/HTTPS 库,具体可查看 GitHub .
环境
示例环境为:
macOS 11.3.1
Conan version 1.36.0
Apple clang version 12.0.5 (clang-1205.0.22.9)
cmake version 3.20.1
cpp-httplib
第一步,先修改 cpp-httplib 以支持被限制的设备。
这一步比较简单,根据设备开发文档,只有两个地方需要修改:
- 只能使用特定的方式创建 socket
- 调用 connect 的时候,需要传入代理地址
由于涉及业务,这部分代码就单纯使用 printf
来代替;我们定义一个宏 MODE_PROXY
,用于条件编译。
// httplib.h
// 1. 修改 2100 ~ 2123 行代码
#ifdef _WIN32
auto sock =
WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0,
WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);
if (sock == INVALID_SOCKET) {
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
}
#elif defined MODE_PROXY
printf("[cpp-httplib] socket by proxy\n");
auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
#else
auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
#endif
// 2. 修改 2247 ~ 2248 行代码
#ifdef MODE_PROXY
printf("[cpp-httplib] connect to proxy\n");
auto ret =
::connect(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));
#else
auto ret =
::connect(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));
#endif
conan
接下来,便是使用 conan 将修改后的 cpp-httplib 托管到远程仓库中。
init
先 cd
进 cpp-httplib 目录,执行 conan new httplib/0.8.8 -t
。
conan new
命令会创建配置文件 conanfile.py
,httplib/0.8.8
指定模块名和版本,-t
指生成测试包 test_package.
来看看 conanfile.py
:
from conans import ConanFile, CMake, tools
class HttplibConan(ConanFile):
# 模块信息
name = "httplib"
version = "0.8.8"
license = "<Put the package license here>"
author = "<Put your name here> <And your email here>"
url = "<Package recipe repository url here, for issues about the package>"
description = "<Description of Httplib here>"
topics = ("<Put some tag here>", "<here>", "<and here>")
# 模块配置
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}
generators = "cmake"
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
# 如果你的源码和 conanfile.py 不在同一个路径,就用这个方法导出源码,我们不用
def source(self):
self.run("git clone https://github.com/conan-io/hello.git")
# This small hack might be useful to guarantee proper /MT /MD linkage
# in MSVC if the packaged project doesn't have variables to set it
# properly
tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(HelloWorld)",
'''PROJECT(HelloWorld)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()''')
# 构建方法,我们可以在这里配置构建参数,比如给 cmake 传递变量
def build(self):
cmake = CMake(self)
cmake.configure(source_folder="hello")
cmake.build()
# Explicit way:
# self.run('cmake %s/hello %s'
# % (self.source_folder, cmake.command_line))
# self.run("cmake --build . %s" % cmake.build_config)
# 打包,在这个方法指定的文件,都会打包到包里,比如我们需要的头文件、编译后的静态/动态库
def package(self):
self.copy("*.h", dst="include", src="hello")
self.copy("*hello.lib", dst="lib", keep_path=False)
self.copy("*.dll", dst="bin", keep_path=False)
self.copy("*.so", dst="lib", keep_path=False)
self.copy("*.dylib", dst="lib", keep_path=False)
self.copy("*.a", dst="lib", keep_path=False)
# 这里可以使用 self.cpp_info 来配置使用本模块的项目,比如这里指定了使用本模块的项目要依赖 hello 这个库,这个 hello 其实是自动生成的,我们需要改成实际的库名 httplib
def package_info(self):
self.cpp_info.libs = ["hello"]
添加 options
上面说了,不同的 settings、options 都会构建不同的包;而我们这里需要两种模式,一种是正常的,一种是我们修改后的代理方式的,所以我们需要添加 options 来让使用方可以控制要构建哪种包。
options = {"shared": [True, False], "fPIC": [True, False], "proxy": [True, False]}
default_options = {"shared": False, "fPIC": True, "proxy": False}
我们这里增加了 proxy
用来让使用方指定是否需要构建代理模式,默认是 False.
settings vs options
settings 跟 options 有什么区别?
- settings 用来指定构建平台、编译器、编译类型等,这些是定义在
~/.conan/settings.yml
里的; - options 是对包的配置,比如动态库还是静态库;一般我们需要定义自己的配置的时候,就在这里定义,比如我们这里定义的
proxy
;
导出源码
默认生成的 conanfile.py 文件中,有一个 source
方法,是用来指定源码的,而我们要用的是 exports_sources
属性。
exports_sources = "httplib.h", "CMakeLists.txt", "httplibConfig.cmake.in"
// 同时删除 source 方法
source vs exports_sources
source
方法是当 conanfile.py 和源码不在一起的时候用的,比如这里自动生成的source
方法,指定的源码是 github 上的一个 hello 库;exports_sources
属性是当 conanfile.py 和源码在同一目录时使用的,这里我们构建 cpp-httplib 的时候,需要的是httplib.h
、CMakeLists.txt
、httplibConfig.cmake.in
三个文件,所以指定为这三个文件;
修改 build 方法
默认生成的 build
方法指定了源码目录,而我们的源码导出后跟 conanfile.py 是同一目录,所以需要修改。
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
修改 package 方法
package
方法用来配置需要打包的文件,其实这里不修改也可以,不过我们这里还是将不需要的部分删除,便于维护。
def package(self):
self.copy("*.h", dst="include", src="")
修改 package_info 方法
package_info
方法一般用来设置 self.cpp_info
,使用方根据这个属性来获得使用我们这个库的一些要求,比如我们需要依赖哪些动态库、定义什么宏之类的。
我们可以在这里根据使用方指定的 proxy
来要求他定义 MODE_PROXY
宏。
def package_info(self):
if self.options.proxy:
self.cpp_info.defines.append("MODE_PROXY")
添加 c++ 11 要求
cpp-httplib 是基于 c++ 11 的,因此我们需要校验使用方指定的 c++ 标准版本。为此,我们在 configure
方法中使用 check_min_cppstd
方法检查,如果使用方配置的版本低于我们指定的版本,就会报错。
def configure(self):
tools.check_min_cppstd(self, "11")
至此,我们的打包配置文件就已经全部修改结束了,看一眼最后的 conanfile.py:
from conans import ConanFile, CMake, tools
class HttplibConan(ConanFile):
# 模块信息
name = "httplib"
version = "0.8.8"
license = "MIT"
url = "https://github.com/yhirose/cpp-httplib"
description = "A C++11 single-file header-only cross platform HTTP/HTTPS library."
topics = ("conan", "cpp-httplib", "http", "https", "header-only")
# 模块配置
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False], "proxy": [True, False]}
default_options = {"shared": False, "fPIC": True, "proxy": False}
generators = "cmake"
# 源文件
exports_sources = "httplib.h", "CMakeLists.txt", "httplibConfig.cmake.in"
def configure(self):
tools.check_min_cppstd(self, "11")
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
# 构建方法,我们可以在这里配置构建参数,比如给 cmake 传递变量
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
# 打包,在这个方法指定的文件,都会打包到包里,比如我们需要的头文件、编译后的静态/动态库
def package(self):
self.copy("*.h", dst="include", src="")
# 这里可以使用 self.cpp_info 来配置使用本模块的项目,比如这里指定了使用本模块的项目要依赖 hello 这个库,这个 hello 其实是自动生成的,我们需要改成实际的库名 httplib
def package_info(self):
if self.options.proxy:
self.cpp_info.defines.append("MODE_PROXY")
修改测试代码
最后,我们需要修改生成的 test_package/example.cpp 以进行测试:
#include <iostream>
#include "httplib.h"
int main() {
#ifdef MODE_PROXY
std::cout << "Mode is MODE_PROXY" << std::endl;
#else
std::cout << "Mode is MODE_DEFAULT" << std::endl;
#endif
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
if (auto res = cli.Get("/hi")) {
std::cout << res->body << std::endl;
} else {
auto err = res.error();
std::cout << err << std::endl;
}
return 0;
}
运行 default 模式
至此,我们的所有的配置就已经修改完成了,接下来运行 conan create . demo/testing
命令,就可以自动导出模块包,并运行我们的测试代码了,输出为:
Mode is MODE_DEFAULT
<a href="https://cpp-httplib-server.yhirose.repl.co/hi">Permanent Redirect</a>.
运行 proxy 模式
要运行 proxy 模式,我们有两种方式,一种是直接在命令行指定 options,一种是在命令行指定 profile 文件。
命令行指定 options
我们直接给 conan create
加上 options : conan create . demo/testing --options httplib:proxy=True
, 也可以使用通配符 *:proxy=True
,运行输出为:
Mode is MODE_PROXY
[cpp-httplib] socket by proxy
[cpp-httplib] connect to proxy
<a href="https://cpp-httplib-server.yhirose.repl.co/hi">Permanent Redirect</a>.
命令行指定 profile
profile 文件是一种配置文件,我们可以在其中指定 settings、options 等信息。conan 默认使用的是 ~/.conan/profiles/default 文件,我们可以通过 --profiles
指定特定的 profile;比如这里,我们可以新建 ~/.conan/profiles/proxy 文件,并在其中指定 options:
[settings]
os=Macos
os_build=Macos
arch=x86_64
arch_build=x86_64
compiler=apple-clang
compiler.version=12.0
compiler.libcxx=libc++
compiler.cppstd=11
build_type=Release
[options]
# 这里也可以使用 *:proxy=True:
httplib:proxy=True
[build_requires]
[env]
然后运行我们的命令:conan create . demo/testing --profile ~/.conan/profiles/proxy
,输出为:
Mode is MODE_PROXY
[cpp-httplib] socket by proxy
[cpp-httplib] connect to proxy
<a href="https://cpp-httplib-server.yhirose.repl.co/hi">Permanent Redirect</a>.
关于 conan create
命令
conan create . demo/testing
命令其实是一组命令的组合:
conan export . demo/testing
conan install httplib/0.8.8@demo/testing --build=httplib
cd test_package
conan test . httplib/0.8.8@demo/testing
这个命令做了几件事:
export
将指定的源码导出到本地缓存中:~/.conan/data/httplib/0.8.8/demo/testinginstall
执行 conanfile.py 里的 configure 、build、package、package_info 方法,进行 c++ 标准版本的检查、构建、导出指定的文件,其实不止执行这几个方法,详情可以看官方文档- 运行
test
命令执行测试
其中 .
指的是模块目录,demo/testing
指定的是 user 和 channel,channel 一般有稳定版(stable)和测试版(testing)。
导出静态库
上面介绍了以单头文件形式导出模块的方式,想必有些同学已经发现了一个小问题,我们把模块要用的 MODE_PROXY
宏定义在了使用方;但这是单头文件形式导出不可避免的方式,因为头文件并不是编译单元,不会被编译,只有在第一次被 include 的时候,才能获取到当前预处理器的宏,也就是说我们只能获得使用方定义的宏。
那有没有其他方式可以将 MODE_PROXY
定义在模块内,而不暴露出去呢?其实是有的,方案也很简单,将使用到这个宏的地方,写到 cpp 里;这样我们的模块导出的,就会变成静态库或者动态库,我们定义的宏就不会暴露到使用方。幸运的是,通过查看 cpp-httplib 的 CMakeLists.txt ,我们可以发现 cpp-httplib 本身就提供了这种方式。
cpp-httplib/CMakeLists.txt 158~190 行:
if(HTTPLIB_COMPILE)
# Put the split script into the build dir
configure_file(split.py "${CMAKE_CURRENT_BINARY_DIR}/split.py"
COPYONLY
)
# Needs to be in the same dir as the python script
configure_file(httplib.h "${CMAKE_CURRENT_BINARY_DIR}/httplib.h"
COPYONLY
)
# Used outside of this if-else
set(_INTERFACE_OR_PUBLIC PUBLIC)
# Brings in the Python3_EXECUTABLE path we can use.
find_package(Python3 REQUIRED)
# Actually split the file
# Keeps the output in the build dir to not pollute the main dir
execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/split.py"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ERROR_VARIABLE _httplib_split_error
)
if(_httplib_split_error)
message(FATAL_ERROR "Failed when trying to split Cpp-httplib with the Python script.\n${_httplib_split_error}")
endif()
# split.py puts output in "out"
set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
# This will automatically be either static or shared based on the value of BUILD_SHARED_LIBS
add_library(${PROJECT_NAME} "${_httplib_build_includedir}/httplib.cc")
target_sources(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
)
可以看到,如果 HTTPLIB_COMPILE
选项被打开,就会将 split.py
和 httplib.h
复制到 ${CMAKE_CURRENT_BINARY_DIR}/out 目录下,然后执行 split.py
,最后将生成的 httplib.cc 编译成静态库;
我们来看一下 split.py
:
import os
import sys
border = '// ----------------------------------------------------------------------------'
PythonVersion = sys.version_info[0];
with open('httplib.h') as f:
lines = f.readlines()
inImplementation = False
if PythonVersion < 3:
os.makedirs('out')
else:
os.makedirs('out', exist_ok=True)
with open('out/httplib.h', 'w') as fh:
with open('out/httplib.cc', 'w') as fc:
fc.write('#include "httplib.h"\n')
fc.write('namespace httplib {\n')
for line in lines:
isBorderLine = border in line
if isBorderLine:
inImplementation = not inImplementation
else:
if inImplementation:
fc.write(line.replace('inline ', ''))
pass
else:
fh.write(line)
pass
fc.write('} // namespace httplib\n')
代码比较简单,就是将分隔符中间的代码放入到 httplib.cc 中,其他部分依然保留在头文件中。刚好,我们上面加的代码,就在这两个分隔符中间,会被复制到 httplib.cc 中。
接下来,我们继续修改 conanfile.py,以支持静态库打包方式:
修改 exports_sources 属性
因为用到了 split.py,所以我们要把这个文件也加到源码文件列表中:
exports_sources = "httplib.h", "CMakeLists.txt", "httplibConfig.cmake.in", "split.py"
修改 build 方法
在 build 方法中,打开 HTTPLIB_COMPILE
、根据 options 控制 cmake 是否要定义 MODE_PROXY
宏:
def build(self):
cmake = CMake(self)
cmake.definitions["HTTPLIB_COMPILE"] = "ON"
if self.options.proxy:
cmake.definitions["MODE_PROXY"] = "ON"
cmake.configure()
cmake.build()
修改 package 方法
有两个地方要修改,一个是我们要导出处理后的头文件,也就是 out 目录下的;第二个是我们需要将静态库也导出:
def package(self):
self.copy("*.h", dst="include", src="out")
self.copy("*.a", dst="lib", keep_path=False)
修改 package_info 方法
之前我们在这里要求使用方定义宏,现在使用方不需要再定义宏了;但是我们需要要求使用方依赖我们的静态库,同时依赖系统的 thread 和 zlib 动态库:
def package_info(self):
self.cpp_info.libs = ["httplib"]
self.cpp_info.system_libs = ["pthread", "z"]
The
system_libs
are for libraries that do not belong to this package, and are installed in the system, likepthread
. Any library that it is built as part of the package, should go tolibs
.《difference between “cpp_info.libs” and “cpp_info.system_libs”》
至此,我们的打包配置文件已经修改完成了,看一眼最后的 conanfile.py:
from conans import ConanFile, CMake, tools
class HttplibConan(ConanFile):
# 模块信息
name = "httplib"
version = "0.8.8"
license = "MIT"
url = "https://github.com/yhirose/cpp-httplib"
description = "A C++11 single-file header-only cross platform HTTP/HTTPS library."
topics = ("conan", "cpp-httplib", "http", "https", "header-only")
# 模块配置
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False], "proxy": [True, False]}
default_options = {"shared": False, "fPIC": True, "proxy": True}
generators = "cmake"
# 源文件
exports_sources = "httplib.h", "CMakeLists.txt", "httplibConfig.cmake.in", "split.py"
def configure(self):
tools.check_min_cppstd(self, "11")
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
# 构建方法,我们可以在这里配置构建参数,比如给 cmake 传递变量
def build(self):
cmake = CMake(self)
cmake.definitions["HTTPLIB_COMPILE"] = "ON"
if self.options.proxy:
cmake.definitions["MODE_PROXY"] = "ON"
cmake.configure()
cmake.build()
# 打包,在这个方法指定的文件,都会打包到包里,比如我们需要的头文件、编译后的静态/动态库
def package(self):
self.copy("*.h", dst="include", src="out")
self.copy("*.a", dst="lib", keep_path=False)
# 这里可以使用 self.cpp_info 来配置使用本模块的项目,比如这里指定了使用本模块的项目要依赖 hello 这个库,这个 hello 其实是自动生成的,我们需要改成实际的库名 httplib
def package_info(self):
self.cpp_info.libs = ["httplib"]
self.cpp_info.system_libs = ["pthread", "z"]
修改 CMakeLists.txt
在 build 方法指定的只是传入 cmake 的参数,我们需要在 CMakeLists.txt 处理,生成真正的宏,直接在 CMakeLists.txt 最后加入以下几行代码:
option(MODE_PROXY "proxy mode" OFF)
if(MODE_PROXY)
message(STATUS "[cpp-httplib] build with proxy mode")
target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} MODE_PROXY)
else()
message(STATUS "[cpp-httplib] build with default mode")
endif()
option
: If no initial<value>
is provided,OFF
is used. If<variable>
is already set as a normal or cache variable, then the command does nothing.
在这里定义的宏,只会在编译静态库的时候生效,也就不会暴露到使用方那边了。
运行 proxy 模式
我们再来运行一下 conan create . demo/testing --options httplib:proxy=True
,输出如下:
Mode is MODE_DEFAULT
[cpp-httplib] socket by proxy
[cpp-httplib] connect to proxy
<a href="https://cpp-httplib-server.yhirose.repl.co/hi">Permanent Redirect</a>.
可以看到,我们定义的宏确实只在静态库内生效了。
其他
删除已打包的库
# 删除本地库
conan remove httplib
# 删除远程库, xxxx 是远程仓库在我们本地的名称
conan remove httplib -r xxxx
上传到远程仓库
# xxxx 是远程仓库在我们本地的名称
conan upload httplib -r xxxx