#!/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 ]] } preserve_exec_bit() { local old="$1" local new="$2" if [[ -e "$old" && -x "$old" ]]; then chmod +x "$new" fi } get_envsubst_vars() { local rel="$1" case "$rel" in bin/flash-emmc.sh.tmpl) echo '${BUILD_TAG}' ;; *) echo '' ;; esac } render_template() { local src="$1" local out="$2" local allowed="${3:-}" mkdir -p "$(dirname "$out")" if [[ -n "$allowed" ]]; then envsubst "$allowed" < "$src" > "$out" else check_template_vars "$src" envsubst < "$src" > "$out" fi if [[ -x "$src" ]]; then chmod +x "$out" fi } replace_file() { local src="$1" local dst="$2" local had_exec=0 [[ -e "$dst" && -x "$dst" ]] && had_exec=1 mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" if [[ "$had_exec" -eq 1 ]]; then chmod +x "$dst" fi 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 local had_exec=0 [[ -e "$dst" && -x "$dst" ]] && had_exec=1 cp -a "$src" "$dst" if [[ "$had_exec" -eq 1 ]]; then chmod +x "$dst" fi 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" "$(get_envsubst_vars "$rel_path")" 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