美文网首页
Qt构建Mac包的自动化脚本

Qt构建Mac包的自动化脚本

作者: 仙人掌__ | 来源:发表于2024-05-05 10:39 被阅读0次

    背景

    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

    相关文章

      网友评论

          本文标题:Qt构建Mac包的自动化脚本

          本文链接:https://www.haomeiwen.com/subject/efcgadtx.html