How it works(27) PostGIS源码解析(A) 项目的配置与编译

1. 引入

PostGIS 项目自然无需作过多介绍,但作为一种被 SQL 语言包装到 PostgreSQL 后面的工具集,它究竟是如何工作的对于普通用户来说其实是一个黑盒。

作为一个 20 年的老开源项目,它也像 Geoserver 一般长期没有激进的功能变革了,但与后者在老框架上缝缝补补不同,PostGIS 的基底——PostgreSQL——还是有其生命力和开发潜力的。因此,有必要打开这个黑盒子看一看,以便在必要的时刻为其赋予有价值的新功能。

本文的 PostGIS 为 3.5.0dev ,编译运行环境为 Ubuntu 22.04(aarch64)

在项目根目录下的 README.postgis 文件中,我们可以看见对不同子目录的职能描述:

子目录 职能
deps 第三方依赖库
doc 生成文档相关的工具和脚本
extensions 与 PostGIS 扩展相关的文件,包括各种扩展的控制文件( .control 文件)和 SQL 脚本
extras 若干非核心脚本和工具
liblwgeom 用于处理轻量几何对象(Light Weight Geometry)的库文件
fuzzers 用于 liblwgeom 的模糊测试(Google fuzz)的工具和脚本等
libpgcommon 用于连接 PostgreSQL 和 lwgeom 对象的公共库
loader Esri Shape 数据输入(shp2pg)输出(pg2shp)工具
macros Autoconf 辅助宏,包含一些来自 gettext 项目的通用宏和 PostGIS 专用宏
postgis PostGIS 核心插件的源码
raster PostGIS-raster 插件的源码
sfcgal PostGIS-sfcgal 插件的源码
topology PostGIS-topology 插件的源码
regress 回归测试相关的脚本和工具
utils 一些工具脚本

想要分析 PostGIS 的核心原理,只需要关注如下目录的内容:

  • extension
  • liblwgeom
  • postgis
  • utils

除非明确指出,后文将不再讨论包括文档(doc),测试(regress,CUnit,fuzzers)、导入导出工具(loader)等非核心功能目录中的内容。其他的非核心扩展(如 raster 扩展)将单独讨论。

2. 项目配置(configure)阶段

要编译 PostGIS 项目首先需要运行根目录下的 configure 脚本对项目进行配置。

2.1 configure 脚本生成

PostGIS 是个经历了长期岁月的老项目,遵循着诞生时的技术路线,它使用的是现在已经不那么流行的 Autotools工具链以及 Perl 脚本。

当使用来自存档文件( tarball )的 PostGIS 源码时, configure 脚本已经存在,无需手动生成该文件。使用来自其他源头的源码编译时,根据 READDME.postgis 文档的说明,需要运行 autogen.sh 脚本生成它。

当你的系统中正确安装了 Autotools 工具链后, autogen.sh 脚本会依次执行如下命令(省略了非核心步骤):

  1. libtoolize --force --copy:Autotools 工具链一系列初始化操作。
  2. aclocal -I macro:根据根目录下 macro 目录中全部.m4 文件的内容,生成 aclocal.m4 文件。
  3. autoconf:根据 configure.ac 模板文件(该文件是 Autoconf 工具约定的默认文件名)生成 configure 脚本,此过程将自动加载 aclocal.m4 文件。

执行 configure 脚本后就可以完成 PostGIS 项目对于当前系统环境的检查与配置,若检查无误,配置完成后即可执行 make 命令进行编译。

configure 脚本是由 Autoconf 工具经由模板展开生成的,可读性较低。因此,我们从原始模板 configure.ac 文件窥探脚本的大致结构,了解 PostGIS 的编译逻辑。

2.2 configure.ac 模板简析

我们可以在 PostGIS 项目的多处发现以 .in 后缀结尾的模板文件(例如 Makefile.in )。在这些模板文件中,有若干以 @变量名@ 形式编写的占位符(例如 @POSTGIS_MAJOR_VERSION@ )。

使用 Autoconf 工具的 AC_CONFIG_FILES 命令,即可将模板中的占位符用同名变量的值替换掉,最终生成它们去掉 .in 后缀的目标文件。这就是 Autoconf 模板文件的核心功能:为不同的占位符变量赋值,生成满足当前系统环境的依赖文件

configure.ac 模板生成 configure 脚本的过程,就是统一设置各种变量并最终将这些变量填充到指定子模板中并生成对应文件的流程。

