195 lines
4.7 KiB
Bash
Executable File
195 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Usage:
|
|
# export HOSTNAME=mybox
|
|
# ./merge-rootfs.sh rootfs-extra /out/rootfs
|
|
#
|
|
# Naming rules:
|
|
# foo -> normal file
|
|
# foo.tmpl -> render with envsubst, then normal handling
|
|
# foo.override -> replace target directly
|
|
# foo.tmpl.override -> render with envsubst, then replace target directly
|
|
#
|
|
# Default handling:
|
|
# etc/* -> merge missing lines
|
|
# opt/scripts/* -> replace
|
|
# everything else -> copy only if missing
|
|
|
|
SRC_ROOT="${1:?source rootfs path required}"
|
|
DST_ROOT="${2:?target rootfs path required}"
|
|
|
|
if [[ ! -d "$SRC_ROOT" ]]; then
|
|
echo "Source rootfs does not exist or is not a directory: $SRC_ROOT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$DST_ROOT" ]]; then
|
|
echo "Target rootfs does not exist or is not a directory: $DST_ROOT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v envsubst >/dev/null 2>&1; then
|
|
echo "envsubst not found. Install gettext or gettext-envsubst." >&2
|
|
exit 1
|
|
fi
|
|
|
|
TMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
|
|
append_missing_lines() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
local changed=1
|
|
local line
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if ! grep -Fqx -- "$line" "$dst"; then
|
|
printf '%s\n' "$line" >> "$dst"
|
|
changed=0
|
|
fi
|
|
done < "$src"
|
|
|
|
return "$changed"
|
|
}
|
|
|
|
check_template_vars() {
|
|
local src="$1"
|
|
local missing=0
|
|
local vars var
|
|
|
|
vars="$(
|
|
grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*' "$src" \
|
|
| sed -E 's/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/\1/' \
|
|
| sort -u || true
|
|
)"
|
|
|
|
while IFS= read -r var; do
|
|
[[ -z "$var" ]] && continue
|
|
if [[ -z "${!var+x}" ]]; then
|
|
echo "Missing required env var: $var (used in $src)" >&2
|
|
missing=1
|
|
fi
|
|
done <<< "$vars"
|
|
|
|
[[ "$missing" -eq 0 ]]
|
|
}
|
|
|
|
render_template() {
|
|
local src="$1"
|
|
local out="$2"
|
|
|
|
mkdir -p "$(dirname "$out")"
|
|
check_template_vars "$src"
|
|
envsubst < "$src" > "$out"
|
|
}
|
|
|
|
replace_file() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
|
|
mkdir -p "$(dirname "$dst")"
|
|
cp -a "$src" "$dst"
|
|
echo "Replaced file: $dst"
|
|
}
|
|
|
|
merge_or_copy_file() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
local rel="$3"
|
|
|
|
mkdir -p "$(dirname "$dst")"
|
|
|
|
# scripts: replace
|
|
if [[ "$rel" == opt/scripts/* ]]; then
|
|
cp -a "$src" "$dst"
|
|
echo "Replaced script: $dst"
|
|
return
|
|
fi
|
|
|
|
# /etc: merge missing lines
|
|
if [[ "$rel" == etc/* ]]; then
|
|
if [[ ! -e "$dst" ]]; then
|
|
cp -a "$src" "$dst"
|
|
echo "Copied new config: $dst"
|
|
return
|
|
fi
|
|
|
|
if [[ ! -f "$dst" ]]; then
|
|
echo "Skipping existing non-regular path: $dst" >&2
|
|
return
|
|
fi
|
|
|
|
if append_missing_lines "$src" "$dst"; then
|
|
echo "Appended missing lines: $dst"
|
|
else
|
|
echo "No changes needed: $dst"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
# default: copy only if missing
|
|
if [[ ! -e "$dst" ]]; then
|
|
cp -a "$src" "$dst"
|
|
echo "Copied new file: $dst"
|
|
else
|
|
echo "Skipped existing file: $dst"
|
|
fi
|
|
}
|
|
|
|
find "$SRC_ROOT" -mindepth 1 | while IFS= read -r src_path; do
|
|
rel_path="${src_path#"$SRC_ROOT"/}"
|
|
|
|
if [[ -d "$src_path" ]]; then
|
|
mkdir -p "$DST_ROOT/$rel_path"
|
|
continue
|
|
fi
|
|
|
|
if [[ -L "$src_path" ]]; then
|
|
dst_path="$DST_ROOT/$rel_path"
|
|
if [[ -e "$dst_path" || -L "$dst_path" ]]; then
|
|
echo "Symlink exists, skipping: $dst_path"
|
|
else
|
|
mkdir -p "$(dirname "$dst_path")"
|
|
ln -s "$(readlink "$src_path")" "$dst_path"
|
|
echo "Created symlink: $dst_path"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [[ ! -f "$src_path" ]]; then
|
|
echo "Skipping unsupported file type: $src_path" >&2
|
|
continue
|
|
fi
|
|
|
|
src_for_merge="$src_path"
|
|
rel_for_target="$rel_path"
|
|
mode="default"
|
|
|
|
if [[ "$rel_for_target" == *.tmpl.override ]]; then
|
|
mode="override"
|
|
rel_for_target="${rel_for_target%.tmpl.override}"
|
|
rendered="$TMP_DIR/$rel_for_target"
|
|
render_template "$src_path" "$rendered"
|
|
src_for_merge="$rendered"
|
|
|
|
elif [[ "$rel_for_target" == *.override ]]; then
|
|
mode="override"
|
|
rel_for_target="${rel_for_target%.override}"
|
|
|
|
elif [[ "$rel_for_target" == *.tmpl ]]; then
|
|
rel_for_target="${rel_for_target%.tmpl}"
|
|
rendered="$TMP_DIR/$rel_for_target"
|
|
render_template "$src_path" "$rendered"
|
|
src_for_merge="$rendered"
|
|
fi
|
|
|
|
dst_path="$DST_ROOT/$rel_for_target"
|
|
|
|
if [[ "$mode" == "override" ]]; then
|
|
replace_file "$src_for_merge" "$dst_path"
|
|
else
|
|
merge_or_copy_file "$src_for_merge" "$dst_path" "$rel_for_target"
|
|
fi
|
|
done
|