I implemented a patch along the lines Craig had suggested. It's a new GUC variable that is a path for extension control files. It's called extension_control_path, and it works exactly the same way as dynamic_library_path. Except that the magic token is called $system instead of $libdir. In fact, most of the patch is refactoring the routines in dfmgr.c to not hardcode dynamic_library_path but allow searching for any file in any path. Once a control file is found, the other extension support files (script files and auxiliary control files) are looked for in the same directory.

This works pretty much fine for the use cases that have been presented here, including installing extensions outside of the core installation tree (for CNPG and Postgres.app) and for testing uninstalled extensions (for Debian).

There are some TODOs in the patch. Some of those are for documentation that needs to be completed. Others are for functions like pg_available_extensions() that need to be rewritten to be aware of the path. I think this would be pretty straightforward.

Some open problems or discussion points:

- You can install extensions into alternative directories using PGXS like

    make install datadir=/else/where/share pkglibdir=/else/where/lib

This works.  I was hoping it would work to use

    make install prefix=/else/where

but that doesn't because of some details in Makefile.global. I think we can tweak that a little bit to make that work too.

- With the current patch, if you install into datadir=/else/where/share, then you need to set extension_control_path=/else/where/share/extension. This is a bit confusing. Maybe we want to make the "extension" part implicit.

- The biggest problem is that many extensions set in their control file

    module_pathname = '$libdir/foo'

This disables the use of dynamic_library_path, so this whole idea of installing an extension elsewhere won't work that way. The obvious solution is that extensions change this to just 'foo'. But this will require a lot updating work for many extensions, or a lot of patching by packagers.

Maybe we could devise some sort of rule that if the extension control file is found via the path outside of $system, then the leading $libdir is ignored.
From 08620bae551bb6b2e3a44aa3df4c8751ad06f804 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <pe...@eisentraut.org>
Date: Mon, 11 Nov 2024 07:29:47 +0100
Subject: [PATCH v0] extension_control_path

The new GUC extension_control_path specifies a path to look for
extension control files.  The default value is $system, which looks in
the compiled-in location, as before.

The path search uses the same code and works in the same way as
dynamic_library_path.

Discussion: 
https://www.postgresql.org/message-id/flat/e7c7bffb-8857-48d4-a71f-88b359fad...@justatheory.com
---
 doc/src/sgml/config.sgml                      | 13 ++++
 doc/src/sgml/extend.sgml                      |  1 +
 doc/src/sgml/ref/create_extension.sgml        |  1 +
 src/backend/commands/extension.c              | 56 ++++++++++++++--
 src/backend/utils/fmgr/dfmgr.c                | 64 +++++++++++--------
 src/backend/utils/misc/guc_tables.c           | 12 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/commands/extension.h              |  2 +
 src/include/fmgr.h                            |  2 +
 9 files changed, 118 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d54f9049569..c4790de7102 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10434,6 +10434,19 @@ <title>Other Defaults</title>
       </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>
+        TODO
+       </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>
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5ce..77a517d5167 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -635,6 +635,7 @@ <title>Extension Files</title>
     <primary>control file</primary>
    </indexterm>
 
+    <!-- TODO -->
     <para>
      The <command>CREATE EXTENSION</command> command relies on a control
      file for each extension, which must be named the same as the extension
diff --git a/doc/src/sgml/ref/create_extension.sgml 
b/doc/src/sgml/ref/create_extension.sgml
index ca2b80d669c..c26218f3adf 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -92,6 +92,7 @@ <title>Parameters</title>
         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>.
+        <!-- TODO -->
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index af6bd8ff426..b328e8e28f4 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -69,6 +69,9 @@
 #include "utils/varlena.h"
 
 
+/* GUC */
+char      *Extension_control_path;
+
 /* Globally visible state variables */
 bool           creating_extension = false;
 Oid                    CurrentExtensionObject = InvalidOid;