简单来说,在 configure.ac 模板中,主要涉及以下类别的参数设置:

  1. 与当前项目相关的参数:
    • 项目版本
    • 文件路径
  2. 检测并设置(初始化)与编译器(C/C++)相关的参数
  3. 检测并设置与依赖库相关的参数:
    • PostgreSQL
    • 输出格式库:
      • libxml2(读写 KML/GML)
      • json-c(读写 Geojson)
      • protobuf-c(输出 MVT 等格式)
    • 空间操作库:
      • GEOS
      • GDAL
      • SFCGAL
      • PROJ

最终,configure.ac 模板会根据上述参数替换下列模板文件中的占位符,生成若干 Makefile 文件(省略了非核心内容和第三方库):

  1. 核心构建脚本:
    • GNUmakefile
  2. 几何数据类型库:
    • liblwgeom/Makefile
  3. 连接辅助库
    • libpgcommon/Makefile
  4. 扩展依赖文件:
    • extensions/Makefile
    • extensions/postgis/Makefile
    • extensions/postgis_topology/Makefile
    • extensions/postgis_tiger_geocoder/Makefile
    • extensions/address_standardizer/Makefile
    • extensions/postgis_raster/Makefile
    • extensions/postgis_sfcgal/Makefile
  5. PostGIS 核心库:
    • postgis/Makefile
    • sfcgal/Makefile
    • raster/Makefile
    • raster/rt_core/Makefile
    • raster/rt_pg/Makefile
    • topology/Makefile

当成功执行 configure 脚本后,便可在根目录执行 make 命令,按照核心构建脚本 GNUmakefile 描述的内容按顺序构建编译。

3. 项目编译(make)与安装(install)阶段

GNUmakefile的核心代码如下:

# 声明所有要编译的子目录,每个子目录都是一个子项目
SUBDIRS = liblwgeom raster
SUBDIRS+= deps libpgcommon postgis topology sfcgal utils extensions

for s in $(SUBDIRS); do \
    $(MAKE) -C $${s} all || exit 1; \
done;

因为每个子目录下的 Makefile 脚本都正确生成,执行 make 命令后整个项目将递归的编译全部子项目,即执行每个子目录下 Makefile 脚本的 all 目标(target),若 all 目标不存在则运行第一个目标。

我们以最核心的 PostGIS 扩展及其依赖为例(liblwgeom、libpgcommon、extensions/postgis、postgis),分析这些子项目都是如何编译的,最终编译出哪些文件以及这些文件如何安装到数据库指定目录。

3.1 liblwgeom

liblwgeom 项目是核心的几何库,不依赖于 PostgreSQL。

