#!/usr/bin/env bash set -euo pipefail DEFAULT_USERS=(kari owen romy) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SYSTEM_EXT_DIR="/usr/share/gnome-shell/extensions" DEFAULT_EXTENSIONS_FILE="${SCRIPT_DIR}/extensions.txt" DCONF_PROFILE_FILE="/etc/dconf/profile/user" DCONF_DEFAULTS_FILE="/etc/dconf/db/local.d/00-gnome-extensions" DCONF_LOCKS_FILE="/etc/dconf/db/local.d/locks/gnome-extensions" DRY_RUN=0 LOCK_DEFAULT=0 REFRESH_EXISTING=0 USERS=("${DEFAULT_USERS[@]}") EXTENSIONS_FILE="${DEFAULT_EXTENSIONS_FILE}" EXTENSION_UUIDS=() EXTENSION_SOURCES=() EXTENSIONS_BASE_DIR="" print_usage() { cat < Supported source values: - local zip path (relative or absolute) - zip URL (https://...zip) - git URL (https://...git) - git URL with subdir (https://...git#path/to/extension) USAGE } run_cmd() { if [[ $DRY_RUN -eq 1 ]]; then printf '[dry-run] ' printf '%q ' "$@" echo return 0 fi "$@" } run_cmd_may_fail() { if [[ $DRY_RUN -eq 1 ]]; then printf '[dry-run] ' printf '%q ' "$@" echo return 0 fi "$@" } write_file() { local path="$1" local content="$2" if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] write ${path}" echo "$content" return 0 fi mkdir -p "$(dirname "$path")" printf '%s\n' "$content" > "$path" } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --extensions-file) EXTENSIONS_FILE="$2" shift 2 ;; --users) IFS=',' read -r -a USERS <<< "$2" shift 2 ;; --lock-default) LOCK_DEFAULT=1 shift ;; --refresh-existing) REFRESH_EXISTING=1 shift ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) print_usage exit 0 ;; *) echo "Unknown argument: $1" >&2 print_usage exit 1 ;; esac done } ensure_root() { if [[ $DRY_RUN -eq 1 ]]; then return 0 fi if [[ ${EUID:-$(id -u)} -ne 0 ]]; then echo "Run as root (sudo) unless using --dry-run." >&2 exit 1 fi } extract_uuid() { local metadata_file="$1" sed -n 's/.*"uuid"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file" | head -n1 } resolve_source_path() { local source="$1" if [[ "$source" = /* ]]; then printf '%s' "$source" else printf '%s/%s' "$EXTENSIONS_BASE_DIR" "$source" fi } is_zip_source() { local source="$1" [[ "$source" == *.zip ]] || [[ "$source" == *.zip\?* ]] } is_url_source() { local source="$1" [[ "$source" =~ ^https?:// ]] } normalize_extension_permissions() { local target_dir="$1" run_cmd chown -R root:root "$target_dir" run_cmd find "$target_dir" -type d -exec chmod 755 {} + run_cmd find "$target_dir" -type f -exec chmod 644 {} + } copy_dir_into_system_extensions() { local source_dir="$1" local uuid="$2" local target_dir="${SYSTEM_EXT_DIR}/${uuid}" if [[ ! -f "$source_dir/metadata.json" ]]; then echo "Invalid extension source for $uuid: metadata.json missing in $source_dir" >&2 return 1 fi if [[ "$(extract_uuid "$source_dir/metadata.json")" != "$uuid" ]]; then echo "UUID mismatch for $uuid in $source_dir/metadata.json" >&2 return 1 fi run_cmd mkdir -p "$target_dir" run_cmd cp -a "$source_dir/." "$target_dir/" normalize_extension_permissions "$target_dir" echo "System-wide extension ensured: $uuid" } resolve_extension_dir_from_tree() { local tree_root="$1" local uuid="$2" local subdir="$3" local candidate local metadata if [[ -n "$subdir" ]]; then candidate="$tree_root/$subdir" if [[ -f "$candidate/metadata.json" ]] && [[ "$(extract_uuid "$candidate/metadata.json")" == "$uuid" ]]; then printf '%s' "$candidate" return 0 fi echo "Subdir '$subdir' does not contain extension $uuid" >&2 return 1 fi if [[ -f "$tree_root/metadata.json" ]] && [[ "$(extract_uuid "$tree_root/metadata.json")" == "$uuid" ]]; then printf '%s' "$tree_root" return 0 fi while IFS= read -r metadata; do if [[ "$(extract_uuid "$metadata")" == "$uuid" ]]; then printf '%s' "$(dirname "$metadata")" return 0 fi done < <(find "$tree_root" -maxdepth 6 -type f -name metadata.json) echo "Could not locate extension $uuid in source tree" >&2 return 1 } install_from_zip_source() { local uuid="$1" local source="$2" local work_dir local zip_path local extract_dir local ext_dir if [[ $DRY_RUN -eq 1 ]]; then if is_url_source "$source"; then run_cmd curl -fsSL "$source" -o /tmp/extension.zip run_cmd unzip -q /tmp/extension.zip -d /tmp/extension-unzip else zip_path="$(resolve_source_path "$source")" run_cmd unzip -q "$zip_path" -d /tmp/extension-unzip fi echo "[dry-run] resolve extension directory for $uuid from zip" run_cmd mkdir -p "${SYSTEM_EXT_DIR}/${uuid}" run_cmd cp -a /tmp/extension-unzip/. "${SYSTEM_EXT_DIR}/${uuid}/" normalize_extension_permissions "${SYSTEM_EXT_DIR}/${uuid}" echo "System-wide extension ensured: $uuid" return 0 fi work_dir="$(mktemp -d)" extract_dir="$work_dir/unzip" mkdir -p "$extract_dir" if is_url_source "$source"; then zip_path="$work_dir/source.zip" curl -fsSL "$source" -o "$zip_path" else zip_path="$(resolve_source_path "$source")" if [[ ! -f "$zip_path" ]]; then echo "Missing zip source for $uuid: $zip_path" >&2 rm -rf "$work_dir" return 1 fi fi unzip -q "$zip_path" -d "$extract_dir" ext_dir="$(resolve_extension_dir_from_tree "$extract_dir" "$uuid" "")" copy_dir_into_system_extensions "$ext_dir" "$uuid" rm -rf "$work_dir" } install_from_git_source() { local uuid="$1" local source="$2" local repo_url local subdir="" local work_dir local repo_dir local ext_dir repo_url="${source%%#*}" if [[ "$source" == *"#"* ]]; then subdir="${source#*#}" fi if [[ $DRY_RUN -eq 1 ]]; then run_cmd git clone --depth 1 "$repo_url" /tmp/extension-repo echo "[dry-run] resolve extension directory for $uuid (subdir: ${subdir:-auto})" run_cmd mkdir -p "${SYSTEM_EXT_DIR}/${uuid}" run_cmd cp -a /tmp/extension-repo/. "${SYSTEM_EXT_DIR}/${uuid}/" normalize_extension_permissions "${SYSTEM_EXT_DIR}/${uuid}" echo "System-wide extension ensured: $uuid" return 0 fi work_dir="$(mktemp -d)" repo_dir="$work_dir/repo" git clone --depth 1 "$repo_url" "$repo_dir" >/dev/null 2>&1 ext_dir="$(resolve_extension_dir_from_tree "$repo_dir" "$uuid" "$subdir")" copy_dir_into_system_extensions "$ext_dir" "$uuid" rm -rf "$work_dir" } install_extension() { local uuid="$1" local source="$2" local target_dir="${SYSTEM_EXT_DIR}/${uuid}" if [[ -f "$target_dir/metadata.json" && $REFRESH_EXISTING -eq 0 ]]; then normalize_extension_permissions "$target_dir" echo "System-wide extension already present: $uuid" return 0 fi if is_zip_source "$source"; then install_from_zip_source "$uuid" "$source" return fi if is_url_source "$source"; then install_from_git_source "$uuid" "$source" return fi echo "Unsupported source for $uuid: $source" >&2 return 1 } load_extensions_file() { local file="$1" local line local lineno=0 local uuid local source if [[ ! -f "$file" ]]; then echo "Missing extensions file: $file" >&2 exit 1 fi EXTENSIONS_BASE_DIR="$(cd "$(dirname "$file")" && pwd)" while IFS= read -r line || [[ -n "$line" ]]; do lineno=$((lineno + 1)) line="${line%%$'\r'}" [[ -z "$line" ]] && continue [[ "${line:0:1}" == "#" ]] && continue uuid="${line%%[[:space:]]*}" source="${line#"$uuid"}" source="$(echo "$source" | sed 's/^[[:space:]]*//')" if [[ -z "$uuid" || -z "$source" || "$uuid" == "$line" ]]; then echo "Invalid line ${lineno} in ${file}: expected ' '" >&2 exit 1 fi EXTENSION_UUIDS+=("$uuid") EXTENSION_SOURCES+=("$source") done < "$file" if [[ ${#EXTENSION_UUIDS[@]} -eq 0 ]]; then echo "No extensions loaded from ${file}" >&2 exit 1 fi echo "Loaded ${#EXTENSION_UUIDS[@]} extension entries from ${file}" } install_system_wide_extensions() { local i local failures=0 for i in "${!EXTENSION_UUIDS[@]}"; do if ! install_extension "${EXTENSION_UUIDS[$i]}" "${EXTENSION_SOURCES[$i]}"; then echo "Failed to install ${EXTENSION_UUIDS[$i]} from ${EXTENSION_SOURCES[$i]}" >&2 failures=$((failures + 1)) fi done if [[ $failures -gt 0 ]]; then echo "System-wide install failed for ${failures} extension(s)." >&2 return 1 fi } json_array_from_uuids() { local out="[" local first=1 local uuid for uuid in "${EXTENSION_UUIDS[@]}"; do if [[ $first -eq 0 ]]; then out+=", " fi out+="'${uuid}'" first=0 done out+="]" printf '%s' "$out" } apply_dconf_defaults() { local extensions_variant extensions_variant="$(json_array_from_uuids)" if [[ -f "$DCONF_PROFILE_FILE" ]]; then if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] ensure ${DCONF_PROFILE_FILE} contains system-db:local" elif ! rg -q '^system-db:local$' "$DCONF_PROFILE_FILE"; then printf '%s\n' 'system-db:local' >> "$DCONF_PROFILE_FILE" fi else write_file "$DCONF_PROFILE_FILE" $'user-db:user\nsystem-db:local' fi write_file "$DCONF_DEFAULTS_FILE" "[org/gnome/shell] enabled-extensions=${extensions_variant} disable-user-extensions=false" if [[ $LOCK_DEFAULT -eq 1 ]]; then write_file "$DCONF_LOCKS_FILE" $'/org/gnome/shell/enabled-extensions\n/org/gnome/shell/disable-user-extensions' fi run_cmd dconf update } apply_user_settings() { local extensions_variant extensions_variant="$(json_array_from_uuids)" local failures=0 local user local uuid for user in "${USERS[@]}"; do if ! id "$user" >/dev/null 2>&1; then echo "Skipping unknown user: $user" continue fi if run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gsettings set org.gnome.shell enabled-extensions "$extensions_variant" \ && run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gsettings set org.gnome.shell disable-user-extensions false; then echo "Applied extension list for user: $user" else echo "Primary gsettings path failed for user: $user, trying dconf fallback" if run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- dconf write /org/gnome/shell/enabled-extensions "$extensions_variant" \ && run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- dconf write /org/gnome/shell/disable-user-extensions false; then echo "Applied extension list for user via dconf fallback: $user" else echo "Failed to apply extension list for user: $user" >&2 failures=$((failures + 1)) fi fi for uuid in "${EXTENSION_UUIDS[@]}"; do run_cmd_may_fail runuser -u "$user" -- dbus-run-session -- gnome-extensions enable "$uuid" >/dev/null 2>&1 || true done done if [[ $failures -gt 0 ]]; then echo "One or more user updates failed (${failures})." >&2 return 1 fi } parse_args "$@" ensure_root load_extensions_file "$EXTENSIONS_FILE" install_system_wide_extensions apply_dconf_defaults apply_user_settings echo "System-wide extension install complete, defaults configured for new users, settings applied to selected existing users." echo "Users should log out and back in once to ensure GNOME Shell loads the new defaults cleanly."