1
0
mirror of https://github.com/postgres/postgres.git synced 2025-06-29 10:41:53 +03:00

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 <peter@eisentraut.org>
Co-authored-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: David E. Wheeler <david@justatheory.com>
Reviewed-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com>
Reviewed-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Reviewed-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
Discussion: https://www.postgresql.org/message-id/flat/E7C7BFFB-8857-48D4-A71F-88B359FADCFD@justatheory.com
This commit is contained in:
Peter Eisentraut
2025-03-19 06:57:20 +01:00
parent 2cce0fe440
commit 4f7f7b0375
13 changed files with 526 additions and 199 deletions

View File

@ -10954,6 +10954,74 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
</listitem>
</varlistentry>
<varlistentry id="guc-extension-control-path" xreflabel="extension_control_path">
<term><varname>extension_control_path</varname> (<type>string</type>)
<indexterm>
<primary><varname>extension_control_path</varname> configuration parameter</primary>
</indexterm>
</term>
<listitem>
<para>
A path to search for extensions, specifically extension control files
(<filename><replaceable>name</replaceable>.control</filename>). The
remaining extension script and secondary control files are then loaded
from the same directory where the primary control file was found.
See <xref linkend="extend-extensions-files"/> for details.
</para>
<para>
The value for <varname>extension_control_path</varname> 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 <literal>$system</literal>, the
compiled-in <productname>PostgreSQL</productname> extension
directory is substituted for <literal>$system</literal>; this
is where the extensions provided by the standard
<productname>PostgreSQL</productname> distribution are installed.
(Use <literal>pg_config --sharedir</literal> to find out the name of
this directory.) For example:
<programlisting>
extension_control_path = '/usr/local/share/postgresql/extension:/home/my_project/share/extension:$system'
</programlisting>
or, in a Windows environment:
<programlisting>
extension_control_path = 'C:\tools\postgresql\extension;H:\my_project\share\extension;$system'
</programlisting>
Note that the path elements should typically end in
<literal>extension</literal> if the normal installation layouts are
followed. (The value for <literal>$system</literal> already includes
the <literal>extension</literal> suffix.)
</para>
<para>
The default value for this parameter is
<literal>'$system'</literal>. If the value is set to an empty
string, the default <literal>'$system'</literal> is also assumed.
</para>
<para>
This parameter can be changed at run time by superusers and users
with the appropriate <literal>SET</literal> 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 <filename>postgresql.conf</filename> configuration
file.
</para>
<para>
Note that if you set this parameter to be able to load extensions from
nonstandard locations, you will most likely also need to set <xref
linkend="guc-dynamic-library-path"/> to a correspondent location, for
example,
<programlisting>
extension_control_path = '/usr/local/share/postgresql/extension:$system'
dynamic_library_path = '/usr/local/lib/postgresql:$libdir'
</programlisting>
</para>
</listitem>
</varlistentry>
<varlistentry id="guc-gin-fuzzy-search-limit" xreflabel="gin_fuzzy_search_limit">
<term><varname>gin_fuzzy_search_limit</varname> (<type>integer</type>)
<indexterm>

View File

@ -649,6 +649,11 @@ RETURNS anycompatible AS ...
control file can specify a different directory for the script file(s).
</para>
<para>
Additional locations for extension control files can be configured using
the parameter <xref linkend="guc-extension-control-path"/>.
</para>
<para>
The file format for an extension control file is the same as for the
<filename>postgresql.conf</filename> file, namely a list of
@ -669,9 +674,9 @@ RETURNS anycompatible AS ...
<para>
The directory containing the extension's <acronym>SQL</acronym> script
file(s). Unless an absolute path is given, the name is relative to
the installation's <literal>SHAREDIR</literal> directory. The
default behavior is equivalent to specifying
<literal>directory = 'extension'</literal>.
the installation's <literal>SHAREDIR</literal> directory. By default,
the script files are looked for in the same directory where the
control file was found.
</para>
</listitem>
</varlistentry>
@ -719,8 +724,8 @@ RETURNS anycompatible AS ...
<para>
The value of this parameter will be substituted for each occurrence
of <literal>MODULE_PATHNAME</literal> in the script file(s). If it is not
set, no substitution is made. Typically, this is set to
<literal>$libdir/<replaceable>shared_library_name</replaceable></literal> and
set, no substitution is made. Typically, this is set to just
<literal><replaceable>shared_library_name</replaceable></literal> and
then <literal>MODULE_PATHNAME</literal> is used in <command>CREATE
FUNCTION</command> 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 <varname>PG_CONFIG</varname> to point to its
<command>pg_config</command> program, either within the makefile
or on the <literal>make</literal> command line.
You can also select a separate installation directory for your extension
by setting the <literal>make</literal> variable <varname>prefix</varname>
on the <literal>make</literal> command line. (But this will then require
additional setup to get the server to find the extension there.)
</para>
<para>

View File

@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the extension to be
installed. <productname>PostgreSQL</productname> will create the
extension using details from the file
<literal>SHAREDIR/extension/</literal><replaceable class="parameter">extension_name</replaceable><literal>.control</literal>.
extension using details from the file <filename><replaceable
class="parameter">extension_name</replaceable>.control</filename>,
found via the server's extension control path (set by <xref
linkend="guc-extension-control-path"/>.)
</para>
</listitem>
</varlistentry>

View File

@ -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)

View File

@ -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 (version)
if (errno == ENOENT && 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.")));
}
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,19 +2205,22 @@ 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();
locations = get_extension_control_directories();
foreach_ptr(char, location, locations)
{
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 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)
{
@ -2159,7 +2246,9 @@ pg_available_extensions(PG_FUNCTION_ARGS)
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));
@ -2184,6 +2273,7 @@ pg_available_extensions(PG_FUNCTION_ARGS)
FreeDir(dir);
}
}
return (Datum) 0;
}
@ -2201,19 +2291,22 @@ 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();
locations = get_extension_control_directories();
foreach_ptr(char, location, locations)
{
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 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)
{
@ -2238,7 +2331,9 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
continue;
/* read the control file */
control = read_extension_control_file(extname);
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,
@ -2247,6 +2342,7 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
FreeDir(dir);
}
}
return (Datum) 0;
}
@ -2373,11 +2469,14 @@ bool
extension_file_exists(const char *extensionName)
{
bool result = false;
char *location;
List *locations;
DIR *dir;
struct dirent *de;
location = get_extension_control_directory();
locations = get_extension_control_directories();
foreach_ptr(char, location, locations)
{
dir = AllocateDir(location);
/*
@ -2415,6 +2514,9 @@ extension_file_exists(const char *extensionName)
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;
}

View File

@ -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 = Dynamic_library_path;
p = 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;

View File

@ -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."),

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -57,4 +57,9 @@ tests += {
],
'regress_args': ['--no-locale'],
},
'tap': {
'tests': [
't/001_extension_control_path.pl',
],
},
}

View File

@ -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();