项目的 all 目标递归执行了如下的编译流程:

  1. all 目标依赖 liblwgeom.la 目标,liblwgeom.la 目标依赖 $(LT_OBJS) 目标:

    # 列出需要编译的文件
    SA_OBJS = \
       stringbuffer.o \
       ... 省略
       lwgeom_sfcgal.o
       varint.o
    NM_OBJS = \
       lwspheroid.o
    
    # 定义libtools的目标对象(与obj对象同名的lo文件)
    LT_SA_OBJS = $(SA_OBJS:.o=.lo)
    LT_NM_OBJS = $(NM_OBJS:.o=.lo)
    LT_OBJS = $(LT_SA_OBJS) $(LT_NM_OBJS)
    
    all: liblwgeom.la
    liblwgeom.la: $(LT_OBJS)
       $(LIBTOOL) --tag=CC --mode=link $(CC) -rpath $(libdir) $(LT_OBJS) $(RYU_LIBPATH)\
                -release $(SOVER) -version-info 0:0:0 $(LDFLAGS) -static -o $@
    
  2. $(LT_OBJS) 目标依赖 ../postgis_config.h../postgis_revision.h 以及$(SA_HEADERS)变量定义的所有文件的存在:

    # 定义依赖的头文件
    SA_HEADERS = \
       bytebuffer.h \
       ... 省略
       varint.h
    
    # 生成精确提交版本
    ../postgis_revision.h:
       $(MAKE) -C .. postgis_revision.h
    
    # postgis_config.h 定义了其他版本信息
    # 当 postgis_config.h 或 postgis_revision.h 发生变化时将重新执行 $(LT_OBJS) 目标
    $(LT_OBJS): ../postgis_config.h ../postgis_revision.h $(SA_HEADERS)
    
  3. 执行$(LT_OBJS) 目标时会触发 $(LT_SA_OBJS) 目标和 $(LT_NM_OBJS) 目标,将通过 libtool 工具生成一系列 .o 文件和对应的 .lo 文件。

    RYU_INCLUDE = -I$(srcdir)/../deps/ryu/..
    
    srcdir = .
    builddir = .
    
    # 定义预处理标志,添加依赖的第三方库的头文件路径
    CPPFLAGS =  $(RYU_INCLUDE) -I/usr/include/libxml2 -I/usr/include -I/usr/include/json-c  -DNDEBUG  -I$(builddir) -I$(srcdir)
    # 定义编译器标志
    CFLAGS = -std=gnu99 -g -O2 -fno-math-errno -fno-signed-zeros -Wall -O2  -fPIC -DPIC
    
    # 从 .c 文件生成 .lo 文件
    $(LT_SA_OBJS): %.lo: %.c
       $(LIBTOOL) --mode=compile $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
    $(LT_NM_OBJS): %.lo: %.c
       $(LIBTOOL) --mode=compile $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
    
  4. $(LT_OBJS)目标满足需求时,开始执行 liblwgeom.la 目标:

    prefix = /usr/local
    exec_prefix = ${prefix}
    libdir = ${exec_prefix}/lib
    
    # 共享对象版本号,一般为POSTGIS_MAJOR_VERSION.POSTGIS_MINOR_VERSION
    SOVER = 3.5
    
    # 定义c编译器
    CC = gcc
    
    # 定义链接器标志位,指定需要链接的库(默认从 LD_LIBRARY_PATH 路径搜索需要链接的库)
    LDFLAGS =  -lm -lgeos_c -lproj -ljson-c -L/usr/lib/aarch64-linux-gnu -lSFCGAL -lgmpxx -no-undefined
    
    RYU_LIBPATH = ../deps/ryu/libryu.la
    $(RYU_LIBPATH): ../deps/ryu/d2s.c
       $(MAKE) -C ../deps/ryu libryu.la
    
    SHELL = /bin/bash
    top_builddir = ..
    LIBTOOL = $(SHELL) $(top_builddir)/libtool
    
    liblwgeom.la: $(LT_OBJS)
       $(LIBTOOL) --tag=CC --mode=link $(CC) -rpath $(libdir) $(LT_OBJS) $(RYU_LIBPATH)\
                -release $(SOVER) -version-info 0:0:0 $(LDFLAGS) -static -o $@
    
  5. 最终在 liblwgeom 目录编译生成了 liblwgeom.la 文件,在 liblwgeom/.libs 下编译生成了 liblwgeom.a 文件。最终编译后的结果形如:

    |-- .libs
    |   |...省略
    |   |-- effectivearea.o
    |   |...省略
    |   |-- liblwgeom.a
    |   |-- liblwgeom.la -> ../liblwgeom.la
    |   |...省略
    ...省略
    |-- effectivearea.c
    |-- effectivearea.h
    |-- effectivearea.lo
    |-- effectivearea.o
    ...省略
    |-- liblwgeom.h
    |-- liblwgeom.h.in
    |-- liblwgeom.la
    ...省略
    

    libtool 工具会在 liblwgeom/.libs 目录和 liblwgeom 目录下同时生成一系列同名的 .o 文件,分别为PIC对象文件和非 PIC 对象文件。

最终生成的是其他库依赖的静态库,不需要安装到 PostgreSQL 中,没有安装步骤。

3.2 libpgcommon

libpgcommon 项目是与 PostgreSQL 交互的工具库。

项目的 all 目标递归执行了如下的编译流程:

  1. all 目标执行 libpgcommon.a 目标,libpgcommon.a 目标依赖 $(SA_OBJS) 目标和$(SA_HEADERS)目标。

    # 定义依赖的头文件
    SA_HEADERS = \
       lwgeom_pg.h \
       ... 省略
       pgsql_compat.h
    
    # 列出需要编译的文件
    SA_OBJS = \
       gserialized_gist.o \
       ... 省略
       shared_gserialized.o
    
    all: libpgcommon.a
    libpgcommon.a: $(SA_OBJS) $(SA_HEADERS)
       gcc-ar rs libpgcommon.a $(SA_OBJS)
    
  2. $(SA_OBJS) 目标依赖postgis_config.h文件(若该文件发生变化,会触发重新执行 $(SA_OBJS) 目标),同时该目标也执行了编译目标,将编译出指定的 .o 文件(猜测因为libpgcommon库直接和 PostgreSQL 打交道,不需要链接其他的库,因此没有必要使用 libtool 管理编译逻辑)。

    srcdir = .
    top_builddir = ..
    
    CC=gcc
    CFLAGS= -I$(srcdir)/../liblwgeom -I$(top_builddir)/liblwgeom -I/usr/include/libxml2 -I/usr/include -I/usr/include/json-c  -DNDEBUG  -std=gnu99 -g -O2 -fno-math-errno -fno-signed-zeros -Wall -O2 -I/usr/include/postgresql/14/server   -fPIC -DPIC
    
    $(SA_OBJS): ../postgis_config.h
    
    # 从 .c 文件生成 .o 文件
    $(SA_OBJS): %.o: %.c
       $(CC) $(CFLAGS) -c -o $@ $<
    
  3. $(SA_OBJS) 目标达成且$(SA_HEADERS)依赖也满足,则会触发libpgcommon.a文件的编译,最终生成libpgcommon.a文件。

最终生成的是其他库依赖的静态库,不需要安装到 PostgreSQL 中,没有安装步骤。

3.3 extensions/postgis

extensions/postgis 项目会生成一系列扩展所依赖的脚本文件(.sql / .control)。因为涉及到部分与 PostgreSQL 直接交互的场景,因此使用了PGXS机制。

下文中的 PGXS 包含pgxs.mk文件及其引用的Makefile.shlibMakefile.global等文件。

编译阶段

项目的all目标递归执行了如下的流程(忽略了处理 unpackaged 模式的逻辑):

  1. all 目标依赖 sql/$(EXTENSION)--$(EXTVERSION).sqlsql/$(EXTENSION)--ANY--$(EXTVERSION).sql 两个子目标:

    EXTVERSION    = 3.5.0dev
    EXTENSION     = postgis
    
    all: sql/$(EXTENSION)--$(EXTVERSION).sql sql/$(EXTENSION)--ANY--$(EXTVERSION).sql
    
  2. sql/$(EXTENSION)--$(EXTVERSION).sql 目标依赖 sql 文件夹以及几个 sql 文件的存在:

    EXTENSION_SCRIPTS = \
       sql/postgis_for_extension.sql \
       sql/spatial_ref_sys_config_dump.sql \
       sql/spatial_ref_sys.sql
    
    # 按顺序把这些文件合并到 postgis--3.5.0dev.sql 脚本中
    # 最终生成的脚本用于全新安装当前版本的 postgis 扩展
    sql/$(EXTENSION)--$(EXTVERSION).sql: $(EXTENSION_SCRIPTS) | sql
       printf '\\echo Use "CREATE EXTENSION $(EXTENSION)" to load this file. \\quit\n' > $@
       cat $^ >> $@
    
  3. 执行 sql/$(EXTENSION)--$(EXTVERSION).sql 目标:

    # 创建 sql 目录
    sql:
       mkdir -p $@
    
    # 生成 postgis_for_extension.sq l脚本
    # 因为 postgis.sql.in 模板比较简单,因此使用c预处理器(c preprocessor)作为模板处理器,替换了里面的 include 和宏定义
    SQLPP = /usr/bin/cpp -traditional-cpp -w -P -Upixel -Ubool
    sql/postgis_for_extension.sql: ../../postgis/postgis.sql.in ../../postgis_revision.h | sql
       $(SQLPP) -I./../../postgis $< > $@.tmp
       grep -v '^#' $@.tmp | \
       $(PERL) -lpe \
          "s'MODULE_PATHNAME'\$(MODULEPATH)'g" \
          | $(PERL) -pe 's/BEGIN\;//g ; s/COMMIT\;//g' > $@
       rm -f $@.tmp
    
    # 移除开头结尾的BEGIN/COMMIT
    sql/spatial_ref_sys.sql: ../../spatial_ref_sys.sql | sql
       $(PERL) -pe 's/BEGIN\;//g ; s/COMMIT\;//g' $< > $@
    
    sql/spatial_ref_sys_config_dump.sql: ../../spatial_ref_sys.sql ../../utils/create_spatial_ref_sys_config_dump.pl | sql
       $(PERL) ../../utils/create_spatial_ref_sys_config_dump.pl $< > $@
    
  4. sql/$(EXTENSION)--ANY--$(EXTVERSION).sql 目标依赖 sql 文件夹以及几个 sql 文件的存在:

    EXTENSION_UPGRADE_SCRIPTS = \
       ../postgis_extension_helper.sql \
       sql/postgis_upgrade.sql \
       ../postgis_extension_helper_uninstall.sql
    
    # 把这些文件按顺序合并到 postgis--ANY--3.5.0dev.sql 文件中
    # 最终生成脚本用于从任意先前版本升级到当前版本的 postgis 扩展
    sql/$(EXTENSION)--ANY--$(EXTVERSION).sql: $(EXTENSION_UPGRADE_SCRIPTS) | sql
       printf '\\echo Use "CREATE EXTENSION $(EXTENSION)" to load this file. \\quit\n' > $@
       cat $^ >> $@
    
  5. 执行sql/$(EXTENSION)--ANY--$(EXTVERSION).sql 目标:

    sql/postgis_upgrade_for_extension.sql: ../../postgis/common_before_upgrade.sql ../../postgis/postgis_before_upgrade.sql sql/postgis_upgrade_for_extension.sql.in ../../postgis/postgis_after_upgrade.sql ../../postgis/common_after_upgrade.sql | sql
       cat $^ | $(PERL) -pe 's/BEGIN\;//g ; s/COMMIT\;//g' > $@
    
    # 执行perl脚本生成指定的SQL脚本
    sql/postgis_upgrade_for_extension.sql.in: sql/postgis_for_extension.sql ../../utils/create_upgrade.pl | sql
       $(PERL) ../../utils/create_upgrade.pl $< > $@
    
    sql/postgis_upgrade.sql: sql/postgis_upgrade_for_extension.sql | sql
       $(PERL) -pe "s/BEGIN\;//g ; s/COMMIT\;//g; s/^(DROP .*)\;/SELECT postgis_extension_drop_if_exists('$(EXTENSION)', '\1');\n\1\;/" $< > $@
    
  6. sql/$(EXTENSION)--$(EXTVERSION).sqlsql/$(EXTENSION)--ANY--$(EXTVERSION).sql 目标完成后即完成 all 目标。

  7. 由于引用了 upgrade-paths-rules.mk

    include ./../upgrade-paths-rules.mk
    

    需要执行 upgrade-paths-rules.mkall 目标:

    POSTGIS_BUILD_DATE=$(shell date $${SOURCE_DATE_EPOCH:+-d @$$SOURCE_DATE_EPOCH} -u "+%Y-%m-%d %H:%M:%S")
    TAG_UPGRADE=$(EXTENSION)--TEMPLATED--TO--ANY.sql
    
    # 生成 postgis--TEMPLATED--TO--ANY.sql
    all: sql/$(TAG_UPGRADE)
    
    # 该文件内容没有意义,或许是因为不需要处理版本间的差异
    sql/$(TAG_UPGRADE): $(MAKEFILE_LIST) | sql
       echo '-- Just tag extension $(EXTENSION) version as "ANY"' > $@
       echo '-- Installed by $(EXTENSION) $(EXTVERSION)' >> $@
       echo '-- Built on $(POSTGIS_BUILD_DATE)' >> $@
    
  8. 最终在 sql 目录下生成如下文件:

    postgis--3.5.0dev.sql
    postgis--ANY--3.5.0dev.sql
    postgis--TEMPLATED--TO--ANY.sql
    postgis_for_extension.sql
    postgis_upgrade.sql
    postgis_upgrade_for_extension.sql
    postgis_upgrade_for_extension.sql.in
    spatial_ref_sys.sql
    spatial_ref_sys_config_dump.sql
    
  9. 通过引用的方式,引入了 PGXS 机制:

    PG_CONFIG := /usr/bin/pg_config
    PGXS := /usr/lib/postgresql/14/lib/pgxs/src/makefiles/pgxs.mk
    include $(PGXS)
    PERL = /usr/bin/perl
    

    PGXS 的 all 目标会生成 postgis.control 文件:

    all: $(DATA_built)
    
    # 最终要被复制到PostgreSQL指定目录下的文件
    DATA_built = \
       $(EXTENSION).control \
       sql/$(EXTENSION)--$(EXTVERSION).sql \
       sql/$(EXTENSION)--ANY--$(EXTVERSION).sql \
       $(NULL)
    
    MODULEPATH = $$libdir/$(EXTENSION)-3
    
    # 因为模板很简单,使用sed即可替换模板中的变量最终生成 postgis.control 文件
    $(EXTENSION).control: $(EXTENSION).control.in Makefile
       cat $< \
          | sed -e 's|@EXTVERSION@|$(EXTVERSION)|g' \
          | sed -e 's|@EXTENSION@|$(EXTENSION)|g' \
          | sed -e 's|@MODULEPATH@|$(MODULEPATH)|g' \
          > $@
    

