236 lines
8.6 KiB
Bash
Executable File
236 lines
8.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# generate-modules.sh — regenerate all sentinel regions from modules.conf
|
|
#
|
|
# Usage:
|
|
# ./generate-modules.sh — apply changes in-place
|
|
# ./generate-modules.sh --dry-run — print a unified diff, make no changes
|
|
#
|
|
# Sentinel format in target files:
|
|
# bash/sh: # BEGIN GENERATED MODULES: <tag> / # END GENERATED MODULES: <tag>
|
|
# markdown: <!-- BEGIN GENERATED MODULES: <tag> --> / <!-- END GENERATED MODULES: <tag> -->
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SETUP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CONF="$SETUP_DIR/modules.conf"
|
|
TUI="$SETUP_DIR/tui-install.sh"
|
|
AF="$SETUP_DIR/generate-answerfile.sh"
|
|
DOCS="$SETUP_DIR/../docs/md/modules.md"
|
|
|
|
DRY_RUN=false
|
|
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
|
|
|
|
# ── Parse modules.conf ────────────────────────────────────────────────────────
|
|
declare -a M_IDS M_CATS M_DESCS M_DEFAULTS M_EXCLUDES
|
|
skipped=()
|
|
|
|
while IFS='|' read -r id cat desc def excl; do
|
|
# Strip leading/trailing whitespace from each field
|
|
id="${id#"${id%%[![:space:]]*}"}"; id="${id%"${id##*[![:space:]]}"}"
|
|
[[ "$id" =~ ^# || -z "$id" ]] && continue
|
|
M_IDS+=("$id")
|
|
M_CATS+=("$cat")
|
|
M_DESCS+=("$desc")
|
|
M_DEFAULTS+=("${def:-off}")
|
|
M_EXCLUDES+=("${excl:-}")
|
|
done < "$CONF"
|
|
|
|
# Identify incomplete entries (missing category or description) — skip them.
|
|
declare -a ACTIVE_IDS
|
|
for i in "${!M_IDS[@]}"; do
|
|
if [[ -z "${M_CATS[$i]}" || -z "${M_DESCS[$i]}" ]]; then
|
|
skipped+=("${M_IDS[$i]}")
|
|
else
|
|
ACTIVE_IDS+=("$i")
|
|
fi
|
|
done
|
|
|
|
if (( ${#skipped[@]} > 0 )); then
|
|
printf '[warn] skipping %d stub(s) with empty category/description:\n' "${#skipped[@]}" >&2
|
|
printf ' %s\n' "${skipped[@]}" >&2
|
|
fi
|
|
|
|
# ── Build generated sections ──────────────────────────────────────────────────
|
|
|
|
# -- module-counters (tui-install.sh: count_steps function body) --------------
|
|
gen_counters=""
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
id="${M_IDS[$i]}"
|
|
gen_counters+=" [[ \"\$a\" == *\"${id}\"* ]] && TOTAL=\$(( TOTAL + 1 ))\n"
|
|
done
|
|
|
|
# -- module-checklist (tui-install.sh and generate-answerfile.sh) -------------
|
|
# Builds the full SELECTED_APPS=$(dialog ...) call including open/close.
|
|
build_checklist_tui() {
|
|
local out
|
|
out=' SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \\\n'
|
|
out+=' --title " Applications " \\\n'
|
|
out+=' --checklist "Optional applications — installed after base components:" "$_APP_H" 76 "$_APP_LIST_H" \\\n'
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
local id="${M_IDS[$i]}" desc="${M_DESCS[$i]}" def="${M_DEFAULTS[$i]}"
|
|
local padded
|
|
padded=$(printf '%-20s' "$id")
|
|
out+=" \"${id}\" \"${padded} ${desc}\" ${def} \\\\\n"
|
|
done
|
|
out+=' 3>&1 1>&2 2>&3) || SELECTED_APPS=""\n'
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
build_checklist_af() {
|
|
local count="${#ACTIVE_IDS[@]}"
|
|
local out
|
|
out=" AF_APPS=\$(dialog --backtitle \"\$BACKTITLE\" \\\\\n"
|
|
out+=" --title \" Applications \" \\\\\n"
|
|
out+=" --checklist \"Optional applications — installed after base components:\" 40 76 ${count} \\\\\n"
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
local id="${M_IDS[$i]}" desc="${M_DESCS[$i]}" def="${M_DEFAULTS[$i]}"
|
|
local padded
|
|
padded=$(printf '%-20s' "$id")
|
|
out+=" \"${id}\" \"${padded} ${desc}\" ${def} \\\\\n"
|
|
done
|
|
out+=' 3>&1 1>&2 2>&3) || AF_APPS=""\n'
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
gen_checklist_tui=$(build_checklist_tui)
|
|
gen_checklist_af=$(build_checklist_af)
|
|
|
|
# -- module-summary (tui-install.sh: confirmation dialog) ---------------------
|
|
gen_summary=""
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
id="${M_IDS[$i]}"
|
|
gen_summary+=" [[ \"\$SELECTED_APPS\" == *\"${id}\"* ]] && SUMMARY+=\" ✦ ${id}\\\\n\"\n"
|
|
done
|
|
|
|
# -- module-conflicts (tui-install.sh: before dispatch) -----------------------
|
|
gen_conflicts=""
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
id="${M_IDS[$i]}"
|
|
excl="${M_EXCLUDES[$i]}"
|
|
[[ -z "$excl" ]] && continue
|
|
IFS=',' read -ra excl_list <<< "$excl"
|
|
for eid in "${excl_list[@]}"; do
|
|
eid="${eid#"${eid%%[![:space:]]*}"}"; eid="${eid%"${eid##*[![:space:]]}"}"
|
|
gen_conflicts+="if [[ \"\$SELECTED_APPS\" == *\"${id}\"* && \"\$SELECTED_APPS\" == *\"${eid}\"* ]]; then\n"
|
|
gen_conflicts+=" warn \"${id} and ${eid} are mutually exclusive — skipping ${eid}\"\n"
|
|
gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS/${eid}/}\"\n"
|
|
gen_conflicts+="fi\n"
|
|
done
|
|
done
|
|
|
|
# -- module-dispatch (tui-install.sh: installation section) -------------------
|
|
gen_dispatch=""
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
id="${M_IDS[$i]}"
|
|
gen_dispatch+="[[ \"\$SELECTED_APPS\" == *\"${id}\"* ]] && run_module \"${id}\" \"\$APPS/${id}.sh\"\n"
|
|
done
|
|
|
|
# -- per-category doc tables (docs/md/modules.md) -----------------------------
|
|
declare -A gen_docs_cat
|
|
for i in "${ACTIVE_IDS[@]}"; do
|
|
id="${M_IDS[$i]}"
|
|
cat="${M_CATS[$i]}"
|
|
desc="${M_DESCS[$i]}"
|
|
gen_docs_cat[$cat]+="| \`${id}\` | ${desc} |\n"
|
|
done
|
|
|
|
build_doc_table() {
|
|
local cat="$1"
|
|
local rows="${gen_docs_cat[$cat]:-}"
|
|
if [[ -z "$rows" ]]; then
|
|
printf ''
|
|
return
|
|
fi
|
|
printf '| ID | Description |\n|----|-------------|\n'
|
|
printf '%b' "$rows"
|
|
}
|
|
|
|
# ── Splice helper (uses python3 for safe multiline replacement) ───────────────
|
|
splice() {
|
|
local file="$1" tag="$2" content="$3" style="${4:-bash}"
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
printf '%b' "$content" > "$tmpfile"
|
|
|
|
if $DRY_RUN; then
|
|
python3 - "$file" "$tag" "$tmpfile" "$style" "dry" <<'PYEOF'
|
|
import sys, re, difflib
|
|
filepath, tag, tmpfile, style, _ = sys.argv[1:]
|
|
with open(tmpfile) as f:
|
|
new_content = f.read()
|
|
if style == 'md':
|
|
begin = f'<!-- BEGIN GENERATED MODULES: {tag} -->'
|
|
end = f'<!-- END GENERATED MODULES: {tag} -->'
|
|
else:
|
|
begin = f'# BEGIN GENERATED MODULES: {tag}'
|
|
end = f'# END GENERATED MODULES: {tag}'
|
|
with open(filepath) as f:
|
|
original = f.read()
|
|
pattern = re.compile(re.escape(begin) + r'\n.*?' + re.escape(end), re.DOTALL)
|
|
if not pattern.search(original):
|
|
print(f'ERROR: sentinel not found in {filepath}: {begin}', file=sys.stderr)
|
|
sys.exit(1)
|
|
replacement = begin + '\n' + new_content + end
|
|
updated = pattern.sub(lambda m: replacement, original)
|
|
if updated == original:
|
|
print(f' (no change) {filepath} [{tag}]', file=sys.stderr)
|
|
sys.exit(0)
|
|
diff = difflib.unified_diff(
|
|
original.splitlines(keepends=True),
|
|
updated.splitlines(keepends=True),
|
|
fromfile=f'{filepath}',
|
|
tofile=f'{filepath} (generated)',
|
|
)
|
|
sys.stdout.writelines(diff)
|
|
PYEOF
|
|
else
|
|
python3 - "$file" "$tag" "$tmpfile" "$style" <<'PYEOF'
|
|
import sys, re
|
|
filepath, tag, tmpfile, style = sys.argv[1:]
|
|
with open(tmpfile) as f:
|
|
new_content = f.read()
|
|
if style == 'md':
|
|
begin = f'<!-- BEGIN GENERATED MODULES: {tag} -->'
|
|
end = f'<!-- END GENERATED MODULES: {tag} -->'
|
|
else:
|
|
begin = f'# BEGIN GENERATED MODULES: {tag}'
|
|
end = f'# END GENERATED MODULES: {tag}'
|
|
with open(filepath) as f:
|
|
original = f.read()
|
|
pattern = re.compile(re.escape(begin) + r'\n.*?' + re.escape(end), re.DOTALL)
|
|
if not pattern.search(original):
|
|
print(f'ERROR: sentinel not found in {filepath}: {begin}', file=sys.stderr)
|
|
sys.exit(1)
|
|
replacement = begin + '\n' + new_content + end
|
|
updated = pattern.sub(lambda m: replacement, original)
|
|
if updated == original:
|
|
print(f' (no change) {filepath} [{tag}]', file=sys.stderr)
|
|
sys.exit(0)
|
|
with open(filepath, 'w') as f:
|
|
f.write(updated)
|
|
print(f' (updated) {filepath} [{tag}]', file=sys.stderr)
|
|
PYEOF
|
|
fi
|
|
rm -f "$tmpfile"
|
|
}
|
|
|
|
# ── Apply all regions ─────────────────────────────────────────────────────────
|
|
echo "==> tui-install.sh"
|
|
splice "$TUI" "module-counters" "$gen_counters"
|
|
splice "$TUI" "module-checklist" "$gen_checklist_tui"
|
|
splice "$TUI" "module-summary" "$gen_summary"
|
|
splice "$TUI" "module-conflicts" "$gen_conflicts"
|
|
splice "$TUI" "module-dispatch" "$gen_dispatch"
|
|
|
|
echo "==> generate-answerfile.sh"
|
|
splice "$AF" "module-checklist" "$gen_checklist_af"
|
|
|
|
echo "==> docs/md/modules.md"
|
|
for cat in ai networking dev system gaming notes media graphics video audio browsers editors virt productivity infra; do
|
|
table=$(build_doc_table "$cat")
|
|
[[ -z "$table" ]] && continue
|
|
splice "$DOCS" "$cat" "${table}"$'\n' md
|
|
done
|
|
|
|
echo "Done."
|