From: Peter Eisentraut Date: Wed, 19 Mar 2025 05:57:20 +0000 (+0100) Subject: extension_control_path X-Git-Tag: REL_18_BETA1~512 X-Git-Url: http://git.postgresql.org/gitweb/?a=commitdiff_plain;h=4f7f7b0375854e2f89876473405a8f21c95012af;p=postgresql.git 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. Some use cases of this are: (1) testing extensions during package builds, (2) installing extensions outside security-restricted containers like Python.app (on macOS), (3) adding extensions to PostgreSQL running in a Kubernetes environment using operators such as CloudNativePG without having to rebuild the base image for each new extension. There is also a tweak in Makefile.global so that it is possible to install extensions using PGXS into an different directory than the default, using 'make install prefix=/else/where'. This previously only worked when specifying the subdirectories, like 'make install datadir=/else/where/share pkglibdir=/else/where/lib', for purely implementation reasons. (Of course, without the path feature, installing elsewhere was rarely useful.) Author: Peter Eisentraut Co-authored-by: Matheus Alcantara Reviewed-by: David E. Wheeler Reviewed-by: Gabriele Bartolini Reviewed-by: Marco Nenciarini Reviewed-by: Niccolò Fei Discussion: https://www.postgresql.org/message-id/flat/E7C7BFFB-8857-48D4-A71F-88B359FADCFD@justatheory.com --- diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 4fd377d1c5f..873290daa61 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10954,6 +10954,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..dc38c325770 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -54,6 +54,7 @@ #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" #include "nodes/queryjumble.h" #include "storage/fd.h" #include "tcop/utility.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 @@ -146,6 +151,7 @@ static void ExecAlterExtensionContentsRecurse(AlterExtensionContentsStmt *stmt, ObjectAddress extension, ObjectAddress object); static char *read_whole_file(const char *filename, int *length); +static ExtensionControlFile *new_ExtensionControlFile(const char *extname); /* @@ -328,29 +334,104 @@ 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; + 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); + + for (;;) + { + int len; + char *mangled; + char *piece = first_path_var_separator(ecp); + + /* Get the length of the next path on ecp */ + if (piece == NULL) + len = strlen(ecp); + else + len = piece - ecp; + + /* Copy the next path found on ecp */ + piece = palloc(len + 1); + strlcpy(piece, ecp, len + 1); + + /* Substitute the path macro if needed */ + mangled = substitute_path_macro(piece, "$system", system_dir); + pfree(piece); + + /* Canonicalize the path based on the OS and add to the list */ + canonicalize_path(mangled); + paths = lappend(paths, mangled); + + /* Break if ecp is empty or move to the next path on ecp */ + if (ecp[len] == '\0') + break; + else + ecp += len + 1; + } + } + + 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, ecp, "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 +447,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); @@ -424,6 +505,11 @@ get_extension_script_filename(ExtensionControlFile *control, * fields of *control. We parse primary file if version == NULL, * else the optional auxiliary file for that version. * + * The control file will be search on Extension_control_path paths if + * control->control_dir is NULL, otherwise it will use the value of control_dir + * to read and parse the .control file, so it assume that the control_dir is a + * valid path for the control file being parsed. + * * Control files are supposed to be very short, half a dozen lines, * so we don't worry about memory allocation risks here. Also we don't * worry about what encoding it's in; all values are expected to be ASCII. @@ -444,27 +530,35 @@ parse_extension_control_file(ExtensionControlFile *control, if (version) filename = get_extension_aux_control_filename(control, version); else - filename = get_extension_control_filename(control->name); + { + /* + * If control_dir is already set, use it, else do a path search. + */ + if (control->control_dir) + { + filename = psprintf("%s/%s.control", control->control_dir, control->name); + } + else + 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", @@ -603,17 +697,7 @@ parse_extension_control_file(ExtensionControlFile *control, static ExtensionControlFile * read_extension_control_file(const char *extname) { - ExtensionControlFile *control; - - /* - * Set up default values. Pointer fields are initially null. - */ - control = (ExtensionControlFile *) palloc0(sizeof(ExtensionControlFile)); - control->name = pstrdup(extname); - control->relocatable = false; - control->superuser = true; - control->trusted = false; - control->encoding = -1; + ExtensionControlFile *control = new_ExtensionControlFile(extname); /* * Parse the primary control file. @@ -2121,68 +2205,74 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; DIR *dir; struct dirent *de; /* 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) + foreach_ptr(char, location, locations) { - /* do nothing */ - } - else - { - while ((de = ReadDir(dir, location)) != NULL) + 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 = new_ExtensionControlFile(extname); + control->control_dir = pstrdup(location); + parse_extension_control_file(control, NULL); - 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 +2291,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; DIR *dir; struct dirent *de; /* 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_ptr(char, location, locations) { - while ((de = ReadDir(dir, location)) != NULL) + 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 = new_ExtensionControlFile(extname); + control->control_dir = pstrdup(location); + parse_extension_control_file(control, NULL); - /* 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 +2469,53 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; 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_ptr(char, location, locations) { - while ((de = ReadDir(dir, location)) != NULL) + 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; @@ -3691,3 +3793,20 @@ read_whole_file(const char *filename, int *length) *length = bytes_to_read; return buf; } + +static ExtensionControlFile * +new_ExtensionControlFile(const char *extname) +{ + /* + * Set up default values. Pointer fields are initially null. + */ + ExtensionControlFile *control = palloc0_object(ExtensionControlFile); + + control->name = pstrdup(extname); + control->relocatable = false; + control->superuser = true; + control->trusted = false; + control->encoding = -1; + + return control; +} diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 4409e3e6fa8..dd4c83d1bba 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,6 @@ pg_noreturn static void incompatible_module_error(const char *libname, const Pg_magic_struct *module_magic_data); 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); /* Magic structure that module needs to match to be accepted */ static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; @@ -398,7 +396,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 +411,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 +439,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; @@ -474,48 +480,61 @@ check_restricted_library_name(const char *name) * Substitute for any macros appearing in the given string. * Result is always freshly palloc'd. */ -static char * -substitute_libpath_macro(const char *name) +char * +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 +551,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 +561,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 +570,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 ead80257192..cc8f2b1230a 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" @@ -4364,6 +4365,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 6abd1baeac8..ad54585cf1d 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -804,6 +804,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..82ee38b31e5 100644 --- a/src/include/fmgr.h +++ b/src/include/fmgr.h @@ -740,6 +740,9 @@ extern bool CheckFunctionValidatorAccess(Oid validatorOid, Oid functionOid); */ extern PGDLLIMPORT char *Dynamic_library_path; +extern char *substitute_path_macro(const char *str, const char *macro, const char *value); +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..7160009739a --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,80 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +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"); + +# Ensure that extensions installed on $system is still visible when using with +# custom extension control path. +my $ret3 = $node->safe_psql('postgres', + "select count(*) > 0 as ok from pg_available_extensions where name = 'amcheck'" +); +is($ret3, "t", + "\$system extension is installed correctly on pg_available_extensions"); + + +my $ret4 = $node->safe_psql('postgres', + "set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'amcheck'" +); +is($ret4, "t", + "\$system extension is installed correctly on pg_available_extensions with empty extension_control_path" +); + +done_testing();