Files
hassio-addons/.github/scripts/resolve_symlinks.sh
Alexandre bfe91cbeac refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts (#2920)
* refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts

All three add-ons are built on the LinuxServer Selkies base image and had
independently drifted copies of the same startup scripts. claude_desktop's
copies carry a set of fixes the two webtops never received, so make
claude_desktop the single source and symlink the shared scripts from
webtop_kde/rootfs (which webtop/rootfs already symlinks in full).

Shared by symlink: 20-folders.sh, 21-gpu_permissions.sh, 80-configuration.sh,
90-ingress.sh and the six etc/nginx/includes files.

Kept add-on specific: everything Claude-only stays in claude_desktop
(81/82/83/84 tool installs, 85-openbox_autostart.sh, defaults/, usr/local/bin,
svc-headroom), and everything webtop-only stays in webtop_kde (90-ssl.sh,
helpers/microsoft-edge-stable, and the new 81-microsoft_edge.sh).

To make the shared scripts add-on agnostic:
- 20-folders.sh derives its default data location from the home directory the
  Dockerfile baked into the abc user instead of hardcoding /data/data. That
  yields /data/data on claude_desktop and /config/data_kde on both webtops,
  matching each add-on's previous behaviour exactly.
- The permission_mode: bypass root guard is skipped on add-ons that do not
  declare that option.
- 80-configuration.sh falls back to pip when the image does not ship uv.
- The Microsoft Edge install moves out of 80-configuration.sh into a
  webtop-only 81-microsoft_edge.sh, which also absorbs the ownership fixup
  that used to run in 20-folders.sh before Edge was installed and so never
  matched anything.

CI: the builder's symlink-resolution step made a single pass over a
pre-computed file list, so resolving webtop/rootfs (a directory symlink)
could copy the symlinks inside it verbatim, leaving links that escape the
webtop build context. Verified on this tree: the old loop leaves 10 dangling
symlinks under webtop/. Extract it to .github/scripts/resolve_symlinks.sh,
repeat until a pass finds nothing, and run it in the PR check too, which had
no resolution step at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: guard two startup aborts found in review

Both are crash paths in code added by this PR, not hardening:

- 20-folders.sh: getent exits 2 when the user does not exist, and under
  bashio's `set -o pipefail` plus the script's `set -e` that aborts at the
  assignment, so the "could not read abc's home" fallback below it was
  unreachable. Same trap already documented in 21-gpu_permissions.sh.
  Verified: without the guard the shell exits 2; with it the fallback runs.

- 81-microsoft_edge.sh: the ownership fixup lost the `-f` guard the original
  had in 20-folders.sh. Without nullglob an unmatched /usr/bin/microsoft-edge*
  reaches chown as a literal and `set -e` kills container startup. Now a
  nullglob array with a warning when empty.

Also make resolve_symlinks.sh fail on a broken symlink instead of deleting it.
Dropping it silently yields an image that builds clean and misbehaves at
runtime; a red build is easier to diagnose. Verified both paths: the repo as-is
resolves to 0 symlinks and exit 0, and an injected broken link exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: address review comments on data-location default and Edge downloads

Two findings from CodeRabbit, both correctness rather than hardening:

- 20-folders.sh rewrites abc's home in /etc/passwd further down, so re-reading
  it on the next boot returned the *previously selected* location as the image
  default. On a restart that reuses the container's writable layer, clearing
  data_location would strand the user on their old custom path instead of
  restoring the built-in one. Cache the value in /etc/.addon_image_home, which
  shares the writable layer's lifetime with the edit it compensates for: a
  rebuilt or recreated container starts from a pristine /etc/passwd and
  regenerates it. Simulated all three cases (first boot, reused container with
  a rewritten passwd, recreated container) under `set -e` + `set -o pipefail`.

- 81-microsoft_edge.sh: both curl calls were unbounded, so a stalled
  packages.microsoft.com would hang cont-init.d and with it the whole add-on.
  Add --fail/--connect-timeout/--max-time and skip the install with a logged
  error when version discovery or the download fails. The desktop is useful
  without Edge; an add-on wedged before Selkies starts is not.

Not addressed, deliberately: escaping $LOCATION/$DEFAULT_LOCATION for sed, and
validating symlink targets in resolve_symlinks.sh. Both are hardening against
inputs that are not reachable in normal use, both predate this PR, and the
maintainer has asked to prioritise usability over that class of change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:23:52 +02:00

46 lines
1.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Replace every symlink in the checked-out repository with a real copy of its target.
#
# Add-ons share files by symlinking across add-on directories (e.g. webtop/rootfs ->
# ../webtop_kde/rootfs, and files inside it -> ../../../../claude_desktop/rootfs/...). A
# Docker build context is a single add-on directory, so any symlink that escapes it has to be
# materialised before the build.
#
# The loop repeats because resolving one symlink can create others: copying a directory
# symlink with `cp -a` preserves the symlinks *inside* it, and those copies are not part of
# the file list the current pass is iterating over. Repeating until a pass finds nothing makes
# the result independent of the order `find` happens to return.
set -euo pipefail
for _ in 1 2 3 4 5; do
mapfile -t links < <(find . -type l)
if [ "${#links[@]}" -eq 0 ]; then
exit 0
fi
for link in "${links[@]}"; do
target=$(readlink -f "$link" || true)
if [ -z "$target" ] || [ ! -e "$target" ]; then
# Fail rather than drop it. A broken link here means an add-on is missing a file
# it expects to ship; silently removing it produces an image that builds fine and
# misbehaves at runtime, which is far harder to diagnose than a red build.
echo "::error::Broken symlink: $link -> $(readlink "$link")"
exit 1
fi
rm "$link"
if [ -d "$target" ]; then
mkdir -p "$link"
cp -a "$target/." "$link/"
else
cp "$target" "$link"
fi
done
done
if [ -n "$(find . -type l)" ]; then
echo "::error::Symlinks still present after 5 resolution passes; possible symlink cycle"
find . -type l
exit 1
fi