Hello

Here's my latest version of the Meson build support files for FreeType 2.
It works!
Compared to the previous versions, there are several improvements:

- The generated binaries are now the same size as the ones from the GNU
Make and CMake builds.

- The installed shared libraries have the proper libtool-compatible suffix,
and the generated pkg-config file is now using the correct FreeType
version, paths and other settings.

- The "docs" target will generate the documentation properly, except that
this will go into $BUILD_DIR/docs/ instead of $SRC_DIR/docs/, since Meson
doesn't support modifying the source tree (a good thing in my opinion).

I'd like to see this submitted to the tree, and I'll keep updating it as
needed. See the commit message for instructions on how to build and install
with Meson.

There is also a second patch that adds a new
scripts/make_distribution_archives.py script. it essentially does the same
thing as "make dist", but doesn't rely on GNU Make / CMake / Meson in any
way. I think it's a better way to generate our distribution archives and a
good way to transition to a different build system overall.

Let me know what you think about these.

- Digit

PS: This introduces several python scripts, I have been using the "yapf"
tool to format them. If you know of a better python reformatter, let me
know.
From 45017badeade312778b7e0107873e9649ff52e34 Mon Sep 17 00:00:00 2001
From: David Turner <david.turner....@gmail.com>
Date: Sun, 17 May 2020 18:45:41 +0200
Subject: [build] Add Meson build project files.

This adds a few files to build the FreeType 2 library
with the Meson build system:

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

- meson_options.txt: user-selectable options for the build.
  These can be printed with 'meson configure', and selected
  as '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: A script invoked by
  meson.build to process the original ftoption.h and
  enable or disabled configuration macro variables based
  on the available dependencies. This is similar to what
  other build systems are using (i.e. Meson configure_file()
  is not used here).

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

- scripts/extract_libtool_version.py: A script invoked by
  meson.build to extract the libtool revision_info from
  builds/unix/configure.raw and generate the corresponding
  shared library suffix.

- scripts/generate_reference_docs.py: A script invoked
  by meson.build to generate the FreeType 2 reference
  documentation (using the docwriter / mkdocs packages
  which must be installed previously).

Example usage:

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

  # Build and install to /tmp/aa/, this includes a pkg-config file.
  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
---
 builds/unix/ftsystem.c              |   2 +-
 meson.build                         | 346 ++++++++++++++++++++++++++++
 meson_options.txt                   |   6 +
 scripts/extract_freetype_version.py | 101 ++++++++
 scripts/extract_libtool_version.py  |  96 ++++++++
 scripts/generate_reference_docs.py  |  63 +++++
 scripts/parse_modules_cfg.py        | 141 ++++++++++++
 scripts/process_ftoption_h.py       |  97 ++++++++
 8 files changed, 851 insertions(+), 1 deletion(-)
 create mode 100644 meson.build
 create mode 100644 meson_options.txt
 create mode 100644 scripts/extract_freetype_version.py
 create mode 100644 scripts/extract_libtool_version.py
 create mode 100644 scripts/generate_reference_docs.py
 create mode 100644 scripts/parse_modules_cfg.py
 create mode 100644 scripts/process_ftoption_h.py

