C++ Mac 打包运行方案(cmake)

发布于:2025-05-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

背景

使用C++编写的一个小项目,需要打包成mac下的可执行文件(免安装版本),方便分发给其他mac执行,需要把项目的动态库都打在软件包中,分发之后可以直接运行,而不需要再重复安装。

动态库梳理

经过依赖精简和梳理,项目最终必须依赖的动态库包括:pcl, bzip2, lz4, yaml, rosbag(用于读取rosbag包,后续会有专门的文章会提到如何做到不依赖ros环境)。 在Mac上,这些动态库都已经通过homebrew工具安装好。

brew install cmake bzip2 lz4 pcl

brew 在安装bzip2lz4 时,也会默认安装他们的静态库文件(liblz4.alibbz2.a),而pcl库是直接安装的动态库文件。

打包方案

yaml动态库在前面的文章中已经转成了静态库代码的形式包含在了项目里。对于动态库,和linux类似,需要把动态库(dylib)直接打包到存放可执行文件的目录中。不过,不同于linux,mac上需要处理动态库自身的动态库依赖。

由于我们使用homebrew安装依赖库,所以我们在编译时(CMakeLists.txt)中需要在brew的安装路径下查找定位依赖库。

# Homebrew 安装路径
set(HOMEBREW_PREFIX "/usr/local/opt")

静态库处理

对于bzip2lz4,我们在编译的时候直接依赖他们的静态库文件,这样比较方便。

# lz4, bzip2 静态库路径
set(LZ4_LIBRARY "${HOMEBREW_PREFIX}/lz4/lib/liblz4.a")
set(BZIP2_LIBRARIES "${HOMEBREW_PREFIX}/bzip2/lib/libbz2.a")

find_package(BZip2 REQUIRED)
# 显式指定 BZip2 静态库(覆盖默认动态库)
if(BZIP2_FOUND)
    set(BZIP2_LIBRARIES "${HOMEBREW_PREFIX}/bzip2/lib/libbz2.a")
    message(STATUS "BZip2 static lib: ${BZIP2_LIBRARIES}")
endif()

# 定位 lz4 静态库
find_library(LZ4_LIBRARY_RELEASE NAMES liblz4.a PATHS "${HOMEBREW_PREFIX}/lz4/lib" REQUIRED)
find_library(LZ4_LIBRARY_DEBUG NAMES liblz4d.a PATHS "${HOMEBREW_PREFIX}/lz4/lib")
if(LZ4_LIBRARY_RELEASE)
    set(LZ4_LIBRARIES ${LZ4_LIBRARY_RELEASE})
    if(LZ4_LIBRARY_DEBUG)
        set(LZ4_LIBRARIES optimized ${LZ4_LIBRARY_RELEASE} debug ${LZ4_LIBRARY_DEBUG})
    endif()
    message(STATUS "lz4 static lib found: ${LZ4_LIBRARIES}")
endif()

动态库处理(PCL库)

# PCL 动态库路径
set(PCL_DIR "${HOMEBREW_PREFIX}/pcl/lib/cmake/pcl")  
list(APPEND CMAKE_PREFIX_PATH "${PCL_DIR}")

# macOS 动态库运行时搜索路径设置
set(CMAKE_INSTALL_RPATH "@executable_path")
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)

# 框架链接(PCL 可能需要的系统框架)
link_libraries("-framework Accelerate" "-framework OpenGL" "-framework Foundation")

find_package(PCL REQUIRED COMPONENTS io)
if(PCL_FOUND)
    message(STATUS "PCL Found: ${PCL_LIBRARIES}")
    include_directories(${PCL_INCLUDE_DIRS})
else()
    message(FATAL_ERROR "PCL not found!")
endif()

编译链接

add_executable(MyApp ...)

target_include_directories(MyApp PUBLIC 
  ${PCL_INCLUDE_DIRS}
  ${CMAKE_SOURCE_DIR}/thirdparty/yaml-cpp/include
  ...
)

# 链接配置:PCL动态库 + 其他静态库
target_link_libraries(MyApp PUBLIC 
  ${PCL_LIBRARIES}       # PCL动态库
  yaml-cpp               # 静态源码编译的yaml-cpp
  ${BZIP2_LIBRARIES}     # BZip2静态库
  ${LZ4_LIBRARIES}       # lz4静态库
)

动态库后处理逻辑

