#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly INVOCATION_DIR="$PWD" usage() { printf 'Usage: %s [-v|--version ] [-p|--prefix ] [-o|--options ]\n' "$0" printf ' %s [-p|--prefix ] [-o|--options ] \n' "$0" printf ' %s --help | -h\n' "$0" } 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 SOURCE_MANIFEST="$SOURCES_DIR/.source-manifest.tsv" readonly STAGING_DIR="$SCRIPT_DIR/tmp" readonly PROCESS_LOG="$INVOCATION_DIR/postgresql-$POSTGRES_VERSION-process.log" 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 SOURCE_MANIFEST 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 exec > >(tee "$PROCESS_LOG") 2>&1 printf 'Installation started: %s\n' "$(date -Is)" printf 'Invocation directory: %s\n' "$INVOCATION_DIR" printf 'PostgreSQL version: %s\n' "$POSTGRES_VERSION" printf 'Install prefix: %s\n' "$PREFIX" printf 'PostgreSQL configure options: %s\n' "${POSTGRES_CONFIGURE_OPTIONS:-}" printf 'Recreating source archive directory: %s\n' "$SOURCES_DIR" rm -rf -- "$SOURCES_DIR" mkdir -p "$SOURCES_DIR" : > "$SOURCE_MANIFEST" "$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_auto_explain.sh" --version "$POSTGRES_VERSION" "$SCRIPT_DIR/install_passwordcheck.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" "$SCRIPT_DIR/install_hypopg.sh" --version "$POSTGRES_VERSION" "$SCRIPT_DIR/install_pg_hint_plan.sh" --version "$POSTGRES_VERSION" "$SCRIPT_DIR/install_wal2json.sh" --version "$POSTGRES_VERSION" readonly CONFIG_FILE="${CONFIG_FILE:-}" readonly SHARED_PRELOAD_LIBRARIES="pg_cron,timescaledb,pgaudit,pg_stat_statements,pg_stat_monitor,passwordcheck" 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", "passwordcheck") 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 [[ -s "$SOURCE_MANIFEST" ]] || { printf 'ERROR: source download manifest is empty: %s\n' "$SOURCE_MANIFEST" >&2 exit 1 } printf '\nSource downloads:\n' while IFS=$'\t' read -r product source_url git_tag; do [[ -n "$product" && -n "$source_url" ]] || { printf 'ERROR: invalid source download record in %s\n' "$SOURCE_MANIFEST" >&2 exit 1 } printf ' %s\n Source URL: %s\n' "$product" "$source_url" if [[ -n "$git_tag" ]]; then printf ' Git tag: %s\n' "$git_tag" else printf ' Git tag: not applicable (PostgreSQL source archive)\n' fi done < "$SOURCE_MANIFEST" 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 //')" printf ' PostgreSQL configure options: %s\n' "${POSTGRES_CONFIGURE_OPTIONS//,/ }" 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 ' auto_explain: PostgreSQL %s contrib module\n' "$POSTGRES_VERSION" printf ' passwordcheck: PostgreSQL %s contrib module\n' "$POSTGRES_VERSION" printf ' pg_repack: %s\n' "$(extension_version pg_repack)" printf ' pg_stat_monitor: %s\n' "$(extension_version pg_stat_monitor)" printf ' pgvector: %s\n' "$(extension_version vector)" printf ' hypopg: %s\n' "$(extension_version hypopg)" printf ' pg_hint_plan: %s\n' "$(extension_version pg_hint_plan)" printf ' wal2json: logical decoding output plugin\n' printf " shared_preload_libraries = '%s'\n" "$SHARED_PRELOAD_LIBRARIES" rm -f -- "$ENGINE_ARCHIVE" tar -czf "$ENGINE_ARCHIVE" -C "$(dirname -- "$PREFIX")" "$(basename -- "$PREFIX")" printf '\nCreated engine archive: %s\n' "$ENGINE_ARCHIVE" printf 'Archive size: %s\n' "$(du -h "$ENGINE_ARCHIVE" | awk '{print $1}')" 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" printf 'Installation completed: %s\n' "$(date -Is)" exit 0