diff --git a/builds/unix/ftsystem.c b/builds/unix/ftsystem.c
index 8437a6689..b4d71d40e 100644
--- 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>
diff --git a/meson.build b/meson.build
new file mode 100644
index 000000000..1d8f2672c
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,346 @@
+# Meson project file for FreeType 2
+
+project('freetype2', 'c', 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
+
+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() / 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 ]
+
+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@'
+    ],
+)
+
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 000000000..e3626e276
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,6 @@
+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.')
diff --git a/scripts/extract_freetype_version.py b/scripts/extract_freetype_version.py
new file mode 100644
index 000000000..ad2ad2a3b
--- /dev/null
+++ b/scripts/extract_freetype_version.py
@@ -0,0 +1,101 @@
+#!/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_MINOR = re.compile(r'^#define\s+FREETYPE_MINOR\s+(.*)$')
+RE_PATCH = re.compile(r'^#define\s+FREETYPE_PATCH\s+(.*)$')
+
+
+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())
diff --git a/scripts/extract_libtool_version.py b/scripts/extract_libtool_version.py
new file mode 100644
index 000000000..e7187c5ed
--- /dev/null
+++ b/scripts/extract_libtool_version.py
@@ -0,0 +1,96 @@
+#!/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())
diff --git a/scripts/generate_reference_docs.py b/scripts/generate_reference_docs.py
new file mode 100644
index 000000000..7acf4740e
--- /dev/null
+++ b/scripts/generate_reference_docs.py
@@ -0,0 +1,63 @@
+#!/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())
diff --git a/scripts/parse_modules_cfg.py b/scripts/parse_modules_cfg.py
new file mode 100644
index 000000000..5a5509d74
--- /dev/null
+++ b/scripts/parse_modules_cfg.py
@@ -0,0 +1,141 @@
+#!/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())
diff --git a/scripts/process_ftoption_h.py b/scripts/process_ftoption_h.py
new file mode 100644
index 000000000..b03bec102
--- /dev/null
+++ b/scripts/process_ftoption_h.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+"""Toggle settings in ftoption.h file based on command-line arguments.
+
+This script takes an ftoption.h file as input, and will rewrite #define/#undef
+lines in it based on --enable=CONFIG_VARNAME or --disable=CONFIG_VARNAME
+arguments passed to it, where CONFIG_VARNAME is a 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
+will exit 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())
-- 
2.20.1

From 4d56417ff87bbb4beffc6f09d106df86b8877723 Mon Sep 17 00:00:00 2001
From: David Turner <david.turner....@gmail.com>
Date: Tue, 25 Aug 2020 20:52:32 +0200
Subject: [build] Add scripts/make_distribution_archives.py

This standalone Python script should be equivalent to running
"make dist" with the Make-based build system, with the following
minor differences:

- Since 'make distclean' doesn't always cleanup objs/ properly,
  the 'make dist' archives may contain some stale binaries like
  objs/.libs/libfreetype.so.6 or others.

- The config.guess and config.sub files are not update, unless
  one uses the --gnu-config-dir=DIR option to specify were they
  are located.

- Some bits of the auto-generated reference documentation may
  appear in slightly different order, probably due to issues
  related to mkdocs and docwriter.

Usage example:

  scripts/make_distribution_archives.py /tmp/freetype2-dist

Will create files the following files under /tmp/freetype2-dist:

  freetype-<version>.tar.gz
  freetype-<version>.tar.xz
  ft<winversion>.zip
---
 scripts/make_distribution_archives.py | 178 ++++++++++++++++++++++++++
 1 file changed, 178 insertions(+)
 create mode 100755 scripts/make_distribution_archives.py

diff --git a/scripts/make_distribution_archives.py b/scripts/make_distribution_archives.py
new file mode 100755
index 000000000..1cc685ffe
--- /dev/null
+++ b/scripts/make_distribution_archives.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+"""Generate distribution archives for a given FreeType 2 release."""
+
+from __future__ import print_function
+
+import argparse
+import atexit
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+_SCRIPT_DIR = os.path.dirname(__file__)
+_TOP_DIR = os.path.abspath(os.path.join(_SCRIPT_DIR, '..'))
+
+
+def get_cmd_output(cmd, cwd=None):
+    """Run a command and return its output as a string."""
+    if cwd is not None:
+        out = subprocess.check_output(cmd, cwd=cwd)
+    else:
+        out = subprocess.check_output(cmd)
+    return out.decode('utf-8').rstrip()
+
+
+def is_git_dir_clean(git_dir):
+    """Return True iff |git_dir| is a git directory in clean state."""
+    out = get_cmd_output(['git', 'status', '--porcelain'], cwd=git_dir)
+    return len(out) == 0
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        '--source_dir', default=_TOP_DIR, help='Source directory path.')
+
+    parser.add_argument(
+        '--version',
+        help=
+        'Specify alternate FreeType version (it is otherwise extracted from '
+        'current sources by default).')
+
+    parser.add_argument(
+        '--gnu-config-dir',
+        help=('Path of input directory containing recent `config.guess` and '
+              '`config.sub` files from GNU config.'))
+
+    parser.add_argument(
+        '--build-dir',
+        help='Specify build directory. Only used for debugging this script.')
+
+    parser.add_argument(
+        '--ignore-clean-check',
+        action='store_true',
+        help='Do not check for a clean source git repository. Only used for '
+        'debugging this script.')
+
+    parser.add_argument(
+        'output_dir', help='Output directory for generated archives.')
+
+    args = parser.parse_args()
+
+    git_dir = args.source_dir if args.source_dir else _TOP_DIR
+    if not args.ignore_clean_check and not is_git_dir_clean(git_dir):
+        sys.stderr.write(
+            'ERROR: Your git repository is not in a clean state: %s\n' %
+            git_dir)
+        return 1
+
+    if args.version:
+        version = args.version
+    else:
+        # Extract FreeType version from sources.
+        version = get_cmd_output([
+            sys.executable,
+            os.path.join(_SCRIPT_DIR, 'extract_freetype_version.py'),
+            os.path.join(_TOP_DIR, 'include', 'freetype', 'freetype.h')
+        ])
+
+    # Determine the build directory. This will be a temporary file that is
+    # cleaned up on script exit by default, unless --build-dir=DIR is used,
+    # in which case we only create and empty the directory, but never remove
+    # its content on exit.
+    if args.build_dir:
+        build_dir = args.build_dir
+        if not os.path.exists(build_dir):
+            os.makedirs(build_dir)
+        else:
+            # Remove anything from the build directory, if any.
+            for item in os.listdir(build_dir):
+                file_path = os.path.join(build_dir, item)
+                if os.path.isdir(file_path):
+                    shutil.rmtree(file_path)
+                else:
+                    os.unlink(file_path)
+    else:
+        # Create a temporary directory, and ensure it is removed on exit.
+        build_dir = tempfile.mkdtemp(prefix='freetype-dist-')
+
+        def clean_build_dir():
+            shutil.rmtree(build_dir)
+
+        atexit.register(clean_build_dir)
+
+    # Copy all source files known to git into $BUILD_DIR/freetype-$VERSION
+    # with the exception of .gitignore and .mailmap files.
+    source_files = [
+        f for f in get_cmd_output(['git', 'ls-files'], cwd=git_dir).split('\n')
+        if os.path.basename(f) not in ('.gitignore', '.mailmap')
+    ]
+
+    freetype_dir = 'freetype-' + version
+    tmp_src_dir = os.path.join(build_dir, freetype_dir)
+    os.makedirs(tmp_src_dir)
+
+    for src in source_files:
+        dst = os.path.join(tmp_src_dir, src)
+        dst_dir = os.path.dirname(dst)
+        if not os.path.exists(dst_dir):
+            os.makedirs(dst_dir)
+        shutil.copyfile(src, dst)
+
+    # Run autogen.sh in directory.
+    subprocess.check_call(['/bin/sh', 'autogen.sh'], cwd=tmp_src_dir)
+    shutil.rmtree(
+        os.path.join(tmp_src_dir, 'builds', 'unix', 'autom4te.cache'))
+
+    # Copy config.guess and config.sub if possible!
+    if args.gnu_config_dir:
+        for f in ('config.guess', 'config.sub'):
+            shutil.copyfile(
+                os.path.join(args.gnu_config_dir, f),
+                os.path.join(tmp_src_dir, 'builds', 'unix', f))
+
+    # Generate reference documentation under docs/
+    subprocess.check_call([
+        sys.executable,
+        os.path.join(_SCRIPT_DIR, 'generate_reference_docs.py'), '--input-dir',
+        tmp_src_dir, '--version', version, '--output-dir',
+        os.path.join(tmp_src_dir, 'docs')
+    ])
+
+    shutil.rmtree(os.path.join(tmp_src_dir, 'docs', 'markdown'))
+    os.unlink(os.path.join(tmp_src_dir, 'docs', 'mkdocs.yml'))
+
+    # Generate our archives
+    freetype_tar = freetype_dir + '.tar'
+
+    subprocess.check_call(
+        ['tar', '-H', 'ustar', '-chf', freetype_tar, freetype_dir],
+        cwd=build_dir)
+
+    subprocess.check_call(['gzip', '-9', '--keep', freetype_tar],
+                          cwd=build_dir)
+
+    subprocess.check_call(['xz', '--keep', freetype_tar], cwd=build_dir)
+
+    ftwinversion = 'ft' + ''.join(version.split('.'))
+    subprocess.check_call(
+        ['zip', '-qlr9', ftwinversion + '.zip', freetype_dir], cwd=build_dir)
+
+    # Copy file to output directory now.
+    if not os.path.exists(args.output_dir):
+        os.makedirs(args.output_dir)
+
+    for f in (freetype_tar + '.gz', freetype_tar + '.xz',
+              ftwinversion + '.zip'):
+        shutil.copyfile(
+            os.path.join(build_dir, f), os.path.join(args.output_dir, f))
+
+    # Done!
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
-- 
2.20.1

Reply via email to