blob: d1c5a4f67d1cc415393abb998b0cd2d39f77312a [file] [log] [blame]
Haibo Huang5406a6a2020-02-26 16:36:20 -08001#! /usr/bin/env python3
jvr3285b4b2001-08-09 18:47:22 +00002
Denis Jacqueryedb08ee22013-11-29 14:11:19 +01003from __future__ import print_function
Haibo Huang8b3c57b2018-07-03 17:43:11 -07004import io
5import sys
6import os
7from os.path import isfile, join as pjoin
8from glob import glob
Haibo Huangd123eeb2020-04-03 12:18:18 -07009from setuptools import setup, find_packages, Command, Extension
10from setuptools.command.build_ext import build_ext as _build_ext
Haibo Huang8b3c57b2018-07-03 17:43:11 -070011from distutils import log
12from distutils.util import convert_path
13import subprocess as sp
14import contextlib
15import re
jvr3285b4b2001-08-09 18:47:22 +000016
Haibo Huang8b3c57b2018-07-03 17:43:11 -070017# Force distutils to use py_compile.compile() function with 'doraise' argument
18# set to True, in order to raise an exception on compilation errors
19import py_compile
20orig_py_compile = py_compile.compile
jvr3285b4b2001-08-09 18:47:22 +000021
Haibo Huang8b3c57b2018-07-03 17:43:11 -070022def doraise_py_compile(file, cfile=None, dfile=None, doraise=False):
23 orig_py_compile(file, cfile=cfile, dfile=dfile, doraise=True)
24
25py_compile.compile = doraise_py_compile
26
Haibo Huangd123eeb2020-04-03 12:18:18 -070027setup_requires = []
28
29if {'bdist_wheel'}.intersection(sys.argv):
30 setup_requires.append('wheel')
31
32if {'release'}.intersection(sys.argv):
33 setup_requires.append('bump2version')
34
35try:
36 __import__("cython")
37except ImportError:
38 has_cython = False
39else:
40 has_cython = True
41
42env_with_cython = os.environ.get("FONTTOOLS_WITH_CYTHON")
43with_cython = (
44 True if env_with_cython in {"1", "true", "yes"}
45 else False if env_with_cython in {"0", "false", "no"}
46 else None
47)
48# --with-cython/--without-cython options override environment variables
49opt_with_cython = {'--with-cython'}.intersection(sys.argv)
50opt_without_cython = {'--without-cython'}.intersection(sys.argv)
51if opt_with_cython and opt_without_cython:
52 sys.exit(
53 "error: the options '--with-cython' and '--without-cython' are "
54 "mutually exclusive"
55 )
56elif opt_with_cython:
57 sys.argv.remove("--with-cython")
58 with_cython = True
59elif opt_without_cython:
60 sys.argv.remove("--without-cython")
61 with_cython = False
62
63if with_cython and not has_cython:
64 setup_requires.append("cython")
65
66ext_modules = []
67if with_cython is True or (with_cython is None and has_cython):
68 ext_modules.append(
69 Extension("fontTools.cu2qu.cu2qu", ["Lib/fontTools/cu2qu/cu2qu.py"]),
70 )
Haibo Huang8b3c57b2018-07-03 17:43:11 -070071
Elliott Hughes69c9aca2018-10-30 14:11:58 -070072extras_require = {
73 # for fontTools.ufoLib: to read/write UFO fonts
74 "ufo": [
Haibo Huangf08648c2019-02-01 17:02:23 -080075 "fs >= 2.2.0, < 3",
Elliott Hughes69c9aca2018-10-30 14:11:58 -070076 ],
77 # for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to
78 # read/write XML files (faster/safer than built-in ElementTree)
79 "lxml": [
80 "lxml >= 4.0, < 5",
Elliott Hughes69c9aca2018-10-30 14:11:58 -070081 ],
82 # for fontTools.sfnt and fontTools.woff2: to compress/uncompress
83 # WOFF 1.0 and WOFF 2.0 webfonts.
84 "woff": [
Haibo Huang7de67992020-12-04 18:55:08 -080085 "brotli >= 1.0.1; platform_python_implementation == 'CPython'",
86 "brotlicffi >= 0.8.0; platform_python_implementation != 'CPython'",
Elliott Hughes69c9aca2018-10-30 14:11:58 -070087 "zopfli >= 0.1.4",
88 ],
89 # for fontTools.unicode and fontTools.unicodedata: to use the latest version
90 # of the Unicode Character Database instead of the built-in unicodedata
91 # which varies between python versions and may be outdated.
92 "unicode": [
93 # the unicodedata2 extension module doesn't work on PyPy.
Haibo Huang6a212842020-03-20 13:06:43 -070094 # Python 3.9 already has Unicode 13.0, so the backport is not needed.
Elliott Hughes69c9aca2018-10-30 14:11:58 -070095 (
Haibo Huang6a212842020-03-20 13:06:43 -070096 "unicodedata2 >= 13.0.0; "
97 "python_version < '3.9' and platform_python_implementation != 'PyPy'"
Elliott Hughes69c9aca2018-10-30 14:11:58 -070098 ),
99 ],
Haibo Huang79019a02019-01-08 14:14:22 -0800100 # for graphite type tables in ttLib/tables (Silf, Glat, Gloc)
101 "graphite": [
102 "lz4 >= 1.7.4.2"
103 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700104 # for fontTools.interpolatable: to solve the "minimum weight perfect
105 # matching problem in bipartite graphs" (aka Assignment problem)
106 "interpolatable": [
107 # use pure-python alternative on pypy
108 "scipy; platform_python_implementation != 'PyPy'",
109 "munkres; platform_python_implementation == 'PyPy'",
110 ],
Haibo Huang79019a02019-01-08 14:14:22 -0800111 # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting
112 # VariationModel
113 "plot": [
114 # TODO: figure out the minimum version of matplotlib that we need
115 "matplotlib",
116 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700117 # for fontTools.misc.symfont, module for symbolic font statistics analysis
118 "symfont": [
119 "sympy",
120 ],
121 # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only)
122 "type1": [
123 "xattr; sys_platform == 'darwin'",
124 ],
Haibo Huang672f6942020-10-28 22:20:03 -0700125 # for fontTools.ttLib.removeOverlaps, to remove overlaps in TTF fonts
126 "pathops": [
127 "skia-pathops >= 0.5.0",
128 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700129}
130# use a special 'all' key as shorthand to includes all the extra dependencies
131extras_require["all"] = sum(extras_require.values(), [])
132
133
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700134# Trove classifiers for PyPI
135classifiers = {"classifiers": [
136 "Development Status :: 5 - Production/Stable",
137 "Environment :: Console",
138 "Environment :: Other Environment",
139 "Intended Audience :: Developers",
140 "Intended Audience :: End Users/Desktop",
141 "License :: OSI Approved :: MIT License",
142 "Natural Language :: English",
143 "Operating System :: OS Independent",
144 "Programming Language :: Python",
145 "Programming Language :: Python :: 2",
146 "Programming Language :: Python :: 3",
147 "Topic :: Text Processing :: Fonts",
148 "Topic :: Multimedia :: Graphics",
149 "Topic :: Multimedia :: Graphics :: Graphics Conversion",
150]}
jvr5808f3f2001-08-09 23:03:47 +0000151
152
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700153# concatenate README.rst and NEWS.rest into long_description so they are
154# displayed on the FontTols project page on PyPI
155with io.open("README.rst", "r", encoding="utf-8") as readme:
156 long_description = readme.read()
157long_description += "\nChangelog\n~~~~~~~~~\n\n"
158with io.open("NEWS.rst", "r", encoding="utf-8") as changelog:
159 long_description += changelog.read()
jvr059cbe32002-07-01 09:11:01 +0000160
161
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700162@contextlib.contextmanager
163def capture_logger(name):
164 """ Context manager to capture a logger output with a StringIO stream.
165 """
166 import logging
jvr91bde172003-01-03 21:01:07 +0000167
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700168 logger = logging.getLogger(name)
169 try:
170 import StringIO
171 stream = StringIO.StringIO()
172 except ImportError:
173 stream = io.StringIO()
174 handler = logging.StreamHandler(stream)
175 logger.addHandler(handler)
176 try:
177 yield stream
178 finally:
179 logger.removeHandler(handler)
180
181
182class release(Command):
183 """
184 Tag a new release with a single command, using the 'bumpversion' tool
185 to update all the version strings in the source code.
186 The version scheme conforms to 'SemVer' and PEP 440 specifications.
187
188 Firstly, the pre-release '.devN' suffix is dropped to signal that this is
189 a stable release. If '--major' or '--minor' options are passed, the
190 the first or second 'semver' digit is also incremented. Major is usually
191 for backward-incompatible API changes, while minor is used when adding
192 new backward-compatible functionalities. No options imply 'patch' or bug-fix
193 release.
194
195 A new header is also added to the changelog file ("NEWS.rst"), containing
196 the new version string and the current 'YYYY-MM-DD' date.
197
198 All changes are committed, and an annotated git tag is generated. With the
199 --sign option, the tag is GPG-signed with the user's default key.
200
201 Finally, the 'patch' part of the version string is bumped again, and a
202 pre-release suffix '.dev0' is appended to mark the opening of a new
203 development cycle.
204
205 Links:
206 - http://semver.org/
207 - https://www.python.org/dev/peps/pep-0440/
208 - https://github.com/c4urself/bump2version
209 """
210
211 description = "update version strings for release"
212
213 user_options = [
214 ("major", None, "bump the first digit (incompatible API changes)"),
215 ("minor", None, "bump the second digit (new backward-compatible features)"),
216 ("sign", "s", "make a GPG-signed tag, using the default key"),
217 ("allow-dirty", None, "don't abort if working directory is dirty"),
218 ]
219
220 changelog_name = "NEWS.rst"
221 version_RE = re.compile("^[0-9]+\.[0-9]+")
222 date_fmt = u"%Y-%m-%d"
223 header_fmt = u"%s (released %s)"
224 commit_message = "Release {new_version}"
225 tag_name = "{new_version}"
226 version_files = [
227 "setup.cfg",
228 "setup.py",
229 "Lib/fontTools/__init__.py",
230 ]
231
232 def initialize_options(self):
233 self.minor = False
234 self.major = False
235 self.sign = False
236 self.allow_dirty = False
237
238 def finalize_options(self):
239 if all([self.major, self.minor]):
240 from distutils.errors import DistutilsOptionError
241 raise DistutilsOptionError("--major/--minor are mutually exclusive")
242 self.part = "major" if self.major else "minor" if self.minor else None
243
244 def run(self):
245 if self.part is not None:
246 log.info("bumping '%s' version" % self.part)
247 self.bumpversion(self.part, commit=False)
248 release_version = self.bumpversion(
249 "release", commit=False, allow_dirty=True)
250 else:
251 log.info("stripping pre-release suffix")
252 release_version = self.bumpversion("release")
253 log.info(" version = %s" % release_version)
254
255 changes = self.format_changelog(release_version)
256
257 self.git_commit(release_version)
258 self.git_tag(release_version, changes, self.sign)
259
260 log.info("bumping 'patch' version and pre-release suffix")
261 next_dev_version = self.bumpversion('patch', commit=True)
262 log.info(" version = %s" % next_dev_version)
263
264 def git_commit(self, version):
265 """ Stage and commit all relevant version files, and format the commit
266 message with specified 'version' string.
267 """
268 files = self.version_files + [self.changelog_name]
269
270 log.info("committing changes")
271 for f in files:
272 log.info(" %s" % f)
273 if self.dry_run:
274 return
275 sp.check_call(["git", "add"] + files)
276 msg = self.commit_message.format(new_version=version)
277 sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE)
278
279 def git_tag(self, version, message, sign=False):
280 """ Create annotated git tag with given 'version' and 'message'.
281 Optionally 'sign' the tag with the user's GPG key.
282 """
283 log.info("creating %s git tag '%s'" % (
284 "signed" if sign else "annotated", version))
285 if self.dry_run:
286 return
287 # create an annotated (or signed) tag from the new version
288 tag_opt = "-s" if sign else "-a"
289 tag_name = self.tag_name.format(new_version=version)
290 proc = sp.Popen(
291 ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE)
292 # use the latest changes from the changelog file as the tag message
293 tag_message = u"%s\n\n%s" % (tag_name, message)
294 proc.communicate(tag_message.encode('utf-8'))
295 if proc.returncode != 0:
296 sys.exit(proc.returncode)
297
298 def bumpversion(self, part, commit=False, message=None, allow_dirty=None):
299 """ Run bumpversion.main() with the specified arguments, and return the
300 new computed version string (cf. 'bumpversion --help' for more info)
301 """
Haibo Huang5406a6a2020-02-26 16:36:20 -0800302 import bumpversion.cli
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700303
304 args = (
305 (['--verbose'] if self.verbose > 1 else []) +
306 (['--dry-run'] if self.dry_run else []) +
307 (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) +
308 (['--commit'] if commit else ['--no-commit']) +
309 (['--message', message] if message is not None else []) +
310 ['--list', part]
311 )
312 log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args))
313
314 with capture_logger("bumpversion.list") as out:
Haibo Huang5406a6a2020-02-26 16:36:20 -0800315 bumpversion.cli.main(args)
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700316
317 last_line = out.getvalue().splitlines()[-1]
318 new_version = last_line.replace("new_version=", "")
319 return new_version
320
321 def format_changelog(self, version):
322 """ Write new header at beginning of changelog file with the specified
323 'version' and the current date.
324 Return the changelog content for the current release.
325 """
326 from datetime import datetime
327
328 log.info("formatting changelog")
329
330 changes = []
331 with io.open(self.changelog_name, "r+", encoding="utf-8") as f:
332 for ln in f:
333 if self.version_RE.match(ln):
334 break
335 else:
336 changes.append(ln)
337 if not self.dry_run:
338 f.seek(0)
339 content = f.read()
340 date = datetime.today().strftime(self.date_fmt)
341 f.seek(0)
342 header = self.header_fmt % (version, date)
343 f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content)
344
345 return u"".join(changes)
346
347
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700348def find_data_files(manpath="share/man"):
349 """ Find FontTools's data_files (just man pages at this point).
350
351 By default, we install man pages to "share/man" directory relative to the
352 base installation directory for data_files. The latter can be changed with
353 the --install-data option of 'setup.py install' sub-command.
354
355 E.g., if the data files installation directory is "/usr", the default man
356 page installation directory will be "/usr/share/man".
357
358 You can override this via the $FONTTOOLS_MANPATH environment variable.
359
360 E.g., on some BSD systems man pages are installed to 'man' instead of
361 'share/man'; you can export $FONTTOOLS_MANPATH variable just before
362 installing:
363
364 $ FONTTOOLS_MANPATH="man" pip install -v .
365 [...]
366 running install_data
367 copying Doc/man/ttx.1 -> /usr/man/man1
368
369 When installing from PyPI, for this variable to have effect you need to
370 force pip to install from the source distribution instead of the wheel
371 package (otherwise setup.py is not run), by using the --no-binary option:
372
373 $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools
374
375 Note that you can only override the base man path, i.e. without the
376 section number (man1, man3, etc.). The latter is always implied to be 1,
377 for "general commands".
378 """
379
380 # get base installation directory for man pages
381 manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath))
382 # all our man pages go to section 1
383 manpagedir = pjoin(manpagebase, 'man1')
384
385 manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)]
386
387 data_files = [(manpagedir, manpages)]
388 return data_files
389
jvr91bde172003-01-03 21:01:07 +0000390
Haibo Huangd123eeb2020-04-03 12:18:18 -0700391class cython_build_ext(_build_ext):
392 """Compile *.pyx source files to *.c using cythonize if Cython is
393 installed and there is a working C compiler, else fall back to pure python dist.
394 """
395
396 def finalize_options(self):
397 from Cython.Build import cythonize
398
399 # optionally enable line tracing for test coverage support
400 linetrace = os.environ.get("CYTHON_TRACE") == "1"
401
402 self.distribution.ext_modules[:] = cythonize(
403 self.distribution.ext_modules,
404 force=linetrace or self.force,
405 annotate=os.environ.get("CYTHON_ANNOTATE") == "1",
406 quiet=not self.verbose,
407 compiler_directives={
408 "linetrace": linetrace,
409 "language_level": 3,
410 "embedsignature": True,
411 },
412 )
413
414 _build_ext.finalize_options(self)
415
416 def build_extensions(self):
417 try:
418 _build_ext.build_extensions(self)
419 except Exception as e:
420 if with_cython:
421 raise
422 from distutils.errors import DistutilsModuleError
423
424 # optional compilation failed: we delete 'ext_modules' and make sure
425 # the generated wheel is 'pure'
426 del self.distribution.ext_modules[:]
427 try:
428 bdist_wheel = self.get_finalized_command("bdist_wheel")
429 except DistutilsModuleError:
430 # 'bdist_wheel' command not available as wheel is not installed
431 pass
432 else:
433 bdist_wheel.root_is_pure = True
434 log.error('error: building extensions failed: %s' % e)
435
436cmdclass = {"release": release}
437
438if ext_modules:
439 cmdclass["build_ext"] = cython_build_ext
440
441
442setup_params = dict(
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700443 name="fonttools",
Elliott Hughes6cf80b82021-04-01 15:12:06 -0700444 version="4.22.0",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700445 description="Tools to manipulate font files",
446 author="Just van Rossum",
447 author_email="just@letterror.com",
448 maintainer="Behdad Esfahbod",
449 maintainer_email="behdad@behdad.org",
450 url="http://github.com/fonttools/fonttools",
451 license="MIT",
452 platforms=["Any"],
Haibo Huang5406a6a2020-02-26 16:36:20 -0800453 python_requires=">=3.6",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700454 long_description=long_description,
455 package_dir={'': 'Lib'},
456 packages=find_packages("Lib"),
457 include_package_data=True,
458 data_files=find_data_files(),
Haibo Huangd123eeb2020-04-03 12:18:18 -0700459 ext_modules=ext_modules,
460 setup_requires=setup_requires,
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700461 extras_require=extras_require,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700462 entry_points={
463 'console_scripts': [
464 "fonttools = fontTools.__main__:main",
465 "ttx = fontTools.ttx:main",
466 "pyftsubset = fontTools.subset:main",
467 "pyftmerge = fontTools.merge:main",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700468 ]
469 },
Haibo Huangd123eeb2020-04-03 12:18:18 -0700470 cmdclass=cmdclass,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700471 **classifiers
472)
Haibo Huangd123eeb2020-04-03 12:18:18 -0700473
474
475if __name__ == "__main__":
476 setup(**setup_params)