From 96f7be3962ceb3caccfcec5473bf5fcf9c9d666e Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v5] extension_control_path The new GUC extension_control_path specifies a path to look for extension control files. The default value is $system, which looks in the compiled-in location, as before. The path search uses the same code and works in the same way as dynamic_library_path. Discussion: https://www.postgresql.org/message-id/flat/E7C7BFFB-8857-48D4-A71F-88B359FADCFD@justatheory.com --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 348 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 76 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 67 ++++ 13 files changed, 453 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e55700f35b8..69c23f17aef 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10726,6 +10726,74 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir' + + extension_control_path (string) + + extension_control_path configuration parameter + + + + + A path to search for extensions, specifically extension control files + (name.control). The + remaining extension script and secondary control files are then loaded + from the same directory where the primary control file was found. + See for details. + + + + The value for extension_control_path must be a + list of absolute directory paths separated by colons (or semi-colons + on Windows). If a list element starts + with the special string $system, the + compiled-in PostgreSQL extension + directory is substituted for $system; this + is where the extensions provided by the standard + PostgreSQL distribution are installed. + (Use pg_config --sharedir to find out the name of + this directory.) For example: + +extension_control_path = '/usr/local/share/postgresql/extension:/home/my_project/share/extension:$system' + + or, in a Windows environment: + +extension_control_path = 'C:\tools\postgresql\extension;H:\my_project\share\extension;$system' + + Note that the path elements should typically end in + extension if the normal installation layouts are + followed. (The value for $system already includes + the extension suffix.) + + + + The default value for this parameter is + '$system'. If the value is set to an empty + string, the default '$system' is also assumed. + + + + This parameter can be changed at run time by superusers and users + with the appropriate SET privilege, but a + setting done that way will only persist until the end of the + client connection, so this method should be reserved for + development purposes. The recommended way to set this parameter + is in the postgresql.conf configuration + file. + + + + Note that if you set this parameter to be able to load extensions from + nonstandard locations, you will most likely also need to set to a correspondent location, for + example, + +extension_control_path = '/usr/local/share/postgresql/extension:$system' +dynamic_library_path = '/usr/local/lib/postgresql:$libdir' + + + + + gin_fuzzy_search_limit (integer) diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). + + Additional locations for extension control files can be configured using + the parameter . + + The file format for an extension control file is the same as for the postgresql.conf file, namely a list of @@ -669,9 +674,9 @@ RETURNS anycompatible AS ... The directory containing the extension's SQL script file(s). Unless an absolute path is given, the name is relative to - the installation's SHAREDIR directory. The - default behavior is equivalent to specifying - directory = 'extension'. + the installation's SHAREDIR directory. By default, + the script files are looked for in the same directory where the + control file was found. @@ -719,8 +724,8 @@ RETURNS anycompatible AS ... The value of this parameter will be substituted for each occurrence of MODULE_PATHNAME in the script file(s). If it is not - set, no substitution is made. Typically, this is set to - $libdir/shared_library_name and + set, no substitution is made. Typically, this is set to just + shared_library_name and then MODULE_PATHNAME is used in CREATE FUNCTION commands for C-language functions, so that the script files do not need to hard-wire the name of the shared library. @@ -1804,6 +1809,10 @@ include $(PGXS) setting PG_CONFIG to point to its pg_config program, either within the makefile or on the make command line. + You can also select a separate installation directory for your extension + by setting the make variable prefix + on the make command line. (But this will then require + additional setup to get the server to find the extension there.) diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] extension_name The name of the extension to be installed. PostgreSQL will create the - extension using details from the file - SHAREDIR/extension/extension_name.control. + extension using details from the file extension_name.control, + found via the server's extension control path (set by .) diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # 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 d9bb4ce5f1e..7e8a28e4064 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,6 +51,7 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" +#include "nodes/pg_list.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" @@ -69,6 +70,9 @@ #include "utils/varlena.h" +/* GUC */ +char *Extension_control_path; + /* Globally visible state variables */ bool creating_extension = false; Oid CurrentExtensionObject = InvalidOid; @@ -79,6 +83,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 @@ -328,29 +333,88 @@ is_extension_script_filename(const char *filename) return (extension != NULL) && (strcmp(extension, ".sql") == 0); } -static char * -get_extension_control_directory(void) +/* + * Return a list of directories declared on extension_control_path GUC. + */ +static List * +get_extension_control_directories(void) { char sharepath[MAXPGPATH]; - char *result; + char *system_dir; + char *ecp; + char *token; + char *path; + List *paths = NIL; get_share_path(my_exec_path, sharepath); - result = (char *) palloc(MAXPGPATH); - snprintf(result, MAXPGPATH, "%s/extension", sharepath); - return result; + system_dir = psprintf("%s/extension", sharepath); + + if (strlen(Extension_control_path) == 0) + { + paths = lappend(paths, system_dir); + } + else + { + /* Duplicate the string so we can modify it */ + ecp = pstrdup(Extension_control_path); + + /* Consume each path between ':' */ + for (token = strtok(ecp, ":"); token != NULL; token = strtok(NULL, ":")) + { + if (strcmp(token, "$system") == 0) + path = system_dir; + else + path = pstrdup(token); + + paths = lappend(paths, path); + } + + pfree(ecp); + } + + return paths; } +/* + * 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; } @@ -366,7 +430,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); @@ -444,27 +508,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", @@ -2121,68 +2183,75 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; DIR *dir; struct dirent *de; + List *locations; + ListCell *cell; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - dir = AllocateDir(location); + locations = get_extension_control_directories(); - /* - * If the control directory doesn't exist, we want to silently return an - * empty set. Any other error will be reported by ReadDir. - */ - if (dir == NULL && errno == ENOENT) - { - /* do nothing */ - } - else + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + dir = AllocateDir(location); + + /* + * If the control directory doesn't exist, we want to silently return + * an empty set. Any other error will be reported by ReadDir. + */ + if (dir == NULL && errno == ENOENT) { - ExtensionControlFile *control; - char *extname; - Datum values[3]; - bool nulls[3]; + /* do nothing */ + } + else + { + while ((de = ReadDir(dir, location)) != NULL) + { + ExtensionControlFile *control; + char *extname; + Datum values[3]; + bool nulls[3]; - if (!is_extension_control_filename(de->d_name)) - continue; + if (!is_extension_control_filename(de->d_name)) + continue; - /* extract extension name from 'name.control' filename */ - extname = pstrdup(de->d_name); - *strrchr(extname, '.') = '\0'; + /* extract extension name from 'name.control' filename */ + extname = pstrdup(de->d_name); + *strrchr(extname, '.') = '\0'; - /* ignore it if it's an auxiliary control file */ - if (strstr(extname, "--")) - continue; + /* ignore it if it's an auxiliary control file */ + if (strstr(extname, "--")) + continue; - control = read_extension_control_file(extname); + control = read_extension_control_file(extname); - memset(values, 0, sizeof(values)); - memset(nulls, 0, sizeof(nulls)); + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); - /* name */ - values[0] = DirectFunctionCall1(namein, - CStringGetDatum(control->name)); - /* default_version */ - if (control->default_version == NULL) - nulls[1] = true; - else - values[1] = CStringGetTextDatum(control->default_version); - /* comment */ - if (control->comment == NULL) - nulls[2] = true; - else - values[2] = CStringGetTextDatum(control->comment); + /* name */ + values[0] = DirectFunctionCall1(namein, + CStringGetDatum(control->name)); + /* default_version */ + if (control->default_version == NULL) + nulls[1] = true; + else + values[1] = CStringGetTextDatum(control->default_version); + /* comment */ + if (control->comment == NULL) + nulls[2] = true; + else + values[2] = CStringGetTextDatum(control->comment); - tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, - values, nulls); - } + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } - FreeDir(dir); + FreeDir(dir); + } } return (Datum) 0; @@ -2201,51 +2270,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - dir = AllocateDir(location); - - /* - * If the control directory doesn't exist, we want to silently return an - * empty set. Any other error will be reported by ReadDir. - */ - if (dir == NULL && errno == ENOENT) - { - /* do nothing */ - } - else + locations = get_extension_control_directories(); + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + dir = AllocateDir(location); + + /* + * If the control directory doesn't exist, we want to silently return + * an empty set. Any other error will be reported by ReadDir. + */ + if (dir == NULL && errno == ENOENT) + { + /* do nothing */ + } + else { - ExtensionControlFile *control; - char *extname; + while ((de = ReadDir(dir, location)) != NULL) + { + ExtensionControlFile *control; + char *extname; - if (!is_extension_control_filename(de->d_name)) - continue; + if (!is_extension_control_filename(de->d_name)) + continue; - /* extract extension name from 'name.control' filename */ - extname = pstrdup(de->d_name); - *strrchr(extname, '.') = '\0'; + /* extract extension name from 'name.control' filename */ + extname = pstrdup(de->d_name); + *strrchr(extname, '.') = '\0'; - /* ignore it if it's an auxiliary control file */ - if (strstr(extname, "--")) - continue; + /* ignore it if it's an auxiliary control file */ + if (strstr(extname, "--")) + continue; - /* read the control file */ - control = read_extension_control_file(extname); + /* read the control file */ + control = read_extension_control_file(extname); - /* scan extension's script directory for install scripts */ - get_available_versions_for_extension(control, rsinfo->setResult, - rsinfo->setDesc); - } + /* scan extension's script directory for install scripts */ + get_available_versions_for_extension(control, rsinfo->setResult, + rsinfo->setDesc); + } - FreeDir(dir); + FreeDir(dir); + } } return (Datum) 0; @@ -2373,47 +2448,56 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; - location = get_extension_control_directory(); - dir = AllocateDir(location); + locations = get_extension_control_directories(); - /* - * If the control directory doesn't exist, we want to silently return - * false. Any other error will be reported by ReadDir. - */ - if (dir == NULL && errno == ENOENT) - { - /* do nothing */ - } - else + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + dir = AllocateDir(location); + + /* + * If the control directory doesn't exist, we want to silently return + * false. Any other error will be reported by ReadDir. + */ + if (dir == NULL && errno == ENOENT) { - char *extname; + /* do nothing */ + } + else + { + while ((de = ReadDir(dir, location)) != NULL) + { + char *extname; - if (!is_extension_control_filename(de->d_name)) - continue; + if (!is_extension_control_filename(de->d_name)) + continue; - /* extract extension name from 'name.control' filename */ - extname = pstrdup(de->d_name); - *strrchr(extname, '.') = '\0'; + /* extract extension name from 'name.control' filename */ + extname = pstrdup(de->d_name); + *strrchr(extname, '.') = '\0'; - /* ignore it if it's an auxiliary control file */ - if (strstr(extname, "--")) - continue; + /* ignore it if it's an auxiliary control file */ + if (strstr(extname, "--")) + continue; - /* done if it matches request */ - if (strcmp(extname, extensionName) == 0) - { - result = true; - break; + /* done if it matches request */ + if (strcmp(extname, extensionName) == 0) + { + result = true; + break; + } } - } - FreeDir(dir); + FreeDir(dir); + } + if (result) + break; } return result; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..46a46715ec7 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -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; @@ -398,7 +397,7 @@ incompatible_module_error(const char *libname, /* * 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. * @@ -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; @@ -475,47 +482,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; @@ -532,7 +552,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); @@ -542,7 +562,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); @@ -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 ad25cbb39c5..c357a5304ae 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -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" @@ -4314,6 +4315,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/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 5362ff80519..24eaeb3dc6b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -791,6 +791,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - 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 0b636405120..24419bfb5c9 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 e609ea875a7..5811307a82c 100644 --- a/src/include/fmgr.h +++ b/src/include/fmgr.h @@ -740,6 +740,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); diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..f857e2140df --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); + +$node->init; + +# Create a temporary directory for the extension control file +my $ext_dir = PostgreSQL::Test::Utils::tempdir(); +my $ext_name = "test_custom_ext_paths"; +my $control_file = "$ext_dir/$ext_name.control"; +my $sql_file = "$ext_dir/$ext_name--1.0.sql"; + +# Create .control .sql file +open my $cf, '>', $control_file or die "Could not create control file: $!"; +print $cf "comment = 'Test extension_control_path'\n"; +print $cf "default_version = '1.0'\n"; +print $cf "relocatable = true\n"; +close $cf; + +# Create --1.0.sql file +open my $sqlf, '>', $sql_file or die "Could not create sql file: $!"; +print $sqlf "/* $sql_file */\n"; +print $sqlf "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n"; +print $sqlf qq'\\echo Use "CREATE EXTENSION $ext_name" to load this file. \\quit\n'; +close $sqlf; + +# Use the correct separator and escape \ when running on Windows. +my $sep = $windows_os ? ";" : ":"; +$node->append_conf( + 'postgresql.conf', qq{ +extension_control_path = '\$system$sep@{[ $windows_os ? ($ext_dir =~ s/\\/\\\\/gr) : $ext_dir ]}' +}); + +# Start node +$node->start; + +my $ecp = $node->safe_psql('postgres', 'show extension_control_path;'); + +is($ecp, "\$system$sep$ext_dir", "Custom extension control directory path configured"); + +$node->safe_psql( + 'postgres', + "CREATE EXTENSION $ext_name"); + +my $ret = $node->safe_psql( + 'postgres', + "select * from pg_available_extensions where name = '$ext_name'"); +is( + $ret, + "test_custom_ext_paths|1.0|1.0|Test extension_control_path", + "Extension is installed correctly on pg_available_extensions"); + +my $ret2 = $node->safe_psql( + 'postgres', + "select * from pg_available_extension_versions where name = '$ext_name'"); +is( + $ret2, + "test_custom_ext_paths|1.0|t|t|f|t|||Test extension_control_path", + "Extension is installed correctly on pg_available_extension_versions"); + +done_testing(); -- 2.39.5 (Apple Git-154)