public inbox for [email protected]help / color / mirror / Atom feed
[PATCH] Avoid touching user indexes while they are being (re)built. 47+ messages / 11 participants [nested] [flat]
* [PATCH] Avoid touching user indexes while they are being (re)built. @ 2019-09-12 14:35 Arseny Sher <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: Arseny Sher @ 2019-09-12 14:35 UTC (permalink / raw) Existing ReindexIsProcessingIndex check is consulted only in genam.c and thus enforced only for system catalogs. Check it also in the planner, so that indexes which are currently being rebuilt are never used. Also cock SetReindexProcessing in index_create to defend from index self usage during its creation. Without this, VACUUM FULL or just CREATE INDEX might fail with something like ERROR: could not read block 3534 in file "base/41366676/56697497": read only 0 of 8192 bytes if there are indexes which usage can be considered during these very indexes (re)building, i.e. index expression scans indexed table. --- src/backend/catalog/index.c | 22 ++++++++++++++++++++-- src/backend/optimizer/util/plancat.c | 5 +++++ src/test/regress/expected/create_index.out | 12 ++++++++++++ src/test/regress/sql/create_index.sql | 13 +++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 3e1d40662d..5bc764ce46 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1174,7 +1174,22 @@ index_create(Relation heapRelation, } else { - index_build(heapRelation, indexRelation, indexInfo, false, true); + /* ensure SetReindexProcessing state isn't leaked */ + PG_TRY(); + { + /* Suppress use of the target index while building it */ + SetReindexProcessing(heapRelationId, indexRelationId); + + index_build(heapRelation, indexRelation, indexInfo, false, true); + } + PG_CATCH(); + { + /* Make sure flag gets cleared on error exit */ + ResetReindexProcessing(); + PG_RE_THROW(); + } + PG_END_TRY(); + ResetReindexProcessing(); } /* @@ -1379,7 +1394,10 @@ index_concurrently_build(Oid heapRelationId, indexInfo->ii_Concurrent = true; indexInfo->ii_BrokenHotChain = false; - /* Now build the index */ + /* + * Now build the index + * SetReindexProcessing is not required since indisvalid is false anyway + */ index_build(heapRel, indexRelation, indexInfo, false, true); /* Close both the relations, but keep the locks */ diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index cf1761401d..9d58cd2574 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -27,6 +27,7 @@ #include "access/xlog.h" #include "catalog/catalog.h" #include "catalog/dependency.h" +#include "catalog/index.h" #include "catalog/heap.h" #include "catalog/pg_am.h" #include "catalog/pg_proc.h" @@ -193,6 +194,10 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, nkeycolumns; int i; + /* Don't chase own tail */ + if (ReindexIsProcessingIndex(indexoid)) + continue; + /* * Extract info from the relation descriptor for the index. */ diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 324db1b6ae..1706964277 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -1323,6 +1323,18 @@ create unique index hash_f8_index_1 on hash_f8_heap(abs(random)); create unique index hash_f8_index_2 on hash_f8_heap((seqno + 1), random); create unique index hash_f8_index_3 on hash_f8_heap(random) where seqno > 1000; -- +-- Create an index which might consider using this very index during the build. +-- +-- primary key ensures relhasindex is set +CREATE TABLE pears(f1 int primary key, f2 int); +INSERT INTO pears SELECT i, i+1 FROM generate_series(1, 100) i; +CREATE FUNCTION pears_f(i int) RETURNS int LANGUAGE SQL IMMUTABLE AS $$ + SELECT f1 FROM pears WHERE pears.f2 = 42 +$$; +CREATE index ON pears ((pears_f(f1))); +DROP TABLE pears; +DROP FUNCTION pears_f; +-- -- Try some concurrent index builds -- -- Unfortunately this only tests about half the code paths because there are diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index f96bebf410..76a781f6b0 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -446,6 +446,19 @@ create unique index hash_f8_index_2 on hash_f8_heap((seqno + 1), random); create unique index hash_f8_index_3 on hash_f8_heap(random) where seqno > 1000; -- +-- Create an index which might consider using this very index during the build. +-- +-- primary key ensures relhasindex is set +CREATE TABLE pears(f1 int primary key, f2 int); +INSERT INTO pears SELECT i, i+1 FROM generate_series(1, 100) i; +CREATE FUNCTION pears_f(i int) RETURNS int LANGUAGE SQL IMMUTABLE AS $$ + SELECT f1 FROM pears WHERE pears.f2 = 42 +$$; +CREATE index ON pears ((pears_f(f1))); +DROP TABLE pears; +DROP FUNCTION pears_f; + +-- -- Try some concurrent index builds -- -- Unfortunately this only tests about half the code paths because there are -- 2.11.0 --=-=-= Content-Type: text/plain -- Arseny Sher Postgres Professional: http://www.postgrespro.com The Russian Postgres Company --=-=-=-- ^ permalink raw reply [nested|flat] 47+ messages in thread
* [PATCH v14 2/7] Don't proc_exit() in startup's SIGTERM handler if forked by system(). @ 2023-02-14 17:44 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: Nathan Bossart @ 2023-02-14 17:44 UTC (permalink / raw) Instead, emit a message to STDERR and _exit() in this case. This change also adds assertions to proc_exit(), ProcKill(), and AuxiliaryProcKill() to verify that these functions are not called by a process forked by system(), etc. --- src/backend/postmaster/startup.c | 17 ++++++++++++++++- src/backend/storage/ipc/ipc.c | 3 +++ src/backend/storage/lmgr/proc.c | 2 ++ src/backend/utils/error/elog.c | 28 ++++++++++++++++++++++++++++ src/include/utils/elog.h | 6 +----- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index efc2580536..0e7de26bc2 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -19,6 +19,8 @@ */ #include "postgres.h" +#include <unistd.h> + #include "access/xlog.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" @@ -121,7 +123,20 @@ StartupProcShutdownHandler(SIGNAL_ARGS) int save_errno = errno; if (in_restore_command) - proc_exit(1); + { + /* + * If we are in a child process (e.g., forked by system() in + * RestoreArchivedFile()), we don't want to call any exit callbacks. + * The parent will take care of that. + */ + if (MyProcPid == (int) getpid()) + proc_exit(1); + else + { + write_stderr_signal_safe("StartupProcShutdownHandler() called in child process\n"); + _exit(1); + } + } else shutdown_requested = true; WakeupRecovery(); diff --git a/src/backend/storage/ipc/ipc.c b/src/backend/storage/ipc/ipc.c index 1904d21795..d5097dc008 100644 --- a/src/backend/storage/ipc/ipc.c +++ b/src/backend/storage/ipc/ipc.c @@ -103,6 +103,9 @@ static int on_proc_exit_index, void proc_exit(int code) { + /* proc_exit() is not safe in forked processes from system(), etc. */ + Assert(MyProcPid == (int) getpid()); + /* Clean up everything that must be cleaned up */ proc_exit_prepare(code); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 22b4278610..b9e2c3aafe 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -805,6 +805,7 @@ ProcKill(int code, Datum arg) dlist_head *procgloballist; Assert(MyProc != NULL); + Assert(MyProc->pid == (int) getpid()); /* not safe if forked by system(), etc. */ /* Make sure we're out of the sync rep lists */ SyncRepCleanupAtProcExit(); @@ -925,6 +926,7 @@ AuxiliaryProcKill(int code, Datum arg) PGPROC *proc; Assert(proctype >= 0 && proctype < NUM_AUXILIARY_PROCS); + Assert(MyProc->pid == (int) getpid()); /* not safe if forked by system(), etc. */ auxproc = &AuxiliaryProcs[proctype]; diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 5898100acb..f9925c8d8e 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -3730,6 +3730,34 @@ write_stderr(const char *fmt,...) } +/* + * Write a message to STDERR using only async-signal-safe functions. This can + * be used to safely emit a message from a signal handler. + * + * TODO: It is likely possible to safely do a limited amount of string + * interpolation (e.g., %s and %d), but that is not presently supported. + */ +void +write_stderr_signal_safe(const char *fmt) +{ + int nwritten = 0; + int ntotal = strlen(fmt); + + while (nwritten < ntotal) + { + int rc; + + rc = write(STDERR_FILENO, fmt + nwritten, ntotal - nwritten); + + /* Just give up on error. There isn't much else we can do. */ + if (rc == -1) + return; + + nwritten += rc; + } +} + + /* * Adjust the level of a recovery-related message per trace_recovery_messages. * diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h index 4a9562fdaa..20f1b54c6a 100644 --- a/src/include/utils/elog.h +++ b/src/include/utils/elog.h @@ -529,11 +529,7 @@ extern void write_pipe_chunks(char *data, int len, int dest); extern void write_csvlog(ErrorData *edata); extern void write_jsonlog(ErrorData *edata); -/* - * Write errors to stderr (or by equal means when stderr is - * not available). Used before ereport/elog can be used - * safely (memory context, GUC load etc) - */ extern void write_stderr(const char *fmt,...) pg_attribute_printf(1, 2); +extern void write_stderr_signal_safe(const char *fmt); #endif /* ELOG_H */ -- 2.25.1 --IS0zKkzwUGydFO0o Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v14-0003-introduce-routine-for-checking-mutually-exclusiv.patch" ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-26 14:06 David E. Wheeler <[email protected]> 0 siblings, 2 replies; 47+ messages in thread From: David E. Wheeler @ 2024-08-26 14:06 UTC (permalink / raw) To: Craig Ringer <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi Hackers, Apologies for the delay in reply; I’ve been at the XOXO Festival and almost completely unplugged for the first time in ages. Happy to see this thread coming alive, though. Thank you Gabriele, Craig, and Jelte! On Aug 21, 2024, at 19:07, Craig Ringer <[email protected]> wrote: > So IMO this should be a _path_ to search for extension control files > and SQL scripts. > > If the current built-in default extension dir was exposed as a var > $extdir like we do for $libdir, this might look something like this > for local development and testing while working with a packaged > postgres build: > > SET extension_search_path = $extsdir, /opt/myapp/extensions, > /usr/local/postgres/my-custom-extension/extensions; > SET dynamic_library_path = $libdir, /opt/myapp/lib, > /usr/local/postgres/my-custom-extension/lib I would very much like something like this, but I’m not sure how feasible it is for a few reasons. The first, and most important, is that extensions are not limited to just a control file and SQL file. They also very often include: * one or more shared library files * documentation files * binary files And maybe more? How many of these directories might an extension install files into: ✦ ❯ pg_config | grep DIR | awk '{print $1}' BINDIR DOCDIR HTMLDIR INCLUDEDIR PKGINCLUDEDIR INCLUDEDIR-SERVER LIBDIR PKGLIBDIR LOCALEDIR MANDIR SHAREDIR SYSCONFDIR I would assume BINDIR, DOCDIR, HTMLDIR, PKGLIBDIR, MANDIR, SHAREDIR, and perhaps LOCALEDIR. But even if it’s just one or two, the only proper way an extension directory would work, IME, is to define a directory-based structure for extensions, where every file for an extension is in a directory named for the extension, and subdirectories are defined for each of the above requisite file types. Something like: extension_name ├── control.ini ├── bin ├── doc ├── html ├── lib ├── local ├── man └── share This would allow multiple paths to work and keep all the files for an extension bundled together. It could also potentially allow for multiple versions of an extension to be installed at once, if we required the version to be part of the directory name. I think this would be a much nicer layout for packaging, installing, and managing extensions versus the current method of strewing files around to a slew of different directories. But it would come at some cost, in terms of backward with the existing layout (or migration to it), significant modification of the server to use the new layout (and extension_search_path), and other annoyances like PATH and MANPATH management. Long term I think it would be worthwhile, but the current patch feels like a decent interim step we could live with, solving most of the integration problems (immutable servers, packaging testing, etc.) at the cost of a slightly unexpected directory layout. What I mean by that is that the current patch is pretty much just using extension_destdir as a prefix to all of those directories from pg_config, so they never have to change, but it does mean that you end up installing extensions in something like /mnt/extensions/pg16/usr/share/postgresql/16 /mnt/extensions/pg16/usr/include/postgresql etc. Best, David ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-26 21:35 Craig Ringer <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Craig Ringer @ 2024-08-26 21:35 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Tue, 27 Aug 2024 at 02:07, David E. Wheeler <[email protected]> wrote: > On Aug 21, 2024, at 19:07, Craig Ringer <[email protected]> wrote: > But even if it’s just one or two, the only proper way an extension directory would work, IME, is to define a directory-based structure for extensions, where every file for an extension is in a directory named for the extension, and subdirectories are defined for each of the above requisite file types. > [...] > I think this would be a much nicer layout for packaging, installing, and managing extensions versus the current method of strewing files around to a slew of different directories. This looks like a good suggestion to me, it would make the packaging, distribution and integration of 3rd party extensions significantly easier without any obvious large or long term cost. > But it would come at some cost, in terms of backward with the existing layout (or migration to it), significant modification of the server to use the new layout (and extension_search_path), and other annoyances like PATH and MANPATH management. Also PGXS, the windows extension build support, and 3rd party cmake builds etc. But not by the looks a drastic change. > Long term I think it would be worthwhile, but the current patch feels like a decent interim step we could live with, solving most of the integration problems (immutable servers, packaging testing, etc.) at the cost of a slightly unexpected directory layout. What I mean by that is that the current patch is pretty much just using extension_destdir as a prefix to all of those directories from pg_config, so they never have to change, but it does mean that you end up installing extensions in something like: > > /mnt/extensions/pg16/usr/share/postgresql/16 > /mnt/extensions/pg16/usr/include/postgresql My only real concern with the current patch is that it limits searching for extensions to one additional configurable location, which is inconsistent with how things like the dynamic_library_path works. Once in, it'll be difficult to change or extend for BC, and if someone wants to add a search path capability it'll break existing configurations. Would it be feasible to define its configuration syntax as accepting a list of paths, but only implement the semantics for single-entry lists and ERROR on multiple paths? That way it could be extended w/o breaking existing configurations later. With that said, I'm not the one doing the work at the moment, and the functionality would definitely be helpful. If there's agreement on supporting a search-path or recursing into subdirectories I'd be willing to have a go at it, but I'm a bit stale on Pg's codebase now so I'd want to be fairly confident the work wouldn't just be thrown out. -- Craig Ringer ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-27 08:56 Gabriele Bartolini <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Gabriele Bartolini @ 2024-08-27 08:56 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi David, Thanks for your email. On Mon, 26 Aug 2024 at 16:07, David E. Wheeler <[email protected]> wrote: > I would assume BINDIR, DOCDIR, HTMLDIR, PKGLIBDIR, MANDIR, SHAREDIR, and > perhaps LOCALEDIR. > > But even if it’s just one or two, the only proper way an extension > directory would work, IME, is to define a directory-based structure for > extensions, where every file for an extension is in a directory named for > the extension, and subdirectories are defined for each of the above > requisite file types. Something like: > > extension_name > ├── control.ini > ├── bin > ├── doc > ├── html > ├── lib > ├── local > ├── man > └── share > I'm really glad you proposed this publicly. I reached the same conclusion the other day when digging deeper into the problem with a few folks from CloudNativePG. Setting aside multi-arch images for now, if we could reorganize the content of a single image (identified by OS distro, PostgreSQL major version, and extension version) with a top-level directory structure as you described, we could easily mount each image as a separate volume. The extension image could follow a naming convention like this (order can be adjusted): `<extension name>-<pg major>-<extension version>-<distro>(-<seq>)`. For example, `pgvector-16-0.7.4-bookworm-1` would represent the first image built in a repository for pgvector 0.7.4 for PostgreSQL 16 on Debian Bookworm. If multi-arch images aren't desired, we could incorporate the architecture somewhere in the naming convention. This would allow multiple paths to work and keep all the files for an > extension bundled together. It could also potentially allow for multiple > versions of an extension to be installed at once, if we required the > version to be part of the directory name. > If we wanted to install multiple versions of an extension, we could mount them in different directories, with the version included in the folder name—for example, `pgvector-0.7.4` instead of just `pgvector`. However, I'm a bit rusty with the extensions framework, so I'll need to check if this approach is feasible and makes sense. Thanks, Gabriele -- Gabriele Bartolini Vice President, Cloud Native at EDB enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-27 15:19 David E. Wheeler <[email protected]> parent: Gabriele Bartolini <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: David E. Wheeler @ 2024-08-27 15:19 UTC (permalink / raw) To: Gabriele Bartolini <[email protected]>; +Cc: Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Aug 27, 2024, at 04:56, Gabriele Bartolini <[email protected]> wrote: > The extension image could follow a naming convention like this (order can be adjusted): `<extension name>-<pg major>-<extension version>-<distro>(-<seq>)`. For example, `pgvector-16-0.7.4-bookworm-1` would represent the first image built in a repository for pgvector 0.7.4 for PostgreSQL 16 on Debian Bookworm. If multi-arch images aren't desired, we could incorporate the architecture somewhere in the naming convention. Well now you’re just describing the binary distribution format RFC[1] (POC[2]) and multi-platform OCI distribution POC[3] :-) > If we wanted to install multiple versions of an extension, we could mount them in different directories, with the version included in the folder name—for example, `pgvector-0.7.4` instead of just `pgvector`. However, I'm a bit rusty with the extensions framework, so I'll need to check if this approach is feasible and makes sense. Right, if we decided to adopt this proposal, it might make sense to include the “default version” as part of the directory name. But there’s quite a lot of work between here and there. Best, David [1]: https://github.com/pgxn/rfcs/pull/2 [2]: https://justatheory.com/2024/06/trunk-poc/ [3]: https://justatheory.com/2024/06/trunk-oci-poc/ ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-27 15:26 David E. Wheeler <[email protected]> parent: Craig Ringer <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: David E. Wheeler @ 2024-08-27 15:26 UTC (permalink / raw) To: Craig Ringer <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Aug 26, 2024, at 17:35, Craig Ringer <[email protected]> wrote: > This looks like a good suggestion to me, it would make the packaging, > distribution and integration of 3rd party extensions significantly > easier without any obvious large or long term cost. Yes! > Also PGXS, the windows extension build support, and 3rd party cmake > builds etc. But not by the looks a drastic change. Right. ISTM it could complicate PGXS quite a bit. If we set, say, SET extension_search_path = $extsdir, /mnt/extensions/pg16, /mnt/extensions/pg16/gosuperfast/extensions; What should be the output of `pg_config --sharedir`? > My only real concern with the current patch is that it limits > searching for extensions to one additional configurable location, > which is inconsistent with how things like the dynamic_library_path > works. Once in, it'll be difficult to change or extend for BC, and if > someone wants to add a search path capability it'll break existing > configurations. Agreed. > Would it be feasible to define its configuration syntax as accepting a > list of paths, but only implement the semantics for single-entry lists > and ERROR on multiple paths? That way it could be extended w/o > breaking existing configurations later. I imagine it’s a simple matter of programming :-) But that leaves the issue of directory organization. The current patch is just a prefix for various PGXS/pg_config directories; the longer-term proposal I’ve made here is not a prefix for sharedir, mandir, etc., but a directory that contains directories named for extensions. So even if we were to take this approach, the directory structure would vary. I suspect we’d have to name it differently and support both long-term. That, too me, is the main issue with this patch. OTOH, we have this patch now, and this other stuff is just a proposal. Actual code trumps ideas in my mind. > With that said, I'm not the one doing the work at the moment, and the > functionality would definitely be helpful. If there's agreement on > supporting a search-path or recursing into subdirectories I'd be > willing to have a go at it, but I'm a bit stale on Pg's codebase now > so I'd want to be fairly confident the work wouldn't just be thrown > out. I think we should get some clarity on the proposal, and then consensus, as you say. I say “get some clarity” because my proposal doesn’t require recursing, and I’m not sure why it’d be needed. Best, David ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-28 02:24 Craig Ringer <[email protected]> parent: David E. Wheeler <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Craig Ringer @ 2024-08-28 02:24 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Wed, 28 Aug 2024 at 03:26, David E. Wheeler <[email protected]> wrote: > Right. ISTM it could complicate PGXS quite a bit. If we set, say, > > SET extension_search_path = $extsdir, /mnt/extensions/pg16, /mnt/extensions/pg16/gosuperfast/extensions; > > What should be the output of `pg_config --sharedir`? `pg_config` only cares about compile-time settings, so I would not expect its output to change. I suspect we'd have to add PGXS extension-path awareness if going for per-extension subdir support. I'm not sure it makes sense to teach `pg_config` about this, since it'd need to have a different mode like pg_config --extension myextname --extension-sharedir since the extension's "sharedir" is $basedir/extensions/myextension/share or whatever. Supporting this looks to be a bit intrusive in the makefiles, requiring them to differentiate between "share dir for extensions" and "share dir for !extensions", etc. I'm not immediately sure if it can be done in a way that transparently converts unmodified extension PGXS makefiles to target the new paths; it might require an additional define, or use of new variables and an ifdef block to add backwards-compat to the extension makefile for building on older postgres. > But that leaves the issue of directory organization. The current patch is just a prefix for various PGXS/pg_config directories; the longer-term proposal I’ve made here is not a prefix for sharedir, mandir, etc., but a directory that contains directories named for extensions. So even if we were to take this approach, the directory structure would vary. Right. The proposed structure is rather a bigger change than I was thinking when I suggested supporting an extension search path not just a single extra path. But it's also a cleaner proposal; the per-extension directory would make it easier to ensure that the extension control file, sql scripts, and dynamic library all match the same extension and version if multiple ones are on the path. Which is desirable when doing things like testing a new version of an in-core extension. > OTOH, we have this patch now, and this other stuff is just a proposal. Actual code trumps ideas in my mind. Right. And I've been on the receiving end of having a small, focused patch derailed by others jumping in and scope-exploding it into something completely different to solve a much wider but related problem. I'm definitely not trying to stand in the way of progress with this; I just want to make sure that it doesn't paint us into a corner that prevents a more general solution from being adopted later. That's why I'm suggesting making the config a multi-value string (list of paths) and raising a runtime "ERROR: searching multiple paths for extensions not yet supported" or something if >1 path configured. If that doesn't work, no problem. > I think we should get some clarity on the proposal, and then consensus, as you say. I say “get some clarity” because my proposal doesn’t require recursing, and I’m not sure why it’d be needed. From what you and Gabriele are discussing (which I agree with), it wouldn't. ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-08-29 15:55 David E. Wheeler <[email protected]> parent: Craig Ringer <[email protected]> 0 siblings, 2 replies; 47+ messages in thread From: David E. Wheeler @ 2024-08-29 15:55 UTC (permalink / raw) To: Craig Ringer <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Aug 27, 2024, at 22:24, Craig Ringer <[email protected]> wrote: > `pg_config` only cares about compile-time settings, so I would not > expect its output to change. Right, of course that’s its original purpose, but extensions depend on it to determine where to install extensions. Not just PGXS, but also pgrx and various Makefile customizations I’ve seen in the wild. > I suspect we'd have to add PGXS extension-path awareness if going for > per-extension subdir support. I'm not sure it makes sense to teach > `pg_config` about this, since it'd need to have a different mode like > > pg_config --extension myextname --extension-sharedir > > since the extension's "sharedir" is > $basedir/extensions/myextension/share or whatever. Right. PGXS would just need to know where to put the directory for an extension. There should be a default for the project, and then it can be overridden with something like DESTDIR (but without full paths under that prefix). > Supporting this looks to be a bit intrusive in the makefiles, > requiring them to differentiate between "share dir for extensions" and > "share dir for !extensions", etc. I'm not immediately sure if it can > be done in a way that transparently converts unmodified extension PGXS > makefiles to target the new paths; it might require an additional > define, or use of new variables and an ifdef block to add > backwards-compat to the extension makefile for building on older > postgres. Yeah, might just have to be an entirely new thing, though it sure would be nice for existing PGXS-using Makefiles to do the right thing. Maybe for the new version of the server with the proposed new pattern it would dispatch to the new thing somehow without modifying all the rest of its logic. > Right. The proposed structure is rather a bigger change than I was > thinking when I suggested supporting an extension search path not just > a single extra path. But it's also a cleaner proposal; the > per-extension directory would make it easier to ensure that the > extension control file, sql scripts, and dynamic library all match the > same extension and version if multiple ones are on the path. Which is > desirable when doing things like testing a new version of an in-core > extension. 💯 > Right. And I've been on the receiving end of having a small, focused > patch derailed by others jumping in and scope-exploding it into > something completely different to solve a much wider but related > problem. I’m not complaining, I would definitely prefer to see consensus on a cleaner proposal along the lines we’ve discussed and a commitment to time from parties able to get it done in time for v18. I’m willing to help where I can with my baby C! Failing that, we can fall back on the destdir patch. > I'm definitely not trying to stand in the way of progress with this; I > just want to make sure that it doesn't paint us into a corner that > prevents a more general solution from being adopted later. That's why > I'm suggesting making the config a multi-value string (list of paths) > and raising a runtime "ERROR: searching multiple paths for extensions > not yet supported" or something if >1 path configured. > > If that doesn't work, no problem. I think the logic would have to be different, so they’d be different GUCs with their own semantics. But if the core team and committers are on board with the general idea of search paths and per-extension directory organization, it would be best to avoid getting stuck with maintaining the current patch’s GUC. OTOH, we could get it committed now and revert it later if we get the better thing done and committed. >> I think we should get some clarity on the proposal, and then consensus, as you say. I say “get some clarity” because my proposal doesn’t require recursing, and I’m not sure why it’d be needed. > > From what you and Gabriele are discussing (which I agree with), it wouldn’t. Ah, great. I’ll try to put some thought into a more formal proposal in a new thread next week. Unless your Gabriele beats me to it 😂. Best, David ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-10-10 11:20 Ebru Aydin Gol <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Ebru Aydin Gol @ 2024-10-10 11:20 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Craig Ringer <[email protected]>; Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Thanks for your efforts, a secondary directory for extensions is a very useful feature. Is there any updates on the patch? -Ebru On Thu, Aug 29, 2024 at 6:55 PM David E. Wheeler <[email protected]> wrote: > On Aug 27, 2024, at 22:24, Craig Ringer <[email protected]> > wrote: > > > `pg_config` only cares about compile-time settings, so I would not > > expect its output to change. > > Right, of course that’s its original purpose, but extensions depend on it > to determine where to install extensions. Not just PGXS, but also pgrx and > various Makefile customizations I’ve seen in the wild. > > > I suspect we'd have to add PGXS extension-path awareness if going for > > per-extension subdir support. I'm not sure it makes sense to teach > > `pg_config` about this, since it'd need to have a different mode like > > > > pg_config --extension myextname --extension-sharedir > > > > since the extension's "sharedir" is > > $basedir/extensions/myextension/share or whatever. > > Right. PGXS would just need to know where to put the directory for an > extension. There should be a default for the project, and then it can be > overridden with something like DESTDIR (but without full paths under that > prefix). > > > Supporting this looks to be a bit intrusive in the makefiles, > > requiring them to differentiate between "share dir for extensions" and > > "share dir for !extensions", etc. I'm not immediately sure if it can > > be done in a way that transparently converts unmodified extension PGXS > > makefiles to target the new paths; it might require an additional > > define, or use of new variables and an ifdef block to add > > backwards-compat to the extension makefile for building on older > > postgres. > > Yeah, might just have to be an entirely new thing, though it sure would be > nice for existing PGXS-using Makefiles to do the right thing. Maybe for the > new version of the server with the proposed new pattern it would dispatch > to the new thing somehow without modifying all the rest of its logic. > > > Right. The proposed structure is rather a bigger change than I was > > thinking when I suggested supporting an extension search path not just > > a single extra path. But it's also a cleaner proposal; the > > per-extension directory would make it easier to ensure that the > > extension control file, sql scripts, and dynamic library all match the > > same extension and version if multiple ones are on the path. Which is > > desirable when doing things like testing a new version of an in-core > > extension. > > 💯 > > > Right. And I've been on the receiving end of having a small, focused > > patch derailed by others jumping in and scope-exploding it into > > something completely different to solve a much wider but related > > problem. > > I’m not complaining, I would definitely prefer to see consensus on a > cleaner proposal along the lines we’ve discussed and a commitment to time > from parties able to get it done in time for v18. I’m willing to help where > I can with my baby C! Failing that, we can fall back on the destdir patch. > > > I'm definitely not trying to stand in the way of progress with this; I > > just want to make sure that it doesn't paint us into a corner that > > prevents a more general solution from being adopted later. That's why > > I'm suggesting making the config a multi-value string (list of paths) > > and raising a runtime "ERROR: searching multiple paths for extensions > > not yet supported" or something if >1 path configured. > > > > If that doesn't work, no problem. > > I think the logic would have to be different, so they’d be different GUCs > with their own semantics. But if the core team and committers are on board > with the general idea of search paths and per-extension directory > organization, it would be best to avoid getting stuck with maintaining the > current patch’s GUC. > > OTOH, we could get it committed now and revert it later if we get the > better thing done and committed. > > >> I think we should get some clarity on the proposal, and then consensus, > as you say. I say “get some clarity” because my proposal doesn’t require > recursing, and I’m not sure why it’d be needed. > > > > From what you and Gabriele are discussing (which I agree with), it > wouldn’t. > > Ah, great. > > I’ll try to put some thought into a more formal proposal in a new thread > next week. Unless your Gabriele beats me to it 😂. > > Best, > > David > > > > ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-10-10 20:35 David E. Wheeler <[email protected]> parent: Ebru Aydin Gol <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: David E. Wheeler @ 2024-10-10 20:35 UTC (permalink / raw) To: Ebru Aydin Gol <[email protected]>; +Cc: Craig Ringer <[email protected]>; Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Oct 10, 2024, at 13:20, Ebru Aydin Gol <[email protected]> wrote: > Thanks for your efforts, a secondary directory for extensions is a very useful feature. > > Is there any updates on the patch? There haven't been, no, but your reply has chastened me! I’ve now started a separate thread[1] proposing directory-based extension packaging, as promised up-thread. Best, David [1]: https://www.postgresql.org/message-id/2CAD6FA7-DC25-48FC-80F2-8F203DECAE6A%40justatheory.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-10-11 08:27 Ebru Aydin Gol <[email protected]> parent: David E. Wheeler <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: Ebru Aydin Gol @ 2024-10-11 08:27 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Craig Ringer <[email protected]>; Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers ❤️ Ebru Aydin Gol reacted via Gmail <https://www.google.com/gmail/about/?utm_source=gmail-in-product&utm_medium=et&utm_campaign=e...; On Thu, Oct 10, 2024 at 11:35 PM David E. Wheeler <[email protected]> wrote: > On Oct 10, 2024, at 13:20, Ebru Aydin Gol <[email protected]> wrote: > > > Thanks for your efforts, a secondary directory for extensions is a very > useful feature. > > > > Is there any updates on the patch? > > There haven't been, no, but your reply has chastened me! I’ve now started > a separate thread[1] proposing directory-based extension packaging, as > promised up-thread. > > Best, > > David > > [1]: > https://www.postgresql.org/message-id/2CAD6FA7-DC25-48FC-80F2-8F203DECAE6A%40justatheory.com > > ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-11-11 07:16 Peter Eisentraut <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 2 replies; 47+ messages in thread From: Peter Eisentraut @ 2024-11-11 07:16 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; Craig Ringer <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers 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 <[email protected]> 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/[email protected] --- 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 Attachments: [text/plain] v0-0001-extension_control_path.patch (15.4K, ../../[email protected]/2-v0-0001-extension_control_path.patch) download | inline diff: From 08620bae551bb6b2e3a44aa3df4c8751ad06f804 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> 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/[email protected] --- 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 ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-11-11 18:15 David E. Wheeler <[email protected]> parent: Peter Eisentraut <[email protected]> 1 sibling, 0 replies; 47+ messages in thread From: David E. Wheeler @ 2024-11-11 18:15 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Craig Ringer <[email protected]>; Gabriele Bartolini <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Nov 11, 2024, at 02:16, Peter Eisentraut <[email protected]> wrote: > I implemented a patch along the lines Craig had suggested. Oh, nice, thank you. > 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. I assume we can bikeshed these names later. :-) > 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. What about shared libraries files? > 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). If I understand correctly, shared modules still lie in dynamic_library_path, yes? That makes things a bit more complicated, as the CNPG use case has to set up multiple persistent volumes to persist files put into various directories, notably sharedir and pkglibdir. > - 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. In the broader extension organization RFC I’ve been working up[1], I propose a new Makefile prefix for the destination directory for an extension. It hinges on the idea that an extension has all of its files organized in a directory with the extension name, rather than separate params for data, pkglib, bin, etc. > - 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. +1 > - 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. Since that’s set at build/install time, couldn’t the definition of `$libdir` here be changed to mean “the directory into which it’s being installed right now?”. Doesn’t seem necessary to search a path if the specific location is set at install time. > 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. > <v0-0001-extension_control_path.patch> This is what I propose,[1] yes. If we redefine the organization of extension files to live in a single directory named for an extension, then once you’ve found the control files all the other files are there, too. But `$libdir` is presumably still meaningful, since the first time you call an extension function in a new connection, it just tries to load that location, it doesn’t go through the control file search process, AFAIK. Perhaps I misunderstand, but I would like to talk through the implications of a more radical rethinking of extension file location along the lines of the other thread[2] and the RFC I’m working up based on them both[1], especially since there are a few other use cases that inform it. A notable one is the idea Gabriele shared with me in Athens to be able to add an extension to a running CNPG pod by simply mounting a read-only volume with all the files it requires. Best, David [1]: https://github.com/theory/justatheory/pull/7/files ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2024-12-05 11:07 Peter Eisentraut <[email protected]> parent: Peter Eisentraut <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Peter Eisentraut @ 2024-12-05 11:07 UTC (permalink / raw) To: pgsql-hackers; +Cc: Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]> On 11.11.24 08:16, Peter Eisentraut wrote: > 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. > 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. I've made a bit of progress on this patch, filled in some documentation and resolved some TODO markers. Also: > 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. This works now. > - 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. I have solved this by just stripping off "$libdir/" from the front of the filename. This works for now. We can think about other ways to tweak this, perhaps, but I don't see any drawback to this in practice. This patch is now complete enough for testing, I think. As I mentioned earlier, I haven't updated pg_available_extensions() etc. to support the path, but that shouldn't prevent some testing. From fdf1bd8abbb9aaff3747c57d653ef19f8be31fe8 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v1] 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/[email protected] TODO: Some utility functions such as pg_available_extensions() are not adjusted to be aware of the path yet. --- doc/src/sgml/config.sgml | 68 +++++++++++++++ doc/src/sgml/extend.sgml | 19 ++-- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 ++-- src/backend/commands/extension.c | 87 ++++++++++++++----- src/backend/utils/fmgr/dfmgr.c | 76 ++++++++++------ src/backend/utils/misc/guc_tables.c | 13 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + 10 files changed, 230 insertions(+), 63 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e0c8325a39c..94ef9fb44d7 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10476,6 +10476,74 @@ <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> + 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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 218940ee5ce..0a20d06abfb 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ <title>Extension Files</title> 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 @@ <title>Extension Files</title> <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 @@ <title>Extension Files</title> <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. @@ -1808,6 +1813,10 @@ <title>Extension Building Infrastructure</title> 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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ <title>Parameters</title> <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index eac3d001211..7c1bffbea07 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index af6bd8ff426..9115a48d25f 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,12 @@ 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 +351,45 @@ 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 *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, 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 +405,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,27 +483,25 @@ 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), + 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", @@ -2114,6 +2151,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: make aware of path */ Datum pg_available_extensions(PG_FUNCTION_ARGS) @@ -2194,6 +2233,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: make aware of path */ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) @@ -2366,6 +2407,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: make aware of path */ bool extension_file_exists(const char *extensionName) @@ -2445,6 +2488,8 @@ convert_requires_to_datum(List *requires) /* * This function reports the version update paths that exist for the * specified extension. + * + * TODO: make aware of path */ 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 8e81ecc7491..2372d0944c0 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 8cf1afbad20..5a280f62b77 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" @@ -4252,6 +4253,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index a2ac7575ca7..83e72b33fb7 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -774,6 +774,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: 71cb352904c1833fe067d6f191269710fe2ca06f -- 2.47.1 Attachments: [text/plain] v1-0001-extension_control_path.patch (23.4K, ../../[email protected]/2-v1-0001-extension_control_path.patch) download | inline diff: From fdf1bd8abbb9aaff3747c57d653ef19f8be31fe8 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v1] 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/[email protected] TODO: Some utility functions such as pg_available_extensions() are not adjusted to be aware of the path yet. --- doc/src/sgml/config.sgml | 68 +++++++++++++++ doc/src/sgml/extend.sgml | 19 ++-- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 ++-- src/backend/commands/extension.c | 87 ++++++++++++++----- src/backend/utils/fmgr/dfmgr.c | 76 ++++++++++------ src/backend/utils/misc/guc_tables.c | 13 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + 10 files changed, 230 insertions(+), 63 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e0c8325a39c..94ef9fb44d7 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10476,6 +10476,74 @@ <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> + 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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 218940ee5ce..0a20d06abfb 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ <title>Extension Files</title> 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 @@ <title>Extension Files</title> <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 @@ <title>Extension Files</title> <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. @@ -1808,6 +1813,10 @@ <title>Extension Building Infrastructure</title> 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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ <title>Parameters</title> <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index eac3d001211..7c1bffbea07 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index af6bd8ff426..9115a48d25f 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,12 @@ 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 +351,45 @@ 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 *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, 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 +405,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,27 +483,25 @@ 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), + 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", @@ -2114,6 +2151,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: make aware of path */ Datum pg_available_extensions(PG_FUNCTION_ARGS) @@ -2194,6 +2233,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: make aware of path */ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) @@ -2366,6 +2407,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: make aware of path */ bool extension_file_exists(const char *extensionName) @@ -2445,6 +2488,8 @@ convert_requires_to_datum(List *requires) /* * This function reports the version update paths that exist for the * specified extension. + * + * TODO: make aware of path */ 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 8e81ecc7491..2372d0944c0 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 8cf1afbad20..5a280f62b77 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" @@ -4252,6 +4253,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index a2ac7575ca7..83e72b33fb7 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -774,6 +774,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: 71cb352904c1833fe067d6f191269710fe2ca06f -- 2.47.1 ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-02-24 13:33 Matheus Alcantara <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-02-24 13:33 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]> Hi, On Thu, Dec 5, 2024 at 8:07 AM Peter Eisentraut <[email protected]> wrote: > > This patch is now complete enough for testing, I think. As I mentioned > earlier, I haven't updated pg_available_extensions() etc. to support the > path, but that shouldn't prevent some testing. To help with this patch I'm attaching a new version with the remaining TODOs fixed and also with a new TAP test. Thoughts? -- Matheus Alcantara Attachments: [application/octet-stream] v2-0001-extension_control_path.patch (33.9K, ../../CAFY6G8dzuLXFjONX5E-TvyD5E9r21b5vp1My_nMcqdGD0VYfYw@mail.gmail.com/2-v2-0001-extension_control_path.patch) download | inline diff: From 092f300f31540fff85b116c25800182caec2a963 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v2] 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/[email protected] --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 353 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 76 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 54 +++ 13 files changed, 445 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a8354576108..cb67387243f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10698,6 +10698,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..362fa4a647a 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,6 +51,7 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" +#include "nodes/pg_list.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.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 @@ -328,29 +333,93 @@ 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; + char *token; + char *path; + 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); + ecp = system_dir; + + if (strlen(Extension_control_path) == 0) + { + paths = lappend(paths, ecp); + } + else + { + /* Duplicate the string so we can modify it */ + ecp = pstrdup(Extension_control_path); + + token = strtok(ecp, ":"); + while (token != NULL) + { + /* Duplicate location to store in paths */ + if (strcmp(token, "$system") == 0) + path = pstrdup(system_dir); + else + path = pstrdup(token); + + paths = lappend(paths, path); + + token = strtok(NULL, ":"); + } + + pfree(system_dir); + pfree(ecp); + } + + 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, 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 +435,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,27 +513,25 @@ 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), + 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", @@ -2121,68 +2188,75 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; DIR *dir; struct dirent *de; + List *locations; + ListCell *cell; /* 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - 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 +2275,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - 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) + locations = get_extension_control_directories(); + foreach(cell, locations) { - /* do nothing */ - } - else - { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - /* 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 +2453,56 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; + ListCell *cell; 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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) + { + /* do nothing */ + } + else { - char *extname; + 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; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..46a46715ec7 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 690bf96ef03..c587e53078e 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" @@ -4305,6 +4306,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e771d87da1f..92d14f728b2 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -790,6 +790,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..5811307a82c 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); diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..659fdcfce56 --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,54 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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 "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; + +$node->append_conf( + 'postgresql.conf', qq{ +extension_control_path = '\$system:$ext_dir' +}); + +# Start node +$node->start; + +my $ecp = $node->safe_psql('postgres', 'show extension_control_path;'); + +is($ecp, "\$system:$ext_dir"); + +my $ret = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extensions where name = '$ext_name'"); +is($ret, "t", "Expected to list available extension on a custom extension control path directory"); + +my $ret2 = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extension_versions where name = '$ext_name'"); +is($ret2, "t", "Expected to list available extension with version on a custom extension control path directory"); + +done_testing(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-02-25 12:45 Andrew Dunstan <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Andrew Dunstan @ 2025-02-25 12:45 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]> On 2025-02-24 Mo 8:33 AM, Matheus Alcantara wrote: > Hi, > > On Thu, Dec 5, 2024 at 8:07 AM Peter Eisentraut <[email protected]> wrote: >> This patch is now complete enough for testing, I think. As I mentioned >> earlier, I haven't updated pg_available_extensions() etc. to support the >> path, but that shouldn't prevent some testing. > To help with this patch I'm attaching a new version with the remaining TODOs > fixed and also with a new TAP test. > > Thoughts? > I think your additions generally look good. We should be able to simplify this: + system_dir = psprintf("%s/extension", sharepath); + ecp = system_dir; + + if (strlen(Extension_control_path) == 0) + { + paths = lappend(paths, ecp); + } cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-02-25 20:29 Matheus Alcantara <[email protected]> parent: Andrew Dunstan <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-02-25 20:29 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; pgsql-hackers; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]> Thanks for reviewing! On Tue, Feb 25, 2025 at 9:45 AM Andrew Dunstan <[email protected]> wrote: > I think your additions generally look good. We should be able to > simplify this: > > > + system_dir = psprintf("%s/extension", sharepath); > + ecp = system_dir; > + > + if (strlen(Extension_control_path) == 0) > + { > + paths = lappend(paths, ecp); > + } > Fixed on the attached v3. -- Matheus Alcantara Attachments: [application/octet-stream] v3-0001-extension_control_path.patch (33.8K, ../../CAFY6G8dGcv1D8tCkjrz+9DvAYG9h4U=hBm0VXjJEXExy44hauQ@mail.gmail.com/2-v3-0001-extension_control_path.patch) download | inline diff: From 1f5f8ed0f23b436e73e4e4624f2c788f9a60f40a Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v3] 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/[email protected] --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 348 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 76 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 54 +++ 13 files changed, 440 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a8354576108..cb67387243f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10698,6 +10698,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..7e8a28e4064 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,6 +51,7 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" +#include "nodes/pg_list.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.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 @@ -328,29 +333,88 @@ 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; + char *token; + char *path; + 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); + + /* Consume each path between ':' */ + for (token = strtok(ecp, ":"); token != NULL; token = strtok(NULL, ":")) + { + if (strcmp(token, "$system") == 0) + path = system_dir; + else + path = pstrdup(token); + + paths = lappend(paths, path); + } + + pfree(ecp); + } + + 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, 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 +430,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,27 +508,25 @@ 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), + 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", @@ -2121,68 +2183,75 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; DIR *dir; struct dirent *de; + List *locations; + ListCell *cell; /* 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - 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 +2270,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - 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 + locations = get_extension_control_directories(); + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - /* 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 +2448,56 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; + ListCell *cell; 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..46a46715ec7 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 690bf96ef03..c587e53078e 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" @@ -4305,6 +4306,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e771d87da1f..92d14f728b2 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -790,6 +790,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..5811307a82c 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); diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..659fdcfce56 --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,54 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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 "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; + +$node->append_conf( + 'postgresql.conf', qq{ +extension_control_path = '\$system:$ext_dir' +}); + +# Start node +$node->start; + +my $ecp = $node->safe_psql('postgres', 'show extension_control_path;'); + +is($ecp, "\$system:$ext_dir"); + +my $ret = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extensions where name = '$ext_name'"); +is($ret, "t", "Expected to list available extension on a custom extension control path directory"); + +my $ret2 = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extension_versions where name = '$ext_name'"); +is($ret2, "t", "Expected to list available extension with version on a custom extension control path directory"); + +done_testing(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-02-28 15:35 Matheus Alcantara <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 2 replies; 47+ messages in thread From: Matheus Alcantara @ 2025-02-28 15:35 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi On Tue, Feb 25, 2025 at 5:29 PM Matheus Alcantara <[email protected]> wrote: > Fixed on the attached v3. > After I've sent the v3 patch I noticed that the tests were failing on windows. The problem was on TAP test that was using ":" as a separator on extension_control_path and also the path was not being escaped correctly resulting in a wrong path configuration. Attached v4 with all these fixes. -- Matheus Alcantara Attachments: [application/octet-stream] v4-0001-extension_control_path.patch (34.0K, ../../CAFY6G8eOMkGXLqzUz=qXrwt4X_HvmVZ13570DAbgAiw+3q03DA@mail.gmail.com/2-v4-0001-extension_control_path.patch) download | inline diff: From 21606ffcfbc99bed8a5f8883b954f3f095b22910 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v4] 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/[email protected] --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 348 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 76 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 56 +++ 13 files changed, 442 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a8354576108..cb67387243f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10698,6 +10698,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..7e8a28e4064 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,6 +51,7 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" +#include "nodes/pg_list.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.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 @@ -328,29 +333,88 @@ 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; + char *token; + char *path; + 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); + + /* Consume each path between ':' */ + for (token = strtok(ecp, ":"); token != NULL; token = strtok(NULL, ":")) + { + if (strcmp(token, "$system") == 0) + path = system_dir; + else + path = pstrdup(token); + + paths = lappend(paths, path); + } + + pfree(ecp); + } + + 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, 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 +430,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,27 +508,25 @@ 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), + 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", @@ -2121,68 +2183,75 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; DIR *dir; struct dirent *de; + List *locations; + ListCell *cell; /* 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - 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 +2270,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - 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 + locations = get_extension_control_directories(); + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - /* 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 +2448,56 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; + ListCell *cell; 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..46a46715ec7 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 690bf96ef03..c587e53078e 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" @@ -4305,6 +4306,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e771d87da1f..92d14f728b2 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -790,6 +790,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..5811307a82c 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); diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..74747b3e9f4 --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,56 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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 "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"); + +my $ret = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extensions where name = '$ext_name'"); +is($ret, "t", "Expected to list available extension on a custom extension control path directory"); + +my $ret2 = $node->safe_psql( + 'postgres', + "select count(1) > 0 as ok from pg_available_extension_versions where name = '$ext_name'"); +is($ret2, "t", "Expected to list available extension with version on a custom extension control path directory"); + +done_testing(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-01 09:15 Gabriele Bartolini <[email protected]> parent: Matheus Alcantara <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Gabriele Bartolini @ 2025-03-01 09:15 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Peter Eisentraut <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi everyone, I have finally been able to test the patch in a Kubernetes environment with CloudNativePG, thanks to Niccolò Fei and Marco Nenciarini, who created a pilot patch for CloudNativePG ( https://github.com/cloudnative-pg/cloudnative-pg/pull/6546). In the meantime, Kubernetes is likely adding the ImageVolume feature starting from the upcoming version 1.33. I will write a blog post soon about how CloudNativePG will benefit from this feature. See https://github.com/kubernetes/enhancements/issues/4639. Although the steps are not easily reproducible by everyone, I can confirm that I successfully mounted a volume in the Postgres pod using a container image that includes only pgvector (with a size of 1.6MB - see https://github.com/EnterpriseDB/pgvector/blob/dev/5645/Dockerfile.cnpg). By setting: dynamic_library_path = '$libdir:/extensions/pgvector/lib' extension_control_path = '$system:/extensions/pgvector/share' I was able to run the following queries: postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; -[ RECORD 1 ]-----+----------------------------------------------------- name | vector default_version | 0.8.0 installed_version | comment | vector data type and ivfflat and hnsw access methods postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; -[ RECORD 1 ]-----+----------------------------------------------------- name | vector default_version | 0.8.0 installed_version | comment | vector data type and ivfflat and hnsw access methods I also successfully ran the following: postgres=# SELECT * FROM pg_extension_update_paths('vector'); By emptying the content of `extension_control_path`, the vector extension disappeared from the list. postgres=# SHOW extension_control_path ; extension_control_path ------------------------ $system (1 row) postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; name | default_version | installed_version | comment ------+-----------------+-------------------+--------- (0 rows) postgres=# SELECT * FROM pg_available_extension_versions WHERE name = 'vector'; name | version | installed | superuser | trusted | relocatable | schema | requires | comment ------+---------+-----------+-----------+---------+-------------+--------+----------+--------- (0 rows) In my opinion, the patch already helps a lot and does what I can reasonably expect from a first iteration of improvements in enabling immutable container images that ship a self-contained extension to be dynamically loaded and unloaded from a Postgres cluster in Kubernetes. From here, it is all about learning how to improve things with an exploratory mindset with future versions of Postgres and Kubernetes. It's a long journey but this is a fundamental step in the right direction. Let me know if there's more testing for me to do. The documentation looks clear to me. Thank you to everyone who contributed to this patch, from the initial discussions to the development phase. I sincerely hope this is included in Postgres 18. Ciao, Gabriele On Fri, 28 Feb 2025 at 16:36, Matheus Alcantara <[email protected]> wrote: > Hi > > On Tue, Feb 25, 2025 at 5:29 PM Matheus Alcantara > <[email protected]> wrote: > > Fixed on the attached v3. > > > After I've sent the v3 patch I noticed that the tests were failing on > windows. > The problem was on TAP test that was using ":" as a separator on > extension_control_path and also the path was not being escaped correctly > resulting in a wrong path configuration. > > Attached v4 with all these fixes. > > -- > Matheus Alcantara > -- Gabriele Bartolini VP, Chief Architect, Kubernetes enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-03 13:39 Gabriele Bartolini <[email protected]> parent: Gabriele Bartolini <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Gabriele Bartolini @ 2025-03-03 13:39 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Peter Eisentraut <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers As promised, here is a blog article that provides more context and information about what this feature will mean in Kubernetes with CloudNativePG: https://www.gabrielebartolini.it/articles/2025/03/the-immutable-future-of-postgresql-extensions-in-k... Thanks, Gabriele On Sat, 1 Mar 2025 at 10:15, Gabriele Bartolini < [email protected]> wrote: > Hi everyone, > > I have finally been able to test the patch in a Kubernetes environment > with CloudNativePG, thanks to Niccolò Fei and Marco Nenciarini, who created > a pilot patch for CloudNativePG ( > https://github.com/cloudnative-pg/cloudnative-pg/pull/6546). > > In the meantime, Kubernetes is likely adding the ImageVolume feature > starting from the upcoming version 1.33. I will write a blog post soon > about how CloudNativePG will benefit from this feature. See > https://github.com/kubernetes/enhancements/issues/4639. > > Although the steps are not easily reproducible by everyone, I can confirm > that I successfully mounted a volume in the Postgres pod using a container > image that includes only pgvector (with a size of 1.6MB - see > https://github.com/EnterpriseDB/pgvector/blob/dev/5645/Dockerfile.cnpg). > > By setting: > > dynamic_library_path = '$libdir:/extensions/pgvector/lib' > extension_control_path = '$system:/extensions/pgvector/share' > > I was able to run the following queries: > > postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; > -[ RECORD 1 ]-----+----------------------------------------------------- > name | vector > default_version | 0.8.0 > installed_version | > comment | vector data type and ivfflat and hnsw access methods > > postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; > -[ RECORD 1 ]-----+----------------------------------------------------- > name | vector > default_version | 0.8.0 > installed_version | > comment | vector data type and ivfflat and hnsw access methods > > I also successfully ran the following: > > postgres=# SELECT * FROM pg_extension_update_paths('vector'); > > By emptying the content of `extension_control_path`, the vector extension > disappeared from the list. > > postgres=# SHOW extension_control_path ; > extension_control_path > ------------------------ > $system > (1 row) > > postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector'; > name | default_version | installed_version | comment > ------+-----------------+-------------------+--------- > (0 rows) > > postgres=# SELECT * FROM pg_available_extension_versions WHERE name = > 'vector'; > name | version | installed | superuser | trusted | relocatable | schema | > requires | comment > > ------+---------+-----------+-----------+---------+-------------+--------+----------+--------- > (0 rows) > > In my opinion, the patch already helps a lot and does what I can > reasonably expect from a first iteration of improvements in enabling > immutable container images that ship a self-contained extension to be > dynamically loaded and unloaded from a Postgres cluster in Kubernetes. From > here, it is all about learning how to improve things with an exploratory > mindset with future versions of Postgres and Kubernetes. It's a long > journey but this is a fundamental step in the right direction. > > Let me know if there's more testing for me to do. The documentation looks > clear to me. > > Thank you to everyone who contributed to this patch, from the initial > discussions to the development phase. I sincerely hope this is included in > Postgres 18. > > Ciao, > Gabriele > > On Fri, 28 Feb 2025 at 16:36, Matheus Alcantara <[email protected]> > wrote: > >> Hi >> >> On Tue, Feb 25, 2025 at 5:29 PM Matheus Alcantara >> <[email protected]> wrote: >> > Fixed on the attached v3. >> > >> After I've sent the v3 patch I noticed that the tests were failing on >> windows. >> The problem was on TAP test that was using ":" as a separator on >> extension_control_path and also the path was not being escaped correctly >> resulting in a wrong path configuration. >> >> Attached v4 with all these fixes. >> >> -- >> Matheus Alcantara >> > > > -- > Gabriele Bartolini > VP, Chief Architect, Kubernetes > enterprisedb.com > -- Gabriele Bartolini VP, Chief Architect, Kubernetes enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-03 15:05 David E. Wheeler <[email protected]> parent: Gabriele Bartolini <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: David E. Wheeler @ 2025-03-03 15:05 UTC (permalink / raw) To: Gabriele Bartolini <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Peter Eisentraut <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Mar 3, 2025, at 08:39, Gabriele Bartolini <[email protected]> wrote: > As promised, here is a blog article that provides more context and information about what this feature will mean in Kubernetes with CloudNativePG: https://www.gabrielebartolini.it/articles/2025/03/the-immutable-future-of-postgresql-extensions-in-k... Very nice writeup, thank you. Makes me wish for the bandwidth to get back to and start refining the PGXN OCI RFC to specify tags and/or metadata for the distribution of different contents, including: * Full extension distribution (as currently written) * Distribution with only pkglibdir and sharedir contents * Distribution with only pkglibdir contents * Distribution with only sharedir contents I think we could come up with a standard that would work very nicely for a variety of use cases. Will have to see if I can scrounge up the time for it. Best, David Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-03 15:10 David E. Wheeler <[email protected]> parent: David E. Wheeler <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: David E. Wheeler @ 2025-03-03 15:10 UTC (permalink / raw) To: Gabriele Bartolini <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Peter Eisentraut <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Mar 3, 2025, at 10:05, David E. Wheeler <[email protected]> wrote: > Very nice writeup, thank you. Makes me wish for the bandwidth to get back to and start refining the PGXN OCI RFC Forgot to link to the POC[1]. The RFC[2] is not OCI-specific, but the POC demonstrates the “full content” version. Will likely want to modify the binary distribution RFC to adopt a standard layout that allows for volume mounting to “install” an extension. Best, David 1: https://justatheory.com/2024/06/trunk-oci-poc/ 2: https://github.com/pgxn/rfcs/pull/2 Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-03 18:45 Matheus Alcantara <[email protected]> parent: Matheus Alcantara <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-03-03 18:45 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi, attached a new v5 with some minor improvements on TAP tests: - Add a proper test name for all test cases - Add CREATE EXTENSION command execution - Change the assert on pg_available_extensions and pg_available_extension_versions to validate the row content Also rebased with master -- Matheus Alcantara Attachments: [application/octet-stream] v5-0001-extension_control_path.patch (34.2K, ../../CAFY6G8fV5FTqn7p8pxbvPYUD5+ffik-J50GChjKH2Gg7d=fRPg@mail.gmail.com/2-v5-0001-extension_control_path.patch) download | inline diff: From 96f7be3962ceb3caccfcec5473bf5fcf9c9d666e Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v5] 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/[email protected] --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 348 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 76 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 2 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 67 ++++ 13 files changed, 453 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e55700f35b8..69c23f17aef 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10726,6 +10726,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..7e8a28e4064 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,6 +51,7 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" +#include "nodes/pg_list.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.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 @@ -328,29 +333,88 @@ 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; + char *token; + char *path; + 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); + + /* Consume each path between ':' */ + for (token = strtok(ecp, ":"); token != NULL; token = strtok(NULL, ":")) + { + if (strcmp(token, "$system") == 0) + path = system_dir; + else + path = pstrdup(token); + + paths = lappend(paths, path); + } + + pfree(ecp); + } + + 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, 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 +430,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,27 +508,25 @@ 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), + 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", @@ -2121,68 +2183,75 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; DIR *dir; struct dirent *de; + List *locations; + ListCell *cell; /* 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - 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 +2270,57 @@ Datum pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - char *location; + List *locations; + ListCell *cell; DIR *dir; struct dirent *de; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); - location = get_extension_control_directory(); - 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 + locations = get_extension_control_directories(); + foreach(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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 = read_extension_control_file(extname); - /* 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 +2448,56 @@ bool extension_file_exists(const char *extensionName) { bool result = false; - char *location; + List *locations; + ListCell *cell; 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(cell, locations) { - while ((de = ReadDir(dir, location)) != NULL) + char *location = (char *) lfirst(cell); + + 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; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..46a46715ec7 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,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; @@ -398,7 +397,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 +412,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 +440,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; @@ -475,47 +482,60 @@ 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 = 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 +552,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 +562,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 +571,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 ad25cbb39c5..c357a5304ae 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" @@ -4314,6 +4315,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 5362ff80519..24eaeb3dc6b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -791,6 +791,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..5811307a82c 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); diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..f857e2140df --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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"); + +done_testing(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-06 13:46 Peter Eisentraut <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Peter Eisentraut @ 2025-03-06 13:46 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; +Cc: Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 03.03.25 19:45, Matheus Alcantara wrote: > Hi, attached a new v5 with some minor improvements on TAP tests: > > - Add a proper test name for all test cases > - Add CREATE EXTENSION command execution > - Change the assert on pg_available_extensions and > pg_available_extension_versions to validate the row content > > Also rebased with master This looks very good to me. I have one issue to point out: The logic in get_extension_control_directories() needs to be a little bit more careful to align with the rules in find_in_path(). For example, it should use first_path_var_separator() to get the platform-specific path separator, and probably also substitute_path_macro() and canonicalize_path() etc., to keep everything consistent. (Maybe it would be ok to move the function to dfmgr.c to avoid having to export too many things from there.) Independent of that, attached is a small patch that suggests to use the newer foreach_ptr() macro in some places. From 35c2106558095e74359cec58b9631fa19ed937b3 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 6 Mar 2025 14:28:08 +0100 Subject: [PATCH] Use foreach_ptr --- src/backend/commands/extension.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 7e8a28e4064..4bdb20aaf54 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,10 +51,10 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" -#include "nodes/pg_list.h" #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" @@ -2183,20 +2183,17 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + List *locations; DIR *dir; struct dirent *de; - List *locations; - ListCell *cell; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach(cell, locations) + foreach_ptr(char, location, locations) { - char *location = (char *) lfirst(cell); - dir = AllocateDir(location); /* @@ -2271,7 +2268,6 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; List *locations; - ListCell *cell; DIR *dir; struct dirent *de; @@ -2279,10 +2275,9 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach(cell, locations) - { - char *location = (char *) lfirst(cell); + foreach_ptr(char, location, locations) + { dir = AllocateDir(location); /* @@ -2449,16 +2444,13 @@ extension_file_exists(const char *extensionName) { bool result = false; List *locations; - ListCell *cell; DIR *dir; struct dirent *de; locations = get_extension_control_directories(); - foreach(cell, locations) + foreach_ptr(char, location, locations) { - char *location = (char *) lfirst(cell); - dir = AllocateDir(location); /* -- 2.48.1 Attachments: [text/plain] 0001-Use-foreach_ptr.patch.nocfbot (2.3K, ../../[email protected]/2-0001-Use-foreach_ptr.patch.nocfbot) download | inline diff: From 35c2106558095e74359cec58b9631fa19ed937b3 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 6 Mar 2025 14:28:08 +0100 Subject: [PATCH] Use foreach_ptr --- src/backend/commands/extension.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 7e8a28e4064..4bdb20aaf54 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -51,10 +51,10 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/schemacmds.h" -#include "nodes/pg_list.h" #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" @@ -2183,20 +2183,17 @@ Datum pg_available_extensions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + List *locations; DIR *dir; struct dirent *de; - List *locations; - ListCell *cell; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach(cell, locations) + foreach_ptr(char, location, locations) { - char *location = (char *) lfirst(cell); - dir = AllocateDir(location); /* @@ -2271,7 +2268,6 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; List *locations; - ListCell *cell; DIR *dir; struct dirent *de; @@ -2279,10 +2275,9 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach(cell, locations) - { - char *location = (char *) lfirst(cell); + foreach_ptr(char, location, locations) + { dir = AllocateDir(location); /* @@ -2449,16 +2444,13 @@ extension_file_exists(const char *extensionName) { bool result = false; List *locations; - ListCell *cell; DIR *dir; struct dirent *de; locations = get_extension_control_directories(); - foreach(cell, locations) + foreach_ptr(char, location, locations) { - char *location = (char *) lfirst(cell); - dir = AllocateDir(location); /* -- 2.48.1 ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-10 20:25 Matheus Alcantara <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-03-10 20:25 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi, Thanks for reviewing and suggestions! On Thu, Mar 6, 2025 at 10:46 AM Peter Eisentraut <[email protected]> wrote: > This looks very good to me. I have one issue to point out: The logic > in get_extension_control_directories() needs to be a little bit more > careful to align with the rules in find_in_path(). For example, it > should use first_path_var_separator() to get the platform-specific path > separator, and probably also substitute_path_macro() and > canonicalize_path() etc., to keep everything consistent. > I fixed this hardcoded path separator issue on the TAP test and forgot to fix it also on code, sorry, fixed on this new version. I also spent some time investigating why the tests on Windows were still passing even using a wrong path separator. Consider extension_control_path = '$system;C:\custom\path' When running on Windows, the get_extension_control_directories was returning [$system;C:, \custom\path] and for somehow the \custom\path was successfully being read and since the tests was only referencing the extension on this custom path everything was passing, but querying for an extension that is only on $system was resulting in an empty query result. In the attached patch I also included a new test case to query on pg_available_extensions for an extension that is installed on the $system, so we can ensure that extensions in both paths can be used correctly. > (Maybe it would be ok to move the function to dfmgr.c to avoid having > to export too many things from there.) > I've exported substitute_path_macro because adding a new function on dfmgr would require #include nodes/pg_list.h and I'm not sure what approach would be better, please let me know what you think. -- Matheus Alcantara Attachments: [application/octet-stream] v6-0001-extension_control_path.patch (35.1K, ../../CAFY6G8cGeUV0f5K8v-Du0ts3iZyRE6Q5dNtYjQq8cjS4epLX5A@mail.gmail.com/2-v6-0001-extension_control_path.patch) download | inline diff: From f0ed47907ab40d3201dfaad6d29f57155337dc2c Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v6] 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/[email protected] --- doc/src/sgml/config.sgml | 68 ++++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 356 +++++++++++------- src/backend/utils/fmgr/dfmgr.c | 77 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 5 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 77 ++++ 13 files changed, 475 insertions(+), 174 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d2fa5f7d1a9..9fec78db6f7 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10725,6 +10725,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..a45389807de 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -54,6 +54,7 @@ #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" #include "nodes/queryjumble.h" #include "storage/fd.h" #include "tcop/utility.h" @@ -69,6 +70,9 @@ #include "utils/varlena.h" +/* GUC */ +char *Extension_control_path; + /* Globally visible state variables */ bool creating_extension = false; Oid CurrentExtensionObject = InvalidOid; @@ -79,6 +83,7 @@ Oid CurrentExtensionObject = InvalidOid; typedef struct ExtensionControlFile { char *name; /* name of the extension */ + char *control_dir; /* directory where control file was found */ char *directory; /* directory for script files */ char *default_version; /* default install target version, if any */ char *module_pathname; /* string to substitute for @@ -328,29 +333,106 @@ 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, 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 +448,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,27 +526,25 @@ 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), + 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", @@ -2121,68 +2201,72 @@ 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) - { - /* 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) { - 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 = read_extension_control_file(extname); - 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 +2285,55 @@ 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) { - ExtensionControlFile *control; - char *extname; + /* do nothing */ + } + else + { + 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 = read_extension_control_file(extname); - /* 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 +2461,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) + { + /* do nothing */ + } + else { - char *extname; + 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; diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..ca12e954ea2 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,6 @@ 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); /* Magic structure that module needs to match to be accepted */ static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; @@ -398,7 +396,7 @@ incompatible_module_error(const char *libname, /* * If name contains a slash, check if the file exists, if so return * the name. Else (no slash) try to expand using search path (see - * find_in_dynamic_libpath below); if that works, return the fully + * find_in_path below); if that works, return the fully * expanded file name. If the previous failed, append DLSUFFIX and * try again. If all fails, just return the original name. * @@ -413,17 +411,25 @@ expand_dynamic_library_name(const char *name) Assert(name); + /* + * If the value starts with "$libdir/", strip that. This is because many + * extensions have hardcoded '$libdir/foo' as their library name, which + * prevents using the path. + */ + if (strncmp(name, "$libdir/", 8) == 0) + name += 8; + have_slash = (first_dir_separator(name) != NULL); if (!have_slash) { - full = find_in_dynamic_libpath(name); + full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); if (full) return full; } else { - full = substitute_libpath_macro(name); + full = substitute_path_macro(name, "$libdir", pkglib_path); if (pg_file_exists(full)) return full; pfree(full); @@ -433,14 +439,14 @@ expand_dynamic_library_name(const char *name) if (!have_slash) { - full = find_in_dynamic_libpath(new); + full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); pfree(new); if (full) return full; } else { - full = substitute_libpath_macro(new); + full = substitute_path_macro(new, "$libdir", pkglib_path); pfree(new); if (pg_file_exists(full)) return full; @@ -474,48 +480,61 @@ check_restricted_library_name(const char *name) * Substitute for any macros appearing in the given string. * Result is always freshly palloc'd. */ -static char * -substitute_libpath_macro(const char *name) +char * +substitute_path_macro(const char *str, const char *macro, const char *value) { const char *sep_ptr; - Assert(name != NULL); + Assert(str != NULL); + Assert(macro[0] == '$'); - /* Currently, we only recognize $libdir at the start of the string */ - if (name[0] != '$') - return pstrdup(name); + /* Currently, we only recognize $macro at the start of the string */ + if (str[0] != '$') + return pstrdup(str); - if ((sep_ptr = first_dir_separator(name)) == NULL) - sep_ptr = name + strlen(name); + if ((sep_ptr = first_dir_separator(str)) == NULL) + sep_ptr = str + strlen(str); - if (strlen("$libdir") != sep_ptr - name || - strncmp(name, "$libdir", strlen("$libdir")) != 0) + if (strlen(macro) != sep_ptr - str || + strncmp(str, macro, strlen(macro)) != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("invalid macro name in dynamic library path: %s", - name))); + errmsg("invalid macro name in path: %s", + str))); - return psprintf("%s%s", pkglib_path, sep_ptr); + return psprintf("%s%s", value, sep_ptr); } /* * Search for a file called 'basename' in the colon-separated search - * path Dynamic_library_path. If the file is found, the full file name + * path given. If the file is found, the full file name * is returned in freshly palloc'd memory. If the file is not found, * return NULL. + * + * path_param is the name of the parameter that path came from, for error + * messages. + * + * macro and macro_val allow substituting a macro; see + * substitute_path_macro(). */ -static char * -find_in_dynamic_libpath(const char *basename) +char * +find_in_path(const char *basename, const char *path, const char *path_param, + const char *macro, const char *macro_val) { const char *p; size_t baselen; Assert(basename != NULL); Assert(first_dir_separator(basename) == NULL); - Assert(Dynamic_library_path != NULL); + Assert(path != NULL); + Assert(path_param != NULL); + + p = path; - p = Dynamic_library_path; + /* + * If the path variable is empty, don't do a path search. + */ if (strlen(p) == 0) return NULL; @@ -532,7 +551,7 @@ find_in_dynamic_libpath(const char *basename) if (piece == p) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("zero-length component in parameter \"dynamic_library_path\""))); + errmsg("zero-length component in parameter \"%s\"", path_param))); if (piece == NULL) len = strlen(p); @@ -542,7 +561,7 @@ find_in_dynamic_libpath(const char *basename) piece = palloc(len + 1); strlcpy(piece, p, len + 1); - mangled = substitute_libpath_macro(piece); + mangled = substitute_path_macro(piece, macro, macro_val); pfree(piece); canonicalize_path(mangled); @@ -551,13 +570,13 @@ find_in_dynamic_libpath(const char *basename) if (!is_absolute_path(mangled)) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); + errmsg("component in parameter \"%s\" is not an absolute path", path_param))); full = palloc(strlen(mangled) + 1 + baselen + 1); sprintf(full, "%s/%s", mangled, basename); pfree(mangled); - elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); + elog(DEBUG3, "%s: trying \"%s\"", __func__, full); if (pg_file_exists(full)) return full; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index ad25cbb39c5..c357a5304ae 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" @@ -4314,6 +4315,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 2d1de9c37bd..d22ef6ef47b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -791,6 +791,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..442c50d6b90 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); @@ -749,6 +751,9 @@ extern Size EstimateLibraryStateSpace(void); extern void SerializeLibraryState(Size maxsize, char *start_address); extern void RestoreLibraryState(char *start_address); +extern char * +substitute_path_macro(const char *str, const char *macro, const char *value); + /* * Support for aggregate functions * diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..19c9ed9748b --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,77 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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"); + +done_testing(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-11 15:58 Peter Eisentraut <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 1 reply; 47+ messages in thread From: Peter Eisentraut @ 2025-03-11 15:58 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 10.03.25 21:25, Matheus Alcantara wrote: > On Thu, Mar 6, 2025 at 10:46 AM Peter Eisentraut <[email protected]> wrote: >> This looks very good to me. I have one issue to point out: The logic >> in get_extension_control_directories() needs to be a little bit more >> careful to align with the rules in find_in_path(). For example, it >> should use first_path_var_separator() to get the platform-specific path >> separator, and probably also substitute_path_macro() and >> canonicalize_path() etc., to keep everything consistent. >> > I fixed this hardcoded path separator issue on the TAP test and forgot > to fix it also on code, sorry, fixed on this new version. >> (Maybe it would be ok to move the function to dfmgr.c to avoid having >> to export too many things from there.) >> > I've exported substitute_path_macro because adding a new function on > dfmgr would require #include nodes/pg_list.h and I'm not sure what > approach would be better, please let me know what you think. Yes, that structure looks ok. But you can remove one level of block in get_extension_control_directories(). I found a bug that was already present in my earlier patch versions: @@ -423,7 +424,7 @@ find_extension_control_filename(ExtensionControlFile *control) ecp = Extension_control_path; if (strlen(ecp) == 0) ecp = "$system"; - result = find_in_path(basename, Extension_control_path, "extension_control_path", "$system", system_dir); + result = find_in_path(basename, ecp, "extension_control_path", "$system", system_dir); Without this, it won't work if you set extension_control_path empty. (Maybe add a test for that?) I think this all works now, but I think the way pg_available_extensions() works is a bit strange and inefficient. After it finds a candidate control file, it calls read_extension_control_file() with the extension name, that calls parse_extension_control_file(), that calls find_extension_control_filename(), and that calls find_in_path(), which searches that path again! There should be a simpler way into this. Maybe pg_available_extensions() should fill out the ExtensionControlFile structure itself, set ->control_dir with where it found it, then call directly to parse_extension_control_file(), and that should skip the finding if the directory is already set. Or something similar. ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-12 13:17 Matheus Alcantara <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 2 replies; 47+ messages in thread From: Matheus Alcantara @ 2025-03-12 13:17 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Tue, Mar 11, 2025 at 12:59 PM Peter Eisentraut <[email protected]> wrote: > Yes, that structure looks ok. But you can remove one level of block in > get_extension_control_directories(). > Sorry, missed during debugging. Fixed > I found a bug that was already present in my earlier patch versions: > > @@ -423,7 +424,7 @@ find_extension_control_filename(ExtensionControlFile > *control) > ecp = Extension_control_path; > if (strlen(ecp) == 0) > ecp = "$system"; > - result = find_in_path(basename, Extension_control_path, > "extension_control_path", "$system", system_dir); > + result = find_in_path(basename, ecp, "extension_control_path", > "$system", system_dir); > > Without this, it won't work if you set extension_control_path empty. > (Maybe add a test for that?) > Fixed, and also added a new test case for this. > I think this all works now, but I think the way > pg_available_extensions() works is a bit strange and inefficient. After > it finds a candidate control file, it calls > read_extension_control_file() with the extension name, that calls > parse_extension_control_file(), that calls > find_extension_control_filename(), and that calls find_in_path(), which > searches that path again! > > There should be a simpler way into this. Maybe > pg_available_extensions() should fill out the ExtensionControlFile > structure itself, set ->control_dir with where it found it, then call > directly to parse_extension_control_file(), and that should skip the > finding if the directory is already set. Or something similar. > Good catch. I fixed this by creating a new function to construct the ExtensionControlFile and changed the pg_available_extensions to set the control_dir. The read_extension_control_file was also changed to just call this new function constructor. I implemented the logic to check if the control_dir is already set on parse_extension_control_file because it seems to me that make more sense to not call find_extension_control_filename instead of putting this logic there since we already set the control_dir when we find the control file, and having the logic to set the control_dir or skip the find_in_path seems more confusing on this function instead of on parse_extension_control_file. Please let me know what you think. -- Matheus Alcantara Attachments: [application/octet-stream] v7-0001-extension_control_path.patch (38.3K, ../../CAFY6G8d56BK5TZ7K+uw4DHLZ=9Th3p+E1x4pp26nJ_K5k4EUwA@mail.gmail.com/2-v7-0001-extension_control_path.patch) download | inline diff: From a0e8ae7af182cf5f37442b85c42b9c40d84419bb Mon Sep 17 00:00:00 2001 From: Peter Eisentraut <[email protected]> Date: Thu, 5 Dec 2024 11:49:05 +0100 Subject: [PATCH v7] 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/[email protected] --- doc/src/sgml/config.sgml | 68 +++ doc/src/sgml/extend.sgml | 19 +- doc/src/sgml/ref/create_extension.sgml | 6 +- src/Makefile.global.in | 19 +- src/backend/commands/extension.c | 416 ++++++++++++------ src/backend/utils/fmgr/dfmgr.c | 77 ++-- src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/extension.h | 2 + src/include/fmgr.h | 5 + src/test/modules/test_extensions/Makefile | 1 + src/test/modules/test_extensions/meson.build | 5 + .../t/001_extension_control_path.pl | 86 ++++ 13 files changed, 535 insertions(+), 183 deletions(-) create mode 100644 src/test/modules/test_extensions/t/001_extension_control_path.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d2fa5f7d1a9..9fec78db6f7 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10725,6 +10725,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> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index ba492ca27c0..64f8e133cae 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -649,6 +649,11 @@ RETURNS anycompatible AS ... control file can specify a different directory for the script file(s). </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> diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index ca2b80d669c..713abd9c494 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -90,8 +90,10 @@ CREATE EXTENSION [ IF NOT EXISTS ] <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> diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 3b620bac5ac..8fe9d61e82a 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -87,9 +87,19 @@ endif # not PGXS # # In a PGXS build, we cannot use the values inserted into Makefile.global # by configure, since the installation tree may have been relocated. -# Instead get the path values from pg_config. +# Instead get the path values from pg_config. But users can specify +# prefix explicitly, if they want to select their own installation +# location. -ifndef PGXS +ifdef PGXS +# Extension makefiles should set PG_CONFIG, but older ones might not +ifndef PG_CONFIG +PG_CONFIG = pg_config +endif +endif + +# This means: if ((not PGXS) or prefix) +ifneq (,$(if $(PGXS),,1)$(prefix)) # Note that prefix, exec_prefix, and datarootdir aren't defined in a PGXS build; # makefiles may only use the derived variables such as bindir. @@ -147,11 +157,6 @@ localedir := @localedir@ else # PGXS case -# Extension makefiles should set PG_CONFIG, but older ones might not -ifndef PG_CONFIG -PG_CONFIG = pg_config -endif - bindir := $(shell $(PG_CONFIG) --bindir) datadir := $(shell $(PG_CONFIG) --sharedir) sysconfdir := $(shell $(PG_CONFIG) --sysconfdir) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index d9bb4ce5f1e..aa45f4810b8 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -54,6 +54,7 @@ #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" #include "nodes/queryjumble.h" #include "storage/fd.h" #include "tcop/utility.h" @@ -69,6 +70,9 @@ #include "utils/varlena.h" +/* GUC */ +char *Extension_control_path; + /* Globally visible state variables */ bool creating_extension = false; Oid CurrentExtensionObject = InvalidOid; @@ -79,6 +83,7 @@ Oid CurrentExtensionObject = InvalidOid; typedef struct ExtensionControlFile { char *name; /* name of the extension */ + char *control_dir; /* directory where control file was found */ char *directory; /* directory for script files */ char *default_version; /* default install target version, if any */ char *module_pathname; /* string to substitute for @@ -147,6 +152,8 @@ static void ExecAlterExtensionContentsRecurse(AlterExtensionContentsStmt *stmt, ObjectAddress object); static char *read_whole_file(const char *filename, int *length); +static ExtensionControlFile *new_ExtensionControlFile(const char *extname); + /* * get_extension_oid - given an extension name, look up the OID @@ -328,29 +335,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 +448,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 +506,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 +531,52 @@ 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 ((file = AllocateFile(filename, "r")) == NULL) { - if (errno == ENOENT) + /* + * Skip searching if control_dir is already set. We assume that + * control_dir is set correctly to find the .control file, otherwise + * ereport extension not available error. + */ + if (control->control_dir != NULL) { - /* no complaint for missing auxiliary file */ - if (version) + /* + * Don't forget to consider path separator, .control suffix and + * null terminator. + */ + filename = palloc(strlen(control->control_dir) + 1 + strlen(control->name) + 8 + 1); + sprintf(filename, "%s/%s.control", control->control_dir, control->name); + + if (!pg_file_exists(filename)) { + /* + * Extension is not available. Free the memory and set to NULL + * for ereporting. + */ pfree(filename); - return; + filename = NULL; } + } + else + filename = find_extension_control_filename(control); + } - /* 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."))); + 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) + { + /* no complaint for missing auxiliary file */ + if (errno == ENOENT && version) + { + pfree(filename); + return; } + ereport(ERROR, (errcode_for_file_access(), errmsg("could not open extension control file \"%s\": %m", @@ -603,17 +715,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 +2223,75 @@ 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) - { - /* 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) { - 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); - memset(values, 0, sizeof(values)); - memset(nulls, 0, sizeof(nulls)); + parse_extension_control_file(control, NULL); - /* 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); + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); - tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, - values, 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); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } - FreeDir(dir); + FreeDir(dir); + } } return (Datum) 0; @@ -2201,51 +2310,55 @@ 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) + 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; + /* do nothing */ + } + else + { + 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 = read_extension_control_file(extname); - /* 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 +2486,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) + 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 + * 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 +3810,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 = (ExtensionControlFile *) palloc0(sizeof(ExtensionControlFile)); + + control->name = pstrdup(extname); + control->relocatable = false; + control->superuser = true; + control->trusted = false; + control->encoding = -1; + + return control; +} diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 87b233cb887..ca12e954ea2 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -71,8 +71,6 @@ 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); /* Magic structure that module needs to match to be accepted */ static const Pg_magic_struct magic_data = PG_MODULE_MAGIC_DATA; @@ -398,7 +396,7 @@ incompatible_module_error(const char *libname, /* * If name contains a slash, check if the file exists, if so return * the name. Else (no slash) try to expand using search path (see - * find_in_dynamic_libpath below); if that works, return the fully + * find_in_path below); if that works, return the fully * expanded file name. If the previous failed, append DLSUFFIX and * try again. If all fails, just return the original name. * @@ -413,17 +411,25 @@ expand_dynamic_library_name(const char *name) Assert(name); + /* + * If the value starts with "$libdir/", strip that. This is because many + * extensions have hardcoded '$libdir/foo' as their library name, which + * prevents using the path. + */ + if (strncmp(name, "$libdir/", 8) == 0) + name += 8; + have_slash = (first_dir_separator(name) != NULL); if (!have_slash) { - full = find_in_dynamic_libpath(name); + full = find_in_path(name, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); if (full) return full; } else { - full = substitute_libpath_macro(name); + full = substitute_path_macro(name, "$libdir", pkglib_path); if (pg_file_exists(full)) return full; pfree(full); @@ -433,14 +439,14 @@ expand_dynamic_library_name(const char *name) if (!have_slash) { - full = find_in_dynamic_libpath(new); + full = find_in_path(new, Dynamic_library_path, "dynamic_library_path", "$libdir", pkglib_path); pfree(new); if (full) return full; } else { - full = substitute_libpath_macro(new); + full = substitute_path_macro(new, "$libdir", pkglib_path); pfree(new); if (pg_file_exists(full)) return full; @@ -474,48 +480,61 @@ check_restricted_library_name(const char *name) * Substitute for any macros appearing in the given string. * Result is always freshly palloc'd. */ -static char * -substitute_libpath_macro(const char *name) +char * +substitute_path_macro(const char *str, const char *macro, const char *value) { const char *sep_ptr; - Assert(name != NULL); + Assert(str != NULL); + Assert(macro[0] == '$'); - /* Currently, we only recognize $libdir at the start of the string */ - if (name[0] != '$') - return pstrdup(name); + /* Currently, we only recognize $macro at the start of the string */ + if (str[0] != '$') + return pstrdup(str); - if ((sep_ptr = first_dir_separator(name)) == NULL) - sep_ptr = name + strlen(name); + if ((sep_ptr = first_dir_separator(str)) == NULL) + sep_ptr = str + strlen(str); - if (strlen("$libdir") != sep_ptr - name || - strncmp(name, "$libdir", strlen("$libdir")) != 0) + if (strlen(macro) != sep_ptr - str || + strncmp(str, macro, strlen(macro)) != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("invalid macro name in dynamic library path: %s", - name))); + errmsg("invalid macro name in path: %s", + str))); - return psprintf("%s%s", pkglib_path, sep_ptr); + return psprintf("%s%s", value, sep_ptr); } /* * Search for a file called 'basename' in the colon-separated search - * path Dynamic_library_path. If the file is found, the full file name + * path given. If the file is found, the full file name * is returned in freshly palloc'd memory. If the file is not found, * return NULL. + * + * path_param is the name of the parameter that path came from, for error + * messages. + * + * macro and macro_val allow substituting a macro; see + * substitute_path_macro(). */ -static char * -find_in_dynamic_libpath(const char *basename) +char * +find_in_path(const char *basename, const char *path, const char *path_param, + const char *macro, const char *macro_val) { const char *p; size_t baselen; Assert(basename != NULL); Assert(first_dir_separator(basename) == NULL); - Assert(Dynamic_library_path != NULL); + Assert(path != NULL); + Assert(path_param != NULL); + + p = path; - p = Dynamic_library_path; + /* + * If the path variable is empty, don't do a path search. + */ if (strlen(p) == 0) return NULL; @@ -532,7 +551,7 @@ find_in_dynamic_libpath(const char *basename) if (piece == p) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("zero-length component in parameter \"dynamic_library_path\""))); + errmsg("zero-length component in parameter \"%s\"", path_param))); if (piece == NULL) len = strlen(p); @@ -542,7 +561,7 @@ find_in_dynamic_libpath(const char *basename) piece = palloc(len + 1); strlcpy(piece, p, len + 1); - mangled = substitute_libpath_macro(piece); + mangled = substitute_path_macro(piece, macro, macro_val); pfree(piece); canonicalize_path(mangled); @@ -551,13 +570,13 @@ find_in_dynamic_libpath(const char *basename) if (!is_absolute_path(mangled)) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), - errmsg("component in parameter \"dynamic_library_path\" is not an absolute path"))); + errmsg("component in parameter \"%s\" is not an absolute path", path_param))); full = palloc(strlen(mangled) + 1 + baselen + 1); sprintf(full, "%s/%s", mangled, basename); pfree(mangled); - elog(DEBUG3, "find_in_dynamic_libpath: trying \"%s\"", full); + elog(DEBUG3, "%s: trying \"%s\"", __func__, full); if (pg_file_exists(full)) return full; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index ad25cbb39c5..c357a5304ae 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" @@ -4314,6 +4315,18 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, + gettext_noop("Sets the path for extension control files."), + gettext_noop("The remaining extension script and secondary control files are then loaded " + "from the same directory where the primary control file was found."), + GUC_SUPERUSER_ONLY + }, + &Extension_control_path, + "$system", + NULL, NULL, NULL + }, + { {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, gettext_noop("Sets the location of the Kerberos server key file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 2d1de9c37bd..d22ef6ef47b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -791,6 +791,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Other Defaults - #dynamic_library_path = '$libdir' +#extension_control_path = '$system' #gin_fuzzy_search_limit = 0 diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 0b636405120..24419bfb5c9 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -17,6 +17,8 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +/* GUC */ +extern PGDLLIMPORT char *Extension_control_path; /* * creating_extension is only true while running a CREATE EXTENSION or ALTER diff --git a/src/include/fmgr.h b/src/include/fmgr.h index e609ea875a7..442c50d6b90 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); @@ -749,6 +751,9 @@ extern Size EstimateLibraryStateSpace(void); extern void SerializeLibraryState(Size maxsize, char *start_address); extern void RestoreLibraryState(char *start_address); +extern char * +substitute_path_macro(const char *str, const char *macro, const char *value); + /* * Support for aggregate functions * diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index 1dbec14cba3..a3591bf3d2f 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -28,6 +28,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext_req_schema3--1.0.sql REGRESS = test_extensions test_extdepend +TAP_TESTS = 1 # force C locale for output stability NO_LOCALE = 1 diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index dd7ec0ce56b..3c7e378bf35 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -57,4 +57,9 @@ tests += { ], 'regress_args': ['--no-locale'], }, + 'tap': { + 'tests': [ + 't/001_extension_control_path.pl', + ], + }, } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl new file mode 100644 index 00000000000..1cf01fca57d --- /dev/null +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -0,0 +1,86 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +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(); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 06:42 Peter Eisentraut <[email protected]> parent: Matheus Alcantara <[email protected]> 1 sibling, 3 replies; 47+ messages in thread From: Peter Eisentraut @ 2025-03-19 06:42 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 12.03.25 14:17, Matheus Alcantara wrote: >> There should be a simpler way into this. Maybe >> pg_available_extensions() should fill out the ExtensionControlFile >> structure itself, set ->control_dir with where it found it, then call >> directly to parse_extension_control_file(), and that should skip the >> finding if the directory is already set. Or something similar. >> > Good catch. I fixed this by creating a new function to construct the > ExtensionControlFile and changed the pg_available_extensions to set the > control_dir. The read_extension_control_file was also changed to just > call this new function constructor. I implemented the logic to check if > the control_dir is already set on parse_extension_control_file because > it seems to me that make more sense to not call > find_extension_control_filename instead of putting this logic there > since we already set the control_dir when we find the control file, and > having the logic to set the control_dir or skip the find_in_path seems > more confusing on this function instead of on > parse_extension_control_file. Please let me know what you think. Committed that, thanks. A small tweak I made was to replace palloc+snprintf by psprintf. Maybe you were not aware that that function exists. I also simplified the error handling in parse_extension_control_file() a bit. If we pass in a control directory (which is the new code we're adding), then we can assume that we already found the file earlier, and then if we now don't find it, then we should just report the file system error instead of the "you should install this extension first" error. It's kind of a "can't happen" error anyway, so the different is small. ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 07:03 Gabriele Bartolini <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 0 replies; 47+ messages in thread From: Gabriele Bartolini @ 2025-03-19 07:03 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Thanks everyone for making this happen. Ciao, Gabriele On Wed, 19 Mar 2025 at 07:42, Peter Eisentraut <[email protected]> wrote: > On 12.03.25 14:17, Matheus Alcantara wrote: > >> There should be a simpler way into this. Maybe > >> pg_available_extensions() should fill out the ExtensionControlFile > >> structure itself, set ->control_dir with where it found it, then call > >> directly to parse_extension_control_file(), and that should skip the > >> finding if the directory is already set. Or something similar. > >> > > Good catch. I fixed this by creating a new function to construct the > > ExtensionControlFile and changed the pg_available_extensions to set the > > control_dir. The read_extension_control_file was also changed to just > > call this new function constructor. I implemented the logic to check if > > the control_dir is already set on parse_extension_control_file because > > it seems to me that make more sense to not call > > find_extension_control_filename instead of putting this logic there > > since we already set the control_dir when we find the control file, and > > having the logic to set the control_dir or skip the find_in_path seems > > more confusing on this function instead of on > > parse_extension_control_file. Please let me know what you think. > > Committed that, thanks. > > A small tweak I made was to replace palloc+snprintf by psprintf. Maybe > you were not aware that that function exists. > > I also simplified the error handling in parse_extension_control_file() a > bit. If we pass in a control directory (which is the new code we're > adding), then we can assume that we already found the file earlier, and > then if we now don't find it, then we should just report the file system > error instead of the "you should install this extension first" error. > It's kind of a "can't happen" error anyway, so the different is small. > > -- Gabriele Bartolini VP, Chief Architect, Kubernetes enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 14:58 Christoph Berg <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 0 replies; 47+ messages in thread From: Christoph Berg @ 2025-03-19 14:58 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Re: Peter Eisentraut > Committed that, thanks. Awesome, thanks! It works perfectly for the Debian "test extension packages at build time" use case, replacing our old extension_destdir patch. PKGARGS="--pgoption extension_control_path=$PWD/debian/$PACKAGE/usr/share/postgresql/$v/extension:\$system --pgoption dynamic_library_path=$PWD/debian/$PACKAGE/usr/lib/postgresql/$v/lib:/usr/lib/postgresql/$v/lib" https://salsa.debian.org/postgresql/postgresql-common/-/commit/3792eea42e4dcef39b5c8d99f63deb8091ef9... Christoph ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 17:29 David E. Wheeler <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 2 replies; 47+ messages in thread From: David E. Wheeler @ 2025-03-19 17:29 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Mar 19, 2025, at 02:42, Peter Eisentraut <[email protected]> wrote: > Committed that, thanks. 🎉 I’ve been meaning to test the patch again, so here goes. First thing I notice is that prefix= uses the magic to insert “postgresql” into the path if it’s not already there: ``` console ❯ make PG_CONFIG=~/dev/c/postgres/pgsql-devel/bin/pg_config prefix=/Users/david/Downloads install /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/postgresql/extension' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/postgresql/extension' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/doc//postgresql/extension' /opt/homebrew/bin/ginstall -c -m 644 .//pair.control '/Users/david/Downloads/share/postgresql/extension/' /opt/homebrew/bin/ginstall -c -m 644 .//sql/pair--0.1.2.sql .//sql/pair--unpackaged--0.1.2.sql '/Users/david/Downloads/share/postgresql/extension/' /opt/homebrew/bin/ginstall -c -m 644 .//doc/pair.md '/Users/david/Downloads/share/doc//postgresql/extension/‘ ``` I think this should at least be documented, but generally feels unexpected to me. I’ve attached a patch that fleshes out the docs, along with an example of setting `extension_control_path` and `dynamic_library_path` to use the locations. It might not have the information right about the need for “postgresql” or “pgsql” in the path. Back in 2003[1] it was just “postgres”, but I couldn’t find the logic for it just now. Everything else works very nicely except for extensions that use the Makefile `MODULEDIR` variable to install all of the share files except the control file into a particular directory, and the `directory` in the control file so that the files can be found. Here’s semver[2], which has both: ```console ❯ make PG_CONFIG=~/dev/c/postgres/pgsql-devel/bin/pg_config prefix=/Users/david/Downloads/postgresql install /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/extension' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/semver' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/doc//semver' /opt/homebrew/bin/ginstall -c -m 644 .//semver.control '/Users/david/Downloads/postgresql/share/extension/' /opt/homebrew/bin/ginstall -c -m 644 .//sql/semver--0.10.0--0.11.0.sql .//sql/semver--0.11.0--0.12.0.sql .//sql/semver--0.12.0--0.13.0.sql .//sql/semver--0.13.0--0.15.0.sql .//sql/semver--0.15.0--0.16.0.sql .//sql/semver--0.16.0--0.17.0.sql .//sql/semver--0.17.0--0.20.0.sql .//sql/semver--0.2.1--0.2.4.sql .//sql/semver--0.2.4--0.3.0.sql .//sql/semver--0.20.0--0.21.0.sql .//sql/semver--0.21.0--0.22.0.sql .//sql/semver--0.22.0--0.30.0.sql .//sql/semver--0.3.0--0.4.0.sql .//sql/semver--0.30.0--0.31.0.sql .//sql/semver--0.31.0--0.31.1.sql .//sql/semver--0.31.1--0.31.2.sql .//sql/semver--0.31.2--0.32.0.sql .//sql/semver--0.32.0--0.32.1.sql .//sql/semver--0.32.1--0.40.0.sql .//sql/semver--0.32.1.sql .//sql/semver--0.40.0.sql .//sql/semver--0.5.0--0.10.0.sql .//sql/semver--unpackaged--0.2.1.sql .//sql/semver.sql '/Users/david/Downloads/postgresql/share/semver/' /opt/homebrew/bin/ginstall -c -m 755 src/semver.dylib '/Users/david/Downloads/postgresql/lib/' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib/bitcode/src/semver' /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib/bitcode'/src/semver/src/ /opt/homebrew/bin/ginstall -c -m 644 src/semver.bc '/Users/david/Downloads/postgresql/lib/bitcode'/src/semver/src/ cd '/Users/david/Downloads/postgresql/lib/bitcode' && /opt/homebrew/Cellar/llvm/19.1.7_1/bin/llvm-lto -thinlto -thinlto-action=thinlink -o src/semver.index.bc src/semver/src/semver.bc /opt/homebrew/bin/ginstall -c -m 644 .//doc/semver.mmd '/Users/david/Downloads/postgresql/share/doc//semver/‘ ``` Following `MODULEDIR=semver`, it puts the SQL files into `share/semver/` instead of `share/extension/`, as expected, but then, even though the control file has `directory=semver`, it can’t load them: ```pgsql david=# create extension semver; ERROR: could not open directory "/Users/david/dev/c/postgres/pgsql-devel/share/semver": No such file or directory ``` Looks like it’s only looking in the `semver` subdirectory under $libdir and not the whole path. But given that the `directory` variable in the control file can be a full path, I don’t see that there’s much of a way to generalize a solution. I guess there are three options: 1. If directory is a full path, try to load the files there. It probably already works that way, though I haven’t tired it. 2. If the directory is not a full path, check for it under each directory in `extension_control_path`? But no, that points to `share/extension`, not `share`, so it can’t really searched unless it also lops off `extension` from the end of each path. 3. Drop support for MODULEDIR and directory. I think I’d opt for #3, personally, just to simplify things. Anyway, I then built envvar, a C extension with no `directory` configuration, and it worked perfectly. I will say, though, that I will kind of miss being able to run `make install` without first running `make`, as the `prefix` variable does not work with `make`. Best, David [1]: https://postgr.es/m/[email protected] [2]: https://github.com/theory/pg-semver/ [3]: https://github.com/theory/pg-envvar Attachments: [application/octet-stream] v1-0001-Flesh-out-docs-for-the-prefix-make-variable.patch (2.6K, ../../[email protected]/2-v1-0001-Flesh-out-docs-for-the-prefix-make-variable.patch) download | inline diff: From e0ffe63f621463662d13bf21e9431a78a3391349 Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" <[email protected]> Date: Wed, 19 Mar 2025 13:18:33 -0400 Subject: [PATCH v1] Flesh out docs for the `prefix` make variable The variable is a bit magical in how it requires "postgresql" or "pgsql" to be part of the path, and files end up in its `share` and `lib` subdirectories. So mention all that and show an example of setting `extension_control_path` and `dynamic_library_path` to use those locations. --- doc/src/sgml/extend.sgml | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 64f8e133cae..4e75a01fae4 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1809,10 +1809,35 @@ 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> + You can also select a separate directory prefix in which to install your + extension's files by setting the <literal>make</literal> variable + <varname>prefix</varname> when executing <literal>make install</literal> + like so: +<programlisting> +make install prefix=/etc/postgresql +</programlisting> + This will install the control SQL files into + <literal>/etc/postgresql/share</literal> and shared modules into + <literal>/etc/postgresql/lib</literal>. If the prefix does not + include the strings <literal>postgresql</literal> or + <literal>pgsql</literal>, such as: +<programlisting> +make install prefix=/etc/extras +</programlisting> + Then the <literal>postgresql</literal> directory will be appended io the + prefix, installing the control SQL files into + <literal>/etc/extras/postgresql/share</literal> and shared modules into + <literal>/etc/extras/postgresql/lib</literal>. Either way, you'll need to + set <xref linkend="guc-extension-control-path"/> and <xref + linkend="guc-dynamic-library-path"/> to allow + <productname>PostgreSQL</productname> to find the files: +</programlisting> +extension_control_path = '/etc/extras/postgresql/share/extension:$system' +dynamic_library_path = '/etc/extras/postgresql/lib:$libdir' + </programlisting> </para> <para> -- 2.48.1 [application/pgp-signature] signature.asc (833B, ../../[email protected]/4-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 18:55 Tom Lane <[email protected]> parent: Matheus Alcantara <[email protected]> 1 sibling, 2 replies; 47+ messages in thread From: Tom Lane @ 2025-03-19 18:55 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Peter Eisentraut <[email protected]> writes: > Committed that, thanks. Buildfarm member snakefly doesn't like this too much. Since no other animals have failed, I guess it must be about local conditions on that machine, but the report is pretty opaque: # +++ tap check in src/test/modules/test_extensions +++ # Failed test '$system extension is installed correctly on pg_available_extensions' # at t/001_extension_control_path.pl line 69. # got: 'f' # expected: 't' # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' # at t/001_extension_control_path.pl line 76. # got: 'f' # expected: 't' # Looks like you failed 2 tests of 5. [06:43:53] t/001_extension_control_path.pl .. Dubious, test returned 2 (wstat 512, 0x200) Failed 2/5 subtests Looking at the test, it presupposes that "amcheck" must be an available extension. I do not see anything that guarantees that that's so, though. It'd fail if contrib hasn't been installed. Is there a reason to use "amcheck" rather than something more certainly available, like "plpgsql"? regards, tom lane ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 19:25 Matheus Alcantara <[email protected]> parent: Tom Lane <[email protected]> 1 sibling, 3 replies; 47+ messages in thread From: Matheus Alcantara @ 2025-03-19 19:25 UTC (permalink / raw) To: Tom Lane <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Wed, Mar 19, 2025 at 3:56 PM Tom Lane <[email protected]> wrote: > > Peter Eisentraut <[email protected]> writes: > > Committed that, thanks. > > Buildfarm member snakefly doesn't like this too much. Since no other > animals have failed, I guess it must be about local conditions on > that machine, but the report is pretty opaque: > > # +++ tap check in src/test/modules/test_extensions +++ > > # Failed test '$system extension is installed correctly on pg_available_extensions' > # at t/001_extension_control_path.pl line 69. > # got: 'f' > # expected: 't' > > # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' > # at t/001_extension_control_path.pl line 76. > # got: 'f' > # expected: 't' > # Looks like you failed 2 tests of 5. > [06:43:53] t/001_extension_control_path.pl .. > Dubious, test returned 2 (wstat 512, 0x200) > Failed 2/5 subtests > > Looking at the test, it presupposes that "amcheck" must be an > available extension. I do not see anything that guarantees > that that's so, though. It'd fail if contrib hasn't been > installed. Is there a reason to use "amcheck" rather than > something more certainly available, like "plpgsql"? There is no specific reason to use "amcheck" instead of "plpgsql". Attached a patch with this change, sorry about that. (Not sure if we should also improve the message to make the test failure less opaque?) -- Matheus Alcantara Attachments: [application/octet-stream] v1-0001-Fix-extension-control-path-tests.patch (1.6K, ../../CAFY6G8dwT=E_SDSobVqpz+2y0otAuKFT4nOwHxQORHXjWfcJ1A@mail.gmail.com/2-v1-0001-Fix-extension-control-path-tests.patch) download | inline diff: From 4fa81f1c04df649b183e1e55053662a35109d0b6 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara <[email protected]> Date: Wed, 19 Mar 2025 16:15:43 -0300 Subject: [PATCH v1] Fix extension control path tests Change expected extension to be installed from amcheck to plpgsql since not all build farm animals has the contrib module installed. --- .../modules/test_extensions/t/001_extension_control_path.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 7160009739a..c186c1470f7 100644 --- a/src/test/modules/test_extensions/t/001_extension_control_path.pl +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -64,14 +64,14 @@ is( $ret2, # 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'" + "select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" ); 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'" + "set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" ); is($ret4, "t", "\$system extension is installed correctly on pg_available_extensions with empty extension_control_path" -- 2.39.5 (Apple Git-154) ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-19 19:42 Tom Lane <[email protected]> parent: Matheus Alcantara <[email protected]> 2 siblings, 0 replies; 47+ messages in thread From: Tom Lane @ 2025-03-19 19:42 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Matheus Alcantara <[email protected]> writes: > (Not sure if we should also improve the message to make the test failure less > opaque?) Yeah, I was wondering how to do that. The earlier tests in that script show the whole row from pg_available_extensions, not just a bool ... but that doesn't help if the problem is we don't find a row. regards, tom lane ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-20 10:01 Peter Eisentraut <[email protected]> parent: Matheus Alcantara <[email protected]> 2 siblings, 0 replies; 47+ messages in thread From: Peter Eisentraut @ 2025-03-20 10:01 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; Tom Lane <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 19.03.25 20:25, Matheus Alcantara wrote: > On Wed, Mar 19, 2025 at 3:56 PM Tom Lane <[email protected]> wrote: >> >> Peter Eisentraut <[email protected]> writes: >>> Committed that, thanks. >> >> Buildfarm member snakefly doesn't like this too much. Since no other >> animals have failed, I guess it must be about local conditions on >> that machine, but the report is pretty opaque: >> >> # +++ tap check in src/test/modules/test_extensions +++ >> >> # Failed test '$system extension is installed correctly on pg_available_extensions' >> # at t/001_extension_control_path.pl line 69. >> # got: 'f' >> # expected: 't' >> >> # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' >> # at t/001_extension_control_path.pl line 76. >> # got: 'f' >> # expected: 't' >> # Looks like you failed 2 tests of 5. >> [06:43:53] t/001_extension_control_path.pl .. >> Dubious, test returned 2 (wstat 512, 0x200) >> Failed 2/5 subtests >> >> Looking at the test, it presupposes that "amcheck" must be an >> available extension. I do not see anything that guarantees >> that that's so, though. It'd fail if contrib hasn't been >> installed. Is there a reason to use "amcheck" rather than >> something more certainly available, like "plpgsql"? > > There is no specific reason to use "amcheck" instead of "plpgsql". Attached a > patch with this change, sorry about that. Committed. I was able to reproduce the problem from scratch using: ./configure ... make # no contrib make -C src/test/modules/test_extensions check So it depended on in which order you build the various components. ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-20 14:35 Tom Lane <[email protected]> parent: Matheus Alcantara <[email protected]> 2 siblings, 0 replies; 47+ messages in thread From: Tom Lane @ 2025-03-20 14:35 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Peter Eisentraut <[email protected]> writes: > On 19.03.25 20:25, Matheus Alcantara wrote: >> On Wed, Mar 19, 2025 at 3:56 PM Tom Lane <[email protected]> wrote: >>> Buildfarm member snakefly doesn't like this too much. > I was able to reproduce the problem from scratch using: > ./configure ... > make # no contrib > make -C src/test/modules/test_extensions check > So it depended on in which order you build the various components. That makes sense, but I wonder how snakefly hit it while other BF animals did not. It's running a reasonably up-to-date BF client version and there's nothing odd-looking about its configuration. Anyway, I see snakefly is green now so that tweak did fix it. regards, tom lane ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-20 14:53 Andrew Dunstan <[email protected]> parent: Tom Lane <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Andrew Dunstan @ 2025-03-20 14:53 UTC (permalink / raw) To: Tom Lane <[email protected]>; Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 2025-03-19 We 2:55 PM, Tom Lane wrote: > Peter Eisentraut<[email protected]> writes: >> Committed that, thanks. > Buildfarm member snakefly doesn't like this too much. Since no other > animals have failed, I guess it must be about local conditions on > that machine, but the report is pretty opaque: > > # +++ tap check in src/test/modules/test_extensions +++ > > # Failed test '$system extension is installed correctly on pg_available_extensions' > # at t/001_extension_control_path.pl line 69. > # got: 'f' > # expected: 't' > > # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' > # at t/001_extension_control_path.pl line 76. > # got: 'f' > # expected: 't' > # Looks like you failed 2 tests of 5. > [06:43:53] t/001_extension_control_path.pl .. > Dubious, test returned 2 (wstat 512, 0x200) > Failed 2/5 subtests > > Looking at the test, it presupposes that "amcheck" must be an > available extension. I do not see anything that guarantees > that that's so, though. It'd fail if contrib hasn't been > installed. Is there a reason to use "amcheck" rather than > something more certainly available, like "plpgsql"? I think something else must be going on. The failure in question came after the step "install-contrib" succeeded, and the log file for that shows: make[1]: Entering directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib' /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' /usr/bin/install -c -m 755 amcheck.so '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib/amcheck.so' /usr/bin/install -c -m 644 ./amcheck.control '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' /usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql ./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-20 22:38 Andrew Dunstan <[email protected]> parent: Andrew Dunstan <[email protected]> 0 siblings, 2 replies; 47+ messages in thread From: Andrew Dunstan @ 2025-03-20 22:38 UTC (permalink / raw) To: Tom Lane <[email protected]>; Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 2025-03-20 Th 10:53 AM, Andrew Dunstan wrote: > > > On 2025-03-19 We 2:55 PM, Tom Lane wrote: >> Peter Eisentraut<[email protected]> writes: >>> Committed that, thanks. >> Buildfarm member snakefly doesn't like this too much. Since no other >> animals have failed, I guess it must be about local conditions on >> that machine, but the report is pretty opaque: >> >> # +++ tap check in src/test/modules/test_extensions +++ >> >> # Failed test '$system extension is installed correctly on pg_available_extensions' >> # at t/001_extension_control_path.pl line 69. >> # got: 'f' >> # expected: 't' >> >> # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' >> # at t/001_extension_control_path.pl line 76. >> # got: 'f' >> # expected: 't' >> # Looks like you failed 2 tests of 5. >> [06:43:53] t/001_extension_control_path.pl .. >> Dubious, test returned 2 (wstat 512, 0x200) >> Failed 2/5 subtests >> >> Looking at the test, it presupposes that "amcheck" must be an >> available extension. I do not see anything that guarantees >> that that's so, though. It'd fail if contrib hasn't been >> installed. Is there a reason to use "amcheck" rather than >> something more certainly available, like "plpgsql"? > > > > I think something else must be going on. The failure in question came > after the step "install-contrib" succeeded, and the log file for that > shows: > > > make[1]: Entering directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' > /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib' > /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' > /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' > /usr/bin/install -c -m 755 amcheck.so '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib/amcheck.so' > /usr/bin/install -c -m 644 ./amcheck.control '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' > /usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql ./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' > make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' > > > (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) > > > Nope, testing shows it's not that, so I am rather confused about what was going on. cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-21 15:52 Matheus Alcantara <[email protected]> parent: Andrew Dunstan <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-03-21 15:52 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Tom Lane <[email protected]>; Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Thu, Mar 20, 2025 at 7:38 PM Andrew Dunstan <[email protected]> wrote: >>> >>> Buildfarm member snakefly doesn't like this too much. Since no other >>> animals have failed, I guess it must be about local conditions on >>> that machine, but the report is pretty opaque: >>> >>> # +++ tap check in src/test/modules/test_extensions +++ >>> >>> # Failed test '$system extension is installed correctly on pg_available_extensions' >>> # at t/001_extension_control_path.pl line 69. >>> # got: 'f' >>> # expected: 't' >>> >>> # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' >>> # at t/001_extension_control_path.pl line 76. >>> # got: 'f' >>> # expected: 't' >>> # Looks like you failed 2 tests of 5. >>> [06:43:53] t/001_extension_control_path.pl .. >>> Dubious, test returned 2 (wstat 512, 0x200) >>> Failed 2/5 subtests >>> >>> Looking at the test, it presupposes that "amcheck" must be an >>> available extension. I do not see anything that guarantees >>> that that's so, though. It'd fail if contrib hasn't been >>> installed. Is there a reason to use "amcheck" rather than >>> something more certainly available, like "plpgsql"? >> >> I think something else must be going on. The failure in question came after the step "install-contrib" succeeded, and the log file for that shows: >> >> >> make[1]: Entering directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' >> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib' >> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' >> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' >> /usr/bin/install -c -m 755 amcheck.so '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib/amcheck.so' >> /usr/bin/install -c -m 644 ./amcheck.control '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' >> /usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql ./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' >> make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' >> >> >> (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) > > Nope, testing shows it's not that, so I am rather confused about what was going on. > I'm not sure if I'm checking on the right place [1] but it seems that the Contrib and ContribInstall is executed after Check step which causes this test failure? 'steps_completed' => [ 'SCM-checkout', 'Configure', 'Build', 'Check', 'Contrib', 'TestModules', 'Install', 'ContribInstall', 'TestModulesInstall', 'MiscCheck', ... ] [1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=snakefly&dt=2025-03-20%2009%3A46%3A05 -- Matheus Alcantara ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-21 16:38 Andrew Dunstan <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: Andrew Dunstan @ 2025-03-21 16:38 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Tom Lane <[email protected]>; Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 2025-03-21 Fr 11:52 AM, Matheus Alcantara wrote: > On Thu, Mar 20, 2025 at 7:38 PM Andrew Dunstan <[email protected]> wrote: >>>> Buildfarm member snakefly doesn't like this too much. Since no other >>>> animals have failed, I guess it must be about local conditions on >>>> that machine, but the report is pretty opaque: >>>> >>>> # +++ tap check in src/test/modules/test_extensions +++ >>>> >>>> # Failed test '$system extension is installed correctly on pg_available_extensions' >>>> # at t/001_extension_control_path.pl line 69. >>>> # got: 'f' >>>> # expected: 't' >>>> >>>> # Failed test '$system extension is installed correctly on pg_available_extensions with empty extension_control_path' >>>> # at t/001_extension_control_path.pl line 76. >>>> # got: 'f' >>>> # expected: 't' >>>> # Looks like you failed 2 tests of 5. >>>> [06:43:53] t/001_extension_control_path.pl .. >>>> Dubious, test returned 2 (wstat 512, 0x200) >>>> Failed 2/5 subtests >>>> >>>> Looking at the test, it presupposes that "amcheck" must be an >>>> available extension. I do not see anything that guarantees >>>> that that's so, though. It'd fail if contrib hasn't been >>>> installed. Is there a reason to use "amcheck" rather than >>>> something more certainly available, like "plpgsql"? >>> I think something else must be going on. The failure in question came after the step "install-contrib" succeeded, and the log file for that shows: >>> >>> >>> make[1]: Entering directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' >>> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib' >>> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' >>> /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension' >>> /usr/bin/install -c -m 755 amcheck.so '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib/amcheck.so' >>> /usr/bin/install -c -m 644 ./amcheck.control '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' >>> /usr/bin/install -c -m 644 ./amcheck--1.3--1.4.sql ./amcheck--1.2--1.3.sql ./amcheck--1.1--1.2.sql ./amcheck--1.0--1.1.sql ./amcheck--1.0.sql '/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/share/extension/' >>> make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/contrib/amcheck' >>> >>> >>> (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) >> Nope, testing shows it's not that, so I am rather confused about what was going on. >> > I'm not sure if I'm checking on the right place [1] but it seems that the > Contrib and ContribInstall is executed after Check step which causes this test > failure? > > 'steps_completed' => [ > 'SCM-checkout', > 'Configure', > 'Build', > 'Check', > 'Contrib', > 'TestModules', > 'Install', > 'ContribInstall', > 'TestModulesInstall', > 'MiscCheck', > ... > ] > > [1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=snakefly&dt=2025-03-20%2009%3A46%3A05 No. In the buildfarm, the Check step only runs the core regression tests, not any TAP tests. The above shows fairly clearly that the failure occurred after the ContribInstall step, which is what's puzzling me. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-21 16:42 Tom Lane <[email protected]> parent: Andrew Dunstan <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Tom Lane @ 2025-03-21 16:42 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Matheus Alcantara <[email protected]> writes: > On Thu, Mar 20, 2025 at 7:38 PM Andrew Dunstan <[email protected]> wrote: >>> (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) >> Nope, testing shows it's not that, so I am rather confused about what was going on. > I'm not sure if I'm checking on the right place [1] but it seems that the > Contrib and ContribInstall is executed after Check step which causes this test > failure? No, this is not failing in Check. I did just notice a clue though: on snakefly, the failing step's log [1] includes make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/src/backend' rm -rf '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log make -C '../../../..' DESTDIR='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install install >'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/install.log 2>&1 make -j1 checkprep >>'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/install.log 2>&1 PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/bin:/opt/postgres/build-farm-18/HEAD/pgsql.build/src/test/modules/test_extensions:$PATH" LD_LIBRARY_PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib:$LD_LIBRARY_PATH" INITDB_TEMPLATE='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template initdb --auth trust --no-sync --no-instructions --lc-messages=C --no-clean '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template >>'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/initdb-template.log 2>&1 echo "# +++ regress check in src/test/modules/test_extensions +++" && PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/bin:/opt/postgres/build-farm-18/HEAD/pgsql.build/src/test/modules/test_extensions:$PATH" LD_LIBRARY_PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib:$LD_LIBRARY_PATH" INITDB_TEMPLATE='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template ../../../../src/test/regress/pg_regress --temp-instance=./tmp_check --inputdir=. --bindir= --temp-config=/opt/postgres/build-farm-18/tmp/buildfarm-C9Iy3s/bfextra.conf --no-locale --port=5678 --dbname=contrib_regression test_extensions test_extdepend # +++ regress check in src/test/modules/test_extensions +++ # initializing database system by running initdb showing that the step made its own tmp_install, and that only the core "install" process was executed, so the lack of amcheck in that install tree is not surprising. But concurrent runs on other animals, eg [2], don't show a tmp_install rebuild happening. So those are using an installation tree that *does* include contrib modules. So what this comes down to is "why is snakefly doing a fresh install here?". I don't know the buildfarm client well enough to identify probable causes. I do note that Makefile.global.in conditionalizes tmp_install rebuild on several variables: temp-install: | submake-generated-headers ifndef NO_TEMP_INSTALL ifneq ($(abs_top_builddir),) ifeq ($(MAKELEVEL),0) rm -rf '$(abs_top_builddir)'/tmp_install $(MKDIR_P) '$(abs_top_builddir)'/tmp_install/log $(MAKE) -C '$(top_builddir)' DESTDIR='$(abs_top_builddir)'/tmp_install install >'$(abs_top_builddir)'/tmp_install/log/install.log 2>&1 $(MAKE) -j1 $(if $(CHECKPREP_TOP),-C $(CHECKPREP_TOP),) checkprep >>'$(abs_top_builddir)'/tmp_install/log/install.log 2>&1 I think we've had trouble before with that MAKELEVEL test... regards, tom lane [1] https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=snakefly&dt=2025-03-20%2009%3A46%3... [2] https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=alligator&dt=2025-03-19%2006%3A10%... ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-21 21:52 Matheus Alcantara <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: Matheus Alcantara @ 2025-03-21 21:52 UTC (permalink / raw) To: David E. Wheeler <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers Hi David, thanks for testing! On Wed, Mar 19, 2025 at 2:29 PM David E. Wheeler <[email protected]> wrote: > > First thing I notice is that prefix= uses the magic to insert > “postgresql” into the path if it’s not already there: > > ``` console > ❯ make PG_CONFIG=~/dev/c/postgres/pgsql-devel/bin/pg_config prefix=/Users/david/Downloads install > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/postgresql/extension' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/postgresql/extension' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/share/doc//postgresql/extension' > /opt/homebrew/bin/ginstall -c -m 644 .//pair.control '/Users/david/Downloads/share/postgresql/extension/' > /opt/homebrew/bin/ginstall -c -m 644 .//sql/pair--0.1.2.sql .//sql/pair--unpackaged--0.1.2.sql '/Users/david/Downloads/share/postgresql/extension/' > /opt/homebrew/bin/ginstall -c -m 644 .//doc/pair.md '/Users/david/Downloads/share/doc//postgresql/extension/‘ > ``` > > I think this should at least be documented, but generally feels > unexpected to me. I’ve attached a patch that fleshes out the docs, > along with an example of setting `extension_control_path` and > `dynamic_library_path` to use the locations. It might not have the > information right about the need for “postgresql” or “pgsql” in the > path. Did you miss to attach the patch? > Everything else works very nicely except for extensions that use the > Makefile `MODULEDIR` variable to install all of the share files except > the control file into a particular directory, and the `directory` in > the control file so that the files can be found. Here’s semver[2], > which has both: > > ```console > ❯ make PG_CONFIG=~/dev/c/postgres/pgsql-devel/bin/pg_config prefix=/Users/david/Downloads/postgresql install > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/extension' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/semver' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/share/doc//semver' > /opt/homebrew/bin/ginstall -c -m 644 .//semver.control '/Users/david/Downloads/postgresql/share/extension/' > /opt/homebrew/bin/ginstall -c -m 644 .//sql/semver--0.10.0--0.11.0.sql .//sql/semver--0.11.0--0.12.0.sql .//sql/semver--0.12.0--0.13.0.sql .//sql/semver--0.13.0--0.15.0.sql .//sql/semver--0.15.0--0.16.0.sql .//sql/semver--0.16.0--0.17.0.sql .//sql/semver--0.17.0--0.20.0.sql .//sql/semver--0.2.1--0.2.4.sql .//sql/semver--0.2.4--0.3.0.sql .//sql/semver--0.20.0--0.21.0.sql .//sql/semver--0.21.0--0.22.0.sql .//sql/semver--0.22.0--0.30.0.sql .//sql/semver--0.3.0--0.4.0.sql .//sql/semver--0.30.0--0.31.0.sql .//sql/semver--0.31.0--0.31.1.sql .//sql/semver--0.31.1--0.31.2.sql .//sql/semver--0.31.2--0.32.0.sql .//sql/semver--0.32.0--0.32.1.sql .//sql/semver--0.32.1--0.40.0.sql .//sql/semver--0.32.1.sql .//sql/semver--0.40.0.sql .//sql/semver--0.5.0--0.10.0.sql .//sql/semver--unpackaged--0.2.1.sql .//sql/semver.sql '/Users/david/Downloads/postgresql/share/semver/' > /opt/homebrew/bin/ginstall -c -m 755 src/semver.dylib '/Users/david/Downloads/postgresql/lib/' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib/bitcode/src/semver' > /opt/homebrew/bin/gmkdir -p '/Users/david/Downloads/postgresql/lib/bitcode'/src/semver/src/ > /opt/homebrew/bin/ginstall -c -m 644 src/semver.bc '/Users/david/Downloads/postgresql/lib/bitcode'/src/semver/src/ > cd '/Users/david/Downloads/postgresql/lib/bitcode' && /opt/homebrew/Cellar/llvm/19.1.7_1/bin/llvm-lto -thinlto -thinlto-action=thinlink -o src/semver.index.bc src/semver/src/semver.bc > /opt/homebrew/bin/ginstall -c -m 644 .//doc/semver.mmd '/Users/david/Downloads/postgresql/share/doc//semver/‘ > ``` > > Following `MODULEDIR=semver`, it puts the SQL files into > `share/semver/` instead of `share/extension/`, as expected, but then, > even though the control file has `directory=semver`, it can’t load > them: > > ```pgsql > david=# create extension semver; > ERROR: could not open directory "/Users/david/dev/c/postgres/pgsql-devel/share/semver": No such file or directory > ``` I've managed to reproduce the issue. The problem is on get_ext_ver_list which calls get_extension_script_directory that try to search for .sql files only on $sharedir. > Looks like it’s only looking in the `semver` subdirectory under > $libdir and not the whole path. > > But given that the `directory` variable in the control file can be a > full path, I don’t see that there’s much of a way to generalize a > solution. I guess there are three options: > > 1. If directory is a full path, try to load the files there. It > probably already works that way, though I haven’t tired it. > Yes, if the directory is a full path it try to load the files from there. It is implemented on get_extension_script_directory. > 2. If the directory is not a full path, check for it under each > directory in `extension_control_path`? But no, that points to > `share/extension`, not `share`, so it can’t really searched unless it > also lops off `extension` from the end of each path. Maybe we could make the "extension" part of the extension control path explicitly, like Peter has mentioned in his first patch version [1]?. If "directory" is not set we could use "extension" otherwise use the "directory" as a path suffix when searching on extension_control_path? [1] https://www.postgresql.org/message-id/0d384836-7e6e-4932-af3b-8dad1f6fee43%40eisentraut.org -- Matheus Alcantara ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-21 22:05 David E. Wheeler <[email protected]> parent: Matheus Alcantara <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: David E. Wheeler @ 2025-03-21 22:05 UTC (permalink / raw) To: Matheus Alcantara <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Mar 21, 2025, at 17:52, Matheus Alcantara <[email protected]> wrote: > Did you miss to attach the patch? No. You can see it in the archive[1]. Direct link[2]. > Maybe we could make the "extension" part of the extension control path > explicitly, like Peter has mentioned in his first patch version [1]?. > If "directory" is not set we could use "extension" otherwise use the > "directory" as a path suffix when searching on extension_control_path? So, omit “extension” from the path options, append it to search for control files, and then append the directory value (if not absolute) if it exists to look for files, and otherwise append “extensions” to find them, too. I think that makes sense. Essentially it becomes a SHAREDIR search path. Best, David [1]: https://postgr.es/m/[email protected] [2]: https://www.postgresql.org/message-id/attachment/174397/v1-0001-Flesh-out-docs-for-the-prefix-make-v... Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-03-22 13:28 Andrew Dunstan <[email protected]> parent: Tom Lane <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: Andrew Dunstan @ 2025-03-22 13:28 UTC (permalink / raw) To: Tom Lane <[email protected]>; Matheus Alcantara <[email protected]>; [email protected]; +Cc: Peter Eisentraut <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; David E. Wheeler <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On 2025-03-21 Fr 12:42 PM, Tom Lane wrote: > Matheus Alcantara<[email protected]> writes: >> On Thu, Mar 20, 2025 at 7:38 PM Andrew Dunstan<[email protected]> wrote: >>>> (wondering if this another of these cases where the "path includes postgres" thing bites us, and we're looking in the wrong place) >>> Nope, testing shows it's not that, so I am rather confused about what was going on. >> I'm not sure if I'm checking on the right place [1] but it seems that the >> Contrib and ContribInstall is executed after Check step which causes this test >> failure? > No, this is not failing in Check. > > I did just notice a clue though: on snakefly, the failing step's > log [1] includes > > make[1]: Leaving directory `/opt/postgres/build-farm-18/HEAD/pgsql.build/src/backend' > rm -rf '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install > /usr/bin/mkdir -p '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log > make -C '../../../..' DESTDIR='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install install >'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/install.log 2>&1 > make -j1 checkprep >>'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/install.log 2>&1 > PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/bin:/opt/postgres/build-farm-18/HEAD/pgsql.build/src/test/modules/test_extensions:$PATH" LD_LIBRARY_PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib:$LD_LIBRARY_PATH" INITDB_TEMPLATE='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template initdb --auth trust --no-sync --no-instructions --lc-messages=C --no-clean '/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template >>'/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/log/initdb-template.log 2>&1 > echo "# +++ regress check in src/test/modules/test_extensions +++" && PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/bin:/opt/postgres/build-farm-18/HEAD/pgsql.build/src/test/modules/test_extensions:$PATH" LD_LIBRARY_PATH="/opt/postgres/build-farm-18/HEAD/pgsql.build/tmp_install/opt/postgres/build-farm-18/HEAD/inst/lib:$LD_LIBRARY_PATH" INITDB_TEMPLATE='/opt/postgres/build-farm-18/HEAD/pgsql.build'/tmp_install/initdb-template ../../../../src/test/regress/pg_regress --temp-instance=./tmp_check --inputdir=. --bindir= --temp-config=/opt/postgres/build-farm-18/tmp/buildfarm-C9Iy3s/bfextra.conf --no-locale --port=5678 --dbname=contrib_regression test_extensions test_extdepend > # +++ regress check in src/test/modules/test_extensions +++ > # initializing database system by running initdb > > showing that the step made its own tmp_install, and that only the core > "install" process was executed, so the lack of amcheck in that install > tree is not surprising. But concurrent runs on other animals, eg [2], > don't show a tmp_install rebuild happening. So those are using an > installation tree that *does* include contrib modules. > > So what this comes down to is "why is snakefly doing a fresh install > here?". I don't know the buildfarm client well enough to identify > probable causes. I do note that Makefile.global.in conditionalizes > tmp_install rebuild on several variables: > > temp-install: | submake-generated-headers > ifndef NO_TEMP_INSTALL > ifneq ($(abs_top_builddir),) > ifeq ($(MAKELEVEL),0) > rm -rf '$(abs_top_builddir)'/tmp_install > $(MKDIR_P) '$(abs_top_builddir)'/tmp_install/log > $(MAKE) -C '$(top_builddir)' DESTDIR='$(abs_top_builddir)'/tmp_install install >'$(abs_top_builddir)'/tmp_install/log/install.log 2>&1 > $(MAKE) -j1 $(if $(CHECKPREP_TOP),-C $(CHECKPREP_TOP),) checkprep >>'$(abs_top_builddir)'/tmp_install/log/install.log 2>&1 Good catch. This is happening because the owner hasn't updated the animal to REL_19_1. In 19 and 19.1 we fixed detection of exiting installs to take account of the 'Is there postgres or pgsql in the prefix' issue. So it was looking in the wrong place. cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-04-25 19:23 David E. Wheeler <[email protected]> parent: David E. Wheeler <[email protected]> 1 sibling, 1 reply; 47+ messages in thread From: David E. Wheeler @ 2025-04-25 19:23 UTC (permalink / raw) To: Christoph Berg <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Matheus Alcantara <[email protected]>; Andrew Dunstan <[email protected]>; Gabriele Bartolini <[email protected]>; Craig Ringer <[email protected]>; Robert Haas <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers On Apr 25, 2025, at 07:33, Christoph Berg <[email protected]> wrote: > > Re: David E. Wheeler >> +<programlisting> >> +make install prefix=/etc/postgresql > > I'd use /usr/local/postgresql there. "/etc" is just wrong. Thank you for the review. Here’s v3*. Best, David * Also reviewable as a GitHub PR[1]. [1]: https://github.com/theory/postgres/pull/10 Attachments: [application/octet-stream] v3-0001-Flesh-out-docs-for-the-prefix-make-variable.patch (2.7K, ../../[email protected]/2-v3-0001-Flesh-out-docs-for-the-prefix-make-variable.patch) download | inline diff: From d49d3445ca5bdde436713dc8a2ae7707683851e3 Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" <[email protected]> Date: Fri, 25 Apr 2025 15:22:23 -0400 Subject: [PATCH v3] Flesh out docs for the `prefix` make variable The variable is a bit magical in how it requires "postgresql" or "pgsql" to be part of the path, and files end up in its `share` and `lib` subdirectories. So mention all that and show an example of setting `extension_control_path` and `dynamic_library_path` to use those locations. --- doc/src/sgml/extend.sgml | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 64f8e133cae..05063d4e7bc 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1809,10 +1809,35 @@ 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> + You can also select a separate directory prefix in which to install your + extension's files by setting the <literal>make</literal> variable + <varname>prefix</varname> when executing <literal>make install</literal> + like so: +<programlisting> +make install prefix=/usr/local/postgresql +</programlisting> + This will install the control SQL files into + <literal>/usr/local/postgresql/share</literal> and shared modules into + <literal>/usr/local/postgresql/lib</literal>. If the prefix does not + include the strings <literal>postgres</literal> or + <literal>pgsql</literal>, such as: +<programlisting> +make install prefix=/usr/local/extras +</programlisting> + Then the <literal>postgresql</literal> directory will be appended io the + prefix, installing the control and SQL files into + <literal>/usr/local/extras/postgresql/share/extension</literal> and shared + modules into <literal>/usr/local/extras/postgresql/lib</literal>. Either + way, you'll need to set <xref linkend="guc-extension-control-path"/> and + <xref linkend="guc-dynamic-library-path"/> to allow + <productname>PostgreSQL</productname> to find the files: +</programlisting> +extension_control_path = '/usr/local/extras/postgresql/share/extension:$system' +dynamic_library_path = '/usr/local/extras/postgresql/lib:$libdir' + </programlisting> </para> <para> -- 2.48.1 [application/pgp-signature] signature.asc (833B, ../../[email protected]/4-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
* Re: RFC: Additional Directory for Extensions @ 2025-05-02 16:48 David E. Wheeler <[email protected]> parent: David E. Wheeler <[email protected]> 0 siblings, 0 replies; 47+ messages in thread From: David E. Wheeler @ 2025-05-02 16:48 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Matheus Alcantara <[email protected]>; pgsql-hackers; Christoph Berg <[email protected]> On May 1, 2025, at 16:24, Peter Eisentraut <[email protected]> wrote: > I see. I have committed it now describing the current state. Quick follow-up to tweak a couple of commas. --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -1813,8 +1813,8 @@ include $(PGXS) <para> You can select a separate directory prefix in which to install your - extension's files, by setting the <command>make</command> variable - <varname>prefix</varname> when executing <literal>make install</literal> + extension's files by setting the <command>make</command> variable + <varname>prefix</varname> when executing <literal>make install</literal>, like so: <programlisting> make install prefix=/usr/local/postgresql Best, David Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 47+ messages in thread
end of thread, other threads:[~2025-05-02 16:48 UTC | newest] Thread overview: 47+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-09-12 14:35 [PATCH] Avoid touching user indexes while they are being (re)built. Arseny Sher <[email protected]> 2023-02-14 17:44 [PATCH v14 2/7] Don't proc_exit() in startup's SIGTERM handler if forked by system(). Nathan Bossart <[email protected]> 2024-08-26 14:06 Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2024-08-26 21:35 ` Re: RFC: Additional Directory for Extensions Craig Ringer <[email protected]> 2024-08-27 15:26 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2024-08-28 02:24 ` Re: RFC: Additional Directory for Extensions Craig Ringer <[email protected]> 2024-08-29 15:55 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2024-10-10 11:20 ` Re: RFC: Additional Directory for Extensions Ebru Aydin Gol <[email protected]> 2024-10-10 20:35 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2024-10-11 08:27 ` Re: RFC: Additional Directory for Extensions Ebru Aydin Gol <[email protected]> 2024-11-11 07:16 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2024-11-11 18:15 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2024-12-05 11:07 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2025-02-24 13:33 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-02-25 12:45 ` Re: RFC: Additional Directory for Extensions Andrew Dunstan <[email protected]> 2025-02-25 20:29 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-02-28 15:35 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-01 09:15 ` Re: RFC: Additional Directory for Extensions Gabriele Bartolini <[email protected]> 2025-03-03 13:39 ` Re: RFC: Additional Directory for Extensions Gabriele Bartolini <[email protected]> 2025-03-03 15:05 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-03-03 15:10 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-03-03 18:45 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-06 13:46 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2025-03-10 20:25 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-11 15:58 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2025-03-12 13:17 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-19 06:42 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2025-03-19 07:03 ` Re: RFC: Additional Directory for Extensions Gabriele Bartolini <[email protected]> 2025-03-19 14:58 ` Re: RFC: Additional Directory for Extensions Christoph Berg <[email protected]> 2025-03-19 17:29 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-03-21 21:52 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-21 22:05 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-04-25 19:23 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-05-02 16:48 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]> 2025-03-19 18:55 ` Re: RFC: Additional Directory for Extensions Tom Lane <[email protected]> 2025-03-19 19:25 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-19 19:42 ` Re: RFC: Additional Directory for Extensions Tom Lane <[email protected]> 2025-03-20 10:01 ` Re: RFC: Additional Directory for Extensions Peter Eisentraut <[email protected]> 2025-03-20 14:35 ` Re: RFC: Additional Directory for Extensions Tom Lane <[email protected]> 2025-03-20 14:53 ` Re: RFC: Additional Directory for Extensions Andrew Dunstan <[email protected]> 2025-03-20 22:38 ` Re: RFC: Additional Directory for Extensions Andrew Dunstan <[email protected]> 2025-03-21 15:52 ` Re: RFC: Additional Directory for Extensions Matheus Alcantara <[email protected]> 2025-03-21 16:38 ` Re: RFC: Additional Directory for Extensions Andrew Dunstan <[email protected]> 2025-03-21 16:42 ` Re: RFC: Additional Directory for Extensions Tom Lane <[email protected]> 2025-03-22 13:28 ` Re: RFC: Additional Directory for Extensions Andrew Dunstan <[email protected]> 2024-08-27 08:56 ` Re: RFC: Additional Directory for Extensions Gabriele Bartolini <[email protected]> 2024-08-27 15:19 ` Re: RFC: Additional Directory for Extensions David E. Wheeler <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox