504 lines
14 KiB
Bash
504 lines
14 KiB
Bash
#!/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}')"
|
||
}
|