美文网首页GIS后端
How it works(27) PostGIS源码解析(A)

How it works(27) PostGIS源码解析(A)

作者: 默而识之者 | 来源:发表于2024-08-08 17:04 被阅读0次

    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 扩展的内部逻辑。

    相关文章

      网友评论

        本文标题:How it works(27) PostGIS源码解析(A)

        本文链接:https://www.haomeiwen.com/subject/zdrckjtx.html