用纯Shell 实现一个通用自解压安装包打包器

在 Linux/Unix 系统中,分发软件时常常需要一个简单的安装程序。一种经典的方式是创建自解压安装包(self-extracting installer):一个可执行的 shell 脚本,内部附加了压缩的 tar 归档文件。运行这个脚本时,它会自动解压负载(payload),然后执行指定的安装脚本。

著名的工具 makeself 就是干这个的,它功能丰富,但代码较长(几百行),支持多种选项(如压缩方式、校验和等)。如果你想要一个更轻量、纯 POSIX 兼容的实现,本文介绍的 shell_pack.sh 就是一个优秀的选择。

这个脚本由 lwq 重构(refactored),完全使用 POSIX sh 编写,无需 bash 扩展,兼容性极强。它能打包任意文件和一个主安装脚本,生成一个自解压的可执行文件。

脚本功能概述

输入

  • -p <package_name>:输出自解压包的文件名(会自动添加执行权限)。
  • -s <install_script>:必须的主安装脚本(解压后会执行这个脚本)。
  • 其余参数:要打包的其他文件/目录。

输出:一个独立的 shell 脚本文件,运行它就会:

  • /tmp 创建临时工作目录。
  • 解压负载(使用 sed 跳过脚本头部,直接 pipe 到 tar zxf)。
  • 可选执行 pre_install.sh(如果存在)。
  • 执行主 install.sh
  • 提供日志记录(带时间戳,输出到 /tmp/<package>.log)。
  • 支持回滚:如果安装失败且存在 rollback.sh,会尝试执行它。
  • 捕获中断信号,确保清理临时目录。
  • 安装成功后自动清理。

优势

  • 纯 POSIX sh,无外部依赖(只需标准 targzipsed 等)。
  • 内置日志、错误处理、回滚机制,比简单自解压脚本更健壮。
  • 体积小,生成的安装包也轻量。

使用示例

假设你有一个项目目录结构:

myapp/
├── install.sh          # 主安装脚本,必选
├── pre_install.sh      # 可选,前置脚本(如检查依赖)
├── rollback.sh         # 可选,回滚脚本(如卸载临时文件)
├── bin/
│   └── mybinary
└── conf/
    └── config.yaml

打包命令:

./shell_pack.sh -p myapp_installer -s install.sh bin/* conf/* pre_install.sh rollback.sh

这会生成 myapp_installer 文件(可执行)。

用户分发后,只需:

chmod +x myapp_installer   # 如果需要
./myapp_installer

它会自动解压到临时目录,执行安装流程,并记录日志到 /tmp/myapp_installer.log

脚本详细解析

1. 参数解析与校验

使用 POSIX 标准的 getopts 解析选项,支持 -h-p-s

校验:

  • 必须提供包名和安装脚本。
  • 安装脚本必须存在。
  • 必须有至少一个文件要打包。

2. 生成 wrapper(自解压头部)

核心是 generate_wrapper 函数,使用 here-document 输出一个完整的自解压脚本,关键技巧是 sed '1,/^#__ARCHIVE_END__/d' "$0" 从自身脚本中跳过头部,直接提取后面的 gzip tar 数据。

3. 构建过程

  • tar zcf 打包安装脚本 + 其他文件 → 临时 tgz。
  • 生成 wrapper → 临时 sh。
  • cat wrapper tgz > package 合并。
  • chmod +x package
  • 清理临时文件。

与 makeself 的对比

特性 本脚本 (shell_pack.sh) makeself
语言 纯 POSIX sh sh(部分 GNU 扩展)
代码行数 ~100 行 ~600 行
内置日志 是(带时间戳) 无(需自定义)
回滚支持 是(rollback.sh)
pre_install 支持 无(需在主脚本中实现)
校验和 支持 CRC/MD5/SHA
多压缩选项 仅 gzip gzip/bzip2/xz 等
临时目录管理 自动清理,trap 保护 支持多种模式
适用场景 轻量、可靠的安装包 功能全面的大型分发

如果你不需要复杂校验和或多种压缩,这个脚本更简洁、更易维护。

扩展建议

  • 如果需要校验和,可以在 wrapper 中添加 md5sumsha256sum 检查。
  • 支持更多可选脚本(如 post_install.sh)。
  • 添加版本信息或进度显示。

这个脚本展示了 shell 的强大:用不到 100 行代码,就能实现一个生产级别的自解压安装工具。推荐收藏,在需要快速分发工具或配置时直接使用!

完整源码:shell_pack.sh

#!/bin/sh
###############################################################################
# File : shell_pack.sh
# Author : lwq (refactored)
# Desc : Universal self-extracting installer packer (POSIX sh)
###############################################################################
set -e
###############################################################################
# usage
###############################################################################
usage() {
cat <<EOF
Usage:
  $0 -p <package_name> -s <install_script> [files...]
Options:
  -h Show this help
  -p <package> Output package name
  -s <script> Install script (required)
  files Other files to include
Example:
  $0 -p demo_installer -s install.sh bin/* conf/*
EOF
}
###############################################################################
# argument parsing (POSIX getopts)
###############################################################################
PACKAGE=""
INSTALL_SCRIPT=""
while getopts "hp:s:" opt; do
    case "$opt" in
        h) usage; exit 0 ;;
        p) PACKAGE="$OPTARG" ;;
        s) INSTALL_SCRIPT="$OPTARG" ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))
FILES="$@"
###############################################################################
# validation
###############################################################################
[ -z "$PACKAGE" ] && { echo "ERROR: package name required"; usage; exit 1; }
[ -z "$INSTALL_SCRIPT" ] && { echo "ERROR: install script required"; usage; exit 1; }
[ ! -f "$INSTALL_SCRIPT" ] && { echo "ERROR: $INSTALL_SCRIPT not found"; exit 1; }
[ $# -eq 0 ] && { echo "ERROR: no files to pack"; exit 1; }
###############################################################################
# temp files
###############################################################################
TMP_TAR="payload_$$.tgz"
TMP_WRAP="wrapper_$$.sh"
###############################################################################
# generate wrapper script
###############################################################################
generate_wrapper() {
cat <<'EOF'
#!/bin/sh
set -e
PKG_NAME="$(basename "$0")"
WORKDIR="/tmp/${PKG_NAME}_unpack.$$"
LOGFILE="/tmp/${PKG_NAME}.log"
log() {
    printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" | tee -a "$LOGFILE"
}
cleanup() {
    [ -d "$WORKDIR" ] && rm -rf "$WORKDIR"
}
rollback() {
    if [ -x "./rollback.sh" ]; then
        log WARN "running rollback..."
        ./rollback.sh >>"$LOGFILE" 2>&1 || true
    fi
}
trap 'log ERROR "unexpected error"; rollback; cleanup; exit 1' INT TERM
log INFO "start installer"
mkdir -p "$WORKDIR"
log INFO "extracting package"
sed '1,/^#__ARCHIVE_END__/d' "$0" | tar zxf - -C "$WORKDIR"
cd "$WORKDIR"
chmod +x install.sh
if [ -x "./pre_install.sh" ]; then
    log INFO "running pre_install"
    ./pre_install.sh >>"$LOGFILE" 2>&1
fi
log INFO "running install"
if ./install.sh >>"$LOGFILE" 2>&1; then
    log INFO "install success"
else
    log ERROR "install failed"
    rollback
    cleanup
    exit 1
fi
cleanup
log INFO "done"
exit 0
#__ARCHIVE_END__
EOF
}
###############################################################################
# build
###############################################################################
echo "Packing files..."
tar zcf "$TMP_TAR" "$INSTALL_SCRIPT" $FILES
echo "Generating wrapper..."
generate_wrapper > "$TMP_WRAP"
cat "$TMP_WRAP" "$TMP_TAR" > "$PACKAGE"
chmod +x "$PACKAGE"
rm -f "$TMP_TAR" "$TMP_WRAP"
echo "Output package: $PACKAGE"

直接保存为 shell_pack.shchmod +x 即可使用。如果你有改进想法,欢迎讨论!

THE END