Qt构建Mac包的自动化脚本

背景

Qt是一个跨平台开发框架,业界也有不少成熟产品基于该技术,它的好处在于一套代码即可产出各个端(mac、windows、linux)的安装包,极大的节省了开发成本。由于Qt的跨平台特性,传统native的构建技术可能就不完全适用了,基于这个背景,需要探索出基于Qt平台的构建技术。如下为mac平台构建技术的探索过程记录。

构建Mac平台包

  • mac安装包的目录结构

它的标准目录结构图如下,其中MacOs(对应着可执行文件)、_CodeSignature(对应签名)、Info.plist(App相关信息)、PkgInfo、Resources这些都是必备文件,其它文件和文件夹(Frameworks等等)则根据需要创建。

image.png
  • mac平台下非app store下release安装包的构建过程
image.png

对于基于Xcode工程的项目,通过xcodebuild命令完成编译过程,而对于qt项目,由于它以xxx.pro(基于qmake)或者CMakeLists.txt(基于Cmake)来组织工程文件目录结构,所以需要通过Cmake或者qmake工具来进行编译,官方现在推荐Cmake方式,而且Windows平台下CMake也更加友好写,所以这里选择CMake方式。

1、设置CMake相关环境变量

export PATH="/Users/xxxxxx/qt/Tools/CMake/CMake.app/Contents/bin:$PATH"
export PATH="/Users/xxxxx/qt/Tools/Ninja:$PATH"

2、配置CMakeLists.txt

Mac应用对于引用的三方动态库采取的策略是集成到安装包的Frameworks文件夹下,CMake默认情况下对于这些动态库的链接是绝对路径方式(通过otool -L命令可以查看),所以还需要改成相对路径的形式(通过install_name_tool 命令),xcodebuild编译时自动完成这个过程,Qt官方也提供了一个脚本自动完成这一过程。只需要在CMakeLists.txt最后添加如下代码即可

