blob: 2f30ef4f6e4ecae4010fed72e3309f6b46366c05 [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": [
Rod S0b59a542022-03-25 12:41:45 -070093 # Python 3.11 already has Unicode 14.0, so the backport is not needed.
Elliott Hughes69c9aca2018-10-30 14:11:58 -070094 (
Rod S0b59a542022-03-25 12:41:45 -070095 "unicodedata2 >= 14.0.0; python_version < '3.11'"
Elliott Hughes69c9aca2018-10-30 14:11:58 -070096 ),
97 ],
Haibo Huang79019a02019-01-08 14:14:22 -080098 # for graphite type tables in ttLib/tables (Silf, Glat, Gloc)
99 "graphite": [
100 "lz4 >= 1.7.4.2"
101 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700102 # for fontTools.interpolatable: to solve the "minimum weight perfect
103 # matching problem in bipartite graphs" (aka Assignment problem)
104 "interpolatable": [
105 # use pure-python alternative on pypy
106 "scipy; platform_python_implementation != 'PyPy'",
107 "munkres; platform_python_implementation == 'PyPy'",
108 ],
Haibo Huang79019a02019-01-08 14:14:22 -0800109 # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting
110 # VariationModel
111 "plot": [
112 # TODO: figure out the minimum version of matplotlib that we need
113 "matplotlib",
114 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700115 # for fontTools.misc.symfont, module for symbolic font statistics analysis
116 "symfont": [
117 "sympy",
118 ],
119 # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only)
120 "type1": [
121 "xattr; sys_platform == 'darwin'",
122 ],
Haibo Huang672f6942020-10-28 22:20:03 -0700123 # for fontTools.ttLib.removeOverlaps, to remove overlaps in TTF fonts
124 "pathops": [
125 "skia-pathops >= 0.5.0",
126 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700127}
128# use a special 'all' key as shorthand to includes all the extra dependencies
129extras_require["all"] = sum(extras_require.values(), [])
130
131
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700132# Trove classifiers for PyPI
133classifiers = {"classifiers": [
134 "Development Status :: 5 - Production/Stable",
135 "Environment :: Console",
136 "Environment :: Other Environment",
137 "Intended Audience :: Developers",
138 "Intended Audience :: End Users/Desktop",
139 "License :: OSI Approved :: MIT License",
140 "Natural Language :: English",
141 "Operating System :: OS Independent",
142 "Programming Language :: Python",
143 "Programming Language :: Python :: 2",
144 "Programming Language :: Python :: 3",
145 "Topic :: Text Processing :: Fonts",
146 "Topic :: Multimedia :: Graphics",
147 "Topic :: Multimedia :: Graphics :: Graphics Conversion",
148]}
jvr5808f3f2001-08-09 23:03:47 +0000149
150
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700151# concatenate README.rst and NEWS.rest into long_description so they are
152# displayed on the FontTols project page on PyPI
153with io.open("README.rst", "r", encoding="utf-8") as readme:
154 long_description = readme.read()
155long_description += "\nChangelog\n~~~~~~~~~\n\n"
156with io.open("NEWS.rst", "r", encoding="utf-8") as changelog:
157 long_description += changelog.read()
jvr059cbe32002-07-01 09:11:01 +0000158
159
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700160@contextlib.contextmanager
161def capture_logger(name):
162 """ Context manager to capture a logger output with a StringIO stream.
163 """
164 import logging
jvr91bde172003-01-03 21:01:07 +0000165
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700166 logger = logging.getLogger(name)
167 try:
168 import StringIO
169 stream = StringIO.StringIO()
170 except ImportError:
171 stream = io.StringIO()
172 handler = logging.StreamHandler(stream)
173 logger.addHandler(handler)
174 try:
175 yield stream
176 finally:
177 logger.removeHandler(handler)
178
179
180class release(Command):
181 """
182 Tag a new release with a single command, using the 'bumpversion' tool
183 to update all the version strings in the source code.
184 The version scheme conforms to 'SemVer' and PEP 440 specifications.
185
186 Firstly, the pre-release '.devN' suffix is dropped to signal that this is
187 a stable release. If '--major' or '--minor' options are passed, the
188 the first or second 'semver' digit is also incremented. Major is usually
189 for backward-incompatible API changes, while minor is used when adding
190 new backward-compatible functionalities. No options imply 'patch' or bug-fix
191 release.
192
193 A new header is also added to the changelog file ("NEWS.rst"), containing
194 the new version string and the current 'YYYY-MM-DD' date.
195
196 All changes are committed, and an annotated git tag is generated. With the
197 --sign option, the tag is GPG-signed with the user's default key.
198
199 Finally, the 'patch' part of the version string is bumped again, and a
200 pre-release suffix '.dev0' is appended to mark the opening of a new
201 development cycle.
202
203 Links:
204 - http://semver.org/
205 - https://www.python.org/dev/peps/pep-0440/
206 - https://github.com/c4urself/bump2version
207 """
208
209 description = "update version strings for release"
210
211 user_options = [
212 ("major", None, "bump the first digit (incompatible API changes)"),
213 ("minor", None, "bump the second digit (new backward-compatible features)"),
214 ("sign", "s", "make a GPG-signed tag, using the default key"),
215 ("allow-dirty", None, "don't abort if working directory is dirty"),
216 ]
217
218 changelog_name = "NEWS.rst"
219 version_RE = re.compile("^[0-9]+\.[0-9]+")
220 date_fmt = u"%Y-%m-%d"
221 header_fmt = u"%s (released %s)"
222 commit_message = "Release {new_version}"
223 tag_name = "{new_version}"
224 version_files = [
225 "setup.cfg",
226 "setup.py",
227 "Lib/fontTools/__init__.py",
228 ]
229
230 def initialize_options(self):
231 self.minor = False
232 self.major = False
233 self.sign = False
234 self.allow_dirty = False
235
236 def finalize_options(self):
237 if all([self.major, self.minor]):
238 from distutils.errors import DistutilsOptionError
239 raise DistutilsOptionError("--major/--minor are mutually exclusive")
240 self.part = "major" if self.major else "minor" if self.minor else None
241
242 def run(self):
243 if self.part is not None:
244 log.info("bumping '%s' version" % self.part)
245 self.bumpversion(self.part, commit=False)
246 release_version = self.bumpversion(
247 "release", commit=False, allow_dirty=True)
248 else:
249 log.info("stripping pre-release suffix")
250 release_version = self.bumpversion("release")
251 log.info(" version = %s" % release_version)
252
253 changes = self.format_changelog(release_version)
254
255 self.git_commit(release_version)
256 self.git_tag(release_version, changes, self.sign)
257
258 log.info("bumping 'patch' version and pre-release suffix")
259 next_dev_version = self.bumpversion('patch', commit=True)
260 log.info(" version = %s" % next_dev_version)
261
262 def git_commit(self, version):
263 """ Stage and commit all relevant version files, and format the commit
264 message with specified 'version' string.
265 """
266 files = self.version_files + [self.changelog_name]
267
268 log.info("committing changes")
269 for f in files:
270 log.info(" %s" % f)
271 if self.dry_run:
272 return
273 sp.check_call(["git", "add"] + files)
274 msg = self.commit_message.format(new_version=version)
275 sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE)
276
277 def git_tag(self, version, message, sign=False):
278 """ Create annotated git tag with given 'version' and 'message'.
279 Optionally 'sign' the tag with the user's GPG key.
280 """
281 log.info("creating %s git tag '%s'" % (
282 "signed" if sign else "annotated", version))
283 if self.dry_run:
284 return
285 # create an annotated (or signed) tag from the new version
286 tag_opt = "-s" if sign else "-a"
287 tag_name = self.tag_name.format(new_version=version)
288 proc = sp.Popen(
289 ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE)
290 # use the latest changes from the changelog file as the tag message
291 tag_message = u"%s\n\n%s" % (tag_name, message)
292 proc.communicate(tag_message.encode('utf-8'))
293 if proc.returncode != 0:
294 sys.exit(proc.returncode)
295
296 def bumpversion(self, part, commit=False, message=None, allow_dirty=None):
297 """ Run bumpversion.main() with the specified arguments, and return the
298 new computed version string (cf. 'bumpversion --help' for more info)
299 """
Haibo Huang5406a6a2020-02-26 16:36:20 -0800300 import bumpversion.cli
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700301
302 args = (
303 (['--verbose'] if self.verbose > 1 else []) +
304 (['--dry-run'] if self.dry_run else []) +
305 (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) +
306 (['--commit'] if commit else ['--no-commit']) +
307 (['--message', message] if message is not None else []) +
308 ['--list', part]
309 )
310 log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args))
311
312 with capture_logger("bumpversion.list") as out:
Haibo Huang5406a6a2020-02-26 16:36:20 -0800313 bumpversion.cli.main(args)
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700314
315 last_line = out.getvalue().splitlines()[-1]
316 new_version = last_line.replace("new_version=", "")
317 return new_version
318
319 def format_changelog(self, version):
320 """ Write new header at beginning of changelog file with the specified
321 'version' and the current date.
322 Return the changelog content for the current release.
323 """
324 from datetime import datetime
325
326 log.info("formatting changelog")
327
328 changes = []
329 with io.open(self.changelog_name, "r+", encoding="utf-8") as f:
330 for ln in f:
331 if self.version_RE.match(ln):
332 break
333 else:
334 changes.append(ln)
335 if not self.dry_run:
336 f.seek(0)
337 content = f.read()
338 date = datetime.today().strftime(self.date_fmt)
339 f.seek(0)
340 header = self.header_fmt % (version, date)
341 f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content)
342
343 return u"".join(changes)
344
345
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700346def find_data_files(manpath="share/man"):
347 """ Find FontTools's data_files (just man pages at this point).
348
349 By default, we install man pages to "share/man" directory relative to the
350 base installation directory for data_files. The latter can be changed with
351 the --install-data option of 'setup.py install' sub-command.
352
353 E.g., if the data files installation directory is "/usr", the default man
354 page installation directory will be "/usr/share/man".
355
356 You can override this via the $FONTTOOLS_MANPATH environment variable.
357
358 E.g., on some BSD systems man pages are installed to 'man' instead of
359 'share/man'; you can export $FONTTOOLS_MANPATH variable just before
360 installing:
361
362 $ FONTTOOLS_MANPATH="man" pip install -v .
363 [...]
364 running install_data
365 copying Doc/man/ttx.1 -> /usr/man/man1
366
367 When installing from PyPI, for this variable to have effect you need to
368 force pip to install from the source distribution instead of the wheel
369 package (otherwise setup.py is not run), by using the --no-binary option:
370
371 $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools
372
373 Note that you can only override the base man path, i.e. without the
374 section number (man1, man3, etc.). The latter is always implied to be 1,
375 for "general commands".
376 """
377
378 # get base installation directory for man pages
379 manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath))
380 # all our man pages go to section 1
381 manpagedir = pjoin(manpagebase, 'man1')
382
383 manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)]
384
385 data_files = [(manpagedir, manpages)]
386 return data_files
387
jvr91bde172003-01-03 21:01:07 +0000388
Haibo Huangd123eeb2020-04-03 12:18:18 -0700389class cython_build_ext(_build_ext):
390 """Compile *.pyx source files to *.c using cythonize if Cython is
391 installed and there is a working C compiler, else fall back to pure python dist.
392 """
393
394 def finalize_options(self):
395 from Cython.Build import cythonize
396
397 # optionally enable line tracing for test coverage support
398 linetrace = os.environ.get("CYTHON_TRACE") == "1"
399
400 self.distribution.ext_modules[:] = cythonize(
401 self.distribution.ext_modules,
402 force=linetrace or self.force,
403 annotate=os.environ.get("CYTHON_ANNOTATE") == "1",
404 quiet=not self.verbose,
405 compiler_directives={
406 "linetrace": linetrace,
407 "language_level": 3,
408 "embedsignature": True,
409 },
410 )
411
412 _build_ext.finalize_options(self)
413
414 def build_extensions(self):
415 try:
416 _build_ext.build_extensions(self)
417 except Exception as e:
418 if with_cython:
419 raise
420 from distutils.errors import DistutilsModuleError
421
422 # optional compilation failed: we delete 'ext_modules' and make sure
423 # the generated wheel is 'pure'
424 del self.distribution.ext_modules[:]
425 try:
426 bdist_wheel = self.get_finalized_command("bdist_wheel")
427 except DistutilsModuleError:
428 # 'bdist_wheel' command not available as wheel is not installed
429 pass
430 else:
431 bdist_wheel.root_is_pure = True
432 log.error('error: building extensions failed: %s' % e)
433
434cmdclass = {"release": release}
435
436if ext_modules:
437 cmdclass["build_ext"] = cython_build_ext
438
439
440setup_params = dict(
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700441 name="fonttools",
Rod S0b59a542022-03-25 12:41:45 -0700442 version="4.31.2",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700443 description="Tools to manipulate font files",
444 author="Just van Rossum",
445 author_email="just@letterror.com",
446 maintainer="Behdad Esfahbod",
447 maintainer_email="behdad@behdad.org",
448 url="http://github.com/fonttools/fonttools",
449 license="MIT",
450 platforms=["Any"],
Rod S0b59a542022-03-25 12:41:45 -0700451 python_requires=">=3.7",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700452 long_description=long_description,
453 package_dir={'': 'Lib'},
454 packages=find_packages("Lib"),
455 include_package_data=True,
456 data_files=find_data_files(),
Haibo Huangd123eeb2020-04-03 12:18:18 -0700457 ext_modules=ext_modules,
458 setup_requires=setup_requires,
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700459 extras_require=extras_require,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700460 entry_points={
461 'console_scripts': [
462 "fonttools = fontTools.__main__:main",
463 "ttx = fontTools.ttx:main",
464 "pyftsubset = fontTools.subset:main",
465 "pyftmerge = fontTools.merge:main",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700466 ]
467 },
Haibo Huangd123eeb2020-04-03 12:18:18 -0700468 cmdclass=cmdclass,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700469 **classifiers
470)
Haibo Huangd123eeb2020-04-03 12:18:18 -0700471
472
473if __name__ == "__main__":
474 setup(**setup_params)