首先,可以使用 otool -L 提取依赖的动态库,不管是App还是动态库本身,都可以用这个命令来定位到他们的依赖。这里和linux不同的地方在于,需要递归去处理动态库及动态库自身的依赖。针对以下两种情况分别处理:

  1. 动态库路径中包含 /usr/local :直接复制对应地址的动态库到目标路径的libs下(dist/libs/

  2. 动态库路径中包含 @rpath:这种情况比较麻烦,因为根据@rpath并不能定位到动态库的路径,需要根据动态库类型替换@rpath为实际的动态库路径:

if echo "$dep" | grep -q "vtk"; then
  replaced_str=${dep//@rpath//usr/local/opt/vtk/lib}
  echo $replaced_str
  cp -L "$replaced_str" dist/libs/

把动态库拷贝到目标路径后,就可以递归调用相同的方法,定位拷贝动态库自身的依赖。从递归返回之后,就可以修改当前动态库依赖的路径为新的目标路径,即dist/libs/

new_dep_path="@executable_path/../libs/$dep_name"
install_name_tool -change "$dep" "$new_dep_path" "$lib" 

完整脚本如下:

#!/bin/bash
set -e

rm -rf build && mkdir build && cd build
cmake .. -DBP_USE_PCL=ON
make -j$(nproc)

process_deps() {
  local lib=$1
  echo "process lib $1"
  
  # 提取当前库的依赖项
  otool -L "$lib" | 
    awk 'NR>1 && /^\t/ {print $1}' | 
    grep -E "/usr/local|@rpath" |
    while read dep; do
      # 复制依赖库到 libs/(如果尚未存在)
      dep_name=$(basename "$dep")
      if [ ! -f "dist/libs/$dep_name" ]; then
        if echo "$dep" | grep -q "vtk"; then
          replaced_str=${dep//@rpath//usr/local/opt/vtk/lib}
          echo $replaced_str
          cp -L "$replaced_str" dist/libs/
        elif echo "$dep" | grep -q "Qt"; then
          replaced_str=${dep//@rpath//usr/local/opt/qt/lib/}
          echo $replaced_str
          cp -L "$replaced_str" dist/libs/
        elif echo "$dep" | grep -q "pcl"; then
          replaced_str=${dep//@rpath//usr/local/opt/pcl/lib/}
          echo $replaced_str
          cp -L "$replaced_str" dist/libs/
        elif echo "$dep" | grep -q "boost"; then
          replaced_str=${dep//@rpath//usr/local/opt/boost/lib/}
          echo $replaced_str
          cp -L "$replaced_str" dist/libs/
        else
          cp -L "$dep" dist/libs/
        fi
        chmod +w dist/libs/"$dep_name"  # 确保可修改权限
        process_deps "dist/libs/$dep_name"  # 递归处理
      fi
      # 修改当前库的依赖路径
      new_dep_path="@executable_path/../libs/$dep_name"
      install_name_tool -change "$dep" "$new_dep_path" "$lib" 
    done
}

# 1. 初始化目录
rm -rf dist
mkdir -p dist/{libs,bin}

# 2. 拷贝可执行文件
cp MyApp dist/bin/
cp ../mac-prepare.sh dist/
chmod +x dist/mac-prepare.sh
APP_PATH="dist/bin/MyApp"
process_deps "$APP_PATH"
echo "Bundle created in dist/"

批量信任

迁移到其他mac上执行时,会提示不可信任的开发者,而且不只是可执行文件会提示,每个动态库都会提示,人工处理显然处理不过来。

在这里插入图片描述

所以,我们要在目标mac上先进行批量信任才能正常运行该可执行文件。

#!/bin/bash
set -e

# 目标机器上执行
BIN_DIR="./bin"
LIB_DIR="./libs"

# # 步骤1:递归移除所有隔离属性
echo "[1/3] Removing quarantine attributes..."
xattr -dr com.apple.quarantine "$BIN_DIR"
xattr -dr com.apple.quarantine "$LIB_DIR"

# # 步骤2:递归重新签名
echo "[2/3] Re-signing binaries and libraries..."
find "$LIB_DIR" \
  -type f \( -name "*.dylib" -o -name "*.so" -o -perm +111 \) \
  -exec codesign --force --sign - {} \; 2>/dev/null

网站公告

今日签到

点亮在社区的每一天
去签到