用纯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,无外部依赖(只需标准
tar、gzip、sed等)。 - 内置日志、错误处理、回滚机制,比简单自解压脚本更健壮。
- 体积小,生成的安装包也轻量。
使用示例
假设你有一个项目目录结构:
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 中添加
md5sum或sha256sum检查。 - 支持更多可选脚本(如 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.sh 并 chmod +x 即可使用。如果你有改进想法,欢迎讨论!