sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
fix(instalador): el bucle de la contraseña no terminaba nunca, y llenó 1,5 G de RAM del anfitrión
1#!/bin/sh
2# lib/cuenta.sh — LA CUENTA DE QUIEN VA A USAR LA MÁQUINA: se pregunta antes de instalar y se
3# escribe al terminar. Es la sección que faltaba en los dos instaladores.
4#
5# ── El defecto, medido en metal el 2026-10-14/15 ─────────────────────────────────────────────────
6# El operador instaló, arrancó, y **no podía hacer nada**: ni entrar como root, ni escalar. Lo dijo
7# así: *«al correr su me dice que debe ser suid, así que se cagó ese boot»* y *«¿de dónde salió el
8# usuario sergio? me di cuenta que debe ser una sección antes de instalar»*.
9#
10# Las dos preguntas tienen la misma respuesta, y es ésta: **nadie preguntó nunca por una cuenta.**
11#
12# · `takana-live-install.sh` (disco entero) pregunta hostname, zona, teclado y la credencial de
13# ROOT — y ningún usuario. En una imagen de escritorio eso es una máquina donde el greeter no
14# puede autenticar a nadie, porque no hay nadie.
15# · `takana-install-partition.sh` (el camino de `--keep`/`--adopt`, el que usó él) **no pregunta
16# NADA**. Los usuarios salen de `lib/usuarios-del-home.sh`, que los cosecha del `/home` que se
17# conserva —un usuario por directorio, con el uid de sus archivos— y les pone `!` en
18# `/etc/shadow`, que es «cuenta sin contraseña posible». Correcto de su parte: inventar una
19# contraseña sería peor. Pero nadie, después, la pedía.
20#
21# Resultado medido: `sergio:!` + `root:!` + ningún `doas.conf` = una máquina instalada, arrancada y
22# **inadministrable**. El encabezado de `takana-core/src/setuid.rs` lo había previsto textualmente
23# («hoy es LATENTE: el instalador sólo pone contraseña de root, así que no hay un humano no-root que
24# lo choque») y el 14 de octubre lo hubo.
25#
26# ── Por qué una librería y no dos secciones ──────────────────────────────────────────────────────
27# El mismo argumento que `lib/usuarios-del-home.sh` escribe para sí: son DOS caminos y UNA pregunta,
28# y dos implementaciones divergen en el caso raro. Acá el caso raro es justo el del operador —el
29# usuario YA existe, cosechado del `/home` conservado, y lo único que falta es su contraseña— y una
30# de las dos copias se lo habría perdido.
31#
32# ── La regla de root, sin criterio ───────────────────────────────────────────────────────────────
33# Root queda **cerrado** si el usuario puede escalar (hay `doas` en la imagen y se pidió que
34# administre): es lo correcto, y es lo que hacen las distros desde hace una década. Si NO puede
35# escalar, entonces se pide contraseña de root **obligatoriamente** — porque una máquina sin
36# ninguna de las dos no se administra, y eso es exactamente lo que acabamos de medir.
37#
38# Se usa así (los dos instaladores):
39#
40# . "$(dirname "$0")/lib/cuenta.sh"
41# cuenta_preguntar "$RAIZ_DEL_MEDIO" # TUI; no pregunta si no hay tty o si es desatendido
42# ...
43# cuenta_aplicar "$ARBOL" # escribe passwd/group/shadow/home/doas.conf
44#
45# Desatendido (arneses y `--unattended`): `TAKANA_USER`, `TAKANA_USER_HASH` (un hash, NO una
46# contraseña), `TAKANA_USER_PW` (sólo donde no haya más remedio), `TAKANA_ADMIN=1|0`,
47# `TAKANA_ROOT_PW` / `TAKANA_ROOT_HASH` como ya existían.
48#
49# ⚠ **La contraseña NO viaja por la cmdline del kernel.** `/proc/cmdline` lo lee cualquiera, así que
50# un hash ahí es un ataque de diccionario offline servido en bandeja. Por eso esto pregunta en el
51# instalador —que tiene tty— en vez de recibirlo del que hizo el kexec.
52
53CUENTA_USER="${TAKANA_USER:-}"
54CUENTA_HASH="${TAKANA_USER_HASH:-}"
55CUENTA_ADMIN="${TAKANA_ADMIN:-1}"
56CUENTA_ROOT_HASH="${TAKANA_ROOT_HASH:-${HAMMER_ROOT_HASH:-}}"
57
58# Los dos instaladores ya traen sus propios `ask`/`read_secret`/`bb`; si falta alguno (al usar la
59# librería desde un arnés) se definen acá. Un `command -v` sobre una función de shell la encuentra.
60command -v bb >/dev/null 2>&1 || bb() { busybox "$@" 2>/dev/null || command "$@"; }
61command -v ask >/dev/null 2>&1 || ask() {
62 _a_v="$1"; printf '%s%s: ' "$2" "$([ -n "${3:-}" ] && printf ' [%s]' "$3")"
63 IFS= read -r _a_r || { _a_r=""; CUENTA_EOF=1; }
64 [ -n "$_a_r" ] || _a_r="${3:-}"
65 eval "$_a_v=\$_a_r"
66}
67command -v read_secret >/dev/null 2>&1 || read_secret() {
68 printf '%s: ' "$2"; stty -echo 2>/dev/null || true
69 IFS= read -r _rs || { _rs=""; CUENTA_EOF=1; }; stty echo 2>/dev/null || true; echo
70 eval "$1=\$_rs"
71}
72
73# ── ⚠ POR QUÉ LOS BUCLES DE CONTRASEÑA SON ACOTADOS, Y NO ES UN GUSTO (2026-10-15) ─────────────
74# Los dos `while :;` de abajo reintentaban SIN TOPE, y los `read_secret` —el de acá y el que traen
75# los dos instaladores— se comen el fallo del `read` (`|| _sec=""`). Juntos: con un stdin en EOF
76# —un arnés, un `< /dev/null`, un pipe que se cerró— el `read` falla, la variable sale vacía, el
77# `[ -n ]` no pasa, y el bucle imprime «no coinciden (o vacía) — reintentá» para siempre, sin
78# dormir ni un milisegundo.
79#
80# MEDIDO en el anfitrión: un `takana-live-install.sh` quedó huérfano colgado de PID 1 y en CUATRO
81# MINUTOS escribió **1,5 G** de esas tres líneas. Y `/tmp` es **tmpfs**, así que eso no era disco:
82# era RAM, en una caja de 7,7 G que ya tenía 1,8 G en swap. El proceso no se veía: su propio script
83# ya estaba borrado y nada en su línea de comandos nombraba el fichero que estaba llenando.
84#
85# El tope va en el BUCLE y no sólo en el lector a propósito: los instaladores traen su propio
86# `read_secret` y la librería no manda sobre él, así que un bucle que se defiende solo es lo único
87# que protege en los tres casos. `CUENTA_EOF` corta en el primer intento cuando el lector SÍ es el
88# nuestro; el tope corta de todas formas.
89CUENTA_EOF=0
90CUENTA_INTENTOS="${CUENTA_INTENTOS:-3}"
91
92# cuenta_hash <contraseña> — SHA-512 por `cryptpw`. Devuelve vacío si este busybox no lo trae, y el
93# llamador lo dice: un hash vacío escribiría una cuenta SIN CONTRASEÑA, que es peor que no tenerla.
94cuenta_hash() {
95 printf '%s' "$1" | bb cryptpw -m sha512 2>/dev/null || true
96}
97
98# cuenta_login_valido <nombre> — el mismo criterio que `lib/usuarios-del-home.sh` usa al cosechar,
99# porque los dos juegos de usuarios tienen que poder convivir en el mismo `/etc/passwd`.
100cuenta_login_valido() {
101 case "$1" in
102 "" | -* | *[!a-z0-9_-]*) return 1 ;;
103 root | sshd | bin | daemon | nobody | messagebus) return 1 ;;
104 *) [ "${#1}" -le 32 ] ;;
105 esac
106}
107
108# cuenta_preguntar [raíz-del-medio] — la sección. No pregunta si no hay tty, si es desatendido, o si
109# ya vino todo por el ambiente.
110cuenta_preguntar() {
111 _cp_raiz="${1:-/}"
112 [ "${TAKANA_UNATTENDED:-0}" = 1 ] && return 0
113 # ⚠ La condición es la MISMA que usa el instalador para decidir si es interactivo, y no sólo
114 # `[ -t 0 ]`: con stdin alimentado por un heredoc —que es como se prueba una TUI sin una persona
115 # delante— no hay tty, y la sección se saltaba entera dejando el arnés en verde sobre un
116 # instalador que no preguntaba nada. Medido escribiéndolo.
117 [ -t 0 ] || [ "${TAKANA_INSTALL_INTERACTIVE:-${HAMMER_INSTALL_INTERACTIVE:-}}" = 1 ] || return 0
118 [ -n "$CUENTA_USER" ] && [ -n "$CUENTA_HASH" ] && return 0
119
120 # ¿Hay con qué escalar en la imagen que se está instalando? La respuesta cambia la regla de root,
121 # así que se MIRA y no se supone.
122 _cp_doas=no
123 for _d in usr/bin/doas bin/doas usr/local/bin/doas usr/bin/sudo; do
124 [ -e "$_cp_raiz/$_d" ] && { _cp_doas=sí; break; }
125 done
126
127 echo
128 echo "Tu cuenta en esta máquina"
129 echo " ─────────────────────────"
130 echo " Es la que vas a usar para entrar al escritorio. Sin una cuenta con contraseña,"
131 echo " el escritorio arranca y no deja entrar a nadie."
132 while :; do
133 ask CUENTA_USER "Nombre de usuario" "${CUENTA_USER:-}"
134 [ -z "$CUENTA_USER" ] && { echo " (sin usuario: la máquina queda sólo con root)"; break; }
135 cuenta_login_valido "$CUENTA_USER" && break
136 echo " ✗ «$CUENTA_USER» no sirve de login: minúsculas, dígitos, «_» y «-», sin empezar con «-»,"
137 echo " y no puede ser un usuario de sistema (root, sshd, nobody…)."
138 CUENTA_USER=""
139 done
140
141 if [ -n "$CUENTA_USER" ]; then
142 if [ -z "$CUENTA_HASH" ]; then
143 if [ -n "$(cuenta_hash x)" ]; then
144 _cp_n=0
145 while :; do
146 _cp_n=$((_cp_n+1))
147 read_secret _cp_p1 "Contraseña para $CUENTA_USER"
148 read_secret _cp_p2 "Repetir"
149 if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_n" -ge "$CUENTA_INTENTOS" ]; then
150 [ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || {
151 echo " ⚠ no pude leer una contraseña para $CUENTA_USER (¿stdin sin terminal?) — su cuenta queda CERRADA"
152 break; }
153 fi
154 [ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || { echo " no coinciden (o vacía) — reintentá"; continue; }
155 CUENTA_HASH=$(cuenta_hash "$_cp_p1")
156 [ -n "$CUENTA_HASH" ] || { echo " ⚠ no pude generar el hash"; break; }
157 break
158 done
159 _cp_p1=; _cp_p2=
160 else
161 echo " ⚠ este busybox no trae cryptpw: no puedo poner contraseña y la cuenta queda cerrada."
162 fi
163 fi
164 if [ "$_cp_doas" = sí ]; then
165 ask CUENTA_ADMIN "¿$CUENTA_USER administra la máquina (doas)? 1=sí 0=no" "1"
166 else
167 echo " ⚠ esta imagen no trae doas ni sudo: nadie puede escalar, así que root necesita contraseña."
168 CUENTA_ADMIN=0
169 fi
170 fi
171
172 # La regla de root, aplicada: cerrada si hay quien escale, obligatoria si no.
173 if [ -n "$CUENTA_USER" ] && [ "$CUENTA_ADMIN" = 1 ] && [ -n "$CUENTA_HASH" ]; then
174 [ -n "$CUENTA_ROOT_HASH" ] || echo " · root queda CERRADO: se administra con «doas» desde $CUENTA_USER."
175 elif [ -z "$CUENTA_ROOT_HASH" ]; then
176 echo
177 echo " Nadie puede escalar todavía ⇒ root necesita contraseña, o la máquina queda inadministrable."
178 if [ -n "$(cuenta_hash x)" ]; then
179 _cp_m=0
180 while :; do
181 _cp_m=$((_cp_m+1))
182 read_secret _cp_r1 "Contraseña de root"
183 read_secret _cp_r2 "Repetir"
184 if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_m" -ge "$CUENTA_INTENTOS" ]; then
185 [ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || {
186 echo " ⚠ no pude leer la contraseña de root (¿stdin sin terminal?) — root queda CERRADO"
187 break; }
188 fi
189 [ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || { echo " no coinciden (o vacía) — reintentá"; continue; }
190 CUENTA_ROOT_HASH=$(cuenta_hash "$_cp_r1"); break
191 done
192 _cp_r1=; _cp_r2=
193 fi
194 fi
195}
196
197# cuenta_resumen — una línea para el resumen de confirmación del instalador.
198cuenta_resumen() {
199 if [ -z "$CUENTA_USER" ]; then printf 'sin usuario (sólo root)'; return; fi
200 printf '%s' "$CUENTA_USER"
201 [ -n "$CUENTA_HASH" ] && printf ' con contraseña' || printf ' SIN contraseña (cuenta cerrada)'
202 [ "$CUENTA_ADMIN" = 1 ] && printf ', administra (doas)'
203}
204
205# _cuenta_romper_hardlink <fichero> — el árbol del stage son HARDLINKS del rootfs sellado (ADR
206# 0005): un `>>` sobre `etc/passwd` escribiría DENTRO del artefacto del store y lo envenenaría para
207# todas las imágenes, sin que nada falle. Es la misma precaución que toma `usuarios_aplicar`, y la
208# razón de que acá no haya un solo `>>` directo.
209_cuenta_romper_hardlink() {
210 [ -e "$1" ] || return 0
211 cp "$1" "$1.cuenta-tmp" && mv "$1.cuenta-tmp" "$1"
212}
213
214# cuenta_aplicar <árbol> — escribe la cuenta en la raíz nueva. Idempotente: si el usuario ya existe
215# (cosechado del `/home` conservado) NO lo vuelve a crear y sólo le pone su contraseña, que es
216# exactamente el caso del operador.
217cuenta_aplicar() {
218 _ca_t="$1"
219 bb mkdir -p "$_ca_t/etc"
220 for _f in etc/passwd etc/group etc/shadow; do _cuenta_romper_hardlink "$_ca_t/$_f"; done
221 [ -e "$_ca_t/etc/passwd" ] || : > "$_ca_t/etc/passwd"
222 [ -e "$_ca_t/etc/group" ] || : > "$_ca_t/etc/group"
223 [ -e "$_ca_t/etc/shadow" ] || { : > "$_ca_t/etc/shadow"; bb chmod 600 "$_ca_t/etc/shadow"; }
224
225 if [ -n "$CUENTA_USER" ]; then
226 if bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd"; then
227 _ca_uid=$(bb awk -F: -v n="$CUENTA_USER" '$1==n{print $3; exit}' "$_ca_t/etc/passwd")
228 echo " · «$CUENTA_USER» ya existe con uid $_ca_uid (del /home conservado): le pongo su contraseña"
229 else
230 # El primer uid libre desde 1000. Se cuenta sobre el passwd que YA tiene los usuarios
231 # cosechados, así que no puede chocar con ellos.
232 _ca_uid=1000
233 while bb awk -F: -v u="$_ca_uid" '$3==u{f=1} END{exit !f}' "$_ca_t/etc/passwd"; do
234 _ca_uid=$((_ca_uid + 1))
235 done
236 printf '%s:x:%s:%s:%s:/home/%s:/bin/sh\n' \
237 "$CUENTA_USER" "$_ca_uid" "$_ca_uid" "$CUENTA_USER" "$CUENTA_USER" >> "$_ca_t/etc/passwd"
238 bb awk -F: -v g="$_ca_uid" '$3==g{f=1} END{exit !f}' "$_ca_t/etc/group" \
239 || printf '%s:x:%s:\n' "$CUENTA_USER" "$_ca_uid" >> "$_ca_t/etc/group"
240 echo " · usuario «$CUENTA_USER» creado con uid $_ca_uid"
241 fi
242 # El hogar: sólo si NO existe. Con `--keep` o `--adopt` ya está ahí con sus archivos y sus
243 # dueños, y pisarlo sería el único daño irreparable que puede hacer un instalador.
244 if [ ! -e "$_ca_t/home/$CUENTA_USER" ]; then
245 bb mkdir -p "$_ca_t/home/$CUENTA_USER"
246 bb chown "$_ca_uid:$_ca_uid" "$_ca_t/home/$CUENTA_USER" 2>/dev/null || true
247 bb chmod 700 "$_ca_t/home/$CUENTA_USER"
248 echo " · /home/$CUENTA_USER creado (uid $_ca_uid)"
249 else
250 echo " · /home/$CUENTA_USER ya existe: NO lo toco"
251 fi
252 # Los grupos del hardware: seatd exige `video` POR NOMBRE, y sin `input` libinput no abre los
253 # dispositivos. Sólo si el grupo existe en la imagen — inventar un gid acá sería adivinar.
254 for _g in video input audio seat; do
255 bb awk -F: -v g="$_g" -v u="$CUENTA_USER" '
256 BEGIN{OFS=":"}
257 $1==g { if ($4=="") $4=u; else if ($4 !~ ("(^|,)" u "(,|$)")) $4=$4","u; f=1 }
258 {print}
259 END{exit !f}' "$_ca_t/etc/group" > "$_ca_t/etc/group.cuenta-tmp" \
260 && mv "$_ca_t/etc/group.cuenta-tmp" "$_ca_t/etc/group" \
261 || rm -f "$_ca_t/etc/group.cuenta-tmp"
262 done
263 if [ -n "$CUENTA_HASH" ]; then
264 _cuenta_set_shadow "$CUENTA_USER" "$CUENTA_HASH" "$_ca_t"
265 else
266 _cuenta_set_shadow "$CUENTA_USER" '!' "$_ca_t"
267 echo " ⚠ «$CUENTA_USER» queda SIN contraseña posible: nadie va a poder entrar con esa cuenta"
268 fi
269 # La regla de escalada. Sin esto doas RECHAZA TODO («no rule matched») aunque esté instalado y
270 # con su bit: quién escala no lo puede decidir el paquete, lo decide esta máquina.
271 if [ "$CUENTA_ADMIN" = 1 ]; then
272 if [ -e "$_ca_t/usr/bin/doas" ] || [ -e "$_ca_t/bin/doas" ]; then
273 if [ -e "$_ca_t/etc/doas.conf" ]; then
274 echo " · /etc/doas.conf ya existe: no lo toco"
275 else
276 printf '# Quién puede ser root en esta máquina. La ÚLTIMA regla que casa es la que vale.\npermit persist %s\n' \
277 "$CUENTA_USER" > "$_ca_t/etc/doas.conf"
278 bb chown 0:0 "$_ca_t/etc/doas.conf" 2>/dev/null || true
279 bb chmod 640 "$_ca_t/etc/doas.conf"
280 echo " · /etc/doas.conf: permit persist $CUENTA_USER"
281 fi
282 else
283 echo " ⚠ se pidió que «$CUENTA_USER» administre y esta imagen NO trae doas: no hay escalada"
284 fi
285 fi
286 fi
287
288 if [ -n "$CUENTA_ROOT_HASH" ]; then
289 _cuenta_set_shadow root "$CUENTA_ROOT_HASH" "$_ca_t"
290 echo " · root con contraseña"
291 fi
292
293 # El entorno de login de esa cuenta. Va acá y no en otra sección porque una cuenta cuyo shell no
294 # encuentra `/usr/local/bin` es media cuenta: es lo que dejó al operador con `claude` instalado y
295 # `claude: not found` en la consola.
296 cuenta_perfil_de_login "$_ca_t"
297
298 # ── El brazo de control, ACÁ y no en un test aparte ───────────────────────────────────────────
299 # Una cuenta mal escrita no se nota hasta el primer arranque, y arreglarla cuesta montar el disco
300 # desde otro sistema. Se comprueba lo que se acaba de escribir, y se dice en voz alta.
301 _ca_mal=0
302 if [ -n "$CUENTA_USER" ]; then
303 bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd" \
304 || { echo " ✗ «$CUENTA_USER» NO quedó en /etc/passwd"; _ca_mal=$((_ca_mal+1)); }
305 if [ -n "$CUENTA_HASH" ]; then
306 bb awk -F: -v n="$CUENTA_USER" '$1==n && $2 ~ /^\$/{f=1} END{exit !f}' "$_ca_t/etc/shadow" \
307 || { echo " ✗ «$CUENTA_USER» no quedó con un hash en /etc/shadow"; _ca_mal=$((_ca_mal+1)); }
308 fi
309 fi
310 # El aviso cubre las TRES formas de quedar sin nadie que administre: sin contraseña de root y
311 # (sin usuario | con usuario que no escala | con usuario sin contraseña). La primera versión de
312 # esta línea sólo miraba `ADMIN != 1`, y con el default `ADMIN=1` y usuario VACÍO se callaba —
313 # que es exactamente la combinación con la que arrancó la máquina del operador.
314 if [ -z "$CUENTA_ROOT_HASH" ] \
315 && { [ -z "$CUENTA_USER" ] || [ "$CUENTA_ADMIN" != 1 ] || [ -z "$CUENTA_HASH" ]; }; then
316 echo " ⚠ root cerrado y nadie escala: esta máquina va a arrancar INADMINISTRABLE"
317 fi
318 [ "$_ca_mal" = 0 ]
319}
320
321# cuenta_perfil_de_login <árbol> — `/etc/profile`, que es lo que lee el shell de esa cuenta.
322#
323# ── Por qué está acá y no en una receta ──────────────────────────────────────────────────────────
324# Medido en la máquina del operador: **`/etc/profile` NO EXISTÍA**. Y como el `ash` de busybox
325# sourcea `/etc/profile.d/*` sólo si ese archivo lo hace, `/etc/profile.d/tawasuyu-dev.sh` —que
326# exportaba el PATH del toolchain— estuvo inerte desde el primer arranque: `claude` instalado en
327# `/usr/local/bin` y la consola contestando `claude: not found`. Las recetas no tienen mecanismo
328# para aportar un fichero de `/etc` (no hay `[[file]]`: lo que entra sale del `install` de la
329# receta, y tocar eso re-hashea), así que el sitio donde nace un `/etc` de ESTA máquina es el
330# instalador — el mismo que escribe `hostname`, `hosts`, `localtime` y `shadow`.
331#
332# **Los seis directorios se garantizan uno por uno** en vez de agregar sólo `/usr/local`: un PATH
333# heredado puede traer unos y no otros, y entonces `su` o `wpa_supplicant` «no existen» porque
334# faltaba `/bin` o `/usr/sbin`. Medido dentro de un chroot de la imagen: con el PATH de la otra
335# máquina, `claude` se encontraba y `su` no.
336#
337# Y los del hogar van con `$HOME`, no con una ruta escrita: este fichero viaja a la máquina de
338# cualquiera. La primera versión de este PATH decía `/home/sergio/...` y eso es justo lo que no
339# puede quedar en algo que instala un desconocido.
340cuenta_perfil_de_login() {
341 _cp_t="$1"
342 [ -e "$_cp_t/etc/profile" ] && { echo " · /etc/profile ya existe: no lo toco"; return 0; }
343 bb mkdir -p "$_cp_t/etc"
344 cat > "$_cp_t/etc/profile" <<'PERFIL'
345# /etc/profile — lo que lee un login shell.
346#
347# El `ash` de busybox sourcea ESTE fichero y nada más: `/etc/profile.d/*` no se lee si acá no se
348# lee. Sin esto, la consola de rescate tampoco tiene PATH.
349for dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
350 case ":${PATH:-}:" in
351 *":$dir:"*) ;;
352 *) PATH="${PATH:+$PATH:}$dir" ;;
353 esac
354done
355# Y los del hogar, al final: lo de la persona no debe tapar lo del sistema.
356for dir in "$HOME/.local/bin" "$HOME/bin"; do
357 [ -d "$dir" ] || continue
358 case ":$PATH:" in *":$dir:"*) ;; *) PATH="$PATH:$dir" ;; esac
359done
360export PATH
361umask 022
362
363for perfil in /etc/profile.d/*.sh; do
364 [ -r "$perfil" ] && . "$perfil"
365done
366unset perfil dir
367PERFIL
368 bb chmod 644 "$_cp_t/etc/profile"
369 echo " · /etc/profile escrito (los seis directorios + los del hogar al final)"
370}
371
372# El mismo `_set_shadow` que ya tenía `takana-live-install.sh`, con otro nombre para no pisárselo.
373_cuenta_set_shadow() {
374 _cs_u="$1"; _cs_h="$2"; _cs_f="$3/etc/shadow"
375 if [ ! -f "$_cs_f" ]; then
376 printf '%s:%s:20000:0:99999:7:::\n' "$_cs_u" "$_cs_h" > "$_cs_f"
377 bb chmod 600 "$_cs_f"
378 return
379 fi
380 bb awk -F: -v u="$_cs_u" -v h="$_cs_h" 'BEGIN{OFS=":"}
381 { if ($1==u){ $2=h; seen=1 } print }
382 END{ if(!seen) print u,h,"20000","0","99999","7","","","" }' "$_cs_f" > "$_cs_f.cuenta-tmp" \
383 && mv "$_cs_f.cuenta-tmp" "$_cs_f"
384 bb chmod 600 "$_cs_f"
385}