From f6c852a714dc2a3fcf08896cf24d99a9a488e5c8 Mon Sep 17 00:00:00 2001
From: "David E. Wheeler" <david@justatheory.com>
Date: Thu, 4 Apr 2024 13:18:31 -0400
Subject: [PATCH v3] Add extension_directory GUC

Based on a [patch] by Christophe Berg in the Debian Project, add a new
GUC, `extension_directory`, that prepends a path for extension loading.
This directory is prepended to paths when loading extensions (control
and SQL files), and to the `$libdir` directive when loading modules that
back functions. Changing the configuration requires a server restart,
and is visible only to super users.

  [patch]: https://salsa.debian.org/postgresql/postgresql/-/blob/17/debian/patches/extension_destdir?ref_type=heads
---
 doc/src/sgml/config.sgml                      | 16 ++++
 doc/src/sgml/extend.sgml                      |  5 +-
 src/backend/commands/extension.c              | 90 +++++++++++++++++++
 src/backend/utils/fmgr/dfmgr.c                | 29 +++++-
 src/backend/utils/misc/guc_tables.c           | 12 +++
 src/backend/utils/misc/postgresql.conf.sample |  2 +
 src/include/utils/guc.h                       |  1 +
 7 files changed, 152 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d8e1282e12..5cc0efe037 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10379,6 +10379,22 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-extension-destdir" xreflabel="extension_destdir">
+      <term><varname>extension_destdir</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>extension_destdir</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This directory is prepended to paths when loading extensions (control
+        and SQL files), and to the '$libdir' directive when loading modules
+        that back functions. This parameter can only be set at server start.
+        For more information see <xref linkend="extend-extensions-files-directory"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
    </sect1>
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5c..bb29670777 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -668,8 +668,9 @@ RETURNS anycompatible AS ...
       <listitem>
        <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
+        file(s). Unless an absolute path is given, the name will be used to
+        search for the extension relative to the <xref linkend="guc-extension-destdir"/>
+        and the installation's <literal>SHAREDIR</literal> directory.  The
         default behavior is equivalent to specifying
         <literal>directory = 'extension'</literal>.
        </para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 77d8c9e186..fbe0e32aca 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -393,6 +393,16 @@ get_extension_control_filename(const char *extname)
 
 	get_share_path(my_exec_path, sharepath);
 	result = (char *) palloc(MAXPGPATH);
+	/*
+	 * If extension_destdir is set, try to find the file there first
+	 */
+	if (*extension_destdir != '\0')
+	{
+		snprintf(result, MAXPGPATH, "%s%s/extension/%s.control",
+				 extension_destdir, sharepath, extname);
+		if (pg_file_exists(result))
+			return result;
+	}
 	snprintf(result, MAXPGPATH, "%s/extension/%s.control",
 			 sharepath, extname);
 
