diff --git a/nix/ext/hypopg.nix b/nix/ext/hypopg.nix index d95a1c7f71..9bd4927263 100644 --- a/nix/ext/hypopg.nix +++ b/nix/ext/hypopg.nix @@ -98,6 +98,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/index_advisor.nix b/nix/ext/index_advisor.nix index 3130d4bcc2..5892127142 100644 --- a/nix/ext/index_advisor.nix +++ b/nix/ext/index_advisor.nix @@ -83,6 +83,7 @@ pkgs.buildEnv { ]; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pg-safeupdate.nix b/nix/ext/pg-safeupdate.nix index 168772ce23..452e9c2c5e 100644 --- a/nix/ext/pg-safeupdate.nix +++ b/nix/ext/pg-safeupdate.nix @@ -83,6 +83,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pg_cron/default.nix b/nix/ext/pg_cron/default.nix index a823fa450b..4450f8e6bb 100644 --- a/nix/ext/pg_cron/default.nix +++ b/nix/ext/pg_cron/default.nix @@ -116,6 +116,7 @@ buildEnv { }; passthru = { + perVersion = lib.mapAttrs (name: value: build name value) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit switch-ext-version latestOnly; diff --git a/nix/ext/pg_net.nix b/nix/ext/pg_net.nix index 87b994a5ef..f769bad314 100644 --- a/nix/ext/pg_net.nix +++ b/nix/ext/pg_net.nix @@ -145,6 +145,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pg_partman.nix b/nix/ext/pg_partman.nix index 4c36e765fc..0c8a4eee6e 100644 --- a/nix/ext/pg_partman.nix +++ b/nix/ext/pg_partman.nix @@ -100,6 +100,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit switch-ext-version libName latestOnly; diff --git a/nix/ext/pg_repack.nix b/nix/ext/pg_repack.nix index 1a7500ea27..78d9764063 100644 --- a/nix/ext/pg_repack.nix +++ b/nix/ext/pg_repack.nix @@ -140,6 +140,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pg_tle.nix b/nix/ext/pg_tle.nix index 45ba670385..9c7ef86755 100644 --- a/nix/ext/pg_tle.nix +++ b/nix/ext/pg_tle.nix @@ -111,6 +111,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgaudit.nix b/nix/ext/pgaudit.nix index 74429fa415..8fb727c666 100644 --- a/nix/ext/pgaudit.nix +++ b/nix/ext/pgaudit.nix @@ -240,6 +240,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgmq/default.nix b/nix/ext/pgmq/default.nix index c12531f2d6..7e2076a2f5 100644 --- a/nix/ext/pgmq/default.nix +++ b/nix/ext/pgmq/default.nix @@ -103,6 +103,7 @@ buildEnv { pathsToLink = [ "/share/postgresql/extension" ]; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgroonga/default.nix b/nix/ext/pgroonga/default.nix index 42345bd49a..b9c3829a0d 100644 --- a/nix/ext/pgroonga/default.nix +++ b/nix/ext/pgroonga/default.nix @@ -181,6 +181,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgrouting/default.nix b/nix/ext/pgrouting/default.nix index 87d190cdc7..cc00281e70 100644 --- a/nix/ext/pgrouting/default.nix +++ b/nix/ext/pgrouting/default.nix @@ -153,6 +153,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgsodium.nix b/nix/ext/pgsodium.nix index 5a615a8722..b5a2dcb72c 100644 --- a/nix/ext/pgsodium.nix +++ b/nix/ext/pgsodium.nix @@ -112,6 +112,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgsql-http.nix b/nix/ext/pgsql-http.nix index 5815f91607..885fcc472e 100644 --- a/nix/ext/pgsql-http.nix +++ b/nix/ext/pgsql-http.nix @@ -113,6 +113,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/pgvector.nix b/nix/ext/pgvector.nix index 5f613b3d52..2db9d0c123 100644 --- a/nix/ext/pgvector.nix +++ b/nix/ext/pgvector.nix @@ -96,6 +96,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/plv8/default.nix b/nix/ext/plv8/default.nix index 93a17434a1..731991937c 100644 --- a/nix/ext/plv8/default.nix +++ b/nix/ext/plv8/default.nix @@ -238,6 +238,7 @@ buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/postgis.nix b/nix/ext/postgis.nix index 99afea6f5b..3f722d9ad9 100644 --- a/nix/ext/postgis.nix +++ b/nix/ext/postgis.nix @@ -256,6 +256,7 @@ in ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/ext/vault.nix b/nix/ext/vault.nix index 18ffe0aa77..9ab5391b1d 100644 --- a/nix/ext/vault.nix +++ b/nix/ext/vault.nix @@ -99,6 +99,7 @@ pkgs.buildEnv { ''; passthru = { + perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse; versions = versionsBuilt; numberOfVersions = numberOfVersionsBuilt; inherit pname latestOnly; diff --git a/nix/packages/default.nix b/nix/packages/default.nix index dc6268b3a0..f8b9aa92a9 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -3,8 +3,8 @@ imports = [ ./postgres.nix ./site-env.nix + ./extension-catalog.nix ]; - perSystem = { inputs', diff --git a/nix/packages/extension-catalog.nix b/nix/packages/extension-catalog.nix new file mode 100644 index 0000000000..d30597850b --- /dev/null +++ b/nix/packages/extension-catalog.nix @@ -0,0 +1,183 @@ +# Multi version extension catalog and update tools. +{ + perSystem = + { + pkgs, + lib, + self', + ... + }: + let + # Mirror a per-version package, adding unversioned control/library names + # only where missing — packages that already ship generic names (e.g. + # postgis and its `-3` convention) pass through untouched. + mkGenericVersion = + leanPkg: + pkgs.runCommand "${leanPkg.pname}-${leanPkg.version}-generic" { } '' + set -euo pipefail + shopt -s nullglob + mkdir -p "$out/lib" "$out/share/postgresql/extension" + ext="$out/share/postgresql/extension" + + for f in ${leanPkg}/lib/*; do ln -s "$f" "$out/lib/$(basename "$f")"; done + for f in ${leanPkg}/share/postgresql/extension/*; do ln -s "$f" "$ext/$(basename "$f")"; done + + # .control with default_version, from --.control + for cf in "$ext"/*--*.control; do + b=$(basename "$cf") + n="''${b%%--*}" + v="''${b#*--}"; v="''${v%.control}" + [ -e "$ext/$n.control" ] && continue + { echo "default_version = '$v'"; cat "$cf"; } > "$ext/$n.control" + done + + # .so -> -.so + for f in "$out"/lib/*-[0-9]*; do + b=$(basename "$f") + suffix=".''${b##*.}" + core="''${b%.*}" + base="''${core%-[0-9]*}" + [ "$base" != "$core" ] || continue + gen="$base$suffix" + [ -e "$out/lib/$gen" ] || ln -sfn "$b" "$out/lib/$gen" + done + + # Sanity check. No generic control is fine only for preload-only + # modules (e.g. pg-safeupdate), which still ship a library. + generic=0 + for c in "$ext"/*.control; do + case "$(basename "$c")" in + *--*) ;; + *) generic=$((generic + 1)) ;; + esac + done + libs=$(ls -A "$out/lib" 2>/dev/null | wc -l) + if [ "$generic" -eq 0 ] && [ "$libs" -eq 0 ]; then + echo "extension-catalog: empty wrapper for ${leanPkg.pname} (no control, no lib)" >&2 + exit 1 + fi + ''; + + wrappersFor = + extsSet: + lib.mapAttrs (_: drv: lib.mapAttrs (_: mkGenericVersion) drv.perVersion) ( + lib.filterAttrs (_: drv: lib.isDerivation drv && drv ? perVersion) extsSet + ); + + # Make catalog json from a set of wrappers: { : { : } } + # Keyed on each wrapper's actual default_version, not the nix attr name, + # which can differ (pgsql-http "1.5.0" builds extversion "1.5"). + mkCatalogFile = + wrappers: + pkgs.runCommand "pg-extensions-catalog" + { + paths = map toString (lib.concatMap lib.attrValues (lib.attrValues wrappers)); + nativeBuildInputs = [ pkgs.jq ]; + # Without this the literal wrapper paths in the JSON would pull + # every wrapper's closure into the catalog's runtime closure. + __structuredAttrs = true; + unsafeDiscardReferences.out = true; + } + '' + set -euo pipefail + obj='{}' + for path in "''${paths[@]}"; do + for ctrl in "$path"/share/postgresql/extension/*.control; do + base=$(basename "$ctrl") + case "$base" in *--*) continue ;; esac + name="''${base%.control}" + ver=$(sed -n "s/^default_version = '\(.*\)'.*/\1/p" "$ctrl" | head -1) + [ -n "$ver" ] || { echo "no default_version in $ctrl" >&2; exit 1; } + obj=$(printf '%s' "$obj" | jq --arg e "$name" --arg v "$ver" --arg p "$path" '.[$e][$v] = $p') + done + done + mkdir -p "$out/share" + printf '%s\n' "$obj" | jq -S . > "$out/share/pg-extensions-catalog.json" + ''; + + script = + name: text: + pkgs.writeShellApplication { + inherit name text; + runtimeInputs = [ + pkgs.coreutils + pkgs.jq + ]; + }; + + # Multi version extensions as attrsets: { = { = { = drv; }; }; } + perMajor = lib.genAttrs [ "15" "17" "orioledb-17" ] ( + major: wrappersFor self'.legacyPackages."psql_${major}".exts + ); + + # Catalog json plus bin/site-extensions-{resolve,update} that use it by default. + catalogs = lib.mapAttrs' ( + major: wrappers: + lib.nameValuePair "site-extensions-catalog-${major}" ( + pkgs.runCommand "site-extensions-catalog" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' + mkdir -p "$out/share" "$out/bin" + ln -s ${mkCatalogFile wrappers}/share/pg-extensions-catalog.json "$out/share/" + makeWrapper ${self'.packages.site-extensions-resolve}/bin/site-extensions-resolve \ + "$out/bin/site-extensions-resolve" \ + --set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json" + makeWrapper ${self'.packages.site-extensions-update}/bin/site-extensions-update \ + "$out/bin/site-extensions-update" \ + --set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json" + '' + ) + ) perMajor; + + versions = lib.mapAttrs' ( + major: wrappers: + lib.nameValuePair "site-extensions-versions-${major}" ( + lib.recurseIntoAttrs (lib.mapAttrs (_: lib.recurseIntoAttrs) wrappers) + ) + ) perMajor; + in + { + packages = catalogs // { + # Takes store paths as args and substitutes from binary cache, with retry. + download-nix-store-paths = script "download-nix-store-paths" '' + for path in "$@"; do + for attempt in 1 2 3; do + timeout -k 10s 120s nix-store -r "$path" >/dev/null && continue 2 + echo "WARNING: attempt $attempt failed for $path" >&2 + done + echo "ERROR: failed to realize $path" >&2 + exit 1 + done + ''; + + # Takes manifest json as argument. Format: {: }. + # Prints nix-store paths of the resolved extensions, one per line. + # Does not download or install. + site-extensions-resolve = script "site-extensions-resolve" '' + : "''${PG_EXTENSIONS_CATALOG:?PG_EXTENSIONS_CATALOG must point at a pg-extensions-catalog.json}" + manifest="''${1:?Usage: $0 path-to/pg-extensions.json}" + jq -r 'to_entries[] | "\(.key)=\(.value)"' "$manifest" | while IFS='=' read -r name version; do + [ -n "$name" ] || continue + jq -er --arg n "$name" --arg v "$version" \ + '.[$n][$v] // error("\($n)=\($v) not in catalog")' "$PG_EXTENSIONS_CATALOG" + done | sort -u + ''; + + # Takes manifest json as argument. + # Downloads paths and installs them as an env into the profile, replacing all existing ones. + site-extensions-update = pkgs.writeShellApplication { + name = "site-extensions-update"; + runtimeInputs = [ + self'.packages.site-extensions-resolve + self'.packages.download-nix-store-paths + ]; + text = '' + manifest="''${1:-/etc/adminapi/pg-extensions.json}" + profile="''${PROFILE:-/nix/var/nix/profiles/site-extensions}" + mapfile -t paths < <(site-extensions-resolve "$manifest") + download-nix-store-paths "''${paths[@]}" + nix-env --profile "$profile" --install "''${paths[@]}" --remove-all + ''; + }; + }; + legacyPackages = catalogs // versions; + }; +}