summaryrefslogtreecommitdiff
path: root/update.sh
blob: 8a7d10808cc530125177ec9f677af22b5cb1af2d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
QUICK=no
DO_COMMIT=no
INSTALL_HOOK=no
DRY_RUN=no

usage() {
  cat <<EOF
Sync live system state back into the dotfiles repo.

Usage: $0 [options]

  -q, --quick        non-interactive, minimal output (used by postsync hook)
  -c, --commit       git add -A && git commit after syncing
  -h, --hook-install write /etc/portage/postsync.d hook (auto after each emerge)
  -n, --dry-run      show what would change without copying
  --help             show this help
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    -q|--quick) QUICK=yes ;;
    -c|--commit) DO_COMMIT=yes ;;
    -h|--hook-install) INSTALL_HOOK=yes ;;
    -n|--dry-run) DRY_RUN=yes ;;
    --help) usage; exit 0 ;;
    *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
  esac
  shift
done

log()  { printf '\033[1;32m[sync]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }

root_cp() {
  if [[ "$DRY_RUN" == "yes" ]]; then
    log "  would copy $1 -> $2"
    return 0
  fi
  if [[ $EUID -eq 0 ]]; then
    cp "$1" "$2"
  elif cp "$1" "$2" 2>/dev/null; then
    :   # readable as current user — done
  elif command -v sudo >/dev/null 2>&1; then
    sudo cp "$1" "$2"
  else
    warn "need root to copy $1"
    return 1
  fi
}

rsync_portage() {
  # -L: dereference symlinks so /etc/portage symlinks back into the repo
  #     are copied as content, never re-created as self-referencing symlinks.
  # Broken symlinks are excluded (rsync -L aborts on dangling referents).
  local exfile
  exfile="$(mktemp)"
  printf '.gitkeep\nmake.profile\n' > "$exfile"
  find /etc/portage -type l ! -exec test -e {} \; -printf '%P\n' >> "$exfile"

  local -a opts=(-aL --delete "--exclude-from=$exfile")
  [[ "$DRY_RUN" == "yes" ]] && opts+=(-n)
  log "portage /etc/portage -> repo/portage/etc/portage"
  rsync "${opts[@]}" /etc/portage/ "$REPO_DIR/portage/etc/portage/"
  rm -f "$exfile"
}

sync_portage() {
  rsync_portage

  if [[ -e /etc/portage/make.profile ]]; then
    log "profile: $(readlink /etc/portage/make.profile)"
    if [[ "$DRY_RUN" != "yes" ]]; then
      printf '%s\n' "$(readlink /etc/portage/make.profile)" > "$REPO_DIR/portage/PROFILE"
    else
      log "  would write: portage/PROFILE"
    fi
  fi

  log "world /var/lib/portage/world -> repo"
  if [[ -L /var/lib/portage/world ]] \
     && [[ "$(readlink -f /var/lib/portage/world)" == "$REPO_DIR/portage/var/lib/portage/world" ]]; then
    log "  already a symlink into the repo — no copy needed"
  else
    root_cp /var/lib/portage/world "$REPO_DIR/portage/var/lib/portage/world"
  fi

  if [[ -s /var/lib/portage/world_sets ]]; then
    log "world_sets /var/lib/portage/world_sets -> repo"
    root_cp /var/lib/portage/world_sets "$REPO_DIR/portage/var/lib/portage/world_sets"
  fi
}

sync_system() {
  log "fstab /etc/fstab -> repo/fstab/etc/fstab"
  root_cp /etc/fstab "$REPO_DIR/fstab/etc/fstab"

  if [[ -f /etc/greetd/config.toml ]]; then
    log "greetd /etc/greetd/config.toml -> repo/config.toml"
    root_cp /etc/greetd/config.toml "$REPO_DIR/config.toml"
  fi
}

install_hook() {
  local hook=/etc/portage/postsync.d/update-dotfiles
  if [[ "$DRY_RUN" == "yes" ]]; then
    log "  would write: $hook"
    return 0
  fi
  log "writing $hook"
  if [[ $EUID -eq 0 ]]; then
    mkdir -p /etc/portage/postsync.d
    cat > "$hook" <<EOF
#!/usr/bin/env bash
# generated by update.sh --hook-install — keeps dotfiles fresh after each emerge
exec "$REPO_DIR/update.sh" --quick
EOF
    chmod +x "$hook"
  elif command -v sudo >/dev/null 2>&1; then
    sudo tee "$hook" >/dev/null <<EOF
#!/usr/bin/env bash
# generated by update.sh --hook-install — keeps dotfiles fresh after each emerge
exec "$REPO_DIR/update.sh" --quick
EOF
    sudo chmod +x "$hook"
  else
    warn "need root to write $hook"
  fi
}

commit() {
  cd "$REPO_DIR"
  if [[ -n "$(git status --porcelain)" ]]; then
    git add -A
    git commit -q -m "sync system state: $(date '+%Y-%m-%d %H:%M')"
    log "committed."
  else
    log "nothing to commit."
  fi
}

main() {
  if [[ "$DRY_RUN" == "yes" ]]; then
    log "dry-run mode — showing changes, nothing written"
  fi
  sync_portage
  sync_system
  [[ "$INSTALL_HOOK" == "yes" ]] && install_hook

  if [[ "$DO_COMMIT" == "yes" ]]; then
    commit
  elif [[ "$DRY_RUN" != "yes" && "$QUICK" != "yes" ]]; then
    cd "$REPO_DIR"
    if [[ -n "$(git status --porcelain)" ]]; then
      warn "dotfiles changed — review and commit (or rerun with --commit)."
      git status --short
    else
      log "repo is up to date."
    fi
  fi
}

main "$@"