install(TARGETS QTTest
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
qt_generate_deploy_app_script(
    TARGET QTTest
    OUTPUT_SCRIPT deploy_script
    NO_UNSUPPORTED_PLATFORM_ERROR
)
install(SCRIPT ${deploy_script})

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(QTTest)
endif()

3、编译

mkdir build
cmake -DCMAKE_PREFIX_PATH=/Users/xxxxx/qt/6.5.3/macos -S ./ -B ./build -G Ninja
cd build
ninja

4、签名公证及验证

调用codesign指令,签名成功后还需要去发给苹果公证,公证完成后才可以放在互联网上分发。具体的公证及dmg生成过程可以参考

# 签名
codesign --force --deep --verbose --options=runtime --sign "证书名称(钥匙串中)" xxx.app路径
# 验证签名
codesign --verify --deep --strict --verbose=2 xxx.app路径
# 将app打包为zip包
/usr/bin/ditto -c -k --sequesterRsrc --keepParent xxx.app路径 xxx.zip路径
# 进行公证
xcrun notarytool submit "xxx.zip路径" --keychain-profile "公证专用秘钥" --wait
/usr/bin/ditto -x -k "${xxx.zip}" ./
# 验证公证结果
xcrun stapler validate xxx.app路径
# 生成dmg
python3 create_dmg.py "xxx.app" "dmg路径"

完整构建脚本

#!/usr/bin/env bash

version="2.0.4"
qt_lib_path=/Users/zhuangshanzhi/qt/6.5.3/macos
qt_cmake_path=/Users/zhuangshanzhi/qt/Tools/CMake/CMake.app/Contents/bin
qt_ninja_path=/Users/zhuangshanzhi/qt/Tools/Ninja

current_path="$(pwd)"
build_result="${current_path}/BuildResult"
parent_path=$(dirname "$current_path")
app_name=$(cat ../CMakeLists.txt |grep -i 'Project(' | sed 's|.*(\([a-zA-Z0-9]*\)\ .*|\1|g')
buildabs_mac="/usr/local/bin/buildabs_mac"
new_build_version=""
build_type="test"
input_path=""
output_path=""
param_count=0
verbose=0
need_notray=0
mark_dmg=0
profile="MytestApp profile"
channels=""
valid_args=(-new -input -output -notary -dmg -profile -channels -version -verbose -help)

# 打印帮助
function print_help_and_exit() {
  echo "-环境参数: test、developerid、release,固定为第一个参数"
  echo "-new: 指定编译版本号,会同步更改 Xcode 工程中的编译版本号"
  echo "-input: .app 文件路径,默认是当前工程目录下的 /Buider/BuildResult 目录"
  echo "-output: dmg 包输出路径,默认是当前工程目录下的 /Buider/BuidResult 目录"
  echo "-notary: 标记需要公证,无需携带参数。(环参数为:developerid、release 时默认会公证)"
  echo "-dmg: 标记需要生成dmg,无需携带参数。(环参数为:developerid、release 时默认会生成)"
  echo "-profile: 自定义用于公证的 keychain-profile 名称,默认:MytestApp profile"
  echo "-channels: 渠道号,多个渠道用英文的逗号隔开,eg.: -channels \"web,ab123\""
  echo "-version: 查看脚本的版本号"
  echo "-verbose 提供的详细状态输出"
  echo "-help: 查看帮助"
  echo ""
}

invalid_param() {
  # 传了无效参数
  echo "⚠️ 参数无效⚠️ :"
  print_help_and_exit
  exit 1
}

# 用于控制加载动画是否继续运行的变量
is_loading=false

# 加载动画函数
loading_animation() {
  chars="/-\|"
  while :; do
    for ((i = 0; i < ${#chars}; i++)); do
      echo -ne "\b$1${chars:$i:1}"
      sleep 0.1
    done
  done
}

loading_animation_1015() {
  chars=('|' '/' '-' '\')
  idx=0
  while true; do
    printf '\r%s' "$1${chars[idx]}"
    sleep 0.1
    ((idx = (idx + 1) % ${#chars[@]}))
  done
}

stop_animation() {
  is_loading=false
  kill $loading_pid
  wait $loading_pid 2>/dev/null
  printf "\r"
}

kill_animation() {
  stop_animation
  exit 0
}

# 启动加载动画
begin_load_animation() {
  is_loading=true
  trap kill_animation SIGINT
  loading_animation &
  loading_pid=$!
}

# 生成快捷方式
if [ ! -f "$buildabs_mac" ]; then
  sudo ln -s "${current_path}/buildabs_mac" "$buildabs_mac"
  sudo chmod +x "$buildabs_mac"
  echo "生成快捷方式成功,后续可直接使用:\`buildabs_mac [action]\`, eg. buildabs_mac test"
fi

# 判断第一个参数是否为 "test", "developerid" 或 "release"
if [ "$1" == "test" ] || [ "$1" == "developerid" ] || [ "$1" == "release" ]; then
  build_type="$1"
  shift # 移除已处理的第一个参数
else
  if [[ $1 =~ ^- ]]; then
    found=0
    for valid_arg in "${valid_args[@]}"; do
      if [ "$1" == "$valid_arg" ]; then
        found=1
        break
      fi
    done
    if [ $found -eq 0 ]; then
      invalid_param
    fi
  else
    invalid_param
  fi
fi

# 遍历所有参数
while (("$#")); do
  # 当前参数以 "-" 开头,表示这是一个选项
  if [[ $1 == -* ]]; then
    option_name="$1"
    case "$option_name" in
    -new)
      # if [[ -n "$2" ]] && [[ $2 != -* ]]; then
      new_build_version="$2"
      shift 2
      ;;
    -input)
      input_path="$2"
      shift 2
      ;;
    -output)
      output_path="$2"
      shift 2
      ;;
    -notary)
      need_notray=1
      shift
      ;;
    -dmg)
      mark_dmg=1
      shift
      ;;
    -profile)
      profile="$2"
      shift 2
      ;;
    -channels)
      channels="$2"
      shift 2
      ;;
    -verbose)
      verbose=1
      shift
      ;;
    -help)
      print_help_and_exit
      exit 0
      ;;
    -version)
      echo "$version"
      exit 0
      ;;
    *)
      invalid_param
      print_help_and_exit
      exit 1
      ;;
    esac
  else
    invalid_param
    print_help_and_exit
    exit 1
  fi
