2026-09-23 17:19:50 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
|
|
set -Eeuo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
readonly INVOCATION_DIR="$PWD"
|
|
|
|
|
|
2026-10-11 16:45:57 +02:00
|
|
|
usage() {
|
|
|
|
|
printf 'Usage: %s [-v|--version <postgresql-version>] [-p|--prefix <path>] [-o|--options <configure-options>]\n' "$0"
|
|
|
|
|
printf ' %s [-p|--prefix <path>] [-o|--options <configure-options>] <postgresql-version>\n' "$0"
|
|
|
|
|
printf ' %s --help | -h\n' "$0"
|
2026-09-23 17:19:50 +02:00
|
|
|
}
|
2026-10-11 16:45:57 +02:00
|
|
|
|
|
|
|
|
for argument in "$@"; do
|
|
|
|
|
if [[ "$argument" == "--help" || "$argument" == "-h" ]]; then
|
|
|
|
|
usage
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
POSTGRES_VERSION=""
|
|
|
|
|
PREFIX_OVERRIDE=""
|
|
|
|
|
CONFIGURE_OPTIONS="--with-ssl=openssl,--with-lz4"
|
|
|
|
|
while (($#)); do
|
|
|
|
|
case "$1" in
|
|
|
|
|
-p|--prefix)
|
|
|
|
|
(($# >= 2)) || { printf 'ERROR: %s requires a path\n' "$1" >&2; exit 2; }
|
|
|
|
|
[[ -n "$2" ]] || { printf 'ERROR: prefix path must not be empty\n' >&2; exit 2; }
|
|
|
|
|
PREFIX_OVERRIDE="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
|
|
|
|
-v|--version)
|
|
|
|
|
(($# >= 2)) || { printf 'ERROR: %s requires a PostgreSQL version\n' "$1" >&2; exit 2; }
|
|
|
|
|
[[ -n "$2" ]] || { printf 'ERROR: PostgreSQL version must not be empty\n' >&2; exit 2; }
|
|
|
|
|
[[ -z "$POSTGRES_VERSION" ]] || { printf 'ERROR: PostgreSQL version was specified more than once\n' >&2; exit 2; }
|
|
|
|
|
POSTGRES_VERSION="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
|
|
|
|
--prefix=*)
|
|
|
|
|
PREFIX_OVERRIDE="${1#*=}"
|
|
|
|
|
[[ -n "$PREFIX_OVERRIDE" ]] || { printf 'ERROR: prefix path must not be empty\n' >&2; exit 2; }
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
--version=*)
|
|
|
|
|
[[ -n "${1#*=}" ]] || { printf 'ERROR: PostgreSQL version must not be empty\n' >&2; exit 2; }
|
|
|
|
|
[[ -z "$POSTGRES_VERSION" ]] || { printf 'ERROR: PostgreSQL version was specified more than once\n' >&2; exit 2; }
|
|
|
|
|
POSTGRES_VERSION="${1#*=}"
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
-o|--options)
|
|
|
|
|
(($# >= 2)) || { printf 'ERROR: %s requires a comma-separated option list\n' "$1" >&2; exit 2; }
|
|
|
|
|
CONFIGURE_OPTIONS="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
|
|
|
|
--options=*)
|
|
|
|
|
CONFIGURE_OPTIONS="${1#*=}"
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
-*)
|
|
|
|
|
printf 'ERROR: unknown option: %s\n' "$1" >&2
|
|
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
[[ -z "$POSTGRES_VERSION" ]] || { printf 'ERROR: unexpected argument: %s\n' "$1" >&2; usage >&2; exit 2; }
|
|
|
|
|
POSTGRES_VERSION="$1"
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
[[ -n "$POSTGRES_VERSION" ]] || { usage >&2; exit 2; }
|
|
|
|
|
[[ "$POSTGRES_VERSION" =~ ^[0-9]+\.[0-9]+([.][0-9]+)?$ ]] || {
|
|
|
|
|
printf 'ERROR: invalid PostgreSQL version: %s\n' "$POSTGRES_VERSION" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
SECONDS=0
|
|
|
|
|
|
|
|
|
|
readonly POSTGRES_ROOT="${POSTGRES_ROOT:-/app/postgres/rdbms}"
|
|
|
|
|
PREFIX="${PREFIX_OVERRIDE:-$POSTGRES_ROOT/$POSTGRES_VERSION}"
|
|
|
|
|
readonly POSTGRES_CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS"
|
|
|
|
|
[[ "$PREFIX" == /* && "$PREFIX" != "/" && "$PREFIX" != "$POSTGRES_ROOT" ]] || {
|
|
|
|
|
printf 'ERROR: prefix must be an absolute version-specific directory, not / or POSTGRES_ROOT itself: %s\n' "$PREFIX" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
readonly NORMALIZED_PREFIX="$(realpath -m -- "$PREFIX")"
|
|
|
|
|
readonly NORMALIZED_POSTGRES_ROOT="$(realpath -m -- "$POSTGRES_ROOT")"
|
|
|
|
|
[[ "$NORMALIZED_PREFIX" != "/" && "$NORMALIZED_PREFIX" != "$NORMALIZED_POSTGRES_ROOT" ]] || {
|
|
|
|
|
printf 'ERROR: prefix must not resolve to / or POSTGRES_ROOT: %s\n' "$PREFIX" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
readonly PREFIX="$NORMALIZED_PREFIX"
|
|
|
|
|
if [[ -n "$POSTGRES_CONFIGURE_OPTIONS" ]]; then
|
|
|
|
|
[[ "$POSTGRES_CONFIGURE_OPTIONS" != ,* &&
|
|
|
|
|
"$POSTGRES_CONFIGURE_OPTIONS" != *, &&
|
|
|
|
|
"$POSTGRES_CONFIGURE_OPTIONS" != *,,* ]] || {
|
|
|
|
|
printf 'ERROR: configure options must be a non-empty comma-separated list\n' >&2
|
|
|
|
|
exit 2
|
|
|
|
|
}
|
|
|
|
|
IFS=',' read -r -a parsed_configure_options <<< "$POSTGRES_CONFIGURE_OPTIONS"
|
|
|
|
|
for option in "${parsed_configure_options[@]}"; do
|
|
|
|
|
[[ "$option" == -* && "$option" != "-" ]] || {
|
|
|
|
|
printf 'ERROR: invalid PostgreSQL configure option: %s\n' "$option" >&2
|
|
|
|
|
exit 2
|
|
|
|
|
}
|
|
|
|
|
done
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
readonly PREFIX_VERSION="$(basename -- "$PREFIX")"
|
|
|
|
|
readonly SOURCES_DIR="$SCRIPT_DIR/sources/$PREFIX_VERSION"
|
|
|
|
|
readonly STAGING_DIR="$SCRIPT_DIR/tmp"
|
2026-09-23 17:19:50 +02:00
|
|
|
readonly PROCESS_LOG="$INVOCATION_DIR/postgresql-$POSTGRES_VERSION-process.log"
|
2026-10-11 16:45:57 +02:00
|
|
|
readonly ENGINE_ARCHIVE="$INVOCATION_DIR/postgresql-$PREFIX_VERSION-engine.tar.gz"
|
|
|
|
|
|
|
|
|
|
[[ "$STAGING_DIR" != "/" ]] || {
|
|
|
|
|
printf 'ERROR: staging directory must not be the filesystem root\n' >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
rm -rf -- "$STAGING_DIR"
|
|
|
|
|
mkdir -p "$STAGING_DIR"
|
|
|
|
|
|
|
|
|
|
export PREFIX POSTGRES_CONFIGURE_OPTIONS
|
|
|
|
|
|
|
|
|
|
if [[ -e "$PREFIX" || -L "$PREFIX" ]]; then
|
|
|
|
|
printf 'WARNING: PostgreSQL installation directory already exists: %s\n' "$PREFIX" >&2
|
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
|
|
|
printf 'Non-interactive input; defaulting to No and exiting without changes.\n' >&2
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
read -r -p "Recreate this directory and continue? [y/N] " response || response=""
|
|
|
|
|
case "$response" in
|
|
|
|
|
Y|y) printf 'Confirmed: %s will be recreated.\n' "$PREFIX" ;;
|
|
|
|
|
*) printf 'Installation cancelled; existing directory was left unchanged.\n'; exit 0 ;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
2026-09-23 17:19:50 +02:00
|
|
|
|
|
|
|
|
exec > >(tee "$PROCESS_LOG") 2>&1
|
|
|
|
|
|
|
|
|
|
printf 'Installation started: %s\n' "$(date -Is)"
|
|
|
|
|
printf 'Invocation directory: %s\n' "$INVOCATION_DIR"
|
2026-10-11 16:45:57 +02:00
|
|
|
printf 'PostgreSQL version: %s\n' "$POSTGRES_VERSION"
|
|
|
|
|
printf 'Install prefix: %s\n' "$PREFIX"
|
|
|
|
|
printf 'PostgreSQL configure options: %s\n' "${POSTGRES_CONFIGURE_OPTIONS:-<none>}"
|
|
|
|
|
printf 'Recreating source archive directory: %s\n' "$SOURCES_DIR"
|
|
|
|
|
rm -rf -- "$SOURCES_DIR"
|
|
|
|
|
mkdir -p "$SOURCES_DIR"
|
2026-09-23 17:19:50 +02:00
|
|
|
|
2026-10-11 16:45:57 +02:00
|
|
|
"$SCRIPT_DIR/install_postgresql.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pg_cron.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_timescaledb.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pgaudit.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_set_user.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pg_stat_statements.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pg_repack.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pg_stat_monitor.sh" --version "$POSTGRES_VERSION"
|
|
|
|
|
"$SCRIPT_DIR/install_pgvector.sh" --version "$POSTGRES_VERSION"
|
2026-09-23 17:19:50 +02:00
|
|
|
|
|
|
|
|
readonly CONFIG_FILE="${CONFIG_FILE:-}"
|
|
|
|
|
readonly SHARED_PRELOAD_LIBRARIES="pg_cron,timescaledb,pgaudit,pg_stat_statements,pg_stat_monitor"
|
|
|
|
|
|
|
|
|
|
if [[ -n "$CONFIG_FILE" ]]; then
|
|
|
|
|
[[ -f "$CONFIG_FILE" ]] || {
|
|
|
|
|
printf 'ERROR: configuration file does not exist: %s\n' "$CONFIG_FILE" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
python3 - "$CONFIG_FILE" <<'PY'
|
|
|
|
|
import pathlib
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
|
|
|
text = path.read_text()
|
|
|
|
|
pattern = r"(?m)^[ \t]*(#?)[ \t]*shared_preload_libraries[ \t]*=[ \t]*'([^']*)'[^\n]*$"
|
|
|
|
|
match = re.search(pattern, text)
|
|
|
|
|
required = ("pg_cron", "timescaledb", "pgaudit", "pg_stat_statements", "pg_stat_monitor")
|
|
|
|
|
if match:
|
|
|
|
|
libraries = [item.strip() for item in match.group(2).split(",") if item.strip()]
|
|
|
|
|
for library in required:
|
|
|
|
|
if library not in libraries:
|
|
|
|
|
libraries.append(library)
|
|
|
|
|
line = "shared_preload_libraries = '" + ",".join(libraries) + "'\\t# (change requires restart)"
|
|
|
|
|
text = re.sub(pattern, line, text, count=1)
|
|
|
|
|
else:
|
|
|
|
|
text = text.rstrip("\n") + "\n\nshared_preload_libraries = '" + ",".join(required) + "'\\t# (change requires restart)\n"
|
|
|
|
|
path.write_text(text)
|
|
|
|
|
PY
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
printf 'Installed PostgreSQL and extensions in %s\n' "$PREFIX"
|
|
|
|
|
if [[ -n "$CONFIG_FILE" ]]; then
|
|
|
|
|
grep -E '^[[:space:]]*shared_preload_libraries[[:space:]]*=' "$CONFIG_FILE"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
extension_version() {
|
|
|
|
|
local extension="$1"
|
|
|
|
|
awk -F"'" '/^[[:space:]]*default_version[[:space:]]*=/{print $2; exit}' \
|
|
|
|
|
"$PREFIX/share/extension/$extension.control"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf '\nInstalled products and extensions:\n'
|
|
|
|
|
printf ' PostgreSQL: %s\n' "$("$PREFIX/bin/pg_config" --version | sed 's/^PostgreSQL //')"
|
2026-10-11 16:45:57 +02:00
|
|
|
printf ' PostgreSQL configure options: %s\n' "${POSTGRES_CONFIGURE_OPTIONS//,/ }"
|
2026-09-23 17:19:50 +02:00
|
|
|
printf ' pg_cron: %s\n' "$(extension_version pg_cron)"
|
|
|
|
|
printf ' TimescaleDB: %s\n' "$(extension_version timescaledb)"
|
|
|
|
|
printf ' pgaudit: %s\n' "$(extension_version pgaudit)"
|
|
|
|
|
printf ' set_user: %s\n' "$(extension_version set_user)"
|
|
|
|
|
printf ' pg_stat_statements: %s\n' "$(extension_version pg_stat_statements)"
|
|
|
|
|
printf ' pg_repack: %s\n' "$(extension_version pg_repack)"
|
|
|
|
|
printf ' pg_stat_monitor: %s\n' "$(extension_version pg_stat_monitor)"
|
2026-10-11 09:00:59 +02:00
|
|
|
printf ' pgvector: %s\n' "$(extension_version vector)"
|
2026-09-23 17:19:50 +02:00
|
|
|
printf " shared_preload_libraries = '%s'\n" "$SHARED_PRELOAD_LIBRARIES"
|
|
|
|
|
|
|
|
|
|
rm -f -- "$ENGINE_ARCHIVE"
|
2026-10-11 16:45:57 +02:00
|
|
|
tar -czf "$ENGINE_ARCHIVE" -C "$(dirname -- "$PREFIX")" "$(basename -- "$PREFIX")"
|
2026-09-23 17:19:50 +02:00
|
|
|
printf '\nCreated engine archive: %s\n' "$ENGINE_ARCHIVE"
|
|
|
|
|
printf 'Archive size: %s\n' "$(du -h "$ENGINE_ARCHIVE" | awk '{print $1}')"
|
2026-10-11 16:45:57 +02:00
|
|
|
elapsed_seconds=$SECONDS
|
|
|
|
|
elapsed_hours=$((elapsed_seconds / 3600))
|
|
|
|
|
elapsed_minutes=$(((elapsed_seconds % 3600) / 60))
|
|
|
|
|
elapsed_remainder=$((elapsed_seconds % 60))
|
|
|
|
|
printf 'Total execution time: %dh %dm %ds\n' \
|
|
|
|
|
"$elapsed_hours" "$elapsed_minutes" "$elapsed_remainder"
|
2026-09-23 17:19:50 +02:00
|
|
|
printf 'Installation completed: %s\n' "$(date -Is)"
|
2026-10-11 16:45:57 +02:00
|
|
|
exit 0
|