shithub: freetype+ttf2subf

Download patch

ref: 66978a58874169c6a4f2f984a7461e2901901784
parent: ab6a21b73314ef302299b47b1707f5b2d03826cf
author: David Turner <david.turner.dev@gmail.com>
date: Sun May 17 14:45:41 EDT 2020

Add Meson build project file.

Example usage:

  # Configure Meson build in directory `build-meson` to generate
  # release binaries comparable to to the ones from the
  # autotools/make build system.
  meson setup build-meson \
        --prefix=/usr/local \
        --buildtype=debugoptimized \
        --strip \
        -Db_ndebug=true

  # After configuring the Meson build with the above command,
  # compile and install to `/usr/local/`; this includes a pkg-config
  # file.
  ninja -C build-meson install

  # Alternatively, compile and install to `/tmp/aa/usr/local/...`
  # for packaging.
  DESTDIR=/tmp/aa ninja -C build-meson install

  # Generate documentation under `build-meson/docs`.
  ninja -C build-meson docs

Library size comparison for stripped `libfreetype.so` generated by
all three build systems:

  - Default build (autotools + libtool): 712 KiB
  - CMake build (RelWithDebInfo):        712 KiB
  - Meson build:                         712 KiB

* meson.build: New top-level Meson build file for the library.

* meson_options.txt: New file.  It holds user-selectable options for
the build, which can be printed with `meson configure`, and selected
at `meson setup` or `meson --reconfigure` time with
`-D<option>=<value>`.

* scripts/parse_modules_cfg.py: A script invoked by `meson.build` to
parse `modules.cfg` and extract important information out of it
(i.e., the list of modules).

* scripts/process_ftoption_h.py: New script invoked by `meson.build`
to process the original `ftoption.h` file.  It enables or disables
configuration macro variables based on the available dependencies.
This is similar to what other build systems are using (i.e., Meson's
`configure_file()` command is not used here).

* scripts/extract_freetype_version.py: New script invoked by
`meson.build` to extract the FreeType version number from
`<freetype/freetype.h>`.

* scripts/extract_libtool_version.py: New script invoked by
`meson.build` to extract the libtool `revision_info` data from
`builds/unix/configure.raw`, and to generate the corresponding
shared library suffix.

* scripts/generate_reference_docs.py: New script invoked by
`meson.build` to generate the FreeType 2 reference documentation
(using the `docwriter` and `mkdocs` packages, which must be already
installed).

