xizhibei / blog

个人博客,(Node.js/Golang/Backend/DevOps),欢迎 Star, Watch 订阅以及评论
https://blog.xizhibei.me
Other
431 stars 29 forks source link

CMake 动态链接库绝对路径问题 #162

Open xizhibei opened 3 years ago

xizhibei commented 3 years ago

今天这篇文章算是对 【CMake 系列】(五)安装、打包与导出 的一个补充。其实我本打算跟上篇文章放在一起,毕竟都属于动态链接库相关的知识,但是这样一来就不容易被出现问题的同学们检索到了(才不是为了再水一篇文章 doge)。

问题的由来

是因为这个问题困扰了我不少时间,在好几个项目里面都遇到了这个问题。

那就是链接动态库的时候,编译出来的可执行文件会带有编译时的绝对路径,于是你将程序拷贝到其它地方运行的时候,必须把动态库放到绝对路径里面去,而不是放在系统里面相关的 lib 路径下面。

举一个例子,假如我们要实现一个 FooConfig.cmake,这个库中既有静态库也有动态库,那么如果我们要在项目中使用,大概的实现方式是:

find_path(FOO_INCLUDE_DIRS NAMES foo.h)

get_filename_component(_IMPORT_PREFIX "${FOO_INCLUDE_DIRS}" PATH)
set(FOO_LIBRARY_DIRS ${_IMPORT_PREFIX}/lib)

if(NOT FOO_FIND_COMPONENTS)
  set(FOO_FIND_COMPONENTS foo bar)
endif()

set(FOO_USE_SHARED 1)
set(_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
if(FOO_USE_SHARED)
  set(CMAKE_FIND_LIBRARY_SUFFIXES .so)
else()
  set(CMAKE_FIND_LIBRARY_SUFFIXES .a)
endif()

foreach(lib ${FOO_FIND_COMPONENTS})
  set(_lib_location "_lib_location-NOTFOUND")
  find_library(_lib_location NAMES "${lib}")
  if(NOT _lib_location)
    message(FATAL_ERROR "FOO lib '${lib}' is not found")
  endif()

  set(_lib_name FOO::${lib})
  add_library(${_lib_name} UNKNWON IMPORTED)
  set_target_properties(
    ${_lib_name}
    PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${FOO_INCLUDE_DIRS}"
               IMPORTED_LOCATION_RELEASE "${_lib_location}"
               IMPORTED_CONFIGURATIONS RELEASE)

  list(APPEND FOO_LIBS "${_lib_name}")

  unset(_lib_location) # clean
  unset(_lib_name) # clean
endforeach()
set(CMAKE_FIND_LIBRARY_SUFFIXES ${_CMAKE_FIND_LIBRARY_SUFFIXES})

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Reader REQUIRED_VARS FOO_INCLUDE_DIRS
                                                       FOO_LIBS)

mark_as_advanced(FOO_INCLUDE_DIRS FOO_LIBS)

# cleanup
unset(_IMPORT_PREFIX)
unset(_CMAKE_FIND_LIBRARY_SUFFIXES)

将它命名为 FooConfig.cmake 然后放在位于项目根目录的 cmake 文件夹下,并且在项目中这样使用:

find_package(Foo REQUIRED HINTS ${PROJECT_SOURCE_DIR}/cmake)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE ${FOO_LIBS})

最后,假如我们查找的库在 /path/to/foo/home 下面,那么我们用在项目中得到的结果会是这样的:

$ readelf -d a.out | grep NEEDED
Dynamic section at offset 0xb5ddb4 contains 2 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [/path/to/foo/home/lib/libfoo.so]
 0x00000001 (NEEDED)                     Shared library: [/path/to/foo/home/lib/libbar.so]

这里就出现了绝对路径,当初这个问题折磨了我很久,一直以为是 RPATH 的问题,最后发现是 CMake 本身的问题。

如何解决

出现这个问题的原因就是库的 Package Find Config 不对,我研究了挺长时间,最后在官方的讨论中找到了原因以及答案:

  1. 缺少了 IMPORTED_NO_SONAME 的属性;
  2. 引入动态库的时候,使用了 UNKNWON 类型的库;

于是,将上面的代码改下即可:

# ...

if(FOO_USE_SHARED)
  set(FOO_LIB_TYPE "SHARED")
  set(CMAKE_FIND_LIBRARY_SUFFIXES .so)
else()
  set(FOO_LIB_TYPE "STATIC")
  set(CMAKE_FIND_LIBRARY_SUFFIXES .a)
endif()

# ...

set_target_properties(
    ${_lib_name}
    PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${FOO_INCLUDE_DIRS}"
               IMPORTED_NO_SONAME_RELEASE true
               IMPORTED_LOCATION_RELEASE "${_lib_location}"
               IMPORTED_CONFIGURATIONS RELEASE)

# ...
kbore commented 3 years ago

请问作者, FooConfig.cmake的内容是靠 install(export) 自动生成的吗? 我想将这部分内容从手写改为cmake自动生成, 但是无法处理区分链接 动态库或静态库的需求, 导致只能手写Findxxx.cmake文件.

xizhibei commented 3 years ago

请问作者, FooConfig.cmake的内容是靠 install(export) 自动生成的吗? 我想将这部分内容从手写改为cmake自动生成, 但是无法处理区分链接 动态库或静态库的需求, 导致只能手写Findxxx.cmake文件.

不是,上面的例子是给第三方没有实现 cmake 支持的库写的,如果是自己的项目就参考 https://github.com/xizhibei/blog/issues/137 这个里面写的自动生成

kbore commented 3 years ago

拜读过 #137 ,但是那样生成的 cmake config 文件应该不支持区分静态库或者动态库, 而且动态库和静态库是两个 cmake config 文件. 不知作者是否有研究过如何将动态库和静态库合并导出一个 cmake config 文件让其他人引用? 目前我是手写的, 正在找有没有自动生成的方法.

xizhibei commented 3 years ago

大概明白你意思了,我说个我们自己使用的方式:

从 target 名称进行区分,比如静态库是 foo 而动态库是 fooShared,这样的话,编译一次就可以自动生成一个 find config,不过你会导出名字变为 libfooShared.so,想要统一的话,可以用 set_target_properties(fooShared PROPERTIES OUTPUT_NAME foo)

kbore commented 3 years ago

比如静态库是 foo 而动态库是 fooShared,这样的话,编译一次就可以自动生成一个 find config -- 请问这步是如何实现的? 我理解成是写成下面这样的代码, 这样会生成2个find config.

install(TARGETS foo EXPORT myfooLib)
install(EXPORT myfooLib)

install(TARGETS fooShared EXPORT myfooSharedLib)
install(EXPORT myfooSharedLib)
xizhibei commented 3 years ago

可以直接参考 #137 的导出部分来,我给个关键的部分

add_library(fooStatic STATIC foo.cpp)
set_target_properties(fooStatic PROPERTIES OUTPUT_NAME foo) # libfoo.a

add_library(fooShared SHARED foo.cpp)
set_target_properties(fooShared PROPERTIES OUTPUT_NAME foo) # libfoo.so

install(
  TARGETS fooStatic fooShared
  EXPORT Foo
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES
  DESTINATION include)

install(
  EXPORT Foo
  FILE FooTargets.cmake
  NAMESPACE Foo::
  DESTINATION lib/cmake/Foo/)

在使用的时候:

find_package(Foo)
target_link_libraries(bar PUBLIC Foo::fooShared)
# 或者
target_link_libraries(bar PUBLIC Foo::fooStatic)
kbore commented 3 years ago

这两天我试了下, 按照您给的建议, 已经将动态库和静态库合一安装, 其他 project 也能正常引用到. 说来也巧, 真的是因为这篇文章独立的标题 我才能检索到这的, 再次感谢下.

另外, 关于安装部分有个问题想再请教下. 假设下面这种场景

  1. library A 是一个单独的 build tree, 它依赖另一个 import target B(也属于 library A 的 build tree).
  2. binary B 是个独立的 build tree, 通过 find_package 引用 library A 的 install 产物, library A 的 target 能正常引用到, 但是会提示 target B not found.
  3. 问题的原因是 library A 在引用 target B 时选择了 PUBLIC 属性, 而 target B 因为是 import target 又无法被 install, library A 的 config 文件中也就不会体现出 target B.

我想了下, 要么停止 library A 的依赖库传染, 要么想办法把 target B 也传递给 binary B. 前者应该会导致依赖不完整,, 后者感觉应该是在您前面提到的 myLibConfig.cmake.in 做文章.

xizhibei commented 3 years ago

hh,挺巧

你说这个问题没遇到过,目前除了你说的,没有更好的办法了

kbore commented 3 years ago

hh,挺巧

你说这个问题没遇到过,目前除了你说的,没有更好的办法了

周末检索各种资料确认了下, CMake官方的态度是这类第三方依赖应该downstream提前安装好, upstream在 myLibConfig.cmake.in 中写明依赖项即可.
根据我自己的工程实践, 觉得使用上还是麻烦了点, 尤其是一些闭源的场景. 目前的解决方案是在 myLibConfig.cmake.in 做文章, 根据需要导出的target清单自动重写了imported target的依赖, 感觉可以接受了.
不管怎样, 感谢作者的博客和答疑, 帮我解决了很大的困惑.