GrabBag/GrabBagPrj/project_pkg_common.sh
2026-06-26 17:49:32 +08:00

504 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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*"
OPENCV450_LIB_DIR_CANDIDATES=(
"AppAlgo/stereo_bolt_delivery/lib/opencv"
"AppAlgo/stereo_bolt_delivery/lib"
"SDK/OpenCV450/Arm/aarch64/lib"
"SDK/OpenCV45D/Arm/aarch64/lib"
"SDK/OpenCV454/Arm/aarch64/lib"
"SDK/OpenCV45/Arm/aarch64/lib"
)
OPENCV450_REQUIRED_LIBS=(
"libopencv_calib3d.so.4.5"
"libopencv_imgcodecs.so.4.5"
"libopencv_dnn.so.4.5"
"libopencv_imgproc.so.4.5"
"libopencv_core.so.4.5"
)
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 <<EOF
用法:
./${script_name} <ProjectName>
./${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
}
resolve_package_source_path() {
local src=$1
if [[ "${src}" = /* ]]; then
printf '%s\n' "${src}"
else
printf '%s/%s\n' "${REPO_DIR}" "${src}"
fi
}
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}"
;;
opencv450|opencv45d)
copy_opencv450_runtime
return 0
;;
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_opencv450_runtime() {
local candidates=()
local candidate=""
local candidate_abs=""
local selected_dir=""
local lib=""
local missing=false
if [ -n "${DRONESCREW_OPENCV450_LIB_DIR:-}" ]; then
candidates+=("${DRONESCREW_OPENCV450_LIB_DIR}")
fi
candidates+=("${OPENCV450_LIB_DIR_CANDIDATES[@]}")
for candidate in "${candidates[@]}"; do
candidate_abs="$(resolve_package_source_path "${candidate}")"
[ -d "${candidate_abs}" ] || continue
missing=false
for lib in "${OPENCV450_REQUIRED_LIBS[@]}"; do
if [ ! -e "${candidate_abs}/${lib}" ]; then
missing=true
break
fi
done
if [ "${missing}" = false ]; then
selected_dir="${candidate_abs}"
break
fi
done
if [ -z "${selected_dir}" ]; then
{
echo "Missing OpenCV 4.5.0 runtime for ${PACKAGE_NAME}."
echo "libstereo_bolt.so requires these exact sonames:"
printf ' %s\n' "${OPENCV450_REQUIRED_LIBS[@]}"
echo "Put them in AppAlgo/stereo_bolt_delivery/lib/opencv,"
echo "or set DRONESCREW_OPENCV450_LIB_DIR to the directory that contains them."
} >&2
exit 1
fi
log_info "Copy OpenCV runtime (opencv450) from ${selected_dir}"
mkdir -p "${PKG_PATH}/usr/lib"
shopt -s nullglob
local opencv_libs=( "${selected_dir}"/libopencv_*.so.4.5* )
shopt -u nullglob
for lib in "${opencv_libs[@]}"; do
cp -a "${lib}" "${PKG_PATH}/usr/lib/"
done
}
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" <<EOF
Package: ${PACKAGE_NAME}
Version: ${PKG_RELEASE_VERSION}
Section: ${PACKAGE_SECTION}
Architecture: ${PKG_ARCH}
Priority: optional
Maintainer: ${PACKAGE_MAINTAINER}
Description: ${PACKAGE_DESCRIPTION}
EOF
if [ -n "${PACKAGE_DEPENDS:-}" ]; then
printf 'Depends: %s\n' "${PACKAGE_DEPENDS}" >> "${PKG_PATH}/DEBIAN/control"
fi
}
set_permissions() {
local target=""
for target in "${PKG_PATH}/usr" "${PKG_PATH}/etc" "${PKG_PATH}/opt"; do
if [ -d "${target}" ]; then
chmod -R 755 "${target}"
fi
done
}
# ===== 流程编排函数 =====
# 加载打包上下文:参数解析 + meta 加载 + 派生变量计算
# 用法: load_package_context "$@" "desktop" 或 load_package_context "$@" "service"
load_package_context() {
local expected_kind="${!#}" # 最后一个参数是期望的 kind
local args=("${@:1:$#-1}") # 去掉最后一个参数
echo "开始加载 ${expected_kind} 上下文"
if [ ${#args[@]} -eq 1 ] && [ "${args[0]}" = "--list" ]; then
list_projects_by_kind "${expected_kind}"
exit 0
fi
if [ ${#args[@]} -eq 1 ] && { [ "${args[0]}" = "-h" ] || [ "${args[0]}" = "--help" ]; }; then
show_pkg_help "${expected_kind}"
exit 0
fi
if [ ${#args[@]} -ne 1 ]; then
show_pkg_help "${expected_kind}"
exit 1
fi
PACKAGE_TARGET="${args[0]}"
echo "加载项目 ${PACKAGE_TARGET} 上下文"
if ! load_package_project_meta "${PACKAGE_TARGET}"; then
echo "未知项目: ${PACKAGE_TARGET}" >&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}"
if [ -n "${PACKAGE_ICON_DEST_REL}" ]; then
ICON_INSTALL_PATH="/${PACKAGE_ICON_DEST_REL}"
else
ICON_INSTALL_PATH="application-x-executable"
fi
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 安装包"
# 使用低压缩等级避免 OOM大包 + xz 默认等级 6 会吃几百 MB 内存)
# 可通过 DEB_COMPRESS_OPTS 环境变量覆盖,例如 DEB_COMPRESS_OPTS="-Zgzip" 或 "-Zxz -z9"
local compress_opts="${DEB_COMPRESS_OPTS:--Zxz -z1}"
fakeroot dpkg-deb ${compress_opts} --build "${PKG_PATH}" "${DEB_FILENAME}" >/dev/null
log_info "安装包已生成: ${DEB_FILENAME}"
log_info "安装包大小: $(ls -lh "${DEB_FILENAME}" | awk '{print $5}')"
}