#!/bin/bash # 通用打包函数(由 project_pkg_desktop.sh 和 project_pkg_service.sh 引用) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" source "${SCRIPT_DIR}/project_registry.sh" # ===== 打包常量 ===== PKG_ARCH="arm64" QT_PKG_PATH="/opt/firefly_qt5.15_arm64_20.04" QT_LIB_PATH="/opt/sysroot/firefly-arm64-sysroot-20.04/lib/aarch64-linux-gnu" DEFAULT_OPENCV_PROFILE="opencv320" OPENCV320_LIB_GLOB="SDK/OpenCV320/Arm/aarch64/libopencv_*.so*" OPENCV480_LIB_GLOB="SDK/OpenCV480/Arm/aarch64/lib/libopencv_*.so*" FIXED_PACKAGE_MAINTAINER="visionrobot" FIXED_QT_REMOVE_WEBENGINE=true FIXED_QT_REMOVE_NFC=true # ===== postinst/postrm 中复用的代码片段 ===== DETECT_CURRENT_USER_SNIPPET=$(cat <<'_SNIPPET_' if [ -n "${SUDO_USER:-}" ]; then current_user="${SUDO_USER}" current_home=$(getent passwd "${current_user}" | cut -d: -f6) else current_user=$(whoami) current_home="${HOME}" fi _SNIPPET_ ) RESOLVE_DESKTOP_DIR_SNIPPET=$(cat <<'_SNIPPET_' resolve_desktop_dir() { local home_dir=$1 local candidate="" if command -v xdg-user-dir >/dev/null 2>&1; then candidate=$(xdg-user-dir DESKTOP 2>/dev/null || true) if [ -n "${candidate}" ] && [ -d "${candidate}" ]; then printf '%s\n' "${candidate}" return 0 fi fi if [ -f "${home_dir}/.config/user-dirs.dirs" ]; then candidate=$(grep '^XDG_DESKTOP_DIR=' "${home_dir}/.config/user-dirs.dirs" | head -n 1 | cut -d= -f2- | tr -d '"') candidate=${candidate//\$HOME/${home_dir}} if [ -n "${candidate}" ] && [ -d "${candidate}" ]; then printf '%s\n' "${candidate}" return 0 fi fi for dir_name in Desktop $'\346\241\214\351\235\242' desktop; do candidate="${home_dir}/${dir_name}" if [ -d "${candidate}" ]; then printf '%s\n' "${candidate}" return 0 fi done return 1 } _SNIPPET_ ) # ===== 通用函数 ===== log_info() { printf '[项目打包] %s\n' "$*" } show_pkg_help() { local kind=$1 local script_name="project_pkg_${kind}.sh" cat < ./${script_name} --list 可打包项目: EOF list_projects_by_kind "${kind}" | sed 's/^/ - /' } copy_glob_items() { local src_pattern=$1 local dest_rel=$2 local copy_mode=$3 local required=$4 local matches=() shopt -s nullglob matches=( ${REPO_DIR}/${src_pattern} ) shopt -u nullglob if [ ${#matches[@]} -eq 0 ]; then if [ "$required" = true ]; then echo "缺少文件: ${src_pattern}" >&2 exit 1 fi return 0 fi mkdir -p "${PKG_PATH}/${dest_rel}" case "$copy_mode" in globa) cp -a "${matches[@]}" "${PKG_PATH}/${dest_rel}/" ;; glob) cp "${matches[@]}" "${PKG_PATH}/${dest_rel}/" ;; *) echo "不支持的 glob 复制模式: ${copy_mode}" >&2 exit 1 ;; esac } copy_file_item() { local src_rel=$1 local dest_rel=$2 local required=$3 local src_abs="${REPO_DIR}/${src_rel}" if [ ! -f "${src_abs}" ]; then if [ "$required" = true ]; then echo "缺少文件: ${src_rel}" >&2 exit 1 fi return 0 fi mkdir -p "$(dirname "${PKG_PATH}/${dest_rel}")" cp "${src_abs}" "${PKG_PATH}/${dest_rel}" } copy_directory_item() { local src_rel=$1 local dest_rel=$2 local required=$3 local src_abs="${REPO_DIR}/${src_rel}" if [ ! -d "${src_abs}" ]; then if [ "$required" = true ]; then echo "缺少目录: ${src_rel}" >&2 exit 1 fi return 0 fi mkdir -p "${PKG_PATH}/${dest_rel}" cp -a "${src_abs}/." "${PKG_PATH}/${dest_rel}/" } copy_from_spec() { local required=$1 local spec=$2 local kind="" local src="" local dest="" IFS='|' read -r kind src dest <<< "${spec}" case "$kind" in glob|globa) copy_glob_items "${src}" "${dest}" "${kind}" "${required}" ;; file) copy_file_item "${src}" "${dest}" "${required}" ;; dir) copy_directory_item "${src}" "${dest}" "${required}" ;; *) echo "不支持的复制规则: ${spec}" >&2 exit 1 ;; esac } process_specs() { local required=$1 shift local spec="" for spec in "$@"; do copy_from_spec "${required}" "${spec}" done } apply_package_defaults() { PACKAGE_SLUG=$(printf '%s' "${PACKAGE_NAME}" | tr '[:upper:]' '[:lower:]') PACKAGE_SECTION="${PACKAGE_SLUG}app" [ "${PACKAGE_KIND}" = "service" ] && PACKAGE_SECTION="${PACKAGE_SLUG}" PACKAGE_MAINTAINER="${FIXED_PACKAGE_MAINTAINER}" : "${PACKAGE_DESCRIPTION:=${PACKAGE_NAME} application}" : "${PACKAGE_COMMENT:=${PACKAGE_NAME} App}" : "${PACKAGE_OPENCV_PROFILE:=${DEFAULT_OPENCV_PROFILE}}" } prepare_package_layout() { rm -rf "${PKG_PATH}" mkdir -p "${PKG_PATH}/DEBIAN" mkdir -p "${PKG_PATH}/etc/profile.d" mkdir -p "${PKG_PATH}/opt/sysroot/lib" mkdir -p "${PKG_PATH}/usr/lib" if [ "${PACKAGE_KIND}" = "desktop" ]; then mkdir -p "${PKG_PATH}/etc/xdg/autostart" mkdir -p "${PKG_PATH}/usr/local/bin" mkdir -p "${PKG_PATH}/usr/share/applications" mkdir -p "${PKG_PATH}/usr/share/pixmaps" fi } copy_qt_runtime() { cp -rfd "${QT_PKG_PATH}/ext" "${PKG_PATH}/opt/firefly_qt5.15" cp "${QT_PKG_PATH}/target_qtEnv.sh" "${PKG_PATH}/etc/profile.d/" local libfile="" local filename="" for libfile in "${QT_LIB_PATH}"/*.so*; do filename=$(basename "${libfile}") if [[ "${filename}" == *icu* ]]; then cp -rfd "${libfile}" "${PKG_PATH}/opt/sysroot/lib/" fi done } cleanup_qt_runtime() { if [ "${FIXED_QT_REMOVE_WEBENGINE}" = true ]; then rm -f "${PKG_PATH}/opt/firefly_qt5.15/lib/libQt5WebEngine"* rm -rf "${PKG_PATH}/opt/firefly_qt5.15/libexec/QtWebEngineProcess" rm -rf "${PKG_PATH}/opt/firefly_qt5.15/resources/qtwebengine"* rm -rf "${PKG_PATH}/opt/firefly_qt5.15/translations/qtwebengine"* fi if [ "${FIXED_QT_REMOVE_NFC}" = true ]; then rm -rf "${PKG_PATH}/opt/firefly_qt5.15/translations/Nfc"* fi } copy_opencv_runtime() { local src_pattern="" case "${PACKAGE_OPENCV_PROFILE}" in opencv320) src_pattern="${OPENCV320_LIB_GLOB}" ;; opencv480) src_pattern="${OPENCV480_LIB_GLOB}" ;; none) return 0 ;; *) echo "不支持的 OpenCV 配置: ${PACKAGE_OPENCV_PROFILE}" >&2 exit 1 ;; esac log_info "复制 OpenCV 运行库 (${PACKAGE_OPENCV_PROFILE})" copy_glob_items "${src_pattern}" "usr/lib" "globa" true } copy_required_files() { copy_opencv_runtime process_specs true "${PACKAGE_COPY_SPECS[@]}" process_specs false "${PACKAGE_OPTIONAL_COPY_SPECS[@]}" copy_file_item "${PACKAGE_BINARY_SOURCE_REL}" "${PACKAGE_BINARY_DEST_REL}" true if [ -n "${PACKAGE_ICON_SOURCE_REL}" ] && [ -n "${PACKAGE_ICON_DEST_REL}" ]; then copy_file_item "${PACKAGE_ICON_SOURCE_REL}" "${PACKAGE_ICON_DEST_REL}" true fi if [ -n "${PACKAGE_SERVICE_SOURCE_REL}" ] && [ -n "${PACKAGE_SERVICE_DEST_REL}" ]; then copy_file_item "${PACKAGE_SERVICE_SOURCE_REL}" "${PACKAGE_SERVICE_DEST_REL}" true fi } write_control_file() { cat > "${PKG_PATH}/DEBIAN/control" <&2 echo >&2 show_pkg_help "${expected_kind}" >&2 exit 1 fi if [ "${PACKAGE_KIND}" != "${expected_kind}" ]; then echo "错误: ${PACKAGE_TARGET} 不是 ${expected_kind} 类型项目" >&2 exit 1 fi apply_package_defaults echo "读取 ${PACKAGE_TARGET} 版本信息" VERSION_FILE="${REPO_DIR}/${PACKAGE_VERSION_FILE_REL}" read_project_version "${VERSION_FILE}" "${PACKAGE_VERSION_MACRO}" echo "生成 ${expected_kind} 打包名称" PKG_VERSION="${PROJECT_VERSION}" BUILD_NUMBER="${PROJECT_BUILD}" PKG_RELEASE_VERSION="${PKG_VERSION}.${BUILD_NUMBER}" PKG_PATH="${HOME}/${PACKAGE_NAME}Pkg" RELEASE_PATH="${REPO_DIR}/Publish" TIMESTAMP="$(date +%Y%m%d%H%M%S)" DEB_FILENAME="${RELEASE_PATH}/${PACKAGE_NAME}_${PKG_VERSION}_${BUILD_NUMBER}_${PKG_ARCH}_${TIMESTAMP}.deb" DESKTOP_FILE_NAME="${PACKAGE_SLUG}.desktop" EXEC_INSTALL_PATH="/${PACKAGE_BINARY_DEST_REL}" ICON_INSTALL_PATH="/${PACKAGE_ICON_DEST_REL}" SERVICE_INSTALL_DIR="/opt/${PACKAGE_SLUG}" echo "加载项目 ${PACKAGE_TARGET} 上下文完成" if [ -n "${PACKAGE_SERVICE_DEST_REL}" ]; then SERVICE_INSTALL_PATH="/${PACKAGE_SERVICE_DEST_REL}" SERVICE_UNIT_NAME="$(basename "${PACKAGE_SERVICE_DEST_REL}")" else SERVICE_INSTALL_PATH="" SERVICE_UNIT_NAME="" fi mkdir -p "${RELEASE_PATH}" log_info "开始打包 ${PACKAGE_NAME} ${PKG_RELEASE_VERSION}" } # 通用打包步骤 run_common_packaging_steps() { log_info "准备打包目录" prepare_package_layout log_info "复制 Qt 运行库" copy_qt_runtime log_info "清理 Qt 冗余文件" cleanup_qt_runtime log_info "复制应用文件" copy_required_files log_info "写入软件包元数据" write_control_file } # 最终打包 finalize_package() { log_info "设置文件权限" set_permissions log_info "生成 DEB 安装包" fakeroot dpkg -b "${PKG_PATH}" "${DEB_FILENAME}" >/dev/null log_info "安装包已生成: ${DEB_FILENAME}" log_info "安装包大小: $(ls -lh "${DEB_FILENAME}" | awk '{print $5}')" }