Dotfiles/setup/tools/generate-modules.sh

239 lines
8.8 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 — deselecting '${eid}'\"\n"
gen_conflicts+=" SELECTED_APPS=\" \$SELECTED_APPS \"\n"
gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS/ ${eid} / }\"\n"
gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS# }\"\n"
gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS% }\"\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."