done

read_default_channel_id() {
  # 获取上层目录路径
  parent_directory="$(dirname "$build_result")"
  grandparent_directory="$(dirname "$parent_directory")"
  plist_file="$grandparent_directory/MytestApp/MytestApp-Info.plist"
  channels=$(defaults read "${plist_file}" "Channel")
}

stepup_workspace() {
  echo "➤➤➤ "
  sub_build_result="$build_result/$new_version"
  if [ ! -d "$build_result" ]; then
    mkdir -p "${build_result}"
  fi
  echo "工作目录:$current_path"
  # 删除,如果存在
  if [ -d "$sub_build_result" ]; then
    rm -r $sub_build_result
    echo "目录已存在 -> 删除"
  fi
  # 创建目录
  if [ ! -d "$sub_build_result" ]; then
    mkdir -p "${sub_build_result}"
    echo "创建目录: ${sub_build_result}"
  fi

  output_path=$sub_build_result
}

buuild_app_if_need() {
    if [ -d "$parent_path/build" ]; then
        rm -rf ../build
    fi
    mkdir -p $parent_path/build
    
    export PATH="$qt_cmake_path:$PATH"
    export PATH="$qt_ninja_path:$PATH"
    
    # 如果没有传入 App,则编译 App
    if [ -z "$input_path" ]; then
        echo "➤➤➤"
        echo "开始编译 App"
        echo "编译日志:${log_path}"
        echo -n "编译ing......"
    
        begin_load_animation
        cmake -DCMAKE_PREFIX_PATH=$qt_lib_path -DCMAKE_INSTALL_PREFIX=../build  -S ../ -B ../build -G Ninja
        cd ../build
        ninja
        ninja install
        cd -
        stop_animation
        
        input_path="$output_path/${app_name}.app"
        cp -R $parent_path/build/${app_name}.app "$output_path"
        
        if [ -e "$input_path" ]; then
            echo "编译完成✅ : $input_path"
        else
            echo "编译失败❌ :,请查看日志。"
        exit 1
        fi
    fi
}

change_channel_id_in_project() {
  echo "➤➤➤"
  echo "更改渠道号: $1"
  defaults write "$input_path/Contents/Info.plist" Channel -string "$1"
  read user_input
}

change_channel_id_in_app_if_need() {
  echo "➤➤➤"
  echo "核对渠道号"

  old_channel=$(defaults read "$input_path/Contents/Info.plist" Channel)
  if [ "$1" = "$old_channel" ]; then
    echo "渠道号一致,无需更改: $1"
    return 1
  else
    echo "更改渠道号: $1"
    defaults write "$input_path/Contents/Info.plist" Channel -string "$1"
    return 0
  fi
}

copy_app() {
  echo "➤➤➤"
  echo "拷贝 App......t"
  echo "input_path: $input_path"
  echo "output_path: $output_path"
  echo "zip_output: $zip_output"

  begin_load_animation
  # /usr/bin/ditto -x -k "$zip_output" "$output_path"
  cp -R "$input_path" "$output_path"
  stop_animation
  input_path=$output_path/$app_name
}

recodesign() {
  echo "删除历史签名"
  codesign --remove-signature "$input_path"

  echo "重签名"
  # codesign --force --deep --verbose --options=runtime --sign "Pixocial Technology (Singapore) Pte Ltd (5V292QZ538)" "$input_path"
  codesign --force --deep --options runtime --timestamp --verbose=2 --sign "Pixocial Technology (Singapore) Pte Ltd (5V292QZ538)" "$input_path"
}

