462 lines
11 KiB
Bash
Executable File
462 lines
11 KiB
Bash
Executable File
#!/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 <<USAGE
|
|
Usage: sudo bash scripts/apply-extension-defaults.sh [options]
|
|
|
|
Options:
|
|
--extensions-file PATH Extensions list file (default: scripts/extensions.txt)
|
|
--users user1,user2 Override target users (default: kari,owen,romy)
|
|
--lock-default Lock dconf defaults
|
|
--refresh-existing Reinstall even if extension dir already exists
|
|
--dry-run Print actions without writing changes
|
|
-h, --help Show help
|
|
|
|
extensions.txt format:
|
|
<uuid> <source>
|
|
|
|
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 '<uuid> <source>'" >&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."
|