git/fs: mount .git/fs: mount/attach disallowed
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,69 @@
+2020-09-21  David Turner  <david@freetype.org>
+
+	Add Meson build project file.
+
+	Example usage:
+
+	  # Configure Meson build in directory `build-meson` to generate
+	  # release binaries comparable to to the ones from the
+	  # autotools/make build system.
+	  meson setup build-meson \
+	        --prefix=/usr/local \
+	        --buildtype=debugoptimized \
+	        --strip \
+	        -Db_ndebug=true
+
+	  # After configuring the Meson build with the above command,
+	  # compile and install to `/usr/local/`; this includes a pkg-config
+	  # file.
+	  ninja -C build-meson install
+
+	  # Alternatively, compile and install to `/tmp/aa/usr/local/...`
+	  # for packaging.
+	  DESTDIR=/tmp/aa ninja -C build-meson install
+
+	  # Generate documentation under `build-meson/docs`.
+	  ninja -C build-meson docs
+
+	Library size comparison for stripped `libfreetype.so` generated by
+	all three build systems:
+
+	  - Default build (autotools + libtool): 712 KiB
+	  - CMake build (RelWithDebInfo):        712 KiB
+	  - Meson build:                         712 KiB
+
+
+	* meson.build: New top-level Meson build file for the library.
+
+	* meson_options.txt: New file.  It holds user-selectable options for
+	the build, which can be printed with `meson configure`, and selected
+	at `meson setup` or `meson --reconfigure` time with
+	`-D<option>=<value>`.
+
+	* scripts/parse_modules_cfg.py: A script invoked by `meson.build` to
+	parse `modules.cfg` and extract important information out of it
+	(i.e., the list of modules).
+
+	* scripts/process_ftoption_h.py: New script invoked by `meson.build`
+	to process the original `ftoption.h` file.  It enables or disables
+	configuration macro variables based on the available dependencies.
+	This is similar to what other build systems are using (i.e., Meson's
+	`configure_file()` command is not used here).
+
+	* scripts/extract_freetype_version.py: New script invoked by
+	`meson.build` to extract the FreeType version number from
+	`<freetype/freetype.h>`.
+
+	* scripts/extract_libtool_version.py: New script invoked by
+	`meson.build` to extract the libtool `revision_info` data from
+	`builds/unix/configure.raw`, and to generate the corresponding
+	shared library suffix.
+
+	* scripts/generate_reference_docs.py: New script invoked by
+	`meson.build` to generate the FreeType 2 reference documentation
+	(using the `docwriter` and `mkdocs` packages, which must be already
+	installed).
+
 2020-09-11  Alexei Podtelezhnikov  <apodtele@gmail.com>
 
 	[raster] Improve the second pass (#58373).
--- a/builds/unix/ftsystem.c
+++ b/builds/unix/ftsystem.c
@@ -18,7 +18,7 @@
 
 #include <ft2build.h>
   /* we use our special ftconfig.h file, not the standard one */
-#include <ftconfig.h>
+#include FT_CONFIG_CONFIG_H
 #include <freetype/internal/ftdebug.h>
 #include <freetype/ftsystem.h>
 #include <freetype/fterrors.h>
--- /dev/null
+++ b/meson.build
@@ -1,0 +1,368 @@
+#
+# Meson project file for FreeType 2
+#
+
+# Copyright (C) 2020 by
+# David Turner, Robert Wilhelm, and Werner Lemberg.
+#
+# This file is part of the FreeType project, and may only be used, modified,
+# and distributed under the terms of the FreeType project license,
+# LICENSE.TXT.  By continuing to use, modify, or distribute this file you
+# indicate that you have read the license and understand and accept it
+# fully.
+
+
+project('freetype2', 'c',
+  meson_version: '>= 0.55.0',
+  default_options: ['default_library=both'],
+)
+
+#
+# Rules to compile the FreeType 2 library itself
+#
+
+
+# Apparently meson doesn't provide a read_file() function, so instead
+# running an external command is required.
+
+python = import('python')
+python_exe = python.find_installation(required: true)
+
+ft2_version = run_command(python_exe,
+  files('scripts/extract_freetype_version.py'),
+  files('include/freetype/freetype.h')).stdout().strip()
+
+ft2_libtool_version = run_command(python_exe,
+  files('scripts/extract_libtool_version.py'),
+  '--soversion',
+  files('builds/unix/configure.raw')).stdout().strip()
+
+ft2_includes = include_directories('include')
+
+
+# Generate a custom `ftmodule.h` version based on the content of
+# `modules.cfg`.
+
+ftmodule_h = custom_target('ftmodule.h',
+  output: 'ftmodule.h',
+  input: 'modules.cfg',
+  command: [python_exe, files('scripts/parse_modules_cfg.py'),
+            '--format=ftmodule.h', '@INPUT@', '--output', '@OUTPUT@'],
+  install: true,
+  install_dir: 'include/freetype2/freetype/config',
+)
+ft2_sources = [ftmodule_h]
+
+
+# FreeType 2 modules.
+
+ft_main_modules = run_command(python_exe,
+  files('scripts/parse_modules_cfg.py'),
+  '--format=main-modules',
+  files('modules.cfg')).stdout().strip().split()
+
+ft2_sources += files([
+  'src/base/ftbase.c',
+  'src/base/ftinit.c',
+])
+
+foreach mod: ft_main_modules
+  source = mod
+  if mod == 'winfonts'
+    source = 'winfnt'
+  elif mod == 'cid'
+    source = 'type1cid'
+  endif
+  ft2_sources += 'src/@0@/@1@.c'.format(mod, source)
+endforeach
+
+# NOTE: The `gzip` and `bzip2` aux modules are handled through options.
+ft_aux_modules = run_command(python_exe,
+  files('scripts/parse_modules_cfg.py'),
+  '--format=aux-modules',
+  files('modules.cfg')).stdout().strip().split()
+
+foreach auxmod: ft_aux_modules
+  source = auxmod
+  # Most sources are named `src/<module>/<module>.c`, but there are a few
+  # exceptions handled here.
+  if auxmod == 'cache'
+    source = 'ftcache'
+  elif auxmod == 'lzw'
+    source = 'ftlzw'
+  elif auxmod == 'gzip' or auxmod == 'bzip2'
+    # Handled through options instead, see below.
+    continue
+  endif
+  ft2_sources += 'src/@0@/@1@.c'.format(auxmod, source)
+endforeach
+
+
+# FreeType 2 base extensions.
+# Normally configured through `modules.cfg`.
+
+base_extensions = run_command(python_exe,
+  files('scripts/parse_modules_cfg.py'),
+  '--format=base-extensions-list',
+  files('modules.cfg')).stdout().split()
+
+foreach ext: base_extensions
+  ft2_sources += files('src/base/' + ext)
+endforeach
+
+
+# Header files.
+
+ft2_public_headers = files([
+  'include/freetype/freetype.h',
+  'include/freetype/ftadvanc.h',
+  'include/freetype/ftbbox.h',
+  'include/freetype/ftbdf.h',
+  'include/freetype/ftbitmap.h',
+  'include/freetype/ftbzip2.h',
+  'include/freetype/ftcache.h',
+  'include/freetype/ftchapters.h',
+  'include/freetype/ftcolor.h',
+  'include/freetype/ftdriver.h',
+  'include/freetype/fterrdef.h',
+  'include/freetype/fterrors.h',
+  'include/freetype/ftfntfmt.h',
+  'include/freetype/ftgasp.h',
+  'include/freetype/ftglyph.h',
+  'include/freetype/ftgxval.h',
+  'include/freetype/ftgzip.h',
+  'include/freetype/ftimage.h',
+  'include/freetype/ftincrem.h',
+  'include/freetype/ftlcdfil.h',
+  'include/freetype/ftlist.h',
+  'include/freetype/ftlzw.h',
+  'include/freetype/ftmac.h',
+  'include/freetype/ftmm.h',
+  'include/freetype/ftmodapi.h',
+  'include/freetype/ftmoderr.h',
+  'include/freetype/ftotval.h',
+  'include/freetype/ftoutln.h',
+  'include/freetype/ftparams.h',
+  'include/freetype/ftpfr.h',
+  'include/freetype/ftrender.h',
+  'include/freetype/ftsizes.h',
+  'include/freetype/ftsnames.h',
+  'include/freetype/ftstroke.h',
+  'include/freetype/ftsynth.h',
+  'include/freetype/ftsystem.h',
+  'include/freetype/fttrigon.h',
+  'include/freetype/fttypes.h',
+  'include/freetype/ftwinfnt.h',
+  'include/freetype/t1tables.h',
+  'include/freetype/ttnameid.h',
+  'include/freetype/tttables.h',
+  'include/freetype/tttags.h',
+])
+
+ft2_config_headers = files([
+  'include/freetype/config/ftconfig.h',
+  'include/freetype/config/ftheader.h',
+  'include/freetype/config/ftstdlib.h',
+  'include/freetype/config/integer-types.h',
+  'include/freetype/config/mac-support.h',
+  'include/freetype/config/public-macros.h',
+])
+
+ft2_defines = []
+
+
+# System support file.
+
+cc = meson.get_compiler('c')
+
+# NOTE: msys2 on Windows has `unistd.h` and `fcntl.h` but not `sys/mman.h`!
+has_unistd_h = cc.has_header('unistd.h')
+has_fcntl_h = cc.has_header('fcntl.h')
+has_sys_mman_h = cc.has_header('sys/mman.h')
+
+if has_unistd_h
+  ft2_defines += ['-DHAVE_UNISTD_H=1']
+endif
+if has_fcntl_h
+  ft2_defines += ['-DHAVE_FCNTL_H']
+endif
+
+mmap_option = get_option('mmap')
+if mmap_option.auto()
+  use_mmap = has_unistd_h and has_fcntl_h and has_sys_mman_h
+else
+  use_mmap = mmap_option.enabled()
+endif
+if use_mmap
+  # This version of ftsystem.c uses mmap() to read input font files.
+  ft2_sources += files(['builds/unix/ftsystem.c',])
+else
+  ft2_sources += files(['src/base/ftsystem.c',])
+endif
+
+
+# Debug support file
+#
+# NOTE: Some specialized versions exist for other platforms not supported by
+# Meson.  Most implementation differences are extremely minor, i.e., in the
+# implementation of FT_Message() and FT_Panic(), and getting the `FT2_DEBUG`
+# value from the environment, when this is supported.  A smaller refactor
+# might make these platform-specific files much smaller, and could be moved
+# into `ftsystem.c` as well.
+#
+if host_machine.system() == 'windows'
+  ft2_debug_src = 'builds/windows/ftdebug.c'
+else
+  ft2_debug_src = 'src/base/ftdebug.c'
+endif
+ft2_sources += files([ft2_debug_src])
+
+ft2_deps = []
+
+
+# Generate `ftoption.h` based on available dependencies.
+
+ftoption_command = [python_exe,
+  files('scripts/process_ftoption_h.py'),
+  '@INPUT@', '--output=@OUTPUT@']
+
+# GZip support
+zlib_option = get_option('zlib')
+if zlib_option == 'disabled'
+  ftoption_command += ['--disable=FT_CONFIG_OPTION_USE_ZLIB']
+else
+  ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_ZLIB']
+  if zlib_option == 'builtin'
+    ftoption_command += ['--disable=FT_CONFIG_OPTION_SYSTEM_ZLIB']
+  else
+    # Probe for the system version.
+    zlib_system = dependency('zlib', required: zlib_option == 'system')
+    ft2_deps += [zlib_system]
+    ftoption_command += ['--enable=FT_CONFIG_OPTION_SYSTEM_ZLIB']
+  endif
+  ft2_sources += files(['src/gzip/ftgzip.c',])
+endif
+
+# BZip2 support
+#
+# IMPORTANT NOTE: Without `static: false` here, Meson will find both the
+# static library version and the shared library version when they are
+# installed on the system, and will try to link them *both* to the final
+# library!
+bzip2_dep = meson.get_compiler('c').find_library('bz2',
+              static: false,
+              required: get_option('bzip2'))
+if bzip2_dep.found()
+  ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_BZIP2']
+  ft2_sources += files(['src/bzip2/ftbzip2.c',])
+  ft2_deps += [bzip2_dep]
+endif
+
+# PNG support
+libpng_dep = dependency('libpng', required: get_option('png'))
+ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_PNG']
+ft2_deps += [libpng_dep]
+
+# Harfbuzz support
+harfbuzz_dep = dependency('harfbuzz',
+                 version: '>= 1.8.0',
+                 required: get_option('harfbuzz'))
+ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_HARFBUZZ']
+ft2_deps += [harfbuzz_dep]
+
+# Brotli decompression support
+brotli_dep = dependency('libbrotlidec', required: get_option('brotli'))
+ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_BROTLI']
+ft2_deps += [brotli_dep]
+
+# We can now generate `ftoption.h`.
+ftoption_h = custom_target('ftoption.h',
+  input: 'include/freetype/config/ftoption.h',
+  output: 'ftoption.h',
+  command: ftoption_command,
+  install: true,
+  install_dir: 'include/freetype2/freetype/config',
+)
+ft2_sources += ftoption_h
+
+
+# QUESTION: What if the compiler doesn't support `-D` but uses `/D` instead
+# as on Windows?
+#
+# Other build systems have something like c_defines to list defines in a
+# more portable way.  For now assume the compiler supports `-D` (hint: Visual
+# Studio does).
+ft2_defines += ['-DFT2_BUILD_LIBRARY=1']
+
+
+# Ensure that the `ftoption.h` file generated above will be used to build
+# FreeType.  Unfortunately, and very surprisingly, configure_file() does not
+# support putting the output file in a sub-directory, so we have to override
+# the default which is `<freetype/config/ftoption.h>`.
+#
+# It would be cleaner to generate the file directly into
+# `${MESON_BUILD_DIR}/freetype/config/ftoption.h`.  See
+# 'https://github.com/mesonbuild/meson/issues/2320' for details.
+ft2_defines += ['-DFT_CONFIG_OPTIONS_H=<ftoption.h>']
+
+ft2_c_args = ft2_defines
+if cc.has_function_attribute('visibility:hidden')
+  ft2_c_args += ['-fvisibility=hidden']
+endif
+
+ft2_lib = library('freetype',
+  sources: ft2_sources + [ftmodule_h],
+  c_args: ft2_c_args,
+  include_directories: ft2_includes,
+  dependencies: ft2_deps,
+  install: true,
+  version: ft2_libtool_version,
+)
+
+
+# To be used by other projects including this one through subproject().
+freetype2_dep = declare_dependency(
+  include_directories: ft2_includes,
+  link_with: ft2_lib,
+  version: ft2_libtool_version)
+
+
+# NOTE: Using both `install_dir` and `subdir` doesn't seem to work below,
+# i.e., the subdir value seems to be ignored, contrary to examples in the
+# Meson documentation.
+install_headers('include/ft2build.h',
+   install_dir: 'include/freetype2')
+install_headers(ft2_public_headers,
+  install_dir: 'include/freetype2/freetype')
+install_headers(ft2_config_headers,
+  install_dir: 'include/freetype2/freetype/config')
+
+
+# TODO(david): Declare_dependency() for using this in a Meson subproject
+#
+pkgconfig = import('pkgconfig')
+pkgconfig.generate(ft2_lib,
+  filebase: 'freetype2',
+  name: 'FreeType 2',
+  description: 'A free, high-quality, and portable font engine.',
+  url: 'https://freetype.org',
+  subdirs: 'freetype2',
+  version: ft2_libtool_version,
+)
+
+
+# NOTE: Unlike the old `make refdoc` command, this generates the
+# documentation under `$BUILD/docs/` since Meson doesn't support modifying
+# the source root directory (which is a good thing).
+gen_docs = custom_target('freetype2 reference documentation',
+  output: 'docs',
+  input: ft2_public_headers + ft2_config_headers,
+  command: [python_exe,
+    files('scripts/generate_reference_docs.py'),
+    '--version=' + ft2_version,
+    '--input-dir=' + meson.source_root(),
+    '--output-dir=@OUTPUT@'
+  ],
+)
+
+# EOF
--- /dev/null
+++ b/meson_options.txt
@@ -1,0 +1,47 @@
+#
+# meson_options.txt
+#
+
+# Copyright (C) 2020 by
+# David Turner, Robert Wilhelm, and Werner Lemberg.
+#
+# This file is part of the FreeType project, and may only be used, modified,
+# and distributed under the terms of the FreeType project license,
+# LICENSE.TXT.  By continuing to use, modify, or distribute this file you
+# indicate that you have read the license and understand and accept it
+# fully.
+
+
+option('zlib',
+  type: 'combo',
+  choices: ['disabled', 'auto', 'builtin', 'system'],
+  value: 'auto',
+  description: 'Support reading gzip-compressed font files.')
+
+option('bzip2',
+  type: 'feature',
+  value: 'auto',
+  description: 'Support reading bzip2-compressed font files.')
+
+option('png',
+  type: 'feature',
+  value: 'auto',
+  description: 'Support color bitmap glyph formats in the PNG format.'
+               + 'Requires libpng.')
+
+option('harfbuzz',
+  type: 'feature',
+  value: 'auto',
+  description: 'Use Harfbuzz library to improve auto-hinting.'
+               + ' If available, many glyphs not directly addressable'
+               + ' by a font\'s character map will be hinted also.')
+
+option('brotli',
+  type: 'feature',
+  value: 'auto',
+  description: 'Use Brotli library to support decompressing WOFF2 fonts.')
+
+option('mmap',
+  type: 'feature',
+  value: 'auto',
+  description: 'Use mmap() to open font files for faster parsing.')
--- /dev/null
+++ b/scripts/extract_freetype_version.py
@@ -1,0 +1,107 @@
+#!/usr/bin/env python
+"""Extract the FreeType version numbers from `<freetype/freetype.h>`.
+
+This script parses the header to extract the version number defined there. 
+By default, the full dotted version number is printed, but `--major`,
+`--minor` or `--patch` can be used to only print one of these values
+instead.
+"""
+
+from __future__ import print_function
+
+import argparse
+import os
+import re
+import sys
+
+# Expected input:
+#
+#  ...
+#  #define FREETYPE_MAJOR  2
+#  #define FREETYPE_MINOR  10
+#  #define FREETYPE_PATCH  2
+#  ...
+
+RE_MAJOR = re.compile(r"^ #define \s+ FREETYPE_MAJOR \s+ (.*) $", re.X)
+RE_MINOR = re.compile(r"^ #define \s+ FREETYPE_MINOR \s+ (.*) $", re.X)
+RE_PATCH = re.compile(r"^ #define \s+ FREETYPE_PATCH \s+ (.*) $", re.X)
+
+
+def parse_freetype_header(header):
+    major = None
+    minor = None
+    patch = None
+
+    for line in header.splitlines():
+        line = line.rstrip()
+        m = RE_MAJOR.match(line)
+        if m:
+            assert major == None, "FREETYPE_MAJOR appears more than once!"
+            major = m.group(1)
+            continue
+
+        m = RE_MINOR.match(line)
+        if m:
+            assert minor == None, "FREETYPE_MINOR appears more than once!"
+            minor = m.group(1)
+            continue
+
+        m = RE_PATCH.match(line)
+        if m:
+            assert patch == None, "FREETYPE_PATCH appears more than once!"
+            patch = m.group(1)
+            continue
+
+    assert (
+        major and minor and patch
+    ), "This header is missing one of FREETYPE_MAJOR, FREETYPE_MINOR or FREETYPE_PATCH!"
+
+    return (major, minor, patch)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument(
+        "--major",
+        action="store_true",
+        help="Only print the major version number.",
+    )
+    group.add_argument(
+        "--minor",
+        action="store_true",
+        help="Only print the minor version number.",
+    )
+    group.add_argument(
+        "--patch",
+        action="store_true",
+        help="Only print the patch version number.",
+    )
+
+    parser.add_argument(
+        "input",
+        metavar="FREETYPE_H",
+        help="The input freetype.h header to parse.",
+    )
+
+    args = parser.parse_args()
+    with open(args.input) as f:
+        header = f.read()
+
+    version = parse_freetype_header(header)
+
+    if args.major:
+        print(version[0])
+    elif args.minor:
+        print(version[1])
+    elif args.patch:
+        print(version[2])
+    else:
+        print("%s.%s.%s" % version)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
--- /dev/null
+++ b/scripts/extract_libtool_version.py
@@ -1,0 +1,105 @@
+#!/usr/bin/env python
+"""Extract the libtool version from `configure.raw`.
+
+This script parses the `configure.raw` file to extract the libtool version
+number.  By default, the full dotted version number is printed, but
+`--major`, `--minor` or `--patch` can be used to only print one of these
+values instead.
+"""
+
+from __future__ import print_function
+
+import argparse
+import os
+import re
+import sys
+
+# Expected input:
+#
+#  ...
+#  version_info='23:2:17'
+#  ...
+
+RE_VERSION_INFO = re.compile(r"^version_info='(\d+):(\d+):(\d+)'")
+
+
+def parse_configure_raw(header):
+    major = None
+    minor = None
+    patch = None
+
+    for line in header.splitlines():
+        line = line.rstrip()
+        m = RE_VERSION_INFO.match(line)
+        if m:
+            assert major == None, "version_info appears more than once!"
+            major = m.group(1)
+            minor = m.group(2)
+            patch = m.group(3)
+            continue
+
+    assert (
+        major and minor and patch
+    ), "This input file is missing a version_info definition!"
+
+    return (major, minor, patch)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument(
+        "--major",
+        action="store_true",
+        help="Only print the major version number.",
+    )
+    group.add_argument(
+        "--minor",
+        action="store_true",
+        help="Only print the minor version number.",
+    )
+    group.add_argument(
+        "--patch",
+        action="store_true",
+        help="Only print the patch version number.",
+    )
+    group.add_argument(
+        "--soversion",
+        action="store_true",
+        help="Only print the libtool library suffix.",
+    )
+
+    parser.add_argument(
+        "input",
+        metavar="CONFIGURE_RAW",
+        help="The input configure.raw file to parse.",
+    )
+
+    args = parser.parse_args()
+    with open(args.input) as f:
+        raw_file = f.read()
+
+    version = parse_configure_raw(raw_file)
+
+    if args.major:
+        print(version[0])
+    elif args.minor:
+        print(version[1])
+    elif args.patch:
+        print(version[2])
+    elif args.soversion:
+        # Convert libtool version_info to the library suffix.
+        # (current,revision, age) -> (current - age, age, revision)
+        print(
+            "%d.%s.%s"
+            % (int(version[0]) - int(version[2]), version[2], version[1])
+        )
+    else:
+        print("%s.%s.%s" % version)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
--- /dev/null
+++ b/scripts/generate_reference_docs.py
@@ -1,0 +1,79 @@
+#!/usr/bin/env python
+"""Generate FreeType reference documentation."""
+
+from __future__ import print_function
+
+import argparse
+import glob
+import os
+import subprocess
+import sys
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        "--input-dir",
+        required=True,
+        help="Top-level FreeType source directory.",
+    )
+
+    parser.add_argument(
+        "--version", required=True, help='FreeType version (e.g. "2.x.y").'
+    )
+
+    parser.add_argument(
+        "--output-dir", required=True, help="Output directory."
+    )
+
+    args = parser.parse_args()
+
+    # Get the list of input files of interest.
+    include_dir = os.path.join(args.input_dir, "include")
+    include_config_dir = os.path.join(include_dir, "config")
+    include_cache_dir = os.path.join(include_dir, "cache")
+
+    all_headers = (
+        glob.glob(os.path.join(args.input_dir, "include", "freetype", "*.h"))
+        + glob.glob(
+            os.path.join(
+                args.input_dir, "include", "freetype", "config", "*.h"
+            )
+        )
+        + glob.glob(
+            os.path.join(
+                args.input_dir, "include", "freetype", "cache", "*.h"
+            )
+        )
+    )
+
+    if not os.path.exists(args.output_dir):
+        os.makedirs(args.output_dir)
+    else:
+        assert os.path.isdir(args.output_dir), (
+            "Not a directory: " + args.output_dir
+        )
+
+    cmds = [
+        sys.executable,
+        "-m",
+        "docwriter",
+        "--prefix=ft2",
+        "--title=FreeType-" + args.version,
+        "--site=reference",
+        "--output=" + args.output_dir,
+    ] + all_headers
+
+    print("Running docwriter...")
+    subprocess.check_call(cmds)
+
+    print("Building static site...")
+    subprocess.check_call(
+        [sys.executable, "-m", "mkdocs", "build"], cwd=args.output_dir
+    )
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
--- /dev/null
+++ b/scripts/parse_modules_cfg.py
@@ -1,0 +1,160 @@
+#!/usr/bin/env python
+"""Parse modules.cfg and dump its output either as ftmodule.h or a list of
+base extensions.
+"""
+
+from __future__ import print_function
+
+import argparse
+import os
+import re
+import sys
+
+# Expected input:
+#
+#  ...
+#  FONT_MODULES += <name>
+#  HINTING_MODULES += <name>
+#  RASTER_MODULES += <name>
+#  AUX_MODULES += <name>
+#  BASE_EXTENSIONS += <name>
+#  ...
+
+
+def parse_modules_cfg(input_file):
+
+    lists = {
+        "FONT_MODULES": [],
+        "HINTING_MODULES": [],
+        "RASTER_MODULES": [],
+        "AUX_MODULES": [],
+        "BASE_EXTENSIONS": [],
+    }
+
+    for line in input_file.splitlines():
+        line = line.rstrip()
+        # Ignore empty lines and those that start with a comment.
+        if not line or line[0] == "#":
+            continue
+
+        items = line.split()
+        assert len(items) == 3 and items[1] == "+=", (
+            "Unexpected input line [%s]" % line
+        )
+        assert items[0] in lists, (
+            "Unexpected configuration variable name " + items[0]
+        )
+
+        lists[items[0]].append(items[2])
+
+    return lists
+
+
+def generate_ftmodule(lists):
+    result = "/* This is a generated file. */\n"
+    for driver in lists["FONT_MODULES"]:
+        if driver == "sfnt":  # Special case for the sfnt 'driver'.
+            result += "FT_USE_MODULE( FT_Module_Class, sfnt_module_class )\n"
+            continue
+
+        name = {
+            "truetype": "tt",
+            "type1": "t1",
+            "cid": "t1cid",
+            "type42": "t42",
+            "winfonts": "winfnt",
+        }.get(driver, driver)
+        result += (
+            "FT_USE_MODULE( FT_Driver_ClassRec, %s_driver_class )\n" % name
+        )
+
+    for module in lists["HINTING_MODULES"]:
+        result += (
+            "FT_USE_MODULE( FT_Module_Class, %s_module_class )\n" % module
+        )
+
+    for module in lists["RASTER_MODULES"]:
+        name = {
+            "raster": "ft_raster1",
+            "smooth": "ft_smooth",
+        }.get(module)
+        result += (
+            "FT_USE_MODULE( FT_Renderer_Class, %s_renderer_class )\n" % name
+        )
+
+    for module in lists["AUX_MODULES"]:
+        if module in ("psaux", "psnames", "otvalid", "gxvalid"):
+            result += (
+                "FT_USE_MODULE( FT_Module_Class, %s_module_class )\n" % module
+            )
+
+    result += "/* EOF */\n"
+    return result
+
+
+def generate_main_modules(lists):
+    return "\n".join(
+        lists["FONT_MODULES"]
+        + lists["HINTING_MODULES"]
+        + lists["RASTER_MODULES"]
+    )
+
+
+def generate_aux_modules(lists):
+    return "\n".join(lists["AUX_MODULES"])
+
+
+def generate_base_extensions(lists):
+    return "\n".join(lists["BASE_EXTENSIONS"])
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        "--format",
+        required=True,
+        choices=(
+            "ftmodule.h",
+            "main-modules",
+            "aux-modules",
+            "base-extensions-list",
+        ),
+        help="Select output format.",
+    )
+
+    parser.add_argument(
+        "input",
+        metavar="CONFIGURE_RAW",
+        help="The input configure.raw file to parse.",
+    )
+
+    parser.add_argument("--output", help="Output file (default is stdout).")
+
+    args = parser.parse_args()
+    with open(args.input) as f:
+        input_data = f.read()
+
+    lists = parse_modules_cfg(input_data)
+
+    if args.format == "ftmodule.h":
+        result = generate_ftmodule(lists)
+    elif args.format == "main-modules":
+        result = generate_main_modules(lists)
+    elif args.format == "aux-modules":
+        result = generate_aux_modules(lists)
+    elif args.format == "base-extensions-list":
+        result = generate_base_extensions(lists)
+    else:
+        assert False, "Invalid output format!"
+
+    if args.output:
+        with open(args.output, "w") as f:
+            f.write(result)
+    else:
+        print(result)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
--- /dev/null
+++ b/scripts/process_ftoption_h.py
@@ -1,0 +1,105 @@
+#!/usr/bin/python
+"""Toggle settings in `ftoption.h` file based on command-line arguments.
+
+This script takes an `ftoption.h` file as input and rewrites
+`#define`/`#undef` lines in it based on `--enable=CONFIG_VARNAME` or
+`--disable=CONFIG_VARNAME` arguments passed to it, where `CONFIG_VARNAME` is
+configuration variable name, such as `FT_CONFIG_OPTION_USE_LZW`, that may
+appear in the file.
+
+Note that if one of `CONFIG_VARNAME` is not found in the input file, this
+script exits with an error message listing the missing variable names.
+"""
+
+import argparse
+import os
+import re
+import sys
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        "input", metavar="FTOPTION_H", help="Path to input ftoption.h file."
+    )
+
+    parser.add_argument("--output", help="Output to file instead of stdout.")
+
+    parser.add_argument(
+        "--enable",
+        action="append",
+        default=[],
+        help="Enable a given build option (e.g. FT_CONFIG_OPTION_USE_LZW).",
+    )
+
+    parser.add_argument(
+        "--disable",
+        action="append",
+        default=[],
+        help="Disable a given build option.",
+    )
+
+    args = parser.parse_args()
+
+    common_options = set(args.enable) & set(args.disable)
+    if common_options:
+        parser.error(
+            "Options cannot be both enabled and disabled: %s"
+            % sorted(common_options)
+        )
+        return 1
+
+    with open(args.input) as f:
+        input_file = f.read()
+
+    options_seen = set()
+
+    new_lines = []
+    for line in input_file.splitlines():
+        # Expected formats:
+        #   #define <CONFIG_VAR>
+        #   /* #define <CONFIG_VAR> */
+        #   #undef <CONFIG_VAR>
+        line = line.rstrip()
+        if line.startswith("/* #define ") and line.endswith(" */"):
+            option_name = line[11:-3].strip()
+            option_enabled = False
+        elif line.startswith("#define "):
+            option_name = line[8:].strip()
+            option_enabled = True
+        elif line.startswith("#undef "):
+            option_name = line[7:].strip()
+            option_enabled = False
+        else:
+            new_lines.append(line)
+            continue
+
+        options_seen.add(option_name)
+        if option_enabled and option_name in args.disable:
+            line = "#undef " + option_name
+        elif not option_enabled and option_name in args.enable:
+            line = "#define " + option_name
+        new_lines.append(line)
+
+    result = "\n".join(new_lines)
+
+    # Sanity check that all command-line options were actually processed.
+    cmdline_options = set(args.enable) | set(args.disable)
+    assert cmdline_options.issubset(
+        options_seen
+    ), "Could not find options in input file: " + ", ".join(
+        sorted(cmdline_options - options_seen)
+    )
+
+    if args.output:
+        with open(args.output, "w") as f:
+            f.write(result)
+    else:
+        print(result)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())