create_zip() {
  # 创建 zip 名称
  name=$(echo $app_name | sed 's/[ ][ ]*/_/g')
  project_version=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" $input_path/Contents/Info.plist)
  if [ "$build_type" == "test" ]; then
    zip_app_name="${name}_${project_version}($project_build_version)_universal_unsigned"
  elif [ "$build_type" == "release" ]; then
    zip_app_name="${name}_${project_version}_universal"
  else
    zip_app_name="${name}_${project_version}($project_build_version)_universal"
  fi
}

verify_codesign() {
  echo "➤➤➤"
  echo "验证签名"
  codesign --verify --deep --strict --verbose=2 "${input_path}"
  # --verbose=2(在这里是 2)表示日志级别。该参数值可以为 0、1、2 或 3。具体含义如下:
  # 0:仅显示错误消息
  # 1:显示错误和警告消息
  # 2:显示所有校验过程中的信息,包括详细的签名内容
  # 3:显示额外的调试信息

  # 确认是否需要公证及生成 dmg
  if [ "$build_type" != "test" ]; then
    need_notray=1
    mark_dmg=1
  else
    open $output_path
  fi
}

app_to_zip() {
  # 压缩 zip
  echo "➤➤➤"
  echo -n "压缩 zip......"
  begin_load_animation
  cd ${output_path} || exit
  zip_output="${output_path}/$zip_app_name.zip"
  /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$input_path" "$zip_output"
  stop_animation
  if [ -e "${zip_output}" ]; then
    echo "压缩完成✅: $zip_output"
  else
    echo "压缩失败❌"
    exit 1
  fi
}

replace_zip() {
  rm -r "$zip_output"
  /usr/bin/ditto -c -k --keepParent "$input_path" "$zip_output"
}

notary_app() {
  # 公证
  if [ $need_notray -eq 1 ]; then
    echo "➤➤➤"
    echo "开始公证......"
    rm -r "$input_path"
    xcrun notarytool submit "${zip_output}" --keychain-profile "${profile}" --wait
    /usr/bin/ditto -x -k "${zip_output}" ./
    validate_result=$(xcrun stapler staple "${input_path}")
    xcrun stapler validate "${input_path}"

    if [[ "${validate_result}" =~ "The staple and validate action worked" ]]; then
      echo '公证成功✅'
      replace_zip &
    else
      echo '公证失败!❌'
      # xcrun notarytool info "3b76bc86-7245-4735-97dd-09ca2f1c4e59" --keychain-profile "MytestApp profile"
      # xcrun notarytool log "3b76bc86-7245-4735-97dd-09ca2f1c4e59" --keychain-profile "MytestApp profile"
      exit 1
    fi
  fi
}

make_dmg() {
  # 生成 DMG
  if [ $mark_dmg -eq 1 ]; then
    cd "$current_path" || exit
    dmg_output_path="${output_path}/$zip_app_name.dmg"
    echo "➤➤➤"
    echo "生成 DMG......"
    echo "input: $input_path"
    echo "output: $dmg_output_path"

    if [ $verbose -eq 1 ]; then
      dmg_log_path="$output_path/MytestApp_make_dmg.log"
      python3 create_dmg.py "$input_path" "$dmg_output_path" >"$dmg_log_path" 2>&1
    else
      begin_load_animation
      python3 create_dmg.py "$input_path" "$dmg_output_path" >/dev/null 2>&1
      stop_animation
    fi

    if [ -f "$dmg_output_path" ]; then
      echo '生成 dmg 包成功✅:'
      echo "$dmg_output_path"
      open $output_path
    else
      echo '生成 dmg 包失败!❌'
      exit 1
    fi
  fi
}

if [ -z "$channels" ]; then
  # 读取默认的渠道号
  read_default_channel_id
fi

#设置输出目录
stepup_workspace
#1、编译App
buuild_app_if_need
#2、签名
recodesign
#3、验证签名
verify_codesign
create_zip
app_to_zip
#4、公证
notary_app
make_dmg "$element"

if [ -e "$zip_output" ]; then
  open "${sub_build_result}"
fi

exit 0

最后目录如下:


image.png

脚本下载地址:
https://gitee.com/nldxrz/ffmpeg-build-scripts/tree/master/qt

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容