@@ -432,6 +442,16 @@ get_extension_aux_control_filename(ExtensionControlFile *control,
 	scriptdir = get_extension_script_directory(control);
 
 	result = (char *) palloc(MAXPGPATH);
+	/*
+	 * If extension_destdir is set, try to find the file there first
+	 */
+	if (*extension_destdir != '\0')
+	{
+		snprintf(result, MAXPGPATH, "%s%s/%s--%s.control",
+				 extension_destdir, scriptdir, control->name, version);
+		if (pg_file_exists(result))
+			return result;
+	}
 	snprintf(result, MAXPGPATH, "%s/%s--%s.control",
 			 scriptdir, control->name, version);
 
@@ -450,6 +470,23 @@ get_extension_script_filename(ExtensionControlFile *control,
 	scriptdir = get_extension_script_directory(control);
 
 	result = (char *) palloc(MAXPGPATH);
+	/*
+	 * If extension_destdir is set, try to find the file there first
+	 */
+	if (*extension_destdir != '\0')
+	{
+		if (from_version)
+			snprintf(result, MAXPGPATH, "%s%s/%s--%s--%s.sql",
+					 extension_destdir, scriptdir, control->name, from_version, version);
+		else
+			snprintf(result, MAXPGPATH, "%s%s/%s--%s.sql",
+					 extension_destdir, scriptdir, control->name, version);
+		if (pg_file_exists(result))
+		{
+			pfree(scriptdir);
+			return result;
+		}
+	}
 	if (from_version)
 		snprintf(result, MAXPGPATH, "%s/%s--%s--%s.sql",
 				 scriptdir, control->name, from_version, version);
@@ -1209,6 +1246,59 @@ get_ext_ver_list(ExtensionControlFile *control)
 	DIR		   *dir;
 	struct dirent *de;
 
+	/*
+	 * If extension_destdir is set, try to find the files there first
+	 */
+	if (*extension_destdir != '\0')
+	{
+		char		location[MAXPGPATH];
+
+		snprintf(location, MAXPGPATH, "%s%s", extension_destdir,
+				get_extension_script_directory(control));
+		dir = AllocateDir(location);
+		while ((de = ReadDir(dir, location)) != NULL)
+		{
+			char	   *vername;
+			char	   *vername2;
+			ExtensionVersionInfo *evi;
+			ExtensionVersionInfo *evi2;
+
+			/* must be a .sql file ... */
+			if (!is_extension_script_filename(de->d_name))
+				continue;
+
+			/* ... matching extension name followed by separator */
+			if (strncmp(de->d_name, control->name, extnamelen) != 0 ||
+				de->d_name[extnamelen] != '-' ||
+				de->d_name[extnamelen + 1] != '-')
+				continue;
+
+			/* extract version name(s) from 'extname--something.sql' filename */
+			vername = pstrdup(de->d_name + extnamelen + 2);
+			*strrchr(vername, '.') = '\0';
+			vername2 = strstr(vername, "--");
+			if (!vername2)
+			{
+				/* It's an install, not update, script; record its version name */
+				evi = get_ext_ver_info(vername, &evi_list);
+				evi->installable = true;
+				continue;
+			}
+			*vername2 = '\0';		/* terminate first version */
+			vername2 += 2;			/* and point to second */
+
+			/* if there's a third --, it's bogus, ignore it */
+			if (strstr(vername2, "--"))
+				continue;
+
+			/* Create ExtensionVersionInfos and link them together */
+			evi = get_ext_ver_info(vername, &evi_list);
+			evi2 = get_ext_ver_info(vername2, &evi_list);
+			evi->reachable = lappend(evi->reachable, evi2);
+		}
+		FreeDir(dir);
+	}
+
 	location = get_extension_script_directory(control);
 	dir = AllocateDir(location);
 	while ((de = ReadDir(dir, location)) != NULL)
diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index eafa0128ef..8dc17da5b5 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -35,6 +35,7 @@
 #include "miscadmin.h"
 #include "storage/fd.h"
 #include "storage/shmem.h"
+#include "utils/guc.h"
 #include "utils/hsearch.h"
 
 
@@ -415,7 +416,7 @@ expand_dynamic_library_name(const char *name)
 {
 	bool		have_slash;
 	char	   *new;
-	char	   *full;
+	char	   *full, *full2;
 
 	Assert(name);
 
@@ -430,6 +431,19 @@ expand_dynamic_library_name(const char *name)
 	else
 	{
 		full = substitute_libpath_macro(name);
+		/*
+		 * If extension_destdir is set, try to find the file there first
+		 */
+		if (*extension_destdir != '\0')
+		{
+			full2 = psprintf("%s%s", extension_destdir, full);
+			if (pg_file_exists(full2))
+			{
+				pfree(full);
+				return full2;
+			}
+			pfree(full2);
+		}
 		if (pg_file_exists(full))
 			return full;
 		pfree(full);
@@ -448,6 +462,19 @@ expand_dynamic_library_name(const char *name)
 	{
 		full = substitute_libpath_macro(new);
 		pfree(new);
+		/*
+		 * If extension_destdir is set, try to find the file there first
+		 */
+		if (*extension_destdir != '\0')
+		{
+			full2 = psprintf("%s%s", extension_destdir, full);
+			if (pg_file_exists(full2))
+			{
+				pfree(full);
+				return full2;
+			}
+			pfree(full2);
+		}
 		if (pg_file_exists(full))
 			return full;
 		pfree(full);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c68fdc008b..6c203edb11 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -542,6 +542,7 @@ char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
 char	   *external_pid_file;
+char	   *extension_destdir;
 
 char	   *application_name;
 
@@ -4508,6 +4509,17 @@ struct config_string ConfigureNamesString[] =
 		check_canonical_path, NULL, NULL
 	},
 
+	{
+		{"extension_destdir", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Path to prepend for extension loading."),
+			gettext_noop("This directory is prepended to paths when loading extensions (control and SQL files), and to the '$libdir' directive when loading modules that back functions. The location is made configurable to allow build-time testing of extensions that do not have been installed to their proper location yet."),
+			GUC_SUPERUSER_ONLY
+		},
+		&extension_destdir,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"ssl_library", PGC_INTERNAL, PRESET_OPTIONS,
 			gettext_noop("Shows the name of the SSL library."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2166ea4a87..bab263a3de 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -771,6 +771,8 @@
 # - Other Defaults -
 
 #dynamic_library_path = '$libdir'
+#extension_destdir = ''			# prepend path when loading extensions
+					# and shared objects (added by Debian)
 #gin_fuzzy_search_limit = 0
 
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 8d1fe04078..9f198cf2b5 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -278,6 +278,7 @@ extern PGDLLIMPORT char *ConfigFileName;
 extern PGDLLIMPORT char *HbaFileName;
 extern PGDLLIMPORT char *IdentFileName;
 extern PGDLLIMPORT char *external_pid_file;
+extern PGDLLIMPORT char *extension_destdir;
 
 extern PGDLLIMPORT char *application_name;
 
-- 
2.44.0