@@ -79,6 +82,7 @@ Oid                   CurrentExtensionObject = InvalidOid;
 typedef struct ExtensionControlFile
 {
        char       *name;                       /* name of the extension */
+       char       *control_dir;        /* directory where control file was 
found */
        char       *directory;          /* directory for script files */
        char       *default_version;    /* default install target version, if 
any */
        char       *module_pathname;    /* string to substitute for
@@ -328,6 +332,9 @@ is_extension_script_filename(const char *filename)
        return (extension != NULL) && (strcmp(extension, ".sql") == 0);
 }
 
+// TODO
+// This is now only for finding/listing available extensions.  Rewrite to use
+// path.  See further TODOs below.
 static char *
 get_extension_control_directory(void)
 {
@@ -341,16 +348,36 @@ get_extension_control_directory(void)
        return result;
 }
 
+/*
+ * Find control file for extension with name in control->name, looking in the
+ * path.  Return the full file name, or NULL if not found.  If found, the
+ * directory is recorded in control->control_dir.
+ */
 static char *
-get_extension_control_filename(const char *extname)
+find_extension_control_filename(ExtensionControlFile *control)
 {
        char            sharepath[MAXPGPATH];
+       char       *system_dir;
+       char       *basename;
        char       *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);
+
+       result = find_in_path(basename, Extension_control_path, 
"extension_control_path", "$system", system_dir);
+
+       if (result)
+       {
+               const char *p;
+
+               p = strrchr(result, '/');
+               Assert(p);
+               control->control_dir = pnstrdup(result, p - result);
+       }
 
        return result;
 }
@@ -366,7 +393,7 @@ get_extension_script_directory(ExtensionControlFile 
*control)
         * installation's share directory.
         */
        if (!control->directory)
-               return get_extension_control_directory();
+               return pstrdup(control->control_dir);
 
        if (is_absolute_path(control->directory))
                return pstrdup(control->directory);
@@ -444,7 +471,15 @@ parse_extension_control_file(ExtensionControlFile *control,
        if (version)
                filename = get_extension_aux_control_filename(control, version);
        else
-               filename = get_extension_control_filename(control->name);
+               filename = find_extension_control_filename(control);
+
+       if (!filename)
+       {
+               ereport(ERROR,
+                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), // XXX?
+                                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)
        {
@@ -457,6 +492,7 @@ parse_extension_control_file(ExtensionControlFile *control,
                                return;
                        }
 
+                       // TODO: this check is obsolete?
                        /* missing control file indicates extension is not 
installed */
                        ereport(ERROR,
                                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -2114,6 +2150,8 @@ RemoveExtensionById(Oid extId)
  * The system view pg_available_extensions provides a user interface to this
  * SRF, adding information about whether the extensions are installed in the
  * current DB.
+ *
+ * TODO
  */
 Datum
 pg_available_extensions(PG_FUNCTION_ARGS)
@@ -2194,6 +2232,8 @@ pg_available_extensions(PG_FUNCTION_ARGS)
  * The system view pg_available_extension_versions provides a user interface
  * to this SRF, adding information about which versions are installed in the
  * current DB.
+ *
+ * TODO
  */
 Datum
 pg_available_extension_versions(PG_FUNCTION_ARGS)
@@ -2366,6 +2406,8 @@ get_available_versions_for_extension(ExtensionControlFile 
*pcontrol,
  * directory.  That's not a bulletproof check, since the file might be
  * invalid, but this is only used for hints so it doesn't have to be 100%
  * right.
+ *
+ * TODO
  */
 bool
 extension_file_exists(const char *extensionName)
@@ -2445,6 +2487,8 @@ convert_requires_to_datum(List *requires)
 /*
  * This function reports the version update paths that exist for the
  * specified extension.
+ *
+ * TODO
  */
 Datum
 pg_extension_update_paths(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index c7aa789b51b..d348d30d9e4 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -81,8 +81,7 @@ static void incompatible_module_error(const char *libname,
                                                                          const 
Pg_magic_struct *module_magic_data) pg_attribute_noreturn();
 static char *expand_dynamic_library_name(const char *name);
 static void check_restricted_library_name(const char *name);
-static char *substitute_libpath_macro(const char *name);
-static char *find_in_dynamic_libpath(const char *basename);
+static char *substitute_path_macro(const char *str, const char *macro, const 
char *value);
 
 /* Magic structure that module needs to match to be accepted */
 static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA;
@@ -408,7 +407,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.
  *
@@ -427,13 +426,13 @@ expand_dynamic_library_name(const char *name)
 
        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);
@@ -443,14 +442,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;
@@ -485,47 +484,56 @@ check_restricted_library_name(const char *name)
  * Result is always freshly palloc'd.
  */
 static char *
-substitute_libpath_macro(const char *name)
+substitute_path_macro(const char *str, const char *macro, const char *value)
 {
        const char *sep_ptr;
 
-       Assert(name != NULL);
+       Assert(str != NULL);
+       Assert(macro[0] == '$');
 
-       /* Currently, we only recognize $libdir at the start of the string */
-       if (name[0] != '$')
-               return pstrdup(name);
+       /* Currently, we only recognize $macro at the start of the string */
+       if (str[0] != '$')
+               return pstrdup(str);
 
-       if ((sep_ptr = first_dir_separator(name)) == NULL)
-               sep_ptr = name + strlen(name);
+       if ((sep_ptr = first_dir_separator(str)) == NULL)
+               sep_ptr = str + strlen(str);
 
-       if (strlen("$libdir") != sep_ptr - name ||
-               strncmp(name, "$libdir", strlen("$libdir")) != 0)
+       if (strlen(macro) != sep_ptr - str ||
+               strncmp(str, macro, strlen(macro)) != 0)
                ereport(ERROR,
                                (errcode(ERRCODE_INVALID_NAME),
-                                errmsg("invalid macro name in dynamic library 
path: %s",
-                                               name)));
+                                errmsg("invalid macro name in path: %s",
+                                               str)));
 
-       return psprintf("%s%s", pkglib_path, sep_ptr);
+       return psprintf("%s%s", value, sep_ptr);
 }
 
 
 /*
  * Search for a file called 'basename' in the colon-separated search
- * path Dynamic_library_path.  If the file is found, the full file name
+ * path given.  If the file is found, the full file name
  * is returned in freshly palloc'd memory.  If the file is not found,
  * return NULL.
+ *
+ * path_param is the name of the parameter that path came from, for error
+ * messages.
+ *
+ * macro and macro_val allow substituting a macro; see
+ * substitute_path_macro().
  */
-static char *
-find_in_dynamic_libpath(const char *basename)
+char *
+find_in_path(const char *basename, const char *path, const char *path_param,
+                        const char *macro, const char *macro_val)
 {
        const char *p;
        size_t          baselen;
 
        Assert(basename != NULL);
        Assert(first_dir_separator(basename) == NULL);
-       Assert(Dynamic_library_path != NULL);
+       Assert(path != NULL);
+       Assert(path_param != NULL);
 
-       p = Dynamic_library_path;
+       p = path;
        if (strlen(p) == 0)
                return NULL;
 
@@ -542,7 +550,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);
@@ -552,7 +560,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);
@@ -561,13 +569,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 8a67f01200c..5a00b99daba 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"
@@ -4233,6 +4234,17 @@ 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("TODO"),
+                       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 39a3ac23127..518c4f40719 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -772,6 +772,7 @@
 # - Other Defaults -
 
 #dynamic_library_path = '$libdir'
+#extension_control_path = '$system'
 #gin_fuzzy_search_limit = 0
 
 
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index c6f3f867eb7..fe8a97570ea 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 1e3795de4a8..2930b61cee5 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -740,6 +740,8 @@ extern bool CheckFunctionValidatorAccess(Oid validatorOid, 
Oid functionOid);
  */
 extern PGDLLIMPORT char *Dynamic_library_path;
 
+extern char *find_in_path(const char *basename, const char *path, const char 
*path_param,
+                                                 const char *macro, const char 
*macro_val);
 extern void *load_external_function(const char *filename, const char *funcname,
                                                                        bool 
signalNotFound, void **filehandle);
 extern void *lookup_external_function(void *filehandle, const char *funcname);

base-commit: e7a9496de90657e2161f68b3a5a9b2d9b0b7bb07
-- 
2.47.0

Reply via email to