extension_control_path
authorPeter Eisentraut <peter@eisentraut.org>
Wed, 19 Mar 2025 05:57:20 +0000 (06:57 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Wed, 19 Mar 2025 06:03:20 +0000 (07:03 +0100)
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

13 files changed:
doc/src/sgml/config.sgml
doc/src/sgml/extend.sgml
doc/src/sgml/ref/create_extension.sgml
src/Makefile.global.in
src/backend/commands/extension.c
src/backend/utils/fmgr/dfmgr.c
src/backend/utils/misc/guc_tables.c
src/backend/utils/misc/postgresql.conf.sample
src/include/commands/extension.h
src/include/fmgr.h
src/test/modules/test_extensions/Makefile
src/test/modules/test_extensions/meson.build
src/test/modules/test_extensions/t/001_extension_control_path.pl [new file with mode: 0644]

index 4fd377d1c5fef4dfe4ad23ab83c2505a91782db7..873290daa613b38b22fded0cb456f60fa6e735d3 100644 (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>
index ba492ca27c089fa780fd2a923b5781ec8412ddb7..64f8e133caeb3a39e711794a085be2571bc71574 100644 (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>
index ca2b80d669c5ee568491384751fe54bc9cfe074a..713abd9c49447003d1687c85929f7a9e2119f4c7 100644 (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>
index 3b620bac5acd4c01b3d19e2b355d036800dc8335..8fe9d61e82a7aada140b62931fd3a2c2cec7ad43 100644 (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)
index d9bb4ce5f1eea2664a94e260aeb28aa9b4ee8e9b..dc38c32577010bdf65e5dad76a1a2f76f5b346b6 100644 (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 (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;
+}
index 4409e3e6fa8ab059d05ef18b03e3374a751b6a62..dd4c83d1bba603897fa0bcae5d76b6464ec9fb6f 100644 (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 = 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;
index ead8025719260cf055e4353ed95aa8fee5935662..cc8f2b1230a2b56b04808efc7c42a64153c58427 100644 (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."),
index 6abd1baeac857957bde81b67d6f41e4a4bd03c0f..ad54585cf1d0632d7a23f3b16d9ec200e912520b 100644 (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
 
 
index 0b6364051201951b0ab082b973182c0f55bdb6df..24419bfb5c90247b62a8d67e193427f7eed9eaf9 100644 (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
index e609ea875a759e4a010666a9fbc776539bf75c33..82ee38b31e53fef108962cd8c7a7e6ebffb6d96a 100644 (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);
index 1dbec14cba3f92884e5dd9749bba582dd4e85039..a3591bf3d2f357b1e117e8a414ccaff0c1f72e05 100644 (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
index dd7ec0ce56b38dd4e0f9a71779bbca4675ab8bc0..3c7e378bf35958d1a8ed12edb7a783cc11526bdc 100644 (file)
@@ -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 (file)
index 0000000..7160009
--- /dev/null
@@ -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();