1. 脚本目录检测和代码健壮性保证
1.1 程序报错退出脚本
#set -o pipefail # trace ERR through pipes - will be enabled "soon"
#set -o nounset ## set -u : exit the script if you try to use an uninitialised variable - one day will be enabled
set -e
set -o errtrace # trace ERR through - enabled
set -o errexit ## set -e : exit the script if any statement returns a non-true return value - enabled
# Important, go read http://mywiki.wooledge.org/BashFAQ/105 NOW!
1.2. ./compile.sh脚本目录检测
SRC="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
cd "${SRC}" || exit
# check for whitespace in ${SRC} and exit for safety reasons
grep -q "[[:space:]]" <<< "${SRC}" && {
echo "\"${SRC}\" contains whitespace. Not supported. Aborting." >&2
exit 1
}
2. single.sh 文件加载(位于./lib)
2.1 检查bash版本是否高于5.x
# The Armbian functions require Bash 5.x.
if [[ "${BASH_VERSINFO:-0}" -lt 5 ]]; then
echo "Armbian build scripts require Bash 5.x. Go get it..." >&2
if [[ "${OSTYPE}" == "darwin"* ]]; then
echo "Armbian build scripts require brew to be installed and working on macOS. (old Bash version)" >&2
echo "Please install brew, *restart your terminal*." >&2
echo "Then run 'brew install bash coreutils git', *restart your terminal* and then try again." >&2
exit 51
fi
exit 50
fi
2.2 macOS系统检查
- 是否以root运行脚本
- Homebrew 包管理器已安装并正常工作
- 通过 Homebrew 的
coreutils安装的realpath命令是否可用 - 更新
PATH环境变量以包含由 Homebrew 安装的 GNUcoreutils二进制文件。 - 确保
${SRC}变量(应指向构建脚本的源目录)位于用户的主目录中。这对于 macOS 上的 Docker 兼容性很重要。
# If under Darwin, we require brew to be installed and working. Check.
if [[ "${OSTYPE}" == "darwin"* ]]; then
# Don't allow running as root on macOS.
if [[ "${EUID}" -eq 0 ]]; then
echo "Armbian build scripts do not support running as root on macOS." >&2
echo "Please run as a normal user." >&2
exit 51
fi
if ! command -v brew &> /dev/null; then
echo "Armbian build scripts require brew to be installed and working on macOS. (brew not available)" >&2
echo "Please install brew, *restart your terminal*." >&2
echo "Then run 'brew install bash coreutils git', *restart your terminal* and then try again." >&2
exit 51
fi
# Run "brew --prefix" to check if brew is working.
if ! brew --prefix &> /dev/null; then
echo "Armbian build scripts require brew to be installed and working on macOS. (brew --prefix failed)" >&2
echo "Please install brew, *restart your terminal*." >&2
echo "Then run 'brew install bash coreutils git', *restart your terminal* and then try again." >&2
exit 51
fi
declare brew_prefix
brew_prefix="$(brew --prefix)"
# Make sure realpath is available via brew's coreutils, under ${brew_prefix}
if ! command -v "${brew_prefix}/opt/coreutils/libexec/gnubin/realpath" &> /dev/null; then
echo "Armbian build scripts require realpath to be installed via brew's coreutils on macOS. (realpath not available)" >&2
echo "Please install brew, *restart your terminal*." >&2
echo "Then run 'brew install bash coreutils git', *restart your terminal* and then try again." >&2
echo "If that fails, try 'brew reinstall bash coreutils git' and try again." >&2
exit 51
fi
# If under Darwin, we need to set the PATH to include the GNU coreutils.
# export PATH with new coreutils gnubin's in front.
export PATH="${brew_prefix}/opt/coreutils/libexec/gnubin:$PATH"
unset brew_prefix
# Under Darwin/Docker, the "${SRC}" should be under "${HOME}" -- otherwise Docker will not be able to share/mount it.
# This is a sanity check to make sure that the user is not trying to build outside of "${HOME}".
if [[ "${SRC}" != "${HOME}"* ]]; then
echo "Armbian build scripts require the Armbian directory ($SRC) to be under your home directory ($HOME) on macOS." >&2
echo "Please clone inside your home directory and try again." >&2
exit 52
fi
fi
2.3 检查realpath命令是否存在
if [[ -z "$(command -v realpath)" ]]; then
echo "Armbian build scripts require coreutils. Go install it." >&2
exit 53
fi
2.4 防止用户直接运行 single.sh,使用 compile.sh 来启动构建过程。
if [[ $(basename "$0") == single.sh ]]; then
echo "Please use compile.sh to start the build process"
exit 255
fi
2.5 通过./lib/library-functions.sh文件加载lib里面的函数脚本
build/lib/library-functions.sh at main · armbian/build · GitHub
3. logging_init函数(位于./lib/functions/logging/logging.sh)
这个函数设置了日志显示的默认行为,包括是否在终端上显示日志、是否启用调试模式等。它还根据终端的背景色调整日志颜色,以提高可读性,并根据运行环境(如 CI/CD 系统、Docker 容器、WSL2 或 macOS)调整日志前缀。
# initialize logging variables. (this does not consider parameters at this point, only environment variables)
logging_init
4. traps_init函数(位于./lib/functions/logging/traps.sh)
初始化一系列的信号捕捉(traps),以便在脚本执行过程中捕捉到特定的信号(如错误、退出、中断和终止信号)时能够执行相应的处理函数 main_trap_handler
function traps_init() {
# shellcheck disable=SC2034 # Array of cleanup handlers.
declare -g -a trap_manager_cleanup_handlers=()
# shellcheck disable=SC2034 # Global to avoid doubly reporting ERR/EXIT pairs.
declare -g -i trap_manager_error_handled=0
trap 'main_trap_handler "ERR" "$?"' ERR
trap 'main_trap_handler "EXIT" "$?"' EXIT
trap 'main_trap_handler "INT" "$?"' INT
trap 'main_trap_handler "TERM" "$?"' TERM
}
5. 确保目录安全
# make sure git considers our build system dir as a safe dir (only if actually building)
[[ "${CONFIG_DEFS_ONLY}" != "yes" && "${PRE_PREPARED_HOST}" != "yes" ]] && git_ensure_safe_directory "${SRC}"
6. cli_entrypoint “$@”函数(位于./lib/functions/cli/entrypoint.sh)
6.1 调用跟踪功能
if [[ "${ARMBIAN_ENABLE_CALL_TRACING}" == "yes" ]]; then
set -T # inherit return/debug traps
mkdir -p "${SRC}"/output/call-traces
echo -n "" > "${SRC}"/output/call-traces/calls.txt
# See https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html
trap 'echo "${FUNCNAME[*]}|${BASH_LINENO[*]}|${BASH_SOURCE[*]}|${LINENO}" >> ${SRC}/output/call-traces/calls.txt ;' RETURN
fi
6.2 映射命令到它们的处理函数以及相关的变量设置
armbian_register_commands位于lib/functions/cli/commands.sh,通过关联数组实现字典的功能
# Decide what we're gonna do. We've a few hardcoded, 1st-argument "commands".
declare -g -A ARMBIAN_COMMANDS_TO_HANDLERS_DICT ARMBIAN_COMMANDS_TO_VARS_DICT
armbian_register_commands # this defines the above two dictionaries
ARMBIAN_COMMANDS_TO_HANDLERS_DICT: 这个关联数组映射了一系列命令字符串到它们对应的处理函数名称。例如,多个与 docker 相关的命令都映射到了同一个处理函数 docker
ARMBIAN_COMMANDS_TO_VARS_DICT: 这个关联数组为特定的命令提供了需要设置的变量。
6.3 处理命令行参数
parse_cmdline_params位于lib/functions/cli/utils-cli.sh,用于将参数分为两类。
# Process the command line, separating params (XX=YY) from non-params arguments.
# That way they can be set in any order.
declare -A -g ARMBIAN_PARSED_CMDLINE_PARAMS=() # A dict of PARAM=VALUE
declare -a -g ARMBIAN_NON_PARAM_ARGS=() # An array of all non-param arguments
parse_cmdline_params "${@}" # which fills the above vars.
ARMBIAN_PARSED_CMDLINE_PARAMS,用于存储形式为 PARAM=VALUE 的命令行参数;
ARMBIAN_NON_PARAM_ARGS,用于存储非参数类的命令行参数。
6.4 环境变量应用与日志初始化
apply_cmdline_params_to_env位于lib/functions/cli/utils-cli.sh,用于将键值类型的参数应用到脚本环境。
# Now load the key=value pairs from cmdline into environment, before loading config or executing commands.
# This will be done _again_ later, to make sure cmdline params override config et al.
apply_cmdline_params_to_env "early" # which uses ARMBIAN_PARSED_CMDLINE_PARAMS
# From here on, no more ${1} or stuff. We've parsed it all into ARMBIAN_PARSED_CMDLINE_PARAMS or ARMBIAN_NON_PARAM_ARGS and ARMBIAN_COMMAND.
# Re-initialize logging, to take into account the new environment after parsing cmdline params.
logging_init
6.5 处理配置或配置文件
declare -a -g ARMBIAN_CONFIG_FILES=() # fully validated, complete paths to config files.
declare -g ARMBIAN_COMMAND_HANDLER="" ARMBIAN_COMMAND="" ARMBIAN_COMMAND_VARS="" # only valid command and handler will ever be set here.
declare -g ARMBIAN_HAS_UNKNOWN_ARG="no" # if any unknown params, bomb.
for argument in "${ARMBIAN_NON_PARAM_ARGS[@]}"; do # loop over all non-param arguments, find commands and configs.
parse_each_cmdline_arg_as_command_param_or_config "${argument}" # sets all the vars above
done
ARMBIAN_CONFIG_FILES,全局的索引数组,用于存储完全验证后的配置文件路径;
parse_each_cmdline_arg_as_command_param_or_config位于lib/functions/cli/utils-cli.sh,检测输入是命令还是配置文件,如果参数被识别为配置文件,将其路径添加到 ARMBIAN_CONFIG_FILES数组中,并将文件名添加到 ARMBIAN_CLI_RELAUNCH_CONFIGS 数组中,将参数设置为ARMBIAN_COMMAND
6.6 命令预处理
在执行最终命令之前,检查是否有需要预先执行的命令,并进行相应的处理。armbian_prepare_cli_command_to_run位于./build/lib/cli/utils-cli.sh
# If we don't have a command at this stage, we should default either to 'build' or 'docker', depending on OS.
# Give the chosen command a chance to refuse running, or, even, change the final command to run.
# This allows for example the 'build' command to auto-launch under docker, even without specifying it.
# Also allows for launchers to keep themselves when re-launched, yet do something diferent. (eg: docker under docker does build).
# Or: build under Darwin does docker...
# each _pre_run can change the command and vars to run too, so do it in a loop until it stops changing.
declare -g ARMBIAN_CHANGE_COMMAND_TO="${ARMBIAN_COMMAND}"
while [[ "${ARMBIAN_CHANGE_COMMAND_TO}" != "" ]]; do
display_alert "Still a command to pre-run, this time:" "${ARMBIAN_CHANGE_COMMAND_TO}" "debug"
declare -g ARMBIAN_COMMAND_REQUIRE_BASIC_DEPS="no" # reset this before every pre_run, so only the last one wins.
ARMBIAN_COMMAND="${ARMBIAN_CHANGE_COMMAND_TO}"
armbian_prepare_cli_command_to_run "${ARMBIAN_COMMAND}"
ARMBIAN_CHANGE_COMMAND_TO=""
armbian_cli_pre_run_command
done
6.7 目录初始化
./output目录和./userpatches
# Init basic dirs.
declare -g -r DEST="${SRC}/output" USERPATCHES_PATH="${SRC}"/userpatches # DEST is the main output dir, and USERPATCHES_PATH is the userpatches dir. read-only.
mkdir -p "${DEST}" "${USERPATCHES_PATH}" # Create output and userpatches directory if not already there
display_alert "Output directory created! DEST:" "${DEST}" "debug"
6.8 生成UUID
# set unique mounting directory for this execution.
# basic deps, which include "uuidgen", will be installed _after_ this, so we gotta tolerate it not being there yet.
declare -g ARMBIAN_BUILD_UUID
if [[ "${ARMBIAN_BUILD_UUID}" != "" ]]; then
display_alert "Using passed-in ARMBIAN_BUILD_UUID" "${ARMBIAN_BUILD_UUID}" "debug"
else
if command -v uuidgen 1> /dev/null; then
ARMBIAN_BUILD_UUID="$(uuidgen)"
else
display_alert "uuidgen not found" "uuidgen not installed yet" "info"
ARMBIAN_BUILD_UUID="no-uuidgen-yet-${RANDOM}-$((1 + $RANDOM % 10))$((1 + $RANDOM % 10))$((1 + $RANDOM % 10))$((1 + $RANDOM % 10))"
fi
display_alert "Generated ARMBIAN_BUILD_UUID" "${ARMBIAN_BUILD_UUID}" "debug"
fi
declare -g -r ARMBIAN_BUILD_UUID="${ARMBIAN_BUILD_UUID}" # Make read-only
display_alert "Build UUID:" "${ARMBIAN_BUILD_UUID}" "debug"
6.9 日志和清理处理
# Super-global variables, used everywhere. The directories are NOT _created_ here, since this very early stage. They are all readonly, for sanity.
declare -g -r WORKDIR_BASE_TMP="${SRC}/.tmp" # a.k.a. ".tmp" dir. it is a shared base dir for all builds, but each build gets its own WORKDIR/TMPDIR.
declare -g -r WORKDIR="${WORKDIR_BASE_TMP}/work-${ARMBIAN_BUILD_UUID}" # WORKDIR at this stage. It will become TMPDIR later. It has special significance to `mktemp` and others!
declare -g -r LOGDIR="${WORKDIR_BASE_TMP}/logs-${ARMBIAN_BUILD_UUID}" # Will be initialized very soon, literally, below.
declare -g -r EXTENSION_MANAGER_TMP_DIR="${WORKDIR_BASE_TMP}/extensions-${ARMBIAN_BUILD_UUID}" # EXTENSION_MANAGER_TMP_DIR used to store extension-composed functions
# @TODO: These are used only by rootfs/image actual build, move there...
declare -g -r SDCARD="${WORKDIR_BASE_TMP}/rootfs-${ARMBIAN_BUILD_UUID}" # SDCARD (which is NOT an sdcard, but will be, maybe, one day) is where we work the rootfs before final imaging. "rootfs" stage.
declare -g -r MOUNT="${WORKDIR_BASE_TMP}/mount-${ARMBIAN_BUILD_UUID}" # MOUNT ("mounted on the loop") is the mounted root on final image (via loop). "image" stage
declare -g -r DESTIMG="${WORKDIR_BASE_TMP}/image-${ARMBIAN_BUILD_UUID}" # DESTIMG is where the backing image (raw, huge, sparse file) is kept (not the final destination)
# Make sure ARMBIAN_LOG_CLI_ID is set, and unique, and readonly.
# Pre-runs might change it before this, but if not set, default to ARMBIAN_COMMAND.
declare -r -g ARMBIAN_LOG_CLI_ID="${ARMBIAN_LOG_CLI_ID:-${ARMBIAN_COMMAND}}"
# If we're on Linux & root, mount tmpfs on LOGDIR. This has it's own cleanup handler.
# It also _creates_ the LOGDIR, and the cleanup handler will delete.
prepare_tmpfs_for "LOGDIR" "${LOGDIR}"
LOG_SECTION="entrypoint" start_logging_section # This will create LOGDIR if it does not exist. @TODO: also maybe causes a spurious group to be created in the log file
add_cleanup_handler trap_handler_cleanup_logging # cleanup handler for logs; it rolls it up from LOGDIR into DEST/logs
add_cleanup_handler trap_handler_reset_output_owner # make sure output folder is owned by pre-sudo/pre-Docker user if that's the case
6.10 基础依赖检查
# @TODO: So gigantic contention point here about logging the basic deps installation.
if [[ "${ARMBIAN_COMMAND_REQUIRE_BASIC_DEPS}" == "yes" ]]; then
if [[ "${OFFLINE_WORK}" == "yes" ]]; then
display_alert "* " "You are working offline!"
display_alert "* " "Sources, time and host will not be checked"
else
# check and install the basic utilities;
LOG_SECTION="prepare_host_basic" do_with_logging prepare_host_basic # This includes the 'docker' case.
fi
fi
6.11 配置文件加载
# Loop over the ARMBIAN_CONFIG_FILES array and source each. The order is important.
for config_file in "${ARMBIAN_CONFIG_FILES[@]}"; do
local config_filename="${config_file##*/}" config_dir="${config_file%/*}"
display_alert "Sourcing config file" "${config_filename}" "debug"
# use pushd/popd to change directory to the config file's directory, so that relative paths in the config file work.
pushd "${config_dir}" > /dev/null || exit_with_error "Failed to pushd to ${config_dir}"
# shellcheck source=/dev/null
LOG_SECTION="userpatches_config:${config_filename}" do_with_logging source "${config_file}"
# reset completely after sourcing config file
set -e
#set -o pipefail # trace ERR through pipes - will be enabled "soon"
#set -o nounset ## set -u : exit the script if you try to use an uninitialised variable - one day will be enabled
set -o errtrace # trace ERR through - enabled
set -o errexit ## set -e : exit the script if any statement returns a non-true return value - enabled
popd > /dev/null || exit_with_error "Failed to popd from ${config_dir}"
# Apply the params received from the command line _again_ after running the config.
# This ensures that params take precedence over stuff possibly defined in the config.
apply_cmdline_params_to_env "after config '${config_filename}'" # which uses ARMBIAN_PARSED_CMDLINE_PARAMS
done
6.12 执行最终命令
# Early check for deprecations
error_if_lib_tag_set # make sure users are not thrown off by using old parameter which does nothing anymore; explain
display_alert "Executing final CLI command" "${ARMBIAN_COMMAND}" "debug"
armbian_cli_run_command
display_alert "Done Executing final CLI command" "${ARMBIAN_COMMAND}" "debug"
# Build done, run the cleanup handlers explicitly.
# This zeroes out the list of cleanups, so it"s not done again when the main script exits normally and trap = 0 runs.
run_cleanup_handlers