安装阶段

extensions/postgis 项目的 install 目标分为两部分:

  1. 源自 upgrade-paths-rules.mk 的更新文件安装:

    1. 复制 sql 目录下的 postgis--3.5.0dev--ANY.sqlpostgis--TEMPLATED--TO--ANY.sql/usr/share/postgresql/14/extension/ 目录下:

      INSTALL_DATA_MODE = 644
      INSTALL = /usr/bin/install -c
      INSTALL_DATA  = $(INSTALL) -m $(INSTALL_DATA_MODE)
      datadir := /usr/share/postgresql/14
      datamoduledir := extension
      
      EXTDIR=$(DESTDIR)$(datadir)/$(datamoduledir)
      
      install: install-upgrade-paths
      install-upgrade-paths: sql/$(TAG_UPGRADE) sql/$(EXTENSION)--ANY--$(EXTVERSION).sql
         mkdir -p "$(EXTDIR)"
         $(INSTALL_DATA) "sql/$(EXTENSION)--ANY--$(EXTVERSION).sql" "$(EXTDIR)/$(EXTENSION)--ANY--$(EXTVERSION).sql"
         $(INSTALL_DATA) "sql/$(TAG_UPGRADE)" "$(EXTDIR)/$(TAG_UPGRADE)"
         ln -fs "$(TAG_UPGRADE)" "$(EXTDIR)/$(EXTENSION)--$(EXTVERSION)--ANY.sql"
      
    2. GNUmakefile 调用 install-extension-upgrades-from-known-versions目标:

      @if test x"$@" = xinstall; then \
         if test x"yes" = xyes; then \
            $(MAKE) install-extension-upgrades-from-known-versions; \
         endif
      endif
      
    3. 执行install-extension-upgrades-from-known-versions目标,在 /usr/share/postgresql/14/extension/ 目录下生成一系列软链接形式的 postgis 升级脚本:

      PGRADEABLE_VERSIONS = \
         2.0.0 \
         2.0.1 \
         ...省略..
         3.5.0alpha2
      
      # 以下被注释的代码为历史遗留的无用代码
      # 详见:https://github.com/postgis/postgis/commit/593c7af97ea74fbe5e288af2030ab3047958c4c1
      # 已被独立的upgradeable_versions.mk脚本替代
      
      # GREP = /usr/bin/grep
      # MICRO_NUMBER  = $(shell echo $(EXTVERSION) | \
      #                         $(PERL) -pe 's/\d.\d.(\d+)[a-zA-Z]*\d*/$1/'
      # PREREL_NUMBER = $(shell echo $(EXTVERSION) | \
      #                         $(PERL) -pe 's/\d\.\d\.(.*)/\1/' | \
      #                         $(GREP) "[a-zA-Z]" | \
      #                         $(PERL) -pe 's/\d+[a-zA-Z]+(\d+)/\1/'
      # MICRO_PREV    = $(shell if test "$(MICRO_NUMBER)x" != "x"; then expr $(MICRO_NUMBER) - 1; fi)
      # PREREL_PREV   = $(shell if test "$(PREREL_NUMBER)x" != "x"; then expr $(PREREL_NUMBER) - 1; fi)
      # PREREL_PREFIX = $(shell echo $(EXTVERSION) | \
      #                         $(PERL) -pe 's/\d\.\d\.(.*)/\1/' | \
      #                         $(GREP) "[a-zA-Z]" | \
      #                         $(PERL) -pe 's/(\d+[a-zA-Z]+)\d*/\1/'
      
      # postgis.pl 脚本通过软链接的方式生成一系列升级文件
      # 例如:postgis--2.0.0--ANY.sql (从 2.0 升级到当前版本) 实际指向 postgis--TEMPLATED--TO--ANY.sql
      install-extension-upgrades-from-known-versions:
         $(PERL) $(top_srcdir)/loader/postgis.pl \
            install-extension-upgrades \
            --extension $(EXTENSION) \
            --pg_sharedir $(DESTDIR)$(PG_SHAREDIR) \
            $(UPGRADEABLE_VERSIONS)
      
  2. 源自 PGXS 的其他 SQL 文件安装:

    1. PGXS 的 install 目标依赖 installdirs 目标:

      datamoduledir := extension
      install: installdirs
         $(INSTALL_DATA) $(addprefix $(srcdir)/, $(addsuffix .control, $(EXTENSION))) '$(DESTDIR)$(datadir)/extension/'
         $(INSTALL_DATA) $(addprefix $(srcdir)/, $(DATA)) $(DATA_built) '$(DESTDIR)$(datadir)/$(datamoduledir)/'
      
    2. installdirs 目标保证指定的安装目录存在:

      MKDIR_P = /bin/mkdir -p
      
      installdirs:
         $(MKDIR_P) '$(DESTDIR)$(datadir)/extension'
         $(MKDIR_P) '$(DESTDIR)$(datadir)/$(datamoduledir)'
      
    3. 当实现 installdirs 目标后,将复制 DATA_built 变量定义的文件到 /usr/share/postgresql/14/extension/ 目录下,包括:

      • sql/postgis--ANY--3.5.0dev.sql
      • sql/postgis--TEMPLATED--TO--ANY.sql
      • postgis.control

3.4 postgis

该项目是 PostGIS 扩展最核心的部件,因此也深度的使用了 PGXS 机制。

编译阶段

项目的 Makefile 中没有显式定义 all 目标,因此我们需要在 PGXS 中找到真正的 all 目标作为入口。

在 PGXS 中存在多个 all 目标,忽略处理旧函数(legacy)的逻辑,可视为三大部分:

# 1. 生成 sql 文件
SQL_built=postgis.sql uninstall_postgis.sql postgis_upgrade.sql
DATA_built=$(SQL_built)
all: $(DATA_built)

# 2. 生成 LLVM-BC 文件
PG_OBJS= \
   postgis_module.o \
   ...省略...
   postgis_legacy.o
OBJS=$(PG_OBJS)
# 我所使用的环境支持llvm,因此会编译出BC文件
# 依赖与OBJS变量中定义的文件同名的.BC文件
all: $(patsubst %.o,%.bc, $(OBJS))

# 3. 生成最终的库文件
MODULE_big=postgis-3
NAME = $(MODULE_big)
include $(top_srcdir)/src/Makefile.shlib
all: all-lib
  1. 首先看 sql 文件的生成。生成时主要使用了通配符匹配所有 sql 对应的模板文件,对于不使用模板的部分文件,也指定了专门的生成方法:

    # 使用C预处理器处理所有的SQL模板,生成指定的文件
    %.sql: %.sql.in
       $(SQLPP) -I../libpgcommon -I. $< > $@.tmp
       grep -v '^#' $@.tmp | $(PERL) -lpe "s'MODULE_PATHNAME'\$(MODULEPATH)'g;s'@extschema@\.''g" > $@
       rm -f $@.tmp
    
    # 按顺序将指定的sql文件合并到 postgis_upgrade.sql 中,并补全为可独立执行的sql脚本
    postgis_upgrade.sql: common_before_upgrade.sql postgis_before_upgrade.sql postgis_upgrade.sql.in postgis_after_upgrade.sql common_after_upgrade.sql
       echo "BEGIN;" > $@
       cat $^ >> $@
       echo "COMMIT;" >> $@
    
    # postgis核心脚本,可以让用户手动安装postgis
    # 当 postgis.sql 依赖的这些文件发生变化后会重新生成
    postgis.sql: sqldefines.h geography.sql.in postgis_brin.sql.in postgis_spgist.sql.in postgis_letters.sql
    
    uninstall_postgis.sql: postgis.sql ../utils/create_uninstall.pl
    $(PERL) ../utils/create_uninstall.pl $< $(POSTGIS_PGSQL_VERSION) > $@
    
  2. 再看 LLVM-BC 对象的生成。使用 CLang 编译器从 c 文件编译出 bc 文件,最终生成与全部.o 文件一一对应的.bc 文件:

    CLANG = /usr/bin/clang-14
    BITCODE_CFLAGS =  -fno-strict-aliasing -fwrapv
    
    COMPILE.c.bc = $(CLANG) -Wno-ignored-attributes $(BITCODE_CFLAGS) $(CPPFLAGS) -flto=thin -emit-llvm -c
    
    # 从同名的C文件编译出对应的BC文件
    %.bc: %.c
       $(COMPILE.c.bc) -o $@ $<
    
  3. 库文件的生成是最核心的部分,all 目标调用了 all-lib 子目标,all-lib 子目标又调用了 all-shared-lib 子目标,all-shared-lib 子目标依赖于 $(OBJS) 目标,因此优先执行 $(OBJS) 目标,即编译生成所有 .o 文件:

    all-lib: all-shared-lib
    
    # 库的名字为 postgis-3.so
    shlib    = $(NAME)$(DLSUFFIX)
    
    all-shared-lib: $(shlib)
    $(shlib): $(OBJS)
       $(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(SHLIB_LINK)
    
    DEPDIR = .deps
    # 定义编译器
    CC = gcc
    # 定义各种编译标志位
    CFLAGS = -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -moutline-atomics -g -g -O2 -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat -Werror=format-security
    CPPFLAGS = -Wdate-time -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE -I/usr/include/libxml2
    COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) -c
    
    # 将 OBJS 变量中定义的每个 .o 文件编译出来
    %.o: %.c
       @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi
       $(COMPILE.c) -o $@ $< -MMD -MP -MF $(DEPDIR)/$(*F).Po
    
  4. 当编译成功所有 OBJS 变量中定义的 .o 文件后,可以开始最终编译 postgis-3.so 文件:

    LDFLAGS =  -lm
    COMPILER = $(CC) $(CFLAGS)
    LINK.shared = $(COMPILER) -shared
    WAGYU_LIBPATH = ../deps/wagyu/libwagyu.la
    # 这里的拼写是个历史遗留笔误
    WAYGU_LIB = $(WAGYU_LIBPATH) -lstdc++
    FLATGEOBUF_LIBPATH = ../deps/flatgeobuf/libflatgeobuf.la
    FLATGEOBUF_LIB = $(FLATGEOBUF_LIBPATH) -lstdc++
    SHLIB_LINK_F = $(WAYGU_LIB) $(FLATGEOBUF_LIB) ../libpgcommon/libpgcommon.a ../liblwgeom/.libs/liblwgeom.a  -lgeos_c -lproj -ljson-c -lprotobuf-c -lxml2 -L/usr/lib/aarch64-linux-gnu -lSFCGAL -lgmpxx -Wl,--exclude-libs,ALL  -lm
    SHLIB_LINK := $(SHLIB_LINK_F)
    
    DLSUFFIX = .so
    
    $(shlib): $(OBJS)
       $(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(SHLIB_LINK)
    
  5. 最终编译/生成如下文件:

    # sql 文件
    postgis.sql
    uninstall_postgis.sql
    postgis_upgrade.sql
    
    # bc 文件
    brin_2d.bc
    ...省略...
    vector_tile.pb-c.bc
    
    # o 文件
    brin_2d.o
    ...省略...
    vector_tile.pb-c.o
    
    # so 文件
    postgis-3.so
    

安装阶段

postgis 项目的没有显式定义install目标,因此安装阶段的逻辑来自于 PGXS 机制:

  1. 安装脚本文件和 BC 文件 :

    MODULEDIR=contrib/postgis-3.5
    datamoduledir := $(MODULEDIR)
    DATA=../spatial_ref_sys.sql
    
    install: all installdirs
       # 定义于 DATA_built 和 DATA 变量中的 SQL 文件将被复制到 /usr/share/postgresql/14/contrib/postgis-3.5/ 目录下
       $(INSTALL_DATA) $(addprefix $(srcdir)/, $(DATA)) $(DATA_built) '$(DESTDIR)$(datadir)/$(datamoduledir)/'
    
       # 将所有 BC 文件安装到 /usr/lib/postgresql/14/lib/bitcode/postgis-3 目录下
       $(call install_llvm_module,$(MODULE_big),$(OBJS))
    
    
  2. 安装共享库文件:

    INSTALL_SHLIB    = $(INSTALL) $(INSTALL_SHLIB_OPTS)
    INSTALL_SHLIB_OPTS = -m 755
    pkglibdir = /usr/lib/postgresql/14/lib
    
    install: install-lib
    install-lib: install-lib-shared
    # 将 postgis-3.so 安装到 /usr/lib/postgresql/14/lib 目录下
    install-lib-shared: $(shlib) installdirs-lib
       $(INSTALL_SHLIB) $< '$(DESTDIR)$(pkglibdir)/$(shlib)'
    
    installdirs-lib:
       $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'
    

4. 总结

通过上述步骤,我们大致了解了 PostGIS 的编译、安装逻辑,此时 PostGIS 库及其依赖文件已经被安装到 PostgreSQL 目录中,但 PostGIS 还并未处于可用状态。下一节我们将讨论通过执行 SQL 语句创建 PostGIS 扩展的内部逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容