diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b470d0e..bcf5de0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,6 +18,8 @@ jobs: with: nix_path: nixpkgs=channel:nixos-unstable - name: Run tests - run: nix-shell --run "with-pg-${{ matrix.pg-version }} make installcheck" + run: nix-shell --run "nxpg-${{ matrix.pg-version }} nxpg-tmp nxpg-test" - if: ${{ failure() }} - run: cat output/regression.diffs + run: | + cat regression.out + cat regression.diffs diff --git a/.gitignore b/.gitignore index 3ec0524..d865e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,12 @@ results/ .history *.so *.bc +*.gcda +*.gcno tags plmustache.control -sql/plmustache--*.sql +plmustache--*.sql +coverage.info +coverage_html +regression.diffs +regression.out diff --git a/Makefile b/Makefile index fd97b0c..4f276a8 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,32 @@ +## Our variables EXTENSION = plmustache EXTVERSION = 0.1 +SED ?= sed -DATA = $(wildcard sql/*--*.sql) +## PGXS variables +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) MODULE_big = $(EXTENSION) -OBJS = src/plmustache.o src/observation.o src/build.o +SRC = $(wildcard src/*.c) +OBJS = $(patsubst src/%.c, src/%.o, $(SRC)) +SHLIB_LINK = -lmustach +PG_CFLAGS = -std=c99 -Wno-declaration-after-statement -Wall -Werror -Wshadow TESTS = $(wildcard test/sql/*.sql) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS_OPTS = --inputdir=test -PG_CONFIG = pg_config -SHLIB_LINK = -lmustach - -PG_CFLAGS = -std=c99 -Wno-declaration-after-statement -Wall -Werror -Wshadow - -PGXS := $(shell $(PG_CONFIG) --pgxs) - -all: sql/$(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control +DATA = $(wildcard *--*.sql) -.PHONY: clean_generated -clean_generated: - rm -f $(EXTENSION).control - rm -f sql/$(EXTENSION)--$(EXTVERSION).sql +EXTRA_CLEAN = $(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control -# extra dep for clean target in pgxs.mk -clean: clean_generated +all: $(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control -sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql +$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql cp $< $@ $(EXTENSION).control: - sed "s/@EXTVERSION@/$(EXTVERSION)/g" $(EXTENSION).control.in > $(EXTENSION).control + $(SED) "s/@EXTVERSION@/$(EXTVERSION)/g" $(EXTENSION).control.in > $@ include $(PGXS) diff --git a/nix/nxpg.nix b/nix/nxpg.nix new file mode 100644 index 0000000..fea608d --- /dev/null +++ b/nix/nxpg.nix @@ -0,0 +1,85 @@ +{ writeShellScriptBin, findutils, entr, callPackage, postgresql_16, postgresql_15, postgresql_14, postgresql_13, postgresql_12 } : +let + prefix = "nxpg"; + supportedPgs = [ + postgresql_16 + postgresql_15 + postgresql_14 + postgresql_13 + postgresql_12 + ]; + build = + writeShellScriptBin "${prefix}-build" '' + set -euo pipefail + + make clean + make + ''; + test = + writeShellScriptBin "${prefix}-test" '' + set -euo pipefail + + make clean + make + make installcheck + ''; + + watch = + writeShellScriptBin "${prefix}-watch" '' + set -euo pipefail + + ${findutils}/bin/find . -type f \( -name '*.c' -o -name '*.h' \) | ${entr}/bin/entr -dr "$@" + ''; + + tmpDb = + writeShellScriptBin "${prefix}-tmp" '' + set -euo pipefail + + export tmpdir="$(mktemp -d)" + + export PGDATA="$tmpdir" + export PGHOST="$tmpdir" + export PGUSER=postgres + export PGDATABASE=postgres + + trap 'pg_ctl stop -m i && rm -rf "$tmpdir"' sigint sigterm exit + + PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" + + # pg versions older than 16 don't support adding "-c" to initdb to add these options + # so we just modify the resulting postgresql.conf to avoid an error + echo "dynamic_library_path='\$libdir:$(pwd)'" >> $PGDATA/postgresql.conf + echo "extension_control_path='\$system:$(pwd)'" >> $PGDATA/postgresql.conf + + default_options="-F -c listen_addresses=\"\" -k $PGDATA" + + pg_ctl start -o "$default_options" + + "$@" + ''; + allPgPaths = map (pg: + let + ver = builtins.head (builtins.splitVersion pg.version); + patchedPg = pg.overrideAttrs(oldAttrs: { + patches = oldAttrs.patches ++ [ + ./patches/${ver}-add-extension_control_path-for.patch + ]; + }); + script = '' + set -euo pipefail + + export PATH=${patchedPg}/bin:"$PATH" + + "$@" + ''; + in + writeShellScriptBin "${prefix}-${ver}" script + ) supportedPgs; +in +[ + build + test + watch + tmpDb + allPgPaths +] diff --git a/nix/patches/12-add-extension_control_path-for.patch b/nix/patches/12-add-extension_control_path-for.patch new file mode 100644 index 0000000..f05e208 --- /dev/null +++ b/nix/patches/12-add-extension_control_path-for.patch @@ -0,0 +1,458 @@ +From 96fa4795cc36376f30f597945d7627cb0caa90ab Mon Sep 17 00:00:00 2001 +From: steve-chavez +Date: Mon, 3 Feb 2025 16:53:35 -0500 +Subject: [PATCH] add extension_control_path for 12 + +--- + src/Makefile.global.in | 19 +++-- + src/backend/commands/extension.c | 70 ++++++++++++++-- + src/backend/utils/fmgr/dfmgr.c | 80 ++++++++++++------- + src/backend/utils/misc/guc.c | 13 +++ + src/backend/utils/misc/postgresql.conf.sample | 1 + + src/include/commands/extension.h | 2 + + src/include/fmgr.h | 3 + + 7 files changed, 143 insertions(+), 45 deletions(-) + +diff --git a/src/Makefile.global.in b/src/Makefile.global.in +index 3aa0bdd6cc0..3ef89b1f6a2 100644 +--- a/src/Makefile.global.in ++++ b/src/Makefile.global.in +@@ -88,9 +88,19 @@ configure_args = @configure_args@ + # + # In a PGXS build, we cannot use the values inserted into Makefile.global + # by configure, since the installation tree may have been relocated. +-# Instead get the path values from pg_config. ++# Instead get the path values from pg_config. But users can specify ++# prefix explicitly, if they want to select their own installation ++# location. + +-ifndef PGXS ++ifdef PGXS ++# Extension makefiles should set PG_CONFIG, but older ones might not ++ifndef PG_CONFIG ++PG_CONFIG = pg_config ++endif ++endif ++ ++# This means: if ((not PGXS) or prefix) ++ifneq (,$(if $(PGXS),,1)$(prefix)) + + # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; + # makefiles may only use the derived variables such as bindir. +@@ -148,11 +158,6 @@ localedir := @localedir@ + + else # PGXS case + +-# Extension makefiles should set PG_CONFIG, but older ones might not +-ifndef PG_CONFIG +-PG_CONFIG = pg_config +-endif +- + bindir := $(shell $(PG_CONFIG) --bindir) + datadir := $(shell $(PG_CONFIG) --sharedir) + sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) +diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c +index 0f9edf68c3a..9cf1555e083 100644 +--- a/src/backend/commands/extension.c ++++ b/src/backend/commands/extension.c +@@ -66,6 +66,9 @@ + #include "utils/varlena.h" + + ++/* GUC */ ++char *Extension_control_path; ++ + /* Globally visible state variables */ + bool creating_extension = false; + Oid CurrentExtensionObject = InvalidOid; +@@ -76,6 +79,7 @@ Oid CurrentExtensionObject = InvalidOid; + typedef struct ExtensionControlFile + { + char *name; /* name of the extension */ ++ char *control_dir; /* directory where control file was found */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ + char *module_pathname; /* string to substitute for +@@ -365,6 +369,12 @@ is_extension_script_filename(const char *filename) + return (extension != NULL) && (strcmp(extension, ".sql") == 0); + } + ++/* ++ * TODO ++ * ++ * This is now only for finding/listing available extensions. Rewrite to use ++ * path. See further TODOs below. ++ */ + static char * + get_extension_control_directory(void) + { +@@ -378,16 +388,45 @@ get_extension_control_directory(void) + return result; + } + ++/* ++ * Find control file for extension with name in control->name, looking in the ++ * path. Return the full file name, or NULL if not found. If found, the ++ * directory is recorded in control->control_dir. ++ */ + static char * +-get_extension_control_filename(const char *extname) ++find_extension_control_filename(ExtensionControlFile *control) + { + char sharepath[MAXPGPATH]; ++ char *system_dir; ++ char *basename; ++ char *ecp; + char *result; + ++ Assert(control->name); ++ + get_share_path(my_exec_path, sharepath); +- result = (char *) palloc(MAXPGPATH); +- snprintf(result, MAXPGPATH, "%s/extension/%s.control", +- sharepath, extname); ++ system_dir = psprintf("%s/extension", sharepath); ++ ++ basename = psprintf("%s.control", control->name); ++ ++ /* ++ * find_in_path() does nothing if the path value is empty. This is the ++ * historical behavior for dynamic_library_path, but it makes no sense for ++ * extensions. So in that case, substitute a default value. ++ */ ++ ecp = Extension_control_path; ++ if (strlen(ecp) == 0) ++ ecp = "$system"; ++ result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); ++ ++ if (result) ++ { ++ const char *p; ++ ++ p = strrchr(result, '/'); ++ Assert(p); ++ control->control_dir = pnstrdup(result, p - result); ++ } + + return result; + } +@@ -403,7 +442,7 @@ get_extension_script_directory(ExtensionControlFile *control) + * installation's share directory. + */ + if (!control->directory) +- return get_extension_control_directory(); ++ return pstrdup(control->control_dir); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); +@@ -481,16 +520,25 @@ parse_extension_control_file(ExtensionControlFile *control, + if (version) + filename = get_extension_aux_control_filename(control, version); + else +- filename = get_extension_control_filename(control->name); ++ filename = find_extension_control_filename(control); ++ ++ if (!filename) ++ { ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("extension \"%s\" is not available", control->name), ++ errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ } + + if ((file = AllocateFile(filename, "r")) == NULL) + { +- if (version && errno == ENOENT) ++ /* no complaint for missing auxiliary file */ ++ if (errno == ENOENT && version) + { +- /* no auxiliary file for this version */ + pfree(filename); + return; + } ++ + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open extension control file \"%s\": %m", +@@ -1917,6 +1965,8 @@ RemoveExtensionById(Oid extId) + * The system view pg_available_extensions provides a user interface to this + * SRF, adding information about whether the extensions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extensions(PG_FUNCTION_ARGS) +@@ -2026,6 +2076,8 @@ pg_available_extensions(PG_FUNCTION_ARGS) + * The system view pg_available_extension_versions provides a user interface + * to this SRF, adding information about which versions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extension_versions(PG_FUNCTION_ARGS) +@@ -2246,6 +2298,8 @@ convert_requires_to_datum(List *requires) + /* + * This function reports the version update paths that exist for the + * specified extension. ++ * ++ * TODO: make aware of path + */ + Datum + pg_extension_update_paths(PG_FUNCTION_ARGS) +diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c +index be684786d64..8c0efad14f4 100644 +--- a/src/backend/utils/fmgr/dfmgr.c ++++ b/src/backend/utils/fmgr/dfmgr.c +@@ -83,8 +83,7 @@ static void internal_unload_library(const char *libname); + static bool file_exists(const char *name); + static char *expand_dynamic_library_name(const char *name); + static void check_restricted_library_name(const char *name); +-static char *substitute_libpath_macro(const char *name); +-static char *find_in_dynamic_libpath(const char *basename); ++static char *substitute_path_macro(const char *str, const char *macro, const char *value); + + /* Magic structure that module needs to match to be accepted */ + static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; +@@ -485,7 +484,7 @@ file_exists(const char *name) + /* + * If name contains a slash, check if the file exists, if so return + * the name. Else (no slash) try to expand using search path (see +- * find_in_dynamic_libpath below); if that works, return the fully ++ * find_in_path below); if that works, return the fully + * expanded file name. If the previous failed, append DLSUFFIX and + * try again. If all fails, just return the original name. + * +@@ -500,17 +499,25 @@ expand_dynamic_library_name(const char *name) + + AssertArg(name); + ++ /* ++ * If the value starts with "$libdir/", strip that. This is because many ++ * extensions have hardcoded '$libdir/foo' as their library name, which ++ * prevents using the path. ++ */ ++ if (strncmp(name, "$libdir/", 8) == 0) ++ name += 8; ++ + have_slash = (first_dir_separator(name) != NULL); + + if (!have_slash) + { +- full = find_in_dynamic_libpath(name); ++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(name); ++ full = substitute_path_macro(name, "$libdir", pkglib_path); + if (file_exists(full)) + return full; + pfree(full); +@@ -520,14 +527,14 @@ expand_dynamic_library_name(const char *name) + + if (!have_slash) + { +- full = find_in_dynamic_libpath(new); ++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + pfree(new); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(new); ++ full = substitute_path_macro(new, "$libdir", pkglib_path); + pfree(new); + if (file_exists(full)) + return full; +@@ -562,47 +569,60 @@ check_restricted_library_name(const char *name) + * Result is always freshly palloc'd. + */ + static char * +-substitute_libpath_macro(const char *name) ++substitute_path_macro(const char *str, const char *macro, const char *value) + { + const char *sep_ptr; + +- AssertArg(name != NULL); ++ Assert(str != NULL); ++ Assert(macro[0] == '$'); + +- /* Currently, we only recognize $libdir at the start of the string */ +- if (name[0] != '$') +- return pstrdup(name); ++ /* Currently, we only recognize $macro at the start of the string */ ++ if (str[0] != '$') ++ return pstrdup(str); + +- if ((sep_ptr = first_dir_separator(name)) == NULL) +- sep_ptr = name + strlen(name); ++ if ((sep_ptr = first_dir_separator(str)) == NULL) ++ sep_ptr = str + strlen(str); + +- if (strlen("$libdir") != sep_ptr - name || +- strncmp(name, "$libdir", strlen("$libdir")) != 0) ++ if (strlen(macro) != sep_ptr - str || ++ strncmp(str, macro, strlen(macro)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("invalid macro name in dynamic library path: %s", +- name))); ++ errmsg("invalid macro name in path: %s", ++ str))); + +- return psprintf("%s%s", pkglib_path, sep_ptr); ++ return psprintf("%s%s", value, sep_ptr); + } + + + /* + * Search for a file called 'basename' in the colon-separated search +- * path Dynamic_library_path. If the file is found, the full file name ++ * path given. If the file is found, the full file name + * is returned in freshly palloc'd memory. If the file is not found, + * return NULL. ++ * ++ * path_param is the name of the parameter that path came from, for error ++ * messages. ++ * ++ * macro and macro_val allow substituting a macro; see ++ * substitute_path_macro(). + */ +-static char * +-find_in_dynamic_libpath(const char *basename) ++char * ++find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val) + { + const char *p; + size_t baselen; + +- AssertArg(basename != NULL); +- AssertArg(first_dir_separator(basename) == NULL); +- AssertState(Dynamic_library_path != NULL); ++ Assert(basename != NULL); ++ Assert(first_dir_separator(basename) == NULL); ++ Assert(path != NULL); ++ Assert(path_param != NULL); ++ ++ p = path; + +- p = Dynamic_library_path; ++ /* ++ * If the path variable is empty, don't do a path search. ++ */ + if (strlen(p) == 0) + return NULL; + +@@ -619,7 +639,7 @@ find_in_dynamic_libpath(const char *basename) + if (piece == p) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("zero-length component in parameter \"dynamic_library_path\""))); ++ errmsg("zero-length component in parameter \"%s\"", path_param))); + + if (piece == NULL) + len = strlen(p); +@@ -629,7 +649,7 @@ find_in_dynamic_libpath(const char *basename) + piece = palloc(len + 1); + strlcpy(piece, p, len + 1); + +- mangled = substitute_libpath_macro(piece); ++ mangled = substitute_path_macro(piece, macro, macro_val); + pfree(piece); + + canonicalize_path(mangled); +@@ -638,13 +658,13 @@ find_in_dynamic_libpath(const char *basename) + if (!is_absolute_path(mangled)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); ++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); + + full = palloc(strlen(mangled) + 1 + baselen + 1); + sprintf(full, "%s/%s", mangled, basename); + pfree(mangled); + +- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); ++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); + + if (file_exists(full)) + return full; +diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c +index b0c29a15ec4..c27575db2f0 100644 +--- a/src/backend/utils/misc/guc.c ++++ b/src/backend/utils/misc/guc.c +@@ -38,6 +38,7 @@ + #include "catalog/pg_authid.h" + #include "commands/async.h" + #include "commands/prepare.h" ++#include "commands/extension.h" + #include "commands/tablespace.h" + #include "commands/user.h" + #include "commands/vacuum.h" +@@ -3681,6 +3682,18 @@ static struct config_string ConfigureNamesString[] = + NULL, NULL, NULL + }, + ++ { ++ {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, ++ gettext_noop("Sets the path for extension control files."), ++ gettext_noop("The remaining extension script and secondary control files are then loaded " ++ "from the same directory where the primary control file was found."), ++ GUC_SUPERUSER_ONLY ++ }, ++ &Extension_control_path, ++ "$system", ++ NULL, NULL, NULL ++ }, ++ + { + {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, + gettext_noop("Sets the location of the Kerberos server key file."), +diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample +index cb7b7651af4..2b7aa68a6b1 100644 +--- a/src/backend/utils/misc/postgresql.conf.sample ++++ b/src/backend/utils/misc/postgresql.conf.sample +@@ -682,6 +682,7 @@ + # - Other Defaults - + + #dynamic_library_path = '$libdir' ++#extension_control_path = '$system' + + + #------------------------------------------------------------------------------ +diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h +index c724430aeca..03541e230dc 100644 +--- a/src/include/commands/extension.h ++++ b/src/include/commands/extension.h +@@ -17,6 +17,8 @@ + #include "catalog/objectaddress.h" + #include "nodes/parsenodes.h" + ++/* GUC */ ++extern PGDLLIMPORT char *Extension_control_path; + + /* + * creating_extension is only true while running a CREATE EXTENSION or ALTER +diff --git a/src/include/fmgr.h b/src/include/fmgr.h +index f61757373bc..29576d202b5 100644 +--- a/src/include/fmgr.h ++++ b/src/include/fmgr.h +@@ -713,6 +713,9 @@ extern char *Dynamic_library_path; + extern PGFunction load_external_function(const char *filename, const char *funcname, + bool signalNotFound, void **filehandle); + extern PGFunction lookup_external_function(void *filehandle, const char *funcname); ++ ++extern char *find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val); + extern void load_file(const char *filename, bool restricted); + extern void **find_rendezvous_variable(const char *varName); + extern Size EstimateLibraryStateSpace(void); +-- +2.42.0 + diff --git a/nix/patches/13-add-extension_control_path-for.patch b/nix/patches/13-add-extension_control_path-for.patch new file mode 100644 index 0000000..82a4201 --- /dev/null +++ b/nix/patches/13-add-extension_control_path-for.patch @@ -0,0 +1,467 @@ +From 35f2e0dd2a09bd40cfc015935e85bb5520124363 Mon Sep 17 00:00:00 2001 +From: steve-chavez +Date: Mon, 3 Feb 2025 15:49:36 -0500 +Subject: [PATCH] add extension_control_path for 13 + +--- + src/Makefile.global.in | 19 +++-- + src/backend/commands/extension.c | 72 +++++++++++++++-- + src/backend/utils/fmgr/dfmgr.c | 80 ++++++++++++------- + src/backend/utils/misc/guc.c | 13 +++ + src/backend/utils/misc/postgresql.conf.sample | 1 + + src/include/commands/extension.h | 2 + + src/include/fmgr.h | 3 + + 7 files changed, 145 insertions(+), 45 deletions(-) + +diff --git a/src/Makefile.global.in b/src/Makefile.global.in +index b75bafd1148..3a579b78fee 100644 +--- a/src/Makefile.global.in ++++ b/src/Makefile.global.in +@@ -87,9 +87,19 @@ vpathsearch = `for f in $(addsuffix /$(1),$(subst :, ,. $(VPATH))); do test -r $ + # + # In a PGXS build, we cannot use the values inserted into Makefile.global + # by configure, since the installation tree may have been relocated. +-# Instead get the path values from pg_config. ++# Instead get the path values from pg_config. But users can specify ++# prefix explicitly, if they want to select their own installation ++# location. + +-ifndef PGXS ++ifdef PGXS ++# Extension makefiles should set PG_CONFIG, but older ones might not ++ifndef PG_CONFIG ++PG_CONFIG = pg_config ++endif ++endif ++ ++# This means: if ((not PGXS) or prefix) ++ifneq (,$(if $(PGXS),,1)$(prefix)) + + # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; + # makefiles may only use the derived variables such as bindir. +@@ -147,11 +157,6 @@ localedir := @localedir@ + + else # PGXS case + +-# Extension makefiles should set PG_CONFIG, but older ones might not +-ifndef PG_CONFIG +-PG_CONFIG = pg_config +-endif +- + bindir := $(shell $(PG_CONFIG) --bindir) + datadir := $(shell $(PG_CONFIG) --sharedir) + sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) +diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c +index ccffec91132..249fb8ee8ea 100644 +--- a/src/backend/commands/extension.c ++++ b/src/backend/commands/extension.c +@@ -67,6 +67,9 @@ + #include "utils/varlena.h" + + ++/* GUC */ ++char *Extension_control_path; ++ + /* Globally visible state variables */ + bool creating_extension = false; + Oid CurrentExtensionObject = InvalidOid; +@@ -77,6 +80,7 @@ Oid CurrentExtensionObject = InvalidOid; + typedef struct ExtensionControlFile + { + char *name; /* name of the extension */ ++ char *control_dir; /* directory where control file was found */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ + char *module_pathname; /* string to substitute for +@@ -367,6 +371,12 @@ is_extension_script_filename(const char *filename) + return (extension != NULL) && (strcmp(extension, ".sql") == 0); + } + ++/* ++ * TODO ++ * ++ * This is now only for finding/listing available extensions. Rewrite to use ++ * path. See further TODOs below. ++ */ + static char * + get_extension_control_directory(void) + { +@@ -380,16 +390,45 @@ get_extension_control_directory(void) + return result; + } + ++/* ++ * Find control file for extension with name in control->name, looking in the ++ * path. Return the full file name, or NULL if not found. If found, the ++ * directory is recorded in control->control_dir. ++ */ + static char * +-get_extension_control_filename(const char *extname) ++find_extension_control_filename(ExtensionControlFile *control) + { + char sharepath[MAXPGPATH]; ++ char *system_dir; ++ char *basename; ++ char *ecp; + char *result; + ++ Assert(control->name); ++ + get_share_path(my_exec_path, sharepath); +- result = (char *) palloc(MAXPGPATH); +- snprintf(result, MAXPGPATH, "%s/extension/%s.control", +- sharepath, extname); ++ system_dir = psprintf("%s/extension", sharepath); ++ ++ basename = psprintf("%s.control", control->name); ++ ++ /* ++ * find_in_path() does nothing if the path value is empty. This is the ++ * historical behavior for dynamic_library_path, but it makes no sense for ++ * extensions. So in that case, substitute a default value. ++ */ ++ ecp = Extension_control_path; ++ if (strlen(ecp) == 0) ++ ecp = "$system"; ++ result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); ++ ++ if (result) ++ { ++ const char *p; ++ ++ p = strrchr(result, '/'); ++ Assert(p); ++ control->control_dir = pnstrdup(result, p - result); ++ } + + return result; + } +@@ -405,7 +444,7 @@ get_extension_script_directory(ExtensionControlFile *control) + * installation's share directory. + */ + if (!control->directory) +- return get_extension_control_directory(); ++ return pstrdup(control->control_dir); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); +@@ -483,16 +522,25 @@ parse_extension_control_file(ExtensionControlFile *control, + if (version) + filename = get_extension_aux_control_filename(control, version); + else +- filename = get_extension_control_filename(control->name); ++ filename = find_extension_control_filename(control); ++ ++ if (!filename) ++ { ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("extension \"%s\" is not available", control->name), ++ errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ } + + if ((file = AllocateFile(filename, "r")) == NULL) + { +- if (version && errno == ENOENT) ++ /* no complaint for missing auxiliary file */ ++ if (errno == ENOENT && version) + { +- /* no auxiliary file for this version */ + pfree(filename); + return; + } ++ + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open extension control file \"%s\": %m", +@@ -1945,6 +1993,8 @@ RemoveExtensionById(Oid extId) + * The system view pg_available_extensions provides a user interface to this + * SRF, adding information about whether the extensions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extensions(PG_FUNCTION_ARGS) +@@ -2053,6 +2103,8 @@ pg_available_extensions(PG_FUNCTION_ARGS) + * The system view pg_available_extension_versions provides a user interface + * to this SRF, adding information about which versions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extension_versions(PG_FUNCTION_ARGS) +@@ -2253,6 +2305,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, + * directory. That's not a bulletproof check, since the file might be + * invalid, but this is only used for hints so it doesn't have to be 100% + * right. ++ * ++ * TODO: make aware of path + */ + bool + extension_file_exists(const char *extensionName) +@@ -2334,6 +2388,8 @@ convert_requires_to_datum(List *requires) + /* + * This function reports the version update paths that exist for the + * specified extension. ++ * ++ * TODO: make aware of path + */ + Datum + pg_extension_update_paths(PG_FUNCTION_ARGS) +diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c +index 9dff1f5e829..a484dadea3b 100644 +--- a/src/backend/utils/fmgr/dfmgr.c ++++ b/src/backend/utils/fmgr/dfmgr.c +@@ -83,8 +83,7 @@ static void internal_unload_library(const char *libname); + static bool file_exists(const char *name); + static char *expand_dynamic_library_name(const char *name); + static void check_restricted_library_name(const char *name); +-static char *substitute_libpath_macro(const char *name); +-static char *find_in_dynamic_libpath(const char *basename); ++static char *substitute_path_macro(const char *str, const char *macro, const char *value); + + /* Magic structure that module needs to match to be accepted */ + static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; +@@ -476,7 +475,7 @@ file_exists(const char *name) + /* + * If name contains a slash, check if the file exists, if so return + * the name. Else (no slash) try to expand using search path (see +- * find_in_dynamic_libpath below); if that works, return the fully ++ * find_in_path below); if that works, return the fully + * expanded file name. If the previous failed, append DLSUFFIX and + * try again. If all fails, just return the original name. + * +@@ -491,17 +490,25 @@ expand_dynamic_library_name(const char *name) + + AssertArg(name); + ++ /* ++ * If the value starts with "$libdir/", strip that. This is because many ++ * extensions have hardcoded '$libdir/foo' as their library name, which ++ * prevents using the path. ++ */ ++ if (strncmp(name, "$libdir/", 8) == 0) ++ name += 8; ++ + have_slash = (first_dir_separator(name) != NULL); + + if (!have_slash) + { +- full = find_in_dynamic_libpath(name); ++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(name); ++ full = substitute_path_macro(name, "$libdir", pkglib_path); + if (file_exists(full)) + return full; + pfree(full); +@@ -511,14 +518,14 @@ expand_dynamic_library_name(const char *name) + + if (!have_slash) + { +- full = find_in_dynamic_libpath(new); ++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + pfree(new); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(new); ++ full = substitute_path_macro(new, "$libdir", pkglib_path); + pfree(new); + if (file_exists(full)) + return full; +@@ -553,47 +560,60 @@ check_restricted_library_name(const char *name) + * Result is always freshly palloc'd. + */ + static char * +-substitute_libpath_macro(const char *name) ++substitute_path_macro(const char *str, const char *macro, const char *value) + { + const char *sep_ptr; + +- AssertArg(name != NULL); ++ Assert(str != NULL); ++ Assert(macro[0] == '$'); + +- /* Currently, we only recognize $libdir at the start of the string */ +- if (name[0] != '$') +- return pstrdup(name); ++ /* Currently, we only recognize $macro at the start of the string */ ++ if (str[0] != '$') ++ return pstrdup(str); + +- if ((sep_ptr = first_dir_separator(name)) == NULL) +- sep_ptr = name + strlen(name); ++ if ((sep_ptr = first_dir_separator(str)) == NULL) ++ sep_ptr = str + strlen(str); + +- if (strlen("$libdir") != sep_ptr - name || +- strncmp(name, "$libdir", strlen("$libdir")) != 0) ++ if (strlen(macro) != sep_ptr - str || ++ strncmp(str, macro, strlen(macro)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("invalid macro name in dynamic library path: %s", +- name))); ++ errmsg("invalid macro name in path: %s", ++ str))); + +- return psprintf("%s%s", pkglib_path, sep_ptr); ++ return psprintf("%s%s", value, sep_ptr); + } + + + /* + * Search for a file called 'basename' in the colon-separated search +- * path Dynamic_library_path. If the file is found, the full file name ++ * path given. If the file is found, the full file name + * is returned in freshly palloc'd memory. If the file is not found, + * return NULL. ++ * ++ * path_param is the name of the parameter that path came from, for error ++ * messages. ++ * ++ * macro and macro_val allow substituting a macro; see ++ * substitute_path_macro(). + */ +-static char * +-find_in_dynamic_libpath(const char *basename) ++char * ++find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val) + { + const char *p; + size_t baselen; + +- AssertArg(basename != NULL); +- AssertArg(first_dir_separator(basename) == NULL); +- AssertState(Dynamic_library_path != NULL); ++ Assert(basename != NULL); ++ Assert(first_dir_separator(basename) == NULL); ++ Assert(path != NULL); ++ Assert(path_param != NULL); ++ ++ p = path; + +- p = Dynamic_library_path; ++ /* ++ * If the path variable is empty, don't do a path search. ++ */ + if (strlen(p) == 0) + return NULL; + +@@ -610,7 +630,7 @@ find_in_dynamic_libpath(const char *basename) + if (piece == p) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("zero-length component in parameter \"dynamic_library_path\""))); ++ errmsg("zero-length component in parameter \"%s\"", path_param))); + + if (piece == NULL) + len = strlen(p); +@@ -620,7 +640,7 @@ find_in_dynamic_libpath(const char *basename) + piece = palloc(len + 1); + strlcpy(piece, p, len + 1); + +- mangled = substitute_libpath_macro(piece); ++ mangled = substitute_path_macro(piece, macro, macro_val); + pfree(piece); + + canonicalize_path(mangled); +@@ -629,13 +649,13 @@ find_in_dynamic_libpath(const char *basename) + if (!is_absolute_path(mangled)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); ++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); + + full = palloc(strlen(mangled) + 1 + baselen + 1); + sprintf(full, "%s/%s", mangled, basename); + pfree(mangled); + +- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); ++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); + + if (file_exists(full)) + return full; +diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c +index 0ac30a859e0..019746bfda9 100644 +--- a/src/backend/utils/misc/guc.c ++++ b/src/backend/utils/misc/guc.c +@@ -39,6 +39,7 @@ + #include "catalog/storage.h" + #include "commands/async.h" + #include "commands/prepare.h" ++#include "commands/extension.h" + #include "commands/tablespace.h" + #include "commands/trigger.h" + #include "commands/user.h" +@@ -3900,6 +3901,18 @@ static struct config_string ConfigureNamesString[] = + NULL, NULL, NULL + }, + ++ { ++ {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, ++ gettext_noop("Sets the path for extension control files."), ++ gettext_noop("The remaining extension script and secondary control files are then loaded " ++ "from the same directory where the primary control file was found."), ++ GUC_SUPERUSER_ONLY ++ }, ++ &Extension_control_path, ++ "$system", ++ NULL, NULL, NULL ++ }, ++ + { + {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, + gettext_noop("Sets the location of the Kerberos server key file."), +diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample +index 8db7c968c8b..905a907e63d 100644 +--- a/src/backend/utils/misc/postgresql.conf.sample ++++ b/src/backend/utils/misc/postgresql.conf.sample +@@ -711,6 +711,7 @@ + # - Other Defaults - + + #dynamic_library_path = '$libdir' ++#extension_control_path = '$system' + + + #------------------------------------------------------------------------------ +diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h +index 8b06df02a72..7cf65ab0b2c 100644 +--- a/src/include/commands/extension.h ++++ b/src/include/commands/extension.h +@@ -17,6 +17,8 @@ + #include "catalog/objectaddress.h" + #include "parser/parse_node.h" + ++/* GUC */ ++extern PGDLLIMPORT char *Extension_control_path; + + /* + * creating_extension is only true while running a CREATE EXTENSION or ALTER +diff --git a/src/include/fmgr.h b/src/include/fmgr.h +index d349510b7c7..81923d3711b 100644 +--- a/src/include/fmgr.h ++++ b/src/include/fmgr.h +@@ -719,6 +719,9 @@ extern char *Dynamic_library_path; + extern PGFunction load_external_function(const char *filename, const char *funcname, + bool signalNotFound, void **filehandle); + extern PGFunction lookup_external_function(void *filehandle, const char *funcname); ++ ++extern char *find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val); + extern void load_file(const char *filename, bool restricted); + extern void **find_rendezvous_variable(const char *varName); + extern Size EstimateLibraryStateSpace(void); +-- +2.42.0 + diff --git a/nix/patches/14-add-extension_control_path-for.patch b/nix/patches/14-add-extension_control_path-for.patch new file mode 100644 index 0000000..6e7a04c --- /dev/null +++ b/nix/patches/14-add-extension_control_path-for.patch @@ -0,0 +1,466 @@ +From 964a3a7fa63aa14df370df44554d92969f1872eb Mon Sep 17 00:00:00 2001 +From: steve-chavez +Date: Mon, 3 Feb 2025 14:19:21 -0500 +Subject: [PATCH] add extension_control_path for 14 + +--- + src/Makefile.global.in | 19 +++-- + src/backend/commands/extension.c | 72 +++++++++++++++-- + src/backend/utils/fmgr/dfmgr.c | 80 ++++++++++++------- + src/backend/utils/misc/guc.c | 13 +++ + src/backend/utils/misc/postgresql.conf.sample | 1 + + src/include/commands/extension.h | 2 + + src/include/fmgr.h | 2 + + 7 files changed, 144 insertions(+), 45 deletions(-) + +diff --git a/src/Makefile.global.in b/src/Makefile.global.in +index 0df9f13f4a1..39678a7b87c 100644 +--- a/src/Makefile.global.in ++++ b/src/Makefile.global.in +@@ -87,9 +87,19 @@ vpathsearch = `for f in $(addsuffix /$(1),$(subst :, ,. $(VPATH))); do test -r $ + # + # In a PGXS build, we cannot use the values inserted into Makefile.global + # by configure, since the installation tree may have been relocated. +-# Instead get the path values from pg_config. ++# Instead get the path values from pg_config. But users can specify ++# prefix explicitly, if they want to select their own installation ++# location. + +-ifndef PGXS ++ifdef PGXS ++# Extension makefiles should set PG_CONFIG, but older ones might not ++ifndef PG_CONFIG ++PG_CONFIG = pg_config ++endif ++endif ++ ++# This means: if ((not PGXS) or prefix) ++ifneq (,$(if $(PGXS),,1)$(prefix)) + + # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; + # makefiles may only use the derived variables such as bindir. +@@ -147,11 +157,6 @@ localedir := @localedir@ + + else # PGXS case + +-# Extension makefiles should set PG_CONFIG, but older ones might not +-ifndef PG_CONFIG +-PG_CONFIG = pg_config +-endif +- + bindir := $(shell $(PG_CONFIG) --bindir) + datadir := $(shell $(PG_CONFIG) --sharedir) + sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) +diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c +index a71504fbcae..7dfd11d3c07 100644 +--- a/src/backend/commands/extension.c ++++ b/src/backend/commands/extension.c +@@ -67,6 +67,9 @@ + #include "utils/varlena.h" + + ++/* GUC */ ++char *Extension_control_path; ++ + /* Globally visible state variables */ + bool creating_extension = false; + Oid CurrentExtensionObject = InvalidOid; +@@ -77,6 +80,7 @@ Oid CurrentExtensionObject = InvalidOid; + typedef struct ExtensionControlFile + { + char *name; /* name of the extension */ ++ char *control_dir; /* directory where control file was found */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ + char *module_pathname; /* string to substitute for +@@ -367,6 +371,12 @@ is_extension_script_filename(const char *filename) + return (extension != NULL) && (strcmp(extension, ".sql") == 0); + } + ++/* ++ * TODO ++ * ++ * This is now only for finding/listing available extensions. Rewrite to use ++ * path. See further TODOs below. ++ */ + static char * + get_extension_control_directory(void) + { +@@ -380,16 +390,45 @@ get_extension_control_directory(void) + return result; + } + ++/* ++ * Find control file for extension with name in control->name, looking in the ++ * path. Return the full file name, or NULL if not found. If found, the ++ * directory is recorded in control->control_dir. ++ */ + static char * +-get_extension_control_filename(const char *extname) ++find_extension_control_filename(ExtensionControlFile *control) + { + char sharepath[MAXPGPATH]; ++ char *system_dir; ++ char *basename; ++ char *ecp; + char *result; + ++ Assert(control->name); ++ + get_share_path(my_exec_path, sharepath); +- result = (char *) palloc(MAXPGPATH); +- snprintf(result, MAXPGPATH, "%s/extension/%s.control", +- sharepath, extname); ++ system_dir = psprintf("%s/extension", sharepath); ++ ++ basename = psprintf("%s.control", control->name); ++ ++ /* ++ * find_in_path() does nothing if the path value is empty. This is the ++ * historical behavior for dynamic_library_path, but it makes no sense for ++ * extensions. So in that case, substitute a default value. ++ */ ++ ecp = Extension_control_path; ++ if (strlen(ecp) == 0) ++ ecp = "$system"; ++ result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); ++ ++ if (result) ++ { ++ const char *p; ++ ++ p = strrchr(result, '/'); ++ Assert(p); ++ control->control_dir = pnstrdup(result, p - result); ++ } + + return result; + } +@@ -405,7 +444,7 @@ get_extension_script_directory(ExtensionControlFile *control) + * installation's share directory. + */ + if (!control->directory) +- return get_extension_control_directory(); ++ return pstrdup(control->control_dir); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); +@@ -483,16 +522,25 @@ parse_extension_control_file(ExtensionControlFile *control, + if (version) + filename = get_extension_aux_control_filename(control, version); + else +- filename = get_extension_control_filename(control->name); ++ filename = find_extension_control_filename(control); ++ ++ if (!filename) ++ { ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("extension \"%s\" is not available", control->name), ++ errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ } + + if ((file = AllocateFile(filename, "r")) == NULL) + { +- if (version && errno == ENOENT) ++ /* no complaint for missing auxiliary file */ ++ if (errno == ENOENT && version) + { +- /* no auxiliary file for this version */ + pfree(filename); + return; + } ++ + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open extension control file \"%s\": %m", +@@ -1946,6 +1994,8 @@ RemoveExtensionById(Oid extId) + * The system view pg_available_extensions provides a user interface to this + * SRF, adding information about whether the extensions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extensions(PG_FUNCTION_ARGS) +@@ -2054,6 +2104,8 @@ pg_available_extensions(PG_FUNCTION_ARGS) + * The system view pg_available_extension_versions provides a user interface + * to this SRF, adding information about which versions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extension_versions(PG_FUNCTION_ARGS) +@@ -2254,6 +2306,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, + * directory. That's not a bulletproof check, since the file might be + * invalid, but this is only used for hints so it doesn't have to be 100% + * right. ++ * ++ * TODO: make aware of path + */ + bool + extension_file_exists(const char *extensionName) +@@ -2335,6 +2389,8 @@ convert_requires_to_datum(List *requires) + /* + * This function reports the version update paths that exist for the + * specified extension. ++ * ++ * TODO: make aware of path + */ + Datum + pg_extension_update_paths(PG_FUNCTION_ARGS) +diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c +index e8c6cdde972..afcb137e661 100644 +--- a/src/backend/utils/fmgr/dfmgr.c ++++ b/src/backend/utils/fmgr/dfmgr.c +@@ -83,8 +83,7 @@ static void internal_unload_library(const char *libname); + static bool file_exists(const char *name); + static char *expand_dynamic_library_name(const char *name); + static void check_restricted_library_name(const char *name); +-static char *substitute_libpath_macro(const char *name); +-static char *find_in_dynamic_libpath(const char *basename); ++static char *substitute_path_macro(const char *str, const char *macro, const char *value); + + /* Magic structure that module needs to match to be accepted */ + static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; +@@ -476,7 +475,7 @@ file_exists(const char *name) + /* + * If name contains a slash, check if the file exists, if so return + * the name. Else (no slash) try to expand using search path (see +- * find_in_dynamic_libpath below); if that works, return the fully ++ * find_in_path below); if that works, return the fully + * expanded file name. If the previous failed, append DLSUFFIX and + * try again. If all fails, just return the original name. + * +@@ -491,17 +490,25 @@ expand_dynamic_library_name(const char *name) + + AssertArg(name); + ++ /* ++ * If the value starts with "$libdir/", strip that. This is because many ++ * extensions have hardcoded '$libdir/foo' as their library name, which ++ * prevents using the path. ++ */ ++ if (strncmp(name, "$libdir/", 8) == 0) ++ name += 8; ++ + have_slash = (first_dir_separator(name) != NULL); + + if (!have_slash) + { +- full = find_in_dynamic_libpath(name); ++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(name); ++ full = substitute_path_macro(name, "$libdir", pkglib_path); + if (file_exists(full)) + return full; + pfree(full); +@@ -511,14 +518,14 @@ expand_dynamic_library_name(const char *name) + + if (!have_slash) + { +- full = find_in_dynamic_libpath(new); ++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + pfree(new); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(new); ++ full = substitute_path_macro(new, "$libdir", pkglib_path); + pfree(new); + if (file_exists(full)) + return full; +@@ -553,47 +560,60 @@ check_restricted_library_name(const char *name) + * Result is always freshly palloc'd. + */ + static char * +-substitute_libpath_macro(const char *name) ++substitute_path_macro(const char *str, const char *macro, const char *value) + { + const char *sep_ptr; + +- AssertArg(name != NULL); ++ Assert(str != NULL); ++ Assert(macro[0] == '$'); + +- /* Currently, we only recognize $libdir at the start of the string */ +- if (name[0] != '$') +- return pstrdup(name); ++ /* Currently, we only recognize $macro at the start of the string */ ++ if (str[0] != '$') ++ return pstrdup(str); + +- if ((sep_ptr = first_dir_separator(name)) == NULL) +- sep_ptr = name + strlen(name); ++ if ((sep_ptr = first_dir_separator(str)) == NULL) ++ sep_ptr = str + strlen(str); + +- if (strlen("$libdir") != sep_ptr - name || +- strncmp(name, "$libdir", strlen("$libdir")) != 0) ++ if (strlen(macro) != sep_ptr - str || ++ strncmp(str, macro, strlen(macro)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("invalid macro name in dynamic library path: %s", +- name))); ++ errmsg("invalid macro name in path: %s", ++ str))); + +- return psprintf("%s%s", pkglib_path, sep_ptr); ++ return psprintf("%s%s", value, sep_ptr); + } + + + /* + * Search for a file called 'basename' in the colon-separated search +- * path Dynamic_library_path. If the file is found, the full file name ++ * path given. If the file is found, the full file name + * is returned in freshly palloc'd memory. If the file is not found, + * return NULL. ++ * ++ * path_param is the name of the parameter that path came from, for error ++ * messages. ++ * ++ * macro and macro_val allow substituting a macro; see ++ * substitute_path_macro(). + */ +-static char * +-find_in_dynamic_libpath(const char *basename) ++char * ++find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val) + { + const char *p; + size_t baselen; + +- AssertArg(basename != NULL); +- AssertArg(first_dir_separator(basename) == NULL); +- AssertState(Dynamic_library_path != NULL); ++ Assert(basename != NULL); ++ Assert(first_dir_separator(basename) == NULL); ++ Assert(path != NULL); ++ Assert(path_param != NULL); ++ ++ p = path; + +- p = Dynamic_library_path; ++ /* ++ * If the path variable is empty, don't do a path search. ++ */ + if (strlen(p) == 0) + return NULL; + +@@ -610,7 +630,7 @@ find_in_dynamic_libpath(const char *basename) + if (piece == p) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("zero-length component in parameter \"dynamic_library_path\""))); ++ errmsg("zero-length component in parameter \"%s\"", path_param))); + + if (piece == NULL) + len = strlen(p); +@@ -620,7 +640,7 @@ find_in_dynamic_libpath(const char *basename) + piece = palloc(len + 1); + strlcpy(piece, p, len + 1); + +- mangled = substitute_libpath_macro(piece); ++ mangled = substitute_path_macro(piece, macro, macro_val); + pfree(piece); + + canonicalize_path(mangled); +@@ -629,13 +649,13 @@ find_in_dynamic_libpath(const char *basename) + if (!is_absolute_path(mangled)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); ++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); + + full = palloc(strlen(mangled) + 1 + baselen + 1); + sprintf(full, "%s/%s", mangled, basename); + pfree(mangled); + +- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); ++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); + + if (file_exists(full)) + return full; +diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c +index ffee83dfbbb..8e35a097fe9 100644 +--- a/src/backend/utils/misc/guc.c ++++ b/src/backend/utils/misc/guc.c +@@ -46,6 +46,7 @@ + #include "catalog/storage.h" + #include "commands/async.h" + #include "commands/prepare.h" ++#include "commands/extension.h" + #include "commands/tablespace.h" + #include "commands/trigger.h" + #include "commands/user.h" +@@ -4049,6 +4050,18 @@ static struct config_string ConfigureNamesString[] = + NULL, NULL, NULL + }, + ++ { ++ {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, ++ gettext_noop("Sets the path for extension control files."), ++ gettext_noop("The remaining extension script and secondary control files are then loaded " ++ "from the same directory where the primary control file was found."), ++ GUC_SUPERUSER_ONLY ++ }, ++ &Extension_control_path, ++ "$system", ++ NULL, NULL, NULL ++ }, ++ + { + {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, + gettext_noop("Sets the location of the Kerberos server key file."), +diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample +index 34a5798a9cd..8c29e0c1f8f 100644 +--- a/src/backend/utils/misc/postgresql.conf.sample ++++ b/src/backend/utils/misc/postgresql.conf.sample +@@ -726,6 +726,7 @@ + # - Other Defaults - + + #dynamic_library_path = '$libdir' ++#extension_control_path = '$system' + #gin_fuzzy_search_limit = 0 + + +diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h +index 24dbf86e970..5662b308014 100644 +--- a/src/include/commands/extension.h ++++ b/src/include/commands/extension.h +@@ -17,6 +17,8 @@ + #include "catalog/objectaddress.h" + #include "parser/parse_node.h" + ++/* GUC */ ++extern PGDLLIMPORT char *Extension_control_path; + + /* + * creating_extension is only true while running a CREATE EXTENSION or ALTER +diff --git a/src/include/fmgr.h b/src/include/fmgr.h +index ab7b85c86e1..93fcac3df5c 100644 +--- a/src/include/fmgr.h ++++ b/src/include/fmgr.h +@@ -718,6 +718,8 @@ extern bool CheckFunctionValidatorAccess(Oid validatorOid, Oid functionOid); + */ + extern char *Dynamic_library_path; + ++extern char *find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val); + extern void *load_external_function(const char *filename, const char *funcname, + bool signalNotFound, void **filehandle); + extern void *lookup_external_function(void *filehandle, const char *funcname); +-- +2.42.0 + diff --git a/nix/patches/15-add-extension_control_path-for.patch b/nix/patches/15-add-extension_control_path-for.patch new file mode 100644 index 0000000..919642f --- /dev/null +++ b/nix/patches/15-add-extension_control_path-for.patch @@ -0,0 +1,479 @@ +From 5bc19e9b7a493edded4eb3eacee49ae4368a2b51 Mon Sep 17 00:00:00 2001 +From: steve-chavez +Date: Sun, 26 Jan 2025 17:56:05 -0500 +Subject: [PATCH] add extension_control_path for 15 + +--- + src/Makefile.global.in | 19 ++-- + src/backend/commands/extension.c | 87 ++++++++++++++----- + src/backend/utils/fmgr/dfmgr.c | 80 ++++++++++------- + src/backend/utils/misc/guc.c | 13 +++ + src/backend/utils/misc/postgresql.conf.sample | 1 + + src/include/commands/extension.h | 2 + + src/include/fmgr.h | 2 + + 7 files changed, 146 insertions(+), 58 deletions(-) + +diff --git a/src/Makefile.global.in b/src/Makefile.global.in +index eeefc73b9f7..98807271b72 100644 +--- a/src/Makefile.global.in ++++ b/src/Makefile.global.in +@@ -87,9 +87,19 @@ vpathsearch = `for f in $(addsuffix /$(1),$(subst :, ,. $(VPATH))); do test -r $ + # + # In a PGXS build, we cannot use the values inserted into Makefile.global + # by configure, since the installation tree may have been relocated. +-# Instead get the path values from pg_config. ++# Instead get the path values from pg_config. But users can specify ++# prefix explicitly, if they want to select their own installation ++# location. + +-ifndef PGXS ++ifdef PGXS ++# Extension makefiles should set PG_CONFIG, but older ones might not ++ifndef PG_CONFIG ++PG_CONFIG = pg_config ++endif ++endif ++ ++# This means: if ((not PGXS) or prefix) ++ifneq (,$(if $(PGXS),,1)$(prefix)) + + # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; + # makefiles may only use the derived variables such as bindir. +@@ -147,11 +157,6 @@ localedir := @localedir@ + + else # PGXS case + +-# Extension makefiles should set PG_CONFIG, but older ones might not +-ifndef PG_CONFIG +-PG_CONFIG = pg_config +-endif +- + bindir := $(shell $(PG_CONFIG) --bindir) + datadir := $(shell $(PG_CONFIG) --sharedir) + sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) +diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c +index df6f021c300..254549e9c8b 100644 +--- a/src/backend/commands/extension.c ++++ b/src/backend/commands/extension.c +@@ -67,6 +67,9 @@ + #include "utils/varlena.h" + + ++/* GUC */ ++char *Extension_control_path; ++ + /* Globally visible state variables */ + bool creating_extension = false; + Oid CurrentExtensionObject = InvalidOid; +@@ -77,6 +80,7 @@ Oid CurrentExtensionObject = InvalidOid; + typedef struct ExtensionControlFile + { + char *name; /* name of the extension */ ++ char *control_dir; /* directory where control file was found */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ + char *module_pathname; /* string to substitute for +@@ -367,6 +371,12 @@ is_extension_script_filename(const char *filename) + return (extension != NULL) && (strcmp(extension, ".sql") == 0); + } + ++/* ++ * TODO ++ * ++ * This is now only for finding/listing available extensions. Rewrite to use ++ * path. See further TODOs below. ++ */ + static char * + get_extension_control_directory(void) + { +@@ -380,16 +390,45 @@ get_extension_control_directory(void) + return result; + } + ++/* ++ * Find control file for extension with name in control->name, looking in the ++ * path. Return the full file name, or NULL if not found. If found, the ++ * directory is recorded in control->control_dir. ++ */ + static char * +-get_extension_control_filename(const char *extname) ++find_extension_control_filename(ExtensionControlFile *control) + { + char sharepath[MAXPGPATH]; ++ char *system_dir; ++ char *basename; ++ char *ecp; + char *result; + ++ Assert(control->name); ++ + get_share_path(my_exec_path, sharepath); +- result = (char *) palloc(MAXPGPATH); +- snprintf(result, MAXPGPATH, "%s/extension/%s.control", +- sharepath, extname); ++ system_dir = psprintf("%s/extension", sharepath); ++ ++ basename = psprintf("%s.control", control->name); ++ ++ /* ++ * find_in_path() does nothing if the path value is empty. This is the ++ * historical behavior for dynamic_library_path, but it makes no sense for ++ * extensions. So in that case, substitute a default value. ++ */ ++ ecp = Extension_control_path; ++ if (strlen(ecp) == 0) ++ ecp = "$system"; ++ result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); ++ ++ if (result) ++ { ++ const char *p; ++ ++ p = strrchr(result, '/'); ++ Assert(p); ++ control->control_dir = pnstrdup(result, p - result); ++ } + + return result; + } +@@ -405,7 +444,7 @@ get_extension_script_directory(ExtensionControlFile *control) + * installation's share directory. + */ + if (!control->directory) +- return get_extension_control_directory(); ++ return pstrdup(control->control_dir); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); +@@ -483,27 +522,25 @@ parse_extension_control_file(ExtensionControlFile *control, + if (version) + filename = get_extension_aux_control_filename(control, version); + else +- filename = get_extension_control_filename(control->name); ++ filename = find_extension_control_filename(control); ++ ++ if (!filename) ++ { ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("extension \"%s\" is not available", control->name), ++ errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ } + + if ((file = AllocateFile(filename, "r")) == NULL) + { +- if (errno == ENOENT) ++ /* no complaint for missing auxiliary file */ ++ if (errno == ENOENT && version) + { +- /* no complaint for missing auxiliary file */ +- if (version) +- { +- pfree(filename); +- return; +- } +- +- /* missing control file indicates extension is not installed */ +- ereport(ERROR, +- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), +- errmsg("extension \"%s\" is not available", control->name), +- errdetail("Could not open extension control file \"%s\": %m.", +- filename), +- errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ pfree(filename); ++ return; + } ++ + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open extension control file \"%s\": %m", +@@ -1952,6 +1989,8 @@ RemoveExtensionById(Oid extId) + * The system view pg_available_extensions provides a user interface to this + * SRF, adding information about whether the extensions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extensions(PG_FUNCTION_ARGS) +@@ -2032,6 +2071,8 @@ pg_available_extensions(PG_FUNCTION_ARGS) + * The system view pg_available_extension_versions provides a user interface + * to this SRF, adding information about which versions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extension_versions(PG_FUNCTION_ARGS) +@@ -2204,6 +2245,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, + * directory. That's not a bulletproof check, since the file might be + * invalid, but this is only used for hints so it doesn't have to be 100% + * right. ++ * ++ * TODO: make aware of path + */ + bool + extension_file_exists(const char *extensionName) +@@ -2285,6 +2328,8 @@ convert_requires_to_datum(List *requires) + /* + * This function reports the version update paths that exist for the + * specified extension. ++ * ++ * TODO: make aware of path + */ + Datum + pg_extension_update_paths(PG_FUNCTION_ARGS) +diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c +index 7f9ea972804..52100e3bebb 100644 +--- a/src/backend/utils/fmgr/dfmgr.c ++++ b/src/backend/utils/fmgr/dfmgr.c +@@ -81,8 +81,7 @@ static void incompatible_module_error(const char *libname, + static bool file_exists(const char *name); + static char *expand_dynamic_library_name(const char *name); + static void check_restricted_library_name(const char *name); +-static char *substitute_libpath_macro(const char *name); +-static char *find_in_dynamic_libpath(const char *basename); ++static char *substitute_path_macro(const char *str, const char *macro, const char *value); + + /* Magic structure that module needs to match to be accepted */ + static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; +@@ -421,7 +420,7 @@ file_exists(const char *name) + /* + * If name contains a slash, check if the file exists, if so return + * the name. Else (no slash) try to expand using search path (see +- * find_in_dynamic_libpath below); if that works, return the fully ++ * find_in_path below); if that works, return the fully + * expanded file name. If the previous failed, append DLSUFFIX and + * try again. If all fails, just return the original name. + * +@@ -436,17 +435,25 @@ expand_dynamic_library_name(const char *name) + + AssertArg(name); + ++ /* ++ * If the value starts with "$libdir/", strip that. This is because many ++ * extensions have hardcoded '$libdir/foo' as their library name, which ++ * prevents using the path. ++ */ ++ if (strncmp(name, "$libdir/", 8) == 0) ++ name += 8; ++ + have_slash = (first_dir_separator(name) != NULL); + + if (!have_slash) + { +- full = find_in_dynamic_libpath(name); ++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(name); ++ full = substitute_path_macro(name, "$libdir", pkglib_path); + if (file_exists(full)) + return full; + pfree(full); +@@ -456,14 +463,14 @@ expand_dynamic_library_name(const char *name) + + if (!have_slash) + { +- full = find_in_dynamic_libpath(new); ++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + pfree(new); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(new); ++ full = substitute_path_macro(new, "$libdir", pkglib_path); + pfree(new); + if (file_exists(full)) + return full; +@@ -498,47 +505,60 @@ check_restricted_library_name(const char *name) + * Result is always freshly palloc'd. + */ + static char * +-substitute_libpath_macro(const char *name) ++substitute_path_macro(const char *str, const char *macro, const char *value) + { + const char *sep_ptr; + +- AssertArg(name != NULL); ++ Assert(str != NULL); ++ Assert(macro[0] == '$'); + +- /* Currently, we only recognize $libdir at the start of the string */ +- if (name[0] != '$') +- return pstrdup(name); ++ /* Currently, we only recognize $macro at the start of the string */ ++ if (str[0] != '$') ++ return pstrdup(str); + +- if ((sep_ptr = first_dir_separator(name)) == NULL) +- sep_ptr = name + strlen(name); ++ if ((sep_ptr = first_dir_separator(str)) == NULL) ++ sep_ptr = str + strlen(str); + +- if (strlen("$libdir") != sep_ptr - name || +- strncmp(name, "$libdir", strlen("$libdir")) != 0) ++ if (strlen(macro) != sep_ptr - str || ++ strncmp(str, macro, strlen(macro)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("invalid macro name in dynamic library path: %s", +- name))); ++ errmsg("invalid macro name in path: %s", ++ str))); + +- return psprintf("%s%s", pkglib_path, sep_ptr); ++ return psprintf("%s%s", value, sep_ptr); + } + + + /* + * Search for a file called 'basename' in the colon-separated search +- * path Dynamic_library_path. If the file is found, the full file name ++ * path given. If the file is found, the full file name + * is returned in freshly palloc'd memory. If the file is not found, + * return NULL. ++ * ++ * path_param is the name of the parameter that path came from, for error ++ * messages. ++ * ++ * macro and macro_val allow substituting a macro; see ++ * substitute_path_macro(). + */ +-static char * +-find_in_dynamic_libpath(const char *basename) ++char * ++find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val) + { + const char *p; + size_t baselen; + +- AssertArg(basename != NULL); +- AssertArg(first_dir_separator(basename) == NULL); +- AssertState(Dynamic_library_path != NULL); ++ Assert(basename != NULL); ++ Assert(first_dir_separator(basename) == NULL); ++ Assert(path != NULL); ++ Assert(path_param != NULL); ++ ++ p = path; + +- p = Dynamic_library_path; ++ /* ++ * If the path variable is empty, don't do a path search. ++ */ + if (strlen(p) == 0) + return NULL; + +@@ -555,7 +575,7 @@ find_in_dynamic_libpath(const char *basename) + if (piece == p) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("zero-length component in parameter \"dynamic_library_path\""))); ++ errmsg("zero-length component in parameter \"%s\"", path_param))); + + if (piece == NULL) + len = strlen(p); +@@ -565,7 +585,7 @@ find_in_dynamic_libpath(const char *basename) + piece = palloc(len + 1); + strlcpy(piece, p, len + 1); + +- mangled = substitute_libpath_macro(piece); ++ mangled = substitute_path_macro(piece, macro, macro_val); + pfree(piece); + + canonicalize_path(mangled); +@@ -574,13 +594,13 @@ find_in_dynamic_libpath(const char *basename) + if (!is_absolute_path(mangled)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); ++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); + + full = palloc(strlen(mangled) + 1 + baselen + 1); + sprintf(full, "%s/%s", mangled, basename); + pfree(mangled); + +- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); ++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); + + if (file_exists(full)) + return full; +diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c +index acbcb8bb81b..04cb9cec348 100644 +--- a/src/backend/utils/misc/guc.c ++++ b/src/backend/utils/misc/guc.c +@@ -50,6 +50,7 @@ + #include "catalog/storage.h" + #include "commands/async.h" + #include "commands/prepare.h" ++#include "commands/extension.h" + #include "commands/tablespace.h" + #include "commands/trigger.h" + #include "commands/user.h" +@@ -4170,6 +4171,18 @@ static struct config_string ConfigureNamesString[] = + NULL, NULL, NULL + }, + ++ { ++ {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, ++ gettext_noop("Sets the path for extension control files."), ++ gettext_noop("The remaining extension script and secondary control files are then loaded " ++ "from the same directory where the primary control file was found."), ++ GUC_SUPERUSER_ONLY ++ }, ++ &Extension_control_path, ++ "$system", ++ NULL, NULL, NULL ++ }, ++ + { + {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, + gettext_noop("Sets the location of the Kerberos server key file."), +diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample +index 7bf891a9020..c53baee5c95 100644 +--- a/src/backend/utils/misc/postgresql.conf.sample ++++ b/src/backend/utils/misc/postgresql.conf.sample +@@ -743,6 +743,7 @@ + # - Other Defaults - + + #dynamic_library_path = '$libdir' ++#extension_control_path = '$system' + #gin_fuzzy_search_limit = 0 + + +diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h +index e24e3759f0c..688faafcfbe 100644 +--- a/src/include/commands/extension.h ++++ b/src/include/commands/extension.h +@@ -17,6 +17,8 @@ + #include "catalog/objectaddress.h" + #include "parser/parse_node.h" + ++/* GUC */ ++extern PGDLLIMPORT char *Extension_control_path; + + /* + * creating_extension is only true while running a CREATE EXTENSION or ALTER +diff --git a/src/include/fmgr.h b/src/include/fmgr.h +index 5314b737052..e04da80d1b6 100644 +--- a/src/include/fmgr.h ++++ b/src/include/fmgr.h +@@ -722,6 +722,8 @@ extern bool CheckFunctionValidatorAccess(Oid validatorOid, Oid functionOid); + */ + extern PGDLLIMPORT char *Dynamic_library_path; + ++extern char *find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val); + extern void *load_external_function(const char *filename, const char *funcname, + bool signalNotFound, void **filehandle); + extern void *lookup_external_function(void *filehandle, const char *funcname); +-- +2.42.0 + diff --git a/nix/patches/16-add-extension_control_path-for.patch b/nix/patches/16-add-extension_control_path-for.patch new file mode 100644 index 0000000..131c389 --- /dev/null +++ b/nix/patches/16-add-extension_control_path-for.patch @@ -0,0 +1,574 @@ +From d0ca9753e3c137784142cf35db1bde36b78b83d9 Mon Sep 17 00:00:00 2001 +From: steve-chavez +Date: Sat, 25 Jan 2025 20:33:32 -0500 +Subject: [PATCH] add extension_control_path for 16.3 + +--- + src/Makefile.global.in | 19 ++-- + src/backend/commands/extension.c | 87 ++++++++++++++----- + src/backend/utils/fmgr/dfmgr.c | 76 ++++++++++------ + src/backend/utils/fmgr/dfmgr.c.rej | 72 +++++++++++++++ + src/backend/utils/misc/guc_tables.c | 13 +++ + src/backend/utils/misc/guc_tables.c.rej | 9 ++ + src/backend/utils/misc/postgresql.conf.sample | 1 + + src/include/commands/extension.h | 2 + + src/include/fmgr.h | 2 + + 9 files changed, 225 insertions(+), 56 deletions(-) + create mode 100644 src/backend/utils/fmgr/dfmgr.c.rej + create mode 100644 src/backend/utils/misc/guc_tables.c.rej + +diff --git a/src/Makefile.global.in b/src/Makefile.global.in +index cc4dc6de91e..b3c3b9e03b0 100644 +--- a/src/Makefile.global.in ++++ b/src/Makefile.global.in +@@ -87,9 +87,19 @@ vpathsearch = `for f in $(addsuffix /$(1),$(subst :, ,. $(VPATH))); do test -r $ + # + # In a PGXS build, we cannot use the values inserted into Makefile.global + # by configure, since the installation tree may have been relocated. +-# Instead get the path values from pg_config. ++# Instead get the path values from pg_config. But users can specify ++# prefix explicitly, if they want to select their own installation ++# location. + +-ifndef PGXS ++ifdef PGXS ++# Extension makefiles should set PG_CONFIG, but older ones might not ++ifndef PG_CONFIG ++PG_CONFIG = pg_config ++endif ++endif ++ ++# This means: if ((not PGXS) or prefix) ++ifneq (,$(if $(PGXS),,1)$(prefix)) + + # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; + # makefiles may only use the derived variables such as bindir. +@@ -147,11 +157,6 @@ localedir := @localedir@ + + else # PGXS case + +-# Extension makefiles should set PG_CONFIG, but older ones might not +-ifndef PG_CONFIG +-PG_CONFIG = pg_config +-endif +- + bindir := $(shell $(PG_CONFIG) --bindir) + datadir := $(shell $(PG_CONFIG) --sharedir) + sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) +diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c +index 2ff0d691d86..a7d564d4a09 100644 +--- a/src/backend/commands/extension.c ++++ b/src/backend/commands/extension.c +@@ -69,6 +69,9 @@ + #include "utils/varlena.h" + + ++/* GUC */ ++char *Extension_control_path; ++ + /* Globally visible state variables */ + bool creating_extension = false; + Oid CurrentExtensionObject = InvalidOid; +@@ -79,6 +82,7 @@ Oid CurrentExtensionObject = InvalidOid; + typedef struct ExtensionControlFile + { + char *name; /* name of the extension */ ++ char *control_dir; /* directory where control file was found */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ + char *module_pathname; /* string to substitute for +@@ -371,6 +375,12 @@ is_extension_script_filename(const char *filename) + return (extension != NULL) && (strcmp(extension, ".sql") == 0); + } + ++/* ++ * TODO ++ * ++ * This is now only for finding/listing available extensions. Rewrite to use ++ * path. See further TODOs below. ++ */ + static char * + get_extension_control_directory(void) + { +@@ -384,16 +394,45 @@ get_extension_control_directory(void) + return result; + } + ++/* ++ * Find control file for extension with name in control->name, looking in the ++ * path. Return the full file name, or NULL if not found. If found, the ++ * directory is recorded in control->control_dir. ++ */ + static char * +-get_extension_control_filename(const char *extname) ++find_extension_control_filename(ExtensionControlFile *control) + { + char sharepath[MAXPGPATH]; ++ char *system_dir; ++ char *basename; ++ char *ecp; + char *result; + ++ Assert(control->name); ++ + get_share_path(my_exec_path, sharepath); +- result = (char *) palloc(MAXPGPATH); +- snprintf(result, MAXPGPATH, "%s/extension/%s.control", +- sharepath, extname); ++ system_dir = psprintf("%s/extension", sharepath); ++ ++ basename = psprintf("%s.control", control->name); ++ ++ /* ++ * find_in_path() does nothing if the path value is empty. This is the ++ * historical behavior for dynamic_library_path, but it makes no sense for ++ * extensions. So in that case, substitute a default value. ++ */ ++ ecp = Extension_control_path; ++ if (strlen(ecp) == 0) ++ ecp = "$system"; ++ result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); ++ ++ if (result) ++ { ++ const char *p; ++ ++ p = strrchr(result, '/'); ++ Assert(p); ++ control->control_dir = pnstrdup(result, p - result); ++ } + + return result; + } +@@ -409,7 +448,7 @@ get_extension_script_directory(ExtensionControlFile *control) + * installation's share directory. + */ + if (!control->directory) +- return get_extension_control_directory(); ++ return pstrdup(control->control_dir); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); +@@ -487,27 +526,25 @@ parse_extension_control_file(ExtensionControlFile *control, + if (version) + filename = get_extension_aux_control_filename(control, version); + else +- filename = get_extension_control_filename(control->name); ++ filename = find_extension_control_filename(control); ++ ++ if (!filename) ++ { ++ ereport(ERROR, ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("extension \"%s\" is not available", control->name), ++ errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ } + + if ((file = AllocateFile(filename, "r")) == NULL) + { +- if (errno == ENOENT) ++ /* no complaint for missing auxiliary file */ ++ if (errno == ENOENT && version) + { +- /* no complaint for missing auxiliary file */ +- if (version) +- { +- pfree(filename); +- return; +- } +- +- /* missing control file indicates extension is not installed */ +- ereport(ERROR, +- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), +- errmsg("extension \"%s\" is not available", control->name), +- errdetail("Could not open extension control file \"%s\": %m.", +- filename), +- errhint("The extension must first be installed on the system where PostgreSQL is running."))); ++ pfree(filename); ++ return; + } ++ + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open extension control file \"%s\": %m", +@@ -2007,6 +2044,8 @@ RemoveExtensionById(Oid extId) + * The system view pg_available_extensions provides a user interface to this + * SRF, adding information about whether the extensions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extensions(PG_FUNCTION_ARGS) +@@ -2087,6 +2126,8 @@ pg_available_extensions(PG_FUNCTION_ARGS) + * The system view pg_available_extension_versions provides a user interface + * to this SRF, adding information about which versions are installed in the + * current DB. ++ * ++ * TODO: make aware of path + */ + Datum + pg_available_extension_versions(PG_FUNCTION_ARGS) +@@ -2259,6 +2300,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, + * directory. That's not a bulletproof check, since the file might be + * invalid, but this is only used for hints so it doesn't have to be 100% + * right. ++ * ++ * TODO: make aware of path + */ + bool + extension_file_exists(const char *extensionName) +@@ -2338,6 +2381,8 @@ convert_requires_to_datum(List *requires) + /* + * This function reports the version update paths that exist for the + * specified extension. ++ * ++ * TODO: make aware of path + */ + Datum + pg_extension_update_paths(PG_FUNCTION_ARGS) +diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c +index b85d52c913c..a3486ded97c 100644 +--- a/src/backend/utils/fmgr/dfmgr.c ++++ b/src/backend/utils/fmgr/dfmgr.c +@@ -81,8 +81,7 @@ static void incompatible_module_error(const char *libname, + static bool file_exists(const char *name); + static char *expand_dynamic_library_name(const char *name); + static void check_restricted_library_name(const char *name); +-static char *substitute_libpath_macro(const char *name); +-static char *find_in_dynamic_libpath(const char *basename); ++static char *substitute_path_macro(const char *str, const char *macro, const char *value); + + /* Magic structure that module needs to match to be accepted */ + static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; +@@ -421,7 +420,7 @@ file_exists(const char *name) + /* + * If name contains a slash, check if the file exists, if so return + * the name. Else (no slash) try to expand using search path (see +- * find_in_dynamic_libpath below); if that works, return the fully ++ * find_in_path below); if that works, return the fully + * expanded file name. If the previous failed, append DLSUFFIX and + * try again. If all fails, just return the original name. + * +@@ -436,17 +435,25 @@ expand_dynamic_library_name(const char *name) + + Assert(name); + ++ /* ++ * If the value starts with "$libdir/", strip that. This is because many ++ * extensions have hardcoded '$libdir/foo' as their library name, which ++ * prevents using the path. ++ */ ++ if (strncmp(name, "$libdir/", 8) == 0) ++ name += 8; ++ + have_slash = (first_dir_separator(name) != NULL); + + if (!have_slash) + { +- full = find_in_dynamic_libpath(name); ++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(name); ++ full = substitute_path_macro(name, "$libdir", pkglib_path); + if (file_exists(full)) + return full; + pfree(full); +@@ -456,14 +463,14 @@ expand_dynamic_library_name(const char *name) + + if (!have_slash) + { +- full = find_in_dynamic_libpath(new); ++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); + pfree(new); + if (full) + return full; + } + else + { +- full = substitute_libpath_macro(new); ++ full = substitute_path_macro(new, "$libdir", pkglib_path); + pfree(new); + if (file_exists(full)) + return full; +@@ -498,47 +505,60 @@ check_restricted_library_name(const char *name) + * Result is always freshly palloc'd. + */ + static char * +-substitute_libpath_macro(const char *name) ++substitute_path_macro(const char *str, const char *macro, const char *value) + { + const char *sep_ptr; + +- Assert(name != NULL); ++ Assert(str != NULL); ++ Assert(macro[0] == '$'); + +- /* Currently, we only recognize $libdir at the start of the string */ +- if (name[0] != '$') +- return pstrdup(name); ++ /* Currently, we only recognize $macro at the start of the string */ ++ if (str[0] != '$') ++ return pstrdup(str); + +- if ((sep_ptr = first_dir_separator(name)) == NULL) +- sep_ptr = name + strlen(name); ++ if ((sep_ptr = first_dir_separator(str)) == NULL) ++ sep_ptr = str + strlen(str); + +- if (strlen("$libdir") != sep_ptr - name || +- strncmp(name, "$libdir", strlen("$libdir")) != 0) ++ if (strlen(macro) != sep_ptr - str || ++ strncmp(str, macro, strlen(macro)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("invalid macro name in dynamic library path: %s", +- name))); ++ errmsg("invalid macro name in path: %s", ++ str))); + +- return psprintf("%s%s", pkglib_path, sep_ptr); ++ return psprintf("%s%s", value, sep_ptr); + } + + + /* + * Search for a file called 'basename' in the colon-separated search +- * path Dynamic_library_path. If the file is found, the full file name ++ * path given. If the file is found, the full file name + * is returned in freshly palloc'd memory. If the file is not found, + * return NULL. ++ * ++ * path_param is the name of the parameter that path came from, for error ++ * messages. ++ * ++ * macro and macro_val allow substituting a macro; see ++ * substitute_path_macro(). + */ +-static char * +-find_in_dynamic_libpath(const char *basename) ++char * ++find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val) + { + const char *p; + size_t baselen; + + Assert(basename != NULL); + Assert(first_dir_separator(basename) == NULL); +- Assert(Dynamic_library_path != NULL); ++ Assert(path != NULL); ++ Assert(path_param != NULL); ++ ++ p = path; + +- p = Dynamic_library_path; ++ /* ++ * If the path variable is empty, don't do a path search. ++ */ + if (strlen(p) == 0) + return NULL; + +@@ -555,7 +575,7 @@ find_in_dynamic_libpath(const char *basename) + if (piece == p) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("zero-length component in parameter \"dynamic_library_path\""))); ++ errmsg("zero-length component in parameter \"%s\"", path_param))); + + if (piece == NULL) + len = strlen(p); +@@ -565,7 +585,7 @@ find_in_dynamic_libpath(const char *basename) + piece = palloc(len + 1); + strlcpy(piece, p, len + 1); + +- mangled = substitute_libpath_macro(piece); ++ mangled = substitute_path_macro(piece, macro, macro_val); + pfree(piece); + + canonicalize_path(mangled); +@@ -574,13 +594,13 @@ find_in_dynamic_libpath(const char *basename) + if (!is_absolute_path(mangled)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), +- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); ++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); + + full = palloc(strlen(mangled) + 1 + baselen + 1); + sprintf(full, "%s/%s", mangled, basename); + pfree(mangled); + +- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); ++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); + + if (file_exists(full)) + return full; +diff --git a/src/backend/utils/fmgr/dfmgr.c.rej b/src/backend/utils/fmgr/dfmgr.c.rej +new file mode 100644 +index 00000000000..0834dec8464 +--- /dev/null ++++ b/src/backend/utils/fmgr/dfmgr.c.rej +@@ -0,0 +1,72 @@ ++diff a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c (rejected hunks) ++@@ -71,8 +71,7 @@ static void incompatible_module_error(const char *libname, ++ const Pg_magic_struct *module_magic_data) pg_attribute_noreturn(); ++ static char *expand_dynamic_library_name(const char *name); ++ static void check_restricted_library_name(const char *name); ++-static char *substitute_libpath_macro(const char *name); ++-static char *find_in_dynamic_libpath(const char *basename); +++static char *substitute_path_macro(const char *str, const char *macro, const char *value); ++ ++ /* Magic structure that module needs to match to be accepted */ ++ static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; ++@@ -413,17 +412,25 @@ expand_dynamic_library_name(const char *name) ++ ++ Assert(name); ++ +++ /* +++ * If the value starts with "$libdir/", strip that. This is because many +++ * extensions have hardcoded '$libdir/foo' as their library name, which +++ * prevents using the path. +++ */ +++ if (strncmp(name, "$libdir/", 8) == 0) +++ name += 8; +++ ++ have_slash = (first_dir_separator(name) != NULL); ++ ++ if (!have_slash) ++ { ++- full = find_in_dynamic_libpath(name); +++ full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); ++ if (full) ++ return full; ++ } ++ else ++ { ++- full = substitute_libpath_macro(name); +++ full = substitute_path_macro(name, "$libdir", pkglib_path); ++ if (pg_file_exists(full)) ++ return full; ++ pfree(full); ++@@ -433,14 +440,14 @@ expand_dynamic_library_name(const char *name) ++ ++ if (!have_slash) ++ { ++- full = find_in_dynamic_libpath(new); +++ full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); ++ pfree(new); ++ if (full) ++ return full; ++ } ++ else ++ { ++- full = substitute_libpath_macro(new); +++ full = substitute_path_macro(new, "$libdir", pkglib_path); ++ pfree(new); ++ if (pg_file_exists(full)) ++ return full; ++@@ -551,13 +571,13 @@ find_in_dynamic_libpath(const char *basename) ++ if (!is_absolute_path(mangled)) ++ ereport(ERROR, ++ (errcode(ERRCODE_INVALID_NAME), ++- errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); +++ errmsg("component in parameter \"%s\" is not an absolute path", path_param))); ++ ++ full = palloc(strlen(mangled) + 1 + baselen + 1); ++ sprintf(full, "%s/%s", mangled, basename); ++ pfree(mangled); ++ ++- elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); +++ elog(DEBUG3, "%s: trying \"%s\"", __func__, full); ++ ++ if (pg_file_exists(full)) ++ return full; +diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c +index b078b934a79..f7208b9777d 100644 +--- a/src/backend/utils/misc/guc_tables.c ++++ b/src/backend/utils/misc/guc_tables.c +@@ -37,6 +37,7 @@ + #include "catalog/namespace.h" + #include "catalog/storage.h" + #include "commands/async.h" ++#include "commands/extension.h" + #include "commands/tablespace.h" + #include "commands/trigger.h" + #include "commands/user.h" +@@ -4024,6 +4025,18 @@ struct config_string ConfigureNamesString[] = + NULL, NULL, NULL + }, + ++ { ++ {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, ++ gettext_noop("Sets the path for extension control files."), ++ gettext_noop("The remaining extension script and secondary control files are then loaded " ++ "from the same directory where the primary control file was found."), ++ GUC_SUPERUSER_ONLY ++ }, ++ &Extension_control_path, ++ "$system", ++ NULL, NULL, NULL ++ }, ++ + { + {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, + gettext_noop("Sets the location of the Kerberos server key file."), +diff --git a/src/backend/utils/misc/guc_tables.c.rej b/src/backend/utils/misc/guc_tables.c.rej +new file mode 100644 +index 00000000000..4a195151820 +--- /dev/null ++++ b/src/backend/utils/misc/guc_tables.c.rej +@@ -0,0 +1,9 @@ ++diff a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c (rejected hunks) ++@@ -39,6 +39,7 @@ ++ #include "catalog/namespace.h" ++ #include "catalog/storage.h" ++ #include "commands/async.h" +++#include "commands/extension.h" ++ #include "commands/event_trigger.h" ++ #include "commands/tablespace.h" ++ #include "commands/trigger.h" +diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample +index 904e9187854..0e0c61545d9 100644 +--- a/src/backend/utils/misc/postgresql.conf.sample ++++ b/src/backend/utils/misc/postgresql.conf.sample +@@ -750,6 +750,7 @@ + # - Other Defaults - + + #dynamic_library_path = '$libdir' ++#extension_control_path = '$system' + #gin_fuzzy_search_limit = 0 + + +diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h +index 74ae3913958..d16eb65b51a 100644 +--- a/src/include/commands/extension.h ++++ b/src/include/commands/extension.h +@@ -17,6 +17,8 @@ + #include "catalog/objectaddress.h" + #include "parser/parse_node.h" + ++/* GUC */ ++extern PGDLLIMPORT char *Extension_control_path; + + /* + * creating_extension is only true while running a CREATE EXTENSION or ALTER +diff --git a/src/include/fmgr.h b/src/include/fmgr.h +index b120f5e7fef..419fc4ad98c 100644 +--- a/src/include/fmgr.h ++++ b/src/include/fmgr.h +@@ -741,6 +741,8 @@ extern bool CheckFunctionValidatorAccess(Oid validatorOid, Oid functionOid); + */ + extern PGDLLIMPORT char *Dynamic_library_path; + ++extern char *find_in_path(const char *basename, const char *path, const char *path_param, ++ const char *macro, const char *macro_val); + extern void *load_external_function(const char *filename, const char *funcname, + bool signalNotFound, void **filehandle); + extern void *lookup_external_function(void *filehandle, const char *funcname); +-- +2.42.0 + diff --git a/nix/pgExtension.nix b/nix/pgExtension.nix deleted file mode 100644 index 4af9892..0000000 --- a/nix/pgExtension.nix +++ /dev/null @@ -1,17 +0,0 @@ -{ stdenv, postgresql, mustach, extensionName }: - -stdenv.mkDerivation { - name = extensionName; - - buildInputs = [ postgresql mustach ]; - - src = ../.; - - installPhase = '' - mkdir -p $out/bin - install -D ${extensionName}.so -t $out/lib - - install -D -t $out/share/postgresql/extension sql/${extensionName}--*.sql - install -D -t $out/share/postgresql/extension ${extensionName}.control - ''; -} diff --git a/nix/pgScript.nix b/nix/pgScript.nix deleted file mode 100644 index e142cc4..0000000 --- a/nix/pgScript.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ postgresql, writeShellScriptBin, options ? "" } : - -let - ver = builtins.head (builtins.splitVersion postgresql.version); - script = '' - export PATH=${postgresql}/bin:"$PATH" - - tmpdir="$(mktemp -d)" - - export PGDATA="$tmpdir" - export PGHOST="$tmpdir" - export PGUSER=postgres - export PGDATABASE=postgres - - trap 'pg_ctl stop -m i && rm -rf "$tmpdir"' sigint sigterm exit - - PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" - - default_options="-F -c listen_addresses=\"\" -k $PGDATA" - - pg_ctl start -o "$default_options" -o "${options}" - - "$@" - ''; -in -writeShellScriptBin "with-pg-${ver}" script diff --git a/shell.nix b/shell.nix index a954cb9..7bced37 100644 --- a/shell.nix +++ b/shell.nix @@ -3,22 +3,13 @@ with import (builtins.fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/24.05.tar.gz"; sha256 = "sha256:1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; }) {}; -mkShell { +mkShell +{ buildInputs = - let - extensionName = "plmustache"; - supportedPgVersions = [ - postgresql_16 - postgresql_15 - postgresql_14 - postgresql_13 - postgresql_12 - ]; - mustach = callPackage ./nix/mustach.nix {}; - pgWExtension = { postgresql }: postgresql.withPackages (p: [ (callPackage ./nix/pgExtension.nix { inherit postgresql extensionName mustach; }) ]); - extAll = map (x: callPackage ./nix/pgScript.nix { postgresql = pgWExtension { postgresql = x;}; }) supportedPgVersions; - in - extAll; + [ + (callPackage ./nix/mustach.nix {}) + ] ++ + (callPackage ./nix/nxpg.nix {}); shellHook = '' export